Repository navigation
[Perf][Prefill] Combine opt for rubin + prefill q8kv8 indexer opt - #36
Merged
Merged
Conversation
Fold the nv_dev v1 combine (src/blackwell_prefill/combine.py) into src/sm100/fwd/combine.py and make it the public combine() for all callers: the Blackwell port route, the original FP8/BF16 path, the NVFP4 KV path and the Q8KV4 prefill. src/sm100/fwd/combine.py now has two sections, one per kernel: - Original combine: SparseAttentionForwardCombineF32F8 and its launcher combine_f32f8, unchanged apart from the names. It implements the FP32 and FP8 E4M3 STG.128 fake layouts as well as BF16/FP16. - v1 combine: SparseAttentionForwardCombine (moved verbatim) and its launcher combine(), the default. It takes BF16/FP16 partials only: the v1 kernel implements just that STG.128 fake layout. interface.py picks the launcher by partial dtype (_select_combine) on the two paths that accept a caller-chosen partial_dtype, the original FP8/BF16 path and the NVFP4 KV path. The Blackwell port route and Q8KV4 always write BF16 partials and call combine() directly. vLLM always writes BF16 partials, so production runs the v1 kernel. Other changes: - v1 launch: min_blocks_per_mp=0 (was 3 with output_scale + PDL), and a 3-stage O_partial ring on SM107 (2 elsewhere). - The Blackwell port route now accepts output_scale and passes it to the combine instead of falling back to the original path. - Delete src/blackwell_prefill/combine.py; drop the per-caller combine selection in interface.py and prefill_q8kv4/interface.py. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…x-AI#17) SM107-specific changes: - SM107MmaFP8Op (K=64) for the FP8 UMMA - TMEM load-and-reduce epilogue on SM107 - 168 registers for every warp role - K loader prefetches the next page-table entry - two Q stages - planner caps the page chunk (8..128) to fill the clusters Changes on all architectures: - define the physical page index before the dynamic K-load loop on the non-SM107 path (port of MiniMax-AI/MSA bc5f83a) - keep the four persistent task-bucket loops rolled (cutlass.range(..., unroll=1)) instead of unrolling them at trace time, which mainly shrinks the generated code Rubin (SM107), full 128-case prefill-indexer suite, score + top-k per call, weighted mean versus dev e441976: - H=1: 54.71 -> 49.34 us (about 1.11x); short queries (<512) about -22% - H=2: 88.04 -> 82.15 us (about 1.07x) Top-k output is bitwise identical in all 128 cases. The prefill and top-k tests in test_q8_indexer.py pass on SM107; on SM100 (B200) they passed before the loop-rolling change. Suggested-by: Fan Shi <fans@nvidia.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Caitlin Wang <caitlinw@nvidia.com>
BatchPrefillIndexerQ8KV8Wrapper always allocated its own score buffer and its only public call ran the TopK as well. A caller that selects pages itself (vLLM's shared TopK over decode and prefill rows) needs the scores written into its own buffer, without the wrapper's TopK. - plan(..., score_out=None): optional caller-owned fp32 buffer of shape [total_q * num_heads, >= max_pages] with contiguous pages per row; a wider row pitch is accepted, so a slice of a larger buffer works. The kernel layout is already dynamic, so no recompile and no kernel change. - run_scores(q, k_cache): public scores-only call; run() is unchanged. Tests: score_out matches the wrapper-owned scores bitwise at an 8192-page pitch, leaves other entries untouched, runs no TopK, and run() TopK is unchanged; invalid score_out buffers are rejected. Prefill and TopK tests in test_q8_indexer.py pass on SM107 (41 passed). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Caitlin Wang <caitlinw@nvidia.com>
fans-nv
added a commit
to fans-nv/vllm
that referenced
this pull request
Oct 13, 2026
Score FP8 indexer prefill with MSA's score-only CuTe scorer instead of FMHA OnlyScore. One plan per batch writes into the unified score buffer shared by all indexer layers; native Top16 and decode dispatch are unchanged. Add attention_config.minimax_m3_indexer_prefill_backend (auto/fmha/cute). "auto" keeps FMHA for BF16, unsupported shapes, or an MSA without run_scores; "cute" raises on unsupported configurations. Requires the MSA plan(score_out=)/run_scores() API (vllm-project/MSA#36); until the MSA pin is bumped, "auto" falls back to FMHA. Co-authored-by: OpenAI Codex Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Fan Shi <fans@nvidia.com>
Open
6 tasks done
zyongye
approved these changes
Oct 13, 2026
pgera
pushed a commit
to pgera/MSA
that referenced
this pull request
Oct 14, 2026
dev (vllm-project#36) folded the v1 combine into src/sm100/fwd/combine.py next to the original FP32 / FP8 E4M3 combine (combine_f32f8) and deleted src/blackwell_prefill/combine.py, while this branch had replaced the v1 combine with the TMA-ring combine in the Blackwell-lineage file. Keep dev's layout: the TMA-ring combine takes the v1 section of src/sm100/fwd/combine.py as the default combine(); combine_f32f8 and the partial-dtype selector stay, so FP32 and FP8 E4M3 partials remain supported. The selector also sends query head counts that are not a multiple of 4 to combine_f32f8, since the TMA-ring combine's LSE tensor map needs the flat (query, head) rows of every split 16-byte aligned; it binds cu_seqlens for combine_f32f8, which the TMA-ring combine does not take. The Blackwell port route goes through the same selector and passes output_scale as dev now does. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Overview
This PR makes two changes to the Q8KV8 paged prefill indexer (
python/fmha_sm100/cute/):Q8KV8PrefillIndexerSm100, ported from perf: optimize prefill q8kv8 indexer for rubin MiniMax-AI/MSA#17 (nv_dev). It includes the follow-upbc5f83afromnv_devandunroll = 1optimization.BatchPrefillIndexerQ8KV8Wrapper:plan(..., score_out=...)and a publicrun_scores(). [MiniMax-M3 Performance] Prefer CuTe for MiniMax M3 FP8 indexer prefill vllm#60930 uses these to run this scorer inside vLLM's shared-score-buffer / native-top-k indexer.Changes
Kernel and planner (
q8kv8_indexer_prefill.py,q8kv8_indexer_prefill_plan.py,q8_indexer_interface.py). SM107 changes only.SM107MmaFP8Op(K=64) for the FP8 UMMA instead of the generic F8F6F4 op (K=32).On all architectures:
cutlass.range(..., unroll=1)) instead of being unrolled at trace time, which shrinks the generated code.physical_pageis defined before the dynamic K-load loop on the non-SM107 path (port ofbc5f83a) (to avoid hanging).Wrapper API (
q8_indexer_interface.py, README):plan(..., score_out=None)takes an optional caller-owned fp32 buffer of shape[total_q * num_heads, >= max_pages], with contiguous pages per row. A wider row pitch is accepted, so a slice of a larger buffer works. The kernel layout is already dynamic, so there's no recompile and no kernel change. The buffer is validated on the host: dtype, device, shape, strides, alignment.run_scores(q, k_cache)is a public scores-only call (no top-k).run()is unchanged.Validation
Correctness:
test_q8_indexer.py -k "prefill or topk_select", with fresh caches and a GPU idle check each run:score_outtests.score_outat an 8,192-page pitch is bitwise identical to the wrapper-owned scores and leaves other entries untouched;run_scoresruns no top-k, andrun()'s top-k is unchanged;Kernel performance (Rubin SM107; 128-case prefill-indexer suite; score + top-k per call; CUDA-graph replay, 5 warmup + 20 timed; versus
deve441976):devdevEnd to end: vLLM nightly
81198e9with vllm-project/vllm#60684 and #60930 overlaid; MiniMax-M3-MXFP8, TP4, Rubin;--kv-cache-dtype fp8,indexer_kv_dtype: "fp8",minimax_m3_indexer_prefill_backend: "cute". Both arms loggedprefill scorer: CuTe.The end-to-end gain is small because fresh 8K prompts are the kernel's weakest case (long queries, short context), and the indexer is a small share of each prefill step. Short-chunk, long-context traffic (chunked prefill, prefix-cached multi-turn) is where the kernel gains concentrate.
Accuracy (NeMo-Skills, same configuration as the end-to-end runs; temperature 0, top-p 1, 32,768-token generation cap):
No significant difference. The kernel produces bitwise-identical top-k to
dev, so the differences are consistent with temperature-0 serving not being bit-reproducible.