fix(actor_group): select the torch_memory_saver LD_PRELOAD lib by CUDA runtime, not by filename (fixes libcudart.so.12 crash on CUDA 13)#2187
Open
littlemex wants to merge 1 commit into
Conversation
…ntime, not filename With --offload-train on the megatron backend, actor_group.py picked the torch_memory_saver preload .so from a hard-coded [_cu12, unsuffixed] list by os.path.exists (first that exists). On a CUDA 13 image the cu12 build exists but links libcudart.so.12 (absent), so LD_PRELOAD makes every child -- incl. the train worker -- die with 'libcudart.so.12: cannot open shared object file'. The correct cu13 build ships in the same wheel but is never enumerated. Delegate .so selection to torch_memory_saver's own CUDA-aware resolver (get_binary_path_from_package) when present; a resolver that raises is propagated rather than masked. For older builds without the resolver, fall back keyed on the CUDA major torch was built for (torch.version.cuda) -- which is compile-time metadata needing no GPU/driver in this process. This matters because slime resolves on the Ray driver, which may be a CPU-only head node with no libcuda.so.1: selecting the cu<major> build by existence there is correct (LD_PRELOAD takes effect on the GPU workers). Only when the major cannot be determined do we probe ctypes.CDLL loadability as a last resort, and if even that is impossible (no libcuda), fall back to the first existing candidate with a warning rather than failing a config the workers could run.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What / Why
With
--offload-trainon the Megatron backend,actor_group.pychooses thetorch_memory_saverpreload.sofrom a hard-coded list (_cu12, then unsuffixed) and picks the first that exists on disk. On a CUDA 13 image thecu12build exists but linkslibcudart.so.12(absent on CUDA 13), soLD_PRELOADmakes every child — including the train worker — die withlibcudart.so.12: cannot open shared object file. The correctcu13build ships in the same wheel but is never considered.This PR stops choosing by filename existence and instead uses the CUDA runtime to pick the right build, by delegating to
torch_memory_saver's own resolver — the mechanism the library already provides for exactly this.Root cause (concise)
slime/ray/actor_group.pyL64-L84: candidate list is["..._preload_cu12.abi3.so", "..._preload.abi3.so"], selected viaos.path.exists. Nocu13candidate; existence ≠ loadability.torch 2.11.0+cu130,torch_memory_saver 0.0.9.post1): both hard-coded candidates are byte-identical cu12 builds thatos.path.exists-pass butctypes.CDLL-fail withlibcudart.so.12: cannot open shared object file; only..._preload_cu13.abi3.soloads.torch_memory_saveralready ships a CUDA-aware resolver —utils.get_binary_path_from_package("torch_memory_saver_hook_mode_preload")— that detects the CUDA major (viatorch.version.cuda, then alibcudart.so.<major>probe over(13, 12)) and returns the matching build. It is what the library uses internally inhooks/mode_preload.py.The change
Replace the hard-coded filename loop with the library resolver, with a robust fallback for older
torch_memory_saverversions that predate it. The fallback keys on the CUDA major torch was built for (torch.version.cuda) — compile-time metadata that needs no GPU/driver, so it works on the Ray driver, which may be a GPU-less (CPU-only) head node. Actypes.CDLLloadability probe is only a last resort for when the major is unknown, since that probe needs the hostlibcuda.so.1and would otherwise fail a config the GPU workers could run. (The version slime's Dockerfile pins today,@a193d9dd, already has the resolver, so the resolver path is what runs in practice; the fallback keeps the fix correct against older/other pins.) Only a missing resolver falls back — a resolver that exists but raises is propagated, not masked.slime/ray/actor_group.py:with a module-level helper:
Before / after on CUDA 13:
Alternatives considered (and why rejected)
I checked each candidate approach on a live CUDA 13 worker before choosing.
Just add
cu13to the hard-coded list (keepos.path.exists, putcu13first).On this box it does pick a loadable
cu13(verified). Rejected: it still selects by existence, not loadability, and it re-hard-codes the CUDA set — the next CUDA bump (cu14) reintroduces the identical bug. It duplicates a decision the library already owns.Just switch
os.path.exists→ctypes.CDLL(keep the currentcu12/unsuffixed list).Verified result: selects nothing (
picked=None) on CUDA 13, becausecu13is not in the list — loadability alone cannot save a list that omits the right file. Rejected: incomplete.Delegate to the resolver only, no fallback.
Verified: returns the correct
cu13. The version slime pins today (@a193d9dd) does have the resolver, so resolver-only would work against the current pin — but keying the fix solely on the presence of a newer API makes it silently regress to the old crash if the pin is ever bumped down or another environment ships an oldertorch_memory_saver(the resolver was added at some point; anything before it raisesImportError). Hence: resolver first, deterministic fallback second, so the fix is correct regardless of the pinnedtorch_memory_saverversion.A
ctypes.CDLLloadability probe as the fallback's primary key.Tempting (loadability is the true test), but it runs in the process that calls this — the Ray driver, which may be a CPU-only head node with no
libcuda.so.1. There every candidate (including the correctcu13) fails todlopen, so the fallback would raise even though the GPU workers (whereLD_PRELOADactually takes effect) could load it. Rejected as the primary key. The fallback instead keys ontorch.version.cuda(build-time metadata, no driver needed) and usesCDLLonly as a last resort when the major is unknown, then degrades to existence-with-warning rather than failing a runnable config.The chosen fix (resolver-first, then a CUDA-major existence fallback with a dlopen last resort) is the only option that is correct on CUDA 13, unchanged on CUDA 12, forward-compatible with future CUDA majors, safe on the older pinned
torch_memory_saver, and safe on a GPU-less Ray driver. No new module, class, or config flag is introduced.Blast radius / risk
cu12/generic build exactly as before; the fallback's first loadable candidate is equivalent to today's behavior.libcudart.so.12crash in a child process.offload_train=Falseis entirely unaffected..soselection); the two changes are orthogonal and compose cleanly.Test plan (with commands)
Deterministic check (no full job, runs in any pod on the image):
Expected on CUDA 13:
End-to-end (a Megatron
--offload-trainrun): launch any offload_train Megatron recipe (e.g. Qwen3-4B), then confirm the train worker no longer hits the cu12 crash. The check below resolves the driver log by content, so it needs no hand-pasted Ray job id (finished jobs age out ofray job list):Verified on hardware (H200, 2× p5en.48xlarge, CUDA 13.0)
..._preload_cu13.abi3.so,CDLLloads.torch.version.cuda=13.0and selects..._preload_cu13.abi3.soby existence — verified on the GPU-less head (nolibcuda.so.1), where the old dlopen-gated approach failed every candidate. This is the CPU-only-driver case.tests/test_tms_preload_resolution.py, 4 passed): resolver-first, resolver-error propagation, CUDA-major existence fallback, and CUDA 12 (cu12 selected, unchanged).--offload-trainQwen3-4B run:libcudart.so.12errors = 0 (previously the train worker died on it), and the job advanced into the training phase.9e724677…) and both fail to load on CUDA 13; only thecu13build (md51bd3044c…, never enumerated by the old code) loads.Checklist
.soselection delegates totorch_memory_saver's resolver when available; a resolver that raises is propagated, not masked.torch.version.cuda) by existence — safe on a GPU-less Ray driver — and only dlopen-probes when the major is unknown.LD_PRELOADresolves to the loadable..._preload_cu13.abi3.soand the--offload-trainrun no longer hitslibcudart.so.12.test_fallback_cuda12_selects_cu12(the cu12 build is selected by existence, as before).tests/test_tms_preload_resolution.py(resolver-first, resolver-error propagation, CUDA-major existence fallback, CUDA 12) — 4 passed.