Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
66 commits
Select commit Hold shift + click to select a range
06954b9
support offloading modules captured by cuda graph
lhb8125 Mar 4, 2026
8a889c0
minor fix
lhb8125 Mar 4, 2026
0a8e867
bug fix
lhb8125 Mar 4, 2026
fba4e8d
add flag to control flush_delayed_groups in fine_grained_callables.py
lhb8125 Mar 2, 2026
ecaa319
Merge branch 'main' into hongbinl/activation_offloading_refactor_cuda…
lhb8125 Mar 4, 2026
5591584
format
lhb8125 Mar 4, 2026
7e763fb
1. move backward_record() to te_cuda_graph_capture()
lhb8125 Mar 5, 2026
39ffca4
format
lhb8125 Mar 5, 2026
8e6c88e
remove the knob forward_only when executing reset()
lhb8125 Mar 5, 2026
a04ea3f
Merge branch 'main' into hongbinl/activation_offloading_refactor_cuda…
lhb8125 Mar 5, 2026
e4263ca
fix ut and reviewer's comments
lhb8125 Mar 5, 2026
973c533
Merge branch 'main' into hongbinl/activation_offloading_refactor_cuda…
lhb8125 Mar 6, 2026
cfe6191
resolve comments
lhb8125 Mar 6, 2026
9446d48
Merge branch 'hongbinl/activation_offloading_refactor_cuda_graph_main…
lhb8125 Mar 6, 2026
b1d7db9
Merge branch 'main' into hongbinl/activation_offloading_refactor_cuda…
lhb8125 Mar 6, 2026
7a5234c
add assertion for recomputing and offloading
lhb8125 Mar 9, 2026
57890f8
fix ut
lhb8125 Mar 10, 2026
344479e
format and fix TE version
lhb8125 Mar 10, 2026
73ed9f5
replace multiple imports with a _get_off_interface()
lhb8125 Mar 11, 2026
1d51cfc
renaming _get_offloading_interface
lhb8125 Mar 11, 2026
f19bf81
minor fix
lhb8125 Mar 11, 2026
7dba82c
Merge branch 'hongbinl/activation_offloading_refactor_cuda_graph_main…
lhb8125 Mar 11, 2026
823eaf3
Merge branch 'main' into hongbinl/activation_offloading_refactor_cuda…
lhb8125 Mar 11, 2026
4befd50
fix comments
lhb8125 Mar 23, 2026
aa793ab
fix ci
lhb8125 Mar 23, 2026
334f48d
fix ut
lhb8125 Mar 25, 2026
b7e95c6
refine docs and minor fix
lhb8125 Mar 30, 2026
ef6fa2b
minor fix
lhb8125 Mar 30, 2026
795f5a9
minor fix
lhb8125 Mar 30, 2026
06c75e6
Merge branch 'main' into hongbinl/activation_offloading_refactor_cuda…
lhb8125 Mar 30, 2026
2390074
Merge branch 'hongbinl/activation_offloading_refactor_cuda_graph_main…
lhb8125 Mar 30, 2026
dd6d702
format
lhb8125 Mar 30, 2026
aaa23de
fix ci
lhb8125 Mar 30, 2026
183244c
fix ci
lhb8125 Mar 30, 2026
ac82c9b
Merge remote-tracking branch 'upstream/main' into pr3697
lhb8125 May 8, 2026
609e3c7
Merge origin/main into PR #3697
lhb8125 May 22, 2026
2e93c38
refactor: use group offload manager API
lhb8125 May 22, 2026
83e3c17
Merge branch 'main' into hongbinl/activation_offloading_refactor_cuda…
lhb8125 May 22, 2026
f530364
Merge remote-tracking branch 'origin/main' into fix/offloading-pr-mai…
lhb8125 May 26, 2026
46307ff
fix: use cuda graph modules for offload checks
lhb8125 May 26, 2026
9793b40
fix: make old nvrx disable async support
lhb8125 May 26, 2026
7ee751b
Merge remote-tracking branch 'refs/remotes/hongbinl/activation_offloa…
lhb8125 May 26, 2026
4485dd5
test: avoid integrity manifest write race
lhb8125 May 26, 2026
aedcdac
fix: allow local cuda graphs with disjoint offload scopes
lhb8125 May 26, 2026
26b427b
fix: narrow local cuda graph offload support
lhb8125 May 26, 2026
afe6bf0
fix: preserve reload cadence for offload fraction
lhb8125 Jun 2, 2026
9874d93
Remove unrelated changes from CUDA graph offload PR
lhb8125 Jun 9, 2026
33ab886
Handle NVFP4 expert FC1 offload input saving
lhb8125 Jun 9, 2026
e7b76c1
Revert "fix: preserve reload cadence for offload fraction"
lhb8125 Jun 9, 2026
557941d
Document CUDA graph offload coordination
lhb8125 Jun 9, 2026
b8102b4
Fix CUDA graph test formatting
lhb8125 Jun 10, 2026
b14985c
Address CUDA graph offload review nits
lhb8125 Jun 16, 2026
46fe4cf
Clarify activation offload fraction semantics
lhb8125 Jun 16, 2026
c017623
Merge remote-tracking branch 'origin/main' into codex/pr-3697-conflic…
lhb8125 Jun 25, 2026
56deb1d
fix: respect activation offload tensor opt-out
lhb8125 Jun 25, 2026
bb69dee
fix: drop unused activation offload opt-out flag
lhb8125 Jun 25, 2026
5b837ac
fix: report local FGAO CUDA graph config errors
lhb8125 Jun 25, 2026
b314de5
fix: remove activation offload import cache
lhb8125 Jun 26, 2026
45d2e90
Merge remote-tracking branch 'origin/main' into codex/pr-3697-conflic…
lhb8125 Jun 26, 2026
008df8a
fix: ignore zero-byte groups in offload fraction
lhb8125 Jun 30, 2026
e68718a
fix: use offload manager for fused grouped mlp
lhb8125 Jun 30, 2026
4c31cfb
fix: consume skipped activation offload groups
lhb8125 Jun 30, 2026
c477ab0
cleanup: remove unused layer offload flags
lhb8125 Jun 30, 2026
c583f5d
style: apply activation offload autoformat
lhb8125 Jun 30, 2026
6f0b3d1
Merge branch 'main' into hongbinl/activation_offloading_refactor_cuda…
lhb8125 Jun 30, 2026
e50a23b
Merge branch 'main' into hongbinl/activation_offloading_refactor_cuda…
yaox12 Jul 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
184 changes: 163 additions & 21 deletions docs/user-guide/features/fine_grained_activation_offloading.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,49 +11,191 @@

Contributed in collaboration with RedNote.

Memory is often the limiting factor for very large sparse MoE models such as DeepSeek-V3 and Qwen3-235B. Fine-grained recomputation lowers activation memory at the cost of extra compute. Offloading can use host-device bandwidth so that reload overlaps compute and keeps overhead small in many setups. Fine-grained activation offloading moves activations at module granularity so you can tune how much activation memory leaves the device and adjust training throughput.
Fine-grained activation offloading reduces GPU memory by asynchronously transferring activations to CPU at the granularity of individual submodules within a transformer layer. Unlike layer-level offloading, it allows precise control over which activations to offload, enabling a tradeoff between memory savings and PCIe bandwidth overhead.

Supported offloading modules are `"attn_norm"`, `"qkv_linear"`, `"core_attn"`, `"attn_proj"`, `"mlp_norm"`, `"expert_fc1"`, `"moe_act"`, and `"fused_group_mlp"`. They can be combined with fine-grained recomputation to free almost all activations for a transformer layer on the device. `fused_group_mlp` requires `--use-transformer-engine-op-fuser` and offloads the whole fused grouped MLP, so it cannot be combined with `expert_fc1` or `moe_act`.

## Features
## User Guide

- Pipeline parallelism: PP=1, PP, and interleaved PP
- Compatible with fine-grained recomputation
- FP8 training
- MTP
- Mixed dense and MoE layers
- A2A overlap
- CUDA graphs
- **Note:** A CUDA graph capture cannot include the offloading modules (temporary limitation).

## Usage
### Basic Usage

```bash
# Enable fine-grained activation offloading
--fine-grained-activation-offloading

# Modules whose inputs are offloaded (refer to your training script for list or delimiter syntax).
# Choices: "attn_norm", "qkv_linear", "core_attn", "attn_proj", "mlp_norm", "expert_fc1", "moe_act", "fused_group_mlp".
--offload-modules expert_fc1
--offload-modules core_attn attn_proj expert_fc1
```

## Max inflight offloads
### Offloadable Modules

Each module offloads its **input** activation to CPU during forward and reloads it before backward:

| Module | Description | Notes |
|---|---|---|
| `attn_norm` | Input layernorm of attention | Skipped if using `IdentityOp` |
| `qkv_linear` | QKV linear projection | |
| `core_attn` | Core attention (softmax + matmul) | |
| `attn_proj` | Output projection of attention | Must be used together with `core_attn` |
| `mlp_norm` | Pre-MLP layernorm | Skipped if using `IdentityOp` |
| `expert_fc1` | First FC layer in MoE experts | MoE models only |
| `moe_act` | Activation function in MoE experts | MoE models only |
| `fused_group_mlp` | Whole fused grouped MLP | Requires `--use-transformer-engine-op-fuser`; cannot be combined with `expert_fc1` or `moe_act` |

### Tuning Parameters

```bash
# Minimum tensor size (in elements) to offload. Smaller tensors are skipped.
# Default: 1048576 (1M elements)
--min-offloaded-tensor-size 1048576

# Fraction of activations to offload, range [0, 1]. Default: 1.0
# Useful for partial offloading when PCIe bandwidth is a bottleneck.
--activation-offload-fraction 0.8

# Reduce offload amount on higher PP ranks (in bytes). Default: 0
# Higher PP ranks have fewer microbatches in flight, so offloading less
# reduces overhead without increasing peak memory.
--delta-offload-bytes-across-pp-ranks 1073741824

# Optional: cap inflight D2H offloads per offload group to N (omit or None in most setups).
# Required as a non-None non-negative integer when fine-grained activation offloading is used with
# local full-iteration CUDA graphs (full_iteration in cuda_graph_scope); see prose below.
--fine-grained-offloading-max-inflight-offloads <N>
```

TransformerConfig.fine_grained_offloading_max_inflight_offloads caps, per offload group (for example `moe_act`, `qkv_linear`), how many D2H copies may be in flight before a main-stream wait_event. 0 waits after each offload; larger values allow more overlap; None skips these joins.
`TransformerConfig.fine_grained_offloading_max_inflight_offloads` caps, per offload group (for example `moe_act`, `qkv_linear`), how many D2H copies may be in flight before a main-stream `wait_event`. `0` waits after each offload; larger values allow more overlap; `None` skips these joins.

With full-iteration CUDA graphs (local graph impl, `full_iteration` in `cuda_graph_scope`) and fine-grained activation offloading enabled, set it to a non-None integer: that path does not rely on `record_stream`, so explicit joins are required.

### Activation Offload Fraction

`--activation-offload-fraction` (`TransformerConfig.activation_offload_fraction`) is a fraction
over eligible offload groups, not a byte fraction and not a selector for which module names are
enabled. It is used together with `--offload-modules`: all module names listed in
`--offload-modules` still register their offload groups, and the fraction is applied once across
the combined eligible groups from all configured modules.

The manager keeps the first N% of eligible groups in forward execution order and leaves the later
groups on GPU. For example, with
`--offload-modules core_attn attn_proj expert_fc1 --activation-offload-fraction 0.5`, the eligible
`core_attn`, `attn_proj`, and `expert_fc1` groups are considered together in execution order, and
the first 50% of that combined group list are offloaded. The fraction does not mean "offload 50% of
the activation bytes" and does not mean "offload only the first 50% of the module names".

The fraction is applied after other eligibility filters such as `min_offloaded_tensor_size`, the
last-group margin used to avoid backward reload stalls, and
`delta_offload_bytes_across_pp_ranks`. Therefore N% is computed over the remaining eligible groups
from all configured offload modules after those filters.

### CUDA Graph Integration

Fine-grained offloading is compatible with CUDA graphs. When CUDA graph is enabled, the following constraints apply:

- `attn_norm` and `mlp_norm` **cannot** be offloaded (they cross CUDA graph boundaries).
- `cuda_graph_scope` must include `attn` and `moe_router`.
- `cuda_graph_impl` must be `transformer_engine`.
- Requires `torch >= 2.9.0` and `transformer_engine >= 2.14.0`.

```bash
# Optional: defer D2H enqueue for offloads *outside* cuda_graph_scope (MoE experts; see below)
--delay-offload-until-cuda-graph
```

**`--delay-offload-until-cuda-graph` (`TransformerConfig.delay_offload_until_cuda_graph`)**

**Inside vs outside `cuda_graph_scope`.** Offload boundaries that lie **inside** the captured `cuda_graph_scope` (for example `qkv_linear`, `core_attn`, and `attn_proj` when `attn` is in scope) are part of CUDA graph **capture and replay**. Their offload-related work is replayed with the graph rather than re-driven from Python each step, so they do **not** incur the same per-step CPU launch overhead as a purely eager path.

Boundaries that run **outside** the captured region still execute as normal eager PyTorch each forward—for the recommended MoE setup, that includes expert compute after a graphed `moe_router` (e.g. offloading `expert_fc1` / `moe_act`). For those groups, each `group_offload` would otherwise submit D2H work from the host as soon as the forward hits the commit point.

**What this flag does.** It only affects offload commits that are explicitly wired with **delayed** group commit (currently the MoE expert path: `expert_fc1`, `moe_act`). Around each layer’s `TransformerEngine` CUDA graph replay, the offload manager enters **replay mode**; delayed commits **enqueue** `(callback, group name, forced tensors)` instead of launching D2H immediately, then **flush_delayed_groups** runs **after** that graph replay returns and issues the queued D2H copies in forward order, without changing the offload/reload semantics.

**When this actually buys time (EP A2A after replay).** The benefit assumes a **real CPU/GPU synchronization gap right after graph replay**—in the usual MoE training layout, **expert parallel (EP) all-to-all** and related dispatch follows the graphed `moe_router` region. That A2A path typically needs the host to coordinate collectives and to **sync with the GPU** (e.g. wait for graph work to finish or for communication staging), so the CPU is not fully overlapped with useful launch work during that interval. Scheduling `flush_delayed_groups` **immediately after** `cudaGraphLaunch` returns uses that window to issue D2H copies from the host: the enqueue cost is largely **hidden** in slack that EP A2A would already incur. If there were no such post-replay sync (or expert work were fully captured inside the graph with no host-visible gap), deferring commits would not provide the same “free” host time.

**Behavioral notes**

- Does **not** replace or “delay” attention-side offloads inside the graphed `attn` region; those are not on the delayed path in the implementation.
- Warmup and non-replay forwards still commit delayed-eligible groups immediately (no replay-mode deferral).
- Must be used together with **fine-grained activation offloading** and **CUDA graph** under the same rules as this section (TE `cuda_graph_impl`, scope including `attn` and `moe_router`, etc.).
- Stream ordering between the graph compute path and `d2h_stream` still uses the existing events (`forward_record` / `backward_record`); this option only changes **when** eligible D2H work is submitted from the host.

### Combining with Fine-Grained Recomputation

Offloading and recomputation are complementary:
- Use **recomputation** for lightweight modules (e.g., layernorm, activation functions) with negligible compute overhead.
- Use **offloading** for heavy modules (e.g., core_attn, expert_fc1) where recomputation would be too costly.

```bash
--recompute-granularity selective
--recompute-modules layernorm moe_act
--fine-grained-activation-offloading
--offload-modules core_attn attn_proj expert_fc1
```

![Fine-grained Activation Offloading and Fine-grained Recomputation](../../images/fine_grained_activation_offloading/offloading_and_recomputing.png)


### Compatibility

| Feature | Supported |
|---|---|
| PP / Interleaved PP / PP=1 | Yes |
| Fine-grained recomputation | Yes |
| FP8 training | Yes |
| MTP (Multi-Token Prediction) | Yes |
| Mixed dense & MoE layers | Yes |
| A2A overlap (EP) | Yes |
| CUDA Graph (TE impl) | Yes |

---

## How It Works

### Architecture Overview

The implementation consists of three layers:

1. **`PipelineOffloadManager`** (singleton): Global coordinator that manages CUDA streams, CPU tensor pools, and chunk lifecycle across pipeline stages.
2. **`ChunkOffloadHandler`**: Per-microbatch handler that tracks tensor groups, executes D2H/H2D transfers, and decides which groups to actually offload.
3. **`FineGrainedActivationOffloadingInterface`**: Lightweight interface used by transformer modules (attention, MoE, etc.) to mark offload boundaries.

### Offload/Reload Flow

```
Forward pass (Layer N): Backward pass (Layer N):
┌─────────────────────┐ ┌───────────────────────┐
│ group_start(input) │─── register ──► │ │
│ │ tensor group │ group_commit_backward │
│ module.forward() │ │ wait H2D complete │
│ │ │ pop tensors from │
│ group_offload(out) │─── D2H async ──► │ CPU → GPU │
│ on d2h_stream │ to pinned CPU │ on h2d_stream │
└─────────────────────┘ └───────────────────────┘
```

1. **`group_start`**: Registers a new tensor group and hooks into `saved_tensors_hooks` to intercept `save_for_backward`.
2. **Forward execution**: All tensors saved by autograd within the group are captured.
3. **`group_offload`**: Triggers asynchronous D2H copy on a dedicated CUDA stream (`d2h_stream`), optionally releases GPU storage of input tensors.
4. **Backward**: Before the group's backward, tensors are reloaded from CPU to GPU on `h2d_stream`, and the compute stream waits for the transfer to complete.

### Warmup and Adaptive Offloading

The first training iteration serves as a **warmup phase** where the manager records tensor groups, their sizes, and the execution order. After warmup, a `post_warmup_callback` runs to:

1. **Reserve margin**: The last N groups (by deduplication count) are kept on GPU to avoid reload blocking the compute stream.
2. **Apply PP rank delta**: Higher PP ranks offload fewer bytes (controlled by `delta_offload_bytes_across_pp_ranks`).
3. **Apply fraction**: Only the first N% of the remaining eligible groups are offloaded across all configured modules (controlled by `activation_offload_fraction`).
4. **Print summary table**: An ASCII table of per-rank offload bytes is printed for debugging.

### CPU Tensor Pool

With full-iteration CUDA graphs (local graph impl, full_iteration in cuda_graph_scope) and fine-grained activation offloading enabled, set it to a non-None integer: that path does not rely on record_stream, so explicit joins are required.
A 'OffloadTensorPool` (on CPU with pinned memory) caches allocated tensors by `(shape, dtype)`. This avoids repeated `cudaMallocHost` / `cudaFreeHost` calls and reduces D2H latency after the first iteration.

## Compatible With Fine-Grained Recomputation
### CUDA Graph Support

- For low-overhead modules such as LayerNorm or `moe_act`, use recomputation to save activation memory.
- For other modules, use offloading to save activation memory.
- Overlap offload and reload with compute when possible.
When offloading interacts with CUDA graphs:

