[None][perf] MiniMax-M3 MSA: split the KV axis for the decode indexer proxy - #17008
Draft
hyukn wants to merge 1 commit into
Draft
[None][perf] MiniMax-M3 MSA: split the KV axis for the decode indexer proxy#17008hyukn wants to merge 1 commit into
hyukn wants to merge 1 commit into
Conversation
… proxy
The MSA indexer proxy is a full-kv_len MQA max-score pass that scores every KV block so the top-k selection can pick 16. At the canonical decode point it costs 1576.5 us/step, 8.01% of the step, at 16.1% of its own KV-read byte floor and under 1% MFU.
The proxy runs on a persistent grid: get_grid_shape returns dim3(num_sm) regardless of available work (fmha_tile_scheduler.hpp:126-129) and each CTA reads its slice from packed_work_range (plan.cuh:386-388). Work items are batch * num_qo_heads * ceil(qo_len/qo_tile_size), and at decode _compute_pack_factor folds all four index heads into one packed qo head (api.py:110-120, 557-558), so batch=16 yields 16 work items on a 152-SM device. The remaining CTAs exit empty while the 16 busy ones each walk ceil(8700/256) = 34 KV tiles serially.
The binding quantity is serial depth per CTA, not CTA count. Widening the head axis with pack_factor=1 raises the work-item count to 64 but leaves the chain at 34 iterations, because the causal clamp eff_kv = min(q_end + offset_q, kv_len) absorbs the entire q_end reduction when offset_q is 8699 (plan.cuh:136-146); it also re-reads K once per head, taking the KV traffic from 35.6 MB to 142.6 MB. num_kv_splits is the axis that shortens the chain (plan.cuh:279-283), and the proxy plan pinned it to 1.
This enables it for the decode proxy plan at num_kv_splits=4, with TRTLLM_MINIMAX_M3_MSA_PROXY_KV_SPLITS as an override (1 restores the previous single-piece plan exactly). Prefill and mixed batches keep num_kv_splits=1: those request `out`, so the split-KV O/LSE reduction does run there and needs its own parity evaluation.
_MsaGraphSafePlan mirrors the split-KV worklists (kv_tile_begin_indices, kv_tile_end_indices, kv_split_indices, num_kv_splits_per_row) and the two split-KV workspaces (workspace_o, workspace_lse) into graph-stable buffers alongside the existing plan mirrors. Widths follow the vendor's own expressions: api.py:704-709 sizes packed_work_info and the three kv_tile_* worklists at one shared max_work_items = 131072 * max(num_kv_splits, 1), api.py:711 sizes workspace_o at total_qo_len * num_kv_splits * num_qo_heads * 128, and api.py:715 sizes workspace_lse at num_kv_splits * total_qo_len * num_qo_heads. Both workspaces are mirrored rather than pinned by address, since both are reallocated by the planner. Mirror buffers are also grown to the plan's actual width while steps are still eager, so the __init__ widths act as pre-allocation hints and the steady state allocates nothing; after capture the addresses are frozen and a short mirror is a hard error.
Kernel effect, standalone proxy pass on GB300 with CUDA-graph replay timings at the traced decode geometry (batch 16, kv_len ~8700, 4 index heads, page_size 128, bf16, OnlyScore):
num_kv_splits | busy CTAs | us | max_score vs splits=1
1 | 16/152 | 32.16 | reference
2 | 32/152 | 21.95 | bitwise-identical
4 | 64/152 | 15.81 | bitwise-identical
8 | 128/152 | 15.78 | bitwise-identical
16 | 128/152 | 15.78 | bitwise-identical
32 | 128/152 | 15.74 | bitwise-identical
2.04x, saturating at the predicted 34/4 = 8 cap, and time is linear in serial depth (ki = 2/34/64 gives 13.7/32.2/48.6 us), which is the signature of a latency chain rather than a bandwidth or FLOP floor. splits=4 reaches the same 15.8 us as splits=8 while halving the workspace_o mirror, so 4 is the default.
End-to-end on GB300, MiniMax-M3-NVFP4, TP1/PP1/EP1, ISL 8192 / OSL 1024, concurrency 16, num_prompts 32:
arm tok/s node
base 6130.02 nvl72d027-T06
same build, KV_SPLITS=1 6171.52 nvl72d010-T11
KV_SPLITS=4 6446.05 nvl72d104-T12
KV_SPLITS=4, under nsys 6339.14 nvl72d010-T18
+4.45% against the paired same-build KV_SPLITS=1 arm and +5.16% against base. The two KV_SPLITS=1 arms agree to 0.68% across different nodes, and the delta is 4x the measured 1.11% same-node repeat spread. The mirror refresh copy is 9.18 us/step against a 934 us/step kernel saving, and it is flat in width (8x the bytes costs 1.06x the time), so the wider mirrors are not a measurable cost.
Accuracy: max_score is torch.equal to the num_kv_splits=1 output at every split width above, measured across batch in {1, 2, 4, 8, 15, 16, 32} and kv_len in {256 ... 16384}. This is expected structurally as well as observed: in OnlyScore mode `out` is None, so the split-KV O/LSE reduction is skipped entirely (api.py:919), each per-k-tile maximum is written by exactly one split from its own [kv_tile_begin, effective_end) range seeded at -INFINITY (mainloop:921-926, 1322), and there is no cross-split combination. Maximum is associative, commutative and exact in floating point, so partitioning the KV axis cannot perturb the result. Downstream top-k block selection is therefore identical, and greedy decode output is unchanged.
Signed-off-by: Yukun He <23156053+hyukn@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
@coderabbitai summary
Description
The MSA indexer proxy is a full-
kv_lenMQA max-score pass that scores every KV block so the top-k selection can pick 16. At the canonical decode point it costs 1576.5 us/step, 8.01% of the step, while sitting at 16.1% of its own KV-read byte floor and under 1% MFU, which makes it the largest device cost in the M3 decode step that is not already at a floor.Why it is slow. The proxy runs on a persistent grid:
get_grid_shapereturnsdim3(num_sm)regardless of how much work exists (fmha_tile_scheduler.hpp:126-129), and each CTA reads its slice out ofpacked_work_range(plan.cuh:386-388). Work items arebatch * num_qo_heads * ceil(qo_len/qo_tile_size), and at decode_compute_pack_factorfolds all four index heads into one packed qo head (api.py:110-120,557-558), so a batch of 16 yields 16 work items on a 152-SM device. The other CTAs pick up an empty range and exit, while the 16 busy ones each walkceil(8700/256) = 34KV tiles serially.Why the head axis is the wrong lever. The binding quantity is serial depth per CTA, not CTA count. Setting
pack_factor=1raises the work-item count to 64 but leaves the chain at 34 iterations, because the causal clampeff_kv = min(q_end + offset_q, kv_len)absorbs the wholeq_endreduction onceoffset_qis 8699 (plan.cuh:136-146). It also re-reads K once per head, taking KV traffic from 35.6 MB to 142.6 MB for no reduction in wall time.Changes
num_kv_splitsfor the decode indexer-proxy plan, which is the axis that actually shortens the per-CTA tile chain (plan.cuh:279-283). The proxy plan had it pinned to 1. Default is 4, withTRTLLM_MINIMAX_M3_MSA_PROXY_KV_SPLITSas an override; setting it to 1 restores the previous single-piece plan exactly.num_kv_splits=1. Those requestout, so the split-KV O/LSE reduction does run there and would need its own parity evaluation before being enabled._MsaGraphSafePlanto mirror the split-KV worklists (kv_tile_begin_indices,kv_tile_end_indices,kv_split_indices,num_kv_splits_per_row) and both split-KV workspaces (workspace_o,workspace_lse) into graph-stable buffers, alongside the plan mirrors that already existed. Widths follow the vendor's own expressions:api.py:704-709sizespacked_work_infoand the threekv_tile_*worklists at one sharedmax_work_items = 131072 * max(num_kv_splits, 1),api.py:711sizesworkspace_oattotal_qo_len * num_kv_splits * num_qo_heads * 128, andapi.py:715sizesworkspace_lseatnum_kv_splits * total_qo_len * num_qo_heads.workspace_ogoes through_alloc_workspace_buf, which reallocates when the requested size grows, andworkspace_lsethrough_alloc_perplan_buf, which reallocates on every call (api.py:78-101), so neither address is stable enough to pin.__init__widths act as pre-allocation hints and the steady state allocates nothing. After capture the addresses are frozen and a short mirror is a hard error.Python only, no rebuild required.
Performance
Kernel
Standalone proxy pass on GB300, CUDA-graph replay timings at the traced decode geometry (batch 16,
kv_len~8700, 4 index heads,page_size128, bf16,OnlyScore):2.04x, saturating at the predicted
34/4 = 8cap. Time is linear in serial depth (ki= 2/34/64 gives 13.7/32.2/48.6 us), which is the signature of a latency chain rather than a bandwidth or FLOP floor.splits=4reaches the same 15.8 us assplits=8while halving theworkspace_omirror, so 4 is the default.End to end
GB300, MiniMax-M3-NVFP4, TP1/PP1/EP1, ISL 8192 / OSL 1024, concurrency 16,
num_prompts32,total_token_throughput:KV_SPLITS=1KV_SPLITS=4KV_SPLITS=4, under nsys+4.45% against the paired same-build
KV_SPLITS=1arm, and +5.16% against base. The twoKV_SPLITS=1arms agree to 0.68% across different nodes, so the delta is not node drift, and it is roughly 4x the 1.11% same-node repeat spread measured on this workload.The added mirror refresh copy is 9.18 us/step against a 934 us/step kernel saving, and its cost is flat in width (8x the bytes costs 1.06x the time), so the wider mirrors are not a measurable cost.
_build_step_plans()runs once per step fromprepare(), not once per layer.Accuracy
max_scoreistorch.equalto thenum_kv_splits=1output at every split width in the table above, measured acrossbatchin {1, 2, 4, 8, 15, 16, 32} andkv_lenin {256 ... 16384}, including consecutive decode lengths. Downstream top-k block selection is therefore identical and greedy decode output is unchanged.This is expected structurally, not only observed. In
OnlyScoremodeoutisNone, so the split-KV O/LSE reduction is skipped entirely (api.py:919). Each per-k-tile maximum is written by exactly one split, seeded at-INFINITYover that split's own[kv_tile_begin, effective_end)range (mainloop:921-926,1322), so there is no cross-split combination to get wrong. Maximum is associative, commutative and exact in floating point, so partitioning the KV axis cannot perturb the result.Test Coverage
tests/unittest/_torch/attention/sparse/test_minimax_m3_msa_backend.pycovers the MSA backend and the graph-safe plan path; the split-KV mirrors are exercised through it whenevernum_kv_splits > 1.num_kv_splits=1plan over the batch andkv_lengrid described above, comparingmax_scorewithtorch.equaland the resulting top-k block indices for exact equality.TRTLLM_MINIMAX_M3_MSA_PROXY_KV_SPLITSon a single node.PR Checklist
Please review the following before submitting your PR:
PR description clearly explains what and why. If using CodeRabbit's summary, please make sure it makes sense.
PR Follows TRT-LLM CODING GUIDELINES to the best of your knowledge.
Test cases are provided for new code paths (see test instructions)
If PR introduces API changes, an appropriate PR label is added - either
api-compatibleorapi-breaking. Forapi-breaking, includeBREAKINGin the PR title.Any new dependencies have been scanned for license and vulnerabilities
CODEOWNERS updated if ownership changes
Documentation updated as needed
Update tava architecture diagram if there is a significant design change in PR.
The reviewers assigned automatically/manually are appropriate for the PR.
Please check this after reviewing the above items as appropriate for this PR.
GitHub Bot Help
To see a list of available CI bot commands, please comment
/bot help.