Skip to content

[PyTorch] Scope the quantized-param caching flag to its own graph capture - #3298

Open
xiuhu17 wants to merge 1 commit into
NVIDIA:mainfrom
xiuhu17:fix_cache_quantized_params
Open

[PyTorch] Scope the quantized-param caching flag to its own graph capture#3298
xiuhu17 wants to merge 1 commit into
NVIDIA:mainfrom
xiuhu17:fix_cache_quantized_params

Conversation

@xiuhu17

@xiuhu17 xiuhu17 commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Description

make_graphed_callables(cache_quantized_params=True) leaks its weight-update flag into every later capture in the same process, which can leave the later module silently training on a stale quantized weight.

The mechanism

Quantized-parameter caching lets a graphed module reuse its quantized weight across microbatches. Because a CUDA graph must replay an identical kernel sequence, the quantize kernel is always launched and a noop_flag — a GPU scalar — decides at runtime whether it writes. That flag has to be process-global: its address is baked into the captured graph, and replay fills it in place.

Nothing scopes it to the capture that asked for it:

  • _make_graphed_callables creates the flag when cache_quantized_params=True (graph.py) and never clears it.
  • Linear, GroupedLinear, LayerNormLinear and LayerNormMLP read it whenever FP8GlobalStateManager.fp8_graph_capturing() is true — they never check whether this capture requested caching. Seeing a non-None flag forces is_first_microbatch = False, so the module takes the cached-workspace path and bakes the flag into its graph.
  • Only a callable graphed with caching writes the flag at replay (graph.py, Graphed.forward).

So:

capture A (cache_quantized_params=True)        -> flag tensor created
run A with is_first_microbatch=False           -> flag = "skip the weight update"
capture B (cache_quantized_params=False)       -> B reads A's leftover flag,
                                                  bakes it in as its noop flag
replay B                                       -> B never writes the flag; it stays
                                                  "skip" forever

B's optimizer keeps updating the master weight, but its GEMM keeps using the quantized weight from capture time. No error, no warning — just output that drifts further from eager every step. In a two-Linear repro the graphed output diverges from eager by ~22 in bf16.

This is not specific to any recipe (reproduced on MXFP8, NVFP4, Float8BlockScaling, Float8CurrentScaling and DelayedScaling). It has stayed hidden because the CUDA-graph tests run each case in a fresh process, so no test ever performs two captures with different caching settings.

The fix

The flag tensor cannot be cleared or reallocated between captures: already-captured graphs bake in its address, and freeing it would make an earlier graph read recycled memory — worse than the original bug. So the read is gated instead:

  • FP8GlobalState gains caching_quantized_params, tracking whether the capture in progress asked for caching.
  • get_skip_fp8_weight_update_tensor() returns None unless that is set.
  • make_graphed_callables sets the scope for the duration of the capture and clears it in a finally.
  • The four modules read through the accessor instead of touching quantization_state directly.

The tensor's address, lifetime and write path are unchanged; only its visibility is now limited to the capture that declared it.

Test

test_make_graphed_callables_weight_caching_does_not_leak in tests/pytorch/test_cuda_graphs.py captures with caching, runs one cached microbatch to leave the flag set to "skip", then captures a second module without caching and asserts its graphed outputs match eager across two optimizer steps. Parametrized over every available recipe.

The test was mutation-checked: with the scope check removed at runtime it fails for all 7 recipes, and passes for all 7 with the fix in place. (Note the flag is only written for the first module of an autocast context, so each poisoning microbatch needs its own context — without that the test passes either way and proves nothing.)

Full tests/pytorch/test_cuda_graphs.py on B200: 676 passed, 1121 skipped, 0 failed.

Type of change

  • Bug fix (non-breaking change which fixes an issue)

Checklist

  • I have read and followed the contributing guidelines
  • The functionality is complete
  • I have commented my code, particularly in hard-to-understand areas
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective

@xiuhu17
xiuhu17 requested a review from ksivaman as a code owner August 1, 2026 10:42
@github-actions github-actions Bot added the community-contribution PRs from external contributor outside the core maintainers, representing community-driven work. label Aug 1, 2026
…ture

`make_graphed_callables(cache_quantized_params=True)` creates a process-global
flag tensor that gates quantized weight updates, and never scopes it. The
modules only check `fp8_graph_capturing()` before reading it, so any *later*
capture in the same process picks up the leftover tensor and bakes it in as its
own quantize noop flag. Nothing ever writes that flag for a callable graphed
without caching, so it keeps whatever the earlier capture left there: if that
was "skip", the second module silently reuses a stale quantized weight for the
rest of training while its master weight keeps being updated.

The flag tensor cannot be cleared or reallocated -- already-captured graphs bake
in its address and replay fills it in place -- so gate the read instead. Track
whether the capture in progress requested caching and hand out the tensor only
then, clearing the scope once capture finishes.

Affects every recipe, not a specific one. TE's own graph tests run each case in
a fresh process, which is why this never showed up in CI.

Signed-off-by: zhihaow6 <zhihaow6@illinois.edu>
@xiuhu17
xiuhu17 force-pushed the fix_cache_quantized_params branch from 0888b55 to 4fdb04c Compare August 1, 2026 10:43
@greptile-apps

greptile-apps Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR scopes the process-global quantized-weight update tensor to captures that explicitly enable quantized-parameter caching, preventing later CUDA graphs from silently reusing stale quantized weights.

  • Adds capture-scoped caching state to FP8GlobalStateManager.
  • Routes four fused module implementations through the scoped accessor.
  • Clears the scope after capture, including exceptional exits.
  • Adds a cross-capture regression test covering supported quantization recipes.

Confidence Score: 5/5

The PR appears safe to merge, with the new scope preventing stale quantized-weight state from leaking into subsequent captures.

The caching state is enabled only for the relevant graph construction, module reads are uniformly routed through the gated accessor, cleanup runs when graph construction returns or raises, and the regression test exercises the cross-capture failure sequence.

Important Files Changed

Filename Overview
transformer_engine/pytorch/graph.py Establishes the caching scope immediately before graph construction and clears it in a finally block.
transformer_engine/pytorch/quantization.py Adds capture-scoped state and gates access to the persistent weight-update tensor.
transformer_engine/pytorch/module/linear.py Uses the scoped accessor when resolving the graph-captured weight-update flag.
transformer_engine/pytorch/module/grouped_linear.py Applies the scoped accessor consistently at both grouped-linear weight-caching sites.
transformer_engine/pytorch/module/layernorm_linear.py Uses the scoped accessor for fused normalization and linear graph capture.
transformer_engine/pytorch/module/layernorm_mlp.py Uses the scoped accessor for fused MLP graph capture.
tests/pytorch/test_cuda_graphs.py Adds a regression test that poisons the persistent flag, performs a non-caching capture, and compares training outputs with eager execution across recipes.

Sequence Diagram

sequenceDiagram
  participant User
  participant Graph as make_graphed_callables
  participant State as FP8GlobalStateManager
  participant Module as Quantized module
  User->>Graph: "capture(cache_quantized_params=true)"
  Graph->>State: set caching scope true
  Graph->>State: initialize persistent skip tensor
  Graph->>Module: capture forward
  Module->>State: get scoped skip tensor
  Graph->>State: clear caching scope
  User->>Graph: "later capture(cache_quantized_params=false)"
  Graph->>State: set caching scope false
  Graph->>Module: capture forward
  Module->>State: get scoped skip tensor
  State-->>Module: None
  Graph->>State: clear caching scope
Loading

Reviews (1): Last reviewed commit: "[PyTorch] Scope the quantized-param cach..." | Re-trigger Greptile

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

Labels

community-contribution PRs from external contributor outside the core maintainers, representing community-driven work.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant