Skip to content

fix(quantization): nvfp4_quantize(backend='cuda') silently corrupts scale factors when global_scale is not float32 - #3497

Merged
bkryu merged 2 commits into
flashinfer-ai:mainfrom
bkryu:nvfp4_quant_cuda_fix
Jun 3, 2026
Merged

bkryu merged 2 commits into
flashinfer-ai:mainfrom
bkryu:nvfp4_quant_cuda_fix

Conversation

@bkryu

@bkryu bkryu commented Jun 2, 2026

Copy link
Copy Markdown
Collaborator

📌 Description

Summary

Fixes #3398. flashinfer.mm_fp4 / nvfp4_quantize (cuda backend) silently produced all-zero or magnitude-wrong outputs for certain batch sizes M when global scales are not provided as bfloat16. The root cause in the issue is that the CUDA quantize kernel reads the global scale as float32, but callers commonly pass a bfloat16 global scale — e.g. (448 * 6) / x.abs().max() inherits x's bf16 dtype. The kernel then misreads it byte-wise, and a dtype guard that would have caught this was commented out.

The PR fixes the issue by converting non-fp32 global scale factors to fp32 and adding checks. Also adds unit tests.

🔍 Related Issues

#3398

🚀 Pull Request Checklist

Thank you for contributing to FlashInfer! Before we review your pull request, please make sure the following items are complete.

✅ Pre-commit Checks

  • I have installed pre-commit by running pip install pre-commit (or used your preferred method).
  • I have installed the hooks with pre-commit install.
  • I have run the hooks manually with pre-commit run --all-files and fixed any reported issues.

If you are unsure about how to set up pre-commit, see the pre-commit documentation.

🧪 Tests

  • Tests have been added or updated as needed.
  • All tests are passing (unittest, etc.).

Reviewer Notes

Summary by CodeRabbit

  • Bug Fixes

    • FP4 quantization now validates and normalizes the global scale so kernels always receive a float32 scale on the correct device, preventing mis-scaling or all-zero outputs when scale is bfloat16/float16 or on a different device.
  • Tests

    • Added a regression test covering global-scale handling across bfloat16, float16, and float32.

@coderabbitai

coderabbitai Bot commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 93d21417-f97e-4f5d-b074-5e0b316ddeca

📥 Commits

Reviewing files that changed from the base of the PR and between 001b5cf and b227563.

📒 Files selected for processing (1)
  • flashinfer/quantization/fp4_quantization.py

📝 Walkthrough

Walkthrough

Normalize global_scale to float32 on the input device in Python, validate the kernel input type in C++, and add a CUDA regression test ensuring correct scaling for bfloat16/float16/float32 inputs.

Changes

FP4 Global Scale Dtype Fix

Layer / File(s) Summary
Python global_scale normalization
flashinfer/quantization/fp4_quantization.py
Move global_scale to input.device and cast to torch.float32 before dispatching to CUDA or cute-dsl backends.
Kernel runtime validation
csrc/nv_internal/tensorrt_llm/thop/fp4Quantize.cpp
Add runtime check that globalScale (when required) is a float32 scalar DLDataType{kDLFloat,32,1}.
CUDA regression test
tests/utils/test_fp4_quantize.py
Parametrized test verifies nvfp4_quantize interprets global_scale correctly for bfloat16, float16, and float32 by asserting non-zero scale-factor bytes and validating dequantization magnitude ratios.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested reviewers

  • yzh119
  • yongwww
  • nv-yunzheq
  • cyx-6
  • djmmoss
  • jimmyzho

Poem

🐰 I hopped across tensors in the night,
Found scales in halves and tiny light,
I nudged them to float32 on the way—
Now kernels read true at break of day. ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the bug fix: nvfp4_quantize corrupts scale factors when global_scale is not float32.
Description check ✅ Passed The PR description includes a detailed summary of the issue, root cause, and solution, with a reference to the related issue #3398. However, the test checklist item is marked incomplete.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@bkryu bkryu added the op: misc norm, activation, sampling, RoPE, quantization, etc. label Jun 2, 2026
@bkryu bkryu self-assigned this Jun 2, 2026

@gemini-code-assist gemini-code-assist 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.

Code Review

This pull request addresses issue #3398 by normalizing the global_scale tensor to float32 in the Python wrapper before passing it to the CUDA kernel, preventing byte-wise misinterpretation of bf16 or fp16 scales. It also adds a C++ type check for globalScale and introduces a regression test covering different scale dtypes. The reviewer noted that the normalization logic is bypassed for the cute-dsl backend and recommended moving it to the beginning of the function, while also ensuring that global_scale is cast to the correct device to prevent device mismatch errors.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +864 to +865
if global_scale is not None and global_scale.dtype != torch.float32:
global_scale = global_scale.to(torch.float32)

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.

medium

The normalization of global_scale to float32 is currently placed after the cute-dsl backend early return (line 842). This means if a user calls fp4_quantize with backend="cute-dsl" and a non-float32 global_scale, it will bypass this normalization.

Additionally, we should also ensure global_scale is on the same device as input (i.e., global_scale = global_scale.to(input.device)) to prevent device mismatch errors or host-to-device copy issues during kernel execution.

To address both issues, we should update this block to handle both device and dtype normalization, and ideally move it to the very beginning of the fp4_quantize function (e.g., right after the sf_vec_size check) so that both backends benefit from it.

    if global_scale is not None:
        if global_scale.device != input.device:
            global_scale = global_scale.to(input.device)
        if global_scale.dtype != torch.float32:
            global_scale = global_scale.to(torch.float32)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Good catches on both — agreed, fixed in the latest commit.

@coderabbitai coderabbitai 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.

🧹 Nitpick comments (2)
flashinfer/quantization/fp4_quantization.py (1)

842-850: ⚡ Quick win

Consider adding global_scale dtype normalization in the cute-dsl path for consistency.

The CUDA backend path normalizes global_scale to float32 at lines 862-865, but the cute-dsl dispatch in _fp4_quantize_cute_dsl doesn't apply the same conversion before invoking the cute-dsl kernel. While the cute-dsl kernel may handle non-float32 scales correctly, applying the same normalization here would ensure consistent behavior and prevent potential future issues.

♻️ Proposed fix
 def _fp4_quantize_cute_dsl(
     input: torch.Tensor,
     global_scale: Optional[torch.Tensor],
     sf_vec_size: int,
     sf_use_ue8m0: bool,
     is_sf_swizzled_layout: bool,
     is_sf_8x4_layout: bool,
     enable_pdl: Optional[bool],
 ) -> Tuple[torch.Tensor, torch.Tensor]:
     """CuTe-DSL dispatch for fp4_quantize. Maps parameters to the appropriate kernel."""
     from ..cute_dsl import is_cute_dsl_available
 
     if not is_cute_dsl_available():
         raise RuntimeError(
             "CuTe-DSL backend requested but CuTe-DSL is not available. "
             "Please install the required dependencies."
         )
+    
+    # Normalize global_scale dtype to match CUDA backend behavior
+    if global_scale is not None and global_scale.dtype != torch.float32:
+        global_scale = global_scale.to(torch.float32)
 
     if sf_vec_size == 16 and not sf_use_ue8m0:
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@flashinfer/quantization/fp4_quantization.py` around lines 842 - 850, The
cute-dsl dispatch path is missing normalization of the global_scale dtype;
before calling _fp4_quantize_cute_dsl convert/cast the global_scale tensor to
float32 (matching the CUDA branch's float32 normalization) so both backends
receive the same dtype; update the call site that currently passes global_scale
to first ensure global_scale = global_scale.to(torch.float32) (or equivalent)
and then pass it into _fp4_quantize_cute_dsl.
tests/utils/test_fp4_quantize.py (1)

1506-1560: 💤 Low value

Well-designed regression test with comprehensive failure-mode coverage.

The test correctly validates both symptoms of issue #3398:

  1. Scale factors becoming all-zero (line 1539-1542)
  2. Incorrect magnitude scaling via median ratio check (line 1554-1559)

The median-based magnitude validation is particularly good—it catches under-scaling that a direction-only (cosine similarity) check would miss.

Optional enhancement: Consider adding a test case that directly exercises fp4_quantize rather than going through nvfp4_quantize, since the fix is in fp4_quantize. This would more precisely test the layer where the fix lives and provide better isolation if the fix is refactored in the future.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/utils/test_fp4_quantize.py` around lines 1506 - 1560, Add a sibling
regression test that calls fp4_quantize directly (instead of nvfp4_quantize)
using the same parameterization (m and scale_dtype) and the same input setup (x
with dtype bfloat16, global_scale built in scale_dtype) found in
test_nvfp4_quantize_global_scale_dtype_regression; reproduce the two assertions:
(a) that the scale-factor bytes (from the sf output of fp4_quantize) are not all
zero and (b) that the dequantized magnitude (using
e2m1_and_ufp8sf_scale_to_float or the appropriate fp4 dequant helper) yields a
median |deq/x| between 0.5 and 2.0. Use fp4_quantize and its returned sf
layout/format exactly (refer to fp4_quantize, nvfp4_quantize, and
e2m1_and_ufp8sf_scale_to_float) so the test targets the fp4_quantize
implementation directly and mirrors the existing nvfp4 test logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@flashinfer/quantization/fp4_quantization.py`:
- Around line 842-850: The cute-dsl dispatch path is missing normalization of
the global_scale dtype; before calling _fp4_quantize_cute_dsl convert/cast the
global_scale tensor to float32 (matching the CUDA branch's float32
normalization) so both backends receive the same dtype; update the call site
that currently passes global_scale to first ensure global_scale =
global_scale.to(torch.float32) (or equivalent) and then pass it into
_fp4_quantize_cute_dsl.

In `@tests/utils/test_fp4_quantize.py`:
- Around line 1506-1560: Add a sibling regression test that calls fp4_quantize
directly (instead of nvfp4_quantize) using the same parameterization (m and
scale_dtype) and the same input setup (x with dtype bfloat16, global_scale built
in scale_dtype) found in test_nvfp4_quantize_global_scale_dtype_regression;
reproduce the two assertions: (a) that the scale-factor bytes (from the sf
output of fp4_quantize) are not all zero and (b) that the dequantized magnitude
(using e2m1_and_ufp8sf_scale_to_float or the appropriate fp4 dequant helper)
yields a median |deq/x| between 0.5 and 2.0. Use fp4_quantize and its returned
sf layout/format exactly (refer to fp4_quantize, nvfp4_quantize, and
e2m1_and_ufp8sf_scale_to_float) so the test targets the fp4_quantize
implementation directly and mirrors the existing nvfp4 test logic.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8741613f-8614-4813-8ea8-ba5f447a39cd

📥 Commits

Reviewing files that changed from the base of the PR and between 5f9135c and 001b5cf.

📒 Files selected for processing (3)
  • csrc/nv_internal/tensorrt_llm/thop/fp4Quantize.cpp
  • flashinfer/quantization/fp4_quantization.py
  • tests/utils/test_fp4_quantize.py

@bkryu

bkryu commented Jun 2, 2026

Copy link
Copy Markdown
Collaborator Author

/bot run

@flashinfer-bot

Copy link
Copy Markdown
Collaborator

GitLab MR !749 has been created, and the CI pipeline #53452715 is currently running. I'll report back once the pipeline job completes.

@bkryu
bkryu enabled auto-merge (squash) June 3, 2026 01:03
@bkryu
bkryu merged commit d8cb755 into flashinfer-ai:main Jun 3, 2026
33 checks passed
@bkryu
bkryu deleted the nvfp4_quant_cuda_fix branch June 8, 2026 17:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

op: misc norm, activation, sampling, RoPE, quantization, etc. run-ci v0.6.13

Projects

None yet

Development

Successfully merging this pull request may close these issues.

mm_fp4 NVFP4 returns erratic M-dependent zeros on SM120: both b12x and cutlass backends affected in FlashInfer 0.6.12

3 participants