Repository navigation
[Common, PyTorch] Add grouped NVFP4 dequantization - #3591
harshithkantamneni wants to merge 5 commits into
Conversation
nvte_group_dequantize had no NVFP4 path (NVIDIA#2726). Add one for rowwise data with compact (non-swizzled) E4M3 scales, as written by the grouped NVFP4 quantizer: one FP32 amax for the group or one per tensor, and tensors that share the last dimension (equal shapes or a varying first dimension). Each thread dequantizes one 16-element block with the same arithmetic as the single-tensor NVFP4 kernel, so the results match it bitwise. Fix the PyTorch group_dequantize binding for NVFP4: size the FP4 data in elements rather than bytes, pass the amax and the swizzled-scale flag, and reject row-scaled NVFP4 and an E4M3 max other than 448, which the grouped tensor cannot describe. Add a C++ test that compares grouped against per-tensor dequantization bitwise, and PyTorch tests against the grouped NVFP4 quantizer, including an empty tensor, the swizzled-scale error and CUDA graph capture. Signed-off-by: Harshith Kantamneni <hkantamneni2@wisc.edu>
|
Look up the owning tensor with common::find_tensor_from_offsets instead of a local binary search, and report per-tensor first dimensions that are not multiples of 128 through common::get_tensor_rows_num, once per tensor in the first block, as group_quantize_mxfp8.cuh does. The dense scale indexing depends on that alignment, which was only checked on the host for equal shapes. Signed-off-by: Harshith Kantamneni <hkantamneni2@wisc.edu>
Merge NVIDIA/TransformerEngine main at bba2d2b without rewriting the existing PR commits. Restore the is_nvfp4 declaration removed by the automatic merge with the scale_inv_dtype changes from NVIDIA#3325. Accept nvfp4_e4m3_max=0 as the E4M3 default as well as explicit 448, and cover both values in the grouped dequantization numerical test. Keep the existing compact E4M3 scale and FP32 amax requirements. Validation: compile the complete cast.cpp translation unit with CUDA 13.4 and PyTorch 2.13; run pre-commit on both changed paths, the C++ portion of L0_pytorch_lint, and L0_license. GPU numerical tests were not run: the local SM120 GPU cannot execute grouped NVFP4 quantization, which supports SM100-SM110. Signed-off-by: Przemek Tredak <ptredak@nvidia.com>
group_quantize with disable_second_level_scale=True produces a grouped NVFP4 tensor without an amax. Treat a missing amax as a unit global scale, as dequantize_nvfp4.cuh does, instead of rejecting the tensor. Cover it with a NO_AMAX mode in the C++ grouped test and with disable_second_level_scale in the PyTorch grouped round trip. Signed-off-by: Harshith Kantamneni <hkantamneni2@wisc.edu>
|
Thanks for merging main and restoring the The Greptile P1 on 8375386 was right: Your merge message mentions the GPU numerical tests could not run on SM120, so I ran them on a B200 (
To check that the new tests exercise the new path, I changed the default amax from 6 * 448 (global scale 1) to 1.0: exactly the 18 |
Signed-off-by: Harshith Kantamneni <hkantamneni2@wisc.edu>
Description
nvte_group_dequantize/tex.group_dequantizesupported MXFP8 and FP8 block scaling but not NVFP4. This adds NVFP4 for rowwise data with compact (non-swizzled) E4M3 scales, as #2726 asks, and fixes the PyTorch binding so it passes an NVFP4 grouped tensor's element count, amax and swizzle flag to the kernel.Fixes #2726
Type of change
Changes
common/cast/nvfp4/group_dequantize_nvfp4.cuh(new): one thread per 16-element block, with the same arithmetic asdequantize_nvfp4.cuh, so output matches per-tensor dequantization bitwise. The owning tensor comes fromrow / rows_per_tensor(equal shapes) orcommon::find_tensor_from_offsetsovertensor_offsets(varying first dimension, empty tensors included).tensor_offsetsis read on the device, so there is no device-to-host copy and the call can be captured in a CUDA graph.common/cast/dispatch/dequantize.cuh: routeNVTE_NVFP4_1D_SCALINGto the new kernel.common/include/transformer_engine/cast.h: document NVFP4 support and its requirements.pytorch/csrc/extensions/cast.cpp(group_dequantize), for NVFP4 only:CheckInputGroupedTensoraccepts it;amaxand_with_gemm_swizzled_scales, which were not set before;tests/cpp/operator/test_dequantize_nvfp4_grouped.cu(new, registered in CMake): grouped vs. per-tensornvte_dequantize, bitwise, over 6 shapes x 3 output types x shared/per-tensor amax.tests/pytorch/test_grouped_tensor.py: grouped vs. per-tensor dequantize fromtex.group_quantizeoutput (including an empty tensor), the swizzled-scale error, and CUDA graph capture.Scope
graph_safe_group_row_cast_col_hadamard_transform_cast_fusion.cu) requires. This makes the padded per-tensor scale layout one dense[rows, cols / 16]array. For equal shapes the per-tensor row count is checked on the host. With a varying first dimension the kernel reports a misaligned tensor throughcommon::get_tensor_rows_num, once per tensor in the first block asgroup_quantize_mxfp8.cuhdoes (NVTE_DEVICE_ERROR, so it stops only in debug builds).VARYING_LAST_DIMandVARYING_BOTH_DIMSare rejected, because the grouped NVFP4 quantizer on main requires a constant last dimension. The kernel is a plain one and does not depend on [Common] Group NVFP4 Quantize Kernels #3458.Testing
Both commits were tested on one B300 (SM103,
NVTE_CUDA_ARCHS=103a) at fb0fed6. The first commit was also tested on one B200 (SM100,100a) at 1770259, with the same results. torch 2.8.0+cu129, cuDNN 9.26, CUDA 12.9. Current main does not build with CUDA 12.8: ptxas rejects theld.global.nc.L2::evict_firstfallback inutil/ptx.cuh(from #3459) with "'.L1::eviction_priority' syntax expected", whiledocs/installation.rstlists 12.8+ for Blackwell.test_operator --gtest_filter='*GroupedDequantizeNVFP4*': 33 passed, 3 skipped (the one-tensor per-tensor-amax cases, which duplicate the shared-amax ones), 0 failed. The reference, per-tensornvte_dequantize, is itself checked against a CPU reference intest_dequantize_nvfp4.cu.--gtest_filter='*Dequantize*': 777 passed, 219 skipped (including the 144 grouped FP8-blockwise cases, which run on SM90 only), 0 failed.Row-count report: a grouped tensor with first dimensions 128 and 384 prints no message; one with 192 and 320 prints the
get_tensor_rows_nummessage exactly twice, once per tensor, and in this release build the call continues, as for MXFP8.New PyTorch tests: 8 passed.
NVTE_GROUPED_LINEAR_SINGLE_PARAM=1 pytest tests/pytorch/test_grouped_tensor.py: 167 passed, 71 skipped, 0 failed.Each planted bug below was a temporary edit, rebuilt and run, then restored; all new tests pass again afterwards.
row * cols - 1passed tofind_tensor_from_offsets)The pre-commit hooks (black 24.4.2, clang-format 18.1.6), cpplint 1.6.0 on the changed
transformer_engine/files, andqa/L0_licensepass. No new build warnings in the changed files.Not run: the full
qa/L0_cppunittestandqa/L0_pytorch_unittestlaunchers, only the subsets above.Notes for reviewers
group_dequantizelines incast.cppand changes two things this PR relies on: the defaultnvfp4_e4m3_maxbecomes 0 (the scale dtype's max), which thenvfp4_e4m3_max == 448check here would reject, anddisable_second_level_scaleallows NVFP4 without an amax, which this kernel rejects. Whichever lands second needs to handle both, and UE5M3 needs a scale-type template parameter in this kernel.cols / 16scales per data row) needs to follow it.tex.group_quantizefor NVFP4 needsfirst_dims. Without it the grouped tensor has notensor_offsets(build_grouped_tensor_offsetsreturns nullopt), and both graph-safe kernels readoffsets[num_tensors]without a check (graph_safe_group_hadamard_transform.cu:241,graph_safe_group_row_cast_col_hadamard_transform_cast_fusion.cu:224), which fails with an illegal memory access. So the PyTorch tests passfirst_dims, and the equal-shape path is covered by the C++ test.Checklist:
AI usage: I used an AI assistant (Claude Code) to write code, tests, the validation scripts and this description. I reviewed every change, directed the tests, and ran the B200 validation.