Skip to content

[PyTorch] Add optional caller-provided output/grad-input buffers to GroupedLinear and fused grouped MLP#3161

Merged
timmoon10 merged 11 commits into
NVIDIA:mainfrom
phu0ngng:pyt-gg-w-symm
Jul 17, 2026
Merged

[PyTorch] Add optional caller-provided output/grad-input buffers to GroupedLinear and fused grouped MLP#3161
timmoon10 merged 11 commits into
NVIDIA:mainfrom
phu0ngng:pyt-gg-w-symm

Conversation

@phu0ngng

@phu0ngng phu0ngng commented Jul 1, 2026

Copy link
Copy Markdown
Collaborator

Description

Let callers have an option to supply preallocated buffers for the forward output and backward input gradient of GroupedLinear and the fused grouped MLP, instead of the module/op allocating them internally. The caller can pass a symmetric-memory buffer that is fed directly into TE EP's combine, eliminating the D2D copy from the token buffer into TE EP's internal staging buffer.

Type of change

  • Documentation change
  • Bug fix
  • New feature (non-breaking change which adds functionality)
  • Breaking change
  • Infra/Build change
  • Code refactoring

Changes

  • GroupedLinear.forward: optional out / dgrad_out buffers, wired through the legacy and GroupedTensor paths (incl. FP8). Validated via a shared validate_or_alloc_output helper (shape/dtype/device/contiguous/no-grad).
  • Fusible ops: Sequential.forward takes an op_kwargs mapping (keyed by module or index) that routes per-op kwargs to the target op, used to pass output/grad-input buffers to grouped linear / grouped MLP.
  • Fused grouped MLP: the GEMMs write the output and dgrad directly into the caller buffers for both nvfp4 and MXFP8, with no copy. The GEMM writes only the packed valid rows (per m_splits); any padded tail is left untouched. Buffers may be supplied independently.

Dependency

The in-place MXFP8 path requires a cuDNN frontend change that exposes an optional output tensor on the grouped-GEMM quant wrapper: NVIDIA/cudnn-frontend#338.

Testing

  • test_grouped_linear_caller_output_buffers: covers out/dgrad_out/both across the legacy and GroupedTensor paths _ buffer aliasing, bit-exact match vs. internal allocation, untouched padded tail, and ValueError on shape mismatch.
  • test_grouped_linear_caller_buffers (ops) and test_grouped_mlp_caller_buffers: verify the fused op runs, the output aliases the buffer, and grads match internal allocation.

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

@phu0ngng
phu0ngng requested a review from ksivaman as a code owner July 1, 2026 11:35
@phu0ngng
phu0ngng requested a review from ptrendx July 1, 2026 11:37
@greptile-apps

greptile-apps Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds optional caller-provided preallocated output and grad-input buffers to GroupedLinear (module and ops APIs) and the fused grouped MLP, enabling zero-copy paths into symmetric-memory buffers for expert-parallelism combine operations.

  • GroupedLinear.forward gains out/dgrad_out parameters, routed through both the legacy and GroupedTensor paths with a new _validate_or_alloc_output helper; a mirrored validate_or_alloc_output is added to ops/_common.py for the ops-level API.
  • Sequential.forward gains an op_kwargs parameter that routes per-op keyword arguments to specific BasicOperation instances via the new _resolve_op_kwargs helper.
  • The MXFP8 grouped MLP now passes the caller's buffer as d_tensor to the cuDNN grouped-GEMM quant kernel via as_strided, while the NVFP4 path switches to in-place += bias addition to preserve the buffer alias.

Confidence Score: 5/5

The change is safe to merge; the new buffer-passing paths are well-validated and the GEMM kernel integration is correct and fully additive.

All three code paths (module API, ops legacy, ops GroupedTensor) validate the caller buffer before any GEMM work begins. The MXFP8 in-place kernel path uses a correctly structured as_strided view matching the kernel's native output layout. OperationFuser normalises basic_op_kwargs=None to empty-dict lists before reaching fused op forward methods. Tests cover alias, bit-exact match, padded-tail sentinel, and wrong-shape rejection across all paths.

No files require special attention beyond the two style notes on duplicate validation logic and the error message in _resolve_op_kwargs.

Important Files Changed

Filename Overview
transformer_engine/pytorch/module/grouped_linear.py Adds out/dgrad_out params to GroupedLinear.forward and a private _validate_or_alloc_output static method; wiring through grouped-tensor and legacy paths looks correct.
transformer_engine/pytorch/ops/_common.py Adds validate_or_alloc_output helper for the ops-level API; shape/dtype/device/contiguous/no-grad checks are all present.
transformer_engine/pytorch/ops/basic/grouped_linear.py Adds OUTPUT_BUFFER_KEY/GRAD_INPUT_BUFFER_KEY constants and threads out_buffer/dgrad_out through both forward paths; dgrad_out is saved on ctx for backward retrieval.
transformer_engine/pytorch/ops/fused/grouped_mlp.py MXFP8 forward and backward paths pass the caller buffer as d_tensor via as_strided to the cuDNN kernel; NVFP4 path uses validate_or_alloc_output and switches bias addition to in-place +=. Logic is correct.
transformer_engine/pytorch/ops/sequential.py Adds op_kwargs to Sequential.forward and _resolve_op_kwargs to route kwargs to OperationFuser groups; relies on OperationFuser normalising None to empty-dict lists.
tests/pytorch/test_grouped_linear.py New parametrized test covers out/dgrad_out/both across legacy and GroupedTensor paths, alias check, padded-tail sentinel, bit-exact match, and a wrong-shape negative test.
tests/pytorch/test_fusible_ops.py New test exercises buffer routing through a two-GroupedLinear Sequential, confirming alias, bit-exact match, and shape-mismatch ValueError.
tests/pytorch/test_grouped_mlp.py New test covers MXFP8 and NVFP4 fused grouped MLP with caller buffers; verifies alias, no-sentinel, and bit-exact match against internal-allocation reference.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Caller
    participant Sequential
    participant OperationFuser
    participant GroupedLinear
    participant GEMM Kernel

    Caller->>Sequential: "forward(x, split_sizes, op_kwargs={op: {output: buf}})"
    Sequential->>Sequential: _resolve_op_kwargs(op_kwargs)
    Sequential->>OperationFuser: "__call__(*xs, basic_op_kwargs=[{output: buf}])"
    Note over OperationFuser: normalises None to empty-dict list
    OperationFuser->>GroupedLinear: fuser_forward(basic_op_kwargs)
    GroupedLinear->>GroupedLinear: validate_or_alloc_output(buf, shape, dtype, device)
    GroupedLinear->>GEMM Kernel: GEMM writes into buf in-place
    GEMM Kernel-->>GroupedLinear: result in buf
    GroupedLinear-->>Caller: y aliases buf, no copy
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant Caller
    participant Sequential
    participant OperationFuser
    participant GroupedLinear
    participant GEMM Kernel

    Caller->>Sequential: "forward(x, split_sizes, op_kwargs={op: {output: buf}})"
    Sequential->>Sequential: _resolve_op_kwargs(op_kwargs)
    Sequential->>OperationFuser: "__call__(*xs, basic_op_kwargs=[{output: buf}])"
    Note over OperationFuser: normalises None to empty-dict list
    OperationFuser->>GroupedLinear: fuser_forward(basic_op_kwargs)
    GroupedLinear->>GroupedLinear: validate_or_alloc_output(buf, shape, dtype, device)
    GroupedLinear->>GEMM Kernel: GEMM writes into buf in-place
    GEMM Kernel-->>GroupedLinear: result in buf
    GroupedLinear-->>Caller: y aliases buf, no copy
Loading

Reviews (9): Last reviewed commit: "Merge branch 'main' into pyt-gg-w-symm" | Re-trigger Greptile

Comment thread transformer_engine/pytorch/module/grouped_linear.py
Comment thread tests/pytorch/test_grouped_linear.py
@pggPL
pggPL self-requested a review July 1, 2026 12:59
@phu0ngng
phu0ngng requested a review from timmoon10 as a code owner July 1, 2026 16:07
@phu0ngng
phu0ngng marked this pull request as draft July 1, 2026 16:08
Comment on lines +175 to +176
output: Optional[torch.Tensor] = None,
grad_input: Optional[torch.Tensor] = None,

@timmoon10 timmoon10 Jul 1, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm skeptical of this API because it looks general, but is actually only supported with grouped linear and grouped MLP. How about something like the following:

def forward(
    self,
    input: torch.Tensor,
    *extra_inputs: torch.Tensor,
    extra_kwargs: Optional[Sequence[Optional[dict[str, Any]]]] = None,
):
    ...

    if extra_kwargs is None:
        extra_kwargs = [None] * len(self)

    # Figure out what extra_kwargs go to each module group, and fail if a non-None extra_kwargs goes to a non-basic op

    for module_group in self._module_groups:
            xs = module_group(*xs, basic_op_kwargs=module_group_basic_op_kwargs)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, Tim. Good call. The output and grad_input kwargs overpromised generality. Building on your suggestion, I went with a generic per op kwargs pass through, but keyed by module or index instead of a positional list:

seq(x, split_sizes, probs, split_sizes,
    op_kwargs={fc1: {GRAD_INPUT_BUFFER_KEY: g}, fc2: {OUTPUT_BUFFER_KEY: o}})

Sequential resolves each key to its group and basic op slot and forwards it as basic_op_kwargs, with no new dispatch path, and raises if a key targets a non-fusible op. This keeps the container kwarg-agnostic, so the buffers are now just op specific kwargs, while avoiding None padding and positional coupling.

Let me know what you think about this new design.

# when the value above was materialized into it; otherwise it is copied.
if output_buffer is not None:
output_buffer = validate_or_alloc_output(output_buffer, fc2_out_shape, dtype, device)
if output_buffer.data_ptr() != fc2_out.data_ptr():

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Under what case this condition can be false? It seems to be it is always true, I do not see we feed the user provided buffer to the kernel?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You were right that it was always true for MXFP8, which uses CuTe GEMM.

I pushed the latest commit in which I reworked it so that it does not trigger extra copies: the caller buffer is passed straight into the CuTe GEMM via d_tensor, so the kernel writes output/dgrad in place for both nvfp4 and MXFP8. This needs a cuDNN FE change I made a PR for: NVIDIA/cudnn-frontend#338 (until it lands and the bundled FE is bumped, the in-place MXFP8 path can't run). So for testing, we will need to check out this cuDNN FE manually.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi, the cuDNN FE PR is merged. I will follow up to upgrade the cuDNN FE submodule in TE.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Made a PR to upgrade cuDNN FE commit in TE submodule #3179

@phu0ngng phu0ngng changed the title [PyTorch] Add optional caller-provided out/dgrad_out buffers to GroupedLinear [PyTorch] Add optional caller-provided output/grad-input buffers to GroupedLinear and fused grouped MLP Jul 2, 2026
@phu0ngng
phu0ngng marked this pull request as ready for review July 2, 2026 14:16
phu0ngng and others added 4 commits July 2, 2026 07:33
…ar module and fusible ops

Signed-off-by: Phuong Nguyen <phuonguyen@nvidia.com>
…ping

Signed-off-by: Phuong Nguyen <phuonguyen@nvidia.com>
…uffers, eliminating the D2D copy + cleanup

Signed-off-by: Phuong Nguyen <phuonguyen@nvidia.com>
pre-commit-ci Bot and others added 2 commits July 2, 2026 14:35
@phu0ngng
phu0ngng requested review from YangFei1990 and timmoon10 July 6, 2026 13:16
@phu0ngng

Copy link
Copy Markdown
Collaborator Author

pipeline 57614140

Comment thread transformer_engine/pytorch/ops/fused/grouped_mlp.py
Comment thread transformer_engine/pytorch/module/grouped_linear.py
Comment thread transformer_engine/pytorch/module/grouped_linear.py Outdated
Comment thread transformer_engine/pytorch/module/grouped_linear.py
Comment thread transformer_engine/pytorch/ops/_common.py Outdated
Comment thread transformer_engine/pytorch/ops/basic/grouped_linear.py
Comment thread transformer_engine/pytorch/ops/fused/grouped_mlp.py
Comment thread transformer_engine/pytorch/ops/fused/grouped_mlp.py Outdated
Comment thread transformer_engine/pytorch/ops/fused/grouped_mlp.py Outdated
Comment thread tests/pytorch/test_grouped_mlp.py Outdated
Comment thread transformer_engine/pytorch/ops/sequential.py
Signed-off-by: YangFei1990 <feiw@nvidia.com>
Signed-off-by: YangFei1990 <feiw@nvidia.com>
@timmoon10

Copy link
Copy Markdown
Member

/te-ci pytorch

@timmoon10 timmoon10 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@timmoon10

Copy link
Copy Markdown
Member

/te-ci pytorch

1 similar comment
@timmoon10

Copy link
Copy Markdown
Member

/te-ci pytorch

@timmoon10
timmoon10 merged commit 68493d2 into NVIDIA:main Jul 17, 2026
19 of 24 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants