Skip to content

Improve device-init grouped linear module with single grouped weight support - #3224

Open
zhongbozhu wants to merge 27 commits into
NVIDIA:mainfrom
zhongbozhu:improve_device_grouped_linear
Open

Improve device-init grouped linear module with single grouped weight support #3224
zhongbozhu wants to merge 27 commits into
NVIDIA:mainfrom
zhongbozhu:improve_device_grouped_linear

Conversation

@zhongbozhu

@zhongbozhu zhongbozhu commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator

Description

Need this Mcore PR to make it work: NVIDIA/Megatron-LM#6000

Fixes numerical issues when using single weight for TE module grouped linear. Limit the single weight feature to the grouped tensor API instead of the legacy path.

E2E validation has been done and posted to the Megatron PR.

Note: needs to pay extra attention to whether bias grad and weight grad are generated properly.

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

Please list the changes introduced in this PR:

  • Change A
  • Change B

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 Jul 20, 2026
@zhongbozhu
zhongbozhu force-pushed the improve_device_grouped_linear branch from aa3b9d1 to 47ba66a Compare July 20, 2026 22:36
Comment thread transformer_engine/pytorch/module/grouped_linear.py Outdated
@zhongbozhu
zhongbozhu force-pushed the improve_device_grouped_linear branch from ff7eee2 to a43f70f Compare July 20, 2026 22:53
Comment thread transformer_engine/pytorch/module/grouped_linear.py Outdated
is_grad_enabled = torch.is_grad_enabled()
num_gemms = self.num_gemms

if FP8GlobalStateManager.fp8_graph_capturing():

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.

Note: this code block was deleted because it was duplicated

@zhongbozhu
zhongbozhu marked this pull request as ready for review July 23, 2026 10:52
@zhongbozhu

Copy link
Copy Markdown
Collaborator Author

/te-ci pytorch

@greptile-apps

greptile-apps Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR extends the GroupedLinear module and op-fuser with a use_grouped_tensor parameter, replaces the internal _is_grouped_tensor_path_supported helpers with public functions (is_module_grouped_tensor_path_supported, is_op_fuser_grouped_tensor_path_supported), and wires single-grouped-weight/bias parameters through the native cuBLASLt grouped-GEMM path end-to-end — including forward quantization, backward dgrad/wgrad, delayed-wgrad accumulation, and workspace caching.

  • Single grouped weight/bias support: _prepare_weights_for_grouped_tensor_gemm and _prepare_bias_for_grouped_tensor_gemm handle GroupedTensorStorage parameters directly, avoiding the per-expert split-quantize round-trip that caused numerical issues.
  • Bug fixes: make_grouped_weights no longer early-returns for FP8 current-scaling recipes (fixing wrong-result quantized_model_init case); _apply_delay_wgrad_param_hooks now uses _parameters.get("weight") instead of attribute access (fixing meta-device AttributeError); group_quantize gains an output kwarg for CUDA-graph-safe weight workspace reuse.
  • C++ layer: setup_grouped_gemm_kernel is refactored to use explicit a_is_discrete/c_is_discrete/d_is_discrete flags; MXFP8 scale swizzling correctly derives per-member row geometry for the SAME_BOTH_DIMS representation; zero-row bias-add paths no longer fail on null-pointer CUDA allocations.

Confidence Score: 4/5

  • Safe to merge after reviewing the two P2 comments; the core numerical fix and both previously flagged meta-device / wrong-result bugs are now correctly addressed.
  • The single-grouped-weight forward and backward paths are new, complex, and touch weight quantization, workspace caching, delayed-wgrad accumulation, and CUDA graph replay simultaneously. The previously identified meta-device crash and quantized_model_init wrong-result issues are fixed. The remaining observations are about duplicated path-support logic across two functions and a missing divisibility assertion in CUDA kernel math — neither affects correctness of the paths being added. The breadth of the change (Python forward/backward, C++ GEMM setup kernel, MXFP8 quantisation kernel, pybind extension) warrants an additional read of the _backward_grouped_tensor single-weight branches before merging.
  • transformer_engine/pytorch/module/grouped_linear.py (_backward_grouped_tensor single-grouped-weight branches) and transformer_engine/common/cast/mxfp8/group_quantize_mxfp8.cuh (SAME_BOTH_DIMS scale derivation).

Important Files Changed

Filename Overview
transformer_engine/pytorch/module/grouped_linear.py Core of the PR: adds use_grouped_tensor parameter, replaces _is_grouped_tensor_path_supported with the public is_module_grouped_tensor_path_supported, adds single-grouped-weight/bias handling throughout forward/backward, fixes make_grouped_weights early-return for FP8 current scaling, updates _apply_delay_wgrad_param_hooks to be meta-device safe, and improves backward_dw error messaging for fallback paths.
transformer_engine/pytorch/ops/basic/grouped_linear.py Replaces _is_graph_safe_path_supported with is_op_fuser_grouped_tensor_path_supported, adds _get_packed_bias_tensor and _get_discrete_bias_tensors helpers, gates discrete-weight code paths to prevent single-grouped params from entering the legacy flow; logic is nearly identical to the module version.
transformer_engine/pytorch/csrc/extensions/cast.cpp Adds output parameter to group_quantize for workspace reuse (CUDA graph weight caching); validates shape/quantizer identity before reusing; correctly threads noop_flag_cpp into all quantization modes (MXFP8, FP8 current scaling, FP8 block scaling).
transformer_engine/common/gemm/cublaslt_grouped_gemm.cu Refactors setup_grouped_gemm_kernel to use explicit a_is_discrete/c_is_discrete/d_is_discrete flags instead of inferring from nullptr checks on base pointers; fixes scale-inv pointer path for discrete-A case; relaxes launch_grouped_bias_add null-data check to handle zero-row (null-pointer) CUDA allocations before checking has_data.
transformer_engine/common/cast/mxfp8/group_quantize_mxfp8.cuh Fixes MXFP8 scale swizzling for the SAME_BOTH_DIMS shape representation used by single grouped weights: derives per-member row count and base offset from first_logical_dim / num_tensors, then passes corrected tensor_rows_for_scales to the colwise stage. Division is exact by the uniform-shape invariant but not asserted.
tests/pytorch/test_grouped_linear.py Adds comprehensive test coverage for single grouped weight/bias: numerical parity vs. discrete path across BF16/FP8 recipes, workspace cache persistence, zero-work bias, meta-splits rejection, and quantized_model_init path. _reset_fp8_state fixture now uses delenv instead of setenv("0") to exercise the use_grouped_tensor=None default branch.
tests/pytorch/test_grouped_mlp.py Adds single_grouped_weight and single_grouped_bias parametrisation to the grouped MLP test matrix; guards with is_module_grouped_tensor_path_supported to skip unsupported combinations.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[GroupedLinear.forward] --> B{single_grouped_weight\nor single_grouped_bias?}
    B -- "Yes, use_grouped_tensor=False" --> ERR1[RuntimeError:\nrequires use_grouped_tensor=True]
    B -- "No / Yes + use_grouped_tensor=True" --> C{debug mode?}
    C -- "Yes + single param" --> ERR2[RuntimeError:\ndebug not supported]
    C -- No --> D{use_grouped_tensor AND\nnot excluded features}
    D -- No --> E[_GroupedLinear.forward\nlegacy split-quantize path]
    D -- Yes --> F{Blackwell +\nFP8 block scaling?}
    F -- Yes --> ERR3[RuntimeError:\nunsupported combo]
    F -- No --> G[is_module_grouped_tensor\n_path_supported?]
    G -- "False + single param" --> ERR4[RuntimeError:\ngrouped-tensor path needed]
    G -- "False + discrete" --> E
    G -- True --> H{CUDA m_splits?}
    H -- No --> ERR5[ValueError]
    H -- Yes --> I[_forward_grouped_tensor]
    I --> J{single_grouped_weight?}
    J -- Yes --> K[_prepare_weights_for\n_grouped_tensor_gemm\nGroupedTensorStorage path]
    J -- No --> L[_prepare_weights_for\n_grouped_tensor_gemm\ndiscrete list path]
    K --> M[general_grouped_gemm\n_for_grouped_tensor]
    L --> M
    M --> N{delay_wgrad?}
    N -- Yes --> O[wgrad_store.put\ndefer weight GEMM]
    N -- No --> P[grouped_gemm_wgrad\nimmediate]
    O --> Q[backward_dw later\nunpacks wgrad_output]
    P --> R[Return grad_input\n+ grad_weights\n+ grad_biases]
Loading

Reviews (27): Last reviewed commit: "resolve comments, enable save original i..." | Re-trigger Greptile

Comment thread transformer_engine/pytorch/module/grouped_linear.py
Comment thread tests/pytorch/test_grouped_linear.py Outdated
Comment thread tests/pytorch/test_grouped_mlp.py Outdated

@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.

The biggest change in this PR is that TE is abandoning any attempt to make single_grouped_weight=True a general feature. Things must be exactly right, or we crash. Given how delicate and experimental this feature has been, I'm not opposed.

The second change is that users must opt-in to access the grouped GEMM kernel. This is also reasonable, since it has alignment requirements for m_splits and it's helpful having a way for users to accept that stricter contract.

We are experiencing many test failures. Given that single_grouped_weight is no longer a general feature, I think it's reasonable we move the corresponding tests to test_grouped_linear.py and test_grouped_mlp.py.

Comment on lines +761 to +764
raise ValueError(
"The native grouped_tensor path requires CUDA m_splits. Pass a CUDA int64 "
"tensor, or select grouped_gemm_backend='legacy'."
)

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 get that the h2d memcpy is suboptimal, but it's trivially easy to handle. Erroring out seems excessively rigid.

Suggested change
raise ValueError(
"The native grouped_tensor path requires CUDA m_splits. Pass a CUDA int64 "
"tensor, or select grouped_gemm_backend='legacy'."
)
m_splits = m_splits.to(device=device)

We need to handle the d2h case anyways when the user has specified grouped_gemm_backend="grouped_tensor", but it's not supported and we fallback to split-quantize.

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.

But if the alignment is not provided in the first place, converting it to a device tensor also wouldn't work right, I am okay with another alignment check before adding this H2D copy.

Comment thread transformer_engine/pytorch/module/grouped_linear.py Outdated
@zhongbozhu
zhongbozhu force-pushed the improve_device_grouped_linear branch 3 times, most recently from b6a9482 to 25314b8 Compare July 25, 2026 07:08
Comment thread tests/pytorch/test_grouped_linear.py Outdated
@zhongbozhu

Copy link
Copy Markdown
Collaborator Author

/te-ci pytorch

@zhongbozhu
zhongbozhu force-pushed the improve_device_grouped_linear branch from 07b2f18 to bb6c1b9 Compare July 28, 2026 03:50
: tensor_base;
size_t tensor_base_for_scales = tensor_base;
size_t tensor_rows_for_scales = rows;
if constexpr (WITH_GEMM_SWIZZLED_SCALES && SHAPE_REP == ShapeRepresentation::SAME_BOTH_DIMS) {

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.

Note: this is for single weight quantize for mxfp8.

Before this change, the weight quantizer didn't have the first_dims because moe weights are uniform shape for both dimension. This will then trigger a CUDA illegal access because offsets_ptr=nullptr

@zhongbozhu
zhongbozhu force-pushed the improve_device_grouped_linear branch 2 times, most recently from 2734746 to 5fc5db7 Compare July 28, 2026 09:21
Comment thread transformer_engine/pytorch/csrc/type_converters.cpp Outdated
@zhongbozhu

Copy link
Copy Markdown
Collaborator Author

/te-ci pytorch

zhongbozhu and others added 24 commits August 6, 2026 22:55
Signed-off-by: Zhongbo Zhu <zhongboz@nvidia.com>
Signed-off-by: Zhongbo Zhu <zhongboz@nvidia.com>

# Conflicts:
#	tests/pytorch/test_grouped_mlp.py
Signed-off-by: Zhongbo Zhu <zhongboz@nvidia.com>
Signed-off-by: Zhongbo Zhu <zhongboz@nvidia.com>
Signed-off-by: Zhongbo Zhu <zhongboz@nvidia.com>
Signed-off-by: Zhongbo Zhu <zhongboz@nvidia.com>
Signed-off-by: Zhongbo Zhu <zhongboz@nvidia.com>
Signed-off-by: Zhongbo Zhu <zhongboz@nvidia.com>
Signed-off-by: Zhongbo Zhu <zhongboz@nvidia.com>
Signed-off-by: Zhongbo Zhu <zhongboz@nvidia.com>
Signed-off-by: zhongboz <zhongboz@nvidia.com>
Signed-off-by: zhongboz <zhongboz@nvidia.com>
Signed-off-by: Zhongbo Zhu <zhongboz@nvidia.com>
Signed-off-by: Zhongbo Zhu <zhongboz@nvidia.com>
Signed-off-by: zhongboz <zhongboz@nvidia.com>
Signed-off-by: Zhongbo Zhu <zhongboz@nvidia.com>
Signed-off-by: zhongboz <zhongboz@nvidia.com>
Signed-off-by: zhongboz <zhongboz@nvidia.com>
Signed-off-by: Zhongbo Zhu <zhongboz@nvidia.com>
Signed-off-by: Zhongbo Zhu <zhongboz@nvidia.com>
Signed-off-by: Zhongbo Zhu <zhongboz@nvidia.com>
@zhongbozhu
zhongbozhu force-pushed the improve_device_grouped_linear branch from 45ee6d1 to ddddc3f Compare August 7, 2026 06:02
Signed-off-by: Zhongbo Zhu <zhongboz@nvidia.com>
Comment thread transformer_engine/pytorch/module/grouped_linear.py
Signed-off-by: Zhongbo Zhu <zhongboz@nvidia.com>
@zhongbozhu

Copy link
Copy Markdown
Collaborator Author

/te-ci pytorch L1

A_ptrs[idx] = a_is_discrete
? a_multi_tensor_args.data_ptrs[idx]
: (a_base == nullptr ? nullptr : a_base + (a_offset * a_bits_per_elem) / 8);
B_ptrs[idx] = b_base == nullptr ? nullptr : b_base + (b_offset * b_bits_per_elem) / 8;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Do we need this?

Suggested change
B_ptrs[idx] = b_base == nullptr ? nullptr : b_base + (b_offset * b_bits_per_elem) / 8;
B_ptrs[idx] = b_base + (b_offset * b_bits_per_elem) / 8;

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.

why not, it's nice to have (although the real fix is in A_ptrs[idx])

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Additional thread divergence. Anyway, first_dims and last_dims should already be in the descriptors and they will be zero and we wont read it from it anyway.

Same applies for a_base == null_ptr and and C and D as well

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Real issue was we were computing a_is_discrete and c/d_is_discrete incorrectly

if test_recipe is not None:
grouped_weight = torch.cat(weight_tensors, dim=0)
weight_quantizer = _make_quantizer(
tex.DType.kFloat8E4M3,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Can we use TE_DType instead of tex.DType here. That is the recommendation now for the public user facing APIs.

Comment on lines +660 to +676
elif skip_fp8_weight_update is not None:
grouped_weight = tex.group_quantize(
source,
weight_quantizer,
num_gemms,
None,
noop_flag=skip_fp8_weight_update,
output=workspace,
)
elif update_workspace:
grouped_weight = tex.group_quantize(
source,
weight_quantizer,
num_gemms,
None,
output=workspace,
)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Can we combine the conditions to one?

Suggested change
elif skip_fp8_weight_update is not None:
grouped_weight = tex.group_quantize(
source,
weight_quantizer,
num_gemms,
None,
noop_flag=skip_fp8_weight_update,
output=workspace,
)
elif update_workspace:
grouped_weight = tex.group_quantize(
source,
weight_quantizer,
num_gemms,
None,
output=workspace,
)
elif skip_fp8_weight_update is not None or update_workspace:
grouped_weight = tex.group_quantize(
source,
weight_quantizer,
num_gemms,
None,
noop_flag=skip_fp8_weight_update,
output=workspace,
)

if dbias_packed is None:
dbias_packed = compute_grouped_dbias(dy_2d, base_split_offsets, N)
grad_biases = [dbias_packed[i].to(dtype=ctx.activation_dtype) for i in range(N)]
if ctx.single_grouped_bias:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I guess we should eventually unified single_grouped_weight and single_grouped_bias into one parameter.

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.

sigh yes, but currently it's not and technically they can be totally orthogonal to each other as well, I have tested all the combinations already so it's fine

__all__ = ["GroupedLinear", "is_module_grouped_tensor_path_supported"]


def is_module_grouped_tensor_path_supported(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Can we put this in pytorch/utils.py and even reuse for the ops. The function should be identical right?

Given that we eventually want to unify the module/ops code, would be better to not duplicate these pieces now.

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 for review, I made a decision to specifically not to do it because they are currently different implementations and I agree with @timmoon10 's proposal to de-duplicate it. But for now, the right thing to do is to keep them separate.

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.

4 participants