Skip to content

Support GDN2 and TE CUDA graphs for recurrence offloading - #8308

Draft
WEIXILIN0307 wants to merge 24 commits into
NVIDIA:mainfrom
WEIXILIN0307:feat/gdn2-te-graph-offload
Draft

WEIXILIN0307 wants to merge 24 commits into
NVIDIA:mainfrom
WEIXILIN0307:feat/gdn2-te-graph-offload

Conversation

@WEIXILIN0307

@WEIXILIN0307 WEIXILIN0307 commented Oct 17, 2026 •

Copy link
Copy Markdown
  • I, the PR author, have personally reviewed every line of this PR.

What does this PR do?

Extend opt-in gdn_core_attn activation offloading to BF16/FLA GDN2 training and Transformer Engine CUDA graphs for GDN and GDN2. The existing recurrence scope, backward prefetch anchor, transfer streams and pinned-memory pool are reused. TE layer-graph replay stays on the caller's autograd stream so shared graph pools remain ordered.

GDN2 keeps sigmoid gate outputs resident when gate backward retains the same storage: the differentiable write gate and the differentiable erase gate without head expansion. This avoids transfers that cannot release the original GPU storage.

Attention and whole-layer TE graphs, gdn_norm_out recomputation and delayed offload are supported. Graph offload requires PyTorch >= 2.9, TE >= 2.14 and positive graph warmup; set NVTE_CPU_OFFLOAD_V1=1 before launch. GDN2 requires fla.ops.gdn2. FP8/FP4, full recomputation and local/full-iteration graphs are outside this PR; low precision is proposed in #8543. See configuration and usage.

Dependencies

Merge #7399, #8243 and #7908 first. This branch also includes the independent mRoPE graph-input fix in #8555, needed for correct Qwen graph training. The original GDN2/graph feature diff excludes that separate fix. Composition with #7852 is not validated.

Validation

At 03b5ef353a1e657a89247f1ae0f67d05d619166f, including #8555 and main at 581248531984920e92f076946ad4629d359442a4:

  • On one RTX PRO 6000, the complete 24-layer Qwen3.5-0.8B language model (excluding vision and MTP) matches eager, graph without offload and graph with offload for six BF16 optimizer steps. Sequence length is 128, with four microbatches and an offload threshold of 1024 elements. Input hashes, loss, gradients, weights and full optimizer states match; the offload arm records transfers.
  • A single-GPU GDN2 training test with synthetic tokens passes with TE graph capture and replay, comparing offload disabled and enabled. A larger graph test run was interrupted during cold compilation and is not counted as a pass.
  • Upstream GPU CI has not run. Fixed-length training initialized by a training step is covered; evaluation-first initialization and tensor-size changes after warmup are not qualified.

Current-head commands, rank logs, source checksums and audit. Earlier Qwen and two-rank GDN/GDN2 tests ran at c5264b66197040f4309edfdb22dc9bbaf62189b1; their original logs remain available. Multi-rank and performance results have not been rerun on the current head.

Memory and runtime

At earlier head c5264b66197040f4309edfdb22dc9bbaf62189b1, BF16 whole-layer graph training on one RTX PRO 6000 used sequence 2048, four microbatches, fraction 1, the default threshold and no norm recomputation. Each fresh-process arm uses three warmup steps and ten measured optimizer steps; capture is timed separately. Peaks are medians of the per-arm peaks, and step changes are paired medians and ranges.

Model Allocated peak off → on (MiB) Reserved peak off → on (MiB) Retained pinned memory on (MiB) Step-time change, median [range]
Qwen3.5-0.8B language model 16137.82 → 16137.82 22576 → 22122 504 +24.75% [+12.71%, +26.55%]
GDN2, eight blocks 3236.12 → 3237.12 5212 → 4964 432 +97.83% [+96.40%, +98.97%]

The Qwen row uses five pairs because of timing variation; GDN2 uses three. These runs reduce reserved memory but not the measured allocated peak. No other compute process was observed on the measured GPU, but other GPUs were shared; host and PCIe isolation are not established. Commands, rank logs, samples, telemetry and audit.

A separate allocator snapshot at the same head and Qwen configuration locates the measured peak during output projection and cross-entropy: 970 MiB of logits and a 1940 MiB FP32 conversion are live together. In the snapshot after that step, the graph pool reserves 4132 → 3690 MiB, while its allocated count is 46 MiB in both arms. In the GDN2 snapshot, graph-pool reserve also decreases, while the total allocated peak increases by 1 MiB. These observations apply to the measured configurations.

As an exploratory follow-up, smaller offload fractions and the existing delayed-offload option did not reduce allocated peak in this configuration. The delayed-offload arm matches losses, gradients, weights and complete optimizer states for six steps; the timing samples are not sufficient to establish a performance improvement. Snapshot traces, commands, comparison data and audit.

