Friday, July 11, 2025

NVIDIA GPU Direct Storage on OpenShift

GPU Direct Storage enables a direct data path for direct memory access (DMA) transfers between GPU memory and storage, which avoids a bounce buffer through the CPU. Using this direct path can relieve system bandwidth bottlenecks and decrease the latency and utilization load on the CPU. GPU Direct Storage can be used with NVMe or even NFS on a Netapp filer, the latter which this blog will cover.

Workflow

This blog is laid out with the follow sections all which build on top of one another to get the goal of successful GPU Direct Storage over NFS.

  • Assumptions
  • Considerations
  • Architecture
  • SRIOV Operator Configuration
  • Netapp VServer Setup
  • Netapp Trident CSI Operator Configuration
  • NVIDIA Network Operator Configuration
  • NVIDIA GPU Operator Configuration
  • GDS Cuda Workload Container

Assumptions

This document assumes that we have already deployed a OpenShift Cluster and have installed the necessary operators required for GPU Direct Storage. Those operators would be Node Feature Discover which should also be configured along with the base installation of the NVIDIA Network Operator (no NicClusterPolicy yet) and the NVIDIA GPU Operator (no GpuClusterPolicy yet), SRIOV Operator (no SRIOV policies or instances) and the Trident CSI Operator (No orchestrators or backends configured yet).

Considerations

If any of the nvme devices in the system participate in either the operating system or other services (machine configs for LVMs or other customized access) the nvme kernel modules will not be able to unload properly even with the workaround defined in this documentation. Any use of GDS requires that the nvme drives are not in use during the deployment of the Network Operator in order for the Network Operator to be able to unload in-tree drivers and then load NVIDIA's out of tree drivers in place.

Architecture

Below is a diagram of how the environment was architected from a networking perspective.

SRIOV Operator Configuration

For GPU Direct Storage over NFS to make performance sense we will need to use SRIOV here. So we first need to configure the SRIOV Operator assuming the SRIOV Operator is installed. The first step is to generate a basic SriovOperatorConfig custom resource file.

$ cat <<EOF > sriov-operator-config.yaml apiVersion: sriovnetwork.openshift.io/v1 kind: SriovOperatorConfig metadata: name: default namespace: openshift-sriov-network-operator spec: enableInjector: true enableOperatorWebhook: true logLevel: 2 EOF

Next we create the SriovOperatorConfig on the cluster.

$ oc create -f sriov-operator-config.yaml sriovoperatorconfig.sriovnetwork.openshift.io/default created

Now one key step here is to patch the SriovOperatorConfig so that it is aware of the NVIDIA Network Operator.

$ oc patch sriovoperatorconfig default --type=merge -n openshift-sriov-network-operator --patch '{ "spec": { "configDaemonNodeSelector": { "network.nvidia.com/operator.mofed.wait": "false", "node-role.kubernetes.io/worker": "", "feature.node.kubernetes.io/pci-15b3.sriov.capable": "true" } } }' sriovoperatorconfig.sriovnetwork.openshift.io/default patched

Now we can move onto generating a SriovNetworkNodePolicy which will define the interface that we want to have VFs. In the case of multiple interfaces we would want to create multiple SriovNetworkNodePolicy files. The example below demonstrates how to configure an interface with an MTU of 9000 and generate 8 VFs.

$ cat <<EOF > sriov-network-node-policy.yaml apiVersion: sriovnetwork.openshift.io/v1 kind: SriovNetworkNodePolicy metadata: name: sriov-legacy-policy namespace: openshift-sriov-network-operator spec: deviceType: netdevice mtu: 9000 nicSelector: vendor: "15b3" pfNames: ["enp55s0np0#0-7"] nodeSelector: feature.node.kubernetes.io/pci-15b3.present: "true" numVfs: 8 priority: 90 isRdma: true resourceName: sriovlegacy EOF

With the SriovNetworkNodePolicy generated we can create it on the cluster which will cause the worker nodes where it is applied to reboot.

$ oc create -f sriov-network-node-policy.yaml sriovnetworknodepolicy.sriovnetwork.openshift.io/sriov-legacy-policy created

Once the node has rebooted we can optionally open a debug pod on the worker nodes and verify with ip link to confirm the interfaces were created. If we are ready to move forward we can next generate the SriovNetwork for the resource we created in the SriovNetworkNodePolicy. Again if we have multiple SriovNetworkNodePolicy files we will also have multiple SriovNetwork files. These define the network space for the VF interfaces. I should note that these networks need to have access to the Netapp data LIF as well in order for RDMA to function. In my example below I excluded the ipaddresses in range of 102.168.10.100-110 because my Netapp data LIF will have ipaddresss in that space.

$ cat <<EOF > sriov-network.yaml apiVersion: sriovnetwork.openshift.io/v1 kind: SriovNetwork metadata: name: sriov-network namespace: openshift-sriov-network-operator spec: vlan: 0 networkNamespace: "default" resourceName: "sriovlegacy" ipam: | { "type": "whereabouts", "range": "192.168.10.0/24", "exclude": [ "192.168.10.100/30", "192.168.10.110/32" ] } EOF

Now we can create the SriovNetwork custom resource on the cluster.

$ oc create -f sriov-network.yaml sriovnetwork.sriovnetwork.openshift.io/sriov-network created

At this point we have configured everything we need for SRIOV and can move onto the next section of the documentation.

Netapp VServer Setup

This section is really just to cover a few items of importance from the Netapp vserver perspective. This does not aim to be a comprehensive guide on how to setup a Netapp MetroCluster or the vservers within them. First in our example environment we had a vserver created and that vserver as two logical interfaces: management and data. With the management interface we can access the vserver and look at a few things. Depending on the environment this may or may not be accessible for the OpenShift administrator. In my case the storage team gave me access. To get on the vserver we can ssh to the vserver ipaddress or fqdn if it exists in DNS.

$ ssh trident@10.6.136.110 (trident@10.6.136.110) Password: Last login time: 5/7/2025 19:31:11

Once we are logged in I want to confirm that NFS 4 is enabled along with RDMA by using vserver nfs show.

ntap-rdu3-nv01-nvidia::> vserver nfs show Vserver: ntap-rdu3-nv01-nvidia General Access: true v3: enabled v4.0: enabled 4.1: enabled UDP: enabled TCP: enabled RDMA: enabled Default Windows User: - Default Windows Group: -

The above output looks good for my needs when doing GPU Direct Storage. Another item we can check is the export-policies with vserver export-policy show.

ntap-rdu3-nv01-nvidia::> vserver export-policy show Vserver Policy Name --------------- ------------------- ntap-rdu3-nv01-nvidia default ntap-rdu3-nv01-nvidia trident-8d6b2406-551a-416b-bcce-22626ed60242 2 entries were displayed.

And finally I wanted to confirm that my data interfaces connected to the NVIDIA high speed switch were indeed operating with jumbo frames. I can see that with the network port show command. Because this is a MetroCluster pair setup we can see the interfaces on both nodes is set appropriately.

ntap-rdu3-nv01-nvidia::> network port show Node: ntap-rdu3-nv01-a Speed(Mbps) Health Port Broadcast Domain Link MTU Admin/Oper Status --------- ------------ ---------------- ---- ---- ----------- -------- e0M Management up 1500 auto/1000 healthy e1b - down 1500 auto/- - e2a nvidia up 9000 auto/200000 healthy e2b - up 1500 auto/100000 healthy e2b-710 nfs up 1500 -/- healthy e6a - down 1500 auto/- - e6b - down 1500 auto/- - e7b - down 1500 auto/- - e8a - down 1500 auto/- - e8b - down 1500 auto/- - Node: ntap-rdu3-nv01-b Speed(Mbps) Health Port Broadcast Domain Link MTU Admin/Oper Status --------- ------------ ---------------- ---- ---- ----------- -------- e0M Management up 1500 auto/1000 healthy e1b - down 1500 auto/- - e2a nvidia up 9000 auto/200000 healthy e2b - up 1500 auto/100000 healthy e2b-710 nfs up 1500 -/- healthy e6a - down 1500 auto/- - e6b - down 1500 auto/- - e7b - down 1500 auto/- - e8a - down 1500 auto/- - e8b - down 1500 auto/- - 20 entries were displayed.

At this point we can exit out of the vserver and move onto configuring the Netapp Trident CSI operator.

Netapp Trident CSI Operator Configuration

Trident is an open-source and fully supported storage orchestrator for containers and Kubernetes distributions, including Red Hat OpenShift. Trident works with the entire NetApp storage portfolio, including the NetApp ONTAP and Element storage systems, and it also supports NFS and iSCSI connections. Trident accelerates the DevOps workflow by allowing end users to provision and manage storage from their NetApp storage systems without requiring intervention from a storage administrator.

We have made the assumption that the Trident Operator and the default Trident Orchestrator have already been deployed.  Our next step will be to configure the secret for the Netapp vfiler with the credentials so that Trident knows how which username and password to connect. 

$ cat <<EOF > netapp-phy-secret.yaml apiVersion: v1 kind: Secret metadata: name: netapp-phy-secret namespace: trident type: Opaque stringData: username: vserver-user password: verserv-password
Once we have our custom resource file generated we can create it on the cluster.
$ oc create -f netapp-phy-secret.yaml secret/netapp-phy-secret created
Next we need to configure the TridentBackendConfig so that Trident knows how to communicate with the Netapp from both a management and data perspective.  Note the credentials we created are referenced here.
$ cat <<EOF > netapp-phy-tridentbackendconfig.yaml apiVersion: trident.netapp.io/v1 kind: TridentBackendConfig metadata: name: netapp-phy-nfs-backend namespace: trident spec: version: 1 storageDriverName: ontap-nas-flexgroup managementLIF: 10.6.136.110 dataLIF: 192.168.10.101 backendName: phy-nfs-backend svm: ntap-rdu3-nv01-nvidia autoExportPolicy: true credentials: name: netapp-phy-secret
With the custom resource file generated we can create it on the cluster.
$ oc create -f netapp-phy-tridentbackendconfig.yaml tridentbackendconfig.trident.netapp.io/netapp-phy-nfs-backend created
We can validate the backend is there with the follow check.
$ oc get tridentbackend -n trident NAME BACKEND BACKEND UUID tbe-n59xq phy-nfs-backend 8d6b2406-551a-416b-bcce-22626ed60242
We can also describe the backend as well.
$ oc describe tridentbackend tbe-n59xq -n trident Name: tbe-n59xq Namespace: trident Labels: <none> Annotations: <none> API Version: trident.netapp.io/v1 Backend Name: phy-nfs-backend Backend UUID: 8d6b2406-551a-416b-bcce-22626ed60242 Config: ontap_config: Aggregate: Auto Export CID Rs: 0.0.0.0/0 ::/0 Auto Export Policy: true Backend Name: phy-nfs-backend Backend Pools: eyJzdm1VVUlEIjoiNjE2OTg1YTYtMjlkZi0xMWYwLWI4YzctZDAzOWVhYzA0MDUzIn0= Chap Initiator Secret: Chap Target Initiator Secret: Chap Target Username: Chap Username: Client Certificate: Client Private Key: Clone Split Delay: 10 Credentials: Name: netapp-phy-secret Data LIF: 192.168.10.101 Debug: false Debug Trace Flags: <nil> Defaults: LUKS Encryption: false Adaptive Qos Policy: Encryption: Export Policy: <automatic> File System Type: ext4 Format Options: Mirroring: false Name Template: Qos Policy: Security Style: unix Size: 1G Skip Recovery Queue: false Snapshot Dir: false Snapshot Policy: none Snapshot Reserve: Space Allocation: true Space Reserve: none Split On Clone: false Tiering Policy: Unix Permissions: ---rwxrwxrwx Deny New Volume Pools: false Disable Delete: false Empty Flexvol Deferred Delete Period: Flags: Disaggregated: false Personality: Unified San Optimized: false Flexgroup Aggregate List: Igroup Name: Labels: <nil> Limit Aggregate Usage: Limit Volume Pool Size: Limit Volume Size: Luns Per Flexvol: Management LIF: 10.6.136.110 Nas Type: nfs Nfs Mount Options: Password: secret:netapp-phy-secret Qtree Prune Flexvols Period: Qtree Quota Resize Period: Qtrees Per Flexvol: Region: Replication Policy: Replication Schedule: San Type: iscsi Smb Share: Storage: <nil> Storage Driver Name: ontap-nas-flexgroup Storage Prefix: Supported Topologies: <nil> Svm: ntap-rdu3-nv01-nvidia Trusted CA Certificate: Usage Heartbeat: Use CHAP: false Use REST: <nil> User State: Username: secret:netapp-phy-secret Version: 1 Zone: Config Ref: 9e1ff3f2-8a2d-4efa-859c-712b920d269b Kind: TridentBackend Metadata: Creation Timestamp: 2025-05-07T19:31:56Z Finalizers: trident.netapp.io Generate Name: tbe- Generation: 1 Resource Version: 38713504 UID: 6536970f-b10e-4e04-8a37-8da56deaf69e Online: true State: online User State: normal Version: 1 Events: <none>
We can also use the tridentctl command to validate the backend and confirm its online.
$ ./trident-installer/tridentctl get backend -n trident +-----------------+---------------------+--------------------------------------+--------+------------+---------+ | NAME | STORAGE DRIVER | UUID | STATE | USER-STATE | VOLUMES | +-----------------+---------------------+--------------------------------------+--------+------------+---------+ | phy-nfs-backend | ontap-nas-flexgroup | 8d6b2406-551a-416b-bcce-22626ed60242 | online | normal | 0 | +-----------------+---------------------+--------------------------------------+--------+------------+---------+
With the Trident backend configured we can move onto generating a storageclass resource file.  Note while this looks just like a standard Trident NFS storageclass the designation of the rdma makes it special.
$ cat <<EOF > netapp-phy-rdma-storageclass.yaml apiVersion: storage.k8s.io/v1 kind: StorageClass metadata: name: netapp-phy-nfs provisioner: csi.trident.netapp.io parameters: backendType: "ontap-nas-flexgroup" mountOptions: - vers=4.1 - proto=rdma - max_connect=16 - rsize=262144 - wsize=262144 - write=eager EOF
Once we have generated the custom resource file we can create it on the cluster.
$ oc create -f netapp-phy-rdma-storageclass.yaml storageclass.storage.k8s.io/netapp-phy-nfs created
We can validate the storageclass by looking at the storage classes available.
$ oc get sc NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSION AGE netapp-phy-nfs csi.trident.netapp.io Delete Immediate false 4s
Now with the storagclass configured we can generate a persistent volume resource file.
$ cat <<EOF > netapp-phy-pvc.yaml kind: PersistentVolumeClaim apiVersion: v1 metadata: name: pvc-netapp-phy-test spec: accessModes: - ReadWriteOnce resources: requests: storage: 850Gi storageClassName: netapp-phy-nfs EOF
We can take the persistent volume resource and create it on the cluster.
$ oc create -f netapp-phy-pvc.yaml persistentvolumeclaim/pvc-netapp-phy-test created
We can validate the persistent volume by looking at the pvc.
$ oc get pvc NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS VOLUMEATTRIBUTESCLASS AGE pvc-netapp-phy-test Bound pvc-ae477c5c-cf10-4bc0-bb71-39d214a237f0 850Gi RWO netapp-phy-nfs <unset> 45s

At this point we have completed the setup of the Trident storage side in preparation for GPU Direct Storage.

NVIDIA Network Operator Configuration

We assume the Network Operator has already been installed on the cluster but the NicClusterPolicy still needs to be created. The following NicClusterPolicy example will provide the needed configuration to ensure RDMA is properly loaded for NFS. The key option in this policy is the ENABLE_NFSRDMA variable and having it set to true. I want to note that this policy also optinonally has an rdmaSharedDevice and ENTRYPOINT_DEBUG set to true for more verbose logging.

$ cat <<EOF > network-sriovleg-nic-cluster-policy.yaml apiVersion: mellanox.com/v1alpha1 kind: NicClusterPolicy metadata: name: nic-cluster-policy spec: ofedDriver: image: doca-driver repository: nvcr.io/nvidia/mellanox version: 25.01-0.6.0.0-0 startupProbe: initialDelaySeconds: 10 periodSeconds: 20 livenessProbe: initialDelaySeconds: 30 periodSeconds: 30 readinessProbe: initialDelaySeconds: 10 periodSeconds: 30 env: - name: UNLOAD_STORAGE_MODULES value: "true" - name: RESTORE_DRIVER_ON_POD_TERMINATION value: "true" - name: CREATE_IFNAMES_UDEV value: "true" - name: ENABLE_NFSRDMA value: "true" - name: ENTRYPOINT_DEBUG value: 'true' EOF

Before creating the NicClusterPolicy on the cluster we need to prepare a script which will allow us to workaround an issue with GPU Direct Storage in the NVIDIA Network Operator. This script when run right after creating the NicClusterPolicy will determine which nodes have mofed pods running on them and based on that node list will ssh as the core user into each node and unload the following modules: nvme, nvme_tcp, nvme_fabrics, nvme_core. By using the script to unload the modules while the mofed container is busying building the doca drivers we eliminate an issue where when the mofed container goes to install the compiled doca drivers there is a failure to load. One might ask what does NVMe have to do with NFS and unfortunately GPU Direct Storage enablement does both so we have to work around this issue.

$ cat <<EOF > nvme-fixer.sh #!/bin/bash ### Set array of modules to be unloaded declare -a modarr=("nvme" "nvme_tcp" "nvme_fabrics" "nvme_core") ### Determine which hosts have mofed container running on them declare -a hostarr=(`oc get pods -n nvidia-network-operator -o custom-columns=POD:.metadata.name,NODE:.spec..nodeName --no-headers|grep mofed|awk {'print $2'}`) ### Iterate through modules on each host and unload them for host in "${hostarr[@]}" do echo "Unloading nvme dependencies on $host..." for module in "${modarr[@]}" do echo "Unloading module $module..." ssh core@$host sudo rmmod $module done done EOF

Change the execute bit on the file.

$ chmod +x nvme-fixer.sh

Now we are ready to create the NicClusterPolicy on the cluster and follow it up by running the nvme-fixer.sh script. If there are any rmmod errors those can safely be ignored as the module was not loaded to start with. In the example below we had two workers nodes that had mofed pods running on them so the script went ahead and unloaded the nvme modules.

$ oc create -f network-sharedrdma-nic-cluster-policy.yaml nicclusterpolicy.mellanox.com/nic-cluster-policy created $ ./nvme-fixer.sh Unloading nvme dependencies on nvd-srv-30.nvidia.eng.rdu2.dc.redhat.com... Unloading module nvme... Unloading module nvme_tcp... rmmod: ERROR: Module nvme_tcp is not currently loaded Unloading module nvme_fabrics... rmmod: ERROR: Module nvme_fabrics is not currently loaded Unloading module nvme_core... Unloading nvme dependencies on nvd-srv-29.nvidia.eng.rdu2.dc.redhat.com... Unloading module nvme... Unloading module nvme_tcp... Unloading module nvme_fabrics... Unloading module nvme_core...

Now we wait for the mofed pod to finish compiling and installed the GPU Direct Storage modules. We will know its complete when the pods are in a running state like below:

$ oc get pods -n nvidia-network-operator NAME READY STATUS RESTARTS AGE mofed-rhcos4.16-56c9d799bf-ds-bvhmj 2/2 Running 0 20h mofed-rhcos4.16-56c9d799bf-ds-jdzxj 2/2 Running 0 20h nvidia-network-operator-controller-manager-85b78c49f6-9lchx 1/1 Running 4 (3h26m ago) 3d14h

This completes the NVIDIA Network Operator portion of the configuration for GPU Direct Storage.

NVIDIA GPU Operator Configuration

Now that the NicClusterPolicy is defined and the proper nvme modules have been loaded we can move onto configuring our GPU ClusterPolicy. The below example is a policy that will enable GPU Direct Storage on the worker nodes that have a proper NVIDIA GPU.

$ cat <<EOF > gpu-cluster-policy.yaml apiVersion: nvidia.com/v1 kind: ClusterPolicy metadata: name: gpu-cluster-policy spec: vgpuDeviceManager: config: default: default enabled: true migManager: config: default: all-disabled name: default-mig-parted-config enabled: true operator: defaultRuntime: crio initContainer: {} runtimeClass: nvidia use_ocp_driver_toolkit: true dcgm: enabled: true gfd: enabled: true dcgmExporter: config: name: '' enabled: true serviceMonitor: enabled: true cdi: default: false enabled: false driver: licensingConfig: configMapName: '' nlsEnabled: true enabled: true kernelModuleType: open certConfig: name: '' useNvidiaDriverCRD: false kernelModuleConfig: name: '' upgradePolicy: autoUpgrade: true drain: deleteEmptyDir: false enable: false force: false timeoutSeconds: 300 maxParallelUpgrades: 1 maxUnavailable: 25% podDeletion: deleteEmptyDir: false force: false timeoutSeconds: 300 waitForCompletion: timeoutSeconds: 0 repoConfig: configMapName: '' virtualTopology: config: '' devicePlugin: config: default: '' name: '' enabled: true mps: root: /run/nvidia/mps gdrcopy: enabled: true kataManager: config: artifactsDir: /opt/nvidia-gpu-operator/artifacts/runtimeclasses mig: strategy: single sandboxDevicePlugin: enabled: true validator: plugin: env: - name: WITH_WORKLOAD value: 'false' nodeStatusExporter: enabled: true daemonsets: rollingUpdate: maxUnavailable: '1' updateStrategy: RollingUpdate sandboxWorkloads: defaultWorkload: container enabled: false gds: enabled: true image: nvidia-fs repository: nvcr.io/nvidia/cloud-native version: 2.25.7 vgpuManager: enabled: false vfioManager: enabled: true toolkit: enabled: true installDir: /usr/local/nvidia EOF

Now let's create the policy on the cluster.

$ oc create -f gpu-cluster-policy.yaml clusterpolicy.nvidia.com/gpu-cluster-policy created

Once the policy is created let's validate the pods are running before we move onto the next step.

$ oc get pods -n nvidia-gpu-operator NAME READY STATUS RESTARTS AGE gpu-feature-discovery-nttht 1/1 Running 0 20h gpu-feature-discovery-r4ktv 1/1 Running 0 20h gpu-operator-7d7f694bfb-957mv 1/1 Running 0 20h nvidia-container-toolkit-daemonset-h96t6 1/1 Running 0 20h nvidia-container-toolkit-daemonset-hqtrl 1/1 Running 0 20h nvidia-cuda-validator-66ml7 0/1 Completed 0 20h nvidia-dcgm-exporter-hbk4r 1/1 Running 0 20h nvidia-dcgm-exporter-pgh4q 1/1 Running 0 20h nvidia-dcgm-nttds 1/1 Running 0 20h nvidia-dcgm-zb4fl 1/1 Running 0 20h nvidia-device-plugin-daemonset-d99md 1/1 Running 0 20h nvidia-device-plugin-daemonset-w7tc4 1/1 Running 0 20h nvidia-driver-daemonset-416.94.202504151456-0-8bdl5 4/4 Running 26 (20h ago) 2d2h nvidia-driver-daemonset-416.94.202504151456-0-j8gps 4/4 Running 20 (20h ago) 2d2h nvidia-node-status-exporter-b22hk 1/1 Running 4 2d2h nvidia-node-status-exporter-lwqhb 1/1 Running 3 2d2h nvidia-operator-validator-cvqn5 1/1 Running 0 20h nvidia-operator-validator-zxrpb 1/1 Running 0 20h

With the NVIDIA GPU Operator pods running we can rsh into the daemonset pods and confirm GDS is enabled by running the lsmod command and cat out the /proc/driver/nvidia-fs/stats file.

$ oc rsh -n nvidia-gpu-operator nvidia-driver-daemonset-416.94.202504151456-0-8bdl5 sh-4.4# lsmod|grep nvidia nvidia_fs 327680 0 nvidia_modeset 1720320 0 video 73728 1 nvidia_modeset nvidia_uvm 4087808 12 nvidia 11665408 36 nvidia_uvm,nvidia_fs,gdrdrv,nvidia_modeset drm 741376 5 drm_kms_helper,drm_shmem_helper,nvidia,mgag200 sh-4.4# cat /proc/driver/nvidia-fs/stats GDS Version: 1.10.0.4 NVFS statistics(ver: 4.0) NVFS Driver(version: 2.20.5) Mellanox PeerDirect Supported: False IO stats: Disabled, peer IO stats: Disabled Logging level: info Active Shadow-Buffer (MiB): 0 Active Process: 0 Reads : err=0 io_state_err=0 Sparse Reads : n=0 io=0 holes=0 pages=0 Writes : err=0 io_state_err=0 pg-cache=0 pg-cache-fail=0 pg-cache-eio=0 Mmap : n=0 ok=0 err=0 munmap=0 Bar1-map : n=0 ok=0 err=0 free=0 callbacks=0 active=0 delay-frees=0 Error : cpu-gpu-pages=0 sg-ext=0 dma-map=0 dma-ref=0 Ops : Read=0 Write=0 BatchIO=0

If everything looks good we can move onto an additional step to confirm GDS is ready for workload consumption.

GDS Cuda Workload Container

Once the GPU Direct Storage drivers are loaded we can use one more additional tool to check and confirm GDS capability. This involves building a container that contains the CUDA packages and then running it on a node.

Now let's generate a service account CRD to use in the default namespace.

$ cat <<EOF > nvidiatools-serviceaccount.yaml apiVersion: v1 kind: ServiceAccount metadata: name: nvidiatools namespace: default EOF

Next we can create it on our cluster.

$ oc create -f default-serviceaccount.yaml serviceaccount/rdma created

Finally with the service account create we can add privleges to it.

$ oc -n default adm policy add-scc-to-user privileged -z nvidiatools clusterrole.rbac.authorization.k8s.io/system:openshift:scc:privileged added: "nvidiatools"

With the service account defined and our pod yaml ready we can create it on the cluster.

The following pod yaml defines this configuration.

$ cat <<EOF > nvidiatools-30-workload.yaml apiVersion: v1 kind: Pod metadata: name: nvidiatools-30-workload namespace: default annotations: # JSON list is the canonical form; adjust if your NAD lives in another namespace k8s.v1.cni.cncf.io/networks: '[{ "name": "sriov-network" }]' spec: serviceAccountName: nvidiatools nodeSelector: kubernetes.io/hostname: nvd-srv-30.nvidia.eng.rdu2.dc.redhat.com volumes: - name: rdma-pv-storage persistentVolumeClaim: claimName: pvc-netapp-phy-test - name: nordma-pv-storage persistentVolumeClaim: claimName: pvc-netapp-phy-nordma-test containers: - name: nvidiatools-30-workload image: quay.io/redhat_emp1/ecosys-nvidia/nvidia-tools:0.0.3 imagePullPolicy: IfNotPresent securityContext: privileged: true capabilities: add: ["IPC_LOCK"] resources: limits: nvidia.com/gpu: 1 openshift.io/sriovlegacy: 1 requests: nvidia.com/gpu: 1 openshift.io/sriovlegacy: 1 volumeMounts: - name: rdma-pv-storage mountPath: /nfsfast - name: nordma-pv-storage mountPath: /nfsslow EOF
$ oc create -f nvidiatools-30-workload.yaml nvidiatools-30-workload created $ oc get pods NAME READY STATUS RESTARTS AGE nvidiatools-30-workload 1/1 Running 0 3s

Once the pod is up and running we can rsh into the pod and run the gdscheck tool to confirm capabilities and configuration of GPU Direct Storage.

$ oc rsh nvidiatools-30-workload sh-5.1# /usr/local/cuda/gds/tools/gdscheck -p GDS release version: 1.13.1.3 nvidia_fs version: 2.20 libcufile version: 2.12 Platform: x86_64 ============ ENVIRONMENT: ============ ===================== DRIVER CONFIGURATION: ===================== NVMe P2PDMA : Unsupported NVMe : Supported NVMeOF : Supported SCSI : Unsupported ScaleFlux CSD : Unsupported NVMesh : Unsupported DDN EXAScaler : Unsupported IBM Spectrum Scale : Unsupported NFS : Supported BeeGFS : Unsupported WekaFS : Unsupported Userspace RDMA : Unsupported --Mellanox PeerDirect : Disabled --rdma library : Not Loaded (libcufile_rdma.so) --rdma devices : Not configured --rdma_device_status : Up: 0 Down: 0 ===================== CUFILE CONFIGURATION: ===================== properties.use_pci_p2pdma : false properties.use_compat_mode : true properties.force_compat_mode : false properties.gds_rdma_write_support : true properties.use_poll_mode : false properties.poll_mode_max_size_kb : 4 properties.max_batch_io_size : 128 properties.max_batch_io_timeout_msecs : 5 properties.max_direct_io_size_kb : 16384 properties.max_device_cache_size_kb : 131072 properties.max_device_pinned_mem_size_kb : 33554432 properties.posix_pool_slab_size_kb : 4 1024 16384 properties.posix_pool_slab_count : 128 64 64 properties.rdma_peer_affinity_policy : RoundRobin properties.rdma_dynamic_routing : 0 fs.generic.posix_unaligned_writes : false fs.lustre.posix_gds_min_kb: 0 fs.beegfs.posix_gds_min_kb: 0 fs.weka.rdma_write_support: false fs.gpfs.gds_write_support: false fs.gpfs.gds_async_support: true profile.nvtx : false profile.cufile_stats : 0 miscellaneous.api_check_aggressive : false execution.max_io_threads : 4 execution.max_io_queue_depth : 128 execution.parallel_io : true execution.min_io_threshold_size_kb : 8192 execution.max_request_parallelism : 4 properties.force_odirect_mode : false properties.prefer_iouring : false ========= GPU INFO: ========= GPU index 0 NVIDIA L40S bar:1 bar size (MiB):65536 supports GDS, IOMMU State: Disabled ============== PLATFORM INFO: ============== IOMMU: disabled Nvidia Driver Info Status: Supported(Nvidia Open Driver Installed) Cuda Driver Version Installed: 12080 Platform: PowerEdge R760xa, Arch: x86_64(Linux 5.14.0-427.65.1.el9_4.x86_64) Platform verification succeeded

Now let's confirm our GPU Direct NFS mount is mounted. Notice in the output the proto is rdma.

sh-5.1# mount|grep nfs 192.168.10.101:/trident_pvc_ae477c5c_cf10_4bc0_bb71_39d214a237f0 on /mnt type nfs4 (rw,relatime,vers=4.1,rsize=262144,wsize=262144,namlen=255,hard,proto=rdma,max_connect=16,port=20049,timeo=600,retrans=2,sec=sys,clientaddr=192.168.10.30,local_lock=none,write=eager,addr=192.168.10.101)

Next we can use gdsio to run some benchmarks across the GPU Direct NFS mount. Before we run the benchmarks let's familiarize ourselves with the all the gdsio switches and what they mean.

sh-5.1# /usr/local/cuda-12.8/gds/tools/gdsio -h gdsio version :1.12 Usage [using config file]: gdsio rw-sample.gdsio Usage [using cmd line options]:/usr/local/cuda-12.8/gds/tools/gdsio -f <file name> -D <directory name> -d <gpu_index (refer nvidia-smi)> -n <numa node> -m <memory type(0 - (cudaMalloc), 1 - (cuMem), 2 - (cudaMallocHost), 3 - (malloc) 4 - (mmap))> -w <number of threads for a job> -s <file size(K|M|G)> -o <start offset(K|M|G)> -i <io_size(K|M|G)> <min_size:max_size:step_size> -p <enable nvlinks> -b <skip bufregister> -V <verify IO> -x <xfer_type> [0(GPU_DIRECT), 1(CPU_ONLY), 2(CPU_GPU), 3(CPU_ASYNC_GPU), 4(CPU_CACHED_GPU), 5(GPU_DIRECT_ASYNC), 6(GPU_BATCH), 7(GPU_BATCH_STREAM)] -B <batch size> -I <(read) 0|(write)1| (randread) 2| (randwrite) 3> -T <duration in seconds> -k <random_seed> (number e.g. 3456) to be used with random read/write> -U <use unaligned(4K) random offsets> -R <fill io buffer with random data> -F <refill io buffer with random data during each write> -a <alignment size in case of random IO> -M <mixed_rd_wr_percentage in case of regular batch mode> -P <rdma url> -J <per job statistics> xfer_type: 0 - Storage->GPU (GDS) 1 - Storage->CPU 2 - Storage->CPU->GPU 3 - Storage->CPU->GPU_ASYNC 4 - Storage->PAGE_CACHE->CPU->GPU 5 - Storage->GPU_ASYNC 6 - Storage->GPU_BATCH 7 - Storage->GPU_BATCH_STREAM Note: read test (-I 0) with verify option (-V) should be used with files written (-I 1) with -V option read test (-I 2) with verify option (-V) should be used with files written (-I 3) with -V option, using same random seed (-k), same number of threads(-w), offset(-o), and data size(-s) write test (-I 1/3) with verify option (-V) will perform writes followed by read

Before we begin running some tests I want to note that the tests are being run from a standard Dell R760xa and from the nvidia-smi topo output we can see we are dealing with a non optimal setup of NODE where the connection traversing PCIe as well as the interconnect between PCIe Host Bridges within a NUMA node. Ideally for peformant numbers we would want to run this on a H100 or B200 where the GPU and NIC are connected to the same PCIe switch and yield a PHB,PXB or PIX connection.

sh-5.1# nvidia-smi topo -mp GPU0 NIC0 NIC1 NIC2 NIC3 NIC4 NIC5 NIC6 NIC7 NIC8 NIC9 CPU Affinity NUMA Affinity GPU NUMA ID GPU0 X NODE NODE NODE NODE NODE NODE NODE NODE NODE NODE 0,2,4,6,8,10 0 N/A NIC0 NODE X NODE NODE NODE NODE NODE NODE NODE NODE NODE NIC1 NODE NODE X PIX PIX PIX PIX PIX PIX PIX PIX NIC2 NODE NODE PIX X PIX PIX PIX PIX PIX PIX PIX NIC3 NODE NODE PIX PIX X PIX PIX PIX PIX PIX PIX NIC4 NODE NODE PIX PIX PIX X PIX PIX PIX PIX PIX NIC5 NODE NODE PIX PIX PIX PIX X PIX PIX PIX PIX NIC6 NODE NODE PIX PIX PIX PIX PIX X PIX PIX PIX NIC7 NODE NODE PIX PIX PIX PIX PIX PIX X PIX PIX NIC8 NODE NODE PIX PIX PIX PIX PIX PIX PIX X PIX NIC9 NODE NODE PIX PIX PIX PIX PIX PIX PIX PIX X Legend: X = Self SYS = Connection traversing PCIe as well as the SMP interconnect between NUMA nodes (e.g., QPI/UPI) NODE = Connection traversing PCIe as well as the interconnect between PCIe Host Bridges within a NUMA node PHB = Connection traversing PCIe as well as a PCIe Host Bridge (typically the CPU) PXB = Connection traversing multiple PCIe bridges (without traversing the PCIe Host Bridge) PIX = Connection traversing at most a single PCIe bridge NIC Legend: NIC0: mlx5_0 NIC1: mlx5_1 NIC2: mlx5_2 NIC3: mlx5_3 NIC4: mlx5_4 NIC5: mlx5_5 NIC6: mlx5_6 NIC7: mlx5_7 NIC8: mlx5_8 NIC9: mlx5_9

Now let's run a few gdsio tests across our RDMA nfs mount. Please note these runs were not performance tuned in any way.  This is merely a demonstration to show the feature functionality.   

In this first example, gdsio is used to generate a random write load of small IOs (4k) to one of the NFS mount point

sh-5.1# /usr/local/cuda-12.8/gds/tools/gdsio -D /nfsfast -d 0 -w 32 -s 500M -i 4K -x 0 -I 3 -T 120 IoType: RANDWRITE XferType: GPUD Threads: 32 DataSetSize: 43222136/16384000(KiB) IOSize: 4(KiB) Throughput: 0.344940 GiB/sec, Avg_Latency: 352.314946 usecs ops: 10805534 total_time 119.498576 secs

Next we will repeat the same test but for random reads.

sh-5.1# /usr/local/cuda-12.8/gds/tools/gdsio -D /nfsfast -d 0 -w 32 -s 500M -i 4K -x 0 -I 2 -T 120 IoType: RANDREAD XferType: GPUD Threads: 32 DataSetSize: 71313540/16384000(KiB) IOSize: 4(KiB) Throughput: 0.569229 GiB/sec, Avg_Latency: 214.448246 usecs ops: 17828385 total_time 119.477201 secs

Small and random IOs are all about IOPS and latency. For our next test we will determine throughput. We will use larger files sizes and much larger IO sizes.

sh-5.1# /usr/local/cuda-12.8/gds/tools/gdsio -D /nfsfast -d 0 -w 32 -s 1G -i 1M -x 0 -I 1 -T 120 IoType: WRITE XferType: GPUD Threads: 32 DataSetSize: 320301056/33554432(KiB) IOSize: 1024(KiB) Throughput: 2.547637 GiB/sec, Avg_Latency: 12487.658159 usecs ops: 312794 total_time 119.900455 secs

This concludes the workflow of configuring and testing GPU Direct Storage on OpenShift over an RDMA NFS mount.