Skip to content

perf(gemm): update mm_fp4 b12x SM120 NVFP4 dense GEMM kernel - #3560

Merged
bkryu merged 8 commits into
flashinfer-ai:mainfrom
yichengj0:b12x-new-fp4-gemm
Jun 11, 2026
Merged

bkryu merged 8 commits into
flashinfer-ai:mainfrom
yichengj0:b12x-new-fp4-gemm

Conversation

@yichengj0

@yichengj0 yichengj0 commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

📌 Description

Summary

This PR refreshes the b12x dense FP4 GEMM kernel to match the latest upstream for better performance.

No public API change. The auto routing heuristic (_heuristic_func_mm_fp4) is unchanged — b12x is preferred only at SM120, not SM121.

Addresses #3517 [Perf][SM12x] Update mm_fp4(backend='b12x') kernels.

Motivation

@lukealonso has made performance improvements to b12x dense GEMMs; we'd like to port over the improvements to FlashInfer. Gains are focused on small-M.

Benchmarks: new b12x vs old b12x

Setup per SKU: benchmarks/flashinfer_benchmark.py --testlist <104 nvfp4 shapes> --use_nvfp4 --use_128x4_sf_layout --refcheck, 30 iters / 5 warmup, CUPTI timing (harness default). 104 shapes = 13 token counts m∈{1..4096} × 8 (n,k) layer shapes taken from real DeepSeek-R1 and Llama-3 model layers, all K%128==0.

The refresh is a consistent small win at small m (where swap_ab and expected_m help most) and break-even-to-positive overall — no SKU regresses in aggregate.

Geomean speedup (old_time / new_time; >1 = new faster), grouped by m:

SKU overall m≤8 16≤m≤128 m≥256
RTX 5080 (SM120) 1.003× 1.019× 1.003× 0.991×
RTX PRO 6000 (SM120) 1.021× 1.060× 1.010× 1.000×
DGX Spark / GB10 (SM121) 1.024× 1.021× 1.028× 1.022×

The win is largest at small m on PRO 6000 / Spark and break-even on the 5080. A few n=4096 k=4096/5376 shapes regress ≤5% (bolded in the per-shape tables); the PRO 6000 has a sharper 8192×2560 regression at medium m (down to 0.873×), which is SKU-specific and flagged as a follow-up.

Full per-shape new-vs-old tables (all 104 shapes, sorted by n, k, m) per SKU:

RTX 5080 (SM120)
n k m old (us) new (us) new TFLOPs speedup
512 7168 1 37.6 39.8 0.2 0.945x
512 7168 2 38.3 38.8 0.4 0.986x
512 7168 4 38.8 38.9 0.8 0.995x
512 7168 8 38.9 39.2 1.5 0.994x
512 7168 16 39.4 40.4 2.9 0.975x
512 7168 32 40.6 41.2 5.7 0.985x
512 7168 64 43.8 43.5 10.8 1.007x
512 7168 128 55.1 55.6 16.9 0.992x
512 7168 256 67.4 67.7 27.8 0.995x
512 7168 512 84.0 84.7 44.4 0.991x
512 7168 1024 94.4 95.9 78.4 0.984x
512 7168 2048 104.4 106.0 141.9 0.985x
512 7168 4096 165.4 168.2 178.7 0.983x
1024 8192 1 58.8 59.1 0.3 0.995x
1024 8192 2 58.7 59.0 0.6 0.995x
1024 8192 4 59.7 59.7 1.1 0.999x
1024 8192 8 60.2 59.9 2.2 1.006x
1024 8192 16 61.1 61.1 4.4 1.001x
1024 8192 32 60.8 60.9 8.8 0.998x
1024 8192 64 70.1 69.7 15.4 1.006x
1024 8192 128 78.5 77.4 27.7 1.014x
1024 8192 256 85.9 85.8 50.1 1.002x
1024 8192 512 96.5 97.0 88.6 0.995x
1024 8192 1024 97.3 97.5 176.2 0.998x
1024 8192 2048 154.2 155.8 220.5 0.990x
1024 8192 4096 251.2 254.7 269.8 0.986x
2560 8192 1 101.1 87.6 0.5 1.154x
2560 8192 2 101.1 91.2 0.9 1.108x
2560 8192 4 101.5 93.7 1.8 1.083x
2560 8192 8 101.9 95.1 3.5 1.072x
2560 8192 16 102.3 100.1 6.7 1.022x
2560 8192 32 101.6 95.3 14.1 1.067x
2560 8192 64 101.2 95.6 28.1 1.058x
2560 8192 128 95.3 94.4 56.9 1.009x
2560 8192 256 109.3 106.8 100.5 1.023x
2560 8192 512 113.9 114.1 188.2 0.998x
2560 8192 1024 157.3 158.3 271.3 0.994x
2560 8192 2048 252.6 254.9 337.0 0.991x
2560 8192 4096 433.5 436.9 393.2 0.992x
4096 4096 1 57.2 57.6 0.6 0.993x
4096 4096 2 57.3 56.4 1.2 1.016x
4096 4096 4 58.1 59.3 2.3 0.981x
4096 4096 8 56.5 57.5 4.7 0.983x
4096 4096 16 59.6 60.2 8.9 0.990x
4096 4096 32 60.3 60.4 17.8 1.000x
4096 4096 64 57.1 59.2 36.3 0.964x
4096 4096 128 69.9 72.8 59.0 0.960x
4096 4096 256 61.2 65.6 130.8 0.933x
4096 4096 512 112.8 113.9 150.8 0.991x
4096 4096 1024 151.8 152.3 225.7 0.997x
4096 4096 2048 220.7 223.1 308.0 0.989x
4096 4096 4096 366.1 376.4 365.2 0.973x
4096 5376 1 72.6 71.3 0.6 1.017x
4096 5376 2 72.5 70.5 1.2 1.028x
4096 5376 4 76.2 75.7 2.3 1.006x
4096 5376 8 75.7 75.2 4.7 1.007x
4096 5376 16 76.1 78.2 9.0 0.973x
4096 5376 32 83.4 85.8 16.4 0.971x
4096 5376 64 77.2 81.4 34.6 0.948x
4096 5376 128 84.4 88.6 63.6 0.953x
4096 5376 256 84.6 86.6 130.2 0.978x
4096 5376 512 133.9 134.5 167.6 0.995x
4096 5376 1024 178.8 180.3 250.1 0.991x
4096 5376 2048 262.6 265.2 340.2 0.990x
4096 5376 4096 464.7 470.1 383.7 0.988x
4096 14336 1 176.1 174.6 0.7 1.009x
4096 14336 2 176.0 175.2 1.3 1.005x
4096 14336 4 176.9 176.7 2.7 1.001x
4096 14336 8 177.6 177.5 5.3 1.000x
4096 14336 16 178.5 178.6 10.5 1.000x
4096 14336 32 178.6 178.9 21.0 0.999x
4096 14336 64 180.0 180.3 41.7 0.998x
4096 14336 128 182.3 182.7 82.3 0.998x
4096 14336 256 194.4 195.0 154.1 0.997x
4096 14336 512 253.2 254.7 236.1 0.994x
4096 14336 1024 375.2 377.6 318.5 0.994x
4096 14336 2048 580.2 585.3 410.9 0.991x
4096 14336 4096 1049.4 1061.3 453.3 0.989x
8192 2560 1 70.1 68.4 0.6 1.025x
8192 2560 2 66.1 65.4 1.3 1.010x
8192 2560 4 65.3 65.2 2.6 1.001x
8192 2560 8 67.5 67.0 5.0 1.008x
8192 2560 16 67.9 65.2 10.3 1.042x
8192 2560 32 68.2 66.1 20.3 1.032x
8192 2560 64 67.1 67.8 39.6 0.989x
8192 2560 128 69.3 69.6 77.2 0.996x
8192 2560 256 89.8 91.6 117.3 0.981x
8192 2560 512 137.4 137.1 156.6 1.002x
8192 2560 1024 176.3 177.1 242.5 0.995x
8192 2560 2048 279.4 280.6 306.1 0.996x
8192 2560 4096 489.2 495.3 346.9 0.988x
14336 4096 1 171.5 162.8 0.7 1.053x
14336 4096 2 171.7 162.1 1.4 1.059x
14336 4096 4 171.5 162.0 2.9 1.059x
14336 4096 8 171.9 163.3 5.8 1.053x
14336 4096 16 171.8 162.9 11.5 1.054x
14336 4096 32 173.4 165.2 22.7 1.049x
14336 4096 64 176.7 167.4 44.9 1.055x
14336 4096 128 181.3 180.4 83.3 1.005x
14336 4096 256 188.7 190.3 158.0 0.992x
14336 4096 512 253.7 255.0 235.8 0.995x
14336 4096 1024 382.4 384.3 312.9 0.995x
14336 4096 2048 620.9 624.4 385.2 0.994x
14336 4096 4096 1140.7 1148.9 418.7 0.993x
RTX PRO 6000 (SM120)
n k m old (us) new (us) new TFLOPs speedup
512 7168 1 22.3 21.7 0.3 1.026x
512 7168 2 22.4 22.5 0.7 0.994x
512 7168 4 22.4 22.5 1.3 0.998x
512 7168 8 22.4 22.5 2.6 0.995x
512 7168 16 22.4 22.4 5.2 0.999x
512 7168 32 22.3 22.5 10.4 0.991x
512 7168 64 22.4 22.7 20.7 0.991x
512 7168 128 22.9 23.1 40.6 0.992x
512 7168 256 32.6 32.8 57.2 0.992x
512 7168 512 35.7 35.9 104.8 0.996x
512 7168 1024 41.5 41.7 180.1 0.995x
512 7168 2048 45.6 47.1 319.2 0.969x
512 7168 4096 51.4 53.6 561.2 0.959x
1024 8192 1 25.6 25.8 0.7 0.993x
1024 8192 2 25.6 25.8 1.3 0.994x
1024 8192 4 25.7 25.9 2.6 0.994x
1024 8192 8 25.7 25.9 5.2 0.992x
1024 8192 16 25.7 25.8 10.4 0.994x
1024 8192 32 25.6 25.8 20.8 0.993x
1024 8192 64 25.8 25.9 41.4 0.995x
1024 8192 128 27.0 28.4 75.6 0.951x
1024 8192 256 39.0 39.6 108.4 0.985x
1024 8192 512 43.1 43.5 197.6 0.991x
1024 8192 1024 47.4 49.5 347.4 0.959x
1024 8192 2048 50.2 50.0 687.0 1.004x
1024 8192 4096 89.5 90.9 756.3 0.985x
2560 8192 1 34.5 31.6 1.3 1.092x
2560 8192 2 34.6 31.7 2.6 1.092x
2560 8192 4 34.7 31.8 5.3 1.090x
2560 8192 8 34.6 32.1 10.5 1.079x
2560 8192 16 34.9 32.7 20.5 1.069x
2560 8192 32 34.9 32.8 40.9 1.065x
2560 8192 64 35.2 33.3 80.7 1.059x
2560 8192 128 41.7 39.9 134.7 1.046x
2560 8192 256 48.2 39.9 269.1 1.207x
2560 8192 512 48.8 48.6 442.2 1.004x
2560 8192 1024 50.6 51.3 837.6 0.987x
2560 8192 2048 87.5 88.2 973.8 0.992x
2560 8192 4096 163.8 166.9 1029.4 0.982x
4096 4096 1 23.6 22.4 1.5 1.053x
4096 4096 2 23.6 22.7 3.0 1.038x
4096 4096 4 23.7 22.8 5.9 1.041x
4096 4096 8 23.7 23.2 11.6 1.021x
4096 4096 16 23.8 23.2 23.2 1.029x
4096 4096 32 24.0 23.6 45.5 1.020x
4096 4096 64 24.0 23.8 90.2 1.009x
4096 4096 128 31.9 31.2 137.7 1.022x
4096 4096 256 35.2 35.2 244.0 1.000x
4096 4096 512 36.8 36.7 467.7 1.001x
4096 4096 1024 54.3 54.5 630.0 0.995x
4096 4096 2048 73.0 73.9 930.3 0.988x
4096 4096 4096 134.6 137.0 1003.4 0.983x
4096 5376 1 30.9 28.9 1.5 1.068x
4096 5376 2 30.8 28.8 3.1 1.069x
4096 5376 4 30.8 29.4 6.0 1.046x
4096 5376 8 30.8 29.2 12.1 1.055x
4096 5376 16 30.7 29.5 23.9 1.042x
4096 5376 32 31.0 29.9 47.1 1.035x
4096 5376 64 31.1 30.0 93.9 1.037x
4096 5376 128 37.6 37.0 152.5 1.018x
4096 5376 256 41.4 38.3 294.7 1.083x
4096 5376 512 43.2 43.2 522.2 0.999x
4096 5376 1024 66.2 66.3 680.7 0.999x
4096 5376 2048 92.4 92.4 975.8 0.999x
4096 5376 4096 166.6 169.3 1065.2 0.984x
4096 14336 1 64.4 60.0 2.0 1.072x
4096 14336 2 64.3 59.8 3.9 1.075x
4096 14336 4 64.4 60.3 7.8 1.067x
4096 14336 8 64.4 60.2 15.6 1.070x
4096 14336 16 64.3 60.4 31.1 1.064x
4096 14336 32 64.4 60.9 61.7 1.059x
4096 14336 64 64.5 61.3 122.6 1.053x
4096 14336 128 65.3 62.8 239.2 1.040x
4096 14336 256 77.5 65.2 460.9 1.188x
4096 14336 512 80.7 82.3 730.7 0.981x
4096 14336 1024 139.4 140.6 855.5 0.992x
4096 14336 2048 206.0 206.8 1163.0 0.996x
4096 14336 4096 395.7 398.7 1206.5 0.992x
8192 2560 1 22.2 22.0 1.9 1.012x
8192 2560 2 21.5 20.7 4.1 1.043x
8192 2560 4 21.4 20.4 8.2 1.049x
8192 2560 8 21.9 21.7 15.5 1.008x
8192 2560 16 21.2 21.7 30.9 0.977x
8192 2560 32 21.6 23.6 56.9 0.915x
8192 2560 64 24.6 26.8 100.2 0.917x
8192 2560 128 25.8 29.7 180.9 0.868x
8192 2560 256 29.1 29.6 362.2 0.981x
8192 2560 512 43.0 42.8 501.7 1.005x
8192 2560 1024 54.7 55.2 777.4 0.989x
8192 2560 2048 93.2 93.7 916.9 0.994x
8192 2560 4096 163.2 167.5 1025.7 0.974x
14336 4096 1 49.1 40.3 2.9 1.218x
14336 4096 2 48.3 39.1 6.0 1.235x
14336 4096 4 49.2 39.9 11.8 1.231x
14336 4096 8 47.8 39.5 23.8 1.211x
14336 4096 16 46.0 39.7 47.3 1.158x
14336 4096 32 44.5 43.1 87.3 1.033x
14336 4096 64 40.7 44.2 170.1 0.922x
14336 4096 128 43.4 45.3 332.0 0.959x
14336 4096 256 59.5 60.7 495.0 0.979x
14336 4096 512 80.0 79.7 754.6 1.004x
14336 4096 1024 116.2 116.5 1032.0 0.997x
14336 4096 2048 212.8 214.3 1122.6 0.993x
14336 4096 4096 390.8 395.2 1217.2 0.989x
DGX Spark / GB10 (SM121)
n k m old (us) new (us) new TFLOPs speedup
512 7168 1 31.1 31.3 0.2 0.991x
512 7168 2 31.5 31.4 0.5 1.003x
512 7168 4 31.6 31.1 0.9 1.016x
512 7168 8 31.6 31.5 1.9 1.005x
512 7168 16 31.7 31.8 3.7 0.997x
512 7168 32 32.5 33.7 7.0 0.964x
512 7168 64 34.1 33.8 13.9 1.009x
512 7168 128 49.0 39.3 23.9 1.245x
512 7168 256 60.7 49.5 38.0 1.227x
512 7168 512 56.8 56.3 66.7 1.008x
512 7168 1024 73.1 73.6 102.2 0.994x
512 7168 2048 129.0 129.0 116.5 1.000x
512 7168 4096 227.0 225.8 133.1 1.005x
1024 8192 1 65.4 59.8 0.3 1.094x
1024 8192 2 64.7 60.2 0.6 1.075x
1024 8192 4 65.9 59.5 1.1 1.108x
1024 8192 8 64.1 60.5 2.2 1.058x
1024 8192 16 63.6 57.1 4.7 1.114x
1024 8192 32 66.5 58.5 9.2 1.136x
1024 8192 64 55.5 58.1 18.5 0.956x
1024 8192 128 72.0 64.4 33.3 1.119x
1024 8192 256 83.1 79.1 54.3 1.050x
1024 8192 512 81.5 84.5 101.6 0.964x
1024 8192 1024 155.6 130.2 132.0 1.195x
1024 8192 2048 200.4 191.2 179.7 1.048x
1024 8192 4096 456.5 410.5 167.4 1.112x
2560 8192 1 117.8 118.0 0.4 0.998x
2560 8192 2 116.2 119.2 0.7 0.975x
2560 8192 4 118.0 118.2 1.4 0.998x
2560 8192 8 116.2 119.4 2.8 0.973x
2560 8192 16 118.2 118.5 5.7 0.998x
2560 8192 32 117.1 119.8 11.2 0.977x
2560 8192 64 117.7 120.7 22.2 0.975x
2560 8192 128 119.6 124.2 43.2 0.963x
2560 8192 256 124.7 128.5 83.6 0.970x
2560 8192 512 151.2 146.9 146.1 1.029x
2560 8192 1024 212.2 206.8 207.6 1.026x
2560 8192 2048 346.6 337.4 254.6 1.027x
2560 8192 4096 951.6 861.4 199.4 1.105x
4096 4096 1 94.2 98.6 0.3 0.956x
4096 4096 2 113.4 99.0 0.7 1.146x
4096 4096 4 111.6 101.8 1.3 1.096x
4096 4096 8 113.1 97.4 2.8 1.161x
4096 4096 16 113.5 101.6 5.3 1.117x
4096 4096 32 113.6 99.6 10.8 1.141x
4096 4096 64 112.5 100.6 21.3 1.119x
4096 4096 128 111.9 99.2 43.3 1.128x
4096 4096 256 107.3 108.4 79.3 0.991x
4096 4096 512 115.5 112.7 152.5 1.025x
4096 4096 1024 158.6 158.4 216.9 1.001x
4096 4096 2048 281.3 272.3 252.3 1.033x
4096 4096 4096 508.9 494.9 277.7 1.028x
4096 5376 1 117.4 118.8 0.4 0.988x
4096 5376 2 116.6 117.4 0.8 0.994x
4096 5376 4 117.0 118.4 1.5 0.989x
4096 5376 8 116.3 117.7 3.0 0.988x
4096 5376 16 118.6 118.8 5.9 0.998x
4096 5376 32 116.5 118.3 11.9 0.985x
4096 5376 64 117.8 119.1 23.7 0.989x
4096 5376 128 118.7 119.9 47.0 0.991x
4096 5376 256 129.1 132.6 85.0 0.973x
4096 5376 512 139.8 142.2 158.6 0.983x
4096 5376 1024 200.0 197.5 228.3 1.012x
4096 5376 2048 338.3 340.3 265.1 0.994x
4096 5376 4096 693.6 645.1 279.6 1.075x
4096 14336 1 231.6 231.8 0.5 0.999x
4096 14336 2 229.6 229.4 1.0 1.001x
4096 14336 4 232.6 232.5 2.0 1.000x
4096 14336 8 232.1 228.7 4.1 1.015x
4096 14336 16 230.7 232.7 8.1 0.991x
4096 14336 32 230.2 228.7 16.4 1.007x
4096 14336 64 231.9 232.7 32.3 0.997x
4096 14336 128 234.1 234.2 64.2 1.000x
4096 14336 256 266.6 265.2 113.4 1.005x
4096 14336 512 303.0 302.6 198.7 1.001x
4096 14336 1024 469.2 466.4 257.9 1.006x
4096 14336 2048 902.7 880.0 273.3 1.026x
4096 14336 4096 3847.1 3765.8 127.7 1.022x
8192 2560 1 113.6 113.0 0.4 1.005x
8192 2560 2 111.7 113.4 0.7 0.986x
8192 2560 4 113.2 113.3 1.5 0.999x
8192 2560 8 113.2 113.8 2.9 0.995x
8192 2560 16 114.2 113.5 5.9 1.006x
8192 2560 32 113.6 114.5 11.7 0.993x
8192 2560 64 114.2 115.4 23.3 0.990x
8192 2560 128 116.4 119.2 45.0 0.976x
8192 2560 256 119.7 121.7 88.2 0.984x
8192 2560 512 135.1 139.3 154.2 0.970x
8192 2560 1024 200.1 199.5 215.3 1.003x
8192 2560 2048 370.9 371.6 231.1 0.998x
8192 2560 4096 644.3 641.7 267.7 1.004x
14336 4096 1 232.3 226.1 0.5 1.028x
14336 4096 2 232.5 226.5 1.0 1.026x
14336 4096 4 231.8 225.8 2.1 1.026x
14336 4096 8 232.2 226.7 4.1 1.024x
14336 4096 16 232.7 226.7 8.3 1.026x
14336 4096 32 232.1 226.4 16.6 1.025x
14336 4096 64 233.3 229.0 32.8 1.019x
14336 4096 128 235.8 232.5 64.7 1.014x
14336 4096 256 246.5 239.6 125.5 1.029x
14336 4096 512 293.4 287.1 209.4 1.022x
14336 4096 1024 441.3 441.2 272.6 1.000x
14336 4096 2048 838.2 836.7 287.5 1.002x
14336 4096 4096 1500.5 1506.2 319.4 0.996x

🔍 Related Issues

Closes #3517.

🚀 Pull Request Checklist

✅ Pre-commit Checks

  • I have installed pre-commit by running pip install pre-commit (or used your preferred method).
  • I have installed the hooks with pre-commit install.
  • I have run the hooks manually with pre-commit run --all-files and fixed any reported issues.

🧪 Tests

  • Tests have been added or updated as needed. (covered by existing tests/gemm/test_mm_fp4.py -k b12x)
  • All tests are passing — pytest -k b12x 396/0; 104-shape --refcheck 104/104 on 5080 / PRO 6000 / GB10.

Reviewer Notes

  • The vendored kernel is not a verbatim upstream copy: it carries backported scale-factor helpers so it builds on public cutlass-dsl 4.5.2 (pure upstream b12x needs an internal/newer cutlass-dsl), and the MXFP8 path is pruned (unsupported there).
  • The SFB-layout N-divisibility assertion in cute_dsl/utils.py is relaxed 64→16 (upstream commit 0daa6ab) to admit the narrow-N swap_ab tiles. This is safe: it only loosens a precondition (every previously-valid N%64==0 tile still passes), the SFB smem layout already rounds N up to a full 128-wide block so the layout is unchanged for existing tiles, and the newly-admitted narrow tiles pass --refcheck.
  • swap_ab here is b12x's device-internal transpose (public C stays row-major) — not the SM100 operand-swap/FFI-transpose convention. The shared _compile_block_scaled_gemm harness used by Sm100/Sm103 is left untouched.

Summary by CodeRabbit

  • Improvements
    • Relaxed a tile-shape constraint to allow more flexible tensor layouts.
    • Deterministic plan selection for low‑precision GEMM with a smaller, predictable candidate set.
    • Earlier, clearer validation for incompatible contraction dimensions to surface errors sooner.
    • Consistent operand orientation and compilation caching to reduce surprises in kernel selection and runtime behavior.

yichengj0 and others added 6 commits June 9, 2026 20:34
…plan

Update the vendored b12x SM120 block-scaled dense GEMM (used by
mm_fp4(backend="b12x") and the SM12x MoE kernels) to the latest upstream
b12x dense.py, and route the dense FP4 default path through upstream's
regime-aware plan selection.

Kernel (dense_blockscaled_gemm_sm120_b12x.py):
- Replace the vendored kernel with upstream b12x dense.py (verbatim), repoint
  the utils import to flashinfer.cute_dsl.utils, and re-add the FlashInfer
  integration shims (@cute.jit `wrapper`, `Sm120B12xBlockScaledDenseGemmKernel`
  alias) that gemm_base / the MoE kernels import.
- Backport 6 SF-helper methods (_thrfrg_SFA/B, _get_layoutSFA/B_TV,
  _partition_fragment_SFA/B) to their inline implementations: upstream delegates
  to newer cutlass.utils.blackwell_helpers APIs not present in the pinned
  nvidia-cutlass-dsl 4.5.2. These are exactly the methods the SM12x MoE kernels
  reuse as a mixin, so MoE keeps working unchanged.

Runner (gemm_base.py):
- Fix the ctor/can_implement calls for upstream's diverged signatures (ctor
  args by keyword since mma_k/tile_k were inserted before use_prefetch;
  can_implement is now M-independent).
- Default (non-autotune) path now selects the tile via upstream's
  _select_default_dense_gemm_plan with expected_m=m, giving the regime-optimal
  tile (incl. the 16x64 M=1 decode tile). swap_ab is requested by the plan only
  for narrow-N tiles; since the transposed-store epilogue is not yet wired in
  this wrapper, the runner falls back to a safe non-swap tile in that case.

utils.py:
- Relax sm120_make_smem_layout_sfb's N divisibility 64 -> 16 (upstream commit
  0daa6ab) so narrow-N tiles are representable once swap_ab is supported.

Results on RTX 5080 (SM120), mm_fp4 nvfp4: correctness unchanged (pytest -k
b12x 396/0, SM12x MoE passing); decode (m<=8) default path up to 1.15x, overall
geomean ~1.00, no static regressions.

Not enabled (follow-up): swap_ab narrow-N tiles + cp.async load_path require
porting upstream's transposed-store epilogue / load-path handling into the
FlashInfer wrapper (enabling swap_ab as-is produces incorrect results). That is
the remaining FP4 upside.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The kernel was copied verbatim from upstream b12x, which dragged in a whole
launch layer that FlashInfer never uses (it drives the kernel via
_compile_block_scaled_gemm + the `wrapper` cute entry, not via dense_gemm).
That dead code also (a) registered `b12x::dense_gemm_launch` torch custom ops,
polluting the global `b12x::` operator namespace and colliding/shadowing with
the real b12x package when both are imported in one process, and (b) kept
`from b12x.* import ...`, making FlashInfer depend on the external b12x package
being installed.

Remove the unused launch layer (~1000 lines): the `b12x::dense_gemm_launch`
and `..._functional` custom-op + fake registrations, `_DenseGemmLaunch`,
`dense_gemm`, `_get_compiled_dense_gemm`, `_dense_gemm_launch_flat`, the
`_DenseGemmPolicy`/`_max_active_clusters_for`/`_dense_gemm_policy_for` helpers,
`_cached_alpha_one`, `_empty_dense_gemm_output`, and the Triton split-K
finalize kernels. None are referenced by the FlashInfer runner, the SM12x MoE
kernels, or the kept `_select_default_dense_gemm_plan` plan path.

Drop the now-unused imports (triton, b12x.cute.compiler, b12x.cute.runtime_control)
and vendor the 3 tiny `b12x.cute.fp4` helpers (get_ptr_as_int64,
scatter_add_bf16, scatter_add_bf16x2 — used only by the opt-in split-K atomic
path) so the file has NO `from b12x` imports.

Result: `import flashinfer.gemm` no longer pulls in the b12x package
(`b12x not in sys.modules`) and no longer registers `b12x::dense_gemm_launch`,
so there is no global-namespace collision with the b12x package. mm_fp4 b12x
correctness unchanged (pytest -k b12x: 396 passed, 0 failed). File shrinks
3655 -> 2648 lines.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
FlashInfer only drives the vendored b12x SM120 dense GEMM kernel for
NVFP4 (mm_fp4 backend="b12x") and the SM12x MoE, never for MXFP8 dense
GEMM. Remove the dead MXFP8 warp-MMA path so the file is FP4-only:

- _setup_attributes: collapse the FP8/FP4 mma_op branch to MmaMXF4NVF4Op
  and drop the cute.nvgpu.warp.MmaMXF8Op reference (which does not exist
  in the public cutlass-dsl 4.5.2 build; it was only reachable via a
  const_expr branch that is always False for FP4).
- can_implement: reject non-Float4E2M1FN up front; remove the FP8 tile
  and tile_k branches.
- _select_default_mma_tiler_mn / _select_default_dense_gemm_plan: drop
  the is_mxfp8 parameter and both MXFP8 tile regimes.
- gemm_base.py: drop the is_mxfp8=False argument at the plan call site.

The surviving Float8E4M3FN references are the NVF4 scale-factor dtype,
which is unrelated to the removed MXFP8 AB-dtype path. The SM12x MoE
kernels borrow only the layout/SF helper methods, none of which touch
the FP8 path, so they are unaffected.

Verified on RTX 5080 (SM120): tests/gemm/test_mm_fp4.py -k b12x
396 passed / 0 failed; tests/moe/test_b12x_fused_moe.py 95 passed.

AI-assisted with Claude Code.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Three changes to the SM120 NVFP4 b12x dense GEMM integration:

1. Enable the device-internal swap_ab path (narrow-N decode tiles, e.g.
   64x32). The transposed-store epilogue was already vendored from
   upstream b12x; the prior garbage output was a C-layout wiring bug, not
   a missing epilogue. Fix: the wrapper reconstructs C row-major (m,n)
   unconditionally; the runner honors plan.swap_ab and builds the kernel
   with the real swap_ab (carried in cache_key), but passes swap_ab=False
   to the SHARED compile harness -- b12x's swap is device-internal, not
   the SM100 operand-swap / FFI-transpose convention -- and passes plain
   `out`. This leaves the shared harness and the SM100/SM103 swap_ab path
   untouched. Perf is ~neutral on RTX 5080 (m=1 decode is launch/latency-
   bound); the win upstream reports needs a larger SM120 part.

2. Guard unsupported K in _b12x_gemm_fp4_requirement: the NVFP4 kernel
   requires K % 128 == 0 (tile_k = sf_vec_size*8), but the default launch
   path does not call can_implement. Enforcing K at the backend gate makes
   b12x fall back to cutlass/cudnn under backend="auto" for unsupported K,
   and raise a clear error under explicit backend="b12x", instead of
   silently launching a kernel that drops the K-tail.

3. Cleanup (from /simplify): delete the now-dead
   _select_default_sm120_mma_tiler (the runner switched to
   _select_default_dense_gemm_plan) and drop the no-op kernel_m/kernel_n
   aliases.

Verified on RTX 5080 (SM120): tests/gemm/test_mm_fp4.py -k b12x 396/0;
swap_ab narrow-N probe cos_sim > 0.97; K-guard rejects explicit b12x and
falls back under auto (cos_sim 0.99). Port audited vs upstream b12x and
correctness-reviewed (only finding was the K-gap, fixed here).

AI-assisted with Claude Code.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
get_valid_tactics previously offered only 4 generic swap_ab-free tiles, so
enabling autotune could regress BELOW the untuned default: the deterministic
plan's regime tiles -- notably the m=1 narrow-N (64,32)+swap_ab decode tile --
were absent from the candidate set, so for those shapes the tuner cached a
generic tile and the swap tile became unreachable under tuning.

Append the plan's pick to get_valid_tactics so the candidate set always
contains the exact default-path tile: autotuning is now monotonic vs the static
default (never worse) and the tuner picks up swap_ab on SKUs where it wins.
Factor the plan computation into a shared _default_dense_plan() helper used by
both forward()'s default path and get_valid_tactics(), making the "candidate
set contains the default tile" invariant structural rather than a convention
two call sites must keep in sync.

Also add a TODO at _heuristic_func_mm_fp4: b12x is supported on SM121
(@supported_compute_capability([120, 121])) but `auto` only routes it at SM120;
widen to major == 12 once the DGX Spark bench confirms b12x wins there.

Verified on RTX 5080 (SM120): autotune path cos_sim > 0.97 incl. narrow-N swap;
tests/gemm/test_mm_fp4.py -k b12x 396/0. Reviewed via /code-review (no
correctness findings; the m=1-bucket-only property makes the swap tile safe).

AI-assisted with Claude Code.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Remove 12 unused imports, rename unused k_tile loop vars to _k_tile, and
add # type: ignore[call-overload] on the CuTe-DSL range(..., unroll=) calls
(matching the sibling blackwell_sm12x/moe_*_kernel.py convention). ruff-format
normalization of the vendored file. No behavior change (b12x refcheck passes).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 10, 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

Relax SM120 SMEM N-dimension divisibility and refactor the SM120 b12x FP4 GEMM path: add deterministic dense-plan selection, validate packed-K (real_k % 128), revise tactic enumeration/defaulting, and centralize swap_ab handling in the kernel constructor.

Changes

SM120 b12x FP4 GEMM Refactoring

Layer / File(s) Summary
Tile Shape Constraint Relaxation for Narrower-N FP4
flashinfer/cute_dsl/utils.py
Divisibility requirement for tile_shape_mnk[1] changed from blk_mn // 2 to blk_mn // 8 and assertion message updated.
Deterministic Default Plan Mechanism
flashinfer/gemm/gemm_base.py
Import _select_default_dense_gemm_plan and add _default_dense_plan(m,n,real_k,device) wrapper; remove legacy SM120 default tiler helper.
FP4 Requirement Validation and K-Dimension Check
flashinfer/gemm/gemm_base.py
Compute real_k = a.shape[1] * 2 for FP4 packing and require real_k % 128 == 0; return False for non-"b12x" backends and raise ValueError for explicit "b12x" when invalid.
Tactic Enumeration and Default Selection
flashinfer/gemm/gemm_base.py
Rework get_valid_tactics() to use M-independent can_implement, restrict tuner candidates to a small fixed tile set, ensure deterministic plan's (mma_tiler_mn, swap_ab) is included, and derive default tactic from _default_dense_plan.
Kernel Launch and Swap Convention Refactoring
flashinfer/gemm/gemm_base.py
Always use kernel_a = a and kernel_b = b.T, pass swap_ab into Sm120B12xBlockScaledDenseGemmKernel constructor via kwarg, force _compile_block_scaled_gemm(..., swap_ab=False, ...) for compilation caching, and add comments clarifying out shape and SM121 heuristic note.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

Possibly related PRs

Suggested labels

run-ci

Suggested reviewers

  • bkryu
  • yzh119
  • aleozlx
  • dhiraj113
  • nv-yunzheq
  • cyx-6
  • sricketts
  • yongwww

"I hopped through tiles both wide and slim,
Plans chosen steady, not on whim,
swap tucked gently into kernel's care,
real_k counted, exact and fair,
The GEMM field hums with tuned delight."

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% 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 PR title 'perf(gemm): update mm_fp4 b12x SM120 NVFP4 dense GEMM kernel' accurately summarizes the main change: updating the b12x FP4 dense GEMM kernel for performance improvements.
Linked Issues check ✅ Passed The PR directly addresses issue #3517 by integrating upstream b12x FP4 dense GEMM kernel improvements with validated correctness and performance benchmarks across multiple SKUs.
Out of Scope Changes check ✅ Passed All changes are directly scoped to the b12x FP4 kernel update objective: updates to cute_dsl/utils.py assertion, gemm_base.py b12x path, and related tiling/swap logic, with no unrelated modifications.
Description check ✅ Passed PR description is comprehensive and well-structured, addressing all required template sections with detailed context, benchmarks, and implementation notes.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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 and usage tips.

@yichengj0 yichengj0 changed the title perf(gemm): update mm_fp4 b12x SM120 NVFP4 kernel + swap_ab narrow-N tiles perf(gemm): update mm_fp4 b12x SM120 NVFP4 dense GEMM kernel Jun 10, 2026

@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 refactors the SM120 block-scaled FP4 GEMM implementation to support narrow-N FP4 tiles and swap_ab. It relaxes layout divisibility constraints, removes the old tiler selection function, and introduces a deterministic, regime-aware planning helper to handle tile selection and swap_ab. It also enforces a contraction dimension constraint (K must be a multiple of 128) in the requirements check. A review comment points out a potential NameError in the newly added requirements check because sf_vec_size is referenced in the error message but is not defined in that scope.

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/gemm/gemm_base.py

@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: 2

🤖 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/gemm/gemm_base.py`:
- Around line 5473-5483: The check computing real_k = a.shape[1] * 2 currently
raises for any real_k % 128 != 0 and thus blocks mm_fp4(..., backend="auto")
from falling back to CUTLASS/cuDNN; change the guard so it returns False for
non-explicit b12x selections and only raises when the caller explicitly
requested backend == "b12x". Concretely, after computing real_k, if real_k % 128
!= 0 then: if backend != "b12x" return False (so the fallback path can try other
backends), otherwise raise the existing ValueError with the same message; keep
references to real_k and a as-is to locate the code.
- Around line 5949-5958: Replace the zero-arg lambda assigned to make_kernel
with a proper local function definition to satisfy Ruff E731: define a def
make_kernel(): that returns Sm120B12xBlockScaledDenseGemmKernel(...) using the
same positional args sf_vec_size, mma_tiler_mn, cluster_shape_mn and the keyword
args use_prefetch=use_prefetch, enable_pdl=enable_pdl, swap_ab=swap_ab so the
binding remains identical to the original lambda.
🪄 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: c26c7ceb-7e8f-47f5-8ba3-53e113fe0c78

📥 Commits

Reviewing files that changed from the base of the PR and between c4eef41 and a9815df.

📒 Files selected for processing (3)
  • flashinfer/cute_dsl/utils.py
  • flashinfer/gemm/gemm_base.py
  • flashinfer/gemm/kernels/dense_blockscaled_gemm_sm120_b12x.py

Comment thread flashinfer/gemm/gemm_base.py Outdated
Comment thread flashinfer/gemm/gemm_base.py Outdated
Comment on lines 5949 to 5958
# NOTE: upstream b12x ctor inserts mma_k/tile_k/single_work_tile_per_cta
# before use_prefetch, so pass these by keyword to avoid mis-binding.
make_kernel = lambda: Sm120B12xBlockScaledDenseGemmKernel(
sf_vec_size,
mma_tiler_mn,
cluster_shape_mn,
use_prefetch,
enable_pdl,
use_prefetch=use_prefetch,
enable_pdl=enable_pdl,
swap_ab=swap_ab,
)

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 | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Inspect the exact code around the reported lines
sed -n '5935,5975p' flashinfer/gemm/gemm_base.py | nl -ba | sed -n '1,120p'

# 2) Locate Ruff configuration and check whether E731 is enabled/selected
ls -la | head
rg -n --hidden --no-ignore-vcs "ruff\.toml|tool\.ruff|ruff:|\\bE731\\b|E731" -S . || true
rg -n --hidden --no-ignore-vcs "\\[tool\\.ruff\\]" -S pyproject.toml ruff.toml setup.cfg tox.ini .ruff.toml 2>/dev/null || true

# Print any relevant ruff config sections for quick inspection
for f in pyproject.toml ruff.toml .ruff.toml setup.cfg; do
  if [ -f "$f" ]; then
    echo "==== $f ===="
    sed -n '1,200p' "$f"
    sed -n '200,400p' "$f"
  fi
done

Repository: flashinfer-ai/flashinfer

Length of output: 111


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Inspect the exact code around the reported lines (use cat -n instead of nl)
echo "---- flashinfer/gemm/gemm_base.py lines 5945-5965 ----"
sed -n '5945,5965p' flashinfer/gemm/gemm_base.py | cat -n

# 2) Locate Ruff configuration and check whether E731 is enabled/selected
echo "---- Ruff config candidate files ----"
ls -la | rg -n "ruff|pyproject|config" || true

echo "---- Search for E731 and ruff config ----"
rg -n --hidden --no-ignore-vcs "E731" -S . || true
rg -n --hidden --no-ignore-vcs "tool\.ruff|\\[tool\\.ruff\\]|\\bselect\\b|\\bignore\\b|E7[0-9]{2}" -S pyproject.toml ruff.toml .ruff.toml setup.cfg tox.ini 2>/dev/null || true
rg -n --hidden --no-ignore-vcs "\\[tool\\.ruff\\]" -S . || true

Repository: flashinfer-ai/flashinfer

Length of output: 2174


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "---- pyproject.toml [tool.ruff.lint] ----"
sed -n '80,190p' pyproject.toml | cat -n

echo "---- pyproject.toml per-file ignores ----"
rg -n "\\[tool\\.ruff\\.lint\\.per-file-ignores\\]" -n pyproject.toml || true
sed -n '190,260p' pyproject.toml | cat -n || true

echo "---- ruff.toml ----"
sed -n '1,120p' ruff.toml | cat -n

Repository: flashinfer-ai/flashinfer

Length of output: 3249


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "---- Search ruff invocation in pre-commit ----"
rg -n "ruff" .pre-commit-config.yaml **/*.sh **/*ci* **/*github* 2>/dev/null || true

echo "---- Show .pre-commit ruff section if present ----"
python3 - <<'PY'
import re, pathlib
p = pathlib.Path(".pre-commit-config.yaml")
if not p.exists():
    print("no .pre-commit-config.yaml")
    raise SystemExit
txt = p.read_text()
for m in re.finditer(r"ruff[^\n]*", txt):
    line = txt.rfind("\n", 0, m.start())+1
    end = txt.find("\n", m.end())
    print(txt[line:end])
PY

echo "---- ruff config used by any scripts (best-effort) ----"
rg -n --hidden --no-ignore-vcs "ruff(\.toml)?|--config|tool\.ruff" -S . || true

Repository: flashinfer-ai/flashinfer

Length of output: 1551


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "---- .pre-commit-config.yaml (ruff section) ----"
sed -n '35,90p' .pre-commit-config.yaml | cat -n

echo "---- Search GitHub workflows for ruff ----"
rg -n "ruff(-check|-format)?|ruff-pre-commit|config-file" .github -S 2>/dev/null || true

echo "---- Search for other ruff invocations ----"
rg -n "ruff-check|ruff .*--config-file|ruff .*--config|ruff-pre-commit" . -S --hidden --no-ignore-vcs || true

Repository: flashinfer-ai/flashinfer

Length of output: 1361


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "---- ruff version ----"
ruff --version || true

echo "---- ruff check (default config) with --select E731 ----"
ruff check --select E731 flashinfer/gemm/gemm_base.py || true

echo "---- ruff check with explicit --config=pyproject.toml ----"
ruff check --config pyproject.toml --select E731 flashinfer/gemm/gemm_base.py || true

echo "---- ruff check with explicit --config=ruff.toml ----"
ruff check --config ruff.toml --select E731 flashinfer/gemm/gemm_base.py || true

Repository: flashinfer-ai/flashinfer

Length of output: 10111


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '5940,6010p' flashinfer/gemm/gemm_base.py | cat -n

Repository: flashinfer-ai/flashinfer

Length of output: 3081


🏁 Script executed:

#!/bin/bash
set -euo pipefail
ruff check flashinfer/gemm/gemm_base.py || true

Repository: flashinfer-ai/flashinfer

Length of output: 16536


Fix Ruff E731 for make_kernel by replacing the zero-arg lambda with a local def (flashinfer/gemm/gemm_base.py:5951).

♻️ Proposed fix
-            make_kernel = lambda: Sm120B12xBlockScaledDenseGemmKernel(
-                sf_vec_size,
-                mma_tiler_mn,
-                cluster_shape_mn,
-                use_prefetch=use_prefetch,
-                enable_pdl=enable_pdl,
-                swap_ab=swap_ab,
-            )
+            def make_kernel():
+                return Sm120B12xBlockScaledDenseGemmKernel(
+                    sf_vec_size,
+                    mma_tiler_mn,
+                    cluster_shape_mn,
+                    use_prefetch=use_prefetch,
+                    enable_pdl=enable_pdl,
+                    swap_ab=swap_ab,
+                )
📝 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
# NOTE: upstream b12x ctor inserts mma_k/tile_k/single_work_tile_per_cta
# before use_prefetch, so pass these by keyword to avoid mis-binding.
make_kernel = lambda: Sm120B12xBlockScaledDenseGemmKernel(
sf_vec_size,
mma_tiler_mn,
cluster_shape_mn,
use_prefetch,
enable_pdl,
use_prefetch=use_prefetch,
enable_pdl=enable_pdl,
swap_ab=swap_ab,
)
# NOTE: upstream b12x ctor inserts mma_k/tile_k/single_work_tile_per_cta
# before use_prefetch, so pass these by keyword to avoid mis-binding.
def make_kernel():
return Sm120B12xBlockScaledDenseGemmKernel(
sf_vec_size,
mma_tiler_mn,
cluster_shape_mn,
use_prefetch=use_prefetch,
enable_pdl=enable_pdl,
swap_ab=swap_ab,
)
🧰 Tools
🪛 Ruff (0.15.15)

[error] 5951-5958: Do not assign a lambda expression, use a def

Rewrite make_kernel as a def

(E731)

🤖 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/gemm/gemm_base.py` around lines 5949 - 5958, Replace the zero-arg
lambda assigned to make_kernel with a proper local function definition to
satisfy Ruff E731: define a def make_kernel(): that returns
Sm120B12xBlockScaledDenseGemmKernel(...) using the same positional args
sf_vec_size, mma_tiler_mn, cluster_shape_mn and the keyword args
use_prefetch=use_prefetch, enable_pdl=enable_pdl, swap_ab=swap_ab so the binding
remains identical to the original lambda.

Source: Linters/SAST tools

Comment on lines +104 to +113
_B12X_TIMING = (
os.getenv("B12X_TIMING", "0") == "1" or os.getenv("VLLM_B12X_TIMING", "0") == "1"
)
_B12X_TIMING_THRESHOLD_MS = float(
os.getenv(
"B12X_TIMING_THRESHOLD_MS",
os.getenv("VLLM_B12X_TIMING_THRESHOLD_MS", "0"),
)
)
_B12X_DENSE_SPLITK_TURBO = os.getenv("B12X_DENSE_SPLITK_TURBO", "0") == "1"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Artifact from b12x, can be removed

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Removed — dropped the unused _B12X_TIMING / _B12X_TIMING_THRESHOLD_MS / _B12X_DENSE_SPLITK_TURBO globals and the now-unused os import.

Comment thread flashinfer/cute_dsl/utils.py Outdated
Comment on lines +588 to +589
# Relaxed 64 -> 16 (upstream b12x commit 0daa6ab) to allow narrow-N FP4 tiles
# (64x32 / 64x16) used with swap_ab.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Not really a necessary comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Removed.

Comment thread flashinfer/gemm/gemm_base.py
Comment thread flashinfer/gemm/gemm_base.py Outdated
Comment thread flashinfer/gemm/gemm_base.py Outdated
Comment on lines +5814 to +5817
# Single source of truth for the deterministic default tile / swap_ab pick.
# Used by BOTH forward()'s default path AND get_valid_tactics(), so the
# autotuner candidate set always contains the exact default-path tile --
# i.e. tuning is monotonic vs the static default (it can't pick worse).

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Can remove comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Removed.

Comment thread flashinfer/gemm/gemm_base.py Outdated
Comment on lines +5871 to +5882
# Generic swap_ab-free candidate set the autotuner profiles. Kept minimal:
# a larger tile grid made the per-bucket pick noisier (overfits the bucket
# representative shape), so we only add a few balanced tiles here.
for mma_tiler_mn in [(64, 64), (64, 128), (128, 64), (128, 128)]:
_add(mma_tiler_mn, swap_ab=False)

# Also expose the deterministic plan's regime-optimal pick for this
# (bucketed) shape -- the same tile forward() uses on the default path,
# which may be a narrow-N swap_ab tile (e.g. (64,32) at m=1) absent from
# the generic set above. Including it keeps autotuning monotonic: the
# tuner can never select something worse than the static default, and it
# picks up swap_ab on SKUs where it actually wins.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Maybe make the comments in these lines shorter for cleanliness

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Shortened.

Comment thread flashinfer/gemm/gemm_base.py Outdated
Comment on lines +5905 to +5911
# Deterministic, regime-aware plan from upstream b12x: picks the
# decode/prefill-optimal tile (via expected_m) and swap_ab for
# narrow-N, instead of the M-independent default tile. For a single
# call the actual m IS the representative regime, so expected_m=m.
plan = _default_dense_plan(m, n, real_k, a.device)
# Honor the plan's swap_ab (narrow-N decode tiles, e.g. 64x32).
# b12x swap_ab is device-internal; see the launch + wrapper notes.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Maybe here as well

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Shortened.

Comment thread flashinfer/gemm/gemm_base.py Outdated
Comment on lines +5960 to +5964
# b12x swap_ab is device-internal (applied via the kernel ctor); the
# public C stays row-major (m, n). The shared harness's `swap_ab` instead
# selects the SM100 operand-swap FFI convention (C declared (n, m)), which
# b12x must not use -- so pass swap_ab=False (keeps c_fake (m, n) + plain
# `out`). cache_key still carries the real swap_ab (separate caching).

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Can also shorten or delete

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Shortened — kept the one-line note on why swap_ab=False is passed to the shared harness, since it guards against an incorrect 'fix'. Happy to delete it entirely if you'd prefer.

Comment thread flashinfer/gemm/gemm_base.py Outdated
Comment on lines +6038 to +6045
# TODO(sm121/DGX Spark): b12x is *supported* on SM121 (requirement is
# @supported_compute_capability([120, 121])) but `auto` only routes it at
# SM120 here, so on a Spark `backend="auto"` falls back to cutlass/cudnn and
# never picks b12x. To enable it, widen this to `major == 12` (covers 120+121).
# Gate the change on the Spark bench: only prefer b12x if it actually beats
# cutlass/cudnn on GB10. Also verify the kernel's hardcoded
# get_smem_capacity_in_bytes("sm_120") assumption holds on SM121 (run the
# e2e handoff's correctness section on the real Spark first).

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

b12x dense GEMM should be runnable on Spark. Also make b12x the preferred backend on Spark. Then remove the comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Per our discussion, keeping cutlass/cudnn as the SM121 default (cross-SKU benchmarks show b12x isn't faster on GB10 in most cases). Replaced the TODO with a concise note; b12x stays available as an explicit backend on SM121.

@@ -6015,6 +6035,14 @@ def _heuristic_func_mm_fp4(
is_sm120 = major == 12 and minor == 0

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Maybe make this is_sm12x and then just check for major == 12 to address the Spark heuristic comment below

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Left is_sm120 as-is per the SM121 decision above — not widening auto to prefer b12x on SM121.

…env-var globals

- _b12x_gemm_fp4_requirement: under backend='auto', return False on K%128!=0
  (fall back to cutlass/cudnn) instead of raising; explicit backend='b12x'
  still raises. (per @bkryu / coderabbit)
- Trim/condense verbose comments in gemm_base.py and drop 'upstream b12x'
  references; remove the unnecessary N-divisibility comment in cute_dsl/utils.py.
- Remove the dead _B12X_TIMING / _B12X_TIMING_THRESHOLD_MS / _B12X_DENSE_SPLITK_TURBO
  env-var globals (defined-but-unused artifacts of the stripped b12x timing/launch
  layer) and the now-unused os import. (per @bkryu)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@yichengj0
yichengj0 force-pushed the b12x-new-fp4-gemm branch 3 times, most recently from a8aaef8 to b2698b2 Compare June 10, 2026 21:15
Per reviewer: do not make b12x the default backend on SM121 (GB10) -- cutlass/
cudnn are faster there (cross-SKU benchmarks). Replace the SM121 TODO with a
concise note (SM121 intentionally excluded; b12x stays an explicit backend) and
drop the unnecessary 'warp-level MMA, underfill tile selection' justification on
the SM120 line. `auto` behavior unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@yichengj0
yichengj0 force-pushed the b12x-new-fp4-gemm branch from b2698b2 to 091b926 Compare June 10, 2026 21:19

@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 (1)
flashinfer/gemm/gemm_base.py (1)

5464-5472: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Preserve backend="auto" fallback for the other b12x-only preconditions.

These guards still raise on CUDA < 13, use_8x4_sf_layout=True, or use_nvfp4=False even when backend="auto". That short-circuits auto-routing before CUTLASS/cuDNN can be considered, unlike the real_k % 128 check just below. Return False for non-explicit b12x selection and only raise when backend == "b12x".

🐛 Proposed fix
     # b12x backend requires CUDA 13+, 128x4 scale factor layout, and NVFP4 only.
     if get_cuda_version().major < 13:
+        if backend != "b12x":
+            return False
         raise ValueError(
             "b12x FP4 GEMM requires CUDA 13 or later. "
             f"Current CUDA version: {get_cuda_version()}."
         )
     if use_8x4_sf_layout:
+        if backend != "b12x":
+            return False
         raise ValueError("b12x FP4 GEMM only supports 128x4 scale factor layout.")
     if not use_nvfp4:
+        if backend != "b12x":
+            return False
         raise ValueError("b12x FP4 GEMM only supports NVFP4 (sf_vec_size=16).")
🤖 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/gemm/gemm_base.py` around lines 5464 - 5472, The current checks
using get_cuda_version().major, use_8x4_sf_layout, and use_nvfp4 unconditionally
raise ValueError and therefore block backend="auto" from falling back; modify
those guards inside the b12x selection logic so they only raise when backend ==
"b12x" and otherwise return False (mirror the behavior of the real_k % 128
check). Concretely, update the branches that reference get_cuda_version(),
use_8x4_sf_layout, and use_nvfp4 so they first test backend == "b12x": if
backend == "b12x" then raise the ValueError with the same message, else return
False. Ensure you keep the exact messages and only change control flow for
functions/variables get_cuda_version, use_8x4_sf_layout, use_nvfp4, and backend.
🤖 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/gemm/gemm_base.py`:
- Around line 5464-5472: The current checks using get_cuda_version().major,
use_8x4_sf_layout, and use_nvfp4 unconditionally raise ValueError and therefore
block backend="auto" from falling back; modify those guards inside the b12x
selection logic so they only raise when backend == "b12x" and otherwise return
False (mirror the behavior of the real_k % 128 check). Concretely, update the
branches that reference get_cuda_version(), use_8x4_sf_layout, and use_nvfp4 so
they first test backend == "b12x": if backend == "b12x" then raise the
ValueError with the same message, else return False. Ensure you keep the exact
messages and only change control flow for functions/variables get_cuda_version,
use_8x4_sf_layout, use_nvfp4, and backend.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a854caeb-f76f-4f44-b401-51b72a214a7a

📥 Commits

Reviewing files that changed from the base of the PR and between b2698b223de6311a2a92517b1b61e10335ae3a83 and 091b926.

📒 Files selected for processing (1)
  • flashinfer/gemm/gemm_base.py

@bkryu bkryu added the run-ci label Jun 10, 2026
@bkryu

bkryu commented Jun 10, 2026

Copy link
Copy Markdown
Collaborator

/bot run

@flashinfer-bot

Copy link
Copy Markdown
Collaborator

GitLab MR !780 has been created, and the CI pipeline #54367367 is currently running. I'll report back once the pipeline job completes.

@bkryu bkryu left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Changes LGTM. Will approve once the CI comes back clean

@bkryu
bkryu merged commit 28406af into flashinfer-ai:main Jun 11, 2026
53 of 54 checks passed
yichengj0 added a commit to yichengj0/flashinfer that referenced this pull request Jun 15, 2026
flashinfer-ai#3560 added a b12x precheck rejecting K not a multiple of tile_k=128, which
broke explicit backend="b12x" for shapes like K=192 (packed_k=96). This is the
vLLM kernels_spark_fp4_flashinfer_nvfp4 regression on DGX Spark / SM121.

The kernel's true floor is K % 32 == 0: A is K-major packed FP4 (K/2 bytes)
loaded via TMA with assumed_align=16. K need not divide tile_k=128 because the
mainloop predicates the partial K-tile and the swizzled SF layout zero-pads
scale-groups, so ragged K computes correctly (verified on GB10: cos_sim
0.98-0.99 for K in {64,96,160,192,224,320}; K%32==16 yields garbage).

Relax both the gemm_base precheck and the kernel can_implement guard to % 32
so the non-autotune (default-plan) and autotune (candidate-enumeration) paths
accept the same shapes. Add ragged-K regression tests and a misaligned-K
rejection test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
bkryu pushed a commit that referenced this pull request Jun 16, 2026
…#3646)

## Problem

PR #3560 added a precheck in `_b12x_gemm_fp4_requirement` that rejects
`K` not a multiple of `tile_k = sf_vec_size * 8 = 128`. For explicit
`backend="b12x"` this `raise`s, breaking shapes like **K=192**
(`packed_k=96`):

```
ValueError: b12x FP4 GEMM requires the contraction dim K to be a multiple of 128
(tile_k = sf_vec_size * 8). Got K=192.
```

These shapes ran before #3560.

## Root cause

`K % 128` is the *full MMA K-tile* condition, not the kernel's real
correctness floor. The actual floor is **`K % 32 == 0`**:

- `A` is K-major packed FP4, `(M, K/2)` bytes, loaded via TMA with
`assumed_align=16`, so `K/2 % 16 == 0` → `K % 32 == 0`.
- `K` need **not** divide `tile_k=128`: the mainloop predicates the
partial final K-tile and the swizzled SF layout zero-pads scale-groups,
so ragged K (96, 160, 192, 224, …) computes correctly.

Verified on GB10 (SM121): `cos_sim` 0.98–0.99 for K ∈ {64, 96, 160, 192,
224, 320} across M ∈ {1, 8, 256}, narrow-N (swap_ab), and both autotune
paths; `K % 32 == 16` (e.g. 112, 176) yields garbage and is correctly
rejected.

## Fix

Relax both K guards to `% 32`:

- `_b12x_gemm_fp4_requirement` precheck (gates the non-autotune
default-plan path).
- `DenseGemmKernel.can_implement` (gates the autotune candidate
enumeration).

Both sites move together so the autotune and non-autotune paths accept
the same shapes.

## Tests

`tests/gemm/test_mm_fp4.py`:
- `test_mm_fp4_b12x_ragged_k` — K ∈ {96, 192} × `auto_tuning` ∈ {False,
True} (both `auto_tuning` values exercise the distinct guard paths).
- `test_mm_fp4_b12x_misaligned_k_raises` — K=112 must reject cleanly.

All new tests pass; the 396 existing b12x NVFP4 cases (K=128/256/512)
remain green on SM121.

## Note for reviewers

#3560's precheck mirrored the b12x donor's `can_implement` contract
(which hard-`raise`s on `k % tile_k`). This PR intentionally **extends
past** that contract to `% 32`, justified by the correctness data above
— the kernel's mainloop already handles ragged K via partial-tile
predication.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

## Release Notes

* **New Features**
* Relaxed the b12x FP4 GEMM K-dimension alignment requirement from
multiples of 128 to multiples of 32 to improve support for more matrix
shapes.

* **Tests**
* Added regression coverage for ragged K values across auto-tuning
settings.
* Added a test ensuring misaligned K values are rejected with a clear
“multiple of 32” error (under supported GPU/CUDA conditions).
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
YangXu1990uiuc added a commit that referenced this pull request Jul 15, 2026
…out (breadth -> unified fuzzers)

GEMM (fuzzer default-on since #3539): bmm_fp8 3456->6 cases (~71 min/leg),
mm_fp4 23760->12 + 4 auto (~59 min on Blackwell legs), mm_bf16 7560->13,
mm_mxfp8 2688->6 (+large-dim 400->4, stats 18->3), bmm_mxfp8 576->5,
bmm_bf16 240->8. Every kept case verified to actually run somewhere (none
self-skip into no-ops). Kept in full: error-path / cache-behavior / invariant
tests, the #3560 ragged-K anchor, and non-fuzzed paths (8x4 SF layout,
bias/pdl epilogues, trtllm weight shuffle).

MoE (rides the #3958 fuzzer default-on flip, assumes #3892 routing axes):
shape fan-out compressed, the quant x routing x weight-layout kernel-selection
matrix kept in FULL -- non-NVFP4 quant numerics are not fuzzed yet, so only
shapes were cut, never modes:
- renormalize trio: shared axis constants -> boundary tokens [8,3072] +
  intermediate [1024,384] (trio 4608 -> ~1500 collected)
- sigmoid 576->192; deepseekv3 27216->4032 (intermediate axis = hitting set of
  every routing config's compatible_intermediate_size, each config still runs)
- routed-parity 3456->144 (routing x quant x packed/unpacked in full);
  per-token 4over6 108->16; fp4 tactic sweep 72->24 (odd-token anchor kept);
  cute-dsl accuracy 144->16 and 24->8
Untouched: error-path / OA-param / routing-replay / kernel-tier / LoRA /
CUDA-graph / EP / tactic-pairing tests; #3595 / #3067 anchors.

Every touched file carries a header directing future coverage to the fuzzers.

AI-assisted (grid analysis + curation by Claude Code).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.

[Perf][SM12x] Update mm_fp4(backend='b12x') kernels

3 participants