NVIDIA GPU Operator¶
Run the real GPU Operator against Mokka: the device plugin, GPU Feature Discovery, DCGM and the validator all come up and behave as they would on a node with hardware.
Why it needs an overlay¶
The GPU Operator's job is to install and manage a driver. Mokka has already provided one, so the operator has to be told to stop at the parts that consume a driver rather than install one — and pointed at where Mokka staged it.
That is what the values below do. Installing the operator with its defaults against a Mokka node fails: the driver DaemonSet tries to build a kernel module that cannot exist.
Prerequisites¶
Takes about 15 minutes, most of it pulling operator images.
Step 1 — Create a cluster with CDI enabled¶
The operator resolves GPUs through the Container Device Interface, so containerd needs CDI turned on and the NVIDIA container toolkit present.
kind create cluster --name mokka-operator
NODE=mokka-operator-control-plane
docker exec "$NODE" bash -c '
apt-get update -qq && apt-get install -y -qq curl gpg
curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey \
| gpg --dearmor -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg
curl -fsSL https://nvidia.github.io/libnvidia-container/stable/deb/nvidia-container-toolkit.list \
| sed "s#deb https://#deb [signed-by=/usr/share/keyrings/nvidia-container-toolkit-keyring.gpg] https://#g" \
> /etc/apt/sources.list.d/nvidia-container-toolkit.list
apt-get update -qq && apt-get install -y -qq nvidia-container-toolkit
'
docker exec "$NODE" nvidia-ctk runtime configure \
--runtime=containerd --cdi.enabled --set-as-default
docker exec "$NODE" sed -i 's/^mode = "auto"$/mode = "cdi"/' \
/etc/nvidia-container-runtime/config.toml
docker exec "$NODE" grep -q '^mode = "cdi"$' \
/etc/nvidia-container-runtime/config.toml
docker exec "$NODE" systemctl restart containerd
Step 2 — Install Mokka¶
helm install nvml-mock oci://ghcr.io/nvidia/k8s-test-infra/chart/nvml-mock \
--namespace mokka --create-namespace \
--wait --timeout 120s
Step 3 — Install the GPU Operator¶
cat > gpu-operator-values.yaml <<'EOF'
driver:
enabled: false
toolkit:
enabled: false
dcgm:
enabled: false
mig:
strategy: none
migManager:
enabled: false
nodeStatusExporter:
enabled: false
cdi:
enabled: true
default: true
devicePlugin:
enabled: true
config:
name: ""
gfd:
enabled: true
env:
- name: GFD_MACHINE_TYPE_FILE
value: "/etc/nvml-mock/machine-type"
dcgmExporter:
enabled: true
env:
- name: DCGM_EXPORTER_COLLECT_INTERVAL
value: "5000"
validator:
driver:
env:
- name: DISABLE_DEV_CHAR_SYMLINK_CREATION
value: "true"
toolkit:
env:
- name: NVIDIA_VISIBLE_DEVICES
value: "all"
cuda:
env:
- name: WITH_WORKLOAD
value: "false"
EOF
helm repo add nvidia https://helm.ngc.nvidia.com/nvidia && helm repo update
helm install gpu-operator nvidia/gpu-operator \
--namespace gpu-operator --create-namespace \
--version v26.3.3 \
-f gpu-operator-values.yaml \
--wait --timeout 600s
The version is deliberate. GPU Operator v26.3.3 is the release CI validates
for this workflow. Later releases can add hardware checks or change the chart
schema, so validate them against Mokka before updating the pin.
Use a values file, not --set
Every operand takes its driver root through a nested env list, and that
shape does not survive the command line: chained --set gfd.env[0].name=…
flags drop values silently, --set validator.cuda.env[0].value="false"
is typed as a bool against a string field, and an unquoted env[0] is
glob-expanded by zsh before Helm sees it. A values file is the only form of
this configuration that works.
Step 4 — Verify¶
kubectl -n gpu-operator wait --for=condition=ready pod --all --timeout=300s
kubectl get node "$NODE" -o jsonpath='{.status.allocatable.nvidia\.com/gpu}'
# 4 — the gb300 default profile has four devices
kubectl get node "$NODE" -o json \
| jq '.metadata.labels | with_entries(select(.key | startswith("nvidia.com")))'
The validator pod reaching Completed is the meaningful signal: it probes the
driver root, exercises CDI injection, and checks the node advertises GPUs.
What each value is doing¶
| Value | Why |
|---|---|
driver.enabled: false |
Mokka is the driver. The real DaemonSet would try to build a kernel module |
toolkit.enabled: false |
The mock libraries are staged on the host by Mokka's DaemonSet, so the toolkit has nothing to inject. CDI carries the devices instead |
cdi.enabled / cdi.default |
The runtime reads /var/run/cdi/nvidia.yaml, which Mokka generates. This is what replaces the toolkit operand |
dcgm.enabled: false |
The separate nv-hostengine DaemonSet is redundant: dcgm-exporter embeds the host engine in-process |
dcgmExporter.enabled: true |
Kept on deliberately — it reads the mock through libdcgm, which is part of what this proves |
GFD_MACHINE_TYPE_FILE |
GFD's default reads /sys/class/dmi/id/product_name, which says kind here and is absent on hosts with no DMI. Mokka writes a file of its own |
mig.strategy: none |
The install above does not partition the board, so there are no slices to advertise and whole GPUs are the right view. MIG itself is simulated: MIG partitioning carves a board and serves the slices through the device plugin in migStrategy=single |
validator.cuda.WITH_WORKLOAD: false |
The CUDA validation step launches a kernel, and CUDA is not simulated |
DISABLE_DEV_CHAR_SYMLINK_CREATION |
The /dev/char symlink step runs modprobe nvidia, which cannot work in a Kind container. Mokka already staged those nodes |
Three labels that look wrong and are not¶
Inspecting the node labels after a run turns up three that seem to contradict the overlay. None of them breaks anything.
nvidia.com/gpu.deploy.driver=trueandnvidia.com/gpu.deploy.container-toolkit=trueare the operator's own scheduling hints, written by GFD for operands it manages. They mean "this node is eligible for that operand", not "that operand is here". Withdriver.enabled=falseandtoolkit.enabled=falsethe DaemonSets are never created, so the labels have nothing to select. Confirm withkubectl -n gpu-operator get ds, which lists neither.nvidia.com/mig.capable=truelooks like it contradictsmig.strategy: none, and does not. The board really is MIG-capable and the mock really partitions it; this install simply does not carve it, which is why the strategy staysnone. Carving it is the MIG partitioning guide.
Troubleshooting¶
The validator crash-loops. It is a statically linked Go binary with no shell, so a missing file in the staged driver root looks like a crash rather than an error. Check what is actually there:
The device plugin reports 0 GPUs. Almost always driver.enabled or
toolkit.enabled left at their defaults — the operator is managing a driver
that does not exist. Confirm with helm get values gpu-operator.
GFD labels the node kind. GFD_MACHINE_TYPE_FILE is missing from the
overlay.
More symptoms in Troubleshooting.
Running it from a checkout¶
The steps above are the whole scenario and never need the repository. If you
have it cloned, run.sh performs the same install and then asserts the result:
It installs Mokka, checks that nvidia-smi -L works inside a mock pod,
installs the operator with the same overlay from
gpu-operator-values.yaml, then
asserts two things that can only hold if the real operands read the mock: some
node lists nvidia.com/gpu as allocatable, and some node carries a
nvidia.com/gpu.product label. Both scan every node, because the operands do
not tolerate the control-plane NoSchedule taint.
| Variable | Default | Effect |
|---|---|---|
GPU_PROFILE |
gb300 |
Any profile under the chart's profiles/ directory |
NAMESPACE |
mokka-operator |
Namespace for the Mokka release |
OPERATOR_NAMESPACE |
gpu-operator |
Namespace for the GPU Operator release |
GPU_OPERATOR_VERSION |
v26.3.3 |
GPU Operator chart version to install |
NVML_MOCK_IMAGE |
ghcr.io/nvidia/nvml-mock:latest |
Published image to install |
BUILD_LOCAL |
false |
Build the image from source and side-load it with kind load |
HELM_TIMEOUT |
15m |
Wait budget for each Helm install |
DEMO_ASSUME_YES |
false |
Skip the confirmation prompt and the co-location refusals |
One Mokka release per cluster at a time
The chart's hostPath mounts are fixed and release-independent
(/var/lib/nvml-mock, /var/run/cdi, /run/nvidia, and the NFD features
directory), and it tolerates every taint, so two Mokka releases land on
every node and write the same per-node state whatever they are called —
including the shared config that any real GPU workload on those nodes then
reads. run.sh refuses with exit 4 if another Mokka release is already
installed anywhere on the cluster.
Clean up¶
Related¶
| To read about | See |
|---|---|
| Every chart value | Installation |
| Changing GPU state while the operator watches | Runtime Control |
| Driving this from CI | Use in CI/CD |
| The full health loop on top of this stack | NVSentinel |