Skip to content

[core][sandbox] Add GPU support to Ray Sandboxes via CDI - #65898

Merged
pcmoritz merged 6 commits into
ray-project:masterfrom
klueska:sandbox-gpu-cdi-support
Nov 3, 2026
Merged

pcmoritz merged 6 commits into
ray-project:masterfrom
klueska:sandbox-gpu-cdi-support

Conversation

@klueska

@klueska klueska commented Sep 3, 2026 •

Copy link
Copy Markdown
Contributor

Description

This PR adds GPU support to Ray Sandboxes by leveraging CDI (Container Device Interface) to inject GPU device nodes and their supported user-mode driver files into a gVisor sandbox at runtime.

This PR builds on #66470, which adds the overlayfs rootfs that GPU sandboxes boot from, and on #66469 and #66528 underneath it. Only the last five commits are new here.

The change is implemented as follows.

  • Generic CDI framework: We added a vendor-agnostic CDI module that can be used to parse CDI specs and merge devices from them with a standard OCI image spec. The implementation of this module is kept free of any Ray imports so it stays a candidate for extraction into a standalone library at some point in the future. As part of this, theAcceleratorManager class gains two new optional methods, get_cdi_kind()/generate_cdi_spec(). These methods are implemented by an AcceleratorManager to both generate a spec with the full set of CDI devices associated with the accelerator, as well as advertise what kind of CDI devices are associated with it (e.g. nvidia.com/gpu).

  • NVIDIA specific implementation : We extended the NvidiaGPUAcceleratorManager to be CDI aware and implement the get_cdi_kind()/generate_cdi_spec() methods mentioned above. At present, we generate CDI devices by shelling out to nvidia-ctk cdi generate, but the preferred long-term path is to build (or leverage) a canonical python library for CDI maintained upstream. Since gVisor only supports NVIDIA GPUs at the moment, the GVisorSandboxBackend gates GPU passthrough to CDI kinds it actually supports (i.e. nvidia.com/gpu), rejecting anything else rather than attempting unverified passthrough.

  • num_gpus and gpu_ids: When creating a Sandbox actor, you can now pass a num_gpus field in addition to the existing cpu and memory fields (similar to what is possible when creating a non-sandboxed actor). When doing so, the sandbox auto-inherits the full Ray GPU assignment derived from num_gpus (with no way to limit it further when calling remote() later on, unlike for cpus and memory). When creating a sandbox via SandboxRuntime.create() (i.e. within a pre-existing actor), there is no num_gpus field supported. Instead, we allow passing a gpu_ids field as an explicit parameter to specify which of the GPUs that have been granted to the surrounding actor should be isolated inside the sandbox. This allows a single actor with multiple GPUs to manage several sandboxes, each pinned to a different GPU.

  • Overlayfs rootfs for GPU sandboxes: NVIDIA's CDI spec relies on createContainer hooks that create library symlinks and update the ldcache in the sandbox's rootfs, and runsc creates mount points there for the driver files and device nodes. The default EROFS rootfs is read-only and shared by every sandbox of an image, so any sandbox with gpu_ids now runs on [core][sandbox] Boot sandboxes from an overlayfs rootfs if required #66470's overlayfs rootfs. These writes land in the sandbox's own upper layer, and the cached image stays untouched.

  • 53 new unit tests and 9 real-GPU CI tests: We added test_cdi.py and test_cdi_lib.py under ray/experimental/sandbox/tests/, and extended the existing suites there and under ray/tests/accelerators/. The real-GPU tests run via two new Buildkite jobs (core: sandbox gpu tests, 1 GPU; core: sandbox multi gpu tests, 4 GPUs).

Validated end-to-end on an NVIDIA L4 GPU and a machine with 4 NVIDIA A100 GPUs.

We also tested on a machine with 4 NVIDIA GB200 GPUs (ARM64/Grace CPU). All of the NVML-based tests passed, but the CUDA tests failed (because gVisor does not yet fully support GB200). Once gVisor supports GB200 we will need to revisit things to ensure that multi-node NVLink is properly supported as well.

AI assistance

Developed with Claude Code (Sonnet 5), including driving the hardware validation above. I reviewed every changed line and take responsibility for the change end-to-end.

Related issues

No tracking issue. Sibling of other active sandbox work: image cache LRU eviction (#65748), per-sandbox netns (#65820), HTTP API (#65633), gRPC facade (#65839), multi-uid netns (#65823); none overlap with this change.

Duplicate check

Searched open/closed issues and PRs for sandbox gpu, CDI, gvisor, nvproxy, nvidia-ctk, and container device interface — nothing addresses GPU or CDI support for Ray Sandboxes.

Testing

  • test_gvisor_gpu.py (real nvidia-ctk generation, device injection, --nvproxy boot, nvidia-smi inside the sandbox, and a real CUDA kernel launch via NVIDIA's vectorAdd sample) — passes on a machine with an NVIDIA L4 GPU and a machine with 4 A100 GPUs.

  • test_gvisor_multi_gpu.py (all four tests, needing 4 GPUs) — only relies on NVML to test. Passes on both a machine with 4 A100 GPUs and a machine with 4 GB200 GPUs, each sandbox pinned to a distinct GPU.

  • python/ray/experimental/sandbox/tests/ (full directory, including the above) — on the L4: 228 passed, 10 skipped. On the GB200 machine, as root: 283 passed, 3 failed, 1 skipped. As the non-root user ray: 269 passed, 3 failed, 15 skipped. The failures are the known CUDA failure (test_sandbox_gpu_cuda_vectoradd_runs_a_real_kernel, see above) and test_netns_egress_and_dns and test_netns_udp_flows_do_not_cross, which need public DNS that this machine's network doesn't provide. The skipped tests need a worker that can mount in the host's user namespace, which ray can't in this Docker test rig, or the CI sandbox job.

  • pre-commit run — passed (SKIP=vale; its Go/cgo toolchain doesn't build in this environment).

Testing manually

Building the published ray image needs a wheel via ./build-wheel.sh, which needs Anyscale's internal cr.ray.io/Bazel-remote-cache access to build in reasonable time. To avoid long build times, we instead overlay this branch's changed .py files onto a stock published rayproject/ray:2.58.0 image, allowing us to test without a core rebuild since nothing outside pure Python changed. These tests require a machine with an NVIDIA GPU, Docker, and the nvidia-container-toolkit-base package installed.

Dockerfile: https://gist.github.com/klueska/9f1297a5d7f5a7b7cf9b31b3fb393feb

Save it locally, then build from a checkout of this branch (the repo itself is the build context):

cd ray
curl -fsSL -o Dockerfile.sandbox-gpu-test https://gist.githubusercontent.com/klueska/9f1297a5d7f5a7b7cf9b31b3fb393feb/raw
docker build -t ray-cdi-gpu-test:latest -f Dockerfile.sandbox-gpu-test .

Run it. Each test runs twice, once as root and once as the non-root user ray. --device nvidia.com/gpu=all for GPU access; --privileged is needed for gVisor in this specific nested-Docker test rig. --basetemp keeps pytest's temporary directories out of /tmp, which test_image_utils.py::test_seeding_stays_inside_the_tree requires:

# Pure-logic CDI tests + real-nvidia-ctk accelerator tests, no gVisor/root needed:
docker run --rm --device nvidia.com/gpu=all ray-cdi-gpu-test:latest \
  python3 -m pytest --basetemp=/home/ray/pytest-tmp -v \
  /home/ray/anaconda3/lib/python3.10/site-packages/ray/experimental/sandbox/tests/test_cdi.py \
  /home/ray/anaconda3/lib/python3.10/site-packages/ray/experimental/sandbox/tests/test_cdi_lib.py \
  /home/ray/anaconda3/lib/python3.10/site-packages/ray/tests/accelerators/test_nvidia_gpu.py

# The real end-to-end GPU sandbox test: exercises real nvidia-ctk generation, device injection,
# and gVisor --nvproxy passthrough. No host setup needed -- nvidia-persistenced can be running
# or not, both are handled.
# The two commands below run these tests as root and as the non-root user ray.
docker run --rm --privileged --user root --device nvidia.com/gpu=all ray-cdi-gpu-test:latest \
  python3 -m pytest --basetemp=/home/ray/pytest-tmp -v \
  /home/ray/anaconda3/lib/python3.10/site-packages/ray/experimental/sandbox/tests/test_gvisor_gpu.py
docker run --rm --privileged --user ray --device nvidia.com/gpu=all ray-cdi-gpu-test:latest \
  python3 -m pytest --basetemp=/home/ray/pytest-tmp -v \
  /home/ray/anaconda3/lib/python3.10/site-packages/ray/experimental/sandbox/tests/test_gvisor_gpu.py

# The four 4-GPU tests: three check that concurrent sandboxes each get a distinct GPU (separate
# Sandbox actors, SandboxRuntime.create, and one actor pinning each of its sandboxes via
# gpu_ids=...), and one checks that a single sandbox given 2 GPUs sees both.
# Needs a machine with 4 GPUs; `--device nvidia.com/gpu=all` must expose at least 4.
# The two commands below run these tests as root and as the non-root user ray.
docker run --rm --privileged --user root --device nvidia.com/gpu=all ray-cdi-gpu-test:latest \
  python3 -m pytest --basetemp=/home/ray/pytest-tmp -v \
  /home/ray/anaconda3/lib/python3.10/site-packages/ray/experimental/sandbox/tests/test_gvisor_multi_gpu.py
docker run --rm --privileged --user ray --device nvidia.com/gpu=all ray-cdi-gpu-test:latest \
  python3 -m pytest --basetemp=/home/ray/pytest-tmp -v \
  /home/ray/anaconda3/lib/python3.10/site-packages/ray/experimental/sandbox/tests/test_gvisor_multi_gpu.py

# Full sandbox suite (mocked-GPU config/scheduling tests + real-gVisor CPU and GPU tests):
# The two commands below run these tests as root and as the non-root user ray.
docker run --rm --privileged --user root --device nvidia.com/gpu=all ray-cdi-gpu-test:latest \
  python3 -m pytest --basetemp=/home/ray/pytest-tmp -v \
  /home/ray/anaconda3/lib/python3.10/site-packages/ray/experimental/sandbox/tests/
docker run --rm --privileged --user ray --device nvidia.com/gpu=all ray-cdi-gpu-test:latest \
  python3 -m pytest --basetemp=/home/ray/pytest-tmp -v \
  /home/ray/anaconda3/lib/python3.10/site-packages/ray/experimental/sandbox/tests/

@klueska
klueska requested review from a team, MengjinYan and edoakes as code owners September 3, 2026 17:20

@gemini-code-assist gemini-code-assist Bot left a comment

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.

Code Review

This pull request introduces GPU support for Ray Sandboxes using the Container Device Interface (CDI) to safely expose GPUs inside isolated gVisor containers. It adds vendor-agnostic CDI spec parsing and OCI merging utilities, integrates CDI spec generation into the NVIDIA GPU accelerator manager, and updates the sandbox configuration and gVisor backend to validate and pass through scheduled GPUs. The review feedback highlights several robust improvements, including resolving inconsistent default type handling and potential None values in CDI parsing, failing fast in the gVisor backend to prevent unnecessary image pulls, elevating the log level for nvidia-ctk failures to aid troubleshooting, and replacing os.mknod with safer socket and FIFO creation methods to ensure compatibility in unprivileged environments.

Comment thread python/ray/_common/cdi_lib.py Resolved
@ray-gardener ray-gardener Bot added core Issues that should be addressed in Ray Core community-contribution Contributed by the community labels Sep 3, 2026
@klueska
klueska force-pushed the sandbox-gpu-cdi-support branch 2 times, most recently from b8a581c to 796d7e4 Compare September 3, 2026 21:02
@klueska
klueska force-pushed the sandbox-gpu-cdi-support branch 3 times, most recently from 3fbcdcb to 5d79700 Compare September 3, 2026 21:34
@klueska
klueska force-pushed the sandbox-gpu-cdi-support branch from 5d79700 to 7d62005 Compare September 4, 2026 06:04

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale Bugbot comment from a previous run.

@klueska
klueska force-pushed the sandbox-gpu-cdi-support branch 5 times, most recently from e183e76 to e74ea74 Compare September 4, 2026 13:50

@dstrodtman dstrodtman left a comment

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.

Docs-side review from the Anyscale docs team (Douglas Strodtman). Claude Code assisted; I read every comment and stand behind each one.

Scope: style, grammar, and docs conventions on doc/source/ray-core/sandboxes.md only, against the Ray documentation style guide. I'm not reviewing the GPU/CDI code or the CI changes; those belong to the Ray core maintainers, and the bot threads already cover them. Nothing here is a merge blocker from the docs side, so this is a COMMENT, not a change request.

The new GPU section is clear and well-organized, and the cross-links between Requirements and the GPU section read well. The suggestions below are all one thing: the style guide asks you to avoid em dashes, semicolons, and parentheticals in prose and restructure into separate sentences instead. The new prose introduced a few of each (Vale independently flags the semicolons and parens; it has no em-dash rule, which is why they need a human eye). Each suggestion preserves your technical claims exactly and only reshapes the punctuation, plus a couple of small word-choice items the guide calls out (actually as filler, since → because for causation).

Take or leave any of them; they're all optional polish. Once you're happy with the prose, the docs side has no objection.

Comment thread doc/source/ray-core/sandboxes.md Outdated

To install `runsc` on a Linux worker node, see the [gVisor installation guide](https://gvisor.dev/docs/user_guide/install/).

To give sandboxes GPU access (see [GPU access](#gpu-access) below), additionally install `nvidia-container-toolkit-base` (which provides `nvidia-ctk`) on GPU worker nodes; Ray uses it to generate a [CDI](https://github.com/cncf-tags/container-device-interface) spec each time a sandbox actor requests a GPU, no other configuration required.

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.

The guide asks to avoid semicolons and parentheticals in prose and split into separate sentences. This also lifts the trailing no other configuration required out of the comma-splice into its own sentence.

Suggested change
To give sandboxes GPU access (see [GPU access](#gpu-access) below), additionally install `nvidia-container-toolkit-base` (which provides `nvidia-ctk`) on GPU worker nodes; Ray uses it to generate a [CDI](https://github.com/cncf-tags/container-device-interface) spec each time a sandbox actor requests a GPU, no other configuration required.
To give sandboxes GPU access, also install `nvidia-container-toolkit-base` on GPU worker nodes. This package provides `nvidia-ctk`, which Ray uses to generate a [CDI](https://github.com/cncf-tags/container-device-interface) spec each time a sandbox actor requests a GPU. No other configuration is required. See [GPU access](#gpu-access).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Updated as requested.

Comment thread doc/source/ray-core/sandboxes.md Outdated

### GPU access

Since `Sandbox` is a standard Ray actor, request GPUs with `num_gpus` like any other actor; the sandbox automatically exposes exactly those GPUs inside the isolated container, the same way it inherits `num_cpus` and `memory`. This requires `nvidia-container-toolkit-base` on the node (see [Requirements](#requirements)) and a gVisor build with `--nvproxy` support.

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.

Splits the semicolon into two sentences. I also matched the parallel sentence under Schedule a Sandbox actor with custom resources, which opens with Because Sandbox is a standard Ray actor, and swapped like for a plain comparison since the guide reserves like for genuine comparisons.

Suggested change
Since `Sandbox` is a standard Ray actor, request GPUs with `num_gpus` like any other actor; the sandbox automatically exposes exactly those GPUs inside the isolated container, the same way it inherits `num_cpus` and `memory`. This requires `nvidia-container-toolkit-base` on the node (see [Requirements](#requirements)) and a gVisor build with `--nvproxy` support.
Because `Sandbox` is a standard Ray actor, request GPUs with `num_gpus` the same way you would for any other actor. The sandbox automatically exposes exactly those GPUs inside the isolated container, the same way it inherits `num_cpus` and `memory`. This requires `nvidia-container-toolkit-base` on the node (see [Requirements](#requirements)) and a gVisor build with `--nvproxy` support.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Reworded beyond what was suggested. PTAL.

Comment thread doc/source/ray-core/sandboxes.md Outdated
ray.get(sandbox_actor.delete.remote())
```

GPU sandbox creation requires `ray.get_gpu_ids()` to return at least one GPU for the calling actor or task — Ray populates it when `num_gpus` is requested, which is how the sandbox knows which GPUs are actually its to use. If it's empty (for example, a custom actor that never requested `num_gpus`, or driver code running outside any Ray task or actor), creating a GPU sandbox fails with a clear error rather than guessing. There's no way to opt in without going through Ray's `num_gpus` scheduling — setting `CUDA_VISIBLE_DEVICES` yourself has no effect here, since this checks Ray's own resource assignment, not that environment variable.

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.

This line has two em dashes and a parenthetical, which the guide asks you to restructure into separate sentences. I also dropped actually (the guide lists it as filler) and changed since to because for the causal clause. Same technical content, just reshaped.

Suggested change
GPU sandbox creation requires `ray.get_gpu_ids()` to return at least one GPU for the calling actor or task — Ray populates it when `num_gpus` is requested, which is how the sandbox knows which GPUs are actually its to use. If it's empty (for example, a custom actor that never requested `num_gpus`, or driver code running outside any Ray task or actor), creating a GPU sandbox fails with a clear error rather than guessing. There's no way to opt in without going through Ray's `num_gpus` scheduling — setting `CUDA_VISIBLE_DEVICES` yourself has no effect here, since this checks Ray's own resource assignment, not that environment variable.
GPU sandbox creation requires `ray.get_gpu_ids()` to return at least one GPU for the calling actor or task. Ray populates it when `num_gpus` is requested, which is how the sandbox knows which GPUs are its to use. If it's empty, creating a GPU sandbox fails with a clear error rather than guessing. This happens, for example, for a custom actor that never requested `num_gpus`, or for driver code running outside any Ray task or actor. There's no way to opt in without going through Ray's `num_gpus` scheduling. Setting `CUDA_VISIBLE_DEVICES` yourself has no effect here, because this checks Ray's own resource assignment, not that environment variable.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This paragraph was removed in the last edit.

Comment thread doc/source/ray-core/sandboxes.md Outdated

GPU sandbox creation requires `ray.get_gpu_ids()` to return at least one GPU for the calling actor or task — Ray populates it when `num_gpus` is requested, which is how the sandbox knows which GPUs are actually its to use. If it's empty (for example, a custom actor that never requested `num_gpus`, or driver code running outside any Ray task or actor), creating a GPU sandbox fails with a clear error rather than guessing. There's no way to opt in without going through Ray's `num_gpus` scheduling — setting `CUDA_VISIBLE_DEVICES` yourself has no effect here, since this checks Ray's own resource assignment, not that environment variable.

To give several sandboxes managed by one actor each their own GPU, use `SandboxRuntime` directly instead of `Sandbox` — a `Sandbox` actor is always exactly one actor to one sandbox, so it can't do this. Schedule the actor with `num_gpus=N`, then pass an explicit `gpu_ids=[id]` to each `SandboxRuntime.create()` call, one call per GPU the actor was assigned:

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.

Em dash → sentence break, per the guide.

Suggested change
To give several sandboxes managed by one actor each their own GPU, use `SandboxRuntime` directly instead of `Sandbox` — a `Sandbox` actor is always exactly one actor to one sandbox, so it can't do this. Schedule the actor with `num_gpus=N`, then pass an explicit `gpu_ids=[id]` to each `SandboxRuntime.create()` call, one call per GPU the actor was assigned:
To give several sandboxes managed by one actor each their own GPU, use `SandboxRuntime` directly instead of `Sandbox`. A `Sandbox` actor is always exactly one actor to one sandbox, so it can't do this. Schedule the actor with `num_gpus=N`, then pass an explicit `gpu_ids=[id]` to each `SandboxRuntime.create()` call, one call per GPU the actor was assigned:

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Reworded given the broader changes mentioned above. PTAL.

@klueska
klueska force-pushed the sandbox-gpu-cdi-support branch 3 times, most recently from 4275626 to bb78674 Compare September 8, 2026 08:59

@xyuzh xyuzh left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Went through the whole thing, including the earlier bot threads. The big ones from those rounds (on-disk spec reuse, wildcard cgroup rule, gpu_ids kwarg collision, writing placeholders into the shared rootfs) all look resolved in the current revision, and the env replace-instead-of-append choice is right given how runsc picks the second-to-last NVIDIA_VISIBLE_DEVICES when the last is void. CI wiring also checks out (coregpubuild shares the Dockerfile, so nvidia-ctk is in the GPU test image).

A handful of things inline. The two I'd really like addressed before merge are the negative caching + 5s timeout combo, and documenting the toolkit / gVisor driver version floors. The rest are questions and nits.

Comment thread python/ray/_common/cdi_lib.py Outdated
spec = _generated_spec_cache[kind]
else:
spec = generate_fn()
_generated_spec_cache[kind] = spec

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Caching None here worries me. Combined with the 5s timeout in nvidia_gpu.generate_cdi_spec, one slow nvidia-ctk run (first NVML init without nvidia-persistenced on a multi-GPU box can easily take several seconds) permanently disables GPU sandboxes for that process. For a Sandbox actor that's a one-off, but for a SandboxRuntime pool actor it's stuck until someone restarts it, and the only signal is a warning log from minutes earlier.

I'd just not cache failures. A failed generation is already the rare path, and retrying it on the next create is cheap relative to the alternative.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Hmm, I'm not even convinced we want to cache anything here at all anymore. This was inspired by the canonical go-based implementation of CDI which never actually does generation at all -- it only reads specs from disk and caches them.

In our case it should be up to the caller to decide if they want the results cached or not, not this library.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I drop the caching of None and moved all caching out of the lib and onto the call-site instead. In the common case a successful CI generation will only happen at at node startup, but if it fails it has another chance to regenerate the next time a spec is requested (i.e. at sandbox creation).

# local driver/device state, no network I/O, and normally completes in
# well under a second. This bounds a hung/misbehaving nvidia-ctk to a
# short, user-visible failure instead of a long stall.
_NVIDIA_CTK_TIMEOUT_SECONDS = 5

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

5s feels tight. nvidia-ctk cdi generate does an NVML init plus ldcache/library discovery, and on a node where persistence mode is off each GPU has to come up first. I've seen that alone go past 5s on 8-GPU hosts. Something like 30–60s costs nothing on the happy path and avoids a spurious hard failure on a cold node.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I originally had it at 60s but that felt really long. That said, even on the unhappy path (i.e. the one where the tool is not installed at all), we still get a fast failure. It's only if the tool actually starts to run and is taking a really long time for some reason that you ever hit this timeout. So I agree, longer is better and will make the change.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Update back to 60s. PTAL.

"cdi",
"generate",
"--format=json",
"--disable-hook=update-ldcache",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Two things here:

  1. --disable-hook only exists from nvidia-container-toolkit v1.18.0 (multiple --device-name-strategy from v1.15.0). On an older toolkit this just exits non-zero and the user sees a generic "couldn't generate CDI spec". Worth stating the version floor in the docs and maybe hinting at it in the warning.

  2. On the comment above about update-ldcache being redundant because the mounted libs "already sit on ld.so's default search path": that's true when host and image are both Debian-style multiarch. A RHEL or Alpine image gets the libs bind-mounted at the host's /usr/lib/x86_64-linux-gnu/..., which isn't on its default search path, so libcuda.so.1 won't resolve. gVisor's GPU docs also say update-ldcache runs fine as a createContainer hook under nvproxy. What did you actually hit when it was enabled? If it really can't run, this should probably be a documented limitation ("image must share the host's library layout").

@klueska klueska Sep 8, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Regarding (1) -- happy to update the docs with a minimum version of v1.18.0. That version is just around a year old, so shouldn't provide any practical limitations.

Regarding (2) -- you're absolutely right about the libs only sitting in the right path when the host layout matches the container images layout (they don't need to be Debian based, but they do need to match). I didn't think about this when putting together the comment or deciding to omit the update-ldcache hook.

Although the gVisor docs claim they work with update-ldcache, this is only true prior to v1.18.0. In v1.18.0 we changed the way the update-ldcache hook works to ensure that it always runs inside its own isolated mount namespace (rather than directly on the host). As part of this it also isolates the proc filesystem, which gVisor doesn't like and blows up on.

I see 3 options:

  1. Live with the limitation that the host (i.e. ray node) fs needs to be of the same "flavor" as the container images being started (I like this option the least)
  2. Don't recommend a minimum version of v1.18.0 and instead recommend a version <= v1.18.0. This means that (in general) we may miss out on the many bug fixes that have come in since then (I like this option the second least).
  3. Write our own update-ldcache hook that we ship and add to the CDI spec ourselves (and in parallel file a bug with the nvidia-container-toolkit to fix things on their end).

I am leaning towards 3, but happy to hear your thoughts.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I've opened a PR agains the nvidia-container-toolkit to address this at its source.

I will recommend the change be backported to 1.18 and 1.19 and 1.20 once merged:
NVIDIA/nvidia-container-toolkit#2059

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

While we wait for the above PR to get merged and backported. I put a temporary "hack" in place to make this work in our environment.

The key insight being that nvidia-ctk always sets NVIDIA_CTK_LIBCUDA_DIR in its CDI edits to the set of directories where the driver libs have been placed. With this available, we can prepend these dirs onto whatever a container's existing LD_LIBRARY_PATH is before a sandbox is brought online.

Of course someone could always update / overwrite LD_LIBRARY_PATH after the container has started to bypass this and cause it to break.

That said, it should cover 95% of the use cases out there and is only a temporary measure until NVIDIA/nvidia-container-toolkit#2059 is merged and backported, so it seems to me like a pragmatic solution for now.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Maybe worth adding to the docs as an extra caveat?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

NVIDIA/nvidia-container-toolkit#2059 has been merged in the nvidia-container-toolkit. Working with the team to get a backport to v1.18.x, v1.19.x and v1.20.x.

Comment thread doc/source/ray-core/sandboxes.md Outdated

To install `runsc` on a Linux worker node, see the [gVisor installation guide](https://gvisor.dev/docs/user_guide/install/). `pasta` ships as the `passt` package on Debian 12+/Ubuntu 23.04+ and Fedora, or as a [static build](https://passt.top/builds/latest/) (x86_64 only; on arm64 use the distro package or build from source — passt has no build dependencies).

To give sandboxes GPU access, also install `nvidia-container-toolkit-base` on GPU worker nodes. This package provides `nvidia-ctk`, which Ray uses to generate a [CDI](https://github.com/cncf-tags/container-device-interface) spec each time a sandbox actor requests a GPU. No other configuration is required. See [GPU access](#gpu-access).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

"No other configuration is required" is a bit optimistic. Things I think Requirements should mention:

  • nvidia-container-toolkit-base >= 1.18 (for --disable-hook).
  • runsc only boots with --nvproxy on NVIDIA driver versions it knows about, and there's no override for unknown ones. Pointing people at runsc nvproxy list-supported-drivers will save a lot of confused bug reports.
  • MIG isn't supported (gVisor has no nvidia-caps, and Ray's MIG id format wouldn't match the CDI device names anyway).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is a fair point. I'll reaudit what belongs here once I've made the rest of the changes being discussed here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I did an audit and updated the docs as suggested. PTAL.

# CI's tag-based scheduling guarantees a GPU is present.


def test_sandbox_gpu_nvidia_smi_sees_assigned_gpu():

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Every real-hardware test here and in the multi-GPU file gives each sandbox exactly one GPU. I'd add one num_gpus=2 case asserting nvidia-smi --query-gpu=uuid returns exactly two lines. That's the only path that exercises multiple frontend device nodes in a single spec, and it's the one most likely to interact badly with the NVIDIA_VISIBLE_DEVICES handling.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

ACK

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added. PTAL.

f"has been validated)."
)

def _gpu_cdi_flags(self) -> List[str]:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Minor design point: this re-derives the runsc flags from the process-global spec cache on every runsc call, including exec and delete. If a later process (or the same one after a failed regeneration) can't get a spec, those calls silently run without --nvproxy. It probably works today since runsc talks to the running sentry, but recording the kind on the config at create time would make later calls independent of cache state, and drop the extra get_spec round trips.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Makes sense -- I don't think I want to make it a member of the generic SandboxConfig class (since in theory that is not gVisor specific), but I can find somewhere appropriate to cache it in the GVisorSandboxBackend class during create().

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I reworked this so that we only "calculate" whether --nvproxy is needed once at sandbox creation. As part of this, we also only pass these args to the runsc run command now, as the rest of the operations (i.e. exec/kill/delete/state) don't actually need them. PTAL.

Comment thread ci/docker/core.build.Dockerfile Outdated
apt-get install -y -qq gnupg
curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey \
| gpg --dearmor -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg
curl -s -L https://nvidia.github.io/libnvidia-container/stable/deb/nvidia-container-toolkit.list \

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Two small ones:

  • This runs for every core build variant, including CPU ones that never use nvidia-ctk. Gating on BUILD_VARIANT == gpubuild would keep the extra external repo out of the CPU images.
  • curl -s -L without -f means a 404 body gets written to sources.list.d and you only find out at the next apt-get update with a confusing error. Same -fsSL as the gpgkey fetch above.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

ACK

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Updated as suggested.

try:
assigned_gpu_ids = [str(i) for i in ray.get_gpu_ids()]
except Exception:
assigned_gpu_ids = []

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Swallowing the exception and then reporting "returned no GPUs" is misleading when the real problem is that Ray isn't initialized at all. I'd either let the exception propagate or include it in the message.

Bigger-picture (not blocking): doing a ray.get_gpu_ids() call inside a dataclass __post_init__ ties config construction to Ray runtime state. Feels like it belongs in SandboxRuntime.create, with SandboxConfig staying a plain value object.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I'll take a closer look at this and see what makes the most sense. I'll comment back here once I have a better feel for the "right" thing to do.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I just dropped the try so that the error propagates up as suggested.

# Needs a real GPU + nvidia-ctk. Only scheduled on a GPU CI runner -- see
# .buildkite/core.rayci.yml's "core: sandbox gpu tests" job.
py_test_module_list(
size = "medium",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

These pull a CUDA base image plus the cuda-sample image and boot gVisor with nvproxy. medium is 300s; I'd go large for both GPU targets so a slow registry doesn't show up as a flaky test.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

ACK

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Updated as requested.

# nvidia-container-toolkit-base (nvidia-ctk), needed by
# ray.experimental.sandbox's real GPU/CDI tests (gVisor --nvproxy; see
# .buildkite/core.rayci.yml's "core: sandbox gpu tests" job).
RUN <<EOF

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I don't think we want this in the main core Dockerfile?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@elliot-barn might have some ideas on where to put this. if not, I suggest we just document what users need to include in their Dockerfile

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I've added a commit that removes this from the core dockerfile and installs it at test time similar to how runsc gets installed for the standard sandbox tests. PTAL.

Comment thread python/ray/_common/cdi_lib.py Outdated
@@ -0,0 +1,332 @@
"""Minimal, vendor-agnostic CDI (Container Device Interface) spec generator

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

this entire comment block is very verbose and difficult to understand. Can we simplify and improve the readability?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

ACK. I will do an audit of it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I have update the text significantly. PTAL.

@klueska
klueska force-pushed the sandbox-gpu-cdi-support branch from bb78674 to c442e55 Compare September 8, 2026 21:35
@github-actions

Copy link
Copy Markdown

This pull request has been automatically marked as stale because it has not had
any activity for 14 days. It will be closed in another 14 days if no further activity occurs.
Thank you for your contributions.

You can always ask for help on our discussion forum or Ray's public slack channel.

If you'd like to keep this open, just leave any comment, and the stale label will be removed.

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale Bugbot comment from a previous run.

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale Bugbot comment from a previous run.

"GPUs for this actor or task. Request GPUs with num_gpus to "
"give a sandbox GPU access."
)
unassigned = [g for g in gpu_ids if g not in assigned_gpu_ids]

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.

The docs say MIG isn't supported, but nothing rejects it here. If a worker's CUDA_VISIBLE_DEVICES lists MIG devices, ray.get_gpu_ids() returns MIG- ids. nvidia-ctk cdi generate --device-name-strategy=uuid emits devices with those names too, so select_devices finds them and the sandbox only fails later inside gVisor, with an unclear error. Could we reject them up front with a clear error?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The latest changes reject MIG devices early.

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale Bugbot comment from a previous run.

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale Bugbot comment from a previous run.

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stale Bugbot comment from a previous run.

@xyuzh xyuzh left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Moving onto #66470's overlay got rid of the shared-rootfs workarounds, nice. Three things on the current head, inline.

Comment thread .buildkite/test.rules.txt Outdated
python/ray/_private/accelerators/accelerator.py
python/ray/_private/accelerators/nvidia_gpu.py
python/ray/tests/accelerators/test_nvidia_gpu.py
@ core_python llm ml tune train data serve

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Since #67252 (Oct 21), master's python/ rule no longer emits llm, so copying its tags here now adds llm for these three files. That's why premerge is red on this PR: the llm builds run and fail on an unrelated vLLM patch that no longer applies. I'd drop llm here and update the comment above to match.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done. I dropped llm from that rule and from nvidia_gpu.py's expected tags in test.rules.test.txt, and updated the comment to match.

"only access GPUs Ray scheduled to its owning actor or task."
)

def _resolve_gpu_run_args(self) -> List[str]:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

GPU sandboxes now rely on runsc running the CDI createContainer hooks (create-symlinks, update-ldcache). runsc only does that since google/gvisor@ae18a84dfad0, first released in release-20260520.0. Older builds log "CreateContainer hook skipped" and boot anyway. They still have --nvproxy and nvproxy list-supported-drivers, so every check here passes, and the failure only shows up later as nvidia-smi or CUDA not finding the driver libraries. Could this check runsc --version, and sandboxes.md:334 state the minimum?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added a check. Before giving a sandbox GPUs, the backend now reads runsc --version and fails sandbox creation if it reports a release older than release-20260520.0.

A runsc built from source reports a commit rather than a release, so it can't be checked that way and gets a warning instead, naming the minimum release.

sandboxes.md now states the minimum as well.

assert len(uuids) == 1, f"expected exactly 1 GPU visible, got {uuids}"
seen_uuids.append(uuids[0])

assert (

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

These tests check that the four sandboxes see four different GPUs, but not that each sees the GPU Ray assigned to it, so a mapping that hands sandbox A sandbox B's GPU still passes. Comparing each sandbox's UUID with the UUID of the GPU in its actor's ray.get_gpu_ids() would catch that. Same for the gpu_ids pool test below.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done. Each test now checks that the GPU a sandbox sees is the one Ray assigned it, comparing its UUID with the UUID of the GPU in the actor's gpu_ids. The pool test does the same with each sandbox's pinned gpu_id.

@edoakes edoakes left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Accelerator change lgtm

Comment thread .buildkite/core.rayci.yml Outdated
tags:
- sandbox_tests
- gpu
instance_type: gpu-large

@elliot-barn elliot-barn Oct 31, 2026 •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

nit can you change this to gpu-large-flex. gpu-large-flex supports g6.12xlarge,g5.12xlarge,g6.24xlarge,g5.24xlarge,g6e.12xlarge,g6e.24xlarge vs just g6.12xlarge

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.

Thanks, I updated it!

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

Reviewed by Cursor Bugbot for commit 8b054cf. Configure here.

klueska and others added 6 commits November 2, 2026 22:51
…s into sandboxes

Previously, a sandbox had no way to reach the host's accelerators.
Exposing a device means adding its device nodes, driver files and hooks
to the sandbox's OCI spec, and these differ by vendor and driver version.

In this commit, cdi_lib implements the subset of CDI (Container Device
Interface) needed to generate a spec, select its devices and merge their
edits into an OCI spec. No canonical Python implementation of CDI
exists, so cdi_lib imports nothing from Ray, which lets it move into a
standalone library later. AcceleratorManager gains optional get_cdi_kind
and generate_cdi_spec methods, and cdi.get_spec generates the spec for a
Ray resource such as "GPU".

Signed-off-by: Kevin Klues <kklues@nvidia.com>
Previously, no accelerator manager implemented the CDI methods, so
cdi.get_spec found no spec for any accelerator.

In this commit, NvidiaGPUAcceleratorManager implements them for the
"nvidia.com/gpu" kind. generate_cdi_spec runs `nvidia-ctk cdi generate`
with any settings from the node's nvidia-cdi-refresh.env, and parses the
spec from its stdout. It rejects nvidia-ctk versions older than 1.20.1,
the first whose update-ldcache hook runs under gVisor.

Signed-off-by: Kevin Klues <kklues@nvidia.com>
Previously, a sandbox had no access to the host's GPUs. Ray could
reserve GPUs for a Sandbox actor through num_gpus, but nothing exposed
them inside the gVisor sandbox the actor runs.

In this commit, sandbox.create gains a num_gpus option, and
SandboxConfig gains a gpu_ids field. The Sandbox actor fills gpu_ids from
ray.get_gpu_ids(), and SandboxRuntime.create takes it directly, so one
actor can pin each of its sandboxes to a different GPU.
The image manager merges those GPUs' CDI edits into the sandbox's OCI
spec, and the gVisor backend runs runsc with --nvproxy once it has
checked that runsc's release runs a CDI spec's createContainer hooks,
and that nvproxy supports the node's driver.

The CDI edits land in the spec before Ray checks whether a sandbox
needs an overlayfs rootfs, so a GPU sandbox's CDI hooks and mounts boot
it from one. Their writes and runsc's mount points then land in the
sandbox's own upper layer rather than the cached image.

Signed-off-by: Kevin Klues <kklues@nvidia.com>
Previously, the sandbox tests ran only on CPU instances, so nothing
exercised nvidia-ctk, device injection or --nvproxy on a real GPU.

In this commit, test_gvisor_gpu.py and test_gvisor_multi_gpu.py run GPU
sandboxes on 1 and 4 real GPUs. They check nvidia-smi, a CUDA kernel and
that each sandbox gets a distinct GPU. They also check that the cached
image stays untouched, for read-only and writable sandboxes and for each
way the worker can mount the overlay. Two new Buildkite jobs run them,
and the existing sandbox job skips them. nvidia-ctk is installed at test
time.

Signed-off-by: Kevin Klues <kklues@nvidia.com>
This commit documents what a GPU worker node needs, and shows how to
give a sandbox GPUs through a Sandbox actor's num_gpus or
SandboxRuntime.create's gpu_ids.

Signed-off-by: Kevin Klues <kklues@nvidia.com>
Signed-off-by: Philipp Moritz <pcmoritz@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

community-contribution Contributed by the community core Issues that should be addressed in Ray Core go add ONLY when ready to merge, run all tests sandbox unstale A PR that has been marked unstale. It will not get marked stale again if this label is on it.

Projects

None yet

Development

Successfully merging this pull request may close these issues.