Earlier A6000 measurements and a gate-selection experiment used different revisions and are retained in the historical graph-training evidence and profile. The historical Qwen graph run omitted mRoPE and is not a valid correct-training performance result.

The option remains disabled by default.

Issue tracking

Related to #6757.

Code provenance

No code copied or adapted from external repositories.

Contribution process

Pre-checks

  • I have added relevant unit tests.
  • I have added relevant functional tests.
  • The existing GDN determinism-manifest association covers this dispatch; numerical kernels are unchanged.
  • I have added proper typing to the changed code.
  • I have added relevant documentation and reproducible commands.
  • I have run Black, isort, Ruff and the project Pylint rules on the changed Python files.

GarlGuo and others added 4 commits September 16, 2026 10:49
…t copy

`ChunkOffloadHandler.reload()` enqueues its host-to-device copy on the caller's
current stream -- which on the `tensor_pop` path is the compute stream -- with no
ordering edge on the group's `_offload_event`, recorded on `d2h_stream`. Only
`bulk_reload_group` waits on that event; `tensor_pop` calls `reload()` inline and
bypasses the wait. A backward that reaches a group before any prefetch ran can
therefore read the pinned staging buffer while the device-to-host copy that fills
it is still in flight. The saved activation silently becomes stale buffer
contents and the gradients computed from it are wrong. Nothing raises, and the
loss curve still descends.

This contradicts the feature's own documented design: "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."

The inline path is not rare. The prefetch trigger `on_group_start_backward` is
the backward of the group-start marker, which autograd runs at the end of that
group's backward, after every `tensor_pop` in it, so the reload LIFO has to be
primed one group ahead by `pre_reload_last_layer`. During warmup nothing primes
it: `should_bulk_offload` returns True unconditionally while `is_warmup`, and
`tensor_pop` drained a group's tensors while leaving the group object on
`_groups_to_reload`, so every `bulk_reload_group` re-targeted an already-drained
tail and the priming branch was never reached. Measured on a 4-layer GPTModel
with `--offload-modules core_attn`, the warmup iteration took the unordered
inline path for all 24 of its reloads, with 0 prefetched.

Add a `reload_pending` latch to `OffloadTensorGroup`, set when the group's
device-to-host copy is enqueued and cleared by whichever reload consumes it. On
the inline path, `tensor_pop` waits on the group's offload event on the compute
stream and drops the group from the LIFO so the next prefetch advances. Keying
the latch on group identity is necessary rather than waiting unconditionally:
`_offload_event` is a single event per group, reused every iteration, so a group
that was not offloaded this iteration would otherwise wait on a stale edge.
`reset()` clears the latch for groups offloaded but never reloaded, and
`bulk_reload_group` clears it too so a prefetched group never re-serializes
compute behind the device-to-host copy.

With no artificial stall at all -- a 256 MiB device-to-host copy and nothing
else -- 20 trials per arm corrupted 19/20 on B300 and 20/20 on A40 before this
change, and 0/20 on both after it.

Add a regression test that stalls `d2h_stream` with `torch.cuda._sleep` before
the commit to make the race deterministic, asserts the stall took effect, and
then checks both the ordering and the reloaded values. Reverting the latch turns
it red.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: GarlGuo <wg247@cornell.edu>
The single-group test stayed green when `_groups_to_reload.remove(group)` was
dropped from `tensor_pop`. Use two forward-ordered groups, so the inline pop of
the second must release the LIFO and let the next prefetch advance to the first.
Raised in review on NVIDIA#7399.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: GarlGuo <wg247@cornell.edu>
@copy-pr-bot

copy-pr-bot Bot commented Oct 17, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

Signed-off-by: Pinoeer-kingxi <13022943007@163.com>
@WEIXILIN0307
WEIXILIN0307 force-pushed the feat/gdn2-te-graph-offload branch from 1f6eab9 to 8002cd3 Compare October 21, 2026 16:49
Signed-off-by: Pinoeer-kingxi <13022943007@163.com>
Signed-off-by: Pinoeer-kingxi <13022943007@163.com>
Signed-off-by: Pinoeer-kingxi <13022943007@163.com>
@WEIXILIN0307
WEIXILIN0307 force-pushed the feat/gdn2-te-graph-offload branch from 8002cd3 to 3b6f5e6 Compare October 22, 2026 04:24
Signed-off-by: Pinoeer-kingxi <13022943007@163.com>
Signed-off-by: Pinoeer-kingxi <13022943007@163.com>
Signed-off-by: Pinoeer-kingxi <13022943007@163.com>
Signed-off-by: Pinoeer-kingxi <13022943007@163.com>
Signed-off-by: Pinoeer-kingxi <13022943007@163.com>
Signed-off-by: Pinoeer-kingxi <13022943007@163.com>
Signed-off-by: Pinoeer-kingxi <13022943007@163.com>
Signed-off-by: Pinoeer-kingxi <13022943007@163.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants