Skip to content

[Kernel] SM 12.x blockwise FP8: gate the CTA swizzle on the activation size too - #55661

Closed
jschmied wants to merge 1 commit into
vllm-project:mainfrom
jschmied:perf/sm12x-blockwise-fp8-swizzle-gate
Closed

jschmied wants to merge 1 commit into
vllm-project:mainfrom
jschmied:perf/sm12x-blockwise-fp8-swizzle-gate

Conversation

@jschmied

@jschmied jschmied commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Purpose

#55180 (merged) switches the SM 12.x blockwise FP8 tile scheduler to a swizzled CTA order whenever the weight exceeds the L2. Above the L2 that order is not unconditionally better, so the weight-only gate leaves a measurable amount on the table. This adds the activation term back, with the small-M island that the earlier version of it was missing.

if (weight_bytes <= l2_bytes)     return 1;
if (m <= 1024)                    return 8;
return (m * k >= 14 MiB) ? 8 : 1;   // FP8 activations: 1 byte/element

An activation-slab term was in #55180 and was dropped during review, correctly at the time: as written it also kept the default order at small M, where the swizzle wins. The island fixes exactly that case, and the sweep below is a much finer grid than the one that informed the original.

Test Plan

Measured with the swizzle forced both ways at the same shape — a standalone build of this dispatch with max_swizzle_size exposed as an argument — so the two orders are compared directly rather than inferred across a gate. 6 shapes x M = 1024..6144 in steps of 512 = 66 cells, four starts, medians. GB10 (sm_121, 24 MiB L2), TP=1. Then the same 66 cells run through the gate as committed here, checking bit-identity against the stock op and that the gated launch lands on the arm the predicate selects.

pytest tests/kernels/quantization/test_cutlass_scaled_mm.py -k blockwise

Test Result

Summed percentage left on the table against the per-cell best, over all 66 cells:

gate regret worst cell
never swizzle 732.7 pp 69.1 %
weight > L2 (current) 118.8 pp 9.9 %
+ (m <= 2560 || m*k >= 14 MiB) 90.2 pp 9.9 %
+ (m*k >= 14 MiB), no island 54.0 pp 7.4 %
+ (m <= 2048 || m*k >= 14 MiB) 48.6 pp 8.7 %
+ (m <= 1024 || m*k >= 14 MiB) (this PR) 35.3 pp 5.8 %

The current gate's worst cells are at M = 2560 — 5120x5120 −10.4 %, 10240x2560 −9.4 %, 16384x2560 −6.4 % — a point the coarser sweep behind #55180 (M stepping by 2048) did not sample. At M = 4096 the 2560-wide weights lose 3–5 %.

The island stops at 1024 rather than 2048 because it is K-dependent: at K = 2560 the swizzle wins at low M, at K = 5120 the default order does, so a wider island gives back 6.9–8.5 % on the 5120-wide weights. M <= 1024 and M <= 1536 are indistinguishable (35.3 vs 35.2 pp); 2048 is not.

What the gate must not lose, and does not: the large wins, all at an activation slab above ~22 MiB — 7168x5120 at M = 6144 +223 %, M = 5632 +199 %, 14336x4096 at M = 6144 +158 %, 5120x5120 at M = 4608 +93 %.

Verification of the code as committed: bit-identical to the stock op on 66/66 cells (twice), and on the 26 cells where the two orders differ by more than 30 % the gated launch tracks the arm the predicate selects, 26/26.

Honest limits: this is one part (GB10, 24 MiB L2). Parts whose L2 holds the weight are unaffected by construction, so SM120 desktop parts (96–128 MiB L2) keep the default order exactly as before. A scalar gate cannot capture all of the structure — 17 of 66 cells still take the worse order, all by 5.8 % or less. And the default order is the noisy one here: its start-to-start spread is median 4.7 % and up to 26 %, against ~1.5 % for the swizzled order, which is why this needed four starts; at two starts the island candidates could not be separated at all.

Essential Elements

  • Purpose clearly explained
  • Test plan and test results included
  • Not a duplicate: gh pr list --search on scaled_mm_blockwise_sm120, max_swizzle_size, swizzle, blockwise fp8 sm120 — no open PR touches this dispatch
  • Model evaluation: not applicable, the swizzle only reorders CTAs and output is bit-identical (asserted on all 66 cells)
  • AI assistance was used for this change; every line was reviewed by me before pushing

cc @gau-nernst — this is the follow-up to the gate we simplified in #55180.

…n size too

vllm-project#55180 switched the tile scheduler to a swizzled CTA order whenever the weight
exceeds the L2. Above the L2 that order is not unconditionally better: on a GB10
(24 MiB L2) it loses to the default order across a band of middling activation
sizes and wins by up to 3.2x above it, so a weight-only gate leaves a measurable
amount on the table.

Measured with the swizzle forced both ways at the same shape, six shapes x
M = 1024..6144 in steps of 512 (66 cells), four starts, medians, bit-identical
on every cell. Summed percentage left on the table against the per-cell best:

  never swizzle                        732.7
  weight > L2                     (now) 118.8   worst cell  9.9 %
  + M*K >= 14 MiB                        54.0   worst cell  7.4 %
  + (M <= 2048 || M*K >= 14 MiB)         48.6   worst cell  8.7 %
  + (M <= 1024 || M*K >= 14 MiB)  (this) 35.3   worst cell  5.8 %

The worst cells for the weight-only gate are at M = 2560 (5120x5120 -10.4 %,
10240x2560 -9.4 %, 16384x2560 -6.4 %), which an earlier coarser sweep stepping
M by 2048 did not sample. The small-M island stops at 1024 rather than 2048
because it is K-dependent: at K = 2560 the swizzle wins at low M, at K = 5120
the default order does, so a wider island gives back 6.9-8.5 % on the 5120-wide
weights. The large wins the gate must not lose are all at an activation slab
above ~22 MiB (7168x5120 at M = 6144 is +223 %).

The kernel launch is unchanged on parts whose L2 holds the weight, so SM120
desktop parts (96-128 MiB L2) are unaffected, as before.

Tests: the two existing prefill cases are joined by M = 2560 and M = 1024 on
16384x2560, so all three arms of the gate are exercised.

Co-authored-by: Claude <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011SuBgdp87NbfLbiigmzn1z
Signed-off-by: Jürgen Schmied <juergenschmied70@gmail.com>

@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 pull request is from a fork — automated review is disabled. A repository maintainer can comment @claude review to run a one-time review.

@coderabbitai

coderabbitai Bot commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Team

Run ID: aed55e2e-aabb-450e-887c-ee792c957b93

📥 Commits

Reviewing files that changed from the base of the PR and between ed29dfa and 653cbcb.

📒 Files selected for processing (2)
  • csrc/libtorch_stable/quantization/w8a8/cutlass/c3x/scaled_mm_blockwise_sm120_fp8.cu
  • tests/kernels/quantization/test_cutlass_scaled_mm.py

Included review availability: Your plan provides up to 10 included reviews per hour; 9 remain after this review.


📝 Summary

Summary by CodeRabbit

  • Performance
    • Improved FP8 blockwise matrix multiplication tile scheduling by selecting the execution order based on activation dimensions and workload size.
    • Added coverage for additional prefill shapes, including large and small activation dimensions, to validate scheduling behavior across supported cases.

Walkthrough

The blockwise FP8 CUTLASS path now uses activation dimensions when selecting CTA swizzle order. The tests add prefill shapes that cover the activation-size gate, small-M gate, default ordering, and non-swap-AB dispatch.

Changes

FP8 swizzle gating

Layer / File(s) Summary
Activation-dependent swizzle selection
csrc/libtorch_stable/quantization/w8a8/cutlass/c3x/scaled_mm_blockwise_sm120_fp8.cu
The swizzle helper now checks m, k, and weight_bytes. The caller passes the activation dimensions.
Gate branch coverage
tests/kernels/quantization/test_cutlass_scaled_mm.py
Prefill cases now cover swizzling through the activation term, default ordering, swizzling through the small-M condition, and non-swap-AB dispatch.

Estimated code review effort: 2 (Simple) | ~10 minutes

Merge Risk: ⚪ Minimal · up to 653cb

This updates SM120 blockwise FP8 CTA ordering to account for activation size while preserving default ordering when weights fit in L2. The changed gate branches are covered by added prefill cases, with no remaining merge-readiness risk identified.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 3 functions across 2 files. 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 and concisely describes the main change: gating the SM 12.x blockwise FP8 CTA swizzle using activation size in addition to weight size.
Description check ✅ Passed The description directly explains the motivation, gate logic, benchmark results, validation, scope, and test plan for the changeset.
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.
  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch

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.

@gau-nernst

Copy link
Copy Markdown
Contributor

I don't think we will merge this. The new logic looks a bit "overfitting" to my taste. I don't quite understand your table but I think the speedup is not too significant -> likely not noticeable e2e.

@jschmied

jschmied commented Sep 7, 2026

Copy link
Copy Markdown
Contributor Author

Measured it end to end, and the objection was right — closing.

Server A/B, six arms, gate as the single variable (both arms route the blockwise GEMM through the same op, differing only in the swizzle the policy picks; one chunk per prompt so M is the prompt length, prefix cache disabled by construction, 3 starts per arm interleaved):

prompt gate / merged pick merged gate gate faster
2.5k 1 / 8 1.135 s 1.124 s +0.97 %
3k 1 / 8 1.291 s 1.280 s +0.85 %
4k 1 / 8 1.582 s 1.571 s +0.70 %
5k 1 / 8 1.869 s 1.857 s +0.64 %
7.5k 8 / 8 — identical launch 2.663 s 2.638 s +0.94 %

The 7.5k row is the control: both policies emit the same kernel, and it "improves" as much as every row where they differ. The whole signal is an arm-order offset, so the effect is zero within noise.

And that null is structural, not a sample-size problem. Over all 154 cells I swept, split by whether this gate changes the decision:

cells median difference between the two orders max
gate changes the decision 74 3.9 % 11.0 %
both policies agree 80 18.8 % 230.8 %

weight > L2 already picks correctly wherever the choice is worth a lot — every difference above +197 % is in the agree bucket. This gate only operates where the two orders are within ~4 % of each other, so even a perfect version of it cannot move a server metric. More starts would not change that.

On "overfitting": also correct. On 8 held-out shapes (including two K values absent from the tuning set) the small-M island contributes exactly zero — slab-only and island both score 236.3 against the weight-only rule's 299.1. Its entire benefit was confined to the data it was fitted on.

One thing worth separating out, since it is in merged code rather than this PR: 6144x4096 weighs 25,165,824 bytes and GB10's l2CacheSize is 25,165,824 bytes, so weight > l2 is false by one byte, the default raster is chosen, and at M=6144 that costs 138 % (69.5 vs 165.6 TF). That is a boundary question rather than a heuristic one, and I will sweep it properly before proposing anything.

Thanks for the quick and correct pushback.

@jschmied jschmied closed this Sep 7, 2026
@github-project-automation github-project-automation Bot moved this to Done in NVIDIA Sep 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants