Repository navigation
[core][sandbox] Add GPU support to Ray Sandboxes via CDI - #65898
Conversation
There was a problem hiding this comment.
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.
b8a581c to
796d7e4
Compare
3fbcdcb to
5d79700
Compare
5d79700 to
7d62005
Compare
e183e76 to
e74ea74
Compare
dstrodtman
left a comment
There was a problem hiding this comment.
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.
|
|
||
| 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. |
There was a problem hiding this comment.
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.
| 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). |
There was a problem hiding this comment.
Updated as requested.
|
|
||
| ### 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. |
There was a problem hiding this comment.
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.
| 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. |
There was a problem hiding this comment.
Reworded beyond what was suggested. PTAL.
| 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. |
There was a problem hiding this comment.
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.
| 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. |
There was a problem hiding this comment.
This paragraph was removed in the last edit.
|
|
||
| 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: |
There was a problem hiding this comment.
Em dash → sentence break, per the guide.
| 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: |
There was a problem hiding this comment.
Reworded given the broader changes mentioned above. PTAL.
4275626 to
bb78674
Compare
xyuzh
left a comment
There was a problem hiding this comment.
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.
| spec = _generated_spec_cache[kind] | ||
| else: | ||
| spec = generate_fn() | ||
| _generated_spec_cache[kind] = spec |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Update back to 60s. PTAL.
| "cdi", | ||
| "generate", | ||
| "--format=json", | ||
| "--disable-hook=update-ldcache", |
There was a problem hiding this comment.
Two things here:
-
--disable-hookonly exists from nvidia-container-toolkit v1.18.0 (multiple--device-name-strategyfrom 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. -
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, solibcuda.so.1won'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").
There was a problem hiding this comment.
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:
- 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)
- Don't recommend a minimum version of
v1.18.0and 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). - Write our own
update-ldcachehook that we ship and add to the CDI spec ourselves (and in parallel file a bug with thenvidia-container-toolkitto fix things on their end).
I am leaning towards 3, but happy to hear your thoughts.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Maybe worth adding to the docs as an extra caveat?
There was a problem hiding this comment.
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.
|
|
||
| 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). |
There was a problem hiding this comment.
"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
--nvproxyon NVIDIA driver versions it knows about, and there's no override for unknown ones. Pointing people atrunsc nvproxy list-supported-driverswill 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).
There was a problem hiding this comment.
This is a fair point. I'll reaudit what belongs here once I've made the rest of the changes being discussed here.
There was a problem hiding this comment.
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(): |
There was a problem hiding this comment.
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.
| f"has been validated)." | ||
| ) | ||
|
|
||
| def _gpu_cdi_flags(self) -> List[str]: |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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().
There was a problem hiding this comment.
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.
| 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 \ |
There was a problem hiding this comment.
Two small ones:
- This runs for every core build variant, including CPU ones that never use nvidia-ctk. Gating on
BUILD_VARIANT == gpubuildwould keep the extra external repo out of the CPU images. curl -s -Lwithout-fmeans a 404 body gets written to sources.list.d and you only find out at the nextapt-get updatewith a confusing error. Same-fsSLas the gpgkey fetch above.
There was a problem hiding this comment.
Updated as suggested.
| try: | ||
| assigned_gpu_ids = [str(i) for i in ray.get_gpu_ids()] | ||
| except Exception: | ||
| assigned_gpu_ids = [] |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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", |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
I don't think we want this in the main core Dockerfile?
There was a problem hiding this comment.
@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
There was a problem hiding this comment.
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.
| @@ -0,0 +1,332 @@ | |||
| """Minimal, vendor-agnostic CDI (Container Device Interface) spec generator | |||
There was a problem hiding this comment.
this entire comment block is very verbose and difficult to understand. Can we simplify and improve the readability?
There was a problem hiding this comment.
ACK. I will do an audit of it.
There was a problem hiding this comment.
I have update the text significantly. PTAL.
bb78674 to
c442e55
Compare
|
This pull request has been automatically marked as stale because it has not had 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. |
| "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] |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
The latest changes reject MIG devices early.
| 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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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]: |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 ( |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
left a comment
There was a problem hiding this comment.
Accelerator change lgtm
| tags: | ||
| - sandbox_tests | ||
| - gpu | ||
| instance_type: gpu-large |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Reviewed by Cursor Bugbot for commit 8b054cf. Configure here.
…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>

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, the
AcceleratorManagerclass gains two new optional methods,get_cdi_kind()/generate_cdi_spec(). These methods are implemented by anAcceleratorManagerto 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
NvidiaGPUAcceleratorManagerto be CDI aware and implement theget_cdi_kind()/generate_cdi_spec()methods mentioned above. At present, we generate CDI devices by shelling out tonvidia-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, theGVisorSandboxBackendgates GPU passthrough to CDI kinds it actually supports (i.e.nvidia.com/gpu), rejecting anything else rather than attempting unverified passthrough.num_gpusandgpu_ids: When creating aSandboxactor, you can now pass anum_gpusfield in addition to the existingcpuandmemoryfields (similar to what is possible when creating a non-sandboxed actor). When doing so, the sandbox auto-inherits the full Ray GPU assignment derived fromnum_gpus(with no way to limit it further when callingremote()later on, unlike for cpus and memory). When creating a sandbox viaSandboxRuntime.create()(i.e. within a pre-existing actor), there is nonum_gpusfield supported. Instead, we allow passing agpu_idsfield 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
createContainerhooks 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 withgpu_idsnow 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.pyandtest_cdi_lib.pyunderray/experimental/sandbox/tests/, and extended the existing suites there and underray/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, andcontainer device interface— nothing addresses GPU or CDI support for Ray Sandboxes.Testing
test_gvisor_gpu.py(realnvidia-ctkgeneration, device injection,--nvproxyboot,nvidia-smiinside the sandbox, and a real CUDA kernel launch via NVIDIA'svectorAddsample) — 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 userray: 269 passed, 3 failed, 15 skipped. The failures are the known CUDA failure (test_sandbox_gpu_cuda_vectoradd_runs_a_real_kernel, see above) andtest_netns_egress_and_dnsandtest_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, whichraycan'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
rayimage needs a wheel via./build-wheel.sh, which needs Anyscale's internalcr.ray.io/Bazel-remote-cache access to build in reasonable time. To avoid long build times, we instead overlay this branch's changed.pyfiles onto a stock publishedrayproject/ray:2.58.0image, 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 thenvidia-container-toolkit-basepackage 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):
Run it. Each test runs twice, once as root and once as the non-root user
ray.--device nvidia.com/gpu=allfor GPU access;--privilegedis needed for gVisor in this specific nested-Docker test rig.--basetempkeeps pytest's temporary directories out of/tmp, whichtest_image_utils.py::test_seeding_stays_inside_the_treerequires: