| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
Signed-off-by: Robin Zhang <robinz@nvidia.com>
Signed-off-by: Robin Zhang <robinz@nvidia.com>
Greptile SummaryThe PR reduces CUDA graph memory retention while making per-callable reset terminal, isolated, and idempotent.
Confidence Score: 5/5The PR appears safe to merge. No blocking failure remains; both previously reported reset defects are addressed by guarded, per-callable teardown and replay checks. Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
C[Capture callable] --> O[Per-callable graph and static state]
O --> R[Replay forward/backward]
O --> X[reset]
X --> G[Reset captured CUDA graphs]
X --> S[Release static closure state]
X --> T[Mark callable terminal]
T --> E[Later replay raises RuntimeError]
Reviews (5): Last reviewed commit: "[PyTorch] Bundle per-callable lifecycle ..." | Re-trigger Greptile |
Sorry, something went wrong.
Signed-off-by: Robin Zhang <robinz@nvidia.com>
Validation report: this PR fixes an OOM during deferred CUDA-graph capture at 64-GPU scaleWe hit a reproducible torch.OutOfMemoryError during TransformerEngine's deferred CUDA-graph capture on a 64-GPU Llama-3 70B NVFP4 pre-training run, and this PR resolves it. We ran a controlled A/B in which the only difference between the two arms was the contents of transformer_engine/pytorch/graph.py. Sharing the evidence in case it is useful for review. Environment and workload
Because VP=5, the run takes the interleaved _order code path in _make_graphed_callables — the same path this PR's test_ordered_warmup_releases_consumed_outputs covers. Failure without this PRThe run trains normally through iteration 3, then dies as soon as capture begins: INFO:megatron.core.transformer.cuda_graphs:Start CUDA Graphs capture...
[rank11]: expandable_segments: memory mapping failed with OOM on device 3
while trying to map 20971520 bytes (free: 15269888, total: 197897486336)
OutOfMemoryError: CUDA out of memory. Tried to allocate 224.00 MiB.
GPU 3 has a total capacity of 184.31 GiB of which 14.56 MiB is free.
Of the allocated memory 170.32 GiB is allocated by PyTorch, with 228.08 MiB
allocated in private pools (e.g., CUDA Graphs), and 8.48 GiB is reserved by
PyTorch but unallocated.
Traceback (identical in shape on every failing rank): File "megatron/core/transformer/cuda_graphs.py", line 2576, in create_cudagraphs
graphs = make_graphed_callables(
File ".../transformer_engine/pytorch/graph.py", line 1612, in make_graphed_callables
graphed_callables = _make_graphed_callables(
File ".../transformer_engine/pytorch/graph.py", line 719, in _make_graphed_callables
outputs = func(*args, **kwargs)
File ".../transformer_engine/pytorch/graph.py", line 1585, in call_func
outputs = old_call_funcs[block_cls](self, *args, **kwargs)
File "megatron/core/transformer/transformer_layer.py", line 1181, in _te_cuda_graph_capture
hidden_states = self._forward_mlp(hidden_states)
So the OOM occurs in the warm-up forward inside _make_graphed_callables, not in steady-state training — the model itself fits and trains fine for three iterations at ~170 GiB resident. Note the allocator breakdown: only ~228 MiB is in CUDA-graph private pools at the point of failure, i.e. the memory pressure is from retained ordinary allocations during warm-up, not from the graph pools themselves. Result with this PR appliedSame image, same 64-GPU allocation, same recipe, same flags. We applied only this PR's change to transformer_engine/pytorch/graph.py on top of the graph.py already installed in the image (it applied cleanly with git apply --check), and left everything else byte-identical:
Why this PR appears to address itOn the interleaved _order warm-up path, the pre-PR code kept every warm-up forward's flattened outputs alive in per_fwd_outputs for the whole warm-up loop, reading them with per_fwd_outputs[per_callable_bwd_idx] and never removing the entry. With 20 graphable layers per rank and VP=5, that is a large number of live activation sets accumulating simultaneously, on top of an already ~170 GiB resident model. This PR's change to per_fwd_outputs.pop(per_callable_bwd_idx) plus the del outputs after each warm-up backward releases each set as soon as its backward consumes it, which matches the observed behavior exactly: the failure is in warm-up, and it disappears with this change. The capture-phase del static_outputs, static_grad_inputs, grad_inputs is likely also relevant at this scale, since it lets weak-referenced graph-pool buffers be reused across the 20 sequential per-layer captures. Scope and caveats
Validated against this PR at commit a3f1d527a9fd879ba7133505814a289b3b2da9c7. Happy to re-run this workload against any later revision of the PR if that would help. This issue was drafted with assistance from the opus AI model. |
Sorry, something went wrong.
| # The per-callable containers now own all tensors that must survive | ||
| # capture. Drop local strong references so weak-refed graph buffers can | ||
| # be returned to the shared CUDA graph pool before the next capture. | ||
| del static_outputs, static_grad_inputs, grad_inputs |
There was a problem hiding this comment.
If we call make_graphed_callables multiple times, how do we ensure that they don't clobber each other's memory?
graph1 = te.make_graphed_callables(layer1, ..., _order=...) # stores static_output as weakref
graph2 = te.make_graphed_callables(layer2, ..., _order=...) # same static_outputs as graph1
x = graph1(x) # writes to static_output
x = activation(x) # cache static_output for backward
x = graph2(x) # overwrites static_output
x.backward() # incorrect activation backwardThis is only relevant when _order is not None, so are we making the assumption that this workflow only involves a single graph capture?
Sorry, something went wrong.
There was a problem hiding this comment.
I think so, make_graphed_callables is expected to be called only once for one pool. This is not related to _order or static output reuse. This is a general assumption. For example, when we call
graph1 = make_graphed_callables(layer1, pool=my_pool) graph2 = make_graphed_callables(layer2, pool=my_pool)
it captures G1F -> G1B -> G2F -> G2B in order. So when you run it in G1F -> G2F -> G2B -> G1B it naturally fails because the G1 forward saved context is overwritten by G2.
The only opportunity is when the two captured modules have no overlap in execution. For example, the first make_graphed_callables captures transformer layers, while the second make_graphed_callables captures the optimizer step.
Sorry, something went wrong.
Signed-off-by: Robin Zhang <robinz@nvidia.com>
|
/te-ci pytorch |
Sorry, something went wrong.
There was a problem hiding this comment.
LGTM, pending CI
Sorry, something went wrong.
| Back | FazBrowse Home | New Git URL |
Description
Reduce avoidable GPU-memory retention across CUDA graph construction and teardown.
The changes preserve warmup/capture order and public APIs.
Testing
Signed-off-by: Robin Zhang robinz@nvidia.com