Skip to content

feat: CDI-based NVIDIA GPU passthrough into gVisor actor containers - #502

Open
eliranw wants to merge 3 commits into
agent-substrate:mainfrom
eliranw:eliranw/gvisor-gpu-poc
Open

feat: CDI-based NVIDIA GPU passthrough into gVisor actor containers#502
eliranw wants to merge 3 commits into
agent-substrate:mainfrom
eliranw:eliranw/gvisor-gpu-poc

Conversation

@eliranw

@eliranw eliranw commented Jul 23, 2026

Copy link
Copy Markdown

Summary

  • atecontroller propagates a pool's nvidia.com/gpu request onto the ateom container and mounts the host NVIDIA toolkit read-only (path overridable via ATE_NVIDIA_TOOLKIT_HOST_PATH)
  • ateom-gvisor generates a CDI spec with nvidia-ctk and injects the device nodes, driver-library mounts, and env into each actor container's OCI spec
  • runs the CDI createContainer hooks except update-ldcache which needs a privileged ateom, staging the SONAME symlinks it would create from each library's ELF DT_SONAME
  • enables runsc --nvproxy at sandbox creation

Requesting nvidia.com/gpu on the pool is the only configuration needed; a pool that requests N GPUs makes all N usable.

Two details of the CDI spec are worth calling out, because getting either wrong fails at runtime rather than at parse time. nvidia-ctk leaves major/minor unset — CDI delegates that to the OCI runtime — so each device node is resolved by stat-ing the host; without it the actor gets 0,0 char devices and NVML reports it cannot communicate with the driver. And it emits per-index, per-UUID, and all devices that repeat the same nodes, so only all is applied. The spec is plain JSON, so encoding/json suffices and no CDI library is vendored.

update-ldcache is the one hook that cannot run here: its ldconfig unshares a mount namespace and mounts a private /proc, which mount_too_revealing() rejects under the pod's masked /proc. Permitting it would need procMount: Unmasked, which Kubernetes only allows with hostUsers: false, and that user namespace breaks the per-actor cgroup delegation from #496. Skipping it avoids the whole chain, so a GPU worker keeps the same posture as any other unprivileged gVisor worker. create-symlinks and enable-cuda-compat still run unmodified.

--nvproxy must be set when the sandbox is created — the pause container, which holds no GPU devices — so runsc's auto-detection never fires on its own; without the flag the GPU subcontainer crashes the sentry on start. GPU detection matches any device index rather than assuming /dev/nvidia0, since a worker sharing a multi-GPU node can be assigned /dev/nvidia2 and /dev/nvidia3.

GPU pools must set spec.ateomImage to a glibc build (KO_DEFAULTBASEIMAGE=debian:stable-slim ko build ./cmd/ateom-gvisor) because the distroless default cannot exec nvidia-ctk; the default base is unchanged for every other pool. atelet also has to run on the GPU nodes to restore actors there, so its DaemonSet needs a toleration for whatever taint they carry. Both are documented in the API guide rather than defaulted.

Testing

  • make test
  • env -u NO_COLOR make verify
  • Real GPU, Tesla T4 / driver 580.65.06, through the full actor flow: nvidia-smi, vectorAdd, nbody at 3.77 TFLOP/s, PyTorch matmul via cuBLAS at 3.5 TFLOP/s (T4 peak FP32 is ~8.1, so no measurable sandbox penalty)
  • Actor whose entrypoint runs the CUDA sample directly exits 0, confirming the injected env reaches the workload without help from the test harness
  • Two workers holding two GPUs each on one 4-GPU node see disjoint device sets

Not yet exercised: snapshot/restore of a running GPU actor.

Fixes #627

  • Tests pass
  • Appropriate changes to documentation are included in the PR

@google-cla

google-cla Bot commented Jul 23, 2026

Copy link
Copy Markdown

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this failed invocation of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.

@eliranw
eliranw force-pushed the eliranw/gvisor-gpu-poc branch from b510ec9 to 2693528 Compare July 23, 2026 14:17
@dims

Copy link
Copy Markdown
Collaborator

eliranw is there a way to do this without introducing all the new vendored components? (21k lines of code is too much!)

@eliranw

eliranw commented Jul 23, 2026

Copy link
Copy Markdown
Author

It seems like we decided to commit vendor packages in this repo. I think it’s best to use the cdi package instead of reimplementing it. As far as I understand, the main downside of committing the vendor folder (instead of gitignoring it) is having a large diff. Do you have a different solution?

To make it easier to review, I moved the vendor changes into a separate commit. I noticed many other PRs do the same thing - does this approach make sense to you?

@eliranw
eliranw force-pushed the eliranw/gvisor-gpu-poc branch from 2693528 to 9cf40a5 Compare July 23, 2026 15:41
@dims

Copy link
Copy Markdown
Collaborator

eliranw it's not whether the vendor-ed stuff is a separate commit or not. just that we should avoid adding stuff if not really required.

see for example - main...dims:substrate:gpu-nocdi

@BenTheElder

Copy link
Copy Markdown
Collaborator

thanks for working on this

I'd also like to keep imported deps minimal.

I started looking at uVM without privileged and we may have a bigger problem there ... ideally I don't want to require a plugin on the host but we need permission to talk to KVM.

I may have an answer to that wrinkle but broadly speaking I'm also hoping that even if not implemented yet we can figure out how we'll extend this to uVM later, as I'm also working on closing the existing gap (durable directory support) at higher priority ...

@eliranw
eliranw force-pushed the eliranw/gvisor-gpu-poc branch 3 times, most recently from f896c76 to 5320561 Compare July 23, 2026 18:31
@eliranw

eliranw commented Jul 23, 2026

Copy link
Copy Markdown
Author

Thanks Davanum Srinivas (@dims) and Benjamin Elder (@BenTheElder). Dropped the vendored CDI as you both suggested and re-tested on a GPU node.

I’ve also gated the whole thing to gVisor-only for now so the NVIDIA toolkit mount only lands on gVisor pools, meaning a micro-VM pool that requests a GPU won’t pick up the nvidia toolkit.

On uVM - I'll start looking into that

@eliranw
eliranw force-pushed the eliranw/gvisor-gpu-poc branch 3 times, most recently from 37f756b to 39d9060 Compare July 30, 2026 19:27
@eliranw

eliranw commented Jul 30, 2026

Copy link
Copy Markdown
Author

Benjamin Elder (@BenTheElder) Davanum Srinivas (@dims) Omer Yahud (@omeryahud) - I've updated this PR to reflect #496 (unprivileged ateom).

I had a few issues getting the GPU CDI hooks working, but right now GPU pools keep exactly the same posture as any other gVisor worker - no user namespace, no procMount: Unmasked, and per-actor cgroup delegation still working. The issue was that only one CDI hook was failing: update-ldcache, which needs the private /proc; skipping just that hook (and writing the SONAME symlinks from DT_SONAME instead) avoids the issue I had. The other CDI hooks run unmodified. I also needed a debian:stable-slim gvisor image to run the cdi generation tool.
I also dropped the vendored CDI library per your earlier comment.

I did a full test end to end on a cluster with a T4 node workerpool on GKE with different NVIDIA tooling to check compatibility: nvidia-smi, vectorAdd, nbody, and a PyTorch cuBLAS matmul, plus two workers with two GPUs each on one 4-GPU node to check isolation betweek workers on a single node. I've added details in the updated description.

Comment thread docs/api-guide.md Outdated
Comment thread docs/api-guide.md
Signed-off-by: Eliran Wolff <eliranw@nvidia.com>
Comment thread cmd/ateom-gvisor/main.go Outdated
// (see nvproxyGlobalArgs). Set once here at boot to avoid a data race on the
// concurrent RunWorkload path.
if gpuPresent() {
runscNvproxy = true

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like passing arguments in a back channel, let's pass it as a function argument to maybeInjectGPU.

You can leave the check here for logging purposes

// maybeApplyGPUPodShape shapes a gVisor worker pod that requests a GPU so ateom
// can inject the GPU into actors via CDI. It mounts the host NVIDIA toolkit
// (version-matched to the node) — the glibc-based ateom image runs those toolkit
// binaries directly — and applies the user-namespace + unmasked-/proc posture the

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this correct? A left-over from a previous implementation?

@eliranw
eliranw force-pushed the eliranw/gvisor-gpu-poc branch from fc0575c to 74bc170 Compare August 3, 2026 08:55
eliranw added 2 commits August 3, 2026 14:02
Generate a per-pod CDI spec with nvidia-ctk, parse it in-tree, and inject the
device nodes (major/minor stat-resolved) of the "all" CDI device, the driver-
library mounts, and env into each actor's OCI spec. Run the CDI createContainer
hooks from the mounted toolkit except update-ldcache, whose ldconfig needs a
private /proc mount; stage the SONAME symlinks it would create from each
library's ELF DT_SONAME instead. That keeps the GPU worker on the same
unprivileged posture as any other gVisor worker (no user namespace, no
procMount:Unmasked, cgroup delegation intact). Enable runsc --nvproxy at sandbox
creation. Detect GPUs by device-node glob so any assigned index works
(multi-GPU). The distroless ateom cannot exec nvidia-ctk, so GPU pools must run a
glibc ateom build (WorkerPool.spec.ateomImage).

Signed-off-by: Eliran Wolff <eliranw@nvidia.com>
Signed-off-by: Eliran Wolff <eliranw@nvidia.com>
@eliranw
eliranw force-pushed the eliranw/gvisor-gpu-poc branch from 74bc170 to 01a3e03 Compare August 3, 2026 12:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

GPU support for gVisor actors

4 participants