Skip to content

feat(attention): asymmetric VO-split NVFP4 paged prefill (qk=512/vo=256) for Gemma-4 on SM120/121 - #3684

Merged
qsang-nv merged 25 commits into
flashinfer-ai:mainfrom
jethac:nvfp4-vosplit-rederive
Aug 13, 2026
Merged

qsang-nv merged 25 commits into
flashinfer-ai:mainfrom
jethac:nvfp4-vosplit-rederive

Conversation

@jethac

@jethac jethac commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Summary

Add an asymmetric VO-split NVFP4 paged-prefill path to the FA2 attention kernels so consumer/SoC Blackwell (SM120 / SM121) can serve models whose global-attention layers are 512-wide — specifically Gemma 4 — with a 4-bit (NVFP4) KV cache.

Upstream NVFP4 paged prefill is symmetric head_dim ≤ 256 only. Gemma 4's full-attention layers are head_dim_qk = 512, head_dim_vo = 256 (value reuses key), which hits three walls: the equal-K/V-stride host check, the VO-split static_assert(!is_fp4), and the output-width hardcode. This PR adds the FP4 V-load + explicit scale-factor strides + asymmetric K/V offsets so the kernel serves the asymmetric (qk=512, vo=256) shape directly, and keys CTA_TILE_Q on head_dim_qk so the dispatch is feasible at 512.

What's in it

  • NVFP4 KV-cache support for FA2 paged attention (SM120) — the FP4 V-load into the VO-split path, the [all-data | all-SF] contiguous scale-factor layout (the trtllm-gen per-page swizzle doesn't commute with head-dim slicing), and the asymmetric K/V byte/scale offsets.
  • SM121 (DGX Spark / GB10) dispatch + a heuristic regression test.
  • CTA_TILE_Q keyed on head_dim_qk for the asymmetric (QK=512, VO=256) case (the dispatcher otherwise picks an infeasible tile / max_mma_kv: 0).
  • plan(): move mask_indptr to the custom mask's device before segment_packbits — a standalone correctness fix (the paged + ragged wrappers passed a CPU mask_indptr).

Validation

  • Numerics (RTX 5090, SM120): qk=512 / vo=256 paged NVFP4 prefill max_abs_err ≈ 0.0047 (the FP4 e2m1 rounding floor itself is ~0.0156); output correctly sized by vo=256; symmetric qk=vo=256/128 cases unregressed; multi-tile seq 512/1024 + causal all sane.
  • End-to-end (via vLLM): the full Gemma 3 + Gemma 4 family serves a calibrated NVFP4 KV cache through this kernel on both SM120 (RTX 5090 / RTX PRO 6000) and SM121 (GB10) — facts match bf16. (vLLM side: companion PR linked below.)

Notes

  • The CC 12.0 / 12.1 findings reproduce identically on an RTX PRO 6000 and a GB10 — one fix serves the 5090, the PRO 6000, the Spark, and the upcoming RTX Spark.
  • Companion vLLM PR (serving orchestration + the two-pass VO split + built-in calibration) is forthcoming; I will cross-link it here once opened.
  • Happy to split the standalone mask_indptr device fix into its own PR if preferred.

NVFP4 split-KV correctness (added)

While validating this kernel under SGLang's radix cache (and as a heads-up for vLLM's
automatic prefix caching), I found a real correctness bug in the NVFP4 split-KV
path -- distinct from the VO-split work, and shared by every consumer of this kernel.

Symptom. With a 4-bit KV cache and prefix reuse on, retrieval cliffs: a Gemma-4
E2B/E4B needle is retrieved perfectly up to ~550 tokens of reused-prefix context,
then collapses (degenerate repetition / <pad>) from ~680 tokens on. bf16 KV has no
such cliff. The tell: --disable-radix-cache (any cold, contiguous prefill) makes it
vanish.

Root cause. Not page-table fragmentation, not calibration (both ruled out by an
instrumented A/B). It's the extend geometry -- a short query window attending a
long cached KV (qo_len << kv_len) -- which makes the FA2 scheduler pick
split-KV (flash-decoding). NVFP4 KV is packed uint8 with a per-16-element FP8
block scale; kv_chunk_size is not aligned to those blocks, so a split boundary can
land mid-block, and the small per-split chunk also trips the 1-byte-KV NUM_MMA_KV
tile floor. A position-aligned dense-cache trace shows the final query token's layer-0
attention output drift ~0.1-1% under split-KV vs the cold full-prefill for an
identical query -- small, but it compounds over ~30 layers until small models lose
the needle. A full-prefill (qo_len == kv_len) never splits, which is why dense-prefill
tests stay green while prefix caching / long-context decode break.

Fix (this push). Gate split-KV off when kv_data_type is NVFP4 (uint8 /
float4_e2m1fn_x2) in both the paged and ragged prefill plan(); FP8 / 16-bit KV are
untouched. ~5 lines, provably correct, at a small decode/short-query parallelism cost
for 4-bit KV only. Adds test_nvfp4_split_kv_gate_dtype_logic (hardware-free
dtype-classification guard).

Verified. Gemma-4 E2B/E4B radix-on retrieval holds to 1448 tokens (== bf16) with
the gate vs cliffing at ~600 without it -- on both sm120 (RTX PRO 6000) and
sm121 (GB10 / DGX Spark). The full Gemma-4 multimodal matrix (text/image/audio,
nvfp4 vs bf16) is green across E2B/E4B/12B/26B-A4B/31B with radix caching on.

Follow-up. A perf-preserving version keeps flash-decoding by making FP4 split points
16-token-aligned (respecting the scale blocks) and clearing the NUM_MMA_KV floor per
chunk -- left as a follow-up so the correctness fix can land now.

Summary by CodeRabbit

Release Notes

  • New Features
    • Improved paged/prefill NVFP4 KV support with separate K/V stride handling and NVFP4 scale-factor stride wiring, including generated JIT parameter setup.
    • Enhanced FP4 GEMM auto-selection for SM12x (e.g., SM120/SM121) to prefer b12x first.
  • Bug Fixes
    • Fixed NVFP4 decode/prefill output sizing and KV stride/softmax-factor loading consistency.
    • Prevented uninitialized lse values in NVFP4 SM120 attention and corrected KV attention tiling logic to use Q/K dimensions.
  • Documentation
    • Updated source-install guidance and CUDA-arch examples (including 12.1a).
  • Tests
    • Added coverage for SM121 NVFP4 backend selection and JIT attention stride/SF generation.

Build-size note (per review)

DISPATCH_GQA_GROUP_SIZE gains a group_size == 6 case (needed for Gemma-4-class GQA ratios), which adds one instantiation per macro user - roughly +20% compile time/binary size for the translation units that expand this macro.

@coderabbitai

coderabbitai Bot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR updates NVFP4 KV-cache handling, attention JIT generation, CTA tile selection, Python wrapper logic, and SM12x build/runtime messages. It separates K/V strides, threads explicit SF strides through prefill paths, adjusts output sizing, and updates SM121-related architecture and error text.

Changes

NVFP4 KV-cache and SM12x support

Layer / File(s) Summary
Paged KV strides and packed FP4 types
include/flashinfer/page.cuh, include/flashinfer/vec_dtypes.cuh, csrc/batch_decode.cu, csrc/batch_prefill.cu, csrc/batch_prefill_customize_config.jinja
paged_kv_t now tracks separate V-cache stride fields and accepts distinct K/V stride arrays. FP4 vector helpers add packed __nv_fp4x2_e2m1 cast and storage support, decode/prefill entry points pass separate K/V strides, and the FP4 prefill config template adds compile-time KV-cache checks.
Explicit SF stride loading through attention kernels
include/flashinfer/attention/prefill.cuh, include/flashinfer/attention/persistent.cuh
Prefill kernels now take explicit SF stride parameters instead of deriving them from KV strides. The paged SF loader adds an optional de-swizzle path, and single, ragged, paged, and persistent attention paths thread separate K/V SF stride values and split local K/V offset scratch space.
CTA tile selection and dispatch guards
include/flashinfer/utils.cuh, include/flashinfer/attention/scheduler.cuh, include/flashinfer/attention/prefill.cuh, csrc/batch_prefill_paged_kernel_inst.jinja, csrc/batch_prefill_ragged_kernel_inst.jinja
FA2DetermineCtaTileQ now accepts head_dim_qk, adds a qk >= 512 path, and checks shared-memory feasibility for small packed-QO cases. Dispatch sites add FA2_REJECT_IF_KV_SMEM_INSUFFICIENT(), PrefillSplitQOKVIndptr forwards head_dim_qk, KernelTraits::IsInvalid uses HEAD_DIM_QK, DISPATCH_GQA_GROUP_SIZE supports group size 6, and the Jinja kernel templates select cta_tile_q variants from head_dim_qk.
JIT code generation and Python runtime wiring
flashinfer/jit/utils.py, flashinfer/jit/attention/utils.py, flashinfer/jit/attention/modules.py, flashinfer/prefill.py, flashinfer/decode.py, tests/jit/test_attention_utils.py, tests/attention/test_nvfp4_attention_sm120.py
JIT URI generation now uses a KV-specific dtype naming helper. Attention JIT utilities add SF-stride tensor detection and setter generation, and module rendering validates required NVFP4 SF tensors while adding extra NVCC flags when needed. Python decode/prefill paths compute NVFP4 output widths from packed V shapes, adjust JIT argument preparation, move custom-mask packing to the mask device, and tests cover the generated code and split-KV gating helper.
SM12x platform support and build targets
flashinfer/gemm/gemm_base.py, flashinfer/mla/_core.py, flashinfer/xqa.py, flashinfer/cute_dsl/utils.py, .github/workflows/release.yml, .github/workflows/nightly-release.yml, docs/installation.rst, tests/gemm/test_mm_fp4.py
mm_fp4 backend selection now prefers b12x, then cutlass, then cudnn on SM12x with CUDA 13+. SM121a minimum CUDA messages in MLA and XQA are updated, the NVFP4 assertion text references SM12x, cute_dsl adds a compatibility shim, release/nightly workflows and installation docs update SM121 arch guidance, and a test covers the SM121 backend ordering.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Suggested reviewers

  • yzh119
  • nv-yunzheq
  • sricketts
  • aleozlx
  • yongwww
  • cyx-6
  • yyihuang
  • kahyunnam
  • jimmyzho
  • samuellees
  • qsang-nv
  • bkryu

Poem

🐇 I hopped through K and V with care,
Split strides now dance through kernel air.
SM12x shines, the tiles align,
And FP4 bytes pack up just fine.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.48% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the asymmetric NVFP4 paged-prefill change and targets the main affected attention path.
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.
Description check ✅ Passed The PR description is detailed and covers summary, validation, notes, and follow-up context, though it omits the template’s issue/checklist sections.
✨ 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.

@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 introduces support for independent K and V cache strides, asymmetric QK/VO head dimension configurations, and explicit scale-factor strides for NVFP4 KV cache in FlashInfer's attention kernels and JIT compilation modules. It also updates the FP4 GEMM heuristics to support SM12x (including GB10) and adds shared memory validation guards to prevent cryptic launch failures. Feedback on the code changes highlights a critical bug in flashinfer/cute_dsl/utils.py where the cute module is referenced without being imported, which will lead to a NameError at runtime.

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 thread flashinfer/cute_dsl/utils.py Outdated
Comment on lines +31 to +35
if not hasattr(cute.nvgpu, "OperandMajorMode"):
try:
cute.nvgpu.OperandMajorMode = cute.nvgpu.tcgen05.OperandMajorMode
except AttributeError:
pass

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.

high

The cute module is referenced here but it is not imported in this file. This will raise a NameError when the module is loaded. Please import cute from cutlass.

Suggested change
if not hasattr(cute.nvgpu, "OperandMajorMode"):
try:
cute.nvgpu.OperandMajorMode = cute.nvgpu.tcgen05.OperandMajorMode
except AttributeError:
pass
import cutlass.cute as cute
if not hasattr(cute.nvgpu, "OperandMajorMode"):
try:
cute.nvgpu.OperandMajorMode = cute.nvgpu.tcgen05.OperandMajorMode
except AttributeError:
pass

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
include/flashinfer/attention/prefill.cuh (3)

657-672: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Initialize RoPE frequencies over HEAD_DIM_QK, not HEAD_DIM_VO.

Asymmetric dispatch makes NUM_MMA_D_QK != NUM_MMA_D_VO reachable. q_smem_inplace_apply_rotary / k_smem_inplace_apply_rotary index rope_freq up to NUM_MMA_D_QK / 2, but this initializer only fills NUM_MMA_D_VO / 2; for qk=512, vo=256 half the table is uninitialized.

🐛 Proposed fix
-  for (uint32_t mma_d = 0; mma_d < KTraits::NUM_MMA_D_VO / 2; ++mma_d) {
+  for (uint32_t mma_d = 0; mma_d < KTraits::NUM_MMA_D_QK / 2; ++mma_d) {
🤖 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 `@include/flashinfer/attention/prefill.cuh` around lines 657 - 672, The outer
for loop in the init_rope_freq function is using KTraits::NUM_MMA_D_VO as the
loop bound instead of KTraits::NUM_MMA_D_QK. This causes the rope_freq array to
be only partially initialized when NUM_MMA_D_QK is larger than NUM_MMA_D_VO,
since the calling functions q_smem_inplace_apply_rotary and
k_smem_inplace_apply_rotary index the rope_freq array up to NUM_MMA_D_QK / 2.
Change the loop condition from mma_d < KTraits::NUM_MMA_D_VO / 2 to mma_d <
KTraits::NUM_MMA_D_QK / 2 to ensure the entire rope_freq table is properly
initialized.

542-588: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Guard rounded-up SF loader lanes before writing shared memory.

NUM_SF_ITERS is rounded up, so threads with flat_byte >= SF_TOTAL_BYTES exist. In the FLASHINFER_PAGED_V_SF_DESWIZZLE branch, those lanes still execute *(uint32_t*)(sf_smem + flat_byte) = packed, which writes past v_sf_smem.

🛡️ Proposed fix
     const uint32_t flat_uint32_idx = thread_id + k * THREADS_PER_CTA;
     const uint32_t flat_byte = flat_uint32_idx * 4;
+    if (flat_byte >= SF_TOTAL_BYTES) {
+      continue;
+    }
@@
-    const bool in_bounds = (flat_byte < SF_TOTAL_BYTES) && (kv_idx_base + sf_smem_row < kv_len);
+    const bool in_bounds = (kv_idx_base + sf_smem_row < kv_len);
🤖 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 `@include/flashinfer/attention/prefill.cuh` around lines 542 - 588, In the
FLASHINFER_PAGED_V_SF_DESWIZZLE branch of the code, the statement that writes
packed data to shared memory `*reinterpret_cast<uint32_t*>(sf_smem + flat_byte)
= packed;` is executed unconditionally for all threads, even those where
in_bounds is false. This causes out-of-bounds writes past the v_sf_smem buffer
for threads with flat_byte >= SF_TOTAL_BYTES. Guard this write operation with
the in_bounds condition by wrapping the assignment in an `if (in_bounds)` check
to prevent threads outside the valid bounds from writing to shared memory.

129-136: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Include FP4 SF smem in the NUM_MMA_KV budget.

SharedStorageQKVO adds k_sf_smem/v_sf_smem proportional to CTA_TILE_KV, but the dispatch budgets only count KV data/repack/VO-split terms. On tight-smem GPUs this can select a larger NUM_MMA_KV that fails the final sizeof(SharedStorage*) check, even though a smaller KV tile would fit.

🛠️ Suggested adjustment
+  constexpr uint32_t kFP4SfSmemPerMmaKV =
+      is_fp4_type_v<DTypeKV> ? (NUM_WARPS_KV * (HEAD_DIM_QK + HEAD_DIM_VO)) : 0u;
   constexpr uint32_t kKVSmemPerMmaKV =
       (kKVShared ? (HEAD_DIM_QK * 16 * NUM_WARPS_KV * sizeof(DTypeKV))
                  : ((HEAD_DIM_QK + HEAD_DIM_VO) * 16 * NUM_WARPS_KV * sizeof(DTypeKV))) +
+      kFP4SfSmemPerMmaKV +
       (kUseRepack ? ((HEAD_DIM_QK > HEAD_DIM_VO ? HEAD_DIM_QK : HEAD_DIM_VO) * 16 * NUM_WARPS_KV *
                      sizeof(DTypeQ))
                   : 0u);

Apply the same term in the single, ragged, and paged dispatch budget blocks.

Also applies to: 2099-2104, 3516-3521, 3704-3710

🤖 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 `@include/flashinfer/attention/prefill.cuh` around lines 129 - 136, The
dispatch budget calculations for single, ragged, and paged dispatch blocks do
not account for the FP4 scale factor shared memory (k_sf_smem and v_sf_smem)
that are added to SharedStorageQKVO when DTypeKV is an FP4 type. This causes the
dispatch logic to select a NUM_MMA_KV value that may exceed actual shared memory
capacity. Add the FP4 SF memory terms (proportional to CTA_TILE_KV multiplied by
the scale factor dimensions divided by NVFP4_SF_VEC_SIZE) to the shared memory
budget calculations in all three dispatch budget blocks wherever NUM_MMA_KV is
constrained by the total SharedStorage size.
🤖 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.

Inline comments:
In `@flashinfer/decode.py`:
- Around line 1555-1561: The `out_head_dim` calculation for NVFP4 at lines
1555-1561 incorrectly determines whether to double the dimension based only on
whether `kv_cache_sf` is not None, rather than checking the actual dtype of the
KV cache. Modify the logic to derive `out_head_dim` from the KV cache dtype
(specifically checking if it is uint8) instead of relying on the presence of
`kv_cache_sf`. This ensures the output width is correctly computed based on the
actual data type and prevents miscomputation if `kv_cache_sf` is accidentally
provided for non-uint8 KV caches, maintaining consistency with the validation
logic.

In `@flashinfer/jit/attention/modules.py`:
- Around line 1915-1917: The gen_customize_batch_attention_module function at
line 1915 is missing the FP4 guard/flag pattern that exists in
gen_customize_batch_prefill_module (lines 1572-1681). Apply the same FP4 compile
gating logic from the batch-prefill module to the batch-attention module to
ensure that FP4-typed code generation is properly protected by FP4 compile
flags, making both paths consistent in how they handle FP4 customization.

In `@include/flashinfer/attention/prefill.cuh`:
- Around line 204-207: The CTA_TILE_Q pruning logic in the IsInvalid() method on
line 207 is inconsistent with FA2DetermineCtaTileQ's dispatcher logic. Update
the condition to check both HEAD_DIM_QK and HEAD_DIM_VO together (not just
HEAD_DIM_QK alone) to handle three distinct cases: true VO-split cases where
HEAD_DIM_VO >= 512, asymmetric cases where HEAD_DIM_QK >= 512 but HEAD_DIM_VO <=
256, and cases where HEAD_DIM_VO >= 512 but HEAD_DIM_QK < 512. Reorder the
pruning conditions to mirror the exact branch order used in FA2DetermineCtaTileQ
to ensure the dispatcher's selections are never incorrectly rejected by this
validity check.

---

Outside diff comments:
In `@include/flashinfer/attention/prefill.cuh`:
- Around line 657-672: The outer for loop in the init_rope_freq function is
using KTraits::NUM_MMA_D_VO as the loop bound instead of KTraits::NUM_MMA_D_QK.
This causes the rope_freq array to be only partially initialized when
NUM_MMA_D_QK is larger than NUM_MMA_D_VO, since the calling functions
q_smem_inplace_apply_rotary and k_smem_inplace_apply_rotary index the rope_freq
array up to NUM_MMA_D_QK / 2. Change the loop condition from mma_d <
KTraits::NUM_MMA_D_VO / 2 to mma_d < KTraits::NUM_MMA_D_QK / 2 to ensure the
entire rope_freq table is properly initialized.
- Around line 542-588: In the FLASHINFER_PAGED_V_SF_DESWIZZLE branch of the
code, the statement that writes packed data to shared memory
`*reinterpret_cast<uint32_t*>(sf_smem + flat_byte) = packed;` is executed
unconditionally for all threads, even those where in_bounds is false. This
causes out-of-bounds writes past the v_sf_smem buffer for threads with flat_byte
>= SF_TOTAL_BYTES. Guard this write operation with the in_bounds condition by
wrapping the assignment in an `if (in_bounds)` check to prevent threads outside
the valid bounds from writing to shared memory.
- Around line 129-136: The dispatch budget calculations for single, ragged, and
paged dispatch blocks do not account for the FP4 scale factor shared memory
(k_sf_smem and v_sf_smem) that are added to SharedStorageQKVO when DTypeKV is an
FP4 type. This causes the dispatch logic to select a NUM_MMA_KV value that may
exceed actual shared memory capacity. Add the FP4 SF memory terms (proportional
to CTA_TILE_KV multiplied by the scale factor dimensions divided by
NVFP4_SF_VEC_SIZE) to the shared memory budget calculations in all three
dispatch budget blocks wherever NUM_MMA_KV is constrained by the total
SharedStorage size.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 97e44c80-6c09-4648-b2fb-a58e620e2f3f

📥 Commits

Reviewing files that changed from the base of the PR and between 9c5ed7c and 0ee50ba.

📒 Files selected for processing (25)
  • .github/workflows/nightly-release.yml
  • .github/workflows/release.yml
  • csrc/batch_decode.cu
  • csrc/batch_prefill.cu
  • csrc/batch_prefill_customize_config.jinja
  • csrc/batch_prefill_paged_kernel_inst.jinja
  • csrc/batch_prefill_ragged_kernel_inst.jinja
  • docs/installation.rst
  • flashinfer/cute_dsl/utils.py
  • flashinfer/decode.py
  • flashinfer/gemm/gemm_base.py
  • flashinfer/jit/attention/modules.py
  • flashinfer/jit/attention/utils.py
  • flashinfer/jit/utils.py
  • flashinfer/mla/_core.py
  • flashinfer/prefill.py
  • flashinfer/xqa.py
  • include/flashinfer/attention/persistent.cuh
  • include/flashinfer/attention/prefill.cuh
  • include/flashinfer/attention/scheduler.cuh
  • include/flashinfer/page.cuh
  • include/flashinfer/utils.cuh
  • include/flashinfer/vec_dtypes.cuh
  • tests/gemm/test_mm_fp4.py
  • tests/jit/test_attention_utils.py

Comment thread flashinfer/decode.py
Comment thread flashinfer/jit/attention/modules.py Outdated
Comment on lines +1915 to +1917
+ generate_sf_stride_setter_lines(
get_sf_stride_tensor_names(additional_tensor_names), prefix="params[i]."
)

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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Mirror FP4 compile gating in batch-attention JIT path.

Line [1915] adds NVFP4 SF-stride wiring for gen_customize_batch_attention_module, but this generator still lacks the FP4 guard/flag pattern used in gen_customize_batch_prefill_module (Line [1572]-Line [1681]). That leaves an inconsistent path where FP4 KV customization can emit FP4-typed code without enabling FP4 compile flags.

Suggested fix
 def gen_customize_batch_attention_module(
@@
 ):
+    require_fp4_kv_cache = dtype_map_kv[dtype_kv] == "__nv_fp4x2_e2m1"
+    if require_fp4_kv_cache:
+        missing_sf_tensors = [
+            name
+            for name in ("maybe_k_cache_sf", "maybe_v_cache_sf")
+            if name not in additional_tensor_names
+        ]
+        if missing_sf_tensors:
+            raise ValueError(
+                "NVFP4 KV batch-attention JIT modules require scale-factor tensors "
+                f"{missing_sf_tensors}; pass maybe_k_cache_sf and maybe_v_cache_sf "
+                "as additional tensors."
+            )
@@
-    return gen_jit_spec(
+    extra_cuda_cflags = ["-DFLASHINFER_ENABLE_PROFILER"] if use_profiler else []
+    if require_fp4_kv_cache:
+        extra_cuda_cflags += common_nvcc_flags
+
+    return gen_jit_spec(
         uri,
         source_paths,
-        extra_cuda_cflags=["-DFLASHINFER_ENABLE_PROFILER"] if use_profiler else [],
+        extra_cuda_cflags=extra_cuda_cflags,
     )
🤖 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/jit/attention/modules.py` around lines 1915 - 1917, The
gen_customize_batch_attention_module function at line 1915 is missing the FP4
guard/flag pattern that exists in gen_customize_batch_prefill_module (lines
1572-1681). Apply the same FP4 compile gating logic from the batch-prefill
module to the batch-attention module to ensure that FP4-typed code generation is
properly protected by FP4 compile flags, making both paths consistent in how
they handle FP4 customization.

Comment on lines 204 to +207
static constexpr bool IsInvalid() {
// The first clause prunes (CTA_TILE_Q, head_dim) pairs FA2DetermineCtaTileQ
// never selects: {16, 32} for head_dim_vo >= 512, {16, 64, 128} otherwise.
return ((HEAD_DIM_VO >= 512 ? (CTA_TILE_Q > 32) : (CTA_TILE_Q == 32)) || (NUM_MMA_D_VO < 4) ||
// never selects: {16, 32} for head_dim_qk >= 512, {16, 64, 128} otherwise.
return ((HEAD_DIM_QK >= 512 ? (CTA_TILE_Q > 32) : (CTA_TILE_Q == 32)) || (NUM_MMA_D_VO < 4) ||

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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Keep CTA pruning consistent with VO-vs-QK tile selection.

Line 207 allows CTA_TILE_Q == 32 whenever HEAD_DIM_QK >= 512, but FA2DetermineCtaTileQ only allows 32 for true HEAD_DIM_VO >= 512 VO-split cases and forces 16 for asymmetric QK >= 512, VO <= 256. It also rejects CTA_TILE_Q == 32 for VO >= 512, QK < 512, even though the dispatcher can select it. Mirror the dispatcher’s branch order here.

🐛 Proposed fix
-    return ((HEAD_DIM_QK >= 512 ? (CTA_TILE_Q > 32) : (CTA_TILE_Q == 32)) || (NUM_MMA_D_VO < 4) ||
+    constexpr bool invalid_cta_tile_q =
+        (HEAD_DIM_VO >= 512)
+            ? (CTA_TILE_Q > 32)
+            : (HEAD_DIM_QK >= 512 ? (CTA_TILE_Q != 16) : (CTA_TILE_Q == 32));
+    return (invalid_cta_tile_q || (NUM_MMA_D_VO < 4) ||
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
static constexpr bool IsInvalid() {
// The first clause prunes (CTA_TILE_Q, head_dim) pairs FA2DetermineCtaTileQ
// never selects: {16, 32} for head_dim_vo >= 512, {16, 64, 128} otherwise.
return ((HEAD_DIM_VO >= 512 ? (CTA_TILE_Q > 32) : (CTA_TILE_Q == 32)) || (NUM_MMA_D_VO < 4) ||
// never selects: {16, 32} for head_dim_qk >= 512, {16, 64, 128} otherwise.
return ((HEAD_DIM_QK >= 512 ? (CTA_TILE_Q > 32) : (CTA_TILE_Q == 32)) || (NUM_MMA_D_VO < 4) ||
static constexpr bool IsInvalid() {
// The first clause prunes (CTA_TILE_Q, head_dim) pairs FA2DetermineCtaTileQ
// never selects: {16, 32} for head_dim_qk >= 512, {16, 64, 128} otherwise.
constexpr bool invalid_cta_tile_q =
(HEAD_DIM_VO >= 512)
? (CTA_TILE_Q > 32)
: (HEAD_DIM_QK >= 512 ? (CTA_TILE_Q != 16) : (CTA_TILE_Q == 32));
return (invalid_cta_tile_q || (NUM_MMA_D_VO < 4) ||
🤖 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 `@include/flashinfer/attention/prefill.cuh` around lines 204 - 207, The
CTA_TILE_Q pruning logic in the IsInvalid() method on line 207 is inconsistent
with FA2DetermineCtaTileQ's dispatcher logic. Update the condition to check both
HEAD_DIM_QK and HEAD_DIM_VO together (not just HEAD_DIM_QK alone) to handle
three distinct cases: true VO-split cases where HEAD_DIM_VO >= 512, asymmetric
cases where HEAD_DIM_QK >= 512 but HEAD_DIM_VO <= 256, and cases where
HEAD_DIM_VO >= 512 but HEAD_DIM_QK < 512. Reorder the pruning conditions to
mirror the exact branch order used in FA2DetermineCtaTileQ to ensure the
dispatcher's selections are never incorrectly rejected by this validity check.

@jethac

jethac commented Jun 22, 2026

Copy link
Copy Markdown
Contributor Author

A heads-up for reviewers before you spend time here:

I'm extending this work to add multimodal support for Gemma 3/4, which I erroneously omitted from the initial version. Gemma 3/4 are natively multimodal, and serving image (and, for Gemma 4, audio) inputs through the NVFP4 VO-split prefill requires the model's span-level bidirectional image masking to flow through the kernel's custom-mask path. Without it the contribution only covers text prompts, which isn't a complete Gemma 3/4 claim.

I'm validating the multimodal path now and will push the update shortly. I'd appreciate holding final review until it lands — apologies for the churn.

@jethac

jethac commented Jun 22, 2026

Copy link
Copy Markdown
Contributor Author

Update — the multimodal support has landed, on the vLLM side: vllm-project/vllm#46329 (commit "Gemma 3/4 multimodal: NVFP4 KV mm-prefix span masking on FA2"). It serves image-token spans bidirectionally via FlashInfer's existing FA2 packed custom_mask path, and the only FlashInfer-side requirement — moving mask_indptr onto the custom mask's device when the mask is on GPU while the indptr arrays are on CPU — is already included in this PR. So #3684 needs no further changes for multimodal.

Validated reading images back under NVFP4 KV, matching bf16: sm120 Gemma-3-4B + Gemma-4-31B (VO split), sm121/GB10 Gemma-3-4B + Gemma-4-E4B (VO split). Thanks for the patience on the churn.

jethac added a commit to jethac/flashinfer that referenced this pull request Jun 23, 2026
Address review on flashinfer-ai#3684: the paged-decode output allocation doubled
out_head_dim whenever kv_cache_sf was non-None. Gate it on the KV cache
actually being uint8-packed (the NVFP4 layout that stores VO at half
width), so a stray scale-factor tensor on a non-uint8 cache fails the
shape check instead of silently allocating a mis-sized output.
@jethac

jethac commented Jun 23, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the reviews. Pushed one fix (7f549fe) and notes on the rest:

  • flashinfer/decode.py (out_head_dim) — fixed: the output-width doubling is now gated on v_cache.dtype == torch.uint8, not just kv_cache_sf is not None, so a stray scale-factor tensor on a non-uint8 cache fails the shape check instead of silently mis-allocating.
  • flashinfer/cute_dsl/utils.py (cute import)cutlass.cute is already imported at the top of the file (import cutlass.cute as cute, line 25); this PR only adds the OperandMajorMode shim beneath it, so there's no missing import. No change needed.
  • flashinfer/jit/attention/modules.py (FP4 gating in gen_customize_batch_attention_module) — this PR routes NVFP4 KV through gen_customize_batch_prefill_module, which carries the require_fp4_kv_cache guard; it does not add FP4 wiring to the batch-attention generator, so that path isn't FP4-reachable from this change. Happy to add the same guard there as a separate hardening if you'd prefer.
  • include/flashinfer/attention/prefill.cuh (CTA pruning, QK vs VO) — intentional for the asymmetric VO-split: FA2DetermineCtaTileQ selects CTA_TILE_Q from head_dim_qk, so the first prune clause keys on QK while the MMA/smem clauses key on VO. Verified on sm120 at qk=512/vo=256.

@jethac

jethac commented Jun 25, 2026

Copy link
Copy Markdown
Contributor Author

Pushed a follow-up commit (8984eb3): disable split-KV for NVFP4 KV in the prefill plan().

Found this validating the kernel under prefix caching. NVFP4 split-KV (flash-decoding) corrupts reads when a short query attends a long cached KV (qo_len << kv_len): a split boundary lands mid 16-element scale block, and the small per-split chunk also trips the 1-byte-KV NUM_MMA_KV floor. It's invisible to dense full-prefill tests (which never split) but cliffs prefix-cached retrieval / long-context decode -- Gemma-4 E2B/E4B drop the needle past ~600 tokens of reused context while bf16 holds. Gating split-KV off for FP4 kv_data_type (uint8 / float4_e2m1fn_x2) restores parity out to 1448 tokens, validated on sm120 (RTX PRO 6000) and sm121 (GB10 / DGX Spark). FP8 / 16-bit KV are untouched.

Worth flagging for the companion vLLM PR and anyone else consuming this kernel: since the fix lives in plan(), every consumer inherits it -- but without it, any engine running NVFP4 KV with prefix caching shares this bug. Full root-cause + the position-aligned dense-cache trace numbers are in the PR description under "NVFP4 split-KV correctness". The perf-preserving version (scale-block-aligned FP4 splits) is left as a follow-up so the correctness fix can land now.

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
flashinfer/prefill.py (2)

1372-1374: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Gate NVFP4 output-width doubling on the packed KV dtype, not kv_cache_sf.

A stray kv_cache_sf with non-uint8 KV still doubles the output width in prefill paths, silently allocating/checking the wrong shape. Match the decode-side fix by using the V-cache dtype as the packed-NVFP4 signal.

Suggested shape guard
- out_head_dim = (
-     v_cache.shape[-1] * 2 if kv_cache_sf is not None else v_cache.shape[-1]
- )
+ out_head_dim = (
+     v_cache.shape[-1] * 2 if v_cache.dtype == torch.uint8 else v_cache.shape[-1]
+ )
- out_head_dim = v.shape[-1] * 2 if kv_cache_sf is not None else v.shape[-1]
+ out_head_dim = v.shape[-1] * 2 if v.dtype == torch.uint8 else v.shape[-1]

Also applies to: 2457-2459, 3511-3513

🤖 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/prefill.py` around lines 1372 - 1374, The NVFP4 output-width logic
in the prefill path is gated on kv_cache_sf, which can incorrectly double the
shape even when the KV cache is not packed. Update the out_head_dim calculation
in the prefill code paths (and the matching decode-side logic if needed) to use
the V-cache dtype, specifically checking whether v is packed NVFP4/uint8, so
output width doubling only happens for packed KV storage and not for unrelated
kv_cache_sf presence.

1975-1981: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Mirror this device fix in the ragged custom-mask path.

This paged path now satisfies segment_packbits’ device requirement, but the ragged plan() path still passes CPU-derived mask_indptr directly when custom_mask is on GPU, leaving the same crash for ragged multimodal/custom-mask callers.

Suggested matching fix for the ragged path
 packed_custom_mask, mask_indptr = segment_packbits(
     custom_mask.contiguous().view(-1),
-    mask_indptr,
+    mask_indptr.to(custom_mask.device),
     bitorder="little",
 )
🤖 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/prefill.py` around lines 1975 - 1981, The ragged custom-mask path
in plan() still passes mask_indptr from the CPU side directly into
segment_packbits, which can violate the device requirement when custom_mask is
on GPU. Mirror the same fix used in the paged path: in the custom-mask handling
inside plan(), ensure mask_indptr is moved to custom_mask.device before calling
segment_packbits, so the ragged multimodal/custom-mask flow matches the device
expectations.
🤖 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.

Outside diff comments:
In `@flashinfer/prefill.py`:
- Around line 1372-1374: The NVFP4 output-width logic in the prefill path is
gated on kv_cache_sf, which can incorrectly double the shape even when the KV
cache is not packed. Update the out_head_dim calculation in the prefill code
paths (and the matching decode-side logic if needed) to use the V-cache dtype,
specifically checking whether v is packed NVFP4/uint8, so output width doubling
only happens for packed KV storage and not for unrelated kv_cache_sf presence.
- Around line 1975-1981: The ragged custom-mask path in plan() still passes
mask_indptr from the CPU side directly into segment_packbits, which can violate
the device requirement when custom_mask is on GPU. Mirror the same fix used in
the paged path: in the custom-mask handling inside plan(), ensure mask_indptr is
moved to custom_mask.device before calling segment_packbits, so the ragged
multimodal/custom-mask flow matches the device expectations.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: cb547752-7c96-4605-b9a6-9f1924cc0001

📥 Commits

Reviewing files that changed from the base of the PR and between 7f549fe and 8984eb3.

📒 Files selected for processing (2)
  • flashinfer/prefill.py
  • tests/attention/test_nvfp4_attention_sm120.py

jethac and others added 9 commits July 31, 2026 00:42
…ound

The previous rationale claimed a split-KV chunk boundary can land in
the middle of a 16-element scale block. That mechanism is wrong: NVFP4
scale factors group 16 consecutive head-dim elements of a single token,
while split-KV partitions the token axis, so a split boundary never
slices a scale block. Reword the gate docstring, call-site comments and
test docstring to state what is actually known -- corruption was
observed empirically when qo_len << kv_len (decode / prefix-cache
extend) and disappears with split-KV disabled at no measured decode
throughput cost -- and cite the interaction between small per-split KV
chunks and the 1-byte-KV NUM_MMA_KV tile floor as an unconfirmed
hypothesis rather than fact. Also drops project-specific NOTE tags from
nearby comments. Comment-only; no behavior change.

Addresses review feedback on flashinfer-ai#3684 from @qsang-nv.

Signed-off-by: Jetha Chan <jethachan@gmail.com>
run() fills declared-but-unprovided JIT scalars from a fixed mapping
and raised a bare KeyError when a JIT module declares a scalar the
mapping does not know how to derive. Raise a ValueError naming the
scalar and listing the derivable set instead. Also drop the redundant
max(0, ...) clamp on the provided-scalar count:
prepare_jit_additional_args always returns at least one entry per
declared tensor name, so the excess over the tensor-name count cannot
be negative; a comment records that invariant.

Addresses review feedback on flashinfer-ai#3684 from @qsang-nv.

Signed-off-by: Jetha Chan <jethachan@gmail.com>
…ll-VO

For head_dim_qk >= 512 with head_dim_vo < 512 (asymmetric heads),
FA2DetermineCtaTileQ always returns CTA_TILE_Q=16, so the CTA_TILE_Q=32
instantiation in the paged/ragged kernel-instantiation lists can never
be dispatched; drop it and instantiate only CTA16 for that shape class.
The dispatch macro case-32 arm still references the symbol, which stays
unresolved in the module exactly like the pre-existing never-selected
CTA_TILE_Q=32 of symmetric head_dim < 512 modules (verified: shipped
modules carry it as an undefined, lazily-bound symbol). Also updates
the KernelTraits::IsInvalid comment to describe the three-way
CTA_TILE_Q selection.

Addresses review feedback on flashinfer-ai#3684 from @qsang-nv.

Signed-off-by: Jetha Chan <jethachan@gmail.com>
…ual-stride rejection

Completes the support-or-reject-explicitly contract for unequal K/V
strides in tree: every consumer reachable from the updated entry points
must either support independently-strided K/V pools or reject them
loudly.

Support half: asymmetric (head_dim_qk != head_dim_vo) NVFP4 paged
prefill over (512,256) and (256,128) x page_size {1,16} x num_kv_heads
{2,8}, causal. K/V pools and their scale-factor tensors are separately
allocated with genuinely different stride families; bf16 sources are
quantized with the in-tree NVFP4 KV quantization kernel and the FA2
output is checked against a float32 reference attention computed on
nvfp4_kv_dequantize_paged output, so kernel and reference consume the
exact same quantized bytes (dequantization oracle, not a requantized
approximation).

Reject half: the CUDA-core decode entry point
(BatchDecodeWithPagedKVCacheRun) addresses both K and V through a
single set of (K) strides, so its restored ICHECK must fire on K/V
pools whose stride families differ instead of silently misaddressing V.
A positive control with identically padded (equal-stride,
non-contiguous) pools runs and matches the reference, proving the
negative case fails because of the stride inequality and not the padded
allocation.

Addresses review feedback on flashinfer-ai#3684 from @qsang-nv.

Signed-off-by: Jetha Chan <jethachan@gmail.com>
Regression test for the FA2DetermineCtaTileQ shared-memory probe at
head dims that reach it today: plan()/JIT do not validate head dims, so
(qk, vo) = (448, 256) under pos_encoding_mode NONE is accepted, and at
2-byte KV its short-q 1x4-layout cost (104448 bytes) exceeds the
101376-byte per-block opt-in limit of 99KB parts.

The test computes the expected tile from the device's actual opt-in
limit (so the assertion is exact on every architecture), asserts the
planned cta_tile_q via PrefillPlanInfo (the same technique as
test_fp8_prefill.py), and for 2-byte KV runs the kernel against an
exact float32 reference: on 99KB parts this proves the probe fires and
the CTA64 fallback keeps the configuration dispatchable where the
CTA16 dispatch would exceed the per-block limit, and on larger-smem
parts it proves the CTA16 selection runs. For 1-byte KV the assertion
is plan-level: the FA2 1-byte KV producers require head_dim to be a
multiple of 128 elements (the 128-bit-per-lane load loop steps
NUM_MMA_D by 8, and the k128B swizzle needs an 8-aligned upcast
stride), so no currently-runnable 1-byte configuration reaches the
flipped CTA64->CTA16 region -- the pin locks the documented planner
behavior for when one does.

Addresses review feedback on flashinfer-ai#3684 from @qsang-nv.

Signed-off-by: Jetha Chan <jethachan@gmail.com>
…trides

The asymmetric NVFP4 stride test does not execute the produce_v fix in
page_produce_kv_on_the_fly: that producer runs only under
USE_KV_SHARED_SMEM, which excludes FP4 and requires HEAD_DIM_QK ==
HEAD_DIM_VO, so the NVFP4 asymmetric path takes the prefetched
thr_local_kv_offset_{k,v} arrays instead.

Add the configuration that does execute it: 16-bit KV at
(qk, vo) = (512, 512), where USE_KV_SHARED_SMEM holds for both CTA
tiles the planner can pick (static reasoning from prefill.cuh):
CTA_TILE_Q=16 for short q (NUM_WARPS_KV=4; NUM_MMA_D_VO=32 % 4 == 0)
and CTA_TILE_Q=32 for long q (kLargeHeadWarpSplit: NUM_WARPS_KV=2;
32 % 2 == 0), so USE_VO_SPLIT -- and with fp16's equal head dims,
USE_KV_SHARED_SMEM -- is true either way; the qo_len parametrization
covers both tiles and the kv_layout parametrization covers NHD and
HND. K and V pools are views of differently padded parent tensors
(identical logical shapes, unequal stride families, mirroring the
decode negative test's construction), so
get_paged_kv_offset_for_logical_row<produce_v=true> must route V rows
through the V strides: with the fix reverted, the V loads walk K's
stride family and the output diverges from the exact float32
reference, which is how this test was validated to catch the bug it
pins. The configuration is SM80+, so it runs on the standard CI
runners.

Addresses review feedback on flashinfer-ai#3684 from @qsang-nv.

Signed-off-by: Jetha Chan <jethachan@gmail.com>
The (448, 256) CtaTileQ smem-probe test parametrizes over kv_dtype in
{float16, float8_e4m3fn}. On pre-SM100 GPUs the FP8 (1-byte) parametrization
errors before reaching the tile assertion: _fa2_head_dim_nvcc_flags restricts
non-NVFP4 1-byte large-head modules to major versions [10, 11, 12], so the JIT
spec-gen inside plan() raises "No supported CUDA architectures found for major
versions [10, 11, 12]". skip_if_head_dim_unsupported only gates the 16-bit path,
so it misses this.

Add a dtype-aware skip mirroring the module gate, and narrow the docstring
wording "exact on every architecture" -> "exact on every supported
architecture". The fp16 parametrization is unaffected (2-byte fallback, SM80+).

Addresses review feedback on flashinfer-ai#3684 from @qsang-nv.

Signed-off-by: Jetha Chan <jethachan@gmail.com>
gen_customize_batch_prefill_module now requires the scale-factor tensors
(maybe_k_cache_sf / maybe_v_cache_sf) as additional inputs whenever the KV
dtype resolves to NVFP4, raising ValueError otherwise. The host-side
test_customize_batch_prefill_nvfp4_large_head_uses_prefill_flags still called
the generator with empty additional-tensor lists, so it tripped that
ValueError before reaching either flag assertion and failed on every arch
(it never touches the GPU). Pass the two uint8_t SF tensors, mirroring the
production caller, so generation completes and the assertions run:
_fa2_prefill_head_dim_nvcc_flags emits sm_86 (allow_nvfp4_sm8_large_head), and
the plain _fa2_head_dim_nvcc_flags still restricts to [10,11,12] and raises.

Signed-off-by: Jetha Chan <jethachan@gmail.com>
…ib.suppress)

Signed-off-by: Jetha Chan <jethachan@gmail.com>
Co-authored-by: TechPrototyper <tech@smartlogics.net>
@jethac

jethac commented Jul 31, 2026

Copy link
Copy Markdown
Contributor Author

@qsang-nv rebased onto current main — now conflict-free at fa894014e (MERGEABLE, 0 behind), nothing else changed vs the head you LGTM'd; ready for you to trigger CI whenever.

@jethac

jethac commented Jul 31, 2026

Copy link
Copy Markdown
Contributor Author

@qsang-nv the CI run (30594351621) has been stuck ~1.5 days on JIT Unittest (H100) — still in_progress with no movement since 07-31 13:46 (looks like a hung/queued runner), so it never completes. The only real failure is JIT Unittest 4 (A10G)tests/test_artifacts.py::test_get_subdir_file_list, an unrelated cubin-download mock test (this PR touches attention kernels, not artifacts/cubin/deep-gemm; it fails on a stale-mock 404 for a deep-gemm checksum, almost certainly red on main too). Could you cancel + re-trigger the run? T4 and A10G shards 1–3/5 are green. Thanks!

@jethac

jethac commented Aug 1, 2026

Copy link
Copy Markdown
Contributor Author

Update — run 30594351621 completed: H100 ran the full suite green (attention/nvfp4/gemm/moe); the only red is tests/test_artifacts.py, which is byte-identical to main here and fails on a missing DEEPGEMM_RUBIN cubin mock (added by the SM107 v0.6.16 merge-back, #4280) — pre-existing on main, unrelated to this PR.

@qsang-nv

qsang-nv commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator

/bot run

@flashinfer-bot

Copy link
Copy Markdown
Collaborator

GitLab MR !1056 has been updated with latest changes, and the CI pipeline #60789494 is currently running. I'll report back once the pipeline job completes.

@flashinfer-bot

Copy link
Copy Markdown
Collaborator

[FAILED] Pipeline #60789494 — 13/18 executed test jobs passed

Compared with nightly #60712014.

Unit Tests

GPU CUDA 12.9 CUDA 13.0 Notes
5090 ⚠️ Infra ⚠️ Infra Infrastructure: CI infrastructure failure (2 jobs; CUDA 12.9, CUDA 13.0)
B300 ❌ New ✅ Pass New: tests.attention.test_trtllm_gen_attention_decode (1 failure; CUDA 12.9)
Test timeout: 1 test file timed out: tests/moe/test_trtllm_gen_fused_moe.py (1 job; CUDA 12.9)
GB200 ✅ Pass ⚠️ Infra Infrastructure: test infrastructure interrupted the job (1 job; CUDA 13.0)
GB300 ✅ Pass ✅ Pass
H100 ✅ Pass ✅ Pass
RTX Pro 6000 Blackwell ✅ Pass ✅ Pass

✅ Pass · 🟡 Old failure · ❌ New failure · ⏱ Test timeout · ⚠️ Infrastructure · ❔ Unknown or unclassified · — Not run

Multi-GPU and Multi-Node Tests — 5/6 passed

GPU CUDA 12.9 CUDA 13.0 Notes
B300 (multi-GPU) ✅ Pass ❔ Failed
GB200 (multi-node) ✅ Pass ✅ Pass
GB300 (multi-node) ✅ Pass ✅ Pass
Failure details

New relative to nightly (attribution uncertain)

  • tests.attention.test_trtllm_gen_attention_decode — 1 failure on B300 / CUDA 12.9
    • AssertionError: Tensors are not close enough! Mismatched elements: 15586 / 8450048 (0.18%) Allowed mismatched elements: 422, but found 15586. Greatest absolute difference: 691 (…

Timeouts, infrastructure, or incomplete jobs

@TechPrototyper

Copy link
Copy Markdown
Contributor

Since the internal CI's RTX 5090 rows were reported as infrastructure failures, here is an independent datapoint from consumer hardware, at the current PR head.

Environment. RTX 5090 (sm_120), driver 595.71.05, CUDA 13.0, torch 2.11.0+cu130, containerized Linux. flashinfer checked out at dd25a783 ("Merge branch 'main' into nvfp4-vosplit-rederive"), editable install, JIT kernel path (TORCH_CUDA_ARCH_LIST=12.0, MAX_JOBS=8), pytest with 1800 s per-test timeout.

Results — zero failures, zero errors across all three suites:

Suite passed failed errors skipped xfailed xpassed wall time
tests/attention 117,829 0 0 265,520 6,717 11,242 1 h 24 min
tests/gemm -k fp4 8,453 0 0 20,170 18 min
tests/moe -k fp4 125 0 0 9,117 9 min

Skips are the expected non-sm120 paths (SM100/hopper-only kernels, unsupported dtypes/head-dims). No test hit the 1800 s timeout.

Two environment notes for reproducibility:

  • scipy had to be added to the image (tests/attention/test_block_sparse.py and test_hopper_fp8_attention.py import it at collection time; without it the whole attention suite aborts on collection unless --continue-on-collection-errors is set).
  • The 11,242 xpassed in the attention suite are tests marked xfail that pass on this platform; not investigated further, mentioned for completeness.

junit XMLs for all three suites available on request.

@qsang-nv

Copy link
Copy Markdown
Collaborator

@flashinfer-bot run

@jethac

jethac commented Aug 12, 2026

Copy link
Copy Markdown
Contributor Author

@TechPrototyper thank you for this. I'd been meaning to rent a 5090 on RunPod and run exactly this myself and just haven't had the time — you saved me a real chunk of work. The scipy collection-time import is a good catch too; pre-existing gap in tests/attention rather than anything this PR introduces.

Your run was at dd25a783, so I've re-run at the current head 00054844 on a rented 5090 — the upstream merge in between touches one file this PR also touches (flashinfer/mla/_core.py).

Environment. RTX 5090 (sm_120), driver 580.126.09, CUDA 13.0 (nvcc 13.0.88), torch 2.9.1+cu130, editable/JIT install at 00054844, TORCH_CUDA_ARCH_LIST=12.0. Different torch minor from the run above, so between us this has sm120 coverage on two torch versions.

19,202 tests executed, zero failures, zero errors:

suite executed failed skipped time
tests/attention -k fp4 508 0 31,229 31 min
tests/gemm -k fp4 6,551 0 22,072 12 min
tests/jit 129 0 0 <1 s
test_batch_{decode,prefill}_kernels.py, unfiltered 12,014 0 7,117 21 min

The last row is the two attention test files this PR modifies, run without the fp4 filter, since the PR also edits shared headers (prefill.cuh, scheduler.cuh, page.cuh, vec_dtypes.cuh) that non-fp4 paths compile against. tests/jit is included because the PR touches test_attention_utils.py and test_jit_cpp_ext.py.

test_nvfp4_attention_sm120.py ran 14/14 with nothing skipped: accuracy across s128–s8192 at d64/d128 causal and non-causal, structured-Q correction, LSE, causal mask column order, split-KV gate dtype logic.

Attention skips are arch gating rather than the feature being passed over — 15,584 trtllm-gen requires SM100/SM103, 9,792 xqa backend limits, 2,560 SM100/SM103-only.

@qsang-nv — the @flashinfer-bot run from Aug 11 hasn't reported back. Could you check whether that pipeline stalled, or re-trigger it? From the Aug 3 run (#60789494), none of the red was attributable here: 5090 lost both CUDA versions to infra failure, GB200/13.0 was an infra interrupt, and the B300/12.9 test_trtllm_gen_attention_decode mismatch plus test_trtllm_gen_fused_moe timeout both pass on 13.0, with attribution flagged uncertain by the bot. GB300, H100 and RTX Pro 6000 were green on both.

junit XMLs available if useful.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants