Fix O(#segments) per-comm memory probe that slows weight sync & training under expandable_segments - #2269
Open
yszhli wants to merge 1 commit into
Open
Fix O(#segments) per-comm memory probe that slows weight sync & training under expandable_segments#2269yszhli wants to merge 1 commit into
yszhli wants to merge 1 commit into
Conversation
The pre-call memory check in _wrap_low_level_call runs available_memory() before
every wrapped collective. available_memory() calls torch.cuda.memory_reserved()/
memory_allocated(), which build the full memory_stats() dict -- O(number of CUDA
caching-allocator segments). Under PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True
the segment count grows across steps, so this probe gets progressively more
expensive; it also calls cudaMemGetInfo, which synchronizes the driver and thus
serializes async collectives.
Weight sync issues one dist.broadcast(async_op=True) per parameter and one
dist.all_gather per expert bucket; training issues per-micro-batch TP all-gather
(_allgather_base) and grad all-reduce. All of these went through the probe because:
(a) they were registered with get_new_comm_function(fn) i.e. op_name=None, which
forces check_memory=True; and
(b) the low-level c10d method names (_allgather_base, allreduce, ...) dispatched
by ReloadableProcessGroup._fwd were not in the skip set.
Measured on Qwen3.5-35B-A3B (MoE + gated-delta-net) RL, 8xB300:
- weight sync (perf/update_weights_time) grew 143s -> 363s -> 966s -> 1596s across
steps; after the fix it is flat at ~7-20s;
- each training micro-batch dropped from ~14s to ~1.5s (a single ~700-token
fwd/bwd on a 3B-active MoE), i.e. the step went from ~57min to ~9min.
py-spy showed the actor pinned in mem_get_info / memory_reserved via
_wrap_low_level_call on those collectives. Free memory during both phases is tens
of GB, so the clear_memory() the probe guards never fires -- it is pure overhead.
Fix:
1. _wrap_low_level_call: use torch.cuda.mem_get_info() directly instead of
available_memory(), dropping the O(segments) memory_stats() call. Any comm op
still subject to the check no longer pays the expandable_segments blow-up.
2. Register the async collectives (all_reduce/all_gather/broadcast/reduce/all_to_all)
with explicit op_names, and list both the dist.* spellings and the c10d method
names (_allgather_base, allgather, allreduce, ...) in _COMM_MEMORY_CHECK_SKIP_OPS,
so the hot weight-sync / training collectives skip the probe entirely. The check
is retained for any op not listed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
yszhli
force-pushed
the
fix/per-comm-memory-probe-perf
branch
from
August 13, 2026 07:20
377cd4b to
ab839be
Compare
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.
Fixes #2268
Problem
slime/utils/reloadable_process_group.py::_wrap_low_level_callruns a memory probe before every wrapped collective:available_memory()callstorch.cuda.memory_reserved()/memory_allocated(), which build the fullmemory_stats()dict — O(number of CUDA caching-allocator segments). UnderPYTORCH_CUDA_ALLOC_CONF=expandable_segments:Truethe segment count grows across steps, so the probe gets progressively more expensive. It also callscudaMemGetInfo, which synchronizes the driver and serializes async collectives.This is on the hot path because weight sync issues one
dist.broadcast(param, async_op=True)per parameter (update_weights_from_distributed) plus onedist.all_gatherper expert bucket, and training issues per-micro-batch TP all-gather (_allgather_base) and grad all-reduce (allreduce). All of them hit the probe because:get_new_comm_function(fn)i.e.op_name=None, which forcescheck_memory=True(True if op_name is None else _should_check_memory_for_comm(op_name)); and_allgather_base,allreduce, …) dispatched byReloadableProcessGroup._fwdare not in_COMM_MEMORY_CHECK_SKIP_OPS.Free memory during both phases is tens of GB, so the
clear_memory()the probe guards never fires — the probe is pure overhead here.Impact (measured)
Qwen3.5-35B-A3B (MoE + gated-delta-net) async GRPO, 8×B300,
expandable_segments:True:perf/update_weights_timepy-spyon the training actor showed it pinned inmem_get_info/memory_reservedvia_wrap_low_level_call, first onupdate_weightsbroadcast and (after only fixing broadcast) on_allgather_base. The geometric growth (143→1596s) is the tell: a constant path cost can't grow like that; it'smemory_stats()walking an ever-growing segment list underexpandable_segments.Fix
_wrap_low_level_call: usetorch.cuda.mem_get_info()directly instead ofavailable_memory(), dropping the O(#segments)memory_stats()call. The free/clear decision only needs the driver-level free byte count.all_reduce/all_gather/broadcast/reduce/all_to_all) with explicitop_names, and list both thedist.*spellings and the c10d method names (_allgather_base,allgather,allreduce, …) in_COMM_MEMORY_CHECK_SKIP_OPS, so the hot weight-sync / training collectives skip the probe entirely. The check is retained for any op not listed.Both changes are safe: none of these collectives allocate significant new memory (they operate on existing tensors), so none benefit from a pre-call
clear_memory(), and the error-path memory dump in theexceptbranch is unchanged.