perf(gemm): split-K, occupancy, and m=1 stream-GEMV decode tactics for mm_bf16_fp4 - #4038
Conversation
|
Caution The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased. |
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughChangesThe BF16×FP4 Cute-DSL backend adds split-K GEMM and an SM12x m=1 GEMV kernel. Tactic selection, compilation, partial reduction, device-specific caching, and CUDA validation now cover these paths. BF16-FP4 Cute-DSL execution
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant GEMMForward
participant TacticSelector
participant CuteDSLCompiler
participant Kernel
participant PartialReducer
GEMMForward->>TacticSelector: select GEMM or GEMV tactic
TacticSelector->>CuteDSLCompiler: compile split and occupancy configuration
CuteDSLCompiler->>Kernel: launch with partial buffer
Kernel->>PartialReducer: write FP32 partials when split
PartialReducer->>GEMMForward: write reduced output
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1🛠️ Fix failing CI checks 💡
🧪 Generate unit tests (beta)
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. Comment |
03e1f42 to
8d37c5c
Compare
|
Caution The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased. |
Direct-load GEMV for bf16 x nvfp4 at tiny M: weights stream GMEM->registers (no SMEM staging, no tensor cores), one output row per warp, warp-shuffle reduction, schedule modeled on MsaProxyScoreDecodeStreamSm12x. Consumes the canonical (N, K/2) packed layout + linear E4M3 scales, so no weight repack. Dev-5080 dram-cold (box capped ~300 GB/s): m=1 parity with the autotuned MMA kernel on all three Qwen3.6-27B shapes (298/292/273 GB/s eff); decisive test is prod 5080 where the MMA kernel sits at 77-85% of the 828 GB/s roof. m=2/m=4 collapse (0.5x/0.22x) because A reloads scale with M on the serial path -- v1 is an m=1 specialist; A-in-SMEM staging is the known v2 lever. Not wired into dispatch yet; standalone cute.compile prototype. AI-assisted (Claude Code). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replace bare literals with constants derived from I32_PER_LANE / CODES_PER_I32 / SF_VEC_SIZE (the 64/2 in the scale-block indexing, the 4/8 fragment sizes) and comment the log2(warp) butterfly count. No codegen change: dev-5080 timing and rel_l2 identical. AI-assisted (Claude Code). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… layout Replace the canonical-layout prototype with a warp-cooperative walk of the backend's prepared operands (tile-packed int32 weight + S0E5M3 scales), so the GEMV and the MMA kernel share one weight format and dispatch per call. Each warp owns a 64-wide N tile and streams its 512B packed K-tiles fully coalesced; the pack's thread mapping fixes per lane which (k, n) each byte holds, leaving four f32 partials per lane plus one xor-1 butterfly per dot. fp4 and scale decode use the hw cvt.f16x2.e2m1x2 and f16 bit-place ops, dropping the prototype's power-of-two unfolds and its K % 1024 restriction. splits shards K across grid.y for grid fill, reusing the MMA kernel's fp32 workspace + fixed-order reduce for determinism; PDL chains match too. Dev-5080 dram-cold m=1 matches the canonical-layout prototype (294/294/271 GB/s eff on the Qwen shapes) that won the m=1 slot on prod 5080. AI-assisted (Claude Code). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Tactic configs gain a kind field ('mma' | 'gemv'); gemv entries carry only
a splits knob. get_valid_tactics gates them to the m=1 bucket (buckets
round up, so no m > 1 call can land there) and to a 12-96 warps-per-SM
residency window. forward dispatches gemv tactics to the new compile
cache entry and raises on m != 1 as a guard; the fallback path is
unchanged. The every-tactic test now skips gemv configs at m > 1 and
covers all gemv splits (correctness + determinism) at the m=1 cell.
Autotuner verified end to end on dev 5080: picks gemv-s32 on down and
gemv-s1 on lm_head unaided (gate_up ties under warm-L2 at this box's
DRAM cap; prod dram-cold data says gemv there too).
AI-assisted (Claude Code).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Reuse the shared f16x2_to_f32x2 from fp4_common instead of a local copy; drop the now-unused Tuple import. - Gate the gemv tactic to SM12x (compute capability major 12), the only arch it is validated on -- the autotuner ranks by time, so an untested arch must not be offered a kernel whose correctness is unchecked there. - Build the tactic-config list once per forward() call instead of two or three times in the gemv branch. - Pin the M == 1 invariant with a comment (M is symbolic in the traced __call__, so the runner's host-side m != 1 guard is the real check). - Cover fp16 output through the gemv path (test_cute_dsl_gemv_fp16_out); the every-tactic test only drove bf16. AI-assisted (Claude Code). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The power-of-2 split menu quantizes wave fill (gate_up lands at 0.54 or 1.08 waves on 84 SMs, nothing in between); derive one extra split from the device SM count so tiles x splits lands at ~0.9-0.95 waves, leaving slack for stragglers and the PDL-overlapped tail of the prior kernel. The reduce loop becomes dynamic since fill splits reach ~100+. Measured on the DRAM-capped dev 5080: fill splits sit exactly on the pow2 baselines (s7 342.3us vs s4 341.0, s47 170.5 vs s32 170.7) while 1.08 waves costs +3%; the upside case needs a production-bandwidth part. AI-assisted. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Serving stacks do not tune every shape (vLLM's warmup autotune never captures compute_logits, so lm_head always takes the fallback), and the tactic=-1 path only knew the MMA heuristic. Pick the gemv instead where it applies, at the smallest split reaching ~20 warps/SM: measured perf saturates there and deeper splits only add partial traffic (P6K lm_head s2 costs 2% over s1). The fill-split formula moves into a shared helper. Recovers +3.1% per-op on P6K lm_head under vLLM as-shipped. AI-assisted. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The P6K probe showed throughput saturates at ~20 warps/SM and deeper splits pay partial traffic, so the knee split (down s47 on 188 SMs, 35.97us vs pow2-best s32 36.50us dram-cold) is the shape the menu was missing. Shared helper feeds both the menu and the m=1 fallback; the fallback docstring drops its duplicate rationale. Comment polish per dev-writing-style (no bench numbers in docstrings). AI-assisted. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- pin the tactic-index space to the device: the autotuner cache key now carries the SM count, since the config list holds device-derived gemv splits and a tuned index must not replay on a different part - keep do_preparation on the MMA heuristic: the autotuner issues it outside its per-tactic exception guard, so a gemv failure there would abort tuning for the whole op - cache the tactic-config builder (all-int args; ~7us -> 0.1us per forward call) and make sm_count required so no caller can silently enumerate a misaligned index space - share the 12-96 warps/SM residency window between the tactic filter and the fallback selector, and enforce the ceiling in the fallback - unroll the partial reduce for the common small split counts again; dynamic loop only for the ~100+ device-derived splits - tests: derive the fallback skip guard from the test's own shape, inline the one-use device helper AI-assisted. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Measured on three SKUs (dev 5080, RTX PRO 6000, GB10), the max-fill split never beats the knee split and loses 1-3% where bandwidth allows any spread, so its menu entry was tuner-rejected dead weight and one compile key per shape. The wave cap folds into the knee helper; the menu is now pow2 + knee. AI-assisted. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
5d22eb4 to
d074a6b
Compare
|
Note GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer. |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 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 `@tests/gemm/test_mm_bf16_fp4.py`:
- Around line 305-309: The GEMV tests must skip tactics on devices without
compute capability 12. In tests/gemm/test_mm_bf16_fp4.py lines 305-309, update
the condition around _bf16_fp4_cute_dsl_tactic_configs to skip GEMV when m is
not 1 or get_compute_capability(device)[0] is not 12; at lines 350-357, add
pytest.skip before the tactic loop when the device major capability is not 12.
🪄 Autofix
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 Plus
Run ID: b5318e40-37a3-45af-bbb0-4d246155cb80
📒 Files selected for processing (4)
flashinfer/gemm/gemm_bf16_fp4_cute_dsl.pyflashinfer/gemm/kernels/cute_dsl/dense_gemm_bf16_fp4_blackwell.pyflashinfer/gemm/kernels/cute_dsl/gemv_bf16_fp4_sm12x.pytests/gemm/test_mm_bf16_fp4.py
The tests enumerate the tactic list directly, bypassing get_valid_tactics' arch gate, so on other supported compute capabilities they would run the gemv where the production path never selects it. Addresses the CodeRabbit review finding. AI-assisted. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Cut restated claims, cross-file narration, and rationale stated in more than one place (determinism, the occupancy trade, the m=1-only rule now each have one owner); comment-only. AI-assisted. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
AI-assisted. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
@flashinfer-bot run |
|
/bot run tests/gemm |
…r mm_bf16_fp4 (flashinfer-ai#4038) ## 📌 Description flashinfer-ai#3597 added `mm_bf16_fp4` (bf16 activations x nvfp4 weights) so vLLM can replace Marlin, its default W4A16 backend, with a FlashInfer kernel; DGX Spark is the lead target. On decode shapes the kernel trailed Marlin for two reasons: small-n grids underfill the GPU, and at single-token batches (m=1) too few resident warps per SM hide DRAM latency. This PR addresses both: m=1 decode beats or matches Marlin on every part we measured, and end-to-end serving of Qwen3.6-27B-NVFP4 flips from behind to ahead at batch 1 with no regression elsewhere. **What it does** - Adds split-K tactics (2/4/8 splits) to the autotuner space, offered only when splitting shortens the grid's last wave by at least 25%. Splits write fp32 partials, and a PDL-chained reduce kernel sums them in fixed order, so results are deterministic run to run. - Adds occupancy tactics: 2 or 3 co-resident CTAs per SM trade pipeline depth for latency hiding on weight-bound grids, plus occupancy 2 combined with split-K for narrow-n shapes. - Adds a streaming GEMV kernel (`gemv_bf16_fp4_sm12x.py`) for the bandwidth-bound m=1 case: no shared memory, no tensor cores, weights stream from global memory to registers with latency hidden by warp count. It reads the same packed operands as the MMA kernel, so the autotuner picks between the two per shape. - Sizes GEMV split-K from the device: alongside the power-of-2 splits, the menu carries a split targeting ~20 warps/SM (the measured saturation point). Tactic indices become device-scoped, so the autotuner cache key now carries the SM count. - Routes the no-autotune m=1 fallback onto the GEMV. Serving stacks do not tune every shape (vLLM's warmup never captures the logits GEMM), so the lm_head always takes this path. - Fixes kernel launch to pass no cluster dimensions: the boilerplate `cluster=[1,1,1]` routed launches through the cluster work distributor, whose co-residency cap silently defeated the occupancy tactics on SM12x. No public API changes. The one observable behavior change: untuned m=1 calls on SM12x now run the GEMV, whose output is bitwise different from the MMA heuristic's but equally accurate and still deterministic. ### Performance #### Split-K on the flashinfer-ai#3597 decode shapes (RTX PRO 6000 / DGX Spark / RTX 5080) Single-token decode GEMMs (m=1). The first table covers serving-class shapes (Llama-8B projection layers plus the flashinfer-ai#3597 example shape); the second covers flashinfer-ai#3597's own benchmark grid. Median GPU time over CUDA-graph replays with a cold L2 cache, as in serving. Baseline is vLLM's Marlin on the same GPU; FlashInfer runs with autotuning. Speedup = Marlin time / FlashInfer time. Each cell is a RTX PRO 6000 / DGX Spark / RTX 5080 triple. | n x k | vs Marlin, before this PR | vs Marlin, with this PR | |--:|:--:|:--:| | 2048x7168 | 0.57 / 1.00 / 0.47 | **1.36** / **1.02** / **0.78** | | 4096x4096 | 0.75 / 0.91 / 0.90 | **1.06** / **1.00** / 0.90 | | 4096x14336 | 0.60 / 0.91 / 0.78 | **0.98** / **1.00** / 0.78 | | 14336x4096 | 0.97 / 1.00 / 0.86 | 0.97 / 1.00 / 0.86 | | 10304x2688 | 0.78 / 0.98 / 0.96 | 0.78 / 0.98 / 0.96 | The same comparison over flashinfer-ai#3597's benchmark grid (4096x4096 appears in the table above): | n x k | vs Marlin, before this PR | vs Marlin, with this PR | |--:|:--:|:--:| | 512x2048 | 2.09 / 0.95 / 0.95 | **4.48** / **1.43** / **2.30** | | 512x4096 | 1.19 / 0.70 / 0.56 | **3.61** / **1.26** / **1.88** | | 1024x2048 | 1.26 / 0.98 / 0.70 | **2.39** / **1.03** / **1.32** | | 1024x4096 | 0.99 / 0.86 / 0.49 | **2.72** / **1.04** / **1.21** | | 2048x512 | 1.86 / 1.25 / 1.19 | 1.86 / 1.25 / 1.19 | | 2048x1024 | 1.36 / 1.16 / 0.90 | **1.40** / 1.16 / **1.02** | | 2048x2048 | 1.17 / 1.11 / 0.70 | **1.72** / **1.02**\* (1.11) / **0.97** | | 2048x4096 | 0.73 / 1.08 / 0.54 | **1.48** / **1.04**\* (1.08) / **0.84** | | 4096x512 | 1.40 / 1.01 / 1.38 | 1.40 / **0.95**\* (1.01) / 1.38 | | 4096x1024 | 1.41 / 0.93 / 1.18 | 1.41 / **0.95** / 1.18 | | 4096x2048 | 0.95 / 0.95 / 1.06 | 0.95 / **1.00** / 1.06 | | 131072x2048 | 0.98 / 0.97 / 0.90 | 0.98 / 0.97 / 0.90 | | 248320x2048 | 0.98 / 1.00 / 0.93 | 0.98 / 1.00 / 0.93 | - Bold marks the cells this PR changes (the tuner picks a new split-K tactic); unbolded picks perform as before. - \* These three cells are tuner mis-picks, not kernel regressions: an accurate pick would keep flashinfer-ai#3597's pre-existing non-split config, and the value in parentheses is what that config achieves. The Reviewer Notes explain the cause. - On the serving shapes, the RTX 5080 column stays below 1.0 even where this PR helps. Profiling of the larger losses points to activation re-reads through L2, a separate problem from grid fill and out of scope here. #### Qwen3.6-27B-NVFP4 decode GEMMs at m=1 (RTX 5080, DGX Spark) Same methodology as above; speedup = Marlin time / FlashInfer time. | GEMM (n x k) | RTX 5080 | DGX Spark | |--|:--:|:--:| | gate_up 34816x5120 | **1.03** | 1.00 | | down 5120x17408 | **1.03** | 1.00 | | lm_head 248320x5120 | **1.04**\* | **1.04** | \* Marlin's lm_head repack does not fit on the 16 GB RTX 5080, so this cell compares against the best in-tree MMA tactic. Spark ties at its bandwidth floor on the first two shapes. #### End-to-end vLLM serving of Qwen3.6-27B-NVFP4 (RTX PRO 6000) Full serving A/B, FlashInfer leg vs Marlin leg under identical settings, aiperf output-token throughput, ratio = FlashInfer / Marlin: batch-1 decode 1.011x, low-concurrency speculative decode 1.08 to 1.11x ahead, and every other cell at parity within run-to-run noise (0.978 to 1.014x) with no regression beyond it. ## 🔍 Related Issues Follow-up to flashinfer-ai#3597. ## 🚀 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 - [x] I have installed `pre-commit` by running `pip install pre-commit` (or used your preferred method). - [x] I have installed the hooks with `pre-commit install`. - [x] I have run the hooks manually with `pre-commit run --all-files` and fixed any reported issues. ## 🧪 Tests - [x] Tests have been added or updated as needed. - [x] All tests are passing (`unittest`, etc.). New tests: - Every enumerated tactic (MMA and GEMV) is checked against a reference and for bit-exact run-to-run determinism. - Unit tests pin the fallback selectors' picks; one test drives tactic=-1 through the GEMV fallback end to end. - Full test file passes on RTX 5080, RTX PRO 6000, and GB10. ## Reviewer Notes - Most gains require autotuning, which serving frameworks run at startup. The no-autotune fallback picks match the tuner's choices on every part we measured. - The autotuner times candidates with a warm L2 while decode serving runs cold, so it can over-rank split tactics; the 25% last-wave guard compensates but does not fully close it (the three Spark cells in the grid table). This measurement gap is general and deserves its own issue. - The fallback picks add JIT-compiled kernel variants per decode shape class, cached in-process only; that cost amortizes to once per machine when this module adopts the flashinfer-ai#3874 CuTe-DSL disk cache, as flashinfer-ai#4029 did for the sibling `mm_fp4` path. The GEMV's device-derived splits widen this surface, so the follow-up is worth prioritizing. - Other FlashInfer cute-dsl kernels also pass `cluster=[1,1,1]` at launch and inherit the same co-residency cap; they are worth a separate audit. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Added split-K support for bf16 × fp4 matrix multiplication to improve performance across varying workloads. - Added a dedicated SM12x GEMV path for efficient single-row operations. - Added automatic tuning for split counts, occupancy, and device-specific execution strategies. - Added support for FP16 GEMV outputs and deterministic partial-result reduction. - **Bug Fixes** - Improved handling of GEMV and split-K fallback selection across supported shapes and GPU configurations. - **Tests** - Added coverage for accuracy, determinism, GEMV correctness, and split-K selection. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: Brian K. Ryu <bryu@nvidia.com>
📌 Description
#3597 added
mm_bf16_fp4(bf16 activations x nvfp4 weights) so vLLM can replace Marlin, its default W4A16 backend, with a FlashInfer kernel; DGX Spark is the lead target. On decode shapes the kernel trailed Marlin for two reasons: small-n grids underfill the GPU, and at single-token batches (m=1) too few resident warps per SM hide DRAM latency. This PR addresses both: m=1 decode beats or matches Marlin on every part we measured, and end-to-end serving of Qwen3.6-27B-NVFP4 flips from behind to ahead at batch 1 with no regression elsewhere.What it does
gemv_bf16_fp4_sm12x.py) for the bandwidth-bound m=1 case: no shared memory, no tensor cores, weights stream from global memory to registers with latency hidden by warp count. It reads the same packed operands as the MMA kernel, so the autotuner picks between the two per shape.cluster=[1,1,1]routed launches through the cluster work distributor, whose co-residency cap silently defeated the occupancy tactics on SM12x.No public API changes. The one observable behavior change: untuned m=1 calls on SM12x now run the GEMV, whose output is bitwise different from the MMA heuristic's but equally accurate and still deterministic.
Performance
Split-K on the #3597 decode shapes (RTX PRO 6000 / DGX Spark / RTX 5080)
Single-token decode GEMMs (m=1). The first table covers serving-class shapes (Llama-8B projection layers plus the #3597 example shape); the second covers #3597's own benchmark grid. Median GPU time over CUDA-graph replays with a cold L2 cache, as in serving. Baseline is vLLM's Marlin on the same GPU; FlashInfer runs with autotuning. Speedup = Marlin time / FlashInfer time. Each cell is a RTX PRO 6000 / DGX Spark / RTX 5080 triple.
The same comparison over #3597's benchmark grid (4096x4096 appears in the table above):
Qwen3.6-27B-NVFP4 decode GEMMs at m=1 (RTX 5080, DGX Spark)
Same methodology as above; speedup = Marlin time / FlashInfer time.
* Marlin's lm_head repack does not fit on the 16 GB RTX 5080, so this cell compares against the best in-tree MMA tactic. Spark ties at its bandwidth floor on the first two shapes.
End-to-end vLLM serving of Qwen3.6-27B-NVFP4 (RTX PRO 6000)
Full serving A/B, FlashInfer leg vs Marlin leg under identical settings, aiperf output-token throughput, ratio = FlashInfer / Marlin: batch-1 decode 1.011x, low-concurrency speculative decode 1.08 to 1.11x ahead, and every other cell at parity within run-to-run noise (0.978 to 1.014x) with no regression beyond it.
🔍 Related Issues
Follow-up to #3597.
🚀 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
pre-commitby runningpip install pre-commit(or used your preferred method).pre-commit install.pre-commit run --all-filesand fixed any reported issues.🧪 Tests
unittest, etc.).New tests:
Reviewer Notes
mm_fp4path. The GEMV's device-derived splits widen this surface, so the follow-up is worth prioritizing.cluster=[1,1,1]at launch and inherit the same co-residency cap; they are worth a separate audit.Summary by CodeRabbit
New Features
Bug Fixes
Tests