Skip to content

[Perf][FlashAttention] FA4: pass no FP8 descale for a unit per-tensor scale - #63232

Merged
zyongye merged 1 commit into
mainfrom
minimax-m31-flash
Nov 2, 2026
Merged

zyongye merged 1 commit into
mainfrom
minimax-m31-flash

Conversation

@zyongye

@zyongye zyongye commented Oct 23, 2026 •

Copy link
Copy Markdown
Member

Overview

For an FP8 KV cache whose per-tensor scales are 1.0, FlashAttention now passes FA4 no descale at all. Before, it passed broadcast descale views that FA4 copied into contiguous tensors, three copy kernels per attention layer and step.

This PR began as the squash of the whole MiniMax-M3.1-Flash tree. That squash has since been split into the PRs below; this PR now holds only the FA4 change.

How #63232 was split

Part of the original squash Where it went State
MiniMax-M3.1 model and DSpark draft model, with their kernels (MSA NVFP4 KV cache and indexer, MegaMoE staging, ROCm MXFP8 linears, ...) #64111 merged
ROCm: NVFP4 MoE re-quantization to MXFP4/MXFP8, and fused shared experts through online quantization #64378 merged
FA4: no FP8 descale for a unit per-tensor scale this PR open
NIXL P/D: re-sharding NVFP4 KV pages across heterogeneous TP (AttentionSpec.kv_head_major_slots) #64475 open
Scheduler: keep KV cache zeroing for mixed-precision UniformTypeKVCacheSpecs groups #64472 open
DSpark/DFlash draft step: Markov bias in the Gumbel kernel, wider DFlash padding, is_padding from the positions kernel, top-p fix #64476 open
Batch-sharded DSpark draft sampling dropped from #64476; #63886 covers it open (#63886)
SimpleCPUOffload: one host buffer per rank with HugeTLB pages already open as #60130; the squash's extra ROCm change (SDMA copy-engine flag for batched copies) was pushed onto #60130 open (#60130)
Scheduler draft-slot change dropped: main's max_num_new_slots_for_drafting already returns 0 for DFlash/DSpark –
Sparse paged-attention KV block count fix (ROCm) dropped: already on main via #63270 –

The full squash as it was before the split is a0ea941 (on 5d35ce3), in this PR's force-push history.

PRs that were opened against this branch (minimax-m31-flash) while it held the squash: #64359, #64273, #64264, #64206, #63528, #63491 and #62509. Their diffs against this branch now include the whole old squash (e.g. #64359 shows 193 files). The model (#64111) and the ROCm re-quantization (#64378) are on main, so they can retarget main.

Claims

  • Unit scales: when FA4 runs with a per-tensor Q/K/V scale of exactly 1.0, FlashAttentionImpl passes None for that descale.
    • FA4 reads a missing descale as 1.0, so the output is bit-identical.
    • FA4's maybe_contiguous no longer copies the three broadcast descale views per layer and step.
  • Other scales: calibrated per-tensor scales and per-head scales keep their descale tensors. Other FlashAttention versions get the same broadcast descale as before; only the strides of its size-1 dims are made canonical, which changes metadata, not values.
  • (1, 1) descale views: a view with one sequence and one KV head per rank now gets canonical strides. FA4 treats such a view as contiguous and skips its copy, and its layout check then rejects the zero stride.

Validation

On GB300:

pytest tests/v1/attention/test_attention_backends.py

148 passed, 9 skipped, 4 failed. The 4 failures are test_sliding_window_encoder_backend_correctness (embeddinggemma-300m). They fail the same way on main (2abde62) in this environment.

The new test_flash_attn_fa4_unit_fp8_descale_is_exact (Blackwell only) checks that:

  • a unit scale gives no descale;
  • FA4's output with no descale equals its output with the broadcast unit descale, bit for bit;
  • a descale of 2 changes the output, so the comparison is not vacuous;
  • calibrated and per-head scales keep their descale tensors.

Not covered:

  • No end-to-end benchmark on this branch; the three copy kernels saved per layer and step were not timed.
  • No accuracy eval: the output is bit-identical by the test above.
  • The (1, 1) stride case is not in the new test, which uses 3 sequences and 4 KV heads.

Details

Credits: qizixi wrote the FA4 change and is a co-author in the commit. AI assistance (Claude) was used to split the original squash into the PRs above, to write the FA4 test and to draft this description. The submitter reviewed the change and ran the tests above.


Pull Request Checklist
  • I used vLLM's /pr-checklist skill. (Mandatory for agents, optional for humans).

  • AI assistance was used during the creation of this PR.

  • Design Fit: Minimizes impact on core components, reuses existing functionality, and justifies added complexity.

  • Testing and Validation: Validates the change and ensures any added tests are meaningful and reliable, with CI coverage or documented CI resource constraints and validation performed outside CI.

  • Code Quality and Style: Keeps code and comments clear and concise, and updates relevant documentation and examples.

  • Pull Request Contents: Includes a brief summary and relevant links, supports claims with evidence, explains root causes and implementation trade-offs, and follows the contributing guide.

🤖 Generated with Claude Code

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Claude Code Review

This repository is configured for manual code reviews. Comment @claude review for a one-time review, or @claude review always to subscribe this PR to a review on every future push.

Tip: disable this comment in your organization's Code Review settings.

