Repository navigation
experts3 CUDA: a split decode pair form, a column block a CTA with its K groups shared by the warps - #1139
experts3 CUDA: a split decode pair form, a column block a CTA with its K groups shared by the warps#1139CanadaApollo6 wants to merge 1 commit into
Conversation
…s K groups shared by the warps For calls with few column blocks (one to three token rows) the pair kernel leaves most warps idle. The split form gives one CTA a column block: its warps compute the per-group mma terms side by side, then each warp runs its tile's fma chain over them in group order, so the sums are expert_kernel's bits. run() takes it below a per-GPU bound set from the pair kernel's resident warps; down and the prompt kernels are unchanged.
|
Thanks @CanadaApollo6 for the split decode pair and for the careful tests: both pair forms forced against affine-4 on the same codes ( - Synthema, Ash's AI Assistant. |
… 1-4 of the kernel contract split_kernel<2, 2, 2, 4> had only its gate-5 record; contract_experts3.zig now runs it through gates 1 to 4. Gate 1: the affine family's NaN and infinite token rows give the pair kernel's bytes, some outputs non-finite. Gate 2: five runs of 4 rows, nine pairs a row over 12 routed 3-bit experts and the 4-bit shared one, guarded. Gate 3: bit-equal to the pair kernel at the 35B-A3B's 512 by 2048 (1-4 rows, a full item, five K groups) and edges. Gate 4: the affine family's integer stress, SwiGLU on exact sums, fp16 and bf16 sums proven to miss first. Planted controls, red at gate 3: zeroed inputs drop a short last round; reversed groups reorder the chain. experts3_tests.zig's Codes, Kernels and packTable become public, so the contract test packs tables as it does. docs/kernel-contract.md lists the new coverage.
|
Landed on main in 93a215b..35e46c0, with your records under your authorship plus the maintainer fix-ups. Thank you. Measured with the whole group (#1139, #1140, #1143, #1147, #1164, #1169, #1171 and #1170's Flash Next slice) against main on GB10s:
- Synthema, Ash's AI Assistant. |
What this changes
Decode rows of the 3-bit routed experts (
experts3) get a second form of the SwiGLU pair kernel for calls with few column blocks. Today a call with one or two token rows leaves most of the GPU idle. Branchqwen35-sm86-experts, one commit onmaind1018b3.experts3.cu: newsplit_kernel<2, 2, 2, 4>, the pair form ofexpert_kernel(M = 2, SwiGLU epilogue).expert_kernelgives each of its four warps a unit of its own.expert_kernelstages them.compute_stagecomputes:pfrom zero by the same four mma k16 steps, the group's scale and bias words, and the rows' input sums. It parks them in shared memory.acc = fma(xs, b, fma(p, s, acc))over those groups in group order. That is the same chain, in the same order, from the same terms, so the sums are the same bits.expert_kernel's SwiGLU store for one tile.expert_kernel, its down form and the prompt kernels do not change.experts3.zig:run()takes the split form for the pair when the call has fewer units than 7/8 of four times the warps the pair kernel keeps resident. Otherwise, and always for down, it launchesexpert_kernelas before. The bound is fixed per GPU atresolve(). Both forms give the same bits, so the choice affects only speed.experts3.zigrun()). The bound scales with SM count. On the A10 (72 SMs) it covers 1 to 3 token rows. By the same count it covers about 3 rows on a 3090 (82 SMs) and 2 on GB10 (48 SMs). It covers about 6 rows on a 4090 or an H100 and about 8 on a 5090, which takes in a drafted 4-row verify window there. The bits are the same either way. I timed it only on the A10: on GB10, H100, 4090, 5090 and 3090 the speed of this form is untimed.Receipt
Environment: NVIDIA A10 (24 GB, sm_86, 72 SMs), Linux, driver 570.148.08, CUDA 13.0
nvcc, Zig 0.17.0, built-Dsm=86 -Doptimize=ReleaseFast. Numbers are A10 (sm_86) unless marked RTX 3090. Base ismaind1018b3, built the same way on the same machine.Checkpoint:
CanadaApollo6/Ornith-1.5-35B-A3B-M3TiS-3090rev 66da2f4e (Qwen3.5-MoE 35B-A3B with 3-bit routed experts and a 4-bit shared expert), drafted by its own MTP layer.Exactness:
tf-cuda-test experts3runssameBitswith the split form forced off and forced on. The shapes are the existing one (128 by 256) and two new ones. One is wide (512 by 2048, 16 experts, 4 rows, 9 picks a row, the shared expert in the last slot). The other is short (128 by 320): its 5 K groups leave the split form's last round with one group for four warps. The pairs' SwiGLU rows and the down sums are byte-equal to affine-4 on the same codes in both forms. PASS.tf-qwen35-cudaover 64 ids:step,window,tree, a second stream beside (TF_Q35_BESIDE=64) and a resumed state (TF_Q35_PREFILL=32 TF_Q35_RESUME=1).windowandtreeequalstep, beside equalsstep, and resumed equals fresh.bench_concurrent --tokens 128 --reps 1 --alone --serial --levels 1,2at--parallel 2with MTP drafts, code and chat, T=0 and seeded T=1: 24 of 24 alone and 12 of 12 serial equal, 0 failed. This is a single run.qwen35-sm86-qmmruns, and none of it reproduces on this machine.bench_cache(2 streams, 2 repeats): qualified.Kernel time:
tf-cuda-test experts3 200times the wide shape over 64 experts. Each call gets fresh picks and the calls run as one captured graph. Times are µs a call.expert_kernelexpert_kernelThese are the
TIMElines of one run on the final commit. An earlier run on another A10 gave 45.9 / 21.2, 49.2 / 40.1, 64.6 / 57.5 and 68.2 / 75.6.RTX 3090 (sm_86, 24 GB, 82 SMs, driver 615.78.08, CUDA 13.4
nvcc, the same build flags), base and this branch alternately, each a fresh server withTENSORFOLD_MEMORY_RESERVE_GIB=4 --prompt-cache-gib 2,--context 8192 --parallel 2:A round is
bench_openai --reps 3at T=0 and seeded T=1, thenbench_concurrent --tokens 128 --reps 2 --alone --serial --levels 1,2at T=0 and seeded T=1, thenbench_cache. Plain: two rounds each. MTP drafts: one round of this branch against three of base.Every request's token hash (34 a round) is the same on this branch as on base, plain and drafted, and plain equals drafted.
Every
bench_concurrentrun reports alone 24 / 0 and serial 12 / 0, andbench_cachequalifies on every run.Decode, median tok/s, one stream, code / chat (mean of the rounds):
With drafts the change is within base's own spread (185.9 to 194.8 code at T=0 over its three rounds): the verify windows are 4 rows, which keep
expert_kernelas on the A10.Decode speed:
bench_openai --tokens 128 --reps 3, median tok/s, one stream,--parallel 1 --context 8192, code / chat. Base and this branch were run back to back on the same machine.tf-qwen35-cuda step(plain, 64 ids): 8.9 -> 7.9 ms a step (112.5 -> 126.8 tok/s).expert_kernel, so drafted decode gains only where rounds call the experts with fewer rows.Prompt speed: prompt rows take
experts3_prefill, which this does not touch.Kernel contract:
kernel-contract-limitson-Dsm=80,86,89,120,121fatbins:The "experts3 decode split" record is
254 / 255 / 255 / 255 / 255registers (sm_80/86/89/120/121), 25600 static shared bytes, bound 128.cuobjdump --dump-resource-usage experts3.fatbin,split_kernel<2, 2, 2, 4>:No stack and no local memory on any of the five, so nothing spills at 254/255 registers. On sm_120/121 cuobjdump reports 26624 shared bytes, while the contract's limits check reads 25600 static bytes there; I have not traced the 1024-byte difference. For comparison, the unchanged pair
expert_kernel<2, 2, 2, 4>shows 255 registers and a 48-byte stack on sm_80 in the same dump.The pair and down records are unchanged and current.
The problem count is 877 on this branch and on base. All of them are records of kernels this branch does not touch, compiled differently by nvcc 13.0.88 here.
zig build kernel-contracton the A10:SUMMARY 75 kernels: 75 pass gates 1-4, 0 fail, 0 known findings; 5 controls: 5 red as planted, 0 missed; 932 limit problems. Base d1018b3 gives the same line.Tests:
tf-cuda-test experts3: passes.zig build test native -Dsm=86: 2958 of 2973 tests pass, with 14 skipped and 1 failure. The failure iscuda_cost_identity's "a changed payload at the same path invalidates it". Base d1018b3 fails the same test inzig build test native -Dsm=86on this machine (2958 of 2973), so the failure does not come from this branch. It passed once in aqwen35-sm86-qmmrun on the same machine, so it is flaky here.tools/zig/lean_check.py: 444 problems on this branch and on base, none added.zig fmt --check: clean.Not run:
experts.cu). The same split applies there, but this PR keeps to one kernel.Checklist
window/tree==step), a resumed state equals a fresh one, and concurrent equals solo, at the engine level and in one servedbench_concurrentrun.split_below, which this change adds, so it does not build on base. It passes after the change.