[Core] Release NCCL communicator memory in sleep mode#46234
Conversation
|
This pull request has merge conflicts that must be resolved before it can be |
666ecc1 to
73998ef
Compare
73998ef to
ba22cb6
Compare
Sleep mode offloads weights and discards the KV cache, but each PyNccl communicator keeps its dynamic GPU buffers resident the whole time (~958 MiB/GPU on an 8-GPU TP+EP worker). Release them with NCCL's native ncclCommSuspend(NCCL_SUSPEND_MEM) / ncclCommResume, which frees a communicator's allocations while keeping its topology/connection state, so resume is cheap (no re-init, no bootstrap rendezvous). Implemented along vLLM's existing NCCL conventions, no new module: - pynccl_wrapper.py: bind ncclCommSuspend/ncclCommResume in NCCLLibrary (skipped gracefully with a one-time warning on NCCL < 2.29.7). - pynccl.py: PyNcclCommunicator.suspend()/resume(), mirroring register_comm_window(); gated on disabled and nccl_version >= 22907. - parallel_state.py: suspend_nccl_comms()/resume_nccl_comms() iterate the _groups registry and act on each group's communicator. Collective and synchronous on return, so no extra device sync is needed. - gpu_worker.py: suspend after allocator.sleep(), resume after wake_up(). The symbols exist only in NCCL >= 2.29.7, so this is a no-op on torch 2.11.0 (NCCL 2.28.9) and activates automatically on torch >= 2.12.1 (NCCL 2.29.7), with no code change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: aoshen02 <aoshen@inferact.ai>
| freed = abs(free_before - torch.cuda.mem_get_info()[0]) | ||
| logger.info("NCCL %s: %d comms, %.1f MiB", label, len(comms), freed / 1024**2) |
There was a problem hiding this comment.
nit: I see why we're using abs here but potentially confusing and misleading especially if there's a bug
| freed = abs(free_before - torch.cuda.mem_get_info()[0]) | |
| logger.info("NCCL %s: %d comms, %.1f MiB", label, len(comms), freed / 1024**2) | |
| delta = torch.cuda.mem_get_info()[0] - free_before | |
| direction = "freed" if delta > 0 else "allocated" | |
| logger.info("NCCL %s: %d comms, %.1f MiB %s", | |
| label, len(comms), abs(delta) / 1024**2, direction) |
|
@aoshen02 clean approach, and ~958 MiB/GPU on 8-GPU TP+EP is a real win. ncclCommSuspend(NCCL_SUSPEND_MEM) keeping topology so resume skips re-init is exactly right. heads up this and #44074 both restructure the gpu_worker sleep/wake block: #44074 routes sleep()/wake_up() through a pluggable SleepModeBackend, and your suspend_nccl_comms() lands right in it. +1 to @tlrmchlsmth on moving it after torch.accelerator.synchronize(); that ordering (sync, then release NCCL mem) is exactly the guarantee the backend dispatch needs to keep, so worth pinning where the NCCL suspend sits relative to the backend's suspend/resume so both land cleanly. happy to coordinate. side note: this gives preserves_nccl a useful middle state — cumem suspends NCCL memory but keeps topology, distinct from cuda_checkpoint tearing it down. |
Address review feedback: replace abs() in the sleep-mode NCCL log with a signed delta plus an explicit freed/allocated direction, so a wrong-sign result (e.g. an accidental allocation during suspend) is surfaced instead of masked. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: aoshen02 <aoshen@inferact.ai>
Thanks, I missed some context, will take a look right now. |
tlrmchlsmth
left a comment
There was a problem hiding this comment.
pre-commit is failing:
vllm/distributed/parallel_state.py:152: error: Found torch.cuda API call. Please refer RFC https://github.com/vllm-project/vllm/issues/30679, use torch.accelerator API instead.
| # Release idle NCCL communicator memory (NCCL >= 2.29.7). Collective | ||
| # across ranks; no-op at world_size 1 or on older NCCL. Done after the | ||
| # cumem unmap so the reported freed bytes include the NCCL release. | ||
| suspend_nccl_comms() |
There was a problem hiding this comment.
we actually have some ongoing work that will allow flashinfer's comms to be suspended as well (see flashinfer-ai/flashinfer#3727), (flashinfer-ai/flashinfer#3745). Could we have a more generic suspend/resume surface in device_communicators, that each communicator can override?
There was a problem hiding this comment.
Nice catch, will do.
fix |
Per review on vllm-project#46234: instead of parallel_state reaching into device_communicator.pynccl_comm, add suspend()/resume() to DeviceCommunicatorBase (default no-op) and override in CudaCommunicator to forward to its pynccl_comm. parallel_state now walks device communicators polymorphically (suspend_device_comms/resume_device_comms), so FlashInfer / all2all comms can add their own suspend later without touching parallel_state. Non-CUDA backends inherit the base no-op. The freed-bytes probe also moves to the torch.accelerator memory API. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: aoshen02 <aoshen@inferact.ai>
Signed-off-by: aoshen02 <aoshen@inferact.ai>
Signed-off-by: aoshen02 <aoshen@inferact.ai>
|
This pr rely on #45321 |
What
Sleep mode (
CuMemAllocator) already offloads weights and discards the KV cachewhile an engine sleeps, but each PyNccl communicator keeps its dynamic GPU
buffers resident the whole time. On an 8-GPU TP+EP deployment that is
~958 MiB/GPU sitting idle through a sleep/wake cycle (multi-model hot-swap,
RL weight sync, etc.).
This PR releases that memory with NCCL's native
ncclCommSuspend(NCCL_SUSPEND_MEM)/ncclCommResume(NCCL ≥ 2.29.7), whichfrees a communicator's allocations while keeping its topology and connection
state, so resume is cheap — no re-init, no bootstrap rendezvous.
How
Implemented along vLLM's existing NCCL conventions — no new module:
pynccl_wrapper.py— addncclCommSuspend/ncclCommResumetoNCCLLibrary.exported_functions+ wrapper methods. They exist only inNCCL ≥ 2.29.7, so the binding skips them gracefully (one-time warning) on
older NCCL.
pynccl.py—PyNcclCommunicator.suspend()/resume(), mirroring theexisting
register_comm_window(); each guards ondisabledandnccl_version >= 22907.parallel_state.py—suspend_nccl_comms()/resume_nccl_comms()iterate the authoritative
_groupsregistry and call.suspend()/.resume()on each group's NCCL communicator. Collective (internal cross-rank barrier),
and synchronous on return (verified against NCCL
mem_manager.cc), so noextra device sync is needed.
gpu_worker.py—suspend_nccl_comms()afterallocator.sleep()(so thereported freed bytes include the NCCL release) and
resume_nccl_comms()afterallocator.wake_up(), before any collective runs again.NCCL version behavior (graceful, auto-activating)
The suspend/resume symbols are NCCL ≥ 2.29.7 only. vLLM does not pin NCCL
directly — it arrives via the
torchwheel:requirements/cuda.txt)So on stock images today it is a safe no-op, and it activates automatically
once vLLM's CUDA
torchpin moves to ≥ 2.12.1 — no code change required.Why this is not a duplicate
the comm alive and only releases its buffers (
ncclCommSuspend), avoiding there-init / re-bootstrap cost.
This is the NCCL half of #45623, split out as a focused, self-contained change:
no dependency on the CUDA-graph offload machinery, and it applies to every
sleep-mode config (the graph-offload piece has a MoE/EP coverage gap and lands
separately).
Tests
Validated end-to-end on 8×H200 (image
vllm/vllm-openai:v0.24.0-ubuntu2404,torch 2.11.0+cu130 with NCCL upgraded to 2.30.7), driving real
vllm serve --enable-sleep-mode→POST /sleep?level=1→/wake_up, measuringthe free-memory delta around the actual
ncclCommSuspendcall on each rank.Parallel-topology sweep (Qwen3-30B-A3B):
100B+ scale — Qwen3-235B-A22B-FP8, TP8+EP, 8×H200: 2 comms/rank,
960 MiB/rank freed