[Bug] `offload_train` train worker dies on CUDA 13 with `libcudart.so.12: cannot open shared object file` — the torch_memory_saver `LD_PRELOAD` `.so` is chosen by filename, not by CUDA runtime
Nobody has claimed this yet.
- Dominant language
- Python
- Stars
- 8.5k
- Forks
- 1.3k
- Avg merge
- 5h 36m
- Merged PRs (30d)
- 22
Description
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
cu13variant is never enumerated. - The test is file existence, not loadability. The
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 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 atorch_memory_saverthat 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 atslime/ray/actor_group.pyL64-L84); the same bug is also present on thev0.2.4tag. - 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-trainmegatron path) - torch_memory_saver: 0.0.9.post1 (ships
_cu12/_cu13/ unsuffixed preload builds and theget_binary_path_from_packageresolver)
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
- I have read the CONTRIBUTING.md and understand the collaboration scope.
- I have read the documentation and my issue is not addressed there.
- I have searched for existing issues and this is not a duplicate.
- I have provided a minimal, reproducible example.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in slime/ray/actor_group.py at the offload-train LD_PRELOAD selection, then read torch_memory_saver/utils.py and hooks/mode_preload.py to understand the available CUDA-aware resolver. Run the minimal ctypes.CDLL reproduction from the issue on CUDA 12 and CUDA 13 environments. Done means the selected preload matches the detected CUDA major, or a clear error is raised when no matching library exists.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python, pytorch
- Domain
- backend, infrastructure
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 72/100