Status: grouped and batched SM120 backends are built and pass the full test suite on an RTX PRO 6000 (188 SMs). Tile coverage and scheduler tuning are still open (see the end). File and line references are to CUTLASS v4.8.0, the version the repository pins.
- The checkpoint format. The SM120 sparse NVFP4 MMA also reads one UE4M3 scale per 32 K
elements (
sm1xx_common.inl:472-477,:173);sm120/config.cuhstatic_asserts it. Weights keep the same paired-4:8, per-32 packing, andreference.pystays the spec.compressruns CUTLASS's SM120StructuredSparseCompressorspecialization, so the in-memory A / E layouts are SM120's own, built from the same checkpoint bytes. - The operand layouts. RowMajor sparse A (alignment 64), ColMajor B (alignment 32) and
ColMajor C / D are exactly what the upstream SM120 example
(
examples/80_blackwell_geforce_sparse_gemm/80b_...) uses. - The quantizers, the layout helpers,
ops.cu, the op schemas and the tests. They only consume the config aliases, whichsm120/config.cuhdefines under the same names.
- The block-scaled sparse GEMM is a different kernel family: warp-level
mma.syncwith register operands, one producer warp group (A/E/SF loads, B loads, scheduler, C loads) and two cooperative MMA warp groups (sm120_gemm_tma_warpspecialized_cooperative_asymmetric_dma.hpp). - The cluster is a static 1x1x1 (
sm120_blockscaled_sparse_mma_builder.inl:179: no programmatic multicast), so every tactic is(config_id, 1, 1)andcheck_clusterrejects anything else. - Shared memory is ~99 KB per SM. The builder sizes stages automatically; when two full stages do
not fit, it falls back to 1.5 stages of B with the metadata read from L2
(
sm120_blockscaled_sparse_mma_builder.inl:103-120). - The epilogue schedule is
SparseTmaWarpSpecializedCooperativeSm120. Its defaultLinearCombinationfusion supports the stridedalpha_ptrwithdAlpha = (0, 0, 1), so the per-expert alpha works unchanged.
B, D and SFB are addressed at a fixed per-expert stride, so the grouped kernel sets up every
collective (TMA descriptors, mainloop, epilogue) with the max shape (M, max_n, K, E), exactly
the batched addressing with L = E. Only the scheduler changes: PersistentTileSchedulerSm90Group
(selected for SM120 at tile_scheduler.hpp:405-417) walks each expert's device-side token count
and never schedules a tile past it. Unlike the SM100 patch, no per-tile effective shape is needed:
M and K are shared, and the last partial N tile writes into padded rows, whose bytes are undefined
by contract. Rows past the last scheduled tile are left untouched.
The changes in csrc/cutlass_overrides/.../sm120_gemm_tma_warpspecialized_cooperative_asymmetric_dma.hpp
(also patches/cutlass-v4.8.0-sm120-sparse-grouped.patch):
- detect
MoEProblemShape;ProblemShapeGemmis the max shape, built frommax_m/max_n/max_k(notget_host_problem_shape(0), which returns group 0's count when host counts are present); - select the group scheduler and hand it the MoE shape in the setup, grid and workspace paths;
accept
kGroupedincan_implement; - drive the static scheduler. The stock kernel only configures the scheduler pipeline and runs the
Warp1 producer loop for the dynamic (CLC) scheduler, but every consumer waits on that pipeline.
Warp1 now publishes tiles through a
PipelineAsyncwhose consumers are the two mainloop load warps, the MMA warps and (when C is read) the epilogue load warp. The loop iswhile (valid), so a CTA with no valid first tile publishes nothing (a published response nobody consumes would hangproducer_tail); - give that pipeline its own shared storage: the group scheduler's
SharedStorage::pipeline()returns the barriers by value; - advance the consumers' scheduler pipeline state for the grouped scheduler too (stock code only advances it for the dynamic one);
- skip the single-argument
fetch_next_work(work)fallback, which the group scheduler lacks and never needs (valid_warpgroup_in_work_tileis always true).
On the RTX PRO 6000: the full tests/ suite, including a zero-count / untouched-padding test; and
a stress run comparing grouped against batched byte for byte on valid rows (zero-count experts,
all experts empty, E up to 128 so the scheduler's 32-group warp walk wraps, many tiles per CTA,
K up to 2048, a CUDA graph replayed with new counts, 50 repeated launches).
Measured with graph-replayed launches on realistic MoE shapes (Qwen3-30B-A3B, Qwen3-235B-A22B, Mixtral-8x7B, DeepSeek-V3; top-k routed counts, 1 to 8192 tokens), interleaving builds and repeating the runs.
- Raster order: AlongN (
configure_group_schedulerinsm120/config.cuh). The group scheduler's default, AlongM, sweeps an expert's M (weight) tiles for one N tile, so an expert with several N tiles streams its weights from DRAM once per N tile. AlongN reuses each weight tile from L2 across its N tiles: Mixtral-8x7B w13 is 12% faster at 512 tokens and 9.5% at 2048, and the other shapes change by under 1%, except DeepSeek-V3 at 8192 tokens (+1-2%, 23 N tiles per expert). A swizzle above 1 was slower everywhere. - Scheduler pipeline depth: unchanged (2). 4 and 8 stages measured the same.
- Persistent grid: unchanged (SM count). Smaller grids win only in scattered wave-quantization cases, and lose at mid and large sizes.
- Quantizer row grid: 8 blocks per SM (
kQuantRowBlocksPerSm; SM100 keeps its ~2048 as 14 x 148). The grid is sized from the paddedE * max_nrows, and every block rebuilds the counts prefix before finding it has no rows, so the former fixed 2048 cost 11-21% at small and mid token counts for 128- and 256-expert models; large token counts are unchanged.
- Tiles.
sparse_1sm_128x64(the default) andsparse_1sm_256x128joined 128x128. With 64 or fewer tokens per expert, 128x64 wastes half as much MMA work on the unused N range as 128x128, and its smaller stages leave room for a deeper pipeline in 99 KB: Qwen3-30B-A3B w13 at 1 token drops from 17.6 to 12.8 µs, Mixtral-8x7B w13 from 104 to 64 µs. 256x128 wins compute-bound prefill (Mixtral-8x7B w2 at 8192 tokens: 1072 -> 965 µs). 128x256 does not build (the SM120 sparse builder cannot tile a 256-wide N block).suggest_tactic(max_n)picks 256x128 for max_n >= 768 and 128x64 otherwise: within 0.5% of the per-shape best on the geometric mean, 7% at worst. - Split-K (
sparse_1sm_128x{64,128}_sk{2,4},PersistentTileSchedulerSm120GroupSplitKincsrc/cutlass_overrides/.../sm120_tile_scheduler_group_splitk.hpp). Each output tile's K range becomes 2 or 4 work units; the splits of a tile run in the same round on the CTAs of one slot, and reduce in one step through per-slot fp32 partial buffers (deterministic; the last split runs the epilogue). It pays only when the tiles are far fewer than the SMs and K is long, e.g. under expert parallelism at decode: DeepSeek-V3 w13 with one active expert 21.1 -> 18.9 µs (sk4), Mixtral-8x7B w2 at 1 token 42.9 -> 39.1 µs (sk2). Ordinary top-k decode on one GPU already has about 100-450 tiles, and there split-K only adds overhead; it stays an autotuning option.
tools/bench_moe.py --dense compares with the best of 8 configurations (4 tiles and schedules x 2
raster orders) of CUTLASS's SM120 dense NVFP4 pointer-array grouped GEMM
(tools/dense_baseline/, from example 79d; per-16 scales, per-expert arrays built on the device,
padded rows skipped). Across the 48 cases, the best sparse tactic is 1.07-1.56x faster (median
1.43x):
| model | w13 speedup (1 -> 8192 tokens) | w2 speedup (1 -> 8192 tokens) |
|---|---|---|
| Qwen3-30B-A3B | 1.39 1.54 1.33 1.35 1.27 1.21 | 1.32 1.39 1.46 1.49 1.20 1.07 |
| Qwen3-235B-A22B | 1.47 1.49 1.45 1.50 1.34 1.36 | 1.45 1.39 1.32 1.36 1.22 1.17 |
| Mixtral-8x7B | 1.54 1.54 1.53 1.41 1.40 1.43 | 1.43 1.55 1.56 1.44 1.35 1.40 |
| DeepSeek-V3 | 1.52 1.53 1.51 1.53 1.50 1.50 | 1.49 1.44 1.38 1.42 1.38 1.28 |
(token counts 1, 16, 128, 512, 2048, 8192). The sparse weight moves about 0.6x the dense bytes, so ~1.6x is the memory-bound ceiling; dense reaches 93-100% of DRAM bandwidth and sparse 80-95% on most memory-bound shapes. The weakest cases are large-token w2 GEMMs with short K (Qwen3-30B-A3B w2, K = 768), where neither kernel is bandwidth-bound.
Remaining costs:
- A fixed ~1-3 µs grouped overhead: every warp walks the device-side counts to find its first
tile (about 0.5 µs per 32 experts). Batched mode takes its first tile straight from
blockIdx. - Short-K split-K overhead. Per-unit fixed costs (pipeline fill, epilogue) dominate when each split has only 1-2 K tiles; stream-K style balancing would be the next step if expert-parallel decode needs it.
- SM100 re-validation. The shared layers changed (
group_mm.cuhpicks grouped or batched from the variant's problem shape and passes a split factor;quant_launchtakes the SM count). SM100 compiles, buttests/andtools/ab_compare.pyagainst a v0.10.0 build should be re-run on a B200:tools/validate_sm100.shdoes both. - CUTLASS version. When bumping, re-apply both patches in
patches/to the new stock headers, diff against the overrides (only upstream's own edits should differ), and re-run the tests on both archs.