![Diagram comparing fine-grained activation offloading and fine-grained recomputation across a transformer layer](../../images/fine_grained_activation_offloading/offloading_and_recomputing.png)
- A dedicated `cuda_graph_stream` runs the captured computation, while `d2h_stream` overlaps D2H transfers for regions that are **inside** the graph capture.
- During CUDA graph **warmup**, offloading is disabled (`pre_warmup_hook` / `post_warmup_hook`).
- The `delay_offload_until_cuda_graph` option defers D2H launches until graph replay, utilizing the CPU idle time during `cudaGraphLaunch` to issue offload commands with near-zero CPU overhead.
20 changes: 10 additions & 10 deletions megatron/core/models/gpt/fine_grained_callables.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.

import weakref
from contextlib import nullcontext
Expand Down Expand Up @@ -537,18 +537,16 @@ def forward_func(
)
if not isinstance(layer.mlp, MoELayer):
return hidden_states, None, None, None
mlp_norm_manager = off_interface(layer.offload_mlp_norm, hidden_states, "mlp_norm")
node.layer_state.mlp_norm_manager = mlp_norm_manager
if layer.recompute_pre_mlp_layernorm:
layer.pre_mlp_norm_checkpoint = tensor_parallel.CheckpointWithoutOutput()
with off_interface(
layer.offload_mlp_norm, hidden_states, "mlp_norm"
) as hidden_states:
with mlp_norm_manager as hidden_states:
pre_mlp_layernorm_output = layer.pre_mlp_norm_checkpoint.checkpoint(
apply_module(layer.pre_mlp_layernorm), hidden_states
)
else:
with off_interface(
layer.offload_mlp_norm, hidden_states, "mlp_norm"
) as hidden_states:
with mlp_norm_manager as hidden_states:
pre_mlp_layernorm_output = apply_module(layer.pre_mlp_layernorm)(
hidden_states
)
Expand Down Expand Up @@ -662,10 +660,12 @@ def submodule_combine_forward(node: ScheduleNode, output: torch.Tensor):
)
# Delay the offload of the mlp norm until after the mlp_bda has been computed
# because the residual is needed in the mlp_bda.
if layer.offload_mlp_norm:
hidden_states = off_interface.group_commit(
hidden_states, name="mlp_norm", forced_released_tensors=[residual]
mlp_norm_manager = getattr(node.layer_state, 'mlp_norm_manager', None)
if mlp_norm_manager is not None:
hidden_states = mlp_norm_manager.group_offload(
hidden_states, forced_released_tensors=[residual]
)
node.layer_state.mlp_norm_manager = None
output = make_viewless_tensor(
inp=hidden_states, requires_grad=hidden_states.requires_grad, keep_graph=True
)
Expand Down
11 changes: 7 additions & 4 deletions megatron/core/models/gpt/gpt_model.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.

from collections import OrderedDict
from typing import Any, Callable, Dict, Literal, Optional
Expand Down Expand Up @@ -474,20 +474,23 @@ def _preprocess(
def preprocess_for_fine_grained_offloading(self):
"""Preprocess for fine-grained activation offloading."""
off_interface.init_chunk_handler(
pp_rank=self.pg_collection.pp.rank(),
vp_size=self.config.virtual_pipeline_model_parallel_size,
vp_stage=self.vp_stage,
min_offloaded_tensor_size=self.config.min_offloaded_tensor_size,
delta_offload_bytes_across_pp_ranks=self.config.delta_offload_bytes_across_pp_ranks,
activation_offload_fraction=self.config.activation_offload_fraction,
max_inflight_offloads=self.config.fine_grained_offloading_max_inflight_offloads,
)
if self.disable_param_offloading:
for param in self.decoder.parameters():
off_interface.mark_not_offloadable(param)
off_interface.mark_not_offload(param)
if self.mtp_process:
for param in self.mtp.parameters():
off_interface.mark_not_offloadable(param)
off_interface.mark_not_offload(param)
if self.post_process:
for param in self.output_layer.parameters():
off_interface.mark_not_offloadable(param)
off_interface.mark_not_offload(param)
self.disable_param_offloading = False

def preprocess_for_paged_stash(self):
Expand Down
Loading
Loading