Skip to content

[None][perf] Fuse MiniMax-M3 MSA per-layer KV-cache writes into one kernel#16755

Merged
pcicotti merged 2 commits into
NVIDIA:feat/m3_with_msafrom
zheyuf:perf/m3-msa-prep
Jul 24, 2026
Merged

[None][perf] Fuse MiniMax-M3 MSA per-layer KV-cache writes into one kernel#16755
pcicotti merged 2 commits into
NVIDIA:feat/m3_with_msafrom
zheyuf:perf/m3-msa-prep

Conversation

@zheyuf

@zheyuf zheyuf commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator

Description

Where this PR targets at:

MiniMax-M3 MSA per-layer KV-cache writes fusion (K, V, index-K):

Take a sparse layer as example:

  1. input RMSNorm
  2. QKV proj  (+ index-Q/K proj for the indexer)
  3. ►► WRITE: scatter new K, V, index-K into the paged caches   ◄◄ our fix
  4. indexer:  proxy-FMHA (reads index-K) → max-reduce → SelectBlocks (top-16 of 128-token blocks)
  5. main sparse attention (reads K,V from selected blocks) → o_proj
  6. post-attn RMSNorm
  7. MoE FFN (router → top-4 experts + shared) + allreduce

The problem

Each MSA layer writes its new-token main K, main V, and (sparse layers) index-K. Writing of K, V and index-K takes four tiny kernels: division, remainder, index cast, index_put scatter. So that's 3*4=12 kernel launches per sparse layer. At 60 layers that is ~720 tiny kernels per decode step, all captured into decode CUDA graphs and re-executed on every step. It accumulates to large launch overhead.

The fix

This PR replaces them with one Triton launch per layer (~720 → 60) launches on the write path each iter).

Measured impact (4x B300, InferenceMAX-style serving benchmark)

trtllm-serve + benchmark_serving, NVFP4 + fp8 KV cache, MSA, decode CUDA graphs + overlap scheduler, no spec decode, random 8k/1k, identical seeded prompt sets, JIT/autotuner warmed.

metric conc 64, TP4/EP4 conc 32, TP2/EP1
output throughput 1,817.1 → 1,942.5 tok/s (+6.9%) 1,082.1 → 1,171.7 tok/s (+8.3%)
median TPOT (≈ decode iteration time) 33.86 → 31.76 ms (−2.1 ms) 28.39 → 26.20 ms (−2.2 ms)
mean TTFT 995 → 954 ms (−4.1%) 715 → 690 ms (−3.5%)

Nsys trace for a layer (look at green parts)

Before fix: 4 tiny kernels-to-be-fused for index-K, then 3 kernels for indexer, then 8 tiny kernels-to-be-fused for K and V.
image

After fix: 1 fused Triton launch (contains previous 12 kernels for K, V and K-index), then 3 kernels for indexer. So it's fuse + reorder: compute the page/offset once, write K + V + index-K together, before the indexer runs.
image

Test Coverage

  • Spec-decode: TestMiniMaxM3::test_nvfp4_eagle3[tp4-ep4-adp=False-overlap=True-msa=True-graphs=True-eval_mode=inferencemax] PASSED on this branch — strict-match 95.45 (baseline same tip: 95.07), acceptance rate 0.844 vs 0.846, mean acceptance length 3.532 vs 3.537

PR Checklist

  • PR title and description
  • Test coverage
  • CI (/bot run)

@zheyuf
zheyuf force-pushed the perf/m3-msa-prep branch 2 times, most recently from fe63ae9 to 5611b35 Compare July 24, 2026 02:22
…ernel

Each MSA layer wrote its new-token main K, main V, and (sparse layers)
index-K through three separate aten advanced-indexing writes, and every
write re-derived the same (page, within-page) slot split on device: a
cast, a floor-div, and a modulo kernel per write. At 60 layers that is
~730 tiny kernels per decode step (~12 per sparse layer), all captured
into decode CUDA graphs and re-executed every step.

Replace them with one Triton launch per layer that scatters all three
tensors into their paged HND caches, deriving each token's slot split
from out_cache_loc in-register and casting to the cache dtype on store
(which folds in the FP8 KV-cache cast). The fused write runs at the top
of the layer's attention core, before the indexer proxy pass reads the
index-K cache; run_indexer and run_msa_paged_gqa skip their legacy
writes via an explicit prewritten marker and fall back unchanged
whenever the fused path reports an unsupported layout.

Measured on 4x B200 (TP4/EP4, MXFP8, MSA, decode CUDA graphs, 1k/1k,
concurrency 64), warm A/B on TOT (incl. NVIDIA#16695), mean of 2 runs:
+6.4% output throughput (2166 -> 2304 tok/s), -20.6% total kernel
launches, aten elementwise time per rank-step 2.59 ms -> 0.96 ms, with
GEMM/NCCL/FMHA buckets unchanged. NVFP4 (fp8 KV) shows +7.9%.

Signed-off-by: Zheyu Fu <zheyuf@NVIDIA.com>
@zheyuf
zheyuf force-pushed the perf/m3-msa-prep branch from 5611b35 to ce22d7d Compare July 24, 2026 04:30
… module

Keep only the reference-parity test (8 parametrized cases covering fp8/bf16
caches, 1/4 KV heads, with/without the index cache) and move it into
test_minimax_m3_msa_backend.py; drop the layout-rejection, int64-offset, and
boundary-slot micro-tests along with the standalone scatter test file.

Signed-off-by: Zheyu Fu <zheyuf@NVIDIA.com>
@zheyuf
zheyuf marked this pull request as ready for review July 24, 2026 08:57
@zheyuf
zheyuf requested review from a team as code owners July 24, 2026 08:57
@zheyuf
zheyuf requested review from PerkzZheng, WeiHaocheng, brb-nv, pcicotti, peihu-nv and pengbowang-nv and removed request for a team, PerkzZheng, WeiHaocheng and pengbowang-nv July 24, 2026 08:57
@pcicotti
pcicotti merged commit 3c85c6b into NVIDIA:feat/m3_with_msa Jul 24, 2026
44 of 64 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants