Repository navigation
core: add the row-split pair seam and split simd_qmm over K - #1432
Conversation
|
Thanks for the pair work: the transport, the watchdog and the contract records for the four new kernels are good. The four kernels pass their gates on an M3 Ultra here. One thing has to change before this stack can land, and it starts in this PR. The pair's tokens must equal the single Mac's. The seam computes each layer's o_proj (or GDN out_proj) and down_proj as two fp32 K-halves, one per rank, then rounds An exact design keeps every sum on one rank. Split o_proj, out_proj and down_proj by output rows, not by K:
No cross-rank partial sum exists then, and no tie policy is needed. It costs about two all-gathers per seam instead of one reduce, with the same weight memory per rank. GLM-5.3's two-Mac path. This rewrites the shipped glm_dsa transport: the device spin-wait becomes a host relay that reopens the encoder at every exchange, and several Checked here: the branch merges cleanly into main, |
|
Thanks for the review. The blocking item is addressed: the pair's tokens now equal the single Mac's, byte for byte, on every fixture in the gate. Exactness. Stage-3 pair gate with Stage 1 against main's own binary. Rather than compare Mechanism. The old seam computed o_proj, GDN out_proj and down_proj as two fp32 K-halves and summed them, which cannot be bit-equal. It is now split by output rows: each rank reads the whole input K, computes its rows with full K, and writes bf16 at its row offset (2,560 of 5,120 hidden rows; 8,704 of 17,408 for down_proj). Exactness also needed a reduction-tree fix: Receipts. One honesty note: the two-Mac A/B on GLM is not reproducible on 2x64 GiB, so this PR's transport coverage rests on the qwen27 pair receipts rather than that A/B. There is no GLM two-Mac receipt, and there will not be one. Next step. The refreshed work is 16 commits on |
|
Thanks @akol1. Splitting by output rows with full K on each rank is the exact design we hoped for, and checking the single-Mac path against main's own binary is a useful receipt. The PR branches haven't changed, though. #1432's head still sums fp32 K-halves across ranks ( On GLM-5.3: since there won't be a two-Mac GLM receipt, please leave glm_dsa's transport as it is on main. That means the changes to Once #1432 carries the refreshed code, we'll review the row split and the |
137cf63 to
0b79f9c
Compare
|
Refreshed and force-pushed, one step per branch, all rebased on current
Each commit message carries the same "stacked on ..." line, so the stack is readable from GLM. Honesty on what was and was not run. The pair in #1435 runs both ranks through Unsplit callers.
The row split and the |
|
Thanks @akol1 for moving the seam to an output-row split and for leaving glm_dsa's transport as it is on main.
Next we will run the pair contract and the |
05d69b8 to
55065d5
Compare
|
Refreshed and force-pushed:
Gates on |
|
Thanks @akol1. All four points are in on
One small addition, not blocking: a Next is our run of the whole series on an M5 Ultra, at #1437's head merged onto main:
If those pass, the series can land in order, starting with this PR. |
|
Thanks @akol1. Here is our run of the whole series on an M5 Ultra: #1437's head ( The pair checks all pass:
One Mac against main, alternating base, new, new, base:
One Mac should run main's program at main's speed, so this is the one thing left before the series lands. The two Metal kernels whose one-Mac code changed are the first place I'd look. |
55065d5 to
2d58921
Compare
|
Thanks @akol1. |
c152fc5 to
cf6f0cb
Compare
|
Heads-up before you spend more cells:
The two |
|
Thanks — the two kernels are back to The fix is the separate instance, and it is in the second branch's kernel commit ( They are
Correction to my last note, before you spend on it: I wrote that A/B on one Mac, in the shape of your prefill table. Both builds served the same checkpoint and returned the same
On this Mac a one-Mac pass binds the fixed prompt instance directly: with Fresh receipts on this head ( Your |
cf6f0cb to
52b11e4
Compare
|
Rebased onto the current
The series-level cold-prefill A/B on one Mac, re-run at the rebased tip against On your M5 cells: this rebase did not touch either This room is one Mac now: the second Mac the pair receipts were recorded on is gone (the cluster was broken), so a fresh two-Mac |
Step 1 of 6 of the two-Mac 27B series; stacked on main (1b9a987) alone, and steps 2-6 stack on this commit. The seam exchanges whole output rows instead of K halves: rank r owns 2,560 of the 5,120 output columns (8,704 of 17,408 for the down projection), reduces over the full K on its own rank, and `pair_gather` copies whole rows in bf16 without doing any arithmetic, so no exchange rounds or sums a partial product across ranks. A broken link surfaces as bf16 NaN `0x7FC0`. The seam's four kernels (`pair_stage`, `pair_gather`, `pair_argmax`, `pair_pick`) move bf16 frames and compare values only, so they carry no sums and no accumulator's error. One Mac fails a row holding any NaN or ±Inf, so `pair_argmax` reports such a row as NaN bits with bit 31 set in its id word, and `pair_pick` turns that flag into the id sentinel: both Macs then refuse a non-finite round where one Mac would, through the same downstream check as a broken link. Five planted controls (a stage on the peer's half, a gather writing its own half twice, an argmax and a pick tie to the higher id, an argmax that does not flag) each go red on the gate named in `docs/kernel-contract.md`; the next commit adds the sixth, for `pair_pick`'s flag rule itself. That transport is a new module, `pair_rows.zig` and `pair_rows.metal`, over `pair_rows_layout.zig`'s slots and the relay protocol unchanged. `pair.metal` and `pair.zig` stay for GLM-5.3-Flash, whose transport this series does not touch: a shipped path cannot change without an A/B against main. `simd_qmm` gains a K-chunk width: `chunks(n, whole)` lets a rank that still holds the whole output width keep the split one Mac derives from that width, instead of one derived from the rank's narrower slice.
The pick's sentinel rule was the one pair rule without a planted control. The argmax control proves the row's non-finite flag is written, but nothing proved `pair_pick` acts on it: a pick that dropped the `((am | ap) & PAIR_FLAG) != 0u` test would still pass every gate. The sixth control replaces that test with `false`, planted only at the nonfinite fill -- at shapes both ranks' words are finite, so dropping the rule is invisible there and the control has to be red at the gate that plants it. The control is red as planted, and the pair docs now count six.
52b11e4 to
8b9319f
Compare
What this changes
Step 1 of 6 of the two-Mac Qwen3.8-27B series, rebased on
main(7c068895). The branch has two commits: the seam (dcccfe7b), then the pick's planted control (52b11e46). Steps 2-6 stack on them (qwen27/rank-slice,qwen27/pair-lifecycle,qwen27/pair-tests,qwen27/pair-tools,qwen27/pair-docs), and each of their bodies says what it stacks on.This commit adds the seam a pair exchanges through, and nothing above it.
zig/src/core/pair_rows_layout.zigfixes the exchange window's ABI;zig/src/core/pair_rows.zigandzig/kernels/metal/core/pair_rows.metalimplement the four launches:pair_stagecopies this rank's own half of a frame row into the exchange slot (a bf16 copy, so it carries the frame's bits),pair_gathercopies each rank's half back to its own offset in the frame row (a bf16 copy, no arithmetic),pair_argmaxreduces one rank's logit spanlo..lo+nto (value, id) with the lower id on a tie and a row-level non-finite flag, andpair_pickchooses over both ranks' argmaxes — a flagged row or the broken-link word draws the same0xFFFFFFFFsentinel, so one downstream check covers both, and that sentinel is never an id.zig/src/core/pair_rows_loopback.zigis the peer for a host without a second Mac: both ranks on one GPU through exactly the seam. A broken link makespair_gatherwrite bf16 NaN (0x7FC0) on both halves, so a row that lost its peer never reads as a valid one.The branch's second commit gives
pair_pick's flag rule a planted control of its own:zig/tests/contract/metal_pair_controls.zigreplaces((am | ap) & PAIR_FLAG) != 0uwithfalseand gates it atnonfinite, where both ranks' words are finite — so dropping the rule is invisible atshapesand only the flag test catches it.metal_main.zigroutes the newpick_no_flagcontrol to the pick wave, and the contract section now counts six controls. That is the same shape asargmax_no_flag, which holds the argmax's rule.simd_qmm.chunks(n, whole)makes the pair's K split the rank's own total width, so a split matrix's K chunk tree is the tree one Mac uses over that width; every unsplitsimd_qmm.callkeeps passing 0 or the full width, so its bits aremain's.zig/src/core/simd_qmm.zigpins the arithmetic, e.g.chunks(3168, 6240) == splits(6240)andchunks(6240, 0) == splits(6240).glm_dsa's pair transport (zig/kernels/metal/core/pair.metal,zig/src/core/pair.zig) is untouched: this commit neither edits nor re-registers it, and no record here names its kernels.Receipt
52b11e46defdfe8fc82db1266fd21838f177750d) on top ofmain(7c0688951584497dad8b0d5894b657d432fa917f); no MLX or PyTorch; macOS 27.0.1 on an Apple M4 Pro (Mac16,11), Metal only (applegpu_g16, no tensor units), Apple metal version 32023.921. No checkpoint: the contract gates run the launches on the GPU and compare them with float64; they do not read weights.tools/bench_concurrent.py --alone --serialortoken_shaagainst"draft": false: n/a at this commit — nothing serves yet, and the seam's exactness is the contract's own word-level comparison. The four pair launches pass all four gates, e.g.PASS nonfinite metal pair stage pair_stage: 5 non-finite and 40955 finite outputs match the float64 classes;gather's line is the same shape with10 non-finite and 81910 finite;PASS nonfinite metal pair argmax pair_argmax: the stated rule holds: a winner is (value bits, id): NaN loses every comparison, a row of only NaNs reports the -inf sentinel with id 0xFFFFFFFF, and a row with any non-finite logit reports NaN bits and id | 0x80000000;PASS nonfinite metal pair pick pair_pick: the stated rule holds: the pick copies one rank's (value, id) word pair; an id with bit 31 set, and the broken-link word, draw 0xFFFFFFFF: the same sentinel, so one downstream check covers both.PASS limits: 313 registered Metal launches against M1-M5, 0 problemsis the same run's register check;SUMMARY 4 kernels: 4 pass gates 1-4, 0 fail, 0 known findings; 6 controls: 6 red as planted, 0 missed; 0 limit problems; gates 31 ms, limits 127 ms(the run reads--only "metal pair", so the tally is the pair's four launches and their six controls).FAIL shapes control: metal pair stage, this rank stages the peer's half: at 8x5120x5120 output 0 is 8.593750e-1 where float64 gives 5.444336e-2: inf of its bound;FAIL shapes control: metal pair gather, this rank writes its own half on both sides: at 8x10240x5120 output 0 is -7.656250e-1 where float64 gives 1.572266e-1: inf of its bound;FAIL shapes control: metal pair argmax, a tie goes to the highest id: at 8x1x124160: row 0: (2e0 186240) where its own rule gives (2e0 124160);FAIL nonfinite control: metal pair argmax, a nonfinite logit is not flagged: row 0: value 2e0 where a non-finite logit must write NaN;FAIL nonfinite control: metal pair pick, a flagged row is not turned into the sentinel: row 0 picks 429031 where its own rule gives 4294967295;FAIL shapes control: metal pair pick, a tie goes to the higher id: at 32x1x1: row 2 picks 702 where its own rule gives 302. Each is followed byCONTROL …, red at <gate>, as plantedin the log.zig build test-kernel-contract's filtered--only simdrun passes four kernels and zero controls,SUMMARY 4 kernels: 4 pass gates 1-4, 0 fail, 0 known findings; 0 controls: 0 red as planted, 0 missed; 0 limit problems; gates 30263 ms, limits 114 ms.tools/bench_openai.py: n/a, no serving path at this commit; the pair's speed needs two Macs and is measured in step 5's receipts, not here.simd_qmmkeeps every unsplit call's split, so a one-Mac prompt reduction is bit-for-bit the same tree. The series-level cold-prefill A/B on one Mac, final tree (7718b1c2, binary sha2560ed3d1a35d8332c4…) againstmain(7c068895, binary sha256415303535b9593fd…), bothtensorfold-native 1.1.8(cached_tokens == 0, 2048/8192/16384 rows, 2 repeats a side): series/main median ratios 1.00016 at 2048 rows (92.310 vs 92.295 tok/s), 1.00013 at 8192 (90.108 vs 90.097) and 1.00018 at 16384 (87.065 vs 87.049); the medians differ by at most 0.02% while one side's own two reps differ by up to 0.05%, so neither a speed-up nor a slowdown is claimed. This GPU binds the simdgroup prompt variant (applegpu_g16, no tensor units), so that A/B is the fallback path, not the M5 prompt-kernel cell.zig build test:568/568 steps succeeded; 1279/1298 tests passed (19 skipped), exit 0.zig build test-kernel-contract:43/43 steps succeeded, exit 0.zig build dev-tools:215/215 steps succeeded.zig build tf-qwen27-tp-check: that step does not exist at this commit (no such step, expected: step 4 adds it).zig fmt --check build.zig zig: clean (fmt rc=0). Lean rule:lean: 425 problems, 425 on the base, 0 newagainstmain's baseline, exit 0 (tools/zig/lean_new.py).tf-qwen27-tp-checkarrives in step 4, on weights this commit does not need); a two-Mac run (the serving link arrives in step 3 and the supervisor in step 5); the unfiltered kernel corpus (it stops atUNSUPPORTED metal moss image: the family runs on tensor units (M5) aloneand needs a recorded rotary file, so the gates above run with--only). The series' two-Mac acceptance receipt was recorded before this rebase, and the second Mac is gone (the cluster was broken), so that receipt cannot be re-run and stays dated to its run; CUDA, HIP/RDNA, M1-M3, M5 and Windows are not in this room.Checklist
"draft": false, a resumed prompt equals a fresh one, and concurrent equals solo. — no serving path at this commit; step 3 refuses drafts and step 4's fixtures are greedy, so there is nothing drafted to compare yet.simd_qmm's K split, which stays inside the rank's own width and is pinned by host tests.SUMMARYline is in the receipt. — the four launches have records (zig/src/core/contract/metal_pair_records.zig), a contract section indocs/kernel-contract.md:146, and theSUMMARYabove.main's K split.applegpu_g16); the launches are also offline-compiled against the M1 and M2 limits by the contract run; no CUDA, HIP, M1-M3, M5 or Windows.127.0.0.1only in the loopback peer and no user path, hostname or peer address.