RHEL 9 GPU node kit: NVIDIA driver local repo, Container Toolkit, dkms, k8s device plugin, runbook
95
Everything a RHEL 9 VM needs to become a GPU node for containerised model inference (OCP / CRI-O, Docker, Podman), baked into one UBI 9 image so it can be carried into an air-gapped site and copied out onto the host:
Carried in /opt/nvidia-gpu-toolkit | What / why |
|---|---|
rpms/driver/nvidia-driver-local-repo-rhel9-615.71.09-1.0-1.x86_64.rpm | NVIDIA driver local repository rpm (the driver guide's air-gap path, 645 MB): nvidia-open (open kernel modules, DKMS), nvidia-driver-cuda (= nvidia-smi, the "nvidia-utils" side), libcuda, libnvidia-ml, persistenced, modprobe, fabricmanager … 40 rpms, signed by NVIDIA's installer key D5BCCB90 (ships inside; fingerprint pinned and rpm -K-verified at build) |
rpms/driver/cuda-rhel9.repo, D42D0685.pub, D5BCCB90.pub | network repo + repo key for the documented online path; the installer key for reference |
rpms/epel/dkms-*.rpm, epel-release-latest-9.noarch.rpm | kmod-nvidia-open-dkms needs dkms >= 3.1.8, which only EPEL ships |
rpms/container-toolkit/*.rpm, .repo, gpgkey | NVIDIA Container Toolkit 1.20.0-1 (four rpms) |
k8s/nvidia-device-plugin.yml | NVIDIA Kubernetes device plugin v0.20.0 static DaemonSet (advertises nvidia.com/gpu) |
k8s/gpu-check-pod.yaml | pod that requests one GPU and runs nvidia-smi from this image |
install.sh | host runbook — every documented command, echoed and run in order |
Installed in the image: nvidia-ctk, nvidia-container-cli, lspci.
Not installed in the image: the driver. The container toolkit injects the
host's nvidia-smi / libcuda into containers at run time, so a missing
nvidia-smi inside a container always means the host side needs fixing.
n8500x/nvidia-gpu-toolkit (~1 GB, driver rpm included)615.71.09, toolkit 1.20.0-1, device plugin v0.20.0
(build args / env NVIDIA_DRIVER_VERSION, NVIDIA_CONTAINER_TOOLKIT_VERSION,
K8S_DEVICE_PLUGIN_VERSION)docker save n8500x/nvidia-gpu-toolkit | gzip > ngt.tgz → docker load, or
push it to the internal registry (the k8s manifests also need
nvcr.io/nvidia/k8s-device-plugin:v0.20.0 mirrored there).docker run --rm -v "$PWD/ngt:/target" n8500x/nvidia-gpu-toolkit packages # (podman works too)
scp -r ngt rhel9-vm: # or shared storage
cd ngt
sudo ./install.sh --runtime crio # OCP / MicroShift / CRI-O node
sudo ./install.sh --runtime docker --verify # Docker host, ends with the documented nvidia-smi sample workload
sudo ./install.sh --runtime podman --verify # Podman via CDI
sudo reboot # first driver install: lets the nvidia module replace nouveau
Offline mode is the default when rpms/ is present. What it runs, in order
(each command is printed before it executes; --dry-run prints only):
nvidia-smi already works, force with --driver, skip with --no-driver):
hostnamectl, uname -r, then the prerequisites
dnf install kernel-devel-$(uname -r) kernel-headers-$(uname -r) gcc gcc-c++ make elfutils-libelf-devel patch file kmod cpio ocl-icd opencl-filesystem libvdpau,
the shipped dkms rpm, the local repository installation
dnf install ./nvidia-driver-local-repo-rhel9-615.71.09-1.0-1.x86_64.rpm,
dnf clean expire-cache, the driver installation dnf install nvidia-open,
then post-install systemctl enable/restart nvidia-persistenced,
cat /proc/driver/nvidia/version, nvidia-smi.dnf install of the four shipped rpms
with --disablerepo='*', the documented repo file dropped in (disabled) for
later online upgrades, nvidia-ctk --version.--verify.The one thing that cannot be pre-baked: the DKMS kernel module is built on the VM against its running kernel, so
kernel-devel/kernel-headers(exactuname -rmatch),gcc,make, dkms' own deps (elfutils-libelf-devel,patch,file,kmod,cpio) and the driver's library deps (ocl-icd,opencl-filesystem,libvdpau, and GTK3/X11 libs pulled bynvidia-settings, which the documentednvidia-openmetapackage requires) must come from the VM's own RHEL BaseOS/AppStream — Satellite, a mirrored repo, or the RHEL ISO mounted as a repo (dnf config-manager --add-repo). Everything NVIDIA/EPEL-side is in the kit. Headless alternative that avoids the GTK pull (not the doc's command):dnf install nvidia-driver-cuda kmod-nvidia-open-dkms.
install.sh option | Runs |
|---|---|
--runtime docker (default) | sudo nvidia-ctk runtime configure --runtime=docker + sudo systemctl restart docker |
--runtime docker --rootless | nvidia-ctk runtime configure --runtime=docker --config=$HOME/.config/docker/daemon.json, systemctl --user restart docker, sudo nvidia-ctk config --set nvidia-container-cli.no-cgroups --in-place |
--runtime containerd | sudo nvidia-ctk runtime configure --runtime=containerd + sudo systemctl restart containerd (Kubernetes; nerdctl needs nothing) |
--runtime crio | sudo nvidia-ctk runtime configure --runtime=crio --set-as-default + sudo systemctl restart crio (default runtime = what the device plugin expects) |
--runtime podman | sudo nvidia-ctk cdi generate --output=/etc/cdi/nvidia.yaml + nvidia-ctk cdi list |
--runtime none | packages only |
--verify | docker: sudo docker run --rm --runtime=nvidia --gpus all <image> nvidia-smi; podman: --device=nvidia.com/gpu=all. <image> defaults to this image because air-gapped hosts have no ubuntu; --verify-image ubuntu gives the doc's literal command |
--online | the documented network paths instead of the shipped files (below) |
--nvlink-disable | NVSwitch board, VM has the GPUs but not the switches: options nvidia NVreg_NvLinkDisable=1 → /etc/modprobe.d/nvidia-nvlink.conf, dracut -f, reboot (see NVSwitch boards) |
--fabric-manager | VM owns all GPUs and all NVSwitches: dnf install nvidia-fabricmanager libnvidia-nscq (same version as the driver, from the kit's local repo) + systemctl enable --now nvidia-fabricmanager, then prints nvidia-smi -q Fabric state |
--no-prereqs, --no-toolkit, --no-restart, --dry-run | skip the kernel-devel/gcc/libs step (already installed) / the toolkit phase / the systemctl restart; print only |
env NVIDIA_DRIVER_VERSION, NVIDIA_CONTAINER_TOOLKIT_VERSION | versions to install (defaults = what is shipped) |
env DNF_OPTS, CURL_OPTS | proxy / TLS-interception knobs, e.g. --setopt=sslverify=false, -k |
See the full plan without touching a host:
docker run --rm n8500x/nvidia-gpu-toolkit install [--runtime podman ...].
--online runs)Driver (guide, RHEL 9):
sudo dnf install kernel-devel-$(uname -r) kernel-headers-$(uname -r) gcc gcc-c++ make
sudo dnf install https://dl.fedoraproject.org/pub/epel/epel-release-latest-9.noarch.rpm
sudo dnf config-manager --add-repo https://developer.download.nvidia.com/compute/cuda/repos/rhel9/x86_64/cuda-rhel9.repo
sudo dnf clean expire-cache
sudo dnf module enable nvidia-driver:open-dkms
sudo dnf install nvidia-open
sudo systemctl restart nvidia-persistenced
cat /proc/driver/nvidia/version
Container toolkit (install guide) and sample workload:
sudo dnf install -y curl
curl -s -L https://nvidia.github.io/libnvidia-container/stable/rpm/nvidia-container-toolkit.repo | \
sudo tee /etc/yum.repos.d/nvidia-container-toolkit.repo
export NVIDIA_CONTAINER_TOOLKIT_VERSION=1.20.0-1
sudo dnf install -y \
nvidia-container-toolkit-${NVIDIA_CONTAINER_TOOLKIT_VERSION} \
nvidia-container-toolkit-base-${NVIDIA_CONTAINER_TOOLKIT_VERSION} \
libnvidia-container-tools-${NVIDIA_CONTAINER_TOOLKIT_VERSION} \
libnvidia-container1-${NVIDIA_CONTAINER_TOOLKIT_VERSION}
sudo nvidia-ctk runtime configure --runtime=docker
sudo systemctl restart docker
sudo docker run --rm --runtime=nvidia --gpus all ubuntu nvidia-smi
The image build runs the same toolkit steps (as root, so without sudo); the
only deviation is --allowerasing on the curl install because UBI ships
curl-minimal, which conflicts with the full curl package.
On an HGX baseboard the eight SXM GPUs are wired to four NVSwitch chips.
The driver will not let CUDA initialise on such a GPU until the NVLink fabric
has been configured by Fabric Manager, and Fabric Manager can only do that
where the switches are. The switches are separate PCIe devices
(Bridge [0680], id 10de:22a3 on H100/H200), so a VM that was given only the
GPUs is stuck:
| Symptom | Meaning |
|---|---|
CUDA apps fail at startup with error 802 system not yet initialized | fabric never configured |
nvidia-smi -q → Fabric ... State: In Progress forever | same |
lspci -nn -d 10de: shows only 3D controller [0302], no Bridge [0680] | the NVSwitches are not in this VM |
Fabric Manager journal: NV_WARNING_NOTHING_TO_DO | it started and found zero NVSwitches to manage |
Fabric Manager journal: No CX Bridge devices detected! | normal on A100/H100/H200 — the prelaunch script looks for the ConnectX-7 bridge that only B200/B300 boards use to reach their switches |
gpu-check (the image's default command) reports the Fabric state and the
NvLinkDisable module setting in its NVLink fabric section.
Three ways out. The first two are hypervisor-side decisions; the kit covers the guest side of each:
| Model | Who holds the switches | Guest side | NVLink |
|---|---|---|---|
| Shared NVSwitch (NVIDIA's recommended) | host / service VM runs Fabric Manager with FABRIC_MODE=1 and activates a partition for the VM's GPUs | nothing extra — plain install.sh | full, for 1/2/4/8-GPU VMs |
| Full passthrough | this VM gets all 8 GPUs and all 4 switches | install.sh --fabric-manager | full |
| NVLink disabled | switches stay wherever they are | install.sh --nvlink-disable + reboot | none — GPU↔GPU over PCIe (64 GB/s/dir vs 900 GB/s NVLink; often no P2P in a VM). Fine for one-model-per-GPU or as a stopgap; slow for tensor-parallel across GPUs |
What --nvlink-disable does on the VM, and how to undo it:
echo "options nvidia NVreg_NvLinkDisable=1" | sudo tee /etc/modprobe.d/nvidia-nvlink.conf
sudo dracut -f --kver "$(uname -r)" && sudo reboot
grep NvLinkDisable /proc/driver/nvidia/params # NvLinkDisable: 1
# undo: sudo rm /etc/modprobe.d/nvidia-nvlink.conf && sudo dracut -f && sudo reboot
What --fabric-manager does (full passthrough only; versions must match the driver):
sudo dnf install -y nvidia-fabricmanager-615.71.09 libnvidia-nscq-615.71.09 # from the kit's local repo
sudo systemctl enable --now nvidia-fabricmanager
nvidia-smi -q | grep -A3 -E '^ *Fabric$' # State: Completed / Status: Success
sudo journalctl -u nvidia-fabricmanager -n 30
References: Fabric Manager user guide, Shared NVSwitch GPU passthrough integration guide.
After install.sh --runtime crio (nvidia is CRI-O's default runtime) on the
node, from a machine with cluster access:
oc apply -f k8s/nvidia-device-plugin.yml # DaemonSet; needs image nvcr.io/nvidia/k8s-device-plugin:v0.20.0 (mirror it)
oc describe node <gpu-node> | grep nvidia.com/gpu # capacity/allocatable shows the GPU
oc apply -f k8s/gpu-check-pod.yaml && oc logs -f gpu-check # nvidia-smi from inside a pod
The device plugin DaemonSet runs privileged; on OCP grant its service account
the privileged SCC (or deploy it in a namespace that allows it). Inference
workloads then just request resources.limits: {nvidia.com/gpu: 1}.
For a full OpenShift cluster with RHCOS workers the supported route is the NVIDIA GPU Operator (driver, toolkit and device plugin as operator-managed containers); this kit is for RHEL 9 hosts you manage yourself (MicroShift, CRI-O/containerd nodes, Docker or Podman hosts).
docker run --rm --runtime=nvidia --gpus all n8500x/nvidia-gpu-toolkit
podman run --rm --security-opt=label=disable --device=nvidia.com/gpu=all n8500x/nvidia-gpu-toolkit
Prints PCI devices (lspci, works before any driver), injected /dev/nvidia*,
nvidia-smi (driver + CUDA per GPU), the NVLink fabric state / NvLinkDisable
setting, nvidia-container-cli info (needs
--privileged) and CDI specs, then RESULT: GPU reachable / GPU NOT reachable
(exit 1) with the reason. Anything that is not a subcommand is exec'd:
docker run --rm --gpus all n8500x/nvidia-gpu-toolkit nvidia-smi
docker run --rm n8500x/nvidia-gpu-toolkit nvidia-ctk --version
docker run --rm n8500x/nvidia-gpu-toolkit readme
| Subcommand | Purpose |
|---|---|
check (default) | GPU diagnostics, exit 1 if no GPU is reachable |
packages | copy rpms/, k8s/, install.sh, README into /target |
install [opts] | print the runbook and a dry-run of install.sh with those options |
readme | this file |
The container runs as root (its jobs — copying into volumes, CDI generation,
nvidia-container-cli — need it; nothing listens on a port).
NVIDIA_VISIBLE_DEVICES=all / NVIDIA_DRIVER_CAPABILITIES=utility,compute are
preset so --runtime=nvidia alone also injects the GPUs.
.\build.ps1 # tags date+latest (downloads ~650 MB driver rpm once; cached after)
.\build.ps1 -Push # also push both tags, records .push_tag
docker build --build-arg NVIDIA_DRIVER_VERSION=615.71.09 --build-arg NVIDIA_CONTAINER_TOOLKIT_VERSION=1.20.0-1 .
Sources: nvidia.github.io/libnvidia-container (toolkit, repo_gpgcheck=1),
developer.download.nvidia.com (driver local-repo rpm, verified in a build
stage: the bundled installer key's fingerprint must equal the pinned
NVIDIA_DRIVER_KEY_FPR, then rpm -K on the rpm and its contents), EPEL
(dkms), GitHub (device plugin manifest), UBI 9 repos. On a host with a TLS-intercepting proxy or AV, build.ps1
auto-detects the Avast root CA and passes DNF_OPTS=--setopt=sslverify=false CURL_OPTS=-k
for the build only; the GPG/signature checks above still hold and no CA or
insecure setting is baked into the image.
To bump: pick a driver from
developer.download.nvidia.com/compute/nvidia-driver
(must have a local_installers/nvidia-driver-local-repo-rhel9-<ver>-1.0-1.x86_64.rpm),
a toolkit from the releases,
and rebuild with the build args.
Content type
Image
Digest
sha256:5e28f21fe…
Size
731.4 MB
Last updated
6 days ago
docker pull n8500x/nvidia-gpu-toolkit