fix(manifests): add NVML init container for NVIDIA GPU Operator support - #2489
fix(manifests): add NVML init container for NVIDIA GPU Operator support#2489bitflicker64 wants to merge 1 commit into
Conversation
There was a problem hiding this comment.
Pull request overview
This PR updates Kepler’s Kubernetes manifests (raw + Helm) to support NVIDIA GPU Operator deployments by avoiding a direct mount of the driver container root filesystem into the Kepler container. Instead, it uses an init container to copy only libnvidia-ml.so* into an emptyDir mounted at /usr/local/nvidia/lib64, and adjusts LD_LIBRARY_PATH accordingly.
Changes:
- Add an
nvidia-libsinit container that copies NVML libraries from the host driver path into anemptyDir. - Update the DaemonSet to mount the copied NVML libraries into the main Kepler container and point
LD_LIBRARY_PATHat that location. - Add a CI job to lint and render the Helm chart with
config.experimental.gpu.enabled=true.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| manifests/k8s/daemonset.yaml | Raw DaemonSet: add init container + emptyDir NVML library mounting and update LD_LIBRARY_PATH. |
| manifests/helm/kepler/values.yaml | Helm values: add knobs for driver path and init image used by the NVML-copy pattern. |
| manifests/helm/kepler/templates/daemonset.yaml | Helm DaemonSet template: gate init container, volumes, mount, and LD_LIBRARY_PATH on gpu.enabled. |
| .github/workflows/pr-checks.yaml | Add Helm lint/template CI coverage for the gpu.enabled=true variant. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
80910b4 to
9f6ec86
Compare
|
rebased after the 3 new commits today |
|
@bitflicker64 Can you squash into a single commit? |
9f6ec86 to
f701c28
Compare
done! |
|
Hi @bitflicker64! Thanks for this PR, this is a great approach for handling the NVML glibc conflicts. I was testing the init container logic and noticed one quick thing that might cause issues: Because find matches both the actual library and its symlinks (e.g., .so, .so.1), using cp -L will dereference all of them and copy the full 50MB file multiple times into the emptyDir. This will consume ~150MB+ immediately, which risks hitting your 200Mi sizeLimit and evicting the pod (especially if the host has leftover files from a driver update). Could you swap that to cp -P or cp -d (or just cp -a) to preserve the symlinks instead of duplicating the file contents? Thanks again for the contribution! |
Makes sense |
f701c28 to
a9f6f26
Compare
|
@bitflicker64 A few notes:
|
|
@bitflicker64 By any chance have we tested this with actual GPU node? |
Pushed with cp -P. Verified on real nvidia driver 580.159.03 symlink chain |
a9f6f26 to
e9612e1
Compare
|
@vprashar2929 latest push ( helm-lint CI is gone. GPU-enabled lint is a local pre-commit hook ( Init-container comments are trimmed to short "why" in the chart. Longer rationale is in the PR body / commit message. Flag coupling is called out in User docs: "Enabling GPU Power Monitoring (NVIDIA GPU Operator)" in GPU validation on a real host (driver 580.159.03, RTX 3050). Not a full GPU Operator cluster:
Still missing: a real Kepler pod on a GPU Operator node (scheduling / runtimeClass / PSA). If there's a preferred lab path, I can re-run there. |
|
@laurall974 might be able to test this 👀 |
sounds good to me ! |
ddbc6bc to
87c0c86
Compare
|
@vprashar2929 @nikimanoledaki re: testing on an actual GPU node — deployed this on single-node k3s v1.36.2 with a real RTX 3050 (driver 580.173.02). Chart unmodified, No driver on the node: init container exits 0 with "No NVML libraries found at /run-nvidia", Driver staged GPU-Operator-style (real NVML chain plus the driver's own 218 MB in, 2.2 MB out, no glibc. NVML resolved from the emptyDir and nowhere else. Also ran it on a Tesla T4 (driver 580.95.05) with the system NVML moved out of the loader's search path: same result, and a control with Not covered: a real GPU Operator install. I staged the driver root by hand and the device nodes reached the pod via Rebased onto latest main, squashed to a single commit. |
|
@laurall974 Can we have this patch tested? |
Set UpSo I was able to test it on real GPU-Operator-style setup: three H100 nodes running the containerized NVIDIA driver via nvidia-driver-daemonset. ResultThe nvidia-libs init container does not complete. It runs for ~5 minutes, then terminates with exit code 1 (reason: Error), and the pod goes into CrashLoopBackOff (BackOff restarting failed container nvidia-libs), so kepler stays in PodInitializing and never starts. Init container status: The only init container output is repeated permission-denied lines from find traversing the driver container's /proc: ConclusionNo Copied N of N or No NVML libraries found line was reached. The main container's /usr/local/nvidia/lib64 emptyDir was never populated and I could not exec into kepler (still initializing). |
|
@laurall974, makes sense. I’ll look into it. Thanks for testing it!! |
Closes sustainable-computing-io#2484 Kepler failed to load NVML on clusters using the NVIDIA GPU Operator because the operator exposes driver libraries at /run/nvidia/driver (driver-container root FS) but the chart mounted that hostPath directly into the Kepler container, pulling in glibc from the driver image and breaking dynamic linking. Replace the direct hostPath mount with an init container that copies only libnvidia-ml.so* into an emptyDir mounted at /usr/local/nvidia/lib64. The emptyDir holds just the NVML libraries -- no glibc conflict. Changes: manifests/helm/kepler/values.yaml - Chart-only knobs under daemonset.nvidia.* (not config.experimental.gpu.* so templates/configmap.yaml does not leak chart internals into the binary config). Enabling config.experimental.gpu.enabled also triggers the init-container plumbing. manifests/helm/kepler/templates/daemonset.yaml - Gated nvidia-libs init container, emptyDir mount, and LD_LIBRARY_PATH when gpu.enabled is true. manifests/k8s/daemonset.yaml - Same pattern, unconditional (raw manifest has no values system). .pre-commit-config.yaml - Local helm-lint-gpu hook for the gpu.enabled=true variant (keeps validation in pre-commit rather than a dedicated CI job). docs/user/installation.md - Document GPU Operator enablement, overrides, and flag coupling. Init container notes (deep rationale): - Copy globs the known driver lib dirs (usr/lib64, usr/lib/{x86_64,aarch64}-linux-gnu) instead of running find over the driver root FS. That root contains the driver container's /proc: find walked procfs for minutes and exited nonzero on unreadable fdinfo entries, so init crash-looped on real GPU Operator clusters. - set -e with no || true on the copy pass so ENOSPC/EACCES fail init. - cp -P (not -L): preserve the driver symlink chain so leftover driver versions do not blow past the 200Mi emptyDir sizeLimit. - runAsUser/runAsGroup 0 with capabilities dropped for root-owned driver libs without depending on the image USER directive. - NVIDIA_VISIBLE_DEVICES / NVIDIA_MIG_MONITOR_DEVICES stay unconditional for the runtimeClassName: nvidia workaround. Verified: helm lint passes both states; gpu-disabled render has no init/nvml volume path; gpu-enabled ConfigMap keeps only enabled/idlePower/dcgmEndpoint under experimental.gpu. Init validated end to end on a 4-node kubeadm cluster (staged driver root with live procfs: sub-second copy, symlink chain intact, no-driver fallback on 3 nodes, usr/lib64 layout) and against a Tesla T4 where NVML was initialized strictly from the copied directory. Signed-off-by: Himanshu Verma <himnshuverma10152006@gmail.com>
87c0c86 to
ece17bb
Compare
|
@laurall974 thanks for the detailed report !! reproduced it, fixed it, pushed (also rebased on main). root cause: fix: dropped repro + verification: 4-node kubeadm v1.30.14, driver root staged with a live procfs mounted at
also re-ran the T4 strict-isolation check with the new script taken byte-for-byte from the manifest: real driver 580.95.05 staged in the operator layout, script exit 0, then NVML initialized strictly from the copied dir ( when you get a chance, could you rerun it on the H100 setup? thanks again for catching this!! |
Setup (rerun on H100 setup after fix)GPU-Operator-style setup: H100 nodes running the containerized NVIDIA driver via daemonset:
nodeSelector:
nvidia.com/gpu.present: "true"
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values: [ <gpu-node-1>, <gpu-node-2>, <gpu-node-3> ]
tolerations:
- key: nvidia.com/gpu
operator: Exists
effect: NoSchedule
- key: node.kubernetes.io/unschedulable
operator: Exists
effect: NoSchedule
securityContext:
privileged: true
runAsNonRoot: false
runAsUser: 0
runAsGroup: 0
config:
experimental:
gpu:
enabled: trueI pulled the rebased branch, redeployed, and observed the pods over time on two separate sets of nodes. Result: init container is fixedThe kepler proceeds past init and starts. But: the main
|
Summary
On clusters with the NVIDIA GPU Operator, Kepler was mounting the whole driver-container root FS from
/run/nvidia/driver. That path includes the driver's glibc, so dynamic linking breaks when NVML loads.This PR switches to the usual pattern: an init container copies only
libnvidia-ml.so*into an emptyDir, and Kepler mounts that at/usr/local/nvidia/lib64withLD_LIBRARY_PATHpointed there. NVML libs only, no driver glibc.Closes #2484
What changed
manifests/helm/kepler/values.yaml: chart-only knobs underdaemonset.nvidia.*(driverPath,nvmlInitImage). Not underconfig.experimental.gpu.*, because the configmap dumps the wholeconfigtree into/etc/kepler/config.yamland the binary only knowsenabled/idlePower/dcgmEndpointthere.manifests/helm/kepler/templates/daemonset.yaml: whenconfig.experimental.gpu.enabledis true, add thenvidia-libsinit container, emptyDir mount,LD_LIBRARY_PATH, hostPath + emptyDir volumes (sizeLimit: 200Mi).manifests/k8s/daemonset.yaml: same pattern, always on (raw manifest has no values system)..pre-commit-config.yaml: localhelm-lint-gpuhook for thegpu.enabled=truevariant. Default state stays on the existing helmlint hook. No extra CI job.docs/user/installation.md: how to enable GPU power monitoring with the GPU Operator, including overrides and the flag coupling note.Design notes
config.experimental.gpu.enabledis intentionally coupled to the chart plumbing. Flip that flag and you get both the binary path and the init container. There is no separatedaemonset.nvidia.enabled.NVIDIA_VISIBLE_DEVICES/NVIDIA_MIG_MONITOR_DEVICESstay unconditional. They were always set, and theruntimeClassName: nvidiaworkaround depends on them.LD_LIBRARY_PATHis a static value. K8s only expands env vars defined earlier in the same pod spec, not the image env, so we cannot prepend whatever the image already had. The dynamic linker still searches/liband/usr/libby default.set -eand does not maskcpwith|| true. A full emptyDir or a permission error should fail the pod at init, not start Kepler with a half-copied library.usr/lib64,usr/lib/x86_64-linux-gnu,usr/lib/aarch64-linux-gnu) instead of runningfindover the driver root. The driver-container root FS has its own/procmounted inside it, so a full walk is slow and exits nonzero on unreadable proc entries, whichset -e(correctly) treats as fatal. An unknown future layout degrades to the same "No NVML libraries found" path as a non-GPU node instead of failing init.cp -P(preserve symlinks), notcp -L. Real NVIDIA drivers are a symlink chain (libnvidia-ml.so->.so.1->.so.580.x). Leftover driver versions on the host makecp -Lblow past the 200Mi sizeLimit;cp -Pkeeps the chain cheap.USER.Review feedback folded in
config.*intodaemonset.nvidia.*(Copilot).runAsUser/runAsGroup: 0(Copilot). KeptreadOnlyRootFilesystem: true(mount points are created by the runtime before the remount; emptyDir writes do not need a writable rootfs).cp -Pinstead ofcp -L(shellyco-code), with host numbers below.find. On a real GPU Operator cluster it traversed the driver container's/procfor minutes and crash-looped on unreadable entries (laurall974's H100 report). The copy pass now globs the known lib dirs only.Testing
helm lintandhelm templatefor bothgpu.enabledstates. Disabled render has no init container / nvml emptyDir path. Enabled ConfigMap only hasenabled/idlePower/dcgmEndpointunderexperimental.gpu.On a real cluster with a real GPU
Single-node k3s v1.36.2 (Ubuntu 24.04), NVIDIA RTX 3050, driver 580.173.02. Chart installed from this branch with
--set config.experimental.gpu.enabled=true, kepler image5495d5d-20260725174242. Manifest unmodified.Non-GPU node path (no
/run/nvidia/driveron the host):Completed, exitCode 0, logs "No NVML libraries found at /run-nvidia"DirectoryOrCreate/run/nvidia/driver, node still schedulesreadOnlyRootFilesystem: true,drop: [ALL],runAsUser/Group: 0— container started and exited 0NVML init failed: ERROR_LIBRARY_NOT_FOUND,no GPUs discovered(this is the log from #2484)Running, ready, 0 restarts, 0 GPU metricsGPU node path —
/run/nvidia/driver/usr/lib64staged as the GPU Operator exposes the driver container root: real NVML symlink chain plus the driver's own userspace (libc.so.6,ld-linux-x86-64.so.2,libcuda.so.580.173.02,libnvidia-glcore,libnvidia-opencl), 218 MB total.Copied 3 of 3 NVML library file(s), exitCode 0/usr/local/nvidia/lib64/proc/<pid>/maps)/usr/local/nvidia/lib64/libnvidia-ml.so.580.173.02discovered GPU name="NVIDIA GeForce RTX 3050 Laptop GPU",NVML initialized device_count=1kepler_node_gpu_watts 3.224,kepler_node_gpu_joules_total 3148.016,kepler_node_gpu_info 1nvidia-smiat the same momentSecond GPU, strict isolation
Tesla T4, driver 580.95.05, system NVML moved out of the loader's search path so the copied libraries were the only possible source.
helm template, byte-for-byte underbusybox:1.36.1Copied 3 of 3cp -Pvscp -Lon the same treeNVML initialized,kepler_node_gpu_watts 13.709LD_LIBRARY_PATHunsetERROR_LIBRARY_NOT_FOUND, 0 GPU metricsEarlier host-only validation (driver 580.159.03) agreed on the sizing: leftover multi-version host was ~152 MiB with
cp -Pvs ~307 MiB withcp -L.GPU Operator
/procregression, multi-node4-node kubeadm v1.30.14 (3x Ubuntu 24.04, 1x Fedora 43), containerd. Driver root staged on one worker the way the operator exposes it, including a live procfs mounted at
/run/nvidia/driver/proc, the mount the H100 report tripped on. Chart from this branch,--set config.experimental.gpu.enabled=true, no nodeSelector, so it schedules on every node including the tainted control plane.find), driver nodefind: /run-nvidia/proc/1/task/1/fdinfo: Permission denied(same lines as the H100 log),Init:CrashLoopBackOffCopied 3 NVML library file(s).so -> .so.1 -> .so.580.65.06)Running1/1, 2170 metrics, live RAPL zones/run/nvidia(incl. Fedora and control plane)No NVML libraries found, exit 0, keplerRunningusr/lib64instead ofx86_64-linux-gnu)Copied 3, keplerRunningT4 re-check with the new script taken byte-for-byte from the manifest: real driver libs (580.95.05) staged in the operator layout, script exit 0, then NVML loaded strictly from the copied directory:
nvmlInitSUCCESS, device discovered, live power readable.Not covered
privileged: truerather thanruntimeClassName: nvidia. PSA is untested.