Skip to content

Experimental Triton GEMM backend for TE PyTorch (BF16/FP16/FP32/FP8/MXFP8)#667

Open
wenchenvincent wants to merge 36 commits into
devfrom
triton_gemm_mxfp8_rebase_2.15
Open

Experimental Triton GEMM backend for TE PyTorch (BF16/FP16/FP32/FP8/MXFP8)#667
wenchenvincent wants to merge 36 commits into
devfrom
triton_gemm_mxfp8_rebase_2.15

Conversation

@wenchenvincent

@wenchenvincent wenchenvincent commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds a Triton-based GEMM backend for the PyTorch bindings as an alternative to the default hipBLASLt path. Opt-in via NVTE_USE_GEMM_TRITON=1; the C++/hipBLASLt path remains the default and is unchanged when the flag is unset.

The backend is exercised end-to-end through TE's Linear, LayerNormLinear, and general_gemm() entry points, so anything that dispatches through general_gemm transparently picks up the Triton kernels when the flag is on.

Motivation. Give TE a Python-side GEMM path we can iterate on for AMD-specific work — MXFP8, MoE-style grouped shapes, autotune experiments, gfx950 kernel tuning — without going through the hipBLASLt release cadence. Not yet a perf replacement for hipBLASLt; BF16 in particular is slower today.

Precision support

Format Status Kernel
FP32 Supported matmul_kernel
FP16 Supported matmul_kernel
BF16 Supported matmul_kernel (perf below hipBLASLt)
Regular FP8 (E4M3 × E4M3, E5M2 × E5M2) Supported matmul_kernel with INPUT_FP8=True
Mixed FP8 (E4M3 × E5M2 — HYBRID recipes' bwd) Gated on PyTorch 2.14.0.dev20260626+ Triton compiler bug (triton-lang/triton#9567)
MXFP8 (block-scaled, 32 elems/block, E8M0 scales) Supported (torch ≥ 2.10) mxfp8_matmul_kernel via tl.dot_scaled()
NVFP4 / other QuantizedTensorStorage Refused with clear ValueError

Epilogues: DEFAULT, BIAS, BGRADB (bias gradient). Fused FP8 output quantization is applied by calling the caller-provided quantizer on the fp32 accumulator output — the kernel's OUTPUT_FP8 path with c_scale + c_amax is present but not wired through the wrapper yet (follow-up perf work).

Architecture

TE Linear.forward
    → transformer_engine.pytorch.cpp_extensions.gemm.general_gemm()
        └─ if NVTE_USE_GEMM_TRITON=1 → te_generic_gemm_triton()      (new)
        └─ else                       → tex.generic_gemm() (hipBLASLt, existing)

te_generic_gemm_triton()  (transformer_engine/pytorch/gemm_triton.py)
    ├── Float8TensorWrapper / MXFP8TensorWrapper — extract data, scales, columnwise copies
    ├── Handle BLAS column-major ↔ Triton row-major (operand swap for MXFP8)
    ├── Dispatch to matmul() or mxfp8_matmul()
    └── If caller passed a quantizer, apply it to the fp32 output → returns Float8Tensor

Key files (matches the triton_kernels/gmm/ subpackage layout):

  • transformer_engine/pytorch/triton_kernels/gemm/
    • gemm_kernels.py — the two @triton.jit kernels: matmul_kernel, mxfp8_matmul_kernel
    • gemm_wrapper.py — Python wrappers (matmul, mxfp8_matmul) and TE-shaped entry points (te_gemm_triton, te_generic_gemm_triton)
    • gemm_common.py — dtype conversions, shape helpers, Float8TensorWrapper, MXFP8TensorWrapper
    • __init__.py — re-exports the public API
  • transformer_engine/pytorch/quantization.py — recipe-support gate that refuses HYBRID under NVTE_USE_GEMM_TRITON=1 (until torch 2.14)

Testing

Three dedicated Triton GEMM test files under tests/pytorch/triton_kernels/, matching the sibling test layout (test_cast.py, test_cast_mxfp8.py, test_grouped_gemm.py, test_norms.py, ...):

File Purpose
tests/pytorch/triton_kernels/test_gemm.py general_gemm() under Triton — equivalence vs. torch.matmul and vs. C++ backend, across regular / FP8 / mixed-FP8 / MXFP8, all layouts, plus a batched-fp8 multidim case
tests/pytorch/triton_kernels/test_gemm_kernel.py Low-level te_gemm_triton() kernel correctness across dtype × shape × layout × bias × grad (1056 parametrizations of one test_correctness fn)
tests/pytorch/triton_kernels/test_gemm_mxfp8.py MXFP8 wrapper sanity + one direct-kernel-with-simulated-data case

Plus a repo-wide pytest hook (tests/pytorch/conftest.py) that converts the three backend-refusal ValueErrors (HYBRID, mixed FP8, unsupported QuantizedTensorStorage) into pytest.skip. Self-retiring — when the runtime gates are relaxed (e.g. torch ≥ 2.14 lands, NVFP4 kernel gets implemented), the marker text disappears from the error and the hook stops firing without code changes.

Eight grouped-GEMM equivalence tests in test_numerics.py (comparing a grouped GEMM to a sequence of individual GEMMs at rtol=0, atol=0) get a _skip_grouped_under_gemm_triton marker: under NVTE_USE_GEMM_TRITON=1 the two sides no longer share a backend, so bit-exact equivalence is broken by design. Comment on upstream's assertion literally says # cuBLAS implementation should be bit-wise match.

CI wiring

ci/pytorch.sh (invoked from .github/workflows/rocm-ci.yml on push to dev/release_v2.*_rocm and on workflow_dispatch):

"triton" label — direct Triton GEMM tests, level 1 (runs at every CI level):

NVTE_USE_GEMM_TRITON=1 run_default_fa_lbl "triton" 1 triton_kernels/test_gemm.py
NVTE_USE_GEMM_TRITON=1 run_default_fa_lbl "triton" 1 triton_kernels/test_gemm_kernel.py
NVTE_USE_GEMM_TRITON=1 NVTE_ROCM_ENABLE_MXFP8=1 run_default_fa_lbl "triton" 1 triton_kernels/test_gemm_mxfp8.py

"gemm-triton" label — TE integration sweep with the Triton backend:

NVTE_USE_GEMM_TRITON=1 run_default_fa_lbl "gemm-triton" 3 test_numerics.py
NVTE_USE_GEMM_TRITON=1 run_default_fa_lbl "gemm-triton" 1 test_fusible_ops.py
NVTE_USE_GEMM_TRITON=1 run_default_fa_lbl "gemm-triton" 1 test_float8_current_scaling_exact.py

Results on gfx950 / PyTorch 2.10

Direct Triton GEMM suites:

Suite Result
triton_kernels/test_gemm.py 212 passed / 72 skipped (mixed-FP8 gated)
triton_kernels/test_gemm_kernel.py 450 passed / 606 skipped (mixed-FP8 gated)
triton_kernels/test_gemm_mxfp8.py 4 passed
test_float8_current_scaling_exact.py under Triton 5 passed / 2 skipped

Full test_numerics.py under NVTE_USE_GEMM_TRITON=1: 0 failed / 1058 passed / 2644 skipped / 28 xfailed in ~6 min.

Known limitations

  • Perf: BF16 performance is below hipBLASLt today; the backend is intended as a correctness-first / experimentation vehicle, not a drop-in perf replacement.
  • Mixed FP8 (HYBRID recipes' backward): blocked on triton-lang/triton#9567. The fix lives only on Triton main; first ships in PyTorch 2.14.0.dev20260626+ nightlies (verified against pytorch/pytorch release-branch Triton pins). Refused with a clear ValueError until torch ≥ 2.14 is detected.
  • NVFP4: not implemented in the Triton kernels. Refused with a clear ValueError; user is directed to unset NVTE_USE_GEMM_TRITON for NVFP4 recipes.
  • Fused FP8 output quantization currently applies the quantizer to the fp32 result outside the kernel; the fused OUTPUT_FP8 path (c_scale + c_amax in-kernel) is present but not yet wired. Follow-up.
  • Grouped GEMM is not routed through this backend; grouped paths (GroupedLinear, test_grouped_gemm, ...) fall through to hipBLASLt / CUTLASS / AITER Triton grouped kernels as before.

Notable correctness fixes surfaced during the v2.15 rebase

  • int64 pointer promotion in matmul_kernel and mxfp8_matmul_kernel — fixes SIGABRT under Triton autotune benchmarking when (M-1) * stride > 2^31. Concrete repro: test_fp8_current_scaling_linear_large_numel_e4m3[bs33-fp32], M=143616, K=15360 → 2.2G.
  • Bias epilogue wiring through te_generic_gemm_triton (previously the wrapper silently dropped α / β / accumulate).
  • restore_value=['c_ptr'] on the mxfp8 autotuner so ACCUMULATE=True benchmark iterations don't multi-copy the result.
  • Float8TensorWrapper batched-columnwise permutation rewritten to match fp8_transpose's actual output layout (old formula silently scrambled batch dims for ndim ≥ 3).
  • gfx950 NaN-safety in the mxfp8 direct-kernel test: route test data through the architecture-native E4M3 dtype (e4m3fn on gfx950 vs e4m3fnuz on gfx942) so 0x7F/0xFF bytes are not NaN encodings under OCP e4m3fn.
  • ci/pytorch.sh pytest_run bug workaround"$TEST_DIR/$@" only prefixes the first positional arg, so passing two file paths silently collected 0 items on the second. Split into two calls.

Test plan

  • pip install -e . --no-build-isolation builds cleanly on gfx950
  • Four dedicated Triton GEMM test files pass with no regression
  • Full test_numerics.py under NVTE_USE_GEMM_TRITON=1 completes with 0 failures
  • Local ci/pytorch.sh smoke run exercises the new CI wiring end-to-end
  • CI green under Build and Test Branch on push
  • Follow-up: fused OUTPUT_FP8 kernel path wired through te_generic_gemm_triton
  • Follow-up: revisit BF16 perf against hipBLASLt

🤖 Generated with Claude Code

Experimental implementation of GEMM with Triton kernel.
Implemented BIAS and BGRADB fusion.
Implemented scaled fp8 MM optional with fp8 output.

Use env var `NVTE_USE_GEMM_TRITON=1` to enable in runtime.
Perf is not as good as hipblasLt.
   Implement Float8Tensor handling in the high-level Triton GEMM wrapper to
   match the C++ backend's behavior. This enables the Triton path to be used
   as a drop-in replacement for hipBLASLt with FP8 quantized tensors.

   Key changes:

   1. Float8TensorWrapper class
      - Mimics C++ TensorWrapper behavior from makeTransformerEngineTensor
      - Extracts FP8 components: _data, _transpose, _scale_inv, _fp8_dtype
      - Handles both Float8Tensor and Float8TensorBase
      - Properly converts columnwise-only tensors to rowwise format
      - Uses permute() to reorder dimensions: [K,M,*batch] -> [*batch,M,K]

   2. Updated te_generic_gemm_triton()
      - Detects and extracts Float8Tensor components
      - Reinterprets uint8 data as native FP8 types (float8_e4m3fnuz/e5m2fnuz)
      - Passes extracted scales and dtypes to te_gemm_triton()
      - Maintains backward compatibility with regular tensors

   3. Fixed getGemmOutputShape()
      - Now exactly matches C++ backend implementation
      - Preserves B's batch structure (except when transb=True)
      - Added comprehensive documentation explaining the API design choice
      - Correctly handles all layouts: TN, NN, NT

   4. Columnwise tensor handling
      - Detects memory-optimized columnwise-only tensors
      - Correctly transposes with dimension reordering for batch support
      - Handles arbitrary batch dimensions in columnwise format
Implement end-to-end MXFP8 (Microscaling FP8) support for Transformer Engine's
Triton GEMM backend using tl.dot_scaled() for block-scaled FP8 computation.

Key components:
- MXFP8TensorWrapper: Python equivalent of C++ TensorWrapper that extracts
  rowwise/columnwise data and E8M0 scales from MXFP8Tensor
- mxfp8_matmul_kernel(): Triton kernel using tl.dot_scaled() for block-scaled
  matmul with E8M0 scale conversion (2^(biased_exp - 127))
- mxfp8_matmul(): Python wrapper for MXFP8 kernel launch
- Updated te_generic_gemm_triton() to detect and dispatch MXFP8 inputs

Implementation verified against C++ codebase (type_converters.cpp,
cublaslt_gemm.cu, utils.cuh) for consistency with MXFP8 data/scale
selection logic and E8M0 conversion formulas.

Note: MXFP8 cannot be transposed after quantization without requantization,
hence dual storage (rowwise + columnwise) is required. This implementation
keeps MXFP8 kernel separate from regular FP8 for independent autotuning.
Add comprehensive tests for MXFP8 Triton GEMM implementation:
- test_mxfp8_gemm_basic.py: Basic wrapper and import tests
- test_mxfp8_kernel_direct.py: Direct kernel test with simulated data
- README.md: Test documentation and running instructions

Tests validate:
- MXFP8TensorWrapper functionality
- mxfp8_matmul() kernel execution
- E8M0 scale handling (biased exponent conversion)
- Output correctness (non-zero results)

Note: These tests use simulated FP8 data for kernel validation.
Full end-to-end testing requires actual MXFP8Tensor instances.
@wenchenvincent wenchenvincent changed the title Rebase Triton GEMM MXFP8 onto dev @ v2.15 + backend & CI fixes Experimental Triton GEMM backend for TE PyTorch (BF16/FP16/FP32/FP8/MXFP8) Jul 9, 2026
@wenchenvincent wenchenvincent marked this pull request as draft July 9, 2026 19:47
This commit corrects the MXFP8 implementation in the Triton GEMM backend
to properly handle column-major to row-major conversion and match BLAS
behavior.

Key changes:
1. Follow C++ selection logic for choosing rowwise/columnwise formats
   based on BLAS transpose flags
2. Implement operand swapping for row-major conversion (same as FP8)
3. Apply logical transpose (views) to both data and scales when needed
4. Remove scale padding handling (to be fixed in later update)

The implementation now correctly handles all three GEMM operations:
- fprop: Y = X @ W^T (TN layout)
- dgrad: dX = dY @ W (NN layout)
- wgrad: dW = dY^T @ X (NT layout)

Critical insight: MXFP8 columnwise is NOT physically transposed - it has
the same shape as rowwise but with different quantization patterns. The
solution uses logical transpose (stride manipulation) for both data and
scales, avoiding any physical data movement.
The kernel was using dimensions from the swapped operand shapes directly,
but these might be transposed views. This caused wrong output dimensions.

Changes:
- Use actual output dimensions (M, N) from d_row_major shape
- Compute K from the inner dimension after swap/transpose
- Add assertions to verify dimension compatibility
- Improve debug output to show the actual computation

This should fix the shape mismatch error in backward pass where gradient
had wrong dimensions.
For MXFP8 tensors, columnwise data has the SAME shape as rowwise data
(unlike Float8Tensor where columnwise is transposed). The wrapper was
incorrectly applying transpose logic when determining tensor size from
columnwise-only data, causing shape mismatches in wgrad operations.

This fix ensures that when only columnwise MXFP8 data is available,
the wrapper correctly reports the tensor size as the columnwise shape
without any transformation.
@wenchenvincent wenchenvincent force-pushed the triton_gemm_mxfp8_rebase_2.15 branch from d095729 to 177bf2d Compare July 9, 2026 19:50
Added API changes in Triton mxfp8 kernel. And only enable mxfp8 Triton
GEMM when torch version >= release 2.10
Also added a standalone reproducer for the Triton compiler bug.
general_gemm() no longer accepts a workspace kwarg on dev (it is now
derived internally via get_cublas_workspace). Drop the workspace
argument from the two tests that still passed it.

Also guard test_mxfp8_kernel_direct.py with a torch >= 2.10 module-level
skip, matching the runtime check in te_generic_gemm_triton(): earlier
Triton versions hit a tl.dot_scaled() RHS-scale compiler bug and
produce NaNs.
The high-level Triton GEMM wrapper hardcoded epilogue='DEFAULT' and passed
an empty bias tensor to the kernel. As a result TE Linear with bias=True
on NVTE_USE_GEMM_TRITON=1 silently skipped the forward BIAS fuse and
zeroed the returned bias gradient (tripping test_numerics.py's
test_linear_accuracy when the bias parameter was non-zero, and masked by
zero-init for the trivial case).

Wire epilogue from (bias, grad): BIAS when bias is present and grad is
False, BGRADB when grad is True (allocating a fresh gradient buffer so we
don't clobber the forward bias). The low-level te_gemm_triton path
already handled this correctly; only te_generic_gemm_triton needed the
fix.

Add regression-guard tests in test_te_generic_gemm_triton.py covering
both BIAS (forward) and BGRADB (backward, matching Linear wgrad's NT
layout). Both new tests fail on the unfixed wrapper.
…CI wiring

- gemm_triton.py:
  * Wire alpha/beta/accumulate through matmul() and matmul_kernel() so
    te_generic_gemm_triton no longer silently drops fused GEMM output
    scaling / accumulation. Adds ACCUMULATE and ALPHA_IS_ONE constexpr
    fast paths; folds β·C into the accumulator before FP8 output scaling
    so amax reflects the final value (matches hipBLASLt epilogue order).
  * Add restore_value=['c_ptr'] to the mxfp8 kernel autotuner: with
    ACCUMULATE=True each benchmark iteration would add computed_c to the
    output again, multi-copying the result. Cost is paid once per shape
    during warmup.
  * Fix Float8TensorWrapper batched-columnwise permutation. fp8_transpose
    (transpose_hip.cpp) collapses leading dims to (M,K), transposes to
    (K,M), and re-splits to [K, D0, ..., D_{n-2}]. The old formula
    (batch_dims + [1, 0]) assumed [K, M, b1, b2, ...] with M kept as a
    separate dim, and silently scrambled batch dimensions for ndim >= 3.
    Replace with list(range(1, ndim)) + [0].

- test_gemm_triton.py: skip mixed FP8 (e4m3+e5m2) cases when torch < 2.12.
  Reason: triton-lang/triton#9567 fixes a mixed-type MFMA selection bug
  on gfx950. The fix landed in Triton release/3.7.x, which ships with
  pytorch-triton-rocm 3.7.x (PyTorch 2.12+). PyTorch 2.10 and 2.11 ship
  pytorch-triton-rocm 3.6.x and do not carry the fix.

- mxfp8/test_mxfp8_kernel_direct.py: build test data through the
  architecture-native E4M3 dtype (e4m3fn on gfx950 with cap>=9.5,
  e4m3fnuz elsewhere). The prior int8->uint8 reinterpret could emit
  0x7F/0xFF bytes that are NaN encodings under OCP e4m3fn on gfx950
  and poisoned the accumulator.

- test_te_generic_gemm_triton.py: drop the (224,544,544) MXFP8 shape.

- ci/pytorch.sh: run the Triton GEMM test files (test_gemm_triton.py,
  test_gemm_triton_generic_fp8.py, test_te_generic_gemm_triton.py,
  mxfp8/) under NVTE_USE_GEMM_TRITON=1 in the default CI config, and
  also rerun test_numerics.py / test_fusible_ops.py /
  test_float8_current_scaling_exact.py with the Triton GEMM backend
  enabled. These were previously not exercised by CI.
- ci/pytorch.sh: run only mxfp8/test_mxfp8_gemm_basic.py and
  mxfp8/test_mxfp8_kernel_direct.py in the Triton GEMM sweep. The
  wildcard mxfp8/ also picked up new-on-dev tests
  (test_mxfp8_group_quantize_graph_safe.py,
  test_mxfp8_quantize_swizzle_fusion.py) that exercise the MXFP8
  quantize/swizzle path — orthogonal to the Triton GEMM backend, and
  currently failing bit-exact scale checks on gfx950 (a dev-side issue,
  not a Triton GEMM regression).

- tests/pytorch/mxfp8/__init__.py: remove. The empty package marker
  caused pytest to treat mxfp8/ as a package, which prevents dev's
  unqualified `from mxfp8_utils import ...` in the two new tests from
  resolving. Removing it lets those tests collect when run manually,
  and our tests do not rely on the package layout.
The prior torch>=(2,12) gate assumed PR #9567 (mixed-type MFMA fix)
would ship with Triton release/3.7.x, which was pytorch-triton-rocm's
line for PyTorch 2.12+. It doesn't. Verified against the triton-lang
and pytorch/pytorch GitHub repos:

- Commit eaaa75cf5 (PR #9567) lives ONLY on Triton main. Not on
  release/3.6.x, release/3.7.x, or release/3.8.x.
- PyTorch release branches through 2.13 pin Triton 3.7.1 or earlier,
  none of which carry the fix.
- Fix first appears in pytorch/pytorch main via the Triton pin bump
  to 43422b04 ("Triton 3.8") on 2026-06-26, which ships in
  PyTorch 2.14.0.dev nightlies from that date onward.

So 2.12 / 2.13 users would have hit the bug with the old gate.
Bumping to (2,14) with an inline reference to the PR.

Also updates the pytest.skip message to reflect reality.
pytest_run in ci/_utils.sh does `python -m pytest ... "$TEST_DIR/$@"`.
When "$@" holds multiple positional args, the shell only attaches
$TEST_DIR/ to the *first* one — the remaining args stay relative to
CWD. Because ci/pytorch.sh runs from the repo root, the second file
path resolves to a nonexistent location and pytest collects 0 items,
silently masking whether the test ran at all.

Every other multi-arg line in this file follows the pattern
"file.py -k pattern", where the trailing args are pytest flags, so
they are unaffected. Our line was the only one passing two file paths.

Split into one call per file to match the surrounding convention.
An alternative would be to fix pytest_run to prefix every path arg,
but that's a broader change in shared CI infra and this hits only
one line here.
…sing

matmul_kernel and mxfp8_matmul_kernel compute pointer offsets as
  ptrs = base + offs_m[:, None] * stride_m + offs_k[None, :] * stride_k
Triton's default int32 arithmetic overflows silently when the product
(M-1) * stride exceeds 2^31, producing garbage pointers and — during
autotune benchmarking — HIP faults that surface as SIGABRT.

Concrete repro:
  test_float8_current_scaling_exact.py::TestFP8CurrentScalingLargeNumel::
  test_fp8_current_scaling_linear_large_numel_e4m3[bs33-fp32]
  M=143616, K=15360 -> (M-1)*K = 2,205,926,400 > 2^31 = 2,147,483,648.
Suite ran as F...F. then aborted the pytest process during the Triton
autotune of a benchmark launch. bs32 (M=139264, (M-1)*K = 2,139,095,040)
sits just under 2^31 and passes; bs33 tips over. The docstring on the
test itself flags this class of bug: "Regression for 32-bit numel
overflow when total elements > 2^31."

Fix: cast the M-dim and N-dim offset tensors to tl.int64 exactly at the
pointer-computation sites -- a_ptrs, b_ptrs, c_ptrs, and (mxfp8 kernel)
a_scale_ptrs and b_scale_ptrs. K-dim offsets stay int32 (bounded by
BLOCK_SIZE_K). Mask comparisons (offs_m < M, offs_n < N) also stay
int32 so the per-tile bounds check is not slowed.

After the fix: full test_float8_current_scaling_exact.py completes
end-to-end (12.4s, 5 passed / 2 failed -- the two remaining failures
are the separate HYBRID-recipe gate, not related to this bug).

No regression on the 4 direct Triton GEMM suites:
  test_gemm_triton.py             450 passed / 606 skipped
  test_gemm_triton_generic_fp8.py  21 passed
  test_te_generic_gemm_triton.py  210 passed /  72 skipped
  mxfp8/                            4 passed
test_fp8_current_scaling_with_linear_module and
test_fp8_current_scaling_with_layernorm_linear_module use
Float8CurrentScaling() with default args, which defaults to
Format.HYBRID (E4M3 fwd, E5M2 bwd). Under NVTE_USE_GEMM_TRITON=1 the
runtime gate in transformer_engine/pytorch/quantization.py:184 raises
ValueError because HYBRID's backward pass produces mixed FP8 (e5m2 x
e4m3) GEMMs, which trigger triton-lang/triton#9567. The fix lives only
on Triton main; no released PyTorch through 2.13 carries it, and
PyTorch 2.14.0.dev nightlies from 2026-06-26 are the first wheels
that do.

Add _skip_if_hybrid_under_gemm_triton(*recipes) and call it at the top
of the two affected tests. The helper inspects the *resolved* recipe's
fp8_format rather than hard-coding which parametrization uses HYBRID,
so it is robust to future defaults and can be reused for other tests
without duplication.

The skip is unconditional on NVTE_USE_GEMM_TRITON + HYBRID (not gated
on torch_version() >= (2, 14)) because the quantization.py gate itself
is unconditional. Both places should relax together after torch 2.14
validates.

Before: 4 passed, 2 failed, 1 SIGABRT (test 7 crashed the process,
                                       masking test 6's result).
After int64 fix (previous commit): 5 passed, 2 failed.
After this commit:                 5 passed, 2 skipped.

Default backend (no NVTE_USE_GEMM_TRITON): 7 passed, unchanged.
… tests

Two coordinated changes to make the "gemm-triton" CI sweep actionable
by cutting known-issue noise (76% of test_numerics failures) and by
replacing a raw AttributeError with a clear refusal (11% of failures).

1) transformer_engine/pytorch/gemm_triton.py

   Float8TensorWrapper only detects Float8Tensor / Float8TensorStorage.
   Every other QuantizedTensorStorage subclass (NVFP4TensorStorage,
   stray MXFP8TensorStorage arriving via this path, future formats)
   silently fell through to the "regular tensor" branch and crashed on
   `tensor.dtype` because QuantizedTensorStorage exposes `_dtype`, not
   `dtype`. Surfaced in test_numerics.py as
     AttributeError: 'NVFP4TensorStorage' object has no attribute 'dtype'
   for 176 parametrizations.

   Refuse those inputs before the fallback with a ValueError that
   matches the phrasing of the existing HYBRID / mixed-FP8 gates
   ("The Triton GEMM backend (NVTE_USE_GEMM_TRITON=1) does not
   support ..."). Import-guard the QuantizedTensorStorage import so
   older callers without the class still work.

2) tests/pytorch/conftest.py (new)

   Register a pytest_runtest_call hookwrapper that inspects test
   exceptions. If the raised ValueError message contains one of the
   three known Triton GEMM backend refusal markers -- Format.HYBRID
   (quantization.py::check_recipe_support), "Mixed FP8 types"
   (gemm_triton.py::te_gemm_triton), or the generic
   NVTE_USE_GEMM_TRITON refusal from Float8TensorWrapper -- convert to
   pytest.skip via outcome.force_exception(). Real assertion failures,
   crashes, and any ValueError without the marker text pass through
   untouched.

   The hook is self-retiring: once the runtime gates are relaxed
   (torch >= 2.14 with the Triton mixed-MFMA fix, NVFP4 kernel
   implemented, ...) the matching messages disappear from the
   ValueError text and the hook stops firing on its own -- no
   coordination with tests required.

Impact on the CI "gemm-triton" sweep (before -> after, this branch):

  test_grouped_linear_accuracy:   1056 failed  -> 160 failed
    (896 HYBRID/NVFP4 refusals now surface as skips)
  Direct Triton GEMM suites (no regression):
    test_gemm_triton.py              450 pass / 606 skip
    test_gemm_triton_generic_fp8.py   21 pass
    test_te_generic_gemm_triton.py   210 pass /  72 skip
    mxfp8/                             4 pass
    test_float8_current_scaling_exact  5 pass /   2 skip

The 160 remaining test_grouped_linear_accuracy failures are the
grouped-vs-sequential tolerance mismatches (test-design issue,
category 3 in the earlier analysis) -- addressed separately.
Eight tests in test_numerics.py compare a grouped/batched GEMM path
against a sequence of individual GEMM calls, asserting bit-exact
(rtol=0, atol=0) equivalence. The upstream code even comments this:
"cuBLAS implementation should be bit-wise match" -- the assumption is
that both sides go through the same GEMM backend.

Under NVTE_USE_GEMM_TRITON=1 that assumption breaks by design: the
"sequential" side (individual Linear/general_gemm calls) is redirected
onto our Triton kernel while the "grouped" side is still hipBLASLt
grouped (or CUTLASS grouped, or AITER Triton grouped). Two different
implementations cannot match bit-exact; the AMD-added `use_triton`
branch's fp32 tolerance (atol=2.6e-6, rtol=0.05) is also too tight
for two different Triton kernels -- fp32 rounding overshoots it by
~5-16x on 4-19 elements out of 2.36M (measured on gfx950 / PyTorch 2.10).

These tests were not written to validate our regular Triton backend,
so we skip them under the override rather than loosen tolerances just
to make them pass vacuously. The direct Triton GEMM test files
(test_gemm_triton.py, test_gemm_triton_generic_fp8.py,
test_te_generic_gemm_triton.py, mxfp8/) cover the correctness of the
Triton kernel itself.

Tests marked:
- test_grouped_linear_accuracy
- test_grouped_linear_accuracy_cutlass
- test_grouped_linear_accuracy_save_original_input
- test_grouped_linear_accuracy_single_gemm
- test_padding_grouped_linear_accuracy
- test_padding_grouped_linear_accuracy_save_original_input
- test_grouped_gemm
- test_fp8_grouped_gemm

Not marked (different premise):
- test_linear_accuracy_save_original_input compares two Linear layers
  differing only in the save_original_input flag; both sides use the
  same GEMM backend, so the equivalence premise still holds. Its
  failures (4 cases) point at a real behavior difference and are worth
  investigating separately.
- test_fp8gemm_with_unfused_quantization compares fused vs. unfused
  quantization; also a distinct assertion.

Sanity: with NVTE_USE_GEMM_TRITON=1, the three largest of the eight
above (test_grouped_linear_accuracy, test_grouped_gemm,
test_fp8_grouped_gemm) collect 1686 items -- all skipped, 0 failed,
2.7s. Without the env var, the skipif condition is False and the
tests run normally.
te_generic_gemm_triton receives `quantization_params` (renamed `quantizer`
here) from general_gemm but silently drops it: the wrapper allocates D
as `output_dtype` (typically fp32/bf16), runs the kernel with
`output_fp8 = False`, and returns the raw high-precision tensor.

Under NVTE_USE_GEMM_TRITON=1, callers that request fused FP8 output
(e.g. quantization_params=Float8CurrentScalingQuantizer) then get a
plain fp32 torch.Tensor back where general_gemm's other paths return a
Float8Tensor. Downstream, `.dequantize()` on the fp32 tensor is a
no-op, so the "compare fused vs post-quantize" invariant
(test_fp8gemm_with_unfused_quantization) reduces to
"raw fp32 GEMM output ~= that output quantized then dequantized" and
fails at ~99.9% of elements with abs deltas at data scale (~0.73),
not rounding scale.

Wire it up minimally by applying the quantizer to D before returning.
The FUSED path in matmul_kernel (OUTPUT_FP8 with c_scale + c_amax) is
still not exercised through this wrapper -- that's a follow-up perf
optimization, not a correctness fix. This makes the wrapper's output
bit-identical to the "compute in fp32, then quantize" path that
general_gemm's C++ path also produces, so both branches of
test_fp8gemm_with_unfused_quantization now converge on the same
Float8Tensor.

Impact on test_numerics.py under NVTE_USE_GEMM_TRITON=1:
  2 failed -> 0 failed
  test_fp8gemm_with_unfused_quantization[out_quantizer0-input_quantizer0-datatype{0,1}-32]
  now PASSED.

No regression on the direct Triton GEMM suites:
  test_gemm_triton.py               450 pass /  606 skip
  test_gemm_triton_generic_fp8.py    21 pass
  test_te_generic_gemm_triton.py    210 pass /   72 skip
  mxfp8/                              4 pass
  test_float8_current_scaling_exact   5 pass /    2 skip
Consistency / conciseness pass over the CI-triage additions from this
branch. No behavior change; verified with a full test_numerics.py run
(0 failed / 1058 passed / 2644 skipped / 28 xfailed) and the direct
suites unchanged.

1) conftest.py: drop marker "does not support Format.HYBRID" -- it is
   already subsumed by "The Triton GEMM backend (NVTE_USE_GEMM_TRITON=1)
   does not support" (the HYBRID ValueError begins with that phrase).
   Halve the module docstring; the hook is 12 lines and does not need a
   15-line preamble.

2) test_float8_current_scaling_exact.py: remove
   _skip_if_hybrid_under_gemm_triton and its two call-sites. The
   conftest hook catches the same quantization.py::check_recipe_support
   ValueError repo-wide, so the per-file helper is now dead code.
   Removes ~30 lines (helper + constant + two call-sites).

3) gemm_triton.py Float8TensorWrapper: shrink the NVFP4 refusal
   comment from 8 lines to 3. The point is the `_dtype` vs `dtype`
   mismatch on QuantizedTensorStorage; everything else was rationale
   that belongs in the commit message, not at the point of use.

4) gemm_triton.py: unify int64-promotion comments at the four
   secondary sites to `# int64 promotion (see matmul_kernel A/B).`
   The detailed rationale stays at matmul_kernel A/B; the C-ptr
   comment that said "M*N can exceed 2^31" was slightly misleading
   because the concrete overflow that triggered our SIGABRT was
   `offs_am * stride_am = M*K`, not M*N.
@@ -0,0 +1,210 @@
# Copyright (c) 2024, Advanced Micro Devices, Inc. All rights reserved.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copyright: please extend the AMD year to the current year.

Suggested change
# Copyright (c) 2024, Advanced Micro Devices, Inc. All rights reserved.
# Copyright (c) 2024-2026, Advanced Micro Devices, Inc. All rights reserved.

@@ -0,0 +1,452 @@
# Copyright (c) 2024, Advanced Micro Devices, Inc. All rights reserved.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copyright: please extend the AMD year to the current year.

Suggested change
# Copyright (c) 2024, Advanced Micro Devices, Inc. All rights reserved.
# Copyright (c) 2024-2026, Advanced Micro Devices, Inc. All rights reserved.

@@ -0,0 +1,82 @@
#!/usr/bin/env python3

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copyright: this new file is missing an AMD copyright header. Per repo convention it should have an AMD header (year = current year) followed by a See LICENSE for license information. line. Please add above the shebang (or replace line 1) with:

Suggested change
#!/usr/bin/env python3
#!/usr/bin/env python3
# Copyright (c) 2026, Advanced Micro Devices, Inc. All rights reserved.
#
# See LICENSE for license information.

@@ -0,0 +1,118 @@
#!/usr/bin/env python3

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copyright: this new file is missing an AMD copyright header. Please add one below the shebang:

Suggested change
#!/usr/bin/env python3
#!/usr/bin/env python3
# Copyright (c) 2026, Advanced Micro Devices, Inc. All rights reserved.
#
# See LICENSE for license information.

Comment on lines +48 to +66
def test_basic_fp32_gemm():
"""Test basic FP32 GEMM for reference"""
try:
M, N, K = 128, 256, 512

A_fp32 = torch.randn(M, K, device='cuda', dtype=torch.float32)
B_fp32 = torch.randn(K, N, device='cuda', dtype=torch.float32)

print(f"✓ Created test tensors: A={A_fp32.shape}, B={B_fp32.shape}")

# Compute reference
C_ref = torch.matmul(A_fp32, B_fp32)
print(f"✓ Computed FP32 reference: C={C_ref.shape}")

assert C_ref.shape == (M, N), f"Expected shape ({M}, {N}), got {C_ref.shape}"

except Exception as e:
pytest.fail(f"Tensor creation failed: {e}")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

