Repository navigation
qwen35 cuda: fuse the dense MLP's NVFP4 prompt gate and up projections into one launch - #2410
GustavBlack wants to merge 11 commits into
Conversation
|
Thanks @GustavBlack, this is careful work. The fused tile keeps each part's
|
|
Thanks for the careful read. All four are in 1. Registry line count — the two prompt_many records moved to their own file, 2. CHANGELOG — under 3. Where it helps — widened to sm_120, with the served A/B run on a local 5090. The admission screen now also admits the RTX 5090, the records drop to
The candidate is equal or faster at every length and nothing falls below the 97% floor. The 2k prompt arrives as one 2093-row chunk (past the screen), so fusing saves the pair's launch overhead and prefill comes out ~9% quicker; 8K and 32K arrive as 4096-row chunks and the saving amortizes to ~1.5-1.8%. Decode holds parity: solo streams never below 0.999x per cell, the eight-concurrent wave's aggregate 1.001-1.002x of main's (per-cell wave ratios reshuffle between legs in both arms; the aggregate and the solo streams are the stable reads). Token hashes: zero mismatches, all cells and all legs — and the same prompts hash identically to the GB10 run's completions across hardware. On the timing question: yes — the separate side is timed with the same 4. One copy of the tile. Both images now compile the same body: One honest record correction: with the screen widened, sm_120 became measurable for prompt_many, and the audit's parser puts the fused kernel's static shared at 512 bytes there, as at sm_121. The 1536 in the old sm_120 record was |
db6f672 to
9e426c0
Compare
|
Two line counts from my summary above moved with the rebase onto |
|
Thanks for the quick turn, @GustavBlack. The records file keeps
The line's ending and its sm_120 wording are ours to settle at landing. |
87d979b to
152f758
Compare
|
Thanks @GustavBlack for the new round. The CUDA common-copy row for
One optional note: the new CHANGELOG wording says the fusion "buys no prefill" on both chips. Your 5090 served A/B shows cold 2K at 0.936 of main's median. The line's final wording is ours to settle at landing, so please just say which reading you'd stand behind once the kernel-only numbers are in. Once these are in, we will run the device check on our side on the GB10. It covers the 27B NVFP4 against main and one NVFP4 model whose prompts run Thank you again for the careful turnaround.
|
152f758 to
ed4f83c
Compare
…s into one launch Qwen3.8-27B's dense MLP sends its gate and up NVFP4 prompt projections through two prompt_lean launches that share one BF16 input. prompt_many.cu ports prompt_lean's FP4 128x256 tile into tf_prompt16_many, which takes two to eight such projections at once: the parts stage in shared memory, tiles distribute by per-part first offsets, and each part writes its own output. nvfp4.zig's matmulMany builds the 48-byte part ABI, validates the group (all fp4, shared k, distinct outs) and launches on a dynamic-shared module. A dense layer's prompt pass routes through a new admission screen (cuda_modelopt_many.zig) when the GPU is a GB10, rows run 2048 or more, and the weights are ModelOpt NVFP4 at k 5120 and n 17408; every other shape keeps today's two launches. prompt16.cu is untouched, and the BF16 arm (ashhart#1764) stays out of it. The kernel contract holds the bit-exact receipt: every output byte equals the separate prompt_lean launches on the same BF16 input and NVFP4 weights (nonfinite, repeat and shapes pass, five runs byte-identical, 5 shapes bit-equal at 0.000 of the float64 bound), with a planted control that swaps two equal-width output bindings and goes red at shapes. The records are opt-in at sm_121 and later_pending for the nvcc 13.2/13.3 receipts. Same-process A-B-A screening on a 5090 has the pair 4.8-5.9x faster at rows 2048 and up; the served A/B on a GB10 with nvidia/Qwen3.8-27B-NVFP4 holds every cell at or above the two-main-median floor (worst 0.980, on the concurrent decode wave) with byte-identical completions. Closes ashhart#1540
… to the RTX 5090 The lean tile now compiles once: common/prompt_lean.cuh holds the body prompt_lean and tf_prompt16_many share, so a later change to the tile cannot split them; prompt16.cu keeps its prompt_kernel and calls the shared body from a wrapper, and the audit re-measures prompt16 FP4 lean at 249 registers on sm_120/121 (was 253). The fusion's admission screen widens from GB10 to the RTX 5090, its records move to prompt_many_records.zig to keep registry.zig within its 600 lines, and the records drop to min_sm 120. On the 5090 the same served A/B holds: cold 2k/8k/32k prefill equal or faster than the separate launches (worst 0.936/0.988/0.983 of the two-main median), solo decode at parity (worst 0.999x), the concurrent wave's aggregate at 1.001-1.002x, and every completion byte-identical by token hash.
…ed into common The mbarrier arrival is now common/prompt_lean.cuh's mbar_copies, so prompt16.cu no longer owns it and the row was stale: the guard read it as an unlisted copy and stopped CI. cuda_common_check.py: 53 local copies, 0 violations; its 9 unit tests pass.
… holds Nine multi-line comments in prompt_lean.cuh, nvfp4.zig and cuda_modelopt_many.zig became one line each at or under 120 columns; the words are kept, the layout is not. lean_check.py: 116 problems on the base, 0 new.
…and format the print The lean arm returns rig.bytes, which downloads every output; the fused arm launched and synchronized only, so the A-B-A charged one side for a copy the other never paid. landed() downloads each fused output, and both arms now time their download. The runner's nvfp4-prompt-many-contract and nvfp4-prompt-many-bench commands had no build step; they sit beside qmm's now.
…_tests stays lean
6824f17 to
649d568
Compare
Qwen3.8 CUDA NVFP4 bit-exact multi-projection prefill launch fusion
Closes #1540 (dense-MLP
{gate, up}NVFP4 prompt projections fused into one kernel launch, bit-exact with today's separate launches).What
zig/kernels/cuda/prompt_many.cu(new):tf_prompt16_many—prompt_lean's FP4 128x256 tile over per-partfirsttile offsets; 2-8 NVFP4 projections sharing one BF16 input, each writing its own output.zig/kernels/cuda/common/prompt_lean.cuh(new): the lean tile's one shared body.prompt_lean(inprompt16.cu) andtf_prompt16_manycompile the same fragments, copy stream, mma chain and epilogue from it, so a later change to the tile cannot split them;prompt16.cukeeps itsprompt_kerneland a wrapperprompt_lean(579 -> 332 lines).zig/src/cuda/nvfp4.zig:matmulMany(validates 2-8 members, all fp4, shared k, distinct outs), 48-byteManyPartABI, module/function wiring.zig/src/families/qwen3_5/cuda_modelopt_many.zig(new): admission screen (GB10 or the RTX 5090's sm_120, rows >= 2048, k=5120, n=17408, 2-8 members, ModelOpt weights); everything else falls back to today's two launches. The BF16 arm (flashnext cuda: prompt passes with the same bits and less work, and a prompt scratch sized by use #1764) is untouched.zig/src/families/qwen3_5/cuda_forward.zig: dense-MLP site routes throughpair()when admitted, else the twoprojAgaincalls unchanged.zig/tests/cuda/contract_prompt_many.zig(rule: every output byte equals separate prompt_lean launches on the same BF16 input and NVFP4 weights; planted control swaps two equal-width output bindings and goes red at shapes), registry records inzig/src/core/contract/prompt_many_records.zig(keepsregistry.zigat 600 lines; opt-in, min_sm 120,later_pendingfor nvcc 13.2/13.3 receipts), wired intocontract_tests.zig/contract_limits.zig/tests/cuda/main.zig(nvfp4-prompt-many-contract|benchsubcommands),zig/build/cuda_kernels.zigimage entry.Receipts
Kernel contract, RTX 5090 (sm_120, nvcc 13.0), focused run
tf-cuda-test nvfp4-prompt-many-contract:PASS nonfinite— byte-equality rule holds under non-finite classesPASS repeat— five runs byte-identical (4,456,448 bytes), inputs and guards untouched, no overlapPASS shapes— 5 shapes bit-equal to the replaced kernel (worst 0.000 of the float64 bound), 0 edges refusedCONTROL ... red at shapes, as planted(swapped bindings at 2048x17408x5120)Same receipt on GB10 (sm_121, nvcc 13.0, DGX Spark): identical wording —
PASS nonfinite/PASS repeat(4,456,448 bytes) /PASS shapes(5 shapes bit-equal, worst 0.000, 0 edges refused) /N/A sums; control red as planted.Rebased onto upstream/main
ca33d5bb4(the second rebase since opening; the first onto519136dd9) and re-verified there each time: native build exits 0, and the focusednvfp4-prompt-many-contractrun at that base confirms the same gates green (PASS nonfinite/PASS repeat(4,456,448 bytes) /PASS shapes(5 shapes bit-equal, worst 0.000) / control red as planted), with the all-SM audit's four prompt rows(record current)at sm_120 and sm_121.All-SM audit (
kernel-contract-limits zig-out/fatbin):PASS limits sm_121 prompt_many NVFP4 lean: 248 registers, 512 static shared bytes, bound 256 (record current), and the same(record current)at sm_120; f32-out twin 246 registers; sub-120 SMsN/A below_minimum(opt-in sm_120+). The records now live inprompt_many_records.zigandregistry.zigis back under its 600-line cap.The shared-body restructure re-measured clean: prompt16 FP4 lean now audits at 249 registers on sm_120/121 (was 253) and its record carries that number; prompt_many's usage is unchanged. One record correction became possible with the widened screen: sm_120 is now measurable for prompt_many, and the audit's parser puts the fused kernel's static shared at 512 bytes there, as at sm_121 — the old sm_120 value of 1536 was
cuobjdump --dump-resource-usage's shared-carveout artifact (visible for every sm_120/121 cubin of these translation units, pre-restructure source included) that had never been audit-validated while sm_120 sat belowmin_sm.Two pre-existing conditions in the audit, both outside this diff and unchanged from upstream/main:
(record stale)under nvcc 13.0 — theirlater_pendingreceipts await nvcc 13.2/13.3 per the contract policy.dsv41_x3ldfails withSymbolNotInImageat sm_121: the registry instantiatesld_kernel<2,8,1,8,8,0>butx3ld.cudoes not. This is upstream's pending record, untouched here; flagging it for whoever owns that record.Same-process A-B-A screening on RTX 5090 (parts=2, gate+up): rows 2048: 23.14/21.54 ms separate vs 3.52 ms fused (6.36x); 4096: 6.82 ms (6.89x); 8192: 13.69 ms (5.50x). The separate side is the same
prompt_leantile in the same warm process —contract_prompt_many.zig's A-B-A timesmatmulTile(..., .lean)per member against the fused arm with identical fixtures, rep loop and cache warmth.Served A/B on GB10 (DGX Spark,
nvidia/Qwen3.8-27B-NVFP4)main/candidate/mainalternation with five repeat candidate legs; one server per leg (--no-drafts --prompt-cache-gib 0 --context 40960), greedy with fixed seed, raw SSE timings, every record ok. Cold prefillt_first(seconds):No metric below the 97% floor of the two-main median. Decode does not regress: solo streams 0.999-1.006x per cell; the eight-concurrent wave's worst cell is 0.980x. Token hashes: every candidate completion is byte-identical to main's for the same prompt and seed, all cells and all legs.
Admission fires on the served path: an instrumented candidate logs
[pair] fired rows=2097for the 2k prompt (the 32-row warm-cache chunk correctly falls back). On GB10 the end-to-end effect is parity, as the memory arithmetic predicts: the pair is bound at ~264 MB/layer against ~273 GB/s, and fusing saves only the shared 21 MB input re-read (~0.3% of prefill, under run noise). The kernel-level win shows where separate launches are bound instead: the 5090 screening above.Served A/B on RTX 5090 (
nvidia/Qwen3.8-27B-NVFP4)Same protocol as the GB10 run on the same fixtures:
main/candidate/mainplus three candidate repeat legs, one server per leg (--no-drafts --prompt-cache-gib 0 --context 40960), greedy with fixed seed, raw SSE timings, 114/114 records ok. Cold prefillt_first(seconds):The candidate is equal or faster at every length, and no metric falls below the 97% floor. The 2k prompt arrives as one 2093-row chunk (past the screen), so fusing saves the pair's launch overhead and prefill comes out ~9% quicker; 8K and 32K arrive as 4096-row chunks and the saving amortizes to ~1.5-1.8%. Decode holds parity: solo streams never below 0.999x per cell, and the eight-concurrent wave's aggregate throughput is 1.001-1.002x of main's (per-cell wave ratios reshuffle between legs in both arms; the aggregate and the solo streams are the stable reads). Token hashes: zero mismatches — every candidate completion is byte-identical to main's, all cells and all legs; the same prompts also hash identically to the GB10 run's completions across hardware.
GB10's served numbers above stand with the widened screen: level with main there (1.008 / 0.993 / 0.993 of the two-main median at 2K/8K/32K), as the memory arithmetic predicts.
Related
atc overlap --json, 2026-11-14 ~12:54 UTC, base eeeb026, head 649d568, re-run after the latest rebase: unchanged): six of the board's entries conflict, five of them on CHANGELOG.md alone (XPU: Qwen3.8-27B attention merge with 16 subgroups per head, bit-identical #2799, hip flashnext: a shared round's streams draft in one head pass a step #2696, glm cuda: TF_GLM_GRAPHS=1 replays verify windows as CUDA graphs (#1490) #2279, dsv41 metal: image prompts #1722, dsv41 metal: the vision tower's kernels under the kernel contract #1721 — every open PR adds an Unreleased bullet, so whoever lands second rebases; landing order is yours to sequence). The real one is idol: Idol's denoiser on CUDA (stacked on #1766) #1767 (idol CUDA denoiser), which shareszig/tests/cuda/contract_limits.zigandzig/tests/fixtures/gradient-serving.tsvand merge-tree conflicts in both — it will need a rebase after this lands, or coordination if it lands first. Four entries clean; the board's snapshot is limited (otherPullRequestsLimit, pullRequestsLimit, timeLimit), so 68 further entries are unknown, and GitHub search confirms qwen35 cuda: fuse the dense MLP's NVFP4 prompt gate and up projections into one launch #2410 is the only open pull request for Qwen3.8 CUDA NVFP4 bit-exact multi-projection prefill launch fusion #1540. ATC session for Qwen3.8 CUDA NVFP4 bit-exact multi-projection prefill launch fusion #1540: 15395910710062 (public, anonymous).