Repository navigation
[Perf] [AMD] batch-invariant: Segmented split-K bf16 matmul for gfx950 - #45599
aarnetalman wants to merge 13 commits into
Conversation
Deterministic inference runs every linear layer through the persistent batch-invariant Triton matmul, with one bf16 launch configuration for all GPUs. On gfx950 a 128x64x128 tile with three stages and 16-wide MFMA instructions is faster. The configuration is still fixed for every M, so batch invariance is unchanged. Other GPUs keep the existing configuration. Signed-off-by: Aarne Talman <aarne.talman@amd.com>
Build the persistent matmul's launch configurations in a small helper and add a CPU test: on gfx950 the bf16 entry must be the measured preset and fit gfx950's 160 KB of LDS, and on other GPUs it must stay the original configuration. A change to the preset now has to be deliberate. Signed-off-by: Aarne Talman <aarne.talman@amd.com>
The preset was tuned on Qwen3-8B and Qwen3.8-27B shapes but measured faster on other models' linear layers too (decode 1.5-1.9x up to N ~ 8k, neutral on wider N). Say that instead of naming the two models. Signed-off-by: Aarne Talman <aarne.talman@amd.com>
On gfx950, bf16 matmuls in batch-invariant mode use a segmented kernel: K is split into 4 fixed segments of 64-wide blocks and the segment sums are added in order at every M. Up to M = 1024 (M = 128 for outputs wider than 8192) the segments run in separate programs (fp32 partials, then a fixed-order reduce), so decode and small prefill GEMMs use more of the GPU; larger M runs them in one program. Both give the same bits, so the tiles and the split are chosen by M and N. When one program loops over all segments, the loop is not software-pipelined (num_stages 1): with pipelining across it the output was not run-to-run deterministic for some K on MI350X. Tests: bitwise row invariance across batch sizes (with and without bias), run-to-run equality, the same bits for every launch configuration and for split vs unsplit, accuracy against fp32 (gfx950); and that unsplit launches are never pipelined (CPU).
…egmented matmul At K up to 256 each of the 4 segments is a single 64-wide BLOCK_K step. When all segments run in one program (large M) the compiler folds each segment's dot into the running total, while the split launch (small M) stores the per-segment partials and reduces them afterwards, so the two launches round differently and rows depend on the batch size. Route K <= 256 to the persistent kernel, which keeps one accumulation order at every M, and add a test that compares rows of small batches with the same rows in a 4096-row batch at short K.
|
I found a batch-invariance bug in this PR and pushed a fix (bb91ae0, together with a merge of current main). When K is 256 or less, each of the 4 segments is just one 64-wide block. In that case the unsplit launch (used for large batches) rounds differently from the split launch (used for small batches), so a row's result could change with the batch size. I ran into it with Kimi-Linear at TP4: its shared-expert down projection has K = 256 per GPU, and the prefix determinism test failed. The fix keeps matmuls with K <= 256 on the persistent kernel, the same as without this PR. Larger K is untouched, so the numbers in the description still hold. I also added a test for short K (96-512). Checked on MI350X: no batch-size differences at any shape I tested (K from 96 to 2,304), the unit tests pass, and Kimi-Linear TP4 now passes every determinism check (prefix gives one output per prompt, before 1/2/1/2; 1,275/1,275 bitwise identical). |
# Conflicts: # test/registered/unit/batch_invariant_ops/test_matmul_persistent_configs.py
This comment has been minimized.
This comment has been minimized.
3c6a7a6 to
a77dc93
Compare
Motivation
In deterministic mode, SGLang runs every bf16 linear layer through a batch-invariant Triton matmul. At decode sizes (a few to a few hundred tokens) that kernel launches only a few programs and leaves most of an MI350X idle, so the decode GEMMs are one of the largest remaining costs of deterministic mode on gfx950.
This PR adds a segmented split-K matmul for bf16 on gfx950: K is split into 4 fixed segments that are summed in a fixed order at every batch size, and at small batch sizes the segments run in separate programs, so decode GEMMs use 4 times as many programs. A token's result still does not depend on its batch.
Impact
--enable-deterministic-inferenceon gfx950 with bf16 models changes. Normal mode, MI300 (gfx942), NVIDIA, and fp16 / fp32 matmuls are unchanged.Design note: the kernel never software-pipelines the loop over segments within one program (
num_stages1 there). With pipelining, some K values were not run-to-run deterministic on MI350X (seen only with 2 segments; cause not understood), and a unit test keeps it off.Modifications
batch_invariant_ops.py:_segmented_matmul_kernel(Triton): per output tile, sums 4 fixed K segments in order;SPLITruns one segment per program and stores fp32 partials, otherwise one program loops over all 4. Bias is added after the segment sum. int64 offsets._segment_reduce_kernel: adds the 4 partials in order (and the bias) and casts to the output dtype._segmented_matmul_launch(M, N): tile, warps,num_stagesand split by M and N, measured on MI350X over the bf16 linear shapes of Qwen3-8B, Qwen3.8-27B, Qwen3-30B-A3B and gpt-oss-20b (M = 1-8192). Split up to M = 1,024 (M = 128 when N > 8,192); unsplit launches usenum_stages1.matmul_persistent: on gfx950, bf16 inputs with bf16 output and K above 256 (_SEGMENT_MIN_K) go to_matmul_segmented_triton.test/registered/unit/batch_invariant_ops/test_segmented_matmul.py(gfx950, registered on the MI35x per-PR suite): bitwise equality of each row across batch sizes 1-4096 (with and without bias), run-to-run equality, the same bits for every launch configuration in the table and for split vs unsplit, and relative error against fp32 below 1%; shapes include K = 2,880 (not a multiple of the segment size) and N = 32. A short-K test checks that rows at K = 96-512 do not depend on the batch size throughmatmul_persistent.test_matmul_persistent_configs.py(CPU): the segment constants, and that unsplit launches are never pipelined.Accuracy Tests
MI350X,
rocm/sgl-dev:v0.5.22-rocm724-mi35x-20261018(ROCm 7.2.4), this PR'sbatch_invariant_ops.pyover the image's SGLang, with the deterministic-mode fixes MI350X needed then (all merged since). Server:Checks:
python3 -m sglang.test.test_deterministic --test-mode {single,prefix,radix_cache} --n-trials 50(prefix also with--return-logprob), and a bitwise comparison of 1,275 responses (the same prompt in batches of 1-50, token IDs and log-probabilities) against batch size 1. Long context (Qwen3-8B with YaRN x4): a 64k and a 120k prompt alone vs next to other requests; a 100k prompt split at different chunk boundaries by other requests; 30k-token cached prefixes.The kernel's split and unsplit paths compute the same segment sums in the same order, so moving the split threshold does not change any output bit (checked on 200 shape and batch-size points, and for every launch in the table by the new unit test); the final launch table was re-checked end to end on Qwen3-8B TP1 (all of the above, and the long-context checks below).
Long context, Qwen3-8B TP1: 64k/120k 8/8 identical, chunk boundaries 12/12, cached 30k prefix 12/12.
GSM8K (1,319 questions, greedy), Qwen3-8B deterministic mode: 93.18% before, 93.33% after.
Unit tests on MI350X (this branch's tree on
rocm/sgl-dev:v0.5.22-rocm724-mi35x-20261020):test_segmented_matmul.pyandtest_matmul_persistent_configs.py10 passed (259 subtests), existingtest_batch_invariant_ops.py6 passed, 1 skipped.Short K: before the fix in the latest commit,
torch.mmin batch-invariant mode gave rows that depended on the batch size at K = 96, 128, 192 and 256 (N = 2,048-5,120, rows of batches of 1-1,024 vs the same rows in a batch of 4,096); this showed up end to end as a TP4 difference in Kimi-Linear, whose shared-expert down projection has K = 256 per rank. With the fix every audited shape (K = 96-2,304) is batch-invariant, the same as the persistent kernel on main.Speed Tests and Profiling
Same node, image and stack as above, before and after this PR, interleaved in one session, 2 rounds each (both shown); deterministic mode, TP1;
python3 -m sglang.benchmark.serving --backend sglang --dataset-name random --random-range-ratio 1.0 --request-rate inf --random-input-len <in> --random-output-len <out> --num-prompts <n> --max-concurrency <c>. Output throughput (tok/s):MoE models gain less, since their expert GEMMs do not use this path: Qwen3-30B-A3B about +2%, gpt-oss-20b +1-2% (earlier version of the launch table).
Kernel time (µs, CUDA graphs, MI350X, bf16), Qwen3-8B layers: current batch-invariant matmul / this PR (time ratio, above 1 = faster):
The gain is at decode sizes, where most of a decode step's GEMM time goes; some mid-size prefill GEMMs are 1-17% slower, which the end-to-end numbers include.
No change without
--enable-deterministic-inference.Checklist
Review and Merge Process
/tag-and-rerun-ci,/tag-run-ci-label,/rerun-failed-ci