test_basic_fp32_gemm only exercises torch.matmul on plain fp32 tensors — no MXFP8 code path or wrapper is touched. Combined with the outer try/except Exception → pytest.fail(str(e)) pattern (also present in the other two tests here), a bug in the surrounding code wouldn't be detected by this test, and real assertion failures would be turned into truncated pytest.fail messages that hide the traceback. Suggest deleting this test and dropping the try/except wrappers in the other two — real exceptions from pytest give better diagnostics than pytest.fail(str(e)).

Comment thread tests/pytorch/mxfp8/README.md Outdated
- **E8M0 conversion**: `scale = 2^(biased_exponent - 127)`
- **Dual storage**: Rowwise + columnwise copies (MXFP8 cannot be transposed after quantization)

See `/root/.claude/plans/ancient-chasing-robin.md` for full implementation plan.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This references a local Claude Code plan file (/root/.claude/plans/…) that doesn't exist in the repo and won't exist on anyone else's machine. Please remove this line (or replace with a link to a real doc / PR description).

Suggested change
See `/root/.claude/plans/ancient-chasing-robin.md` for full implementation plan.

@github-actions

Copy link
Copy Markdown

Claude review

Reviewed the PR against the merge base (bc3766e6, ~2.7k additions across 12 files). This is the first Claude review pass on the PR.

High-level verdict. The Triton GEMM backend is well-scoped: correctness-first, cleanly opt-in via NVTE_USE_GEMM_TRITON=1, and the default hipBLASLt path is preserved. The gating rationale for HYBRID / mixed-FP8 / NVFP4 (via runtime ValueError + self-retiring pytest hook) is nicely aligned with the referenced upstream Triton bug (triton-lang/triton#9567). Rebase notes and the correctness fixes flagged in the PR description (int64 pointer promotion, bias epilogue wiring, batched-columnwise permutation) are convincing.

Main concern. In transformer_engine/pytorch/cpp_extensions/gemm.py, the new top-level from ..gemm_triton import te_generic_gemm_triton triggers an unconditional import triton even when NVTE_USE_GEMM_TRITON=0 (the default). This makes triton a hard dependency of the entire PyTorch backend — please make the import lazy inside the if use_gemm_triton: branch. Also, the commented-out #from ..gemm_triton import te_gemm_triton sibling line should be removed. See inline comment.

Other inline findings (all minor):

  • One test (test_basic_fp32_gemm) doesn't exercise MXFP8 and should be dropped; broad try/except Exception → pytest.fail(str(e)) in the two mxfp8/ tests hides tracebacks — recommend removing the wrappers.
  • tests/pytorch/mxfp8/README.md references a local /root/.claude/plans/... path.
  • Minor style nit on the env-var lookup line.

Copyright headers: 6 files need updates.

  • 4 files carry AMD Copyright (c) 2024 — should be 2024-2026 (current-year end): transformer_engine/pytorch/gemm_triton.py, tests/pytorch/test_gemm_triton.py, tests/pytorch/test_gemm_triton_generic_fp8.py, tests/pytorch/test_te_generic_gemm_triton.py.
  • 2 new files are missing an AMD header entirely: tests/pytorch/mxfp8/test_mxfp8_gemm_basic.py, tests/pytorch/mxfp8/test_mxfp8_kernel_direct.py. All modified upstream files (quantization.py, cpp_extensions/gemm.py, test_numerics.py, ci/pytorch.sh, conftest.py) have correct AMD lines with the NVIDIA year preserved.

See inline comments for suggested fixes.

Match the layout of the existing tests/pytorch/triton_kernels/ suite
(test_cast.py, test_cast_mxfp8.py, test_grouped_gemm.py, test_norms.py):
one test file per Triton kernel module, named after the module, and
mxfp8-specific tests split into a sibling file (mirrors the
test_cast.py / test_cast_mxfp8.py pair).

Renames (via git mv, contents unchanged):
- test_gemm_triton.py               -> triton_kernels/test_gemm.py
- test_gemm_triton_generic_fp8.py   -> triton_kernels/test_gemm_fp8.py
- test_te_generic_gemm_triton.py    -> triton_kernels/test_gemm_generic.py

Merge (two small files consolidated into one):
- mxfp8/test_mxfp8_gemm_basic.py  (82 lines)
+ mxfp8/test_mxfp8_kernel_direct.py (118 lines)
  -> triton_kernels/test_gemm_mxfp8.py (158 lines after de-duplicated
     imports and a shared torch_version() < (2, 10) module-level
     skip guard).

The dev-owned files under tests/pytorch/mxfp8/ (README.md,
mxfp8_utils.py, test_mxfp8_group_quantize_graph_safe.py,
test_mxfp8_quantize_swizzle_fusion.py) are untouched -- they belong
to the upstream MXFP8 quantize/swizzle work, not to our Triton GEMM
backend. The standalone Triton compiler bug reproducer at
triton_dot_scaled_rhs_scale_bug.py lives on the sibling
triton_gemm_mxfp8_local branch and is not in the PR tree.

ci/pytorch.sh: point the "triton" label at the new paths (5 lines
removed, 4 lines added -- the two mxfp8 lines collapse into one).

No behavior change. Verified on gfx950 / PyTorch 2.10:
  triton_kernels/test_gemm.py           450 pass /  606 skip  (54s)
  triton_kernels/test_gemm_fp8.py        21 pass              (16s)
  triton_kernels/test_gemm_generic.py   210 pass /   72 skip  (62s)
  triton_kernels/test_gemm_mxfp8.py       4 pass              (5s)
Added alongside the initial MXFP8 GEMM test suite in an earlier
commit. Now that test_mxfp8_gemm_basic.py and test_mxfp8_kernel_direct.py
have moved to tests/pytorch/triton_kernels/ (merged into
test_gemm_mxfp8.py), the README's context is stale and there is
nothing left in mxfp8/ that we own to document.
Fold tests/pytorch/triton_kernels/test_gemm_fp8.py into
tests/pytorch/triton_kernels/test_gemm_generic.py, then rename the
files to reflect the new roles:

  test_gemm_generic.py -> test_gemm.py         (primary user-facing)
  test_gemm.py         -> test_gemm_kernel.py  (low-level kernel)

Merge details -- the 3 tests in test_gemm_fp8.py resolve as:

- test_generic_gemm_triton_fp8 (M x K x N x fp8_format x layout, 18
  parametrizations): dropped. Fully redundant with
  test_triton_vs_pytorch_fp8 in test_gemm.py, which exercises the same
  general_gemm path with a superset of shapes (6 shapes vs 3).

- test_generic_gemm_triton_fp8_multidim (2 batched cases): moved.
  Not covered elsewhere -- exercises the wrapper's flatten-leading-dims
  path for ndim > 2. Renamed to test_triton_vs_pytorch_fp8_multidim.

- test_generic_gemm_triton_fp8_backward_compatibility (1 fp16 case):
  dropped. Redundant with test_triton_vs_pytorch_regular[fp16-*].

ci/pytorch.sh: replace the two lines pointing at test_gemm_fp8.py and
test_gemm_generic.py with a single line for the renamed test_gemm.py,
plus the rename of test_gemm_kernel.py.

Result vs previous:
  test_gemm.py             212 pass /  72 skip   (was 210 pass -- +2 multidim)
  test_gemm_kernel.py      450 pass / 606 skip   (unchanged content)
  test_gemm_mxfp8.py         4 pass              (unchanged)
Dropped 19 redundant tests (18 general_gemm_triton_fp8 + 1 bwd_compat).
No behavior change on the retained coverage.
Bot review on PR #667 flagged five items still applicable after the
recent refactors. Applying all five.

1) cpp_extensions/gemm.py: lazy-import te_generic_gemm_triton inside
   the `if use_gemm_triton:` branch instead of at module load. Also
   drop the dead-code commented `#from ..triton_kernels.gemm import
   te_gemm_triton` line and re-space the env-var lookup to match the
   surrounding file's style (double quotes, no interior spaces).

   Note: `triton` remains a hard runtime dep of the PyTorch bindings
   even without our line, because tensor/mxfp8_tensor.py transitively
   pulls in triton_kernels/cast_transpose.py which does
   `import triton` at module scope. This change removes one redundant
   eager import site; making triton a truly soft dep would require
   touching cast_transpose.py and friends -- out of scope for this PR.

2) tests/pytorch/triton_kernels/test_gemm_mxfp8.py:
   - Drop test_basic_fp32_gemm: it only did `torch.matmul` on plain
     fp32 tensors and never touched an MXFP8 code path.
   - Drop the `try/except Exception -> pytest.fail(str(e))` wrappers
     in the remaining tests. They turned real tracebacks into
     truncated one-liners; pytest's native uncaught-exception
     handling gives better diagnostics.
   - Prune debug prints and unused `import sys`.

3) tests/pytorch/triton_kernels/test_gemm.py and test_gemm_kernel.py:
   bump AMD copyright end-year from 2024 to 2024-2026 (both files
   were modified through 2026-07 per repo convention).

Verified on gfx950 / PyTorch 2.10 -- all four suites unchanged:
  test_gemm.py               212 pass /  72 skip
  test_gemm_kernel.py        450 pass / 606 skip
  test_gemm_mxfp8.py           3 pass  (was 4; one dropped as redundant)
  test_float8_current_scaling  5 pass /   2 skip
@wenchenvincent wenchenvincent marked this pull request as ready for review July 10, 2026 16:42
Comment thread tests/pytorch/triton_kernels/test_gemm_kernel.py
Comment thread tests/pytorch/triton_kernels/test_gemm.py
@github-actions

Copy link
Copy Markdown

Claude re-review

Re-reviewed the PR against HEAD^1 (dev tip). Three new commits since the prior review at 2026-07-10T03:19Z, all purely reorganizational:

  • 6177136e — relocate test_gemm_triton*.pytests/pytorch/triton_kernels/; merge two mxfp8/ test files into test_gemm_mxfp8.py
  • db6c724c — drop tests/pytorch/mxfp8/README.md (was the file referencing a local /root/.claude/plans/... path)
  • 3b9bb72c — consolidate test_gemm_fp8.py into test_gemm.py; rename low-level file to test_gemm_kernel.py; drop 19 redundant fp8 parametrizations

Addressed since prior review: the mxfp8 README.md (local Claude path leak) and the two missing-AMD-header findings on mxfp8/test_mxfp8_gemm_basic.py / test_mxfp8_kernel_direct.py (files removed; content merged into test_gemm_mxfp8.py, which has a correct 2026 header). Layout now matches the sibling triton_kernels/ test convention.

Still open from prior review (unchanged in the current PR head, so not re-flagged inline):

  • transformer_engine/pytorch/cpp_extensions/gemm.py — unconditional module-level from ..triton_kernels.gemm import te_generic_gemm_triton (turns triton into a hard dependency of the PyTorch backend even under NVTE_USE_GEMM_TRITON=0); dead #from ..triton_kernels.gemm import te_gemm_triton line; bool( int(...) ) style on line 517 — see prior inline comments.
  • tests/pytorch/triton_kernels/test_gemm_mxfp8.pytest_basic_fp32_gemm (unchanged from the merged-in file) still exercises only torch.matmul on fp32 and touches no MXFP8 path; the surrounding try/except Exception → pytest.fail(str(e)) pattern still hides tracebacks. Same finding as the prior comments on the pre-merge mxfp8 files.

Copyright headers: 2 files need updates. Both are renames from the reorganization that carried a stale Copyright (c) 2024, line forward — see inline suggestions on tests/pytorch/triton_kernels/test_gemm.py:1 and tests/pytorch/triton_kernels/test_gemm_kernel.py:1. All other in-scope files (including new triton_kernels/gemm/*.py and conftest.py) have correct headers. NVIDIA copyrights on modified upstream files are preserved.

Comment thread tests/pytorch/triton_kernels/test_gemm_kernel.py Outdated
Comment thread tests/pytorch/triton_kernels/test_gemm_kernel.py
Comment thread tests/pytorch/triton_kernels/test_gemm_mxfp8.py Outdated
Comment thread tests/pytorch/triton_kernels/test_gemm_mxfp8.py Outdated
Comment thread tests/pytorch/conftest.py
Comment thread tests/pytorch/test_numerics.py Outdated
Comment thread transformer_engine/pytorch/cpp_extensions/gemm.py Outdated
Comment thread transformer_engine/pytorch/triton_kernels/gemm/__init__.py Outdated
Comment thread transformer_engine/pytorch/triton_kernels/gemm/gemm_common.py Outdated
Applied review comments 1, 2, 3, 4, 6, 7, 8 from PR #667 in one pass.
Comments 5 and 9 will be answered on-thread (see below).

Applied
=======

#7 cpp_extensions/gemm.py: gate NVTE_USE_GEMM_TRITON behind
   IS_HIP_EXTENSION. Our Triton kernels use gfx942/gfx950-specific MFMA
   instructions and autotune configs; refuse to enable on non-HIP
   builds.

#1 + #4 + #8: deduplicate dtype utilities against
   triton_kernels/common.py, which is the authoritative source shared
   across all Triton kernel backends here.

   - gemm_common.py: drop our copies of _get_fp8_dtypes(),
     torch_to_te_dtype(), te_to_torch_dtype(). Keep is_fp8_dtype()
     (operates on tex.DType; common.py's is_fp8_torch_dtype operates on
     torch dtypes -- different signatures), reinterpret_as_fp8_tensor,
     getGemmOutputShape, product.
   - gemm_wrapper.py: import torch_dtype_to_te_dtype /
     te_dtype_to_torch_dtype from ..common; use those instead of the
     removed local names.
   - __init__.py: drop the redundant re-exports (torch_to_te_dtype,
     te_to_torch_dtype, _get_fp8_dtypes). Note in the file directing
     callers to triton_kernels.common.
   - test_gemm_kernel.py: import get_torch_e4m3_type,
     get_torch_e5m2_type, torch_dtype_to_te_dtype from
     triton_kernels.common; define a local _get_fp8_dtypes() shim over
     them to keep the test's parametrization tables unchanged.
   - test_gemm_mxfp8.py: previously used _get_e4m3_dtype() -- dropped
     along with the test that used it (see below).

#2 + #3: add module docstrings to test_gemm.py, test_gemm_kernel.py,
   test_gemm_mxfp8.py making the scope of each file explicit and
   pointing at the sibling files. Prompted by wangye805's confusion
   between test_gemm.py and test_gemm_kernel.py, and by the question of
   whether MXFP8 is only tested in test_gemm_mxfp8.py.

#6 test_numerics.py: rewrite the comment above and the reason= text
   for _skip_grouped_under_gemm_triton to make it explicit that the
   skip is a *backend mismatch* between the two sides of the
   comparison, not Triton non-determinism.
     - sequential side -> our Triton (via NVTE_USE_GEMM_TRITON=1)
     - grouped side    -> hipBLASLt / CUTLASS / AITER-Triton grouped
                          (controlled by the SEPARATE
                          NVTE_USE_GROUPED_GEMM_TRITON env var)
   With just NVTE_USE_GEMM_TRITON=1 set, the two sides diverge in fp32
   rounding because they run different kernels. Not our Triton being
   non-deterministic.

Also removed test_mxfp8_kernel_with_simulated_data (an early kernel-
bring-up smoke test that only asserted non-zero output; MXFP8
correctness is fully covered by test_gemm.py::test_triton_vs_*_mxfp8
with real MXFP8Tensor and both PyTorch/C++ references).

To be answered on-thread
========================

#5 (conftest.py hook vs explicit skip lists) -- keep the hook: it fires
   only on three specific ValueError substrings from our own gate code,
   is self-retiring when the gates are relaxed, and explicit skips
   would require ~1300 marks. Detail in the PR reply.

#9 (Float8TensorWrapper / MXFP8TensorWrapper vs the TE tensor types) --
   defer; needs a side-by-side to see how much of the wrapper API is
   genuinely required for the Triton-kernel call surface (fields
   accessed, dimension reordering, missing-transpose fallbacks) vs.
   what could route through the TE tensor types directly.

Verified on gfx950 / PyTorch 2.10 -- all three direct suites clean:
  test_gemm.py         212 pass /  72 skip
  test_gemm_kernel.py  450 pass / 606 skip
  test_gemm_mxfp8.py     2 pass          (was 3; smoke test removed)
Both remaining tests were redundant:

- test_mxfp8_imports: bare 'imports don't ImportError' check. The same
  three modules are imported at the top of test_gemm.py; if any went
  missing, collection there fails with the same signal.

- test_mxfp8_wrapper_regular_tensor: MXFP8TensorWrapper's
  non-MXFP8-input fallback branch is implicitly exercised on every
  test_triton_vs_pytorch_regular / _vs_cpp_regular case in test_gemm.py,
  because te_generic_gemm_triton wraps the plain-precision inputs
  through the same path.

Also remove the corresponding ci/pytorch.sh line and update the two
docstrings that pointed at the file as a sibling.
…ti-stream

Align the block comment and reason= text with the framing from the PR
thread reply on wangye805's comment: the original test invariant is that
'the same GEMM kernel, used in sequential Linear and multi-stream gemm,
reproduces the same result'. Under NVTE_USE_GEMM_TRITON=1 the Triton
kernel is not used inside the multi-stream GEMM path, so the invariant
does not apply.

Drops the backend enumeration (hipBLASLt / CUTLASS / AITER-Triton
grouped) in favor of the higher-level 'multi-stream GEMM path' phrasing.
…age directly

Response to wangye805 review comment #9: the wrapper classes were doing
mostly pure delegation to the underlying QuantizedTensorStorage fields
(_data, _transpose, _scale_inv, _fp8_dtype, _rowwise_data,
_columnwise_data, _rowwise_scale_inv, _columnwise_scale_inv). A
side-by-side found ~180 lines were pure attribute passthrough; only
two things were doing real work:

  1. Reconstructing a rowwise buffer from a columnwise-only
     Float8TensorStorage by inverting tex.fp8_transpose's
     [K, D0..D_{n-2}] permutation. (Was the site of a batch-dim
     scrambling bug earlier in this branch.)
  2. Selecting between the rowwise and columnwise MXFP8 pre-quantized
     copies based on the BLAS transa/transb flags, matching the C++
     CanonicalizeGemmInput mapping (MXFP8 cannot be safely re-transposed
     after quantization).

Both are kept as free functions in gemm_common.py --
materialize_rowwise_from_columnwise() and data_and_scale_for_transpose().
Subclassing the storage types was rejected: TE creates the storage
instances inside its own quantizers, so a subclass would never actually
reach us; we'd have to either modify production TE tensor code (which
this PR must not do) or wrap/cast at dispatch time (which is what the
old wrappers already did).

te_generic_gemm_triton now:
  - classifies each operand once via _classify_input() into
    ("regular", "fp8", "mxfp8") -- NVFP4 and other unsupported
    QuantizedTensorStorage subclasses hit the same ValueError as before
  - reads storage attributes directly, no per-instance wrapper object
  - shares the FP8 A/B extraction via _extract_fp8_operand(), which
    handles the columnwise-only materialization once and returns
    (data, scale_inv, fp8_dtype, size)

Semantics vs. the old wrappers -- verified line-by-line against the C++
NVTETensorFromFloat8Tensor / CanonicalizeGemmInput contract:

  Float8:
    - "both None" -> RuntimeError with the exact same message text
    - _transpose_invalid check preserved
    - Columnwise-only rowwise materialization preserved
    - Materialized buffer cached in local for reuse (size(), reshape,
      kernel call all read one materialization)
    - Regular-tensor fallback (empty _scale_inv, _fp8_dtype=None)
      preserved
  MXFP8:
    - transa/transb -> rowwise/columnwise mapping preserved (matches
      C++ CanonicalizeGemmInput)
    - "neither rowwise nor columnwise" is now an *explicit*
      RuntimeError (was implicit .size()-on-None in the old wrapper --
      small hardening)
    - Mixed MXFP8/non-MXFP8 refusal preserved
    - torch >= 2.10 gate preserved
  NVFP4:
    - "does not support {ClassName}" ValueError preserved (moved from
      Float8TensorWrapper.__init__ to _classify_input at the dispatch
      boundary)

Validation
==========

Direct suites (gfx950 / PyTorch 2.10):
  triton_kernels/test_gemm.py            212 pass /  72 skip
  triton_kernels/test_gemm_kernel.py     450 pass / 606 skip
  test_float8_current_scaling_exact.py     5 pass /   2 skip

Megatron llama3-8B e2e (12 iters, MBS=2 BS=64 SEQ=8192 TP=1 CP=1,
8 x gfx950), losses at iter 12 shown for comparison:

  BF16 hipBLASLt                 3574 ms/iter, 945 TF/s   loss 0.045  (baseline)
  BF16 our Triton                5385 ms/iter, 632 TF/s   loss 0.045  ok
  FP8 delayed E4M3 our Triton    3012 ms/iter,1120 TF/s   loss 0.051  ok
  MXFP8 our Triton               3662 ms/iter, 921 TF/s   loss 0.091  ok
  MXFP8 hipBLASLt (control)      2707 ms/iter,1246 TF/s   loss 0.099

All three refactored dispatch branches exercised at model scale --
"regular" (BF16 through our Triton), "fp8" (E4M3-only delayed recipe;
HYBRID stays gated off), and "mxfp8". 0 skipped iters, 0 NaN iters
across all runs. MXFP8 recipe plateau at ~0.09 reproduces on the
hipBLASLt control -- it's a property of the recipe on this config,
not our kernel.

Diffstat: 3 files changed, +216 -383 lines net.
Three small cleanups noticed during a full-tree review after
fe5d9d6 (the Float8TensorWrapper / MXFP8TensorWrapper removal):

- gemm_wrapper.py + gemm_common.py: drop the ``torch_dtype_to_te_dtype``
  import. It was carried through from the pre-refactor code but has
  no call sites left; ``te_dtype_to_torch_dtype`` remains and is still
  used.
- gemm_wrapper.py: rewrite the "Compute dimensions using wrapper sizes /
  Wrapper handles Float8TensorStorage which doesn't have .shape" comment
  block above the M/N/K computation. There is no wrapper anymore --
  A_size / B_size are picked in the FP8 / MXFP8 / regular branches of
  te_generic_gemm_triton and always exist.
- tests/pytorch/conftest.py: the marker-list block comment said the
  NVFP4-refusal ValueError came from Float8TensorWrapper. It now comes
  from _classify_input in gemm_wrapper.py. Text updated; the marker
  strings themselves are unchanged (the error message text is the
  same, verified against gemm_wrapper.py:72-78).

Zero behavior change. Import sanity + a 36-case FP8 pytest slice
(test_triton_vs_cpp_fp8) both pass.

Also verified during the review, nothing to fix:
- No orphan Float8TensorWrapper / MXFP8TensorWrapper class references
  anywhere in the tree.
- No orphan attribute accesses (.is_fp8, .is_mxfp8, .nominal_dtype,
  .get_data_for_gemm, .get_data_and_scale_for_gemm, A_wrapper,
  B_wrapper).
- Only one Python import site outside the gemm package
  (test_gemm_kernel.py -> te_gemm_triton, re-exported via __init__.py).
- cpp_extensions/gemm.py's lazy from ..triton_kernels.gemm import
  te_generic_gemm_triton inside the if use_gemm_triton branch is
  still intact.
- Conftest hook markers still match the current error message text
  raised by _classify_input.
- Semantic contract vs. old wrappers preserved: RuntimeError text for
  "neither rowwise nor columnwise" case, _transpose_invalid check,
  output-dtype fallback chain via a_nominal_dtype (mxfp8 -> float32
  fallback, fp8 -> None fallback that raises if output_dtype is also
  None, regular -> A.dtype), MXFP8 will_transpose mapping matching the
  C++ CanonicalizeGemmInput table.
@wenchenvincent wenchenvincent requested a review from wangye805 July 13, 2026 21:45
@wenchenvincent wenchenvincent added ci-level 3 CI test level 3 and removed ci-level 3 CI test level 3 labels Jul 13, 2026
Response to wangye805 review comment #1 (similar util already exists
in test_common). Drop the local helpers that duplicated the
str -> torch dtype mapping:

- Delete `_get_fp8_dtypes()` shim -- inline the two
  ``get_torch_e4m3_type()`` / ``get_torch_e5m2_type()`` calls.
- Delete the ``tl_to_torch_types`` dict entirely -- it was just an
  inverted map of ``name_to_tl_types``, and
  ``test_common.str_to_torch_dtype`` covers the same conversion
  directly from the string name (already has fp16/bf16/fp32/fp8e4/fp8e5).
- Keep ``name_to_tl_types`` -- ``gen_input`` still needs the Triton
  dtype for the reference ``copy_kernel`` and to decide the fp8 code
  path; there is no str -> tl.dtype helper in test_common.

Replaces at three call sites in ``test_correctness``:
  torch_out_dtype = tl_to_torch_types[name_to_tl_types[out_dtype]]
    -> str_to_torch_dtype(out_dtype)
  A_type = torch_to_te_dtype(tl_to_torch_types[name_to_tl_types[a_in_dtype]])
    -> torch_to_te_dtype(str_to_torch_dtype(a_in_dtype))
  (same for B_type, D_type, torch_bias_dtype)

Import style follows the sibling triton_kernels tests
(``from test_common import ...``, not a relative import -- pytest adds
the test directory to sys.path so bare imports resolve).

Zero behavior change. Verified: 1056 tests collected, 450 passed /
606 skipped -- identical to the pre-cleanup baseline.
@wenchenvincent wenchenvincent added the ci-level 3 CI test level 3 label Jul 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci-level 3 CI test level 3

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants