Skip to content

CuTeDSL NVFP4 1D quantize kernels - #3809

Draft
tdophung wants to merge 4 commits into
NVIDIA:mainfrom
tdophung:teddy/nvfp4-cutedsl-register-quantize
Draft

tdophung wants to merge 4 commits into
NVIDIA:mainfrom
tdophung:teddy/nvfp4-cutedsl-register-quantize

Conversation

@tdophung

@tdophung tdophung commented Nov 4, 2026 •

Copy link
Copy Markdown
Collaborator

Description

Adds register-resident (g2r) NVFP4 1D quantize kernels to the CuTeDSL NVFP4 backend: the CuTeDSL counterparts of the CUDA register kernel in the companion CUDA PR (teddy/nvfp4-register-quantize-1d). The backend is organized like the MXFP8 one after #3642: a common module with the config, a kernel base class (supports() / eligible_to_dispatch()), the NVFP4QuantizeEntry dispatcher and compilation, plus one module per kernel.

Depends on #3223. The first commit brings that PR's NVFP4 backend onto current main, whose CuTeDSL infrastructure has changed since it branched; it stays authored by its original author. This PR will be rebased onto #3223 once that lands, so the first commit is expected to shrink or disappear. Opened as a draft for visibility until then.

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

  • Port the [Common] Experimental CuTeDSL NVFP4 backend #3223 CuTeDSL NVFP4 backend to the current CuTeDSL plumbing (NVTE_WITH_CUTEDSL, NVTE_ENABLE_CUTEDSL_BACKEND, TVMFFIConfigCache), with CUDA fallback (and a reason under NVTE_WARN_IF_CUTEDSL_BACKEND_NOT_CHOSEN) for configs the compiled ABI cannot take. Also fixes the transposed-output shape for rank > 2 inputs and a ptxas rejection of an immediate operand to max.xorsign.abs.bf16x2.
  • Split the backend into quantize_nvfp4_common.py and per-kernel modules; the TMA-staged tuned-1D kernel becomes NVFP4QuantizeGeneralG2SKernel, the fallback for every config.
  • Add NVFP4QuantizeRowwiseG2RKernel (rowwise only) and NVFP4QuantizeBidimensionalG2RKernel (rowwise + columnwise), dispatched for BF16 input without fast math or row-scaled NVFP4 when their alignment and grid checks pass; exactly one kernel launches per call. Round-to-nearest output is byte-identical to the CUDA kernels.
  • Stochastic rounding in the g2r kernels draws Philox once per output block, keyed by the block's coordinates and direction (philox_rng.py); each draw is plain Philox4x32 (as curanddx computes it) with NVTE_BUILD_NUM_PHILOX_ROUNDS rounds. Its random bits for a given seed differ from the general kernel's.
  • Tests (test_nvfp4_cutedsl_backend.py, laid out like the MXFP8 one): CUDA vs CuTeDSL byte equality over shapes, directions, fast math, row scaling, zero blocks and the noop flag; inputs the g2r kernels must hand to g2s (16-byte-aligned pointer, grid.y overflow); stochastic rounding checked statistically and for determinism; the per-block draw against plain Philox4x32.

Results (GB200, ncu kernel time, BF16 randn, E4M3 scales, cold L2)

Speedup over TE main (quantize_transpose_tuned_1D), shapes 1024x1024 to 32768x16384:

Mode CuTeDSL g2r vs main CuTeDSL g2r vs CUDA register kernel
Rowwise, round-to-nearest 1.04-1.48x 0.99-1.04x
Rowwise, stochastic rounding 1.12-1.49x 0.97-1.08x
Rowwise + columnwise, round-to-nearest 1.12-1.41x 1.00-1.04x
Rowwise + columnwise, stochastic rounding 1.21-1.63x 1.00-1.13x

Per-shape numbers and the diff: https://claude.ai/artifact/3eqb1p2shCLjyyD68SuQ3f?sk=R5I3hkJLPOMfmm88DJYRUA

Testing

  • tests/pytorch/nvfp4/test_nvfp4_cutedsl_backend.py with NVTE_ENABLE_CUTEDSL_BACKEND=1: 53 passed.
  • C++ test_operator with the backend on, --gtest_filter='OperatorTest/FusedCastTransposeNVFP4*:OperatorTestRowScaled/FusedCastTransposeNVFP4*:OperatorTestUE5M3/FusedCastTransposeNVFP4*': 112 passed.
  • Old-vs-new dump comparison against the CUDA register kernel (580 cases, 196 served by CuTeDSL, including SR payloads, special values, ragged/tall shapes, noop, and inputs over 2^31 elements): byte-identical apart from 3 cases at 262144x64 that the CUDA kernel's row limit sends to tuned_1D.

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

janekb04 and others added 3 commits November 2, 2026 15:27
…tructure

Bring the NVFP4 tuned-1D CuTeDSL backend from NVIDIA#3223 onto main, whose
CuTeDSL plumbing has changed since that PR branched:

- Build and dispatch the backend only with NVTE_WITH_CUTEDSL, and enable
  it with NVTE_ENABLE_CUTEDSL_BACKEND like MXFP8.
- Give NVFP4QuantConfig a to_id() and SM architecture so it uses
  TVMFFIConfigCache, and pass the device index to DLTensorWrapper, whose
  FP4 packing main already provides.
- Pass the transposed output as (cols, rows): for a rank > 2 input TE
  shapes it {cols, d0, d1, ...}, which the default 2D flattening of
  DLTensorWrapper collapses to the wrong extents.
- Fall back to CUDA, with a reason under
  NVTE_WARN_IF_CUTEDSL_BACKEND_NOT_CHOSEN, for configurations the
  compiled ABI cannot take: non-BF16 input, GEMM-swizzled or non-E4M3
  scales, missing amax tensors, and flat dims not divisible by 32.
- Keep the NVFP4-only helpers in CuTeDSL/cast/nvfp4/utils.py and
  philox_rng.py instead of the shared utils.py, using main's inline_ptx
  style, and register the native TVM-FFI function as MXFP8 does.
- Start the packed abs-max from a zero held in a register: as a literal
  it became an immediate operand, which ptxas rejects for
  max.xorsign.abs.bf16x2.
- Switch the test to main's backend switches and config keys.

Signed-off-by: Jan Bielak <jbielak@nvidia.com>
Signed-off-by: Teddy Do <tdophung@nvidia.com>
Lay out the NVFP4 backend the way the MXFP8 one is organized:
quantize_nvfp4_common.py holds the config, a kernel base class with
supports() / eligible_to_dispatch(), the NVFP4QuantizeEntry that picks a
kernel at run time, and compilation and TVM-FFI registration. The
TMA-staged tuned-1D kernel moves to quantize_nvfp4_g2s_general.py as
NVFP4QuantizeGeneralG2SKernel, the fallback for every config, so that
specialized kernels can be added beside it without touching it.

No functional change.

Signed-off-by: Teddy Do <tdophung@nvidia.com>
Add two specialized kernels to the CuTeDSL NVFP4 backend, the CuTeDSL
counterparts of the CUDA register-resident 1D quantize kernel, and
dispatch to them ahead of the general TMA-staged kernel for BF16 input
without fast math or row-scaled NVFP4:

- NVFP4QuantizeRowwiseG2RKernel (rowwise only) streams each 16-element
  block from one 32-byte load straight to its output, with the next wave
  of rows prefetched into L2.
- NVFP4QuantizeBidimensionalG2RKernel (rowwise + columnwise) reads every
  row twice, once per direction, with the second read served by L2, and
  stages only the transposed output in shared memory. A cost model picks
  the number of 64-row slabs per CTA per launch.

The per-block coefficient depends only on the block's scale byte, so its
divide is computed once per CTA into a shared-memory table. Round-to-
nearest output is byte-identical to the CUDA kernels.

Stochastic rounding draws Philox once per output block, keyed by the
block's coordinates and direction (philox_rng.py). Each draw is plain
Philox4x32 of a known counter, as curanddx computes it, with the round
count from NVTE_BUILD_NUM_PHILOX_ROUNDS; builds with fewer than 3 rounds
keep the general kernel for stochastic rounding. The random bits for a
given seed differ from the general kernel's, which follow its thread
layout.

The kernels launch with the driver's default shared-memory carveout:
with min_blocks_per_mp set, CuTeDSL otherwise derives one that ignores
the driver's per-CTA reservation and limits residency below the target.

Test that the folded block draw equals plain Philox4x32, and that an
input that is only 16-byte aligned is served by the general kernel.

Signed-off-by: Teddy Do <tdophung@nvidia.com>
@tdophung tdophung changed the title Add register-resident CuTeDSL NVFP4 quantize kernels CuTeDSL NVFP4 1D quantize kernels Nov 4, 2026
Follow test_mxfp8_cutedsl_backend.py: quantize with the CUDA and the
CuTeDSL backend and compare bytes, guarding against a silent CUDA
fallback by checking that the config's kernel is registered. Drop the
call-counting proxies and the cases for configs the CuTeDSL backend
does not serve, which only compared the CUDA kernel against itself.

Add the g2r grid.y overflow cases that the g2s kernel must serve, and
shorten the module docstring.

Signed-off-by: Teddy Do <tdophung@nvidia.com>

This branch has not been deployed

No deployments
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.

2 participants