Skip to content

[Common/PyTorch] Enable FP8BS for Grouped GEMM on Blackwell via MXFP8 Broadcast - #3614

Merged
denera merged 8 commits into
NVIDIA:mainfrom
denera:pytorch/grouped-linear-fp8bs-blackwell
Oct 19, 2026
Merged

denera merged 8 commits into
NVIDIA:mainfrom
denera:pytorch/grouped-linear-fp8bs-blackwell

Conversation

@denera

@denera denera commented Oct 2, 2026 •

Copy link
Copy Markdown
Collaborator

Description

Extends Float8BlockScaling on the grouped-tensor path from Hopper-only to Blackwell and newer. This covers grouped quantization, cuBLASLt grouped GEMM, and te.pytorch.GroupedLinear (module and fusible op). The new path applies wherever MXFP8 grouped GEMM is supported (SM100/SM103/SM107/SM110) by broadcasting FP32 power-of-2 scales into swizzled E8M0 MXFP8 scales.

Fixes #3520

Performance

Broadcast + Swizzle

Grouped tensors w/ nvte_swizzle_grouped_block_scaling_to_mxfp8_scaling_factors

Operand Tensors × shape MXFP8 scales Time
Activations (1D), uniform splits 8 × 4096 × 7168 7.3 MB 5.7 µs
Activations (1D), varying splits 8 × 4096 × 7168 7.3 MB 5.9 µs
Activations (1D), varying splits 32 × 1024 × 7168 7.3 MB 6.8 µs
Activations (1D), varying splits 128 × 256 × 7168 7.3 MB 9.0 µs
Activations (1D), varying splits 256 × 128 × 7168 7.3 MB 11.3 µs
Transposed activations (1D, wgrad), varying last dims 128 × 7168 × 256 7.3 MB 9.1 µs
Weights (2D) 8 × 4096 × 7168 7.3 MB 4.9 µs
Weights (2D) 128 × 2048 × 7168 58.7 MB 38.6 µs

List of Tensors w/ nvte_multi_tensor_swizzle_block_scaling_to_mxfp8_scaling_factors

Tensors × shape Fused multi-tensor conversion
8 × 4096 × 7168 weights (2D) 3.7 µs
32 × 2048 × 7168 weights (2D) 6.7 µs
128 × 2048 × 7168 weights (2D) 26.1 µs
128 × 256 × 7168 activations (1D) 7.2 µs

Conversion Cost per GroupedGemm in an MoE Layer

Based on 16 local experts × 2560 tokens, with 4096 hidden size and 1024 expert FFN.

GEMM Conversion kernels per call Grouped GEMM
FC1 fprop (TN) 21 µs (activations 14 µs, 16 weights 7 µs) 653 µs
FC1 dgrad (NN) 18 µs 675 µs
FC1 wgrad (NT) 27 µs (2 grouped operands) 739 µs
FC2 fprop (TN) 13 µs 488 µs
FC2 dgrad (NN) 20 µs 421 µs
FC2 wgrad (NT) 22 µs (2 grouped operands) 416 µs

GroupedLinear

Based on 4096 hidden size and 1024 FC2 FFN.

End-to-End Timing (ms)

Shape GEMM Mode Grouped GEMM: MXFP8 FP8BS FP8BS/MXFP8 Multi-stream: MXFP8 FP8BS FP8BS/MXFP8
16x2560 FC1 fwd 0.932 1.032 1.11× 0.916 1.081 1.18×
16x2560 FC1 fwd+bwd 3.301 3.818 1.16× 3.332 3.796 1.14×
16x2560 FC2 fwd 0.506 0.563 1.11× 0.542 0.630 1.16×
16x2560 FC2 fwd+bwd 1.971 2.392 1.21× 1.921 2.230 1.16×
64x512 FC1 fwd 0.957 1.114 1.16× 1.438 1.525 1.06×
64x512 FC1 fwd+bwd 4.249 4.627 1.09× 4.956 5.350 1.08×
64x512 FC2 fwd 0.571 0.671 1.18× 1.438 1.505 1.05×
64x512 FC2 fwd+bwd 2.315 2.576 1.11× 4.816 5.856 1.22×

Breakdown per FWD+BWD (µs)

Shape GEMM Precision GEMM Quantize FP8BS→MXFP8 conversion Other Total
16x2560 FC1 MXFP8 1968 488 — 509 2965
16x2560 FC1 FP8BS 2154 817 53 534 3558
16x2560 FC2 MXFP8 1230 349 — 217 1796
16x2560 FC2 FP8BS 1255 605 47 205 2112
64x512 FC1 MXFP8 2267 452 — 1063 3782
64x512 FC1 FP8BS 2297 733 83 1004 4116
64x512 FC2 MXFP8 1275 326 — 530 2132
64x512 FC2 FP8BS 1292 513 50 512 2368

Notes

FP8BS GroupedLinear slowdown vs. MXFP8 is due to the quantization, not the conversion.

FP8BS forces TN layout in both non-grouped and grouped cases:

  • Pays the cost of a transposed column-wise copy
  • Selects different (slightly slower) GEMM heuristics than NN/NT

Optimizing FP8BS quantization kernels (both grouped and non-grouped) on Blackwell deferred to another PR.

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

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

@denera denera self-assigned this Oct 2, 2026
@denera
denera requested a review from vthumbe1503 October 2, 2026 13:00
@denera
denera force-pushed the pytorch/grouped-linear-fp8bs-blackwell branch from 0778269 to 55c2ca4 Compare October 2, 2026 13:01
@denera

denera commented Oct 2, 2026

Copy link
Copy Markdown
Collaborator Author

/te-ci pytorch

@greptile-apps

greptile-apps Bot commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 5/5

[Medium impact] The PR appears safe to merge, with the previous binding failure fixed and no new actionable issues found.

Summary

This PR enables grouped FP8 block scaling on Blackwell by converting its scales to the MXFP8 format.

  • Grouped FP8 scales convert to MXFP8 for GEMM.
  • Grouped GEMM uses MXFP8 scales for block-scaled inputs.
  • Grouped Linear supports FP8 block scaling on Blackwell and newer.

Review was source-only; no runtime tests were run.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  A["Grouped FP8 block-scaled operands"] --> B{"GPU architecture"}
  B -->|Hopper| C["Native grouped FP8 block-scaling GEMM"]
  B -->|Blackwell| D["Broadcast and swizzle scales to MXFP8"]
  D --> E["MXFP8 grouped GEMM using TN views"]
  C --> F["Output"]
  E --> F
Loading

Reviews (7) · Last reviewed commit: "Added compile-time static check for grou..." · Reviewed by Greptile

Comment thread transformer_engine/pytorch/ops/basic/grouped_linear.py
Comment thread transformer_engine/common/swizzle/swizzle_block_scaling.cu Outdated
@denera
denera force-pushed the pytorch/grouped-linear-fp8bs-blackwell branch from 55c2ca4 to 1229f6d Compare October 2, 2026 13:31
@denera

denera commented Oct 2, 2026

Copy link
Copy Markdown
Collaborator Author

/te-ci pytorch

@vthumbe1503 vthumbe1503 left a comment •

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.

Thanks @denera! Mostly LGTM with some minor comments on CPU overheads. Could we also add some perf numbers for this kernel in the description?

Comment on lines +600 to +602
NVTE_CHECK_CUDA(cudaFuncSetAttribute(kernel, cudaFuncAttributeMaxDynamicSharedMemorySize,
static_cast<int>(smem_bytes)));
}

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.

We could cache this as well.

Comment on lines +594 to +599
NVTE_CHECK_CUDA(cudaDeviceGetAttribute(
&max_smem_bytes, cudaDevAttrMaxSharedMemoryPerBlockOptin, cuda::current_device()));
NVTE_CHECK(smem_bytes <= static_cast<size_t>(max_smem_bytes), "Too many tensors (",
input->num_tensors, ") to convert FP8 block scaling to MXFP8 scales: needs ",
smem_bytes, " bytes of shared memory, but the device allows ", max_smem_bytes,
".");

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.

We can cache this value per device in cuda_runtime.cpp to avoid cpu overheads.


// Emulate the FP8 block scaling recipe with MXFP8 on Blackwell and newer as it is not
// natively supported by cublasLt grouped GEMM (mirrors generic_gemm).
std::vector<at::Tensor> mxfp8_emulation_scales;

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.

Is there a need to define this std::vector and emplace the scales in it? It doesnt seem to be used anywhere

mxfp8_emulation_scales.emplace_back(convert_block_scaling_to_mxfp8_tensor(A_tensor, transa));
}
mxfp8_emulation_scales.emplace_back(
convert_grouped_block_scaling_to_mxfp8_tensor(grouped_B, !transb));

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.

Isnt grouped_swizzle function the right place for this implementation(I dont exactly remember the API).

std::vector<at::Tensor> mxfp8_emulation_scales;
if (needs_fp8_block_scaling_emulation(operand_modes)) {
for (auto& A_tensor : te_A_wrappers) {
mxfp8_emulation_scales.emplace_back(convert_block_scaling_to_mxfp8_tensor(A_tensor, transa));

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.

For weights we would be calling multi_tensor_swizzle somewhere. Maybe that is the right place for this implementation?

Comment on lines +280 to +282
__host__ __device__ __forceinline__ size_t round_up(const size_t a, const size_t b) {
return ceil_div(a, b) * b;
}

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.

Could we reuse DIVUP_TO_MULTIPLE() from common/common.h for the corresponding calculations below?

Comment on lines +277 to +279
__host__ __device__ __forceinline__ size_t ceil_div(const size_t a, const size_t b) {
return (a + b - 1) / b;
}

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.

Could we reuse DIVUP() from common/common.h for the corresponding calculations below?

@denera
denera force-pushed the pytorch/grouped-linear-fp8bs-blackwell branch from 1229f6d to 0d8f8d5 Compare October 6, 2026 19:49
@denera
denera force-pushed the pytorch/grouped-linear-fp8bs-blackwell branch from 0d8f8d5 to d8c2ac1 Compare October 8, 2026 13:59
…ewer

Drop the Hopper-only arch gates from the grouped FP8 block-scaling quantize and dequantize kernels. On SM100 and newer the recipe is emulated with MXFP8 GEMM, so grouped quantize now rejects non-power-of-2 scales there, matching the non-grouped quantize kernels.

Signed-off-by: Alp Dener <adener@nvidia.com>
…e conversion

Add nvte_swizzle_grouped_block_scaling_to_mxfp8_scaling_factors, the grouped variant of nvte_swizzle_block_scaling_to_mxfp8_scaling_factors used to emulate FP8 block scaling with MXFP8 grouped GEMM. A persistent kernel reads per-tensor dims on the device, so captured launches stay correct across CUDA graph replays, and tabulates each tensor's first tile and scale offsets in shared memory. It then walks 128x128 tiles across all tensors, finds each tile's tensor by binary search, and writes the swizzled E8M0 scales at the offsets the grouped GEMM expects. nvte_get_grouped_block_scaling_to_mxfp8_scale_inv_size returns the output buffer size, which depends only on the logical shape.

Add nvte_multi_tensor_swizzle_block_scaling_to_mxfp8_scaling_factors, which converts a list of tensors, such as per-expert weights, in one launch per up to 128 tensors instead of one launch per tensor.

All conversion kernels share the per-tile code. All three APIs are covered by a host-reference unit test.

Signed-off-by: Alp Dener <adener@nvidia.com>
cuBLASLt grouped GEMM has no native FP8 block scaling on Blackwell and newer. As in non-grouped GEMM, the grouped-tensor GEMM bindings now convert FP8 block-scaling operands to MXFP8 by broadcasting their scales, viewing transposed columnwise data as rowwise data of the transposed tensors, and force the TN layout. The grouped GEMM itself only ever sees an MXFP8 GEMM.

Lists of discrete operands, such as per-expert weights, are converted in one multi-tensor launch. The multi-stream grouped GEMM now does the same instead of converting one tensor at a time.

Signed-off-by: Alp Dener <adener@nvidia.com>
…tests

The exact operands used by the grouped FP8 block-scaling GEMM tests gave nearly every 128x128 block the same power-of-2 scale, so a wrong scale offset, orientation or broadcast in the MXFP8 emulation would not change the numerics. Multiply each operand tile by its own random power-of-2 factor, which every quantization block absorbs exactly into its scale, so quantization stays lossless while per-block scales now differ.

Signed-off-by: Alp Dener <adener@nvidia.com>
Admit FP8 block scaling on the GroupedLinear module and fusible-op grouped-tensor paths wherever MXFP8 is supported, now that the grouped GEMM bindings emulate it with MXFP8 on Blackwell and newer, and drop the module's Blackwell error. Reject non-power-of-2 FP8 block-scaling recipes on Blackwell and newer when entering autocast, since MXFP8 emulation needs power-of-2 scales; previously non-grouped GEMM only failed later inside quantization.

Signed-off-by: Alp Dener <adener@nvidia.com>
Capture grouped quantize and grouped-tensor GEMM in one CUDA graph for MXFP8 and FP8 block scaling (TN, NN and NT layouts), then replay it after updating the device-side split sizes in place, including zero-token experts and fewer tokens than the buffer capacity. Each run is compared against an exact high-precision reference over each expert's real region. On Blackwell and newer this covers the FP8 block-scaling MXFP8 emulation under graph replay.

Also tighten the mixed-operand rejection test to match the binding's error message.

Signed-off-by: Alp Dener <adener@nvidia.com>
…S scale conversion

Document in both GroupedLinear docstrings (module and fusible op) and the module's grouped-tensor path predicate that, with MXFP8 or FP8 block scaling, the grouped-tensor path requires every split size to be a multiple of 128, which is checked only on the device. For the fusible op, this is the alignment that split_alignment declares. With FP8 block scaling now on this path on Blackwell, fusible-op users no longer fall back to split quantization there.

Check that the per-tensor dims passed to nvte_swizzle_grouped_block_scaling_to_mxfp8_scaling_factors are int64, as the kernel reads them. Say to unset NVTE_FP8_BLOCK_SCALING_FP32_SCALES before importing Transformer Engine in the Blackwell non-power-of-2 rejection message, because the recipe defaults read it at import time.

Signed-off-by: Alp Dener <adener@nvidia.com>
@denera
denera force-pushed the pytorch/grouped-linear-fp8bs-blackwell branch from d8c2ac1 to 667b675 Compare October 15, 2026 19:02
@denera

denera commented Oct 15, 2026

Copy link
Copy Markdown
Collaborator Author

/te-ci pytorch

vthumbe1503
vthumbe1503 previously approved these changes Oct 15, 2026

@vthumbe1503 vthumbe1503 left a comment

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.

LGTM

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

I tested 667b6751 on B200 (sm_100), built with NVTE_CUDA_ARCHS=100a and NVTE_WITH_NCCL_EP=0.

One setup note first. With the cuBLAS that ships with the CUDA 13.0 toolkit (cuBLASLt 13.1), none of this PR's grouped-tensor path runs: is_module_grouped_tensor_path_supported returns False, and 936 cases in the subset below are skipped with "Grouped GEMM requires cuBLAS 13.3+". To exercise the PR I rebuilt against cuBLAS 13.8.1 headers and libraries (runtime tex.get_cublasLt_version() = 130801).

Results with that build:

Tests Result
test_grouped_linear.py -k "cuda_graph or grouped_tensor or single_grouped or grouped_parameter or fused_path" 872 passed, 77 skipped, 0 failed
FP8 block-scaling CUDA-graph cases: test_grouped_gemm_grouped_tensor_cuda_graph_changed_splits[fp8_block_scaling-{TN,NN,NT}], test_grouped_linear_fused_path_cuda_graph_safe[{True,False}-fp8_block_scaling] 5/5 passed
test_grouped_linear.py -k "Float8BlockScaling or fp8_block_scaling" 610 passed, 72 skipped
test_grouped_tensor.py + test_recipe.py 393 passed, 22 skipped

The skips are the declared ones: "NT (wgrad) has no weight operand" (72), "fp8_model_params requires FP8" (4), and NVFP4 single_grouped_weight (1). I did not run the C++ test_swizzle_block_scaling. Hopper and sm_103 were not covered.

One observation outside this PR's diff:

  • is_module_grouped_tensor_path_supported gates on the runtime cuBLASLt version, but the grouped-GEMM entry points are compiled out when TE is built against cuBLAS < 13.3.
  • With an older build running against a newer cuBLASLt (for example after upgrading nvidia-cublas), the predicate says yes and GroupedLinear(use_grouped_tensor=True) raises nvte_get_grouped_gemm_setup_workspace_size requires cuBLAS 13.3+, but compile-time cuBLAS version is ... instead of falling back.
  • That is how 856 cases of the same subset failed before I rebuilt. Checking a compile-time capability in the predicate would turn this into a clean fallback.

@denera

denera commented Oct 19, 2026

Copy link
Copy Markdown
Collaborator Author
  • is_module_grouped_tensor_path_supported gates on the runtime cuBLASLt version, but the grouped-GEMM entry points are compiled out when TE is built against cuBLAS < 13.3.

This is a very fair point and we should definitely guard this against the compile-time static cuBLAS version.

@vthumbe1503 I added a fix for this but it invalidated the earlier review. Could you take another look and approve again so we can merge?

@denera
denera requested a review from vthumbe1503 October 19, 2026 21:58
Comment thread transformer_engine/pytorch/module/grouped_linear.py Outdated
…NOT built with grouped GEMM support even if cuBLAS runtime version supports it.

Signed-off-by: Alp Dener <adener@nvidia.com>
@denera
denera force-pushed the pytorch/grouped-linear-fp8bs-blackwell branch from eb14ae6 to de53764 Compare October 19, 2026 22:29
@denera
denera merged commit f4a69dc into NVIDIA:main Oct 19, 2026
12 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.

Graph-Safe FP8 BlockScaling Support on Blackwell for GroupedLinear

4 participants