Skip to content

Commit 9ea05cf

Browse files
pggPLksivaman
andauthored
[PyTorch] Support FP8 weight caching in compiled Linear (#3505)
* [PyTorch] Support FP8 weight caching in compiled Linear Create a fresh FP8 workspace on the first microbatch so the functional Linear op does not mutate a cached input. Reuse the workspace on the remaining microbatches and preserve the fused weight-gradient accumulation fallback. Add repeated optimizer-step comparisons, deferred backward coverage, and CUDA Graph replay assertions. GPU validation is pending in this draft. Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com> * [PyTorch] Remove redundant microbatch cache identity test Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com> * [PyTorch] Measure microbatch cache memory and mark CUDA Graph minibatches Add an isolated-process memory example covering eager, functional cache refresh, compile and CUDA Graph replay. Mark each complete minibatch as one CUDA Graph iteration so cached weights survive the intervening backward calls. Linear tests: 18 passed, 12 skipped, 1 existing XPASS on RTX Ada. Four 4096-wide layers peak at 452 MiB in eager and 539 MiB in compile after warmup; creating fresh caches in eager does not raise that global peak. Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com> * [PyTorch] Keep memory diagnostics outside the microbatch PR Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com> --------- Signed-off-by: Pawel Gadzinski <pgadzinski@nvidia.com> Co-authored-by: Kirthi Shankar Sivamani <ksivamani@nvidia.com>
1 parent 53d1a3c commit 9ea05cf

2 files changed

Lines changed: 114 additions & 73 deletions

File tree

‎tests/pytorch/test_torch_compile.py‎

Lines changed: 97 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -2238,81 +2238,117 @@ def fn(inp):
22382238

22392239

22402240
@pytest.mark.skipif(not _opaque_available, reason="torch opaque object API not available")
2241-
@pytest.mark.skipif(not fp8_available, reason=reason_for_no_fp8)
2242-
def test_te_linear_compile_is_first_microbatch():
2243-
"""te.Linear with ``is_first_microbatch`` under torch.compile: FP8 weight
2244-
caching updates the cached workspace in place, which the functional custom
2245-
op can't express, so the schedule must fall back to eager -- warning +
2246-
numerics identical to eager, cache reused in place across steps. The eager
2247-
reference runs on a separate module so it cannot mask a corrupted or
2248-
rebuilt cache."""
2249-
dtype = torch.bfloat16
2250-
device = "cuda"
2251-
fp8_recipe = recipe.Float8CurrentScaling()
2252-
model = te.Linear(64, 32, params_dtype=dtype, device=device)
2253-
ref_model = te.Linear(64, 32, params_dtype=dtype, device=device)
2254-
with torch.no_grad():
2255-
ref_model.weight.copy_(model.weight)
2256-
ref_model.bias.copy_(model.bias)
2257-
2258-
schedule = [True, False, False]
2259-
is_first = schedule[0] # rebound each step; closed over by the fns.
2241+
@pytest.mark.parametrize("compile_mode", _compile_modes)
2242+
@pytest.mark.parametrize("deferred_backward", [False, True])
2243+
@pytest.mark.parametrize(
2244+
"fp8_recipe",
2245+
[
2246+
pytest.param(
2247+
recipe.Float8CurrentScaling(),
2248+
marks=pytest.mark.skipif(not fp8_available, reason=reason_for_no_fp8),
2249+
),
2250+
pytest.param(
2251+
recipe.Float8BlockScaling(),
2252+
marks=pytest.mark.skipif(
2253+
not fp8_block_scaling_available, reason=reason_for_no_fp8_block_scaling
2254+
),
2255+
),
2256+
pytest.param(
2257+
recipe.MXFP8BlockScaling(),
2258+
marks=pytest.mark.skipif(not mxfp8_available, reason=reason_for_no_mxfp8),
2259+
),
2260+
pytest.param(
2261+
recipe.NVFP4BlockScaling(disable_stochastic_rounding=True),
2262+
marks=pytest.mark.skipif(not nvfp4_available, reason=reason_for_no_nvfp4),
2263+
),
2264+
],
2265+
ids=recipe_id,
2266+
)
2267+
def test_te_linear_compile_is_first_microbatch(
2268+
fp8_recipe, compile_mode, deferred_backward, monkeypatch
2269+
):
2270+
"""Reuse cached weights across microbatches and refresh after optimizer updates."""
2271+
dtype, device = torch.bfloat16, "cuda"
2272+
model = te.Linear(128, 128, params_dtype=dtype, device=device)
2273+
ref_model = te.Linear(128, 128, params_dtype=dtype, device=device)
2274+
ref_model.load_state_dict(model.state_dict())
22602275

2261-
def fn(inp):
2276+
def fn(inp, is_first):
22622277
with te.autocast(recipe=fp8_recipe):
22632278
return model(inp, is_first_microbatch=is_first)
22642279

2265-
def ref_fn(inp):
2280+
def ref_fn(inp, is_first):
22662281
with te.autocast(recipe=fp8_recipe):
22672282
return ref_model(inp, is_first_microbatch=is_first)
22682283

2269-
# Eager priming: FP8 state must exist before tracing (creating quantizers
2270-
# in-graph breaks later recompiles; upstream Dynamo bug).
2271-
is_first = None
2272-
fn(torch.randn(32, 64, dtype=dtype, device=device, requires_grad=True))
2273-
is_first = schedule[0]
2284+
for forward in (fn, ref_fn):
2285+
inp = torch.randn(128, 128, dtype=dtype, device=device, requires_grad=True)
2286+
forward(inp, None).sum().backward()
22742287

2275-
torch._dynamo.reset()
2276-
compiled = torch.compile(fn)
2288+
replay_count = 0
2289+
original_replay = torch.cuda.CUDAGraph.replay
22772290

2278-
cached_workspace = None
2279-
for step, is_first in enumerate(schedule):
2280-
base = torch.randn(32, 64, dtype=dtype, device=device)
2291+
def replay(graph):
2292+
nonlocal replay_count
2293+
replay_count += 1
2294+
return original_replay(graph)
22812295

2282-
inp_ref = base.detach().clone().requires_grad_(True)
2283-
ref_model.zero_grad(set_to_none=True)
2284-
out_ref = ref_fn(inp_ref)
2285-
out_ref.sum().backward()
2286-
2287-
inp = base.detach().clone().requires_grad_(True)
2288-
model.zero_grad(set_to_none=True)
2289-
if step == 0:
2290-
with pytest.warns(
2291-
UserWarning, match="Falling back to eager execution under torch.compile"
2292-
):
2293-
out = compiled(inp).clone()
2294-
else:
2295-
out = compiled(inp).clone()
2296-
out.sum().backward()
2296+
if compile_mode == "reduce-overhead":
2297+
monkeypatch.setattr(torch.cuda.CUDAGraph, "replay", replay)
2298+
torch._dynamo.reset()
2299+
compiled = torch.compile(fn, fullgraph=True, mode=compile_mode)
22972300

2298-
torch.testing.assert_close(out, out_ref.detach(), atol=_EAGER_ATOL, rtol=_EAGER_RTOL)
2299-
torch.testing.assert_close(inp.grad, inp_ref.grad, atol=_EAGER_ATOL, rtol=_EAGER_RTOL)
2300-
torch.testing.assert_close(
2301-
model.weight.grad, ref_model.weight.grad, atol=_EAGER_ATOL, rtol=_EAGER_RTOL
2302-
)
2301+
with _assert_no_cudagraph_skips(compile_mode == "reduce-overhead"):
2302+
for step in range(5):
2303+
if compile_mode == "reduce-overhead":
2304+
torch.compiler.cudagraph_mark_step_begin()
2305+
model.zero_grad(set_to_none=True)
2306+
ref_model.zero_grad(set_to_none=True)
2307+
pending = []
2308+
replays_before = replay_count
2309+
for microbatch in range(3):
2310+
base = torch.randn(128, 128, dtype=dtype, device=device)
2311+
inp_ref = base.detach().clone().requires_grad_(True)
2312+
inp = base.detach().clone().requires_grad_(True)
2313+
is_first = microbatch == 0
2314+
out_ref = ref_fn(inp_ref, is_first)
2315+
out = compiled(inp, is_first).clone()
2316+
torch.testing.assert_close(out, out_ref, atol=_EAGER_ATOL, rtol=_EAGER_RTOL)
2317+
2318+
workspace = model._fp8_workspaces["weight"]
2319+
if is_first:
2320+
cached_workspace = workspace
2321+
else:
2322+
assert workspace is cached_workspace
2323+
torch.testing.assert_close(
2324+
workspace.dequantize(),
2325+
ref_model._fp8_workspaces["weight"].dequantize(),
2326+
atol=_EAGER_ATOL,
2327+
rtol=_EAGER_RTOL,
2328+
)
23032329

2304-
workspace = model._fp8_workspaces.get("weight")
2305-
assert workspace is not None, f"no cached FP8 weight after step {step}"
2306-
if step == 0:
2307-
cached_workspace = workspace
2308-
else:
2309-
assert workspace is cached_workspace, f"cache rebuilt at step {step}"
2330+
pending.append((out, out_ref, inp, inp_ref))
2331+
if not deferred_backward or microbatch == 2:
2332+
for result, ref_result, input_tensor, ref_input in pending:
2333+
ref_result.sum().backward()
2334+
result.sum().backward()
2335+
torch.testing.assert_close(
2336+
input_tensor.grad, ref_input.grad, atol=_EAGER_ATOL, rtol=_EAGER_RTOL
2337+
)
2338+
for param, ref_param in zip(model.parameters(), ref_model.parameters()):
2339+
torch.testing.assert_close(
2340+
param.grad, ref_param.grad, atol=_EAGER_ATOL, rtol=_EAGER_RTOL
2341+
)
2342+
pending.clear()
2343+
if step == 4 and compile_mode == "reduce-overhead":
2344+
assert replay_count > replays_before, "CUDA graphs were recorded but never replayed"
2345+
with torch.no_grad():
2346+
for param, ref_param in zip(model.parameters(), ref_model.parameters()):
2347+
param.add_(param.grad, alpha=-0.001)
2348+
ref_param.add_(ref_param.grad, alpha=-0.001)
2349+
del cached_workspace, workspace, out, out_ref, result, ref_result
23102350

23112351
torch._dynamo.reset()
2312-
compiled_fg = torch.compile(fn, fullgraph=True)
2313-
is_first = True
2314-
with pytest.raises(Exception, match=re.escape("FP8 weight caching")):
2315-
compiled_fg(torch.randn(32, 64, dtype=dtype, device=device, requires_grad=True))
23162352

23172353

23182354
@pytest.mark.skipif(not _opaque_available, reason="torch opaque object API not available")

‎transformer_engine/pytorch/module/linear.py‎

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -211,11 +211,8 @@ def compile_unsupported_reason(self) -> Optional[str]:
211211
# A quantized dgrad can't cross the op boundary: grads are packed
212212
# one plain Tensor[] slot each (_pack_bwd_result).
213213
return "a quantized input grad (fp8_grad=True)"
214-
if self.cache_weight and self.fp8:
215-
# The cached workspace is updated in place on the first microbatch,
216-
# which the functional op (mutates_args=()) can't express. Without
217-
# FP8 no workspace exists, so is_first_microbatch is inert.
218-
return "FP8 weight caching (is_first_microbatch)"
214+
if self.fp8 and self.skip_fp8_weight_update is not None:
215+
return "GPU-controlled FP8 weight cache updates"
219216
if self.fuse_wgrad_accumulation:
220217
return "fuse_wgrad_accumulation (main_grad)"
221218
for quantizer in (
@@ -2389,6 +2386,13 @@ def forward(
23892386
* it also allows skipping gradient accumulation during the
23902387
first microbatch (since it is the first gradient being
23912388
produced)
2389+
2390+
Under ``torch.compile``, the FP8 weight cache is replaced on
2391+
the first microbatch and reused on subsequent microbatches.
2392+
``fuse_wgrad_accumulation=True`` remains unsupported.
2393+
With ``mode="reduce-overhead"``, call
2394+
``torch.compiler.cudagraph_mark_step_begin()`` once before
2395+
each minibatch to keep the cache live across its microbatches.
23922396
"""
23932397
is_grad_enabled = torch.is_grad_enabled()
23942398

@@ -2415,7 +2419,7 @@ def forward(
24152419

24162420
if torch.compiler.is_compiling() and _linear_op is not None:
24172421
reason = self._compile_eager_fallback_reason(
2418-
inp, is_first_microbatch, fp8_output, fp8_grad, is_grad_enabled, debug
2422+
inp, skip_fp8_weight_update, fp8_output, fp8_grad, is_grad_enabled, debug
24192423
)
24202424
if reason is not None:
24212425
# A break inside the try/finally below would skip the whole frame.
@@ -2460,9 +2464,10 @@ def forward(
24602464
set_quantizer_amax_reduction_group(quantizer, None)
24612465

24622466
cache_name = None if (is_first_microbatch is None or self.is_fsdp2) else "weight"
2463-
weight_workspace = (
2464-
self._fp8_workspaces.get(cache_name) if cache_name is not None else None
2465-
)
2467+
# A compiled refresh must not pass the old cache into the functional op.
2468+
weight_workspace = None
2469+
if cache_name is not None and not (use_compiled_op and is_first_microbatch):
2470+
weight_workspace = self._fp8_workspaces.get(cache_name)
24662471

24672472
dgrad_use_split_accumulator = _2X_ACC_DGRAD
24682473
wgrad_use_split_accumulator = _2X_ACC_WGRAD
@@ -2659,7 +2664,7 @@ def _get_weight_tensors(self) -> List[Union[torch.Tensor, QuantizedTensorStorage
26592664
def _compile_eager_fallback_reason(
26602665
self,
26612666
inp: torch.Tensor,
2662-
is_first_microbatch: Optional[bool],
2667+
skip_fp8_weight_update: Optional[torch.Tensor],
26632668
fp8_output: bool,
26642669
fp8_grad: bool,
26652670
is_grad_enabled: bool,
@@ -2698,8 +2703,8 @@ def _compile_eager_fallback_reason(
26982703
and not (self.ub_overlap_rs_dgrad or self.ub_bulk_wgrad)
26992704
):
27002705
return "a quantized input grad (fp8_grad=True)"
2701-
if fp8 and is_first_microbatch is not None and not self.is_fsdp2:
2702-
return "FP8 weight caching (is_first_microbatch)"
2706+
if fp8 and skip_fp8_weight_update is not None:
2707+
return "GPU-controlled FP8 weight cache updates"
27032708
return None
27042709

27052710
@torch._dynamo.disable

0 commit comments

Comments
 (0)