Repository navigation
[PyTorch] Share CUDA graph memory across dynamic CP variants - #3353
xiaoyao0115 wants to merge 6 commits into
Conversation
e1cfcd3 to
c095214
Compare
3710b66 to
de59f96
Compare
560c2d3 to
da564cf
Compare
da564cf to
a0a7dad
Compare
Dynamic context parallelism (DCP) picks a CP size for every microbatch, and CP communicators are capture-time constants. TECudaGraphHelper therefore captures one graph bank per power-of-two CP size, from --min-dynamic-context-parallel-size to DP x CP, each with the real process group of that size. All banks share one TE memory pool through make_graphed_callables(_graph_memory_slots=...) (NVIDIA/TransformerEngine#3353), so graph memory does not grow with the number of banks. - Slot plan: the physical slot ring is sized from the liveness union of every PP/VPP order the DCP scheduler can produce. Saved-tensor and returned-gradient arenas are colored so that frames that are live at the same time never share storage. - Replay: a layer selects its bank (graphs, process group, static inputs) from PackedSeqParams.local_cp_size, including the mHC attention split and the hybrid wrapper. Router-only captures get a DCP PackedSeqParams so they use the bank's group. - Capture state: THD capture seeds a bounded RoPE table and cu_seqlens per bank, and eager RoPE uses the same bound under DCP. During training the packed microbatch count is checked against the captured range, and the DCP scheduler honors thd_max_packed_sequences, so a pack never exceeds the captured cu_seqlens capacity. Capture can be retried, and capture-only state is released on failure and on delete. - Config rules: DCP with CUDA graphs requires TE graphs and power-of-two DP x CP, and rejects delayed wgrad, EP overlap, moe_paged_stash and mamba scopes. Runs without DCP keep dev behavior. Co-authored-by: HaochenYuan <haocheny@nvidia.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tailai Ma <tailaim@nvidia.com>
27c0fff to
8980cd0
Compare
Dynamic context parallelism (DCP) picks a CP size for every microbatch, and CP communicators are capture-time constants. TECudaGraphHelper therefore captures one graph bank per power-of-two CP size, from --min-dynamic-context-parallel-size to DP x CP, each with the real process group of that size. All banks share one TE memory pool through make_graphed_callables(_graph_memory_slots=...) (NVIDIA/TransformerEngine#3353), so graph memory does not grow with the number of banks. - Slot plan: the physical slot ring is sized from the liveness union of every PP/VPP order the DCP scheduler can produce. Saved-tensor and returned-gradient arenas are colored so that frames that are live at the same time never share storage. - Replay: a layer selects its bank (graphs, process group, static inputs) from PackedSeqParams.local_cp_size, including the mHC attention split and the hybrid wrapper. Router-only captures get a DCP PackedSeqParams so they use the bank's group. - Capture state: THD capture seeds a bounded RoPE table and cu_seqlens per bank, and eager RoPE uses the same bound under DCP. During training the packed microbatch count is checked against the captured range, and the DCP scheduler honors thd_max_packed_sequences, so a pack never exceeds the captured cu_seqlens capacity. Capture can be retried, and capture-only state is released on failure and on delete. - Config rules: DCP with CUDA graphs requires TE graphs and power-of-two DP x CP, and rejects delayed wgrad, EP overlap, moe_paged_stash and mamba scopes. Runs without DCP keep dev behavior. Co-authored-by: HaochenYuan <haocheny@nvidia.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tailai Ma <tailaim@nvidia.com>
Dynamic context parallelism (DCP) picks a CP size for every microbatch, and CP communicators are capture-time constants. TECudaGraphHelper therefore captures one graph bank per power-of-two CP size, from --min-dynamic-context-parallel-size to DP x CP, each with the real process group of that size. All banks share one TE memory pool through make_graphed_callables(_graph_memory_slots=...) (NVIDIA/TransformerEngine#3353), so graph memory does not grow with the number of banks. - Slot plan: the physical slot ring is sized from the liveness union of every PP/VPP order the DCP scheduler can produce. Saved-tensor and returned-gradient arenas are colored so that frames that are live at the same time never share storage. - Replay: a layer selects its bank (graphs, process group, static inputs) from PackedSeqParams.local_cp_size, including the mHC attention split and the hybrid wrapper, and checks that the microbatch's runtime CP group is the one the bank captured. A CP-off microbatch may carry its singleton group in cp_group or in cp_singleton_group. Router-only captures get a DCP PackedSeqParams so they use the bank's group. - Capture state: THD capture seeds a bounded RoPE table and cu_seqlens per bank, and eager RoPE uses the same bound under DCP. During training the packed microbatch count is checked against the captured range, and the DCP scheduler honors thd_max_packed_sequences, so a pack never exceeds the captured cu_seqlens capacity. Capture can be retried, and capture-only state is released on failure and on delete. - Config rules: DCP with CUDA graphs requires TE graphs and power-of-two DP x CP, and rejects delayed wgrad, EP overlap, moe_paged_stash and mamba scopes. Runs without DCP keep dev behavior apart from fixes on the paths they share: - The metadata-only THD padding path (PP stages without tokens) derives its padding target from the CP-local length, as the tensor path already does. - With CP=1 the dp_balanced scheduler gathers on the DPxCP communicator, which has the same ranks in the same order as DP. - Packed THD capture with fixed CP>1 seeds the full RoPE table and graph cu_seqlens instead of the CP-sliced table. - After an attention-only graph replay, the eager MLP receives the real PackedSeqParams. - PP stages without graphable layers still join the all-reduce that sizes the dynamic microbatch slot ring, so stages with graphs do not wait for them. Co-authored-by: HaochenYuan <haocheny@nvidia.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tailai Ma <tailaim@nvidia.com>
Dynamic context parallelism (DCP) picks a CP size for every microbatch, and CP communicators are capture-time constants. TECudaGraphHelper therefore captures one graph bank per power-of-two CP size, from --min-dynamic-context-parallel-size to DP x CP, each with the real process group of that size. All banks share one TE memory pool through make_graphed_callables(_graph_memory_slots=...) (NVIDIA/TransformerEngine#3353), so graph memory does not grow with the number of banks. - Slot plan: the physical slot ring is sized from the liveness union of every PP/VPP order the DCP scheduler can produce. Saved-tensor and returned-gradient arenas are colored so that frames that are live at the same time never share storage. - Replay: a layer selects its bank (graphs, process group, static inputs) from PackedSeqParams.local_cp_size, including the mHC attention split and the hybrid wrapper, and checks that the microbatch's runtime CP group is the one the bank captured. A CP-off microbatch may carry its singleton group in cp_group or in cp_singleton_group. Router-only captures get a DCP PackedSeqParams so they use the bank's group. - Capture state: THD capture seeds a bounded RoPE table and cu_seqlens per bank, and eager RoPE uses the same bound under DCP. During training the packed microbatch count is checked against the captured range, and the DCP scheduler honors thd_max_packed_sequences, so a pack never exceeds the captured cu_seqlens capacity. Capture can be retried, and capture-only state is released on failure and on delete. - Config rules: DCP with CUDA graphs requires TE graphs and power-of-two DP x CP, and rejects delayed wgrad, EP overlap, moe_paged_stash and mamba scopes. Runs without DCP keep dev behavior apart from fixes on the paths they share: - The metadata-only THD padding path (PP stages without tokens) derives its padding target from the CP-local length, as the tensor path already does. - With CP=1 the dp_balanced scheduler gathers on the DPxCP communicator, which has the same ranks in the same order as DP. - Packed THD capture with fixed CP>1 seeds the full RoPE table and graph cu_seqlens instead of the CP-sliced table. - After an attention-only graph replay, the eager MLP receives the real PackedSeqParams. - PP stages without graphable layers still join the all-reduce that sizes the dynamic microbatch slot ring, so stages with graphs do not wait for them. Co-authored-by: HaochenYuan <haocheny@nvidia.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tailai Ma <tailaim@nvidia.com>
[Medium impact] The changes affect PyTorch CUDA graph capture, especially callers using shared slots. Fix gradient copying for weights shared between fused and ordinary operations before merging. Findings
SummaryAdds private CUDA graph slots so mutually exclusive CP variants can share one memory pool.
Diagram%%{init: {'theme': 'neutral'}}%%
flowchart LR
A[CP graph variants] --> P[Shared CUDA memory pool]
P --> F[Forward replay]
F --> S[Saved tensors and outputs in slot arenas]
S --> B[Matching backward replay]
B --> G[Returned gradients]
B --> R[Slot available for reuse]
Reviews (4) · Last reviewed commit: "Check layouts of module-state outputs ac..." · Reviewed by Greptile |
Dynamic context parallelism (DCP) picks a CP size for every microbatch, and CP communicators are capture-time constants. TECudaGraphHelper therefore captures one graph bank per power-of-two CP size, from --min-dynamic-context-parallel-size to DP x CP, each with the real process group of that size. All banks share one TE memory pool through make_graphed_callables(_graph_memory_slots=...) (NVIDIA/TransformerEngine#3353), so graph memory does not grow with the number of banks. - Slot plan: the physical slot ring is sized from the liveness union of every PP/VPP order the DCP scheduler can produce. Saved-tensor and returned-gradient arenas are colored so that frames that are live at the same time never share storage. - Replay: a layer selects its bank (graphs, process group, static inputs) from PackedSeqParams.local_cp_size, including the mHC attention split and the hybrid wrapper, and checks that the microbatch's runtime CP group is the one the bank captured. A CP-off microbatch may carry its singleton group in cp_group or in cp_singleton_group. Router-only captures get a DCP PackedSeqParams so they use the bank's group. - Capture state: THD capture seeds a bounded RoPE table and cu_seqlens per bank, and eager RoPE uses the same bound under DCP. During training the packed microbatch count is checked against the captured range, and the DCP scheduler honors thd_max_packed_sequences, so a pack never exceeds the captured cu_seqlens capacity. Capture can be retried, and capture-only state is released on failure and on delete. - Config rules: DCP with CUDA graphs requires TE graphs and power-of-two DP x CP, and rejects delayed wgrad, EP overlap, moe_paged_stash and mamba scopes. Runs without DCP keep dev behavior apart from fixes on the paths they share: - The metadata-only THD padding path (PP stages without tokens) derives its padding target from the CP-local length, as the tensor path already does. - With CP=1 the dp_balanced scheduler gathers on the DPxCP communicator, which has the same ranks in the same order as DP. - Packed THD capture with fixed CP>1 seeds the full RoPE table and graph cu_seqlens instead of the CP-sliced table. - After an attention-only graph replay, the eager MLP receives the real PackedSeqParams. - PP stages without graphable layers still join the all-reduce that sizes the dynamic microbatch slot ring, so stages with graphs do not wait for them. Co-authored-by: HaochenYuan <haocheny@nvidia.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tailai Ma <tailaim@nvidia.com>
70d2a6f to
cc7f709
Compare
Dynamic context parallelism (DCP) picks a CP size for every microbatch, and CP communicators are capture-time constants. TECudaGraphHelper therefore captures one graph bank per power-of-two CP size, from --min-dynamic-context-parallel-size to DP x CP, each with the real process group of that size. All banks share one TE memory pool through make_graphed_callables(_graph_memory_slots=...) (NVIDIA/TransformerEngine#3353), so graph memory does not grow with the number of banks. - Slot plan: the physical slot ring is sized from the liveness union of every PP/VPP order the DCP scheduler can produce. Saved-tensor and returned-gradient arenas are colored so that frames that are live at the same time never share storage. - Replay: a layer selects its bank (graphs, process group, static inputs) from PackedSeqParams.local_cp_size, including the mHC attention split and the hybrid wrapper, and checks that the microbatch's runtime CP group is the one the bank captured. A CP-off microbatch may carry its singleton group in cp_group or in cp_singleton_group. Router-only captures get a DCP PackedSeqParams so they use the bank's group. - Capture state: THD capture seeds a bounded RoPE table and cu_seqlens per bank, and eager RoPE uses the same bound under DCP. During training the packed microbatch count is checked against the captured range, and the DCP scheduler honors thd_max_packed_sequences, so a pack never exceeds the captured cu_seqlens capacity. Capture can be retried, and capture-only state is released on failure and on delete. - Config rules: DCP with CUDA graphs requires TE graphs and power-of-two DP x CP, and rejects delayed wgrad, EP overlap, moe_paged_stash and mamba scopes. Runs without DCP keep dev behavior apart from fixes on the paths they share: - The metadata-only THD padding path (PP stages without tokens) derives its padding target from the CP-local length, as the tensor path already does. - With CP=1 the dp_balanced scheduler gathers on the DPxCP communicator, which has the same ranks in the same order as DP. - Packed THD capture with fixed CP>1 seeds the full RoPE table and graph cu_seqlens instead of the CP-sliced table. - After an attention-only graph replay, the eager MLP receives the real PackedSeqParams. - PP stages without graphable layers still join the all-reduce that sizes the dynamic microbatch slot ring, so stages with graphs do not wait for them. Co-authored-by: HaochenYuan <haocheny@nvidia.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tailai Ma <tailaim@nvidia.com>
Add a private `_graph_memory_slots` argument to `make_graphed_callables`. Each callable gets a seven-field entry (saved_arena, io_slot, branch, chunk, layer, warmup_group, grad_arena), which lets mutually exclusive graph variants, such as Megatron-LM's per-CP-size graph banks, capture into one `torch.cuda.MemPool`: - Forward replays snapshot the tensors saved for backward into liveness-colored arenas, so a replay of another variant cannot overwrite them before the matching backward. Native saved tensors are grouped by storage identity, and aliases and versions are preserved. - Branches of one slot share output and returned-gradient storage, so each arena is sized by the largest branch rather than the sum. - During slot capture, quantized tensors other than DistributedWeights are decomposed into their physical storages and rebuilt for backward. Fused-wgrad parameters stay autograd inputs on replay, with a zeroed placeholder gradient when they lack `grad_added_to_main_grad`. - Delayed weight-gradient `_order` entries are rejected with slots, because only tensors saved for backward are protected. - `NVTE_CUDA_GRAPH_SLOT_DEBUG=1` adds a host-side check that no other live graph owns the arena or output family a replay reuses. Without slots, capture behaves as before, except that a failed capture now restores GC state, temporary hooks, allocator settings and `.grad` state, and `make_weak_ref` keeps the strides of non-contiguous views. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tailai Ma <tailaim@nvidia.com>
Slot arenas protect only tensors saved for backward, so state that attention backward read from ctx could be overwritten by another graph variant's replay. - FP8 attention saves its quantizer scale snapshots through autograd in FusedAttnFunc and in the CP P2P, all-gather and A2A paths; backward rebinds the restored scales. Like FusedAttnFunc, the CP paths snapshot every Float8Quantizer scale, so S also keeps its forward scale under current scaling. - THD all-gather saves its per-step cu_seqlens and cu_seqlens_kv_padded the same way. - Captured offload: scopes may use _graph_memory_offload_context instead of nesting another saved-tensor hook. Selected saves go to pinned host arenas colored like the GPU saved arenas, forward and backward graphs capture the D2H and H2D copies, and reloaded views keep their aliases. If any view of a storage stays on GPU, all of them do. - Under active CPU offload v1, quantized wrappers are saved through their physical buffers so a CPU copy keeps the FP8 data and scales; DistributedWeights are still saved by reference. - Outputs that overlap themselves, such as expanded views, are copied into the arena by storage span with a pass-through backward, and branches of one slot must match in shape, stride, dtype, device and autograd state. - tests/pytorch/attention/test_saved_quantizer_scales.py covers the saved scales, the v1 offload buffers and FP8 attention replay (delayed and current scaling) with frames overlapping within and across graph banks. L0 runs its single-process cases; L1 runs the CP cases under torchrun. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tailai Ma <tailaim@nvidia.com>
When offload uses a separate D2H stream, the copy only waits for compute up to the save, so a later in-place write in the same forward could change the bytes it reads; record_stream protects lifetime, not values. Each offloaded save is now first copied on the compute stream and the D2H copy reads that snapshot. Same-stream offload is unchanged. The snapshots stay in the graph pool until the end of the forward graph. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tailai Ma <tailaim@nvidia.com>
…r CP AttnFuncWithCPAndKVAllGather saves the per-step THD Q cu_seqlens through prepare_for_saving, and backward unpacks two steps of each. The NO_LOAD_BALANCE THD path computes only one step, so forward saved two objects fewer than backward unpacked and eager training failed with a ValueError (FP8 and CUDA graph capture are already rejected for NO_LOAD_BALANCE). Forward now pads the missing step with None so the saved layout is the same for every load-balancing strategy. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tailai Ma <tailaim@nvidia.com>
Branches with equal (io_slot, chunk, layer) are documented to share output and gradient storage, but output targets are carved from each branch's saved arena and returned gradients from its grad arena. make_graphed_callables now rejects branches of one slot that name different saved or grad arenas instead of silently giving them separate storage. Also document the d2h_stream and h2d_stream arguments of _graph_memory_offload_context and the snapshot copies that asynchronous offload keeps in the graph pool, and add docstrings to _CopyOverlappingOutput for pylint. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tailai Ma <tailaim@nvidia.com>
Outputs that view module parameters or buffers stay in the module's storage, and the branch family check skipped them, so branches of one slot could return such views with different shapes or strides although _graph_memory_slots requires identical output layouts. They are now compared like arena outputs; only their storage may differ. The docstring states that exception, and the layout-rejection test adds a module-state case. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tailai Ma <tailaim@nvidia.com>
Dynamic context parallelism (DCP) picks a CP size for every microbatch, and CP communicators are capture-time constants. TECudaGraphHelper therefore captures one graph bank per power-of-two CP size, from --min-dynamic-context-parallel-size to DP x CP, each with the real process group of that size. All banks share one TE memory pool through make_graphed_callables(_graph_memory_slots=...) (NVIDIA/TransformerEngine#3353), so graph memory does not grow with the number of banks. - Slot plan: the physical slot ring is sized from the liveness union of every PP/VPP order the DCP scheduler can produce. Saved-tensor and returned-gradient arenas are colored so that frames that are live at the same time never share storage. - Replay: a layer selects its bank (graphs, process group, static inputs) from PackedSeqParams.local_cp_size, including the mHC attention split and the hybrid wrapper, and checks that the microbatch's runtime CP group is the one the bank captured. A CP-off microbatch may carry its singleton group in cp_group or in cp_singleton_group. Router-only captures get a DCP PackedSeqParams so they use the bank's group. - Capture state: THD capture seeds a bounded RoPE table and cu_seqlens per bank, and eager RoPE uses the same bound under DCP. During training the packed microbatch count is checked against the captured range, and the DCP scheduler honors thd_max_packed_sequences, so a pack never exceeds the captured cu_seqlens capacity. Capture can be retried, and capture-only state is released on failure and on delete. - Config rules: DCP with CUDA graphs requires TE graphs and power-of-two DP x CP, and rejects delayed wgrad, EP overlap, moe_paged_stash and mamba scopes. Runs without DCP keep dev behavior apart from fixes on the paths they share: - The metadata-only THD padding path (PP stages without tokens) derives its padding target from the CP-local length, as the tensor path already does. - With CP=1 the dp_balanced scheduler gathers on the DPxCP communicator, which has the same ranks in the same order as DP. - Packed THD capture with fixed CP>1 seeds the full RoPE table and graph cu_seqlens instead of the CP-sliced table. - After an attention-only graph replay, the eager MLP receives the real PackedSeqParams. - PP stages without graphable layers still join the all-reduce that sizes the dynamic microbatch slot ring, so stages with graphs do not wait for them. Co-authored-by: HaochenYuan <haocheny@nvidia.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tailai Ma <tailaim@nvidia.com>
cc7f709 to
15fe37e
Compare
| clone_param_grads_on_return | ||
| and param not in fused_wgrad_params | ||
| and not getattr(param, "skip_backward_post_hook", False) |
There was a problem hiding this comment.
Shared weight gradients get overwritten
With slots enabled, a weight used by both a fused-wgrad TE layer and an ordinary PyTorch operation gets a real returned gradient from the ordinary operation. For example, a module can add self.linear(inp) to torch.nn.functional.linear(inp, self.linear.weight).
This check skips cloning that real gradient because the weight belongs to fused_wgrad_params, even with clone_param_grads_on_return=True. Replay returns the captured buffer directly, so the next backward overwrites retained gradients and can corrupt .grad accumulation. Skip cloning only for actual dummy gradients, not every weight used by a fused-wgrad layer.
Description
Megatron's dynamic context parallelism (NVIDIA/Megatron-LM#6479) captures one CUDA graph bank per context-parallel (CP) size, because CP communicators are capture-time constants. Giving each bank its own memory pool multiplies graph memory by the number of banks.
This PR adds a private
_graph_memory_slotsargument tomake_graphed_callables. It lets mutually exclusive graph variants share onetorch.cuda.MemPool. FP8 attention and fine-grained activation offloading also work inside these graphs:_graph_memory_offload_contextlets a captured scope offload selected saved tensors to pinned host arenas.Without
_graph_memory_slots, graph capture behaves as before, apart from the error-path cleanup (change 3) and themake_weak_reffix (change 5). Two other changes also apply outside graphs, including eager training:prepare_for_saving(change 7). Under current scaling the CP paths now also snapshot the S scale, asFusedAttnFuncalready did; other values are unchanged.The branch is rebased onto current
main(1f251a1) and has six commits, listed below.Type of change
make_weak_refstrides (change 5); FP8 wrappers saved under v1 CPU offload hooks (change 2)Changes
1. Slot memory (
graph.py)Each graph input carries
(saved_arena, io_slot, branch, chunk, layer, warmup_group, grad_arena):saved_arenaholds the tensors the forward saves for backward, plus its public outputs.grad_arenaholds the gradients returned for user inputs. The caller (Megatron) assigns arenas so that two inputs share one only if their forward-to-backward lifetimes never overlap in any schedule it replays.Inputs with equal
(io_slot, chunk, layer)are branches of one slot. They share output and gradient storage, so each arena is sized by the largest branch rather than the sum. Branches must:saved_arenaandgrad_arena;requires_grad, addressed bytes and alias topology). Outputs that view module parameters or buffers stay in the module's storage, but their layouts must match too.make_graphed_callablesrejects branches that break either rule.Warmup runs once per
warmup_group.Forward replays snapshot saved tensors into the arenas, so a replay of another variant cannot overwrite them before the matching backward. Native saved tensors are grouped by storage identity, and aliases and versions are preserved. Public outputs that may overlap internally, such as
expandviews, are copied by their addressed storage span. The gradient passes through to the source view unchanged.Contract: only tensors saved for backward (
ctx.save_for_backwardor saved-tensor hooks) are protected. For that reason, delayed weight-gradient_orderentries are rejected when slots are used. Fused-wgrad parameters remain autograd inputs on replay. When a parameter lacksgrad_added_to_main_grad, its placeholder gradient is zeroed, because autograd accumulates it into.grad.2. Quantized tensors (
quantized_tensor.py)prepare_for_savingdecomposes quantized wrappers (QuantizedTensorStorage,GroupedTensorStoragesubclasses) into their physical storages in two cases:NVTE_CPU_OFFLOAD_V1andis_cpu_offload_enabled()).restore_from_savedrebuilds the wrappers in backward. A hook that copies a wrapper by its nominal dtype would otherwise lose the FP8 data. All other paths save as before.3. Capture error paths (
graph.py)If capture fails, GC state, temporary hooks, allocator settings and
.gradstate are restored. This also applies without slots.4. Debug guard
With
NVTE_CUDA_GRAPH_SLOT_DEBUG=1, each replay checks on the host that no other live graph owns the arena or output family it is about to reuse. The variable is documented indocs/envvars.rst.5.
make_weak_ref(utils.py)make_weak_refnow keeps the strides of non-contiguous views. Previously, transposed or expanded static outputs were reinterpreted as row-major after the reuse block dropped their strong references.6. Activation offloading in slot graphs (
graph.py)_graph_memory_offload_context(checker, *, d2h_stream=None, h2d_stream=None)is a private context manager. Inside a captured scope, the existing slot saved-tensor hook offloads the saves thatcheckerselects, so no second hook pair is installed.d2h_streamand joins them before the forward ends. The backward graph reloads each storage group onh2d_streamwhen it is first unpacked.d2h_streamdiffers from the current stream, each offloaded save is first copied (its addressed span) on the current stream, and the D2H reads that copy. Without the copy, a later in-place write in the same forward could change the values before the D2H reads them.7. FP8 attention state in saved tensors (
backends.py,context_parallel.py)Backward used to read FP8 scale snapshots and THD all-gather metadata from plain
ctxattributes. Slot graphs do not protect those, so another variant's replay could overwrite them.FusedAttnFuncsaves the S quantizer scale (Float8Quantizer) throughprepare_for_savingand rebinds it in backward._prepare_backward_quantizers, and_restore_backward_quantizersrebinds them in backward. LikeFusedAttnFunc, they snapshot everyFloat8Quantizerscale: QKV/O/S under delayed scaling and S under current scaling, whose S/dP use a delayed sub-recipe. Before, the CP paths snapshotted only under a delayed global recipe, so CP backward under current scaling read the live S scale.cu_seqlens, their padded variant, andcu_seqlens_kv_padded. The saved layout always holds two steps.NO_LOAD_BALANCEcomputes only one step, so it savesNonefor the second.Possible split
If reviewers prefer, changes 3 (error-path cleanup), 5 (
make_weak_ref), 2 (quantized decomposition), 7 (attention state) and 4 (debug guard) can each become a separate PR ahead of changes 1 and 6.Commits
On
main1f251a1:83d11ca3[PyTorch] Share CUDA graph memory across graph variants with slot arenas: changes 1–5.485b1a8eProtect slot graph outputs and captured attention state: changes 6 and 7, the v1-offload part of change 2, overlapping public outputs and full branch-layout checks (change 1), andtests/pytorch/attention/test_saved_quantizer_scales.py(wired into L0 and L1).9fc2372fSnapshot saved activations before asynchronous graph offload (change 6).58c37155Keep the two-step THD Q metadata layout for NO_LOAD_BALANCE all-gather CP. This fixes aNO_LOAD_BALANCETHD all-gather backward failure that commit 2 introduced, with or without graphs.ec656d35Require slot branches to share arenas and document async offload copies: the arena-ID check (change 1), docstrings and pylint fixes.15fe37e0Check layouts of module-state outputs across slot branches (change 1).Review follow-up
485b1a8e.485b1a8e, extended to module-state outputs in15fe37e0.9fc2372f.ec656d35._CopyOverlappingOutput:ec656d35.Testing
Tests in this PR:
tests/pytorch/test_cuda_graphs.pyadds slot tests. They cover:none/sync/async. The async case delays the D2H stream withtorch.cuda._sleep.tests/pytorch/attention/test_saved_quantizer_scales.py(new) covers:test_fp8_attention_slot_replay_matches_eager_with_overlapping_frames, which runs FP8 DPA with CP1 and CP2 banks sharing slots. Frames overlap within each bank (slot wrap) and across banks, and it covers {delayed, current scaling} × {SBHD, THD} × {no CP, P2P, A2A, all-gather} × offload off/on against eager.The CP cases need 2 or 4 ranks;
test_cp_cases_with_two_rankslaunches them under torchrun. L0 runs the single-process cases and L1 runs the CP cases. THD cases need SM100+ and cuDNN 9.23+.The
NO_LOAD_BALANCElayout (commit 4) is covered by the existingtests/pytorch/attention/test_attention_with_cp.py::test_cp_with_{fused,flash}_attention_no_load_balance(L1).Results at
15fe37e0(NGC PyTorch 26.03 containers, cuDNN Frontend 1.32.2):test_cuda_graphs.py, whole file with the L0 environment (run twice)test_saved_quantizer_scales.py, L0 command (-k "not two_ranks")test_saved_quantizer_scales.py, L1 command (two-rank CP cases)test_saved_quantizer_scales.py, 4 rankstest_attention_with_cp.py -k "all_gather or no_load_balance"-k preserves_versions_of_mutated_saved_storagewithgraph.pybefore9fc2372f[async]fails; all pass at head[async]fails; all pass at head-k fused_attention_no_load_balancewithcontext_parallel.pybefore58c37155test_cpu_offloading.py,test_cpu_offloading_v1.py,test_quantized_tensor.py,test_distributed_weight.py,test_attention.py -k fp8,test_kv_cache.py,test_attention_with_cp.py -k fp8,test_torch_compile.py -k dpa_torch_compile, compared test by test withmain1f251a12maindoes not also haveOn H100, the FP8 THD cases skip for hardware reasons. Black 24.4.2 (repository pre-commit arguments) and pylint 3.3.1 (
pylintrc) pass locally on the changed files. CI has not run on this head yet.test_slot_memory_variants_reuse_native_saved_allocationscompares saved-arena sizes. An earlier version read the pool throughtorch.cuda.MemPool.snapshot(include_traces=False), which corrupted the Python heap in this container and made the whole-file run segfault later; the library code never calls it.Megatron validation on this TE build: Megatron-LM#6479
9da6607dd(table in that PR). End-to-end throughput and memory have not been measured on this head; the earlier numbers predate the rebase and the FP8 and offload changes.Limitations
d2h_stream, each offloaded save costs one extra device-to-device copy on the compute stream. The copies stay alive until the end-of-forward join, so the forward graph's pool peak can grow by up to the sum of the offloaded spans. This has not been benchmarked.cuda_graph_modulesscopes are required);--dataloader-inter-document-masking._graph_memory_slots, plus_graph_memory_offload_contextwhen offloading. It therefore accepts a TE that has only the first commit, which lacks the FP8 attention and THD all-gather protection.Checklist:
🤖 Generated with Claude Code