Skip to content

[PyTorch] Fix DistributedWeight handling under saved-tensor hooks and in the fused grouped MLP - #3545

Open
xrennvidia wants to merge 3 commits into
NVIDIA:mainfrom
xrennvidia:xren/dist-weight-saved-tensor-hooks
Open

xrennvidia wants to merge 3 commits into
NVIDIA:mainfrom
xrennvidia:xren/dist-weight-saved-tensor-hooks

Conversation

@xrennvidia

@xrennvidia xrennvidia commented Sep 18, 2026 •

Copy link
Copy Markdown
Collaborator

Description

Three fixes for distributed (e.g. GTP-sharded) weights in the PyTorch frontend,
all surfaced while running Megatron-Core MoE experts with fine-grained
activation offloading and per-module quantization recipes.

  1. Linear and GroupedLinear lose the DistributedWeight subclass in
    backward when torch.autograd.graph.saved_tensors_hooks are installed:
    autograd unpacks the saved parameter as a fresh plain tensor, so backward
    takes the plain-parameter branch and fails with
    "weight was removed while fuse_wgrad_accumulation=True". Keep the
    DistributedWeight objects on the backward args and prefer them over the
    saved tensors, on both the split-quantize and native grouped-tensor paths.
  2. The fused grouped-MLP op assumed gathered distributed weights were already
    quantized, which fails for a module whose per-module recipe does not create
    native-quantized params (e.g. MXFP8 experts under a global NVFP4 recipe).
    Gather first, then quantize whatever is still unquantized with the op's
    per-expert weight quantizers, mirroring module/linear.py. The per-expert weight
    quantizers are saved on the forward context and used in backward, so a later
    forward under a different (or no) recipe cannot reset them out from under an
    earlier backward.
  3. The fused grouped-MLP save path kept NVFP4 rowwise input data and scales
    that no wgrad path reads. Prune them as already done for MXFP8. Per decoder
    expert layer of a Nemotron hybrid proxy (83k tokens, hidden 3072) this
    removes ~136 MiB from the ~1.9 GiB saved set with unchanged numerics.

Type of change

  • Documentation change (change only to the documentation, either a fix or a new content)
  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Infra/Build change
  • Code refactoring

Changes

  • module/linear.py, module/grouped_linear.py: keep DistributedWeight
    objects on LinearBwdArgs / GroupedLinearBwdArgs /
    GroupedLinearGraphSafeBwdArgs and use them in backward.
  • ops/fused/grouped_mlp.py: gather distributed weights before quantizing;
    quantize re-gathered BF16 weights with the per-expert quantizers, save those
    quantizers on the FC1/FC2 contexts for backward; move the FC2 backward
    re-gather ahead of the NVFP4 amax computation; drop NVFP4 rowwise input
    data and scales from the saved state.
  • New tests, all registered in qa/L0_pytorch_unittest/test.sh:
    • tests/pytorch/test_module_distributed_weight_saved_tensor_hooks.py:
      Linear and both GroupedLinear paths with identity saved-tensor hooks,
      using an in-repo fake DistributedWeight implementer. The grouped-tensor
      case skips when the native path is unsupported on the device / cuBLASLt.
    • tests/pytorch/test_ops_grouped_mlp_distributed_weight.py: fused grouped
      MLP with BF16 and MXFP8 fake distributed weights under per-module MXFP8,
      optionally inside a global NVFP4 autocast; bit-exact against a plain reference
      up to the fake hooks' scales plus a backward after a second forward under no
      recipe or NVFP4..
    • tests/pytorch/test_ops_grouped_mlp_nvfp4_saved_tensors.py: exactly one
      FP4 data and one scale tensor saved per quantized input on the fused NVFP4
      path, for group sizes 1 and 4. The single-group dense path saves a GroupedTensor
      wrapper, whose buffers are inspected directly.

Checklist:

  • I have read and followed the contributing guidelines
  • The functionality is complete
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes

@github-actions github-actions Bot added the community-contribution PRs from external contributor outside the core maintainers, representing community-driven work. label Sep 18, 2026
@greptile-apps

greptile-apps Bot commented Sep 18, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 5/5

[High impact] The PR appears safe to merge; no new actionable issue was established.

Summary

This PR keeps distributed-weight objects available during backward and fixes gathered-weight quantization in the fused grouped MLP.

  • Linear layers keep distributed weights available during backward.
  • The fused grouped MLP quantizes gathered weights with each expert’s recipe.
  • The fused grouped MLP saves only the input layout used for weight gradients.

The earlier coverage thread was manually resolved without explanation. The current tests cover that concern and skip unsupported native grouped execution rather than counting a fallback as native coverage. The earlier recipe-change concern is addressed by saving the quantizers on the forward context.

Review was based on code inspection; GPU tests were not run.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Distributed weight shards] --> B[Gather full weights]
  B --> C{Already quantized?}
  C -->|Yes| D[Use gathered weights]
  C -->|No| E[Quantize with expert quantizers]
  E --> D
  D --> F[Fused grouped MLP forward]
  F --> G[Save expert quantizers and columnwise inputs]
  G --> H[Backward gathers weights again]
  H --> I[Quantize plain weights with saved quantizers]
  I --> J[Compute gradients]
Loading

Reviews (19) · Last reviewed commit: "[PyTorch] Drop NVFP4 rowwise input copie..." · Reviewed by Greptile

Comment thread transformer_engine/pytorch/module/grouped_linear.py Outdated
@xrennvidia
xrennvidia force-pushed the xren/dist-weight-saved-tensor-hooks branch from 45d5cb7 to eea3da3 Compare September 18, 2026 09:16
@fanshiqing

Copy link
Copy Markdown
Member

LGTM, thanks for the fix!

@fanshiqing fanshiqing linked an issue Sep 23, 2026 that may be closed by this pull request
@ksivaman

Copy link
Copy Markdown
Member

@xrennvidia Can you rebase this with main? #3517 has been merged

@xrennvidia

Copy link
Copy Markdown
Collaborator Author

@xrennvidia Can you rebase this with main? #3517 has been merged

Done.

@greptile-apps

This comment has been minimized.

Comment thread tests/pytorch/test_module_distributed_weight_saved_tensor_hooks.py
@greptile-apps

greptile-apps Bot commented Sep 28, 2026

Copy link
Copy Markdown
Contributor

Want your agent to iterate on Greptile's feedback? Try greploops.

@xrennvidia
xrennvidia force-pushed the xren/dist-weight-saved-tensor-hooks branch from 9d8b650 to dddf418 Compare September 28, 2026 13:32
Comment thread transformer_engine/pytorch/module/grouped_linear.py Outdated
@ksivaman

Copy link
Copy Markdown
Member

/te-ci pytorch L0

@xrennvidia
xrennvidia force-pushed the xren/dist-weight-saved-tensor-hooks branch from daefe2e to c594c31 Compare October 18, 2026 06:53
…ooks

_Linear and _GroupedLinear save the DistributedWeight parameter itself for
backward and gate their distributed-weight path on getting that object back
(is_distributed_weight(saved_weight)). That holds only while no
torch.autograd.graph.saved_tensors_hooks are installed: with hooks active,
SavedVariable::unpack rebuilds a fresh plain tensor from the unpack hook's
result, dropping the Python subclass and its is_distributed_weight marker.
The backward then takes the plain-parameter branch, whose weakrefs point at
the transient all-gathered weights, and fails with
"weight was removed while fuse_wgrad_accumulation=True".

Seen with Megatron-Core fine-grained activation offloading (which installs
such hooks around expert fc1/act) on bf16 GTP-sharded GroupedLinear experts.
Quantized weights were unaffected because their storage objects already stay
on ctx via prepare_for_saving/restore_from_saved, and the op-fuser path reads
the module's own weight attribute instead of the saved tensor.

Keep the DistributedWeight objects as ordinary LinearBwdArgs /
GroupedLinearBwdArgs / GroupedLinearGraphSafeBwdArgs references and prefer
them over the saved tensors in backward, on both the split-quantize and the
native grouped-tensor GroupedLinear paths.

Add tests/pytorch/test_module_distributed_weight_saved_tensor_hooks.py, using
an in-repo fake DistributedWeight implementer, covering Linear and both
GroupedLinear paths with identity saved-tensor hooks installed. The
grouped-tensor case skips when the native path is unsupported on the device
or cuBLASLt rather than silently duplicating the split-quantize case.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Signed-off-by: Xiaowei Ren <[email protected]>
@xrennvidia
xrennvidia force-pushed the xren/dist-weight-saved-tensor-hooks branch from c594c31 to 7638a3c Compare October 18, 2026 06:57
@xrennvidia
xrennvidia requested a review from timmoon10 as a code owner October 18, 2026 07:05
@xrennvidia xrennvidia changed the title [PyTorch] Keep DistributedWeight objects on ctx across saved-tensor hooks [PyTorch] Fix DistributedWeight handling under saved-tensor hooks and in the fused grouped MLP Oct 18, 2026
Comment thread transformer_engine/pytorch/ops/fused/grouped_mlp.py Outdated
xrennvidia and others added 2 commits October 18, 2026 00:35
A distributed (e.g. GTP) weight whose shards are not natively quantized is
re-gathered as BF16. The fused grouped-MLP op assumed gathered weights were
already quantized: forward quantized the shards before the gather, and backward
read _columnwise_data / the NVFP4 amax straight off the gathered tensor, which
fails for a module whose per-module recipe (e.g. MXFP8 experts under a global
NVFP4 recipe) does not create native-quantized params.

Gather first, then quantize whatever is still unquantized with the op's
per-expert weight quantizers, mirroring module/linear.py's handling of a
re-gathered BF16 distributed weight. Native-quantized shards gather as quantized
tensors and pass through unchanged. The FC2 backward re-gather moves ahead of
the NVFP4 amax / global-scale computation so that path no longer reads
un-gathered shards.

The per-expert weight quantizer lists are built once in forward and saved on
the FC1/FC2 contexts; backward quantizes the re-gathered weights with those
rather than reading the live op. A later forward on the same module under a
different recipe, or with quantization disabled, resets the ops' quantizers
before the earlier backward runs (pipelined microbatches, or a module shared
across recipe scopes), which would otherwise hit None or quantize in the wrong
format.

Regression tests: a fake routed-expert DistributedWeight group whose materialize
hooks return BF16 tensors (or MXFP8 tensors for the native-quantized case)
drives the CuTeDSL fused grouped MLP under per-module MXFP8, optionally inside a
global NVFP4 autocast. Distinct power-of-two scales in the forward/backward
hooks make the gathered weights observable: output, dgrad and the main_grad
wgrads must match a plain reference bit-exactly up to those scales. A unit
check confirms already-quantized gathered weights pass through the helper
unchanged. A further case runs a second forward under no recipe or NVFP4
between an MXFP8 forward and its backward and requires bit-exact agreement
with a reference. Fails on the unpatched op with AttributeError on
_columnwise_data in backward; passes with the fix.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Signed-off-by: Xiaowei Ren <[email protected]>
…saved state

Every wgrad path reads only the columnwise tiles of the saved FC1/FC2 inputs:
the cuDNN grouped wgrad kernel, the single-group dense GEMM (general_gemm with
the NT layout unwraps both operands columnwise) and the generic grouped-GEMM
fallback (NVFP4 forces a TN layout by taking the columnwise data for a
non-transposed A and a transposed B, which is the wgrad case). The save path
already pruned the rowwise data and scales for MXFP8 but kept them for NVFP4,
citing the generic fallback, so each quantized NVFP4 input was held (and, under
activation offloading, copied to host and back) twice.

Prune them for NVFP4 as well, unconditionally. The amax tensors are untouched.
Per decoder expert layer of a Nemotron hybrid proxy (83k tokens, hidden 3072)
this removes a 121 MiB data and a 15 MiB scale tensor from the ~1.9 GiB saved
set, with unchanged numerics on both the cuDNN kernel and the forced generic
fallback (NVTE_DISABLE_CUTEDSL_WGRAD_FUSED_GROUPED_MLP=1).

Adds a saved_tensors_hooks test asserting exactly one FP4 data and one scale
tensor is saved per quantized input on the fused NVFP4 path, for group sizes
1 and 4, with token counts chosen so the inputs cannot be confused with weights
in the count, and registers it in the L0 unit-test runner. Grouped storage is
saved as raw buffers; the single-group dense path (cuDNN frontend >= 1.27)
saves a GroupedTensor wrapper whole, so the test inspects the wrapper's buffers
directly and requires its rowwise data and scales to be None.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Signed-off-by: Xiaowei Ren <[email protected]>

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

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.

GTP+TE integration

3 participants