Skip to content

[Dev] Add batched gemm for DSv4HybridAttention - #5722

Open
yaox12 wants to merge 15 commits into
NVIDIA:devfrom
yaox12:xiny/batched_gemm
Open

yaox12 wants to merge 15 commits into
NVIDIA:devfrom
yaox12:xiny/batched_gemm

Conversation

@yaox12

@yaox12 yaox12 commented Jul 9, 2026 •

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

What does this PR do?

Use transformer_engine.pytorch.modules.BatchedLinear for DSv4HybridAttention.linear_o_group_proj, which

  1. supports MXFP8 and BF16
  2. eliminates the .contiguous() call after the group projection.

Issue tracking

For PRs from open-source community contributors:

  • New features: a linked issue is required. Please open a feature request and reference it here before submitting the PR.
  • Small updates (bug fixes, minor improvements): a linked issue is recommended and will accelerate the PR review process.

Linked issue:

Contribution process

Pre-checks

  • I have added relevant unit tests
  • I have added relevant functional tests
  • I have added proper typing to my code Typing guidelines
  • I have added relevant documentation
  • I have run the autoformatter.sh on my PR

Code review

Feel free to message or comment @NVIDIA/mcore-oncall to help accelerate your merge into main. The less complex your PR is, the faster it will be approved and merged!

All PRs start as draft. If you open a non-draft PR, it will be automatically converted to draft.

Step 1: Mark PR as "Ready for Review"

  1. When your PR is ready, click Ready for Review.
  2. An oncall reviewer is auto-assigned and expert reviewers are notified based on your changes.
    • Some PRs may jump straight to step 2. This is determined by .github/CODEOWNERS.

⚠️ Only mark as ready once merge-conflicts are resolved and the CI is passing.
Final Review might get declined if these requirements are not fulfilled.

Step 2: Final Review

For PRs that change megatron/core, once all expert reviewers have approved, the Final Review label is applied automatically and final reviewers are assigned.

For PRs outside megatron/core, this step is skipped.

Step 3: Approved

Once all required reviewers have approved, the Approved label is applied automatically.

Merge

Any member of mcore-engineers will be able to merge your PR.

@yaox12
yaox12 requested review from a team as code owners July 9, 2026 07:15
@copy-pr-bot

copy-pr-bot Bot commented Jul 9, 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.

@yaox12
yaox12 marked this pull request as draft July 9, 2026 07:23
def _apply_linear_o_group_proj(self, input_: torch.Tensor) -> torch.Tensor:
"""Apply TE BatchedGEMM when available, otherwise use the legacy einsum."""
op = self._get_linear_o_group_proj_op()
if op is None:

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.

Can we add a linear_o_group_proj to DSv4HybridSelfAttentionSubmodules spec to select the TE op?

I think the silent fallback makes it not easy to locate some issues.

@yaox12

yaox12 commented Jul 13, 2026

Copy link
Copy Markdown
Member Author

/claude strict-review

@claude

claude Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Strict Review Summary

Findings by severity: CRITICAL: 0 · IMPORTANT: 0 · SUGGESTION: 1

Assessment

This is a well-scoped, well-tested change that swaps the grouped output projection (DSv4HybridAttention.linear_o_group_proj) to transformer_engine.pytorch.BatchedLinear (enabling MXFP8/BF16 and removing the post-projection .contiguous()), while keeping the legacy einsum path as a fallback when TE BatchedLinear is unavailable.

I traced the areas most likely to hide bugs and found them handled correctly:

  • Checkpoint backward compatibility — the _linear_o_group_proj_state_dict_hook re-flattens the runtime 3D weight (linear_o_group_proj.weight) back to the legacy 2D linear_o_group_proj key, and _load_from_state_dict accepts both 2D legacy and 3D runtime layouts. _extra_state is popped on save and setdefault(..., None) on load, so TE-produced state stays out of the canonical checkpoint. Verified against the new test_o_group_proj_legacy_checkpoint_compatibility (BF16 + quantized).
  • Distributed checkpointing — sharded_state_dict wraps the weight in a ShardedTensorFactory that rewrites local/global shapes and offsets from the 3D (groups, rank, in_features) layout to the flattened 2D (groups*rank, in_features) layout, with an assert that sharding only happens along group/in-feature dims (rank axis must be un-sharded, offset 0, fragmentation 1). This mirrors the established apply_swiglu_sharded_factory / apply_head_wise_attn_gate_sharded_factory pattern and flows correctly through make_sharded_optimizer_tensor for the distributed optimizer (verified via get_param_id_to_sharded_param_map in the test).
  • Numerics / gradients — the new test_o_group_proj_numerics_and_gradients checks forward + input/weight grads against the einsum reference for BF16 and MXFP8 with appropriate tolerances.
  • TE availability guarding — getattr(te, "pytorch", None) / getattr(te_pytorch, "BatchedLinear", None) degrade gracefully to the einsum path; native-parity test updated to accept both parameter names.

Most impactful note

One minor [SUGGESTION Simplification] (inline): the einsum-fallback _apply_linear_o_group_proj does a no-op .reshape(...) of an already-3D weight on every forward — safe to drop.

Risk level: Low. No correctness, performance, or backward-compatibility concerns. The change is confined to an experimental attention variant, preserves checkpoint format across both implementations, and is covered by targeted unit tests.

yaox12 added 3 commits July 15, 2026 20:03
Signed-off-by: Xin Yao <xiny@nvidia.com>
Signed-off-by: Xin Yao <xiny@nvidia.com>
Signed-off-by: Xin Yao <xiny@nvidia.com>
@yaox12
yaox12 force-pushed the xiny/batched_gemm branch from 7f8f133 to 37937e6 Compare July 16, 2026 05:49
@yaox12
yaox12 marked this pull request as ready for review July 16, 2026 08:36
@hxbai

hxbai commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

/claude strict-review

@claude

claude Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Strict Review Summary — PR #5722: Add batched gemm for DSv4HybridAttention

Findings by severity — CRITICAL: 1, IMPORTANT: 0, SUGGESTION: 0

This is a carefully engineered PR. Swapping the raw grouped nn.Parameter + torch.einsum for te.pytorch.BatchedLinear (with a clean einsum fallback + warning when TE is too old) is sound, the MXFP8/BF16 numerics + gradient tests are thorough, and the substantial state-dict shim work correctly preserves the legacy 2D checkpoint layout across state_dict/load_state_dict/sharded_state_dict. The forward-path change (apply_linear_o_group_proj) is behavior-preserving, and the einsum equation is unchanged. No backward-compat issue for existing checkpoints in the plain state_dict path, and no new direct parallel_state.get*_group() reads.

Most impactful finding:

[CRITICAL Correctness] The sharded_state_dict build_fn unconditionally reshapes to the full 2D checkpoint_local_shape. Under the DEFAULT distributed-optimizer sharding (fully_sharded_model_space), the optimizer wraps this factory around flat, partial DP-shard state with flattened_range set — a full-shape reshape either raises a numel-mismatch RuntimeError (weight straddling a DP-shard boundary) or produces 2D data with a stale non-None flattened_range. The established precedent (ssm/mamba_mixer.py::_split_tensor_factory) handles this by narrowing/keeping data flat and passing flattened_range through. Suggested fix: skip the reshape when flattened_range is not None (the two leading axes merge contiguously, so flat element order is identical), and add a distopt save/load test — the current test only exercises factory.build() with full-shape data.

Risk assessment: Medium. The core BatchedLinear integration and non-distributed checkpoint paths are well-tested and low-risk. The risk concentrates in the one distributed-optimizer save path above, which is the default distopt sharding type and is not covered by the added tests. Recommend resolving the CRITICAL finding (and adding distopt coverage) before merge.

def build_fn(
key: str, tensor: torch.Tensor, replica_id: ReplicaId, flattened_range: Optional[slice]
) -> ShardedTensor:
checkpoint_tensor = tensor.reshape(checkpoint_local_shape)

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.

[CRITICAL Correctness] build_fn unconditionally reshapes to the full 2D checkpoint_local_shape, which breaks distributed-optimizer checkpoint save under the default sharding type (fully_sharded_model_space → sharded_param_state_fs_model_space; see distrib_optimizer.py line 1503 default and 1563).

In that path the optimizer wraps this factory around flat, partial state via replace(sharded_metadata, data=state_ten, flattened_range=item_slice) (distrib_optimizer.py lines 2026–2035), where state_ten is a 1D DP-shard slice whose numel is param_range.end - param_range.start — i.e. only the portion of this parameter residing on the local DP rank. When the grouped-proj weight straddles a DP-shard boundary, tensor.reshape(checkpoint_local_shape) raises a RuntimeError (numel mismatch); even when it doesn't straddle, you end up with 2D-shaped data carrying a non-None flattened_range, which is inconsistent for the save machinery.

Contrast:

  • sharded_param_state_fully_reshardable reshapes to the full shape before wrapping and asserts flattened_range is None (lines 1770, 1805), so a full-shape reshape is valid there.
  • The established reshape/split factory in ssm/mamba_mixer.py (_split_tensor_factory) handles the flat case by using ShardedTensor.narrow and passing flattened_range through — it never reshapes flat partial data to the full multi-dim shape.

The previous plain-nn.Parameter implementation produced a normal ShardedTensor that the optimizer handled natively, so this is a regression on the default distopt save path for DSv4HybridAttention.

Fix: the (groups, rank, in)→(groups*rank, in) reshape merges the two leading axes contiguously, so the flat element order is identical. When flattened_range is not None, skip the reshape and keep tensor flat, building the checkpoint ShardedTensor with local_shape=checkpoint_local_shape and the passed-through flattened_range (mirror the mamba/narrow pattern). Only reshape in the non-flattened case. Please also add a distributed-optimizer save/load test (the current sharded_state_dict test only exercises factory.build() with full-shape weight data, not the flat optimizer-shard path).

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

The previous implementation did assume full-shaped factory data, so it would fail if invoked with a flat partial optimizer state.

After tracing the checkpoint entry point, I changed the fix slightly instead of adding DSv4-specific support for fully_sharded_model_space:

  • fully_sharded_model_space is only the fallback when DistributedOptimizer.sharded_state_dict() is called without checkpoint metadata.
  • The normal checkpoint path sets dp_reshardable by default, or fully_reshardable when requested. dp_reshardable does not invoke this parameter factory, while fully_reshardable passes full-shaped state with flattened_range=None.
  • fully_sharded_model_space is deprecated and is already incompatible globally because ShardedTensor.validate_metadata_integrity() rejects every non-None flattened_range. Supporting it only for this DSv4 parameter would therefore not make a complete optimizer checkpoint valid.

I simplified the implementation accordingly:

  • The fallback path now retains the original 2D Parameter, so it uses the existing state-dict and distributed-checkpoint behavior directly.
  • Only the TE BatchedLinear path installs the 3D-to-2D compatibility factory.
  • The factory supports the active non-flat checkpoint formats, preserves PP/FSDP group-axis metadata, and explicitly rejects the deprecated flat format.

I also added coverage for the fallback 2D ShardedTensor path and pipeline-offset metadata. The full test file passes on GB200 with two ranks (35 passed on each rank), including BF16, MXFP8, quantized model initialization, legacy checkpoint loading, and the einsum fallback.

If fully_sharded_model_space needs to be restored, I think it should be addressed globally in the distributed-checkpoint/optimizer implementation rather than with a DSv4-specific irregular-shard conversion.

@hxbai hxbai mentioned this pull request Jul 30, 2026
14 of 21 tasks
yaox12 added 2 commits August 3, 2026 15:01
@yaox12

yaox12 commented Aug 3, 2026

Copy link
Copy Markdown
Member Author

/claude strict-review

@claude claude Bot left a comment

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.

Strict review passed — no significant issues found. LGTM

Traced the full change: TE BatchedLinear replaces the hand-rolled Parameter+einsum grouped output projection, with a warning-guarded einsum fallback for older TE.

Verified:

  • Backward-compat checkpoints: the legacy 2D linear_o_group_proj key is preserved on save (state-dict hook) and load (_load_from_state_dict reshape) in both directions; dist-checkpoint mapping via ShardedTensorFactory (3D group-axis → 2D axis-0). Covered by tests for BF16 and MXFP8 quantized weights, plus a sharded_offsets case.
  • Init parity: _initialize_grouped_output_projection applies init_method on a view(-1, in), matching the old 2D init exactly.
  • Numerics + gradients: TE path matches the einsum reference for BF16 and MXFP8 (fwd + input/weight grads).
  • _extra_state drop is safe: delayed-scaling FP8 is excluded and only MXFP8/BF16 are targeted (no persistent amax history), so nothing is lost.
  • No new global process-group reads; no unused new identifiers (the te pylint disable is correctly removed now that te.pytorch is read).

Findings — CRITICAL: 0, IMPORTANT: 0, SUGGESTION: 0. Risk level: low. Comprehensive unit coverage (numerics, gradients, checkpoint round-trip, sharded state dict, fallback path).

@hxbai

hxbai commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

/ok to test cbbd9ea

@yaox12

yaox12 commented Aug 26, 2026

Copy link
Copy Markdown
Member Author

/ok to test 03d463c

Signed-off-by: Xin Yao <xiny@nvidia.com>
@yaox12

yaox12 commented Aug 26, 2026

Copy link
Copy Markdown
Member Author

/ok to test 1b641a8

Signed-off-by: Xin Yao <xiny@nvidia.com>
hxbai added a commit to hxbai/Megatron-LM that referenced this pull request Oct 12, 2026
Signed-off-by: Hongxiao Bai <hongxiaob@nvidia.com>
@yaox12

yaox12 commented Oct 22, 2026

Copy link
Copy Markdown
Member Author

/resolve-conflict

yaox12 and others added 2 commits October 26, 2026 00:31
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Xin Yao <xiny@nvidia.com>
…tch_dim rename

- Add get_dsv4_o_group_proj_spec() and use it in both the config-aware DSv4
  attention spec and HybridModel's static hybrid_stack_spec (csa_layer and
  csa_qk_layernorm_layer), which previously had no linear_o_group_proj and
  silently fell back to torch.einsum.
- hybrid_dsv4_stack_spec: also override csa_qk_layernorm_layer, so 'C' layers
  with qk_layernorm use the config-aware DSv4 attention.
- Pass input_batch_dim to te.ops.BatchedLinear (renamed from batch_dim in TE).
- Tests: cover csa_qk_layernorm_layer, move attention to CUDA before
  BatchedLinear forward, compare MXFP8 weights on the same device, and patch
  the static spec in the einsum-fallback test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch was previously deployed

2 inactive (outdated) deployments
public — 1b641a88 Deployed Aug 26, 2026 by copy-pr-bot[bot] via finalize / publish-docs #7376
test — 1b641a88 Deployed Aug 26, 2026 by copy-pr-bot[bot] via cicd-wait-in-queue #20130
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.

5 participants