Skip to content

Add 2d quant for mxfp8 - #2634

Open
kunlunl wants to merge 8 commits into
NVIDIA:mainfrom
kunlunl:2d_mxfp8
Open

Add 2d quant for mxfp8#2634
kunlunl wants to merge 8 commits into
NVIDIA:mainfrom
kunlunl:2d_mxfp8

Conversation

@kunlunl

@kunlunl kunlunl commented Jan 29, 2026

Copy link
Copy Markdown
Contributor

Description

Please include a brief summary of the changes, relevant motivation and context.

Fixes # (issue)

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

@greptile-apps

greptile-apps Bot commented Jan 29, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR adds opt-in 2D block quantization for MXFP8 and propagates the setting from recipes through the PyTorch and common C++ quantization paths.

  • Adds 32×32-block scale computation to standard and grouped MXFP8 kernels.
  • Extends quantization configuration, recipe state, quantizer bindings, and MXFP8 tensor metadata with the 2D setting.
  • Adds C++ and PyTorch numerical, grouped-linear, CUDA-graph, and CI coverage.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains in the current code, and all previously reported concerns are fixed or invalid.

Important Files Changed

Filename Overview
transformer_engine/common/cast/mxfp8/quantize_mxfp8.cuh Adds synchronized 32×32 amax reduction and routes opt-in 2D quantization through generic MXFP8 kernels.
transformer_engine/common/cast/mxfp8/group_quantize_mxfp8.cuh Extends grouped MXFP8 quantization to use 2D block scaling when configured.
transformer_engine/common/common.h Adds the MXFP8 2D flag and its correctly indexed byte-sized configuration attribute.
transformer_engine/common/transformer_engine.cpp Adds explicit C API get/set handling for the new MXFP8 quantization attribute.
transformer_engine/pytorch/csrc/quantizer.cpp Propagates the Python MXFP8 quantizer's 2D setting into allocation and quantization calls.
transformer_engine/pytorch/quantization.py Enables 2D quantization selectively for supported forward weight roles.
transformer_engine/pytorch/tensor/mxfp8_tensor.py Adds the 2D setting to MXFP8 quantizer construction, copying, and value semantics.
tests/pytorch/test_mxfp8_2d_quantize.py Provides exact scale-byte, data, role-selection, preallocation, and directional coverage for 2D MXFP8.
tests/cpp/operator/test_cast_mxfp8.cu Adds common C++ API numerical coverage across scaling directions, shapes, and data types.
qa/L0_pytorch_unittest/test.sh Adds the new PyTorch 2D MXFP8 suite to the L0 CI job.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  Recipe["MXFP8BlockScaling<br/>enable_2d_quantization"] --> State["MXFP8 recipe state"]
  State --> Quantizer["MXFP8Quantizer<br/>with_2d_quantization"]
  Quantizer --> Binding["PyTorch C++ binding"]
  Binding --> Config["NVTE QuantizationConfig"]
  Config --> Dispatch["MXFP8 cast dispatch"]
  Dispatch --> Kernel["32x32 block-amax<br/>and E8M0 scale kernel"]
  Kernel --> Tensor["MXFP8 data and<br/>row/column scale metadata"]
Loading

Reviews (7): Last reviewed commit: "[pre-commit.ci] auto fixes from pre-comm..." | Re-trigger Greptile

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

4 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

Comment on lines +455 to +460
e8m0_t scale_from_shmem;
if (thread_lane < THREADS_X) {
scale_from_shmem = block_scales_2d[thread_lane];
}
// Broadcast: each thread gets scale from lane matching its tid_X_rowwise
biased_exponent = __shfl_sync(0xffffffff, scale_from_shmem, tid_X_rowwise);

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.

scale_from_shmem is potentially uninitialized for threads where thread_lane >= THREADS_X. While __shfl_sync only reads from lanes specified by tid_X_rowwise (which should be < THREADS_X), it's safer to initialize this variable.

Suggested change
e8m0_t scale_from_shmem;
if (thread_lane < THREADS_X) {
scale_from_shmem = block_scales_2d[thread_lane];
}
// Broadcast: each thread gets scale from lane matching its tid_X_rowwise
biased_exponent = __shfl_sync(0xffffffff, scale_from_shmem, tid_X_rowwise);
e8m0_t scale_from_shmem = 0;
if (thread_lane < THREADS_X) {
scale_from_shmem = block_scales_2d[thread_lane];
}

Comment thread transformer_engine/common/cast/mxfp8/quantize_mxfp8.cuh Outdated

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

12 files reviewed, 3 comments

Edit Code Review Agent Settings | Greptile

Comment on lines 415 to 423
@@ -420,7 +421,8 @@ struct QuantizationConfig {
sizeof(NVTETensor), // rng_seed and offset
sizeof(uint8_t), // nvfp4_2d_quantization
sizeof(uint8_t), // stochastic_rounding

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.

QuantizationConfig layout mismatch

QuantizationConfig::attr_sizes[] was extended with mxfp8_2d_quantization (common.h:415-423), but QuantizationConfig itself uses bool fields. If nvte_set/get_quantization_config_attribute assumes all attributes are packed according to attr_sizes, adding an extra byte-sized attribute without updating any corresponding packing/unpacking logic can break attribute indexing for subsequent attributes (or any code that iterates kNVTEQuantizationConfigNumAttributes). Please double-check the code that uses attr_sizes to ensure the new attribute is reflected everywhere it’s consumed (and that kNVTEQuantizationConfigNumAttributes matches the size of attr_sizes).

Comment thread tests/pytorch/test_mxfp8_2d_quantize.py Outdated
Comment on lines +100 to +108
) # (num_block_rows, num_block_cols, 32, 32)

# Compute amax for each 32x32 block
block_amax = torch.amax(
torch.abs(x_blocks.to(torch.float32)), dim=(-1, -2)
) # (num_block_rows, num_block_cols)

# Convert to E8M0 scale inverse
block_scale_e8m0 = float_to_e8m0(block_amax) # (num_block_rows, num_block_cols)

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.

Incorrect float bit-cast

float_to_e8m0 does val_u32 = val.view(torch.int32) (test_mxfp8_2d_quantize.py:104-106). On PyTorch, .view(dtype) is a numeric cast, not a bit reinterpretation. This makes the reference implementation compute wrong exponents/mantissas and can cause false failures/passes.

Use val.view(torch.int32) only if you’ve explicitly reinterpreted bytes (e.g., via val.view(torch.uint8) + view(torch.int32) on the same storage) or use val.to(torch.int32) with torch.frexp/torch.bitwise_* alternatives. As written, the reference is not modeling the GPU’s IEEE754 bit extraction.

@greptile-apps

greptile-apps Bot commented Feb 10, 2026

Copy link
Copy Markdown
Contributor
Additional Comments (1)

transformer_engine/common/cast/mxfp8/quantize_mxfp8.cuh
2D flag ignored for weights

In mxfp8::quantize(...), use_2d_quantization unconditionally forces scaling_type = ScalingType::BIDIMENSIONAL (quantize_mxfp8.cuh:776-779). That changes the requested scaling mode even when callers intended rowwise-only or colwise-only (e.g., weight-only paths could request rowwise-only). This will silently produce different scale tensor shapes/semantics than the caller expects.

If 2D block scaling is only valid when both rowwise+colwise outputs are requested, it should be validated (error) instead of overriding scaling_type; otherwise the override should be limited to the specific call sites that already requested bidimensional scaling.

Signed-off-by: kunlunl <kunlunl@nvidia.com>
Comment thread tests/pytorch/test_mxfp8_2d_quantize.py Outdated
Comment on lines +408 to +411
False,
]


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.

P1 Test assertion breaks when env var is active

MXFP8BlockScaling() uses os.getenv("NVTE_MXFP8_ENABLE_2D_QUANTIZATION", "0") == "1" as its default for enable_2d_quantization. If a developer or CI job has this env var set to "1", the assertion mxfp8_recipe.enable_2d_quantization is False will always fail — even though the recipe is behaving exactly as designed. The test should either monkeypatch the env var to "0" before constructing the recipe, or assert on the attribute when the recipe is explicitly constructed with enable_2d_quantization=False.

Signed-off-by: kunlunl <kunlunl@nvidia.com>
@Oleg-Goncharov

Copy link
Copy Markdown
Collaborator

Hi @kunlunl, could you please share performance benchmarks for this kernel?

@kunlunl

kunlunl commented May 14, 2026

Copy link
Copy Markdown
Contributor Author

Benchmarked the direct MXFP8 quantization kernel path on a B200 node with preallocated output tensors, timing only MXFP8Quantizer.update_quantized(x, out).

  • Existing 1D MXFP8 is unchanged within noise: rowwise 1D median delta after vs before is -0.03%, bidirectional 1D median delta is +0.09% across the full BF16 shape grid.
  • For large shapes (rows * cols >= 16M), 2D rowwise-only quantization has +21.13% median overhead vs rowwise 1D, with range +13.87% to +40.46%.
  • For large shapes, 2D bidirectional quantization has +34.84% median overhead vs bidirectional 1D, with range +23.44% to +43.08%.
  • Small 512/1024-row shapes are launch-bound at roughly 8-10 us, so their overhead percentages are noisier.

Raw data:

dtype shape [before PR] rowwise-only 1D (ms) [after PR] rowwise-only 1D (ms) rowwise-only 1D delta rowwise-only 2D (ms) rowwise-only 2D overhead (before PR) bidirectional 1D (ms) [after PR] bidirectional 1D (ms) bidirectional 1D delta bidirectional 2D (ms) bidirectional 2D overhead
bf16 512x4096 0.008495 0.008579 +0.99% 0.008508 -0.83% 0.009059 0.009260 +2.22% 0.009092 -1.81%
bf16 512x8192 0.008423 0.008436 +0.15% 0.010898 +29.18% 0.009011 0.009208 +2.19% 0.009281 +0.79%
bf16 512x14336 0.008415 0.008266 -1.77% 0.010321 +24.86% 0.009019 0.009003 -0.18% 0.008732 -3.01%
bf16 1024x4096 0.008345 0.008319 -0.31% 0.008281 -0.46% 0.008971 0.009027 +0.62% 0.008605 -4.67%
bf16 1024x8192 0.008388 0.008389 +0.01% 0.008289 -1.19% 0.008954 0.009027 +0.82% 0.009780 +8.34%
bf16 1024x14336 0.008502 0.008407 -1.12% 0.008791 +4.57% 0.010690 0.010708 +0.17% 0.014229 +32.88%
bf16 2048x4096 0.008506 0.008348 -1.86% 0.008343 -0.06% 0.009010 0.008923 -0.97% 0.009845 +10.33%
bf16 2048x8192 0.008643 0.008326 -3.67% 0.009929 +19.25% 0.011830 0.011859 +0.25% 0.016414 +38.41%
bf16 2048x14336 0.011797 0.011786 -0.09% 0.015681 +33.05% 0.020977 0.020964 -0.06% 0.027227 +29.88%
bf16 4096x4096 0.008481 0.008275 -2.43% 0.009962 +20.39% 0.011860 0.011834 -0.22% 0.016379 +38.41%
bf16 4096x8192 0.013639 0.013624 -0.11% 0.018682 +37.13% 0.024959 0.024925 -0.14% 0.030895 +23.95%
bf16 4096x14336 0.026210 0.026269 +0.23% 0.029964 +14.07% 0.041282 0.041316 +0.08% 0.051001 +23.44%
bf16 8192x4096 0.013846 0.013847 +0.01% 0.019450 +40.46% 0.024972 0.025005 +0.13% 0.031574 +26.27%
bf16 8192x8192 0.030222 0.030212 -0.03% 0.034402 +13.87% 0.046742 0.046652 -0.19% 0.058996 +26.46%
bf16 8192x14336 0.052623 0.052612 -0.02% 0.060849 +15.66% 0.077843 0.078558 +0.92% 0.104298 +32.77%
bf16 8192x28672 0.101789 0.102477 +0.68% 0.124094 +21.09% 0.156356 0.153673 -1.72% 0.216968 +41.19%
bf16 14336x4096 0.027565 0.026853 -2.58% 0.034055 +26.82% 0.042927 0.041762 -2.71% 0.055737 +33.46%
bf16 16384x4096 0.030578 0.030799 +0.72% 0.038679 +25.59% 0.047245 0.047752 +1.07% 0.065045 +36.21%
bf16 16384x8192 0.059549 0.059547 -0.00% 0.072155 +21.17% 0.089288 0.089659 +0.42% 0.125423 +39.89%
bf16 16384x14336 0.102436 0.102350 -0.08% 0.121594 +18.80% 0.153783 0.153917 +0.09% 0.215644 +40.10%
bf16 28672x8192 0.101802 0.102307 +0.50% 0.126185 +23.34% 0.157015 0.154281 -1.74% 0.220748 +43.08%

"""Reference MXFP8 2D quantization using one scale per 32x32 block."""
rows, cols = x.shape
assert rows % MXFP8_BLOCK_SIZE == 0
assert cols % MXFP8_BLOCK_SIZE == 0

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.

Considering this is the only test of this functionality (and it would be better if we also had C++ tests covering it, since it is a change in common), we should test also the cases where the shapes are less nice, and so should not impose those requirements. The MXFP8 quantization kernel does not actually need them - the only real restriction if the 16B alignment from TMA, so the cols % 16 should be 0.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Partially addressed in Python and fully covered at the common C++ level. I added a C++ operator test in tests/cpp/operator/test_cast_mxfp8.cu with an independent 32x32-block CPU reference, exact E8M0 scale comparison, and FP8 data comparison. It covers rowwise-only, colwise-only, and bidirectional modes over a broader shape matrix including less-nice shapes such as {1,16}, {16,48}, {65,80}, {127,400}, and {993,512}.

For the Python tests, I kept the current shape list 32-aligned. The Python MXFP8Quantizer public allocation path still enforces the existing 32-aligned MXFP8 storage contract, and relaxing it to cols % 16 == 0 would broaden Python allocator behavior beyond the intended scope of this PR. The less-nice shape coverage is therefore in the common C++ test, which exercises the kernel/API path directly.

If set to `True`, 2D block scaling is used for weight tensors.
"""

enable_2d_quantization: bool = os.getenv("NVTE_MXFP8_ENABLE_2D_QUANTIZATION", "0") == "1"

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.

This should be the last argument, otherwise it is a breaking API change.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Checked and addressed.


def _use_2d_quantization(idx: int) -> bool:
role = self._slot_role(idx)
return role.module_type in ("linear", "grouped_linear") and role.tensor_type == "weight"

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.

If we want to use it in grouped linear then we should also enable the grouped quantization kernel with this feature.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed. I restricted the recipe policy to regular linear weight roles only. grouped_linear and other unsupported or unknown roles stay on the existing 1D MXFP8 path. There is also a test that verifies grouped-linear weight roles do not enable 2D quantization.

Comment on lines +24 to +34
(64, 64),
(128, 128),
(256, 256),
(256, 1024),
(1024, 256),
(256, 288),
(320, 320),
(352, 256),
(2048, 2048),
(1024, 2048),
(2048, 1024),

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 don't think we need all these shapes to be honest and that increases the CI workload. Could we be more thoughtful here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed with a smaller Python shape set. The Python test matrix now keeps representative 32-aligned shapes rather than the full original list. I did keep (320, 320) and (352, 256) because they were "less nice".

Comment on lines +268 to +272
#pragma unroll
for (int i = 16; i > 0; i /= 2) {
thread_amax = fmaxf(thread_amax, __shfl_xor_sync(0xffffffff, thread_amax, i));
}
}

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 pretty sure we already have the warp-level reduction somewhere (I think in utils.cuh). If that one is good, please use that instead. If not, please improve it rather than adding another reduction code here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed. The colwise 2D reduction now uses the existing warp-level max reduction helper.

float amax_2d = 0.0f;
#pragma unroll
for (int i = 0; i < THREADS_Y; ++i) {
amax_2d = fmaxf(amax_2d, block_amax_2d[tid_X_rowwise * THREADS_Y + i]);

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.

Do we need to use FP32 here? Maybe we could use BF16 version for the BF16 input data.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed. The rowwise 2D amax staging type now uses IType for the cast-only BF16/FP16 path, reducing shared-memory pressure and avoiding unnecessary FP32 staging there. It still uses float for FP32 input and for activation/dbias paths where keeping FP32 intermediate precision is safer.

kunlunl and others added 3 commits May 15, 2026 10:08
@github-actions github-actions Bot added the community-contribution PRs from external contributor outside the core maintainers, representing community-driven work. label Aug 4, 2026
fp8_dpa: bool = False
fp8_mha: bool = False
backward_override: Optional[str] = os.getenv("NVTE_BACKWARD_OVERRIDE", None)
enable_2d_quantization: bool = os.getenv("NVTE_MXFP8_ENABLE_2D_QUANTIZATION", "0") == "1"

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.

If the env variable is not strictly needed I would prefer to not introduce one, but rather rely on the user just specifying the option in their MXFP8 recipe.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done.


def _use_2d_quantization(idx: int) -> bool:
role = self._slot_role(idx)
return role.module_type == "linear" and role.tensor_type == "weight"

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.

There is an issue here with the GroupedLinear - it uses the grouped quantization, which was not updated to be 2D-quant aware, so will break. We should add the 2D awareness to the grouped quantize kernel too.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done.

kunlunl and others added 2 commits August 10, 2026 04:40
@ptrendx

ptrendx commented Aug 11, 2026

Copy link
Copy Markdown
Member

/te-ci

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