RuibinCheung added a commit to RuibinCheung/vllm that referenced this pull request Oct 26, 2026
…ear for M <= 16

On gfx950, RocmDotScaledMxfp8LinearKernel quantized every activation with a
separate mxfp8_e4m3_quantize launch before the tl.dot_scaled GEMM. For bf16
activations with 1-16 rows, the GEMM now loads the bf16 tile and quantizes it
in registers with the same fp32 operations, so each linear is one launch and
the output is bit-identical.

Every N-tile requantizes its activation, so the in-kernel work grows with K
while the saved launch does not. With BLOCK_K >= 256 the kernel quantizes
only min(16, next_pow2(M)) rows and repeats them to fill the 16-row MFMA
tile (rows >= M are never stored); at BLOCK_K=128 repeating the rows costs
more than it saves. On MI355X this fuses K <= 1024 for M <= 16 (1.14-1.64x)
and K=2048 for M <= 8 (1.06-1.26x on the MiniMax-M3 TP=4 o_proj, 0.89x at
M=16); larger K keeps the separate quant. K becomes a constexpr, which the
fused loop needs to be profitable (the same change is in vllm-project#63232); each
power-of-2 row count is one more specialization, compiled during warmup.

fp16 is not fused, because the in-kernel fp8 cast enables the FP16 overflow
clamp for the whole kernel. VLLM_BATCH_INVARIANT keeps the unfused path.

Adds unit tests and benchmarks/kernels/benchmark_rocm_mxfp8_linear_fused_quant.py.

Signed-off-by: RuibinCheung <ruibzhan@amd.com>
RuibinCheung added a commit to RuibinCheung/vllm that referenced this pull request Oct 26, 2026
…ear for M <= 16

On gfx950, RocmDotScaledMxfp8LinearKernel quantized every activation with a
separate mxfp8_e4m3_quantize launch before the tl.dot_scaled GEMM. For bf16
activations with 1-16 rows, the GEMM now loads the bf16 tile and quantizes it
in registers with the same fp32 operations, so each linear is one launch and
the output is bit-identical.

Every N-tile requantizes its activation, so the in-kernel work grows with K
while the saved launch does not. With BLOCK_K >= 256 the kernel quantizes
only min(16, next_pow2(M)) rows and repeats them to fill the 16-row MFMA
tile (rows >= M are never stored); at BLOCK_K=128 repeating the rows costs
more than it saves. On MI355X this fuses K <= 1024 for M <= 16 (1.14-1.64x)
and K=2048 for M <= 4 (1.03-1.27x); past M=4 the K=2048 gain does not hold
for every N, and larger K keeps the separate quant. K becomes a constexpr,
which the fused loop needs to be profitable (the same change is in vllm-project#63232);
each power-of-2 row count is one more specialization, compiled during
warmup.

fp16 is not fused, because the in-kernel fp8 cast enables the FP16 overflow
clamp for the whole kernel. VLLM_BATCH_INVARIANT keeps the unfused path.

Adds unit tests and benchmarks/kernels/benchmark_rocm_mxfp8_linear_fused_quant.py.

Signed-off-by: RuibinCheung <ruibzhan@amd.com>
@fxmarty-amd

Copy link
Copy Markdown
Contributor

@zyongye when can we expect it to land?

@mergify

mergify Bot commented Oct 28, 2026

Copy link
Copy Markdown
Contributor

This pull request has merge conflicts that must be resolved before it can be
merged. Please rebase the PR, @zyongye.

https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork

@amd-dlimpus

Copy link
Copy Markdown

FYI @zyongye: #64264 is a small ROCm follow-up stacked on this branch. With NVFP4 KV on MI355X, the FlyDSL kernels compile lazily on first launch, and minimax_m3_msa_warmup returns early on ROCm. So the first C16 batch paid about 1.5 s extra of mean TTFT inside requests. #64264 prebuilds every variant at startup (about 12 s per rank). It doesn't touch the CUDA path.

… scale

FA4 reads a missing descale as 1.0, and it copies every expanded descale
view into a contiguous tensor: three copy kernels per layer and step. The
scales are fixed once the weights are loaded, so a per-tensor scale of 1.0
(an FP8 KV cache without calibrated scales) passes no descale. Calibrated
and per-head scales keep theirs; a (1, 1) expanded view gets canonical
strides, because FA4 treats it as contiguous and its layout check then
rejects the zero stride.

Co-authored-by: qizixi <22851944+zixi-qi@users.noreply.github.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Yongye Zhu <zyy1102000@gmail.com>
@zyongye

zyongye commented Oct 31, 2026

Copy link
Copy Markdown
Member Author

/ci run

@github-actions

Copy link
Copy Markdown

✅ Triggered Buildkite CI #98868 for commit 3e48944f896b.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci/build deepseek Related to DeepSeek models dflash DSv4.1 Related to DeepSeek-V4.1 models k3 kimi kv-cache-manager kv-connector minimax mrv2 Model Runner V2 specific multi-modality Related to multi-modality (#4194) nvidia performance Performance-related issues quantization qwen Related to Qwen models recipe rocm Related to AMD ROCm scheduler speculative-decoding tool-calling

Projects

Status: Done
Status: Done
Status: Done
Status: Unsorted queue

Development

Successfully merging this pull request may close these issues.

9 participants