Bug Description
With --offload-train on the Megatron backend, slime/ray/actor_group.py sets LD_PRELOAD to a torch_memory_saver preload .so. It picks the file from a hard-coded list — ..._preload_cu12.abi3.so, then the unsuffixed ..._preload.abi3.so — and takes the first that exists on disk (os.path.exists).
On a CUDA 13 image this is wrong twice over:
- The
cu13 variant is never enumerated.
- The test is file existence, not loadability. The
cu12 .so exists as a file, so it is selected — but it links libcudart.so.12, which is absent on CUDA 13. Once it is LD_PRELOADed, every child process (including the train worker) fails with libcudart.so.12: cannot open shared object file: No such file or directory.
The wheel actually ships a correct cu13 build right next to the selected one; the selection logic just never considers it.
Location — actor_group.py L64-L84:
if self.args.offload_train and self.args.train_backend == "megatron":
import torch_memory_saver
for path in [
"torch_memory_saver_hook_mode_preload_cu12.abi3.so",
"torch_memory_saver_hook_mode_preload.abi3.so",
]:
dynlib_path = os.path.join(
os.path.dirname(os.path.dirname(torch_memory_saver.__file__)),
path,
)
if os.path.exists(dynlib_path):
break
else:
raise FileNotFoundError(...)
env_vars["LD_PRELOAD"] = dynlib_path
Steps to Reproduce
- Environment: a CUDA 13 build of PyTorch (
torch 2.11.0+cu130) with a torch_memory_saver that ships CUDA-suffixed preload builds (0.0.9.post1: _cu12 / _cu13 / unsuffixed).
- Run any Megatron GRPO recipe with
--offload-train (the default offload path), e.g. the Qwen3-4B colocated recipe.
- Shortly after the SGLang rollout servers come up, the train worker crashes:
bash: error while loading shared libraries: libcudart.so.12: cannot open shared object file: No such file or directory
Deterministic minimal reproduction of the selection defect (no full job, no multi-GPU) — load each candidate the way LD_PRELOAD would:
import os, ctypes, torch, torch_memory_saver
base = os.path.dirname(os.path.dirname(torch_memory_saver.__file__))
print(torch.__version__, "| torch.version.cuda =", torch.version.cuda)
for name in [
"torch_memory_saver_hook_mode_preload_cu12.abi3.so", # current candidate[0]
"torch_memory_saver_hook_mode_preload.abi3.so", # current candidate[1]
"torch_memory_saver_hook_mode_preload_cu13.abi3.so", # correct, but not in the list
]:
p = os.path.join(base, name)
try:
ctypes.CDLL(p); load = "CDLL OK"
except OSError as e:
load = f"CDLL FAIL: {e}"
print(f" exists={os.path.exists(p)!s:5} {load} <- {name}")
Output on a CUDA 13 image:
2.11.0+cu130 | torch.version.cuda = 13.0
exists=True CDLL FAIL: libcudart.so.12: cannot open shared object file: No such file or directory <- ..._preload_cu12.abi3.so
exists=True CDLL FAIL: libcudart.so.12: cannot open shared object file: No such file or directory <- ..._preload.abi3.so
exists=True CDLL OK <- ..._preload_cu13.abi3.so
So os.path.exists passes for all three, slime picks the first (cu12), and it cannot load. The unsuffixed build is byte-identical to the cu12 build (same missing dependency), so the fallback cannot rescue a CUDA 13 host:
$ md5sum ..._preload.abi3.so ..._preload_cu12.abi3.so ..._preload_cu13.abi3.so
9e724677af9e636c1b3c3e910faef96d ..._preload.abi3.so
9e724677af9e636c1b3c3e910faef96d ..._preload_cu12.abi3.so # identical to the unsuffixed one
1bd3044ce135ab84e633f3db24b35965 ..._preload_cu13.abi3.so
$ ldd ..._preload_cu12.abi3.so | grep libcudart -> libcudart.so.12 => not found
$ ldd ..._preload_cu13.abi3.so | grep libcudart -> libcudart.so.13 => /usr/local/cuda/.../libcudart.so.13
Expected Behavior
On CUDA 13, LD_PRELOAD should point at the cu13 preload build (or fail with a clear, actionable error if none matches the runtime), and the train worker should start.
Actual Behavior
LD_PRELOAD points at a cu12 build that cannot load; every child process dies with libcudart.so.12: cannot open shared object file, and the train worker never starts. There is no message pointing at the CUDA-version mismatch.
Environment
- slime version: current
main (the defect is at slime/ray/actor_group.py L64-L84); the same bug is also present on the v0.2.4 tag.
- Python version: 3.12
- PyTorch version: 2.11.0+cu130
- CUDA/ROCm version: CUDA 13.0
- GPU type and count: NVIDIA H200, 2 nodes × 8 GPUs
- OS: Linux
- SGLang version (if relevant): 0.5.12.post1
- Megatron-LM version (if relevant): 3714d81 (train backend; the crash is on the
--offload-train megatron path)
- torch_memory_saver: 0.0.9.post1 (ships
_cu12 / _cu13 / unsuffixed preload builds and the get_binary_path_from_package resolver)
Logs
(SGLangEngine ...) The server is fired up and ready to roll! # rollout side comes up
(raylet, ip=...) bash: error while loading shared libraries: libcudart.so.12: cannot open shared object file: No such file or directory
Additional Context
Root cause. The selection decides on filename presence rather than CUDA-runtime match, and never enumerates the cu13 variant. torch_memory_saver already solves exactly this: torch_memory_saver/utils.py exposes get_binary_path_from_package(stem), which detects the CUDA major (torch.version.cuda first, then probing libcudart.so.<major> over (13, 12)) and returns the matching <stem>_cu<major>.*.so; the library uses it internally for this very stem in hooks/mode_preload.py. slime hard-codes a filename list instead of calling this resolver.
Suggested fix (PR attached). Delegate .so selection to torch_memory_saver's own CUDA-aware resolver when present, and otherwise fall back to a candidate list keyed on the detected CUDA major (torch.version.cuda, which needs no GPU/driver in the process — important because slime resolves this on the Ray driver, which may be a CPU-only head node). Note: slime's docker/Dockerfile pins torch_memory_saver@a193d9dd, which already includes the resolver, so the resolver path is what ships; the fallback keeps the fix correct against older/other pins.
Not a duplicate. I searched issues/PRs and found nothing covering this CUDA-major selection defect. Three nearby tickets touch the same area but are distinct root causes: #1936 / #1937 (an "invalid LD_PRELOAD" assertion under --disable-weights-backuper; #1937 edits the same actor_group.py block but changes whether the preload setup runs, not which .so is chosen — orthogonal), #1690 (cudaErrorInvalidValue after checkpoint save; a runtime-interception bug), and #1895 (CUresult error 1 in actor.sleep(); a double-sleep regression).
Pre-submission Checklist
Bug Description
With
--offload-trainon the Megatron backend,slime/ray/actor_group.pysetsLD_PRELOADto atorch_memory_saverpreload.so. It picks the file from a hard-coded list —..._preload_cu12.abi3.so, then the unsuffixed..._preload.abi3.so— and takes the first that exists on disk (os.path.exists).On a CUDA 13 image this is wrong twice over:
cu13variant is never enumerated.cu12.soexists as a file, so it is selected — but it linkslibcudart.so.12, which is absent on CUDA 13. Once it isLD_PRELOADed, every child process (including the train worker) fails withlibcudart.so.12: cannot open shared object file: No such file or directory.The wheel actually ships a correct
cu13build right next to the selected one; the selection logic just never considers it.Location — actor_group.py L64-L84:
Steps to Reproduce
torch 2.11.0+cu130) with atorch_memory_saverthat ships CUDA-suffixed preload builds (0.0.9.post1:_cu12/_cu13/ unsuffixed).--offload-train(the default offload path), e.g. the Qwen3-4B colocated recipe.Deterministic minimal reproduction of the selection defect (no full job, no multi-GPU) — load each candidate the way
LD_PRELOADwould:Output on a CUDA 13 image:
So
os.path.existspasses for all three, slime picks the first (cu12), and it cannot load. The unsuffixed build is byte-identical to thecu12build (same missing dependency), so the fallback cannot rescue a CUDA 13 host:Expected Behavior
On CUDA 13,
LD_PRELOADshould point at thecu13preload build (or fail with a clear, actionable error if none matches the runtime), and the train worker should start.Actual Behavior
LD_PRELOADpoints at acu12build that cannot load; every child process dies withlibcudart.so.12: cannot open shared object file, and the train worker never starts. There is no message pointing at the CUDA-version mismatch.Environment
main(the defect is atslime/ray/actor_group.pyL64-L84); the same bug is also present on thev0.2.4tag.--offload-trainmegatron path)_cu12/_cu13/ unsuffixed preload builds and theget_binary_path_from_packageresolver)Logs
Additional Context
Root cause. The selection decides on filename presence rather than CUDA-runtime match, and never enumerates the
cu13variant.torch_memory_saveralready solves exactly this:torch_memory_saver/utils.pyexposesget_binary_path_from_package(stem), which detects the CUDA major (torch.version.cudafirst, then probinglibcudart.so.<major>over(13, 12)) and returns the matching<stem>_cu<major>.*.so; the library uses it internally for this very stem inhooks/mode_preload.py. slime hard-codes a filename list instead of calling this resolver.Suggested fix (PR attached). Delegate
.soselection totorch_memory_saver's own CUDA-aware resolver when present, and otherwise fall back to a candidate list keyed on the detected CUDA major (torch.version.cuda, which needs no GPU/driver in the process — important because slime resolves this on the Ray driver, which may be a CPU-only head node). Note: slime'sdocker/Dockerfilepinstorch_memory_saver@a193d9dd, which already includes the resolver, so the resolver path is what ships; the fallback keeps the fix correct against older/other pins.Not a duplicate. I searched issues/PRs and found nothing covering this CUDA-major selection defect. Three nearby tickets touch the same area but are distinct root causes: #1936 / #1937 (an "invalid LD_PRELOAD" assertion under
--disable-weights-backuper; #1937 edits the sameactor_group.pyblock but changes whether the preload setup runs, not which.sois chosen — orthogonal), #1690 (cudaErrorInvalidValueafter checkpoint save; a runtime-interception bug), and #1895 (CUresult error 1inactor.sleep(); a double-sleep regression).Pre-submission Checklist