|
1 | 1 | # CUDA Graphs |
2 | 2 |
|
3 | 3 | CUDA graphs capture a sequence of GPU operations once and replay them with |
4 | | -minimal host overhead, eliminating repeated kernel-launch and driver costs on |
5 | | -every training step. Megatron Bridge supports two capture implementations and |
6 | | -fine-grained scope selection to balance performance gain against memory cost. |
| 4 | +minimal host overhead, reducing repeated kernel-launch and driver costs on |
| 5 | +every training step. |
7 | 6 |
|
8 | | -This page is the stable overview for what CUDA graphs are, when to use them, |
9 | | -and which constraints are durable. For operational setup, code anchors, and |
10 | | -verification commands, see [skills/perf-techniques/cuda-graphs/SKILL.md](../skills/perf-techniques/cuda-graphs/SKILL.md). |
| 7 | +This page is the stable guide for what CUDA graphs are, when they help, and |
| 8 | +what tradeoffs to expect. For exact enablement knobs, code anchors, and |
| 9 | +verification commands, see `skills/perf-techniques/cuda-graphs/SKILL.md`. |
11 | 10 |
|
12 | 11 | ## What It Is |
13 | 12 |
|
14 | | -CUDA graphs work by recording a sequence of GPU operations (kernels, memory |
15 | | -copies, etc.) into a graph during a capture phase, then replaying that graph |
16 | | -on subsequent steps. This eliminates per-step host-side overhead such as |
17 | | -kernel launch latency and driver API calls. |
| 13 | +CUDA graphs record a fixed sequence of GPU work during a capture phase and then |
| 14 | +replay that sequence on later steps. The main benefit is lower host-side |
| 15 | +launch overhead. |
18 | 16 |
|
19 | | -In Bridge, there are two capture implementations: |
| 17 | +Megatron Bridge supports two capture implementations: |
20 | 18 |
|
21 | 19 | | `cuda_graph_impl` | Mechanism | Scope support | |
22 | 20 | |---|---|---| |
23 | | -| `"local"` | MCore `CudaGraphManager` / `FullCudaGraphWrapper` | `full_iteration` (whole fwd+bwd) | |
| 21 | +| `"local"` | MCore `CudaGraphManager` / `FullCudaGraphWrapper` | `full_iteration` | |
24 | 22 | | `"transformer_engine"` | TE `make_graphed_callables()` per layer | `attn`, `mlp`, `moe`, `moe_router`, `moe_preprocess`, `mamba` | |
25 | | -| `"none"` (default) | Disabled | — | |
| 23 | +| `"none"` | Disabled | — | |
| 24 | + |
| 25 | +`"local"` captures the whole forward-backward iteration. `"transformer_engine"` |
| 26 | +captures selected submodules and is usually the more flexible default path. |
| 27 | + |
| 28 | +## What Problem It Solves |
| 29 | + |
| 30 | +CUDA graphs mainly solve launch-bound training steps where GPU compute is fast |
| 31 | +enough that repeated host-driver submission overhead becomes noticeable. |
| 32 | + |
| 33 | +This is most useful when: |
| 34 | + |
| 35 | +- tensor shapes are static across steps |
| 36 | +- the workload has high step frequency or relatively small kernels |
| 37 | +- the run has enough memory headroom to keep graph buffers resident |
| 38 | + |
| 39 | +It is less about changing the math and more about reducing runtime overhead. |
| 40 | + |
| 41 | +## Impacted Training Dimensions |
| 42 | + |
| 43 | +| Dimension | Effect | Confidence | Why | |
| 44 | +|---|---|---|---| |
| 45 | +| `speed` | `+` to `++` | medium | Replays pre-captured GPU work and reduces launch overhead. | |
| 46 | +| `memory` | `-` | high | Graph buffers stay allocated for replay and reduce memory reuse. | |
| 47 | +| `scale` | `+/-` | low | Can help at scale if launch overhead matters, but memory overhead can limit larger configs. | |
| 48 | +| `convergence` | `0` | medium | Intended to preserve training math when capture constraints are satisfied. | |
| 49 | +| `stability` | `-` | medium | Adds stricter runtime constraints around shapes, supported scopes, and environment settings. | |
26 | 50 |
|
27 | 51 | ## When to Use It |
28 | 52 |
|
29 | | -CUDA graphs are most effective when: |
| 53 | +Enable CUDA graphs when all of the following are mostly true: |
| 54 | + |
| 55 | +- sequence length and micro-batch size are static |
| 56 | +- host overhead is a meaningful part of step time |
| 57 | +- the run has spare memory budget |
| 58 | +- you want throughput improvement without changing the training objective |
| 59 | + |
| 60 | +As a rule of thumb: |
| 61 | + |
| 62 | +- prefer `transformer_engine` scoped graphs for the safer first rollout |
| 63 | +- use `local` `full_iteration` graphs only when you specifically want the |
| 64 | + largest launch-overhead reduction and can accept the stricter constraints |
| 65 | + |
| 66 | +## When Not to Use It |
| 67 | + |
| 68 | +Avoid CUDA graphs when any of these are true: |
| 69 | + |
| 70 | +- sequence length or batch shapes vary step to step |
| 71 | +- CPU offloading is enabled |
| 72 | +- memory is already tight, especially with `PP > 1` |
| 73 | +- you rely on runtime checks that conflict with `full_iteration` capture |
| 74 | +- you need unsupported scope combinations for MoE or recompute paths |
| 75 | +- SFT/LoRA with packed sequences (`packed_sequence=True`) — TE-scoped graphs |
| 76 | + cannot capture `packed_seq_params` (non-Tensor input) |
| 77 | +- full activation recompute (`recompute_granularity=full`) with TE-scoped |
| 78 | + graphs — only `local` full-iteration graphs support full recompute |
30 | 79 |
|
31 | | -- **Tensor shapes are static** across training steps (fixed sequence length, |
32 | | - fixed micro-batch size). Variable-length sequences break graph replay |
33 | | - assumptions. |
34 | | -- **Host overhead is significant** relative to GPU compute — smaller models |
35 | | - or high step rates benefit most. |
36 | | -- **Memory budget allows it** — graph capture allocates static buffers, |
37 | | - typically adding a few GB. Models with `PP > 1` can consume over 10 GB |
38 | | - of additional memory. |
| 80 | +## Feature Interactions |
39 | 81 |
|
40 | | -### Local full-iteration graphs |
| 82 | +The most important interactions are: |
41 | 83 |
|
42 | | -Captures the entire forward-backward pass as one graph. Provides the highest |
43 | | -host-overhead reduction but requires disabling NaN checks and has the largest |
44 | | -memory footprint. |
| 84 | +- `use_te_rng_tracker` and `rng.te_rng_tracker`: required when CUDA graphs are enabled |
| 85 | +- `rerun_state_machine.check_for_nan_in_loss`: must be disabled for `local` + `full_iteration` |
| 86 | +- MoE routing scopes: `moe` and `moe_router` are mutually exclusive |
| 87 | +- `moe_preprocess`: requires `moe_router` |
| 88 | +- `delay_wgrad_compute`: adds extra constraints when captured scopes include attention or MoE router |
| 89 | +- `PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True`: requires `NCCL_GRAPH_REGISTER=0` in the relevant path |
| 90 | +- CPU offloading: incompatible |
45 | 91 |
|
46 | | -### Transformer Engine scoped graphs |
| 92 | +These interactions are stable enough to treat as design constraints, not just |
| 93 | +debugging tips. |
47 | 94 |
|
48 | | -Captures individual layer components (attention, MLP, MoE router, etc.) |
49 | | -through TE. More flexible, works with MoE models where only dense modules |
50 | | -can be graphed, and supports selective scope combinations. |
| 95 | +## Bridge Configuration |
51 | 96 |
|
52 | | -## Configuration |
| 97 | +Minimal high-level configuration: |
53 | 98 |
|
54 | 99 | ```python |
55 | | -cfg.model.cuda_graph_impl = "transformer_engine" # or "local" |
56 | | -cfg.model.cuda_graph_scope = ["attn", "moe_router"] # scope list |
57 | | -cfg.model.cuda_graph_warmup_steps = 3 # warmup before capture |
58 | | -cfg.rng.te_rng_tracker = True # required |
| 100 | +cfg.model.cuda_graph_impl = "transformer_engine" # or "local" |
| 101 | +cfg.model.cuda_graph_scope = ["attn"] # or other valid scopes |
| 102 | +cfg.model.cuda_graph_warmup_steps = 3 |
| 103 | +cfg.model.use_te_rng_tracker = True |
| 104 | +cfg.rng.te_rng_tracker = True |
59 | 105 | ``` |
60 | 106 |
|
61 | | -### Key constraints |
| 107 | +If you use `local` + `full_iteration`, also disable: |
62 | 108 |
|
63 | | -- `cfg.rng.te_rng_tracker` must be `True` when `cuda_graph_impl != "none"`. |
64 | | -- `full_iteration` scope requires `cuda_graph_impl = "local"` and |
65 | | - `rerun_state_machine.check_for_nan_in_loss = False`. |
66 | | -- MoE models with token-dropless routing have limited graph support |
67 | | - (dense modules only). |
68 | | -- `cuda_graph_impl = "none"` automatically clears `cuda_graph_scope`. |
| 109 | +```python |
| 110 | +cfg.rerun_state_machine.check_for_nan_in_loss = False |
| 111 | +cfg.ddp.check_for_nan_in_grad = False |
| 112 | +``` |
| 113 | + |
| 114 | +## Minimal Runnable Example |
| 115 | + |
| 116 | +For a minimal Bridge-facing example, start from the functional smoke test: |
| 117 | + |
| 118 | +- `tests/functional_tests/recipes/test_llama_recipes_pretrain_cuda_graphs.py` |
| 119 | + |
| 120 | +For a lightweight CLI-driven path, use the performance harness with scoped |
| 121 | +capture and a small model recipe. |
| 122 | + |
| 123 | +## Expected Metric Changes |
69 | 124 |
|
70 | | -## MoE Considerations |
| 125 | +| Metric | Expected Change | Conditions | Evidence | |
| 126 | +|---|---|---|---| |
| 127 | +| `step_time` | ~15-25% down | Static shapes, MoE, TE scoped (`attn+moe_router+moe_preprocess`) | measured: Qwen3-30B-A3B 623→484ms; GPT-OSS-20B 467-520→391-399ms | |
| 128 | +| `tokens_per_sec` | ~20-33% up | Same as above | measured: Qwen3-30B-A3B 214→274 TFLOP/s/GPU; GPT-OSS-20B 37.9-42.2→49.4-50.4 | |
| 129 | +| `peak_memory` | same pre-capture | TE scoped graphs on H100 80GB | measured: no increase in allocated memory on Qwen3-30B-A3B and GPT-OSS-20B | |
| 130 | +| `OOM risk` | up | Tight memory budget or large MoE configs | measured: GPT-OSS-120B blocked at ~70/79 GB before capture | |
71 | 131 |
|
72 | | -MoE models often cannot graph the full expert dispatch path due to dynamic |
73 | | -token routing. Common practice: |
| 132 | +Do not assume a fixed throughput gain across models. The improvement depends on |
| 133 | +how launch-bound the workload is and how much scope is captured. |
74 | 134 |
|
75 | | -- Graph `moe_router` and `moe_preprocess` (static portions). |
76 | | -- Add `attn` scope for the dense attention blocks. |
77 | | -- Leave expert dispatch in eager mode. |
| 135 | +## Measured Results (Qwen3-30B-A3B MoE, H100, TP2 PP2 EP4, 2 nodes) |
78 | 136 |
|
79 | | -Do not combine `moe` scope with `moe_router` scope — they are mutually |
80 | | -exclusive. |
| 137 | +### Pretrain |
81 | 138 |
|
82 | | -## Memory Impact |
| 139 | +TE-scoped CUDA graphs (`attn + moe_router + moe_preprocess`) on Qwen3-30B-A3B |
| 140 | +with mock data, GBS=8, MBS=1: |
| 141 | + |
| 142 | +- **~22% faster** iteration time (484ms vs 623ms steady-state) |
| 143 | +- **~28% higher TFLOP/s** (274 vs 214 TFLOP/s/GPU) |
| 144 | +- **Loss matches** baseline within 0.001 across all 20 iterations |
| 145 | +- 24 graphable layers per pipeline rank, capture completes in ~5.6s |
| 146 | +- No memory increase pre-capture, no NCCL errors |
| 147 | + |
| 148 | +### SFT (packed sequences) |
| 149 | + |
| 150 | +SFT with packed sequences (`packed_sequence=True`, SQuAD dataset) hits a |
| 151 | +hard incompatibility: |
| 152 | + |
| 153 | +``` |
| 154 | +AssertionError: CUDA graph accepts only Tensor inputs. |
| 155 | +inference_context and packed_seq_params are excluded from input list. |
| 156 | +``` |
83 | 157 |
|
84 | | -CUDA graphs allocate static buffers that persist for the duration of training. |
85 | | -Expect a few GB of additional memory. With `PP > 1`, memory overhead can |
86 | | -exceed 10 GB due to pipeline-stage buffering. Plan activation memory |
87 | | -accordingly. |
| 158 | +TE-scoped CUDA graphs require all forward inputs to be Tensors. Packed |
| 159 | +sequence SFT passes `packed_seq_params` (a dataclass), which is not captured. |
| 160 | +The baseline SFT runs fine without graphs (~880ms/iter). |
| 161 | + |
| 162 | +Workarounds: disable packing, or use `local` full-iteration graphs. Also make |
| 163 | +sure the TE/container build actually supports the packed-sequence attention |
| 164 | +backend your recipe needs. |
| 165 | + |
| 166 | +## Additional Validation (GPT-OSS, H100, Mar 2026) |
| 167 | + |
| 168 | +### GPT-OSS-20B pretrain |
| 169 | + |
| 170 | +TE-scoped CUDA graphs on `gpt-oss-20b` with `TP2 PP4 EP4 CP1`, 2 nodes, and |
| 171 | +mock data: |
| 172 | + |
| 173 | +- capture succeeds with 6 graphable layers per pipeline rank; capture completes |
| 174 | + in ~0.95s |
| 175 | +- steady-state iteration time improves by ~16-24% (467-520ms to 391-399ms) |
| 176 | +- throughput improves by ~19-33% (37.9-42.2 to 49.4-50.4 TFLOP/s/GPU) |
| 177 | +- the pre-capture memory report is unchanged and the 20-iteration run completes |
| 178 | + without NCCL or illegal-memory-access errors |
| 179 | +- loss comparison is inconclusive: the first ~10 post-capture iterations are |
| 180 | + close, but the run used mock data, `GBS=4`, and a production LR, so later |
| 181 | + divergence is too noisy to treat as a correctness signal |
| 182 | + |
| 183 | +A cleaner loss-match pass should lower LR and/or raise GBS before drawing |
| 184 | +equivalence conclusions. |
| 185 | + |
| 186 | +### GPT-OSS-20B SFT and LoRA |
| 187 | + |
| 188 | +Both packed-sequence finetuning workloads were blocked in the |
| 189 | +`mbridge-260128.sqsh` container before any CUDA-graph-specific behavior could |
| 190 | +be isolated: |
| 191 | + |
| 192 | +- baseline and graphed runs both fail with no TE attention backend available |
| 193 | + for the packed-sequence path |
| 194 | +- treat this as an environment/container blocker first, not as proof that CUDA |
| 195 | + graphs are or are not the root cause |
| 196 | +- after upgrading TE/container support, these workloads still need separate |
| 197 | + validation because packed-sequence plus TE-scoped graphs remains a sensitive |
| 198 | + combination |
| 199 | + |
| 200 | +### GPT-OSS-120B pretrain |
| 201 | + |
| 202 | +`gpt-oss-120b` pretrain at `TP2 PP4 EP8`, 4 nodes, hits OOM on iteration 2: |
| 203 | + |
| 204 | +- iteration 1 already uses ~69-70 GB allocated and ~72-73 GB reserved on 79 GB |
| 205 | + H100s |
| 206 | +- the failure is a `torch.OutOfMemoryError` on an additional 1.54 GiB |
| 207 | + allocation |
| 208 | +- treat larger MoE rollouts as memory-gated even before capture benefits are |
| 209 | + realized; more PP or different memory settings may be needed |
| 210 | + |
| 211 | +## Common Failure Modes |
| 212 | + |
| 213 | +- Missing TE RNG tracker settings causes an assertion before training starts. |
| 214 | +- Dynamic sequence or batch shapes break capture or replay assumptions. |
| 215 | +- `local` `full_iteration` graphs fail when NaN-loss checking is still enabled. |
| 216 | +- Illegal scope combinations such as `moe` with `moe_router` fail validation. |
| 217 | +- Runs that fit in eager mode can OOM after enabling graphs because buffers stay pinned. |
| 218 | +- Full activation recompute (`recompute_granularity=full`) with TE-scoped graphs |
| 219 | + asserts: `full recompute is only supported with full iteration CUDA graph`. |
| 220 | + Disable recompute or switch to `local` implementation. |
| 221 | +- Packed-sequence SFT/LoRA asserts: `CUDA graph accepts only Tensor inputs. |
| 222 | + inference_context and packed_seq_params are excluded from input list.` |
| 223 | + TE-scoped graphs cannot capture non-Tensor forward arguments. |
| 224 | +- Older TE/container builds can fail packed-sequence attention before graph |
| 225 | + capture begins (`Available backends = {FlashAttention=False, |
| 226 | + FusedAttention=False, UnfusedDotProductAttention=False}`). In that case the |
| 227 | + baseline and graph runs are both blocked, so fix the environment first. |
88 | 228 |
|
89 | 229 | ## Related Docs |
90 | 230 |
|
91 | | -- [docs/performance-guide.md](../performance-guide.md) |
92 | | -- [docs/training/communication-overlap.md](communication-overlap.md) |
93 | | -- [skills/perf-techniques/cuda-graphs/SKILL.md](../skills/perf-techniques/cuda-graphs/SKILL.md) |
| 231 | +- [Performance Guide](../performance-guide.md) |
| 232 | +- [Communication Overlap](communication-overlap.md) |
| 233 | +- `skills/perf-techniques/cuda-graphs/SKILL.md` |
0 commit comments