Repository navigation
Conversation
54957a6 to
53a0d27
Compare
|
6a3be2e to
0b4f1f4
Compare
|
All three are fixed on
|
|
Thanks @akol1. The exemption is gone, stops act only on the recorded pids after checking the binary, and host keys stay verified. Three asks:
We are accepting this series on the loopback gate plus your two-Mac receipts, and the refreshed bodies quote none. Please post a two-Mac |
e1c16eb to
2ee8596
Compare
|
Refreshed and force-pushed:
Two-Mac acceptance on the refreshed headsBuilt the refreshed series at its tip (
So the acceptance condition is met on the refreshed heads, two-turn case included, and the refreshed bodies quote this receipt. One honest line: Gates on |
|
Thanks @akol1. The oracle now records its GPU family and Two small things, not blocking:
|
eddd27e to
8060011
Compare
8060011 to
d2e1e62
Compare
|
Both asks are in on A runtime per case. The recorder refuses a pair serve. That guard could not fire before this commit: With that pair serving, the recorder now refuses: A bug this flushed out in the acceptance script.
Fresh receipts on The head moved once more since my heads-up: it is now |
d2e1e62 to
42d938e
Compare
|
Rebased onto the current
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 |
|
Thanks @akol1, the tools look good: the oracle records its build per case and its GPU family, the recorder refuses a pair, and the /health line one Mac reports doesn't change. There's one fix before this can land, and it's small. In /bin/sh -c 'set -eu; chk(){ return 1; }; up(){ chk; echo started anyway; }; if up; then echo up-ok; fi'That prints "started anyway" and then "up-ok". Please make it That fix is all this PR needs. It lands after #1432 to #1435, in series order. |
42d938e to
ac0688c
Compare
|
Rebased onto All three fixes are in
Receipts, run here on this head; The first is your three-line repro with Numbers from this head, run here, not carried over: The supervisor's two-rank paths still need two Macs, and the second Mac this receipt was recorded on is gone; the checks above are the one-host failures, and the host tests plus step 4's loopback gate carry the pair comparison. |
|
Thanks @akol1. All three fixes are in. |
ac0688c to
2e3a3f8
Compare
|
Rebased: Numbers from this head, run here, not carried over: The two-Mac |
`main`'s 5c9706f moved the 27B window timing out of the lane backend into `zig/src/native/qwen27_costs.zig`: the lane backend's `measure` now returns that file's `measure`, and the `cost_rule.fastest(timer, i)` loop lives there. `zig/cost_rule_cases.zig` still embeds `qwen27_lanes.zig` and looks for the loop in it, so the assertion fails on `main` itself: `zig build test-cost-rule` reports `29/30`, and `zig build test` depends on that same run, so any branch on top of `main` starts red. This commit retargets the assertion to the file that owns the timing and keeps the lane-backend half of the check: `qwen27_costs.zig` contains `cost_rule.fastest(timer, i)`, and `qwen27_lanes.zig` no longer does. No production code changes; one test file, three lines. A prerequisite, not part of the two-Mac series: step 1 of 6 (`qwen27/rank-slice`) stacks on this commit.
Step 2 of 6, stacked on step 1 (`qwen27/pair-seam`), on the prerequisite cost-rule retarget (`3abfc26e`), and on main (`c39c09b9`); step 3 stacks on this commit. A rank's layer program now carries one `Reduce` interface instead of assuming the whole matrix. Attention's o_proj and the MLP's down_proj seam this rank's output columns, gather the whole frame, and let the producer kernel reduce over full K on its own rank; the head seams the last row's logits and takes the pair's argmax over the whole vocab, so the picked token is the single Mac's token, not rank 0's half-vocab pick. On one Mac `reduce` is null and every seam is compiled out of the program: the single-Mac program, its digests and its tests are the ones that ran before. The rank-local K-split is fixed while the slice is. A projection's split is keyed on the checkpoint tensor's rows, not the view's rows: `Triple` carries `whole` (the matrix's row count) and `prepare` derives `tensorRows` from it, while the M5 and prompt tables keep the checkpoint rows they always keyed. `in_proj_qkv` loads as three blocks of one tensor (10,240 rows), so its gate/up-style stack keeps main's 4 K slices on 6,144 rows instead of reading the q block's 2,048 rows and splitting into 8; every width main served on one Mac keeps main's split, and host tests "a rank's row slice keeps the checkpoint matrix's K split" and "every one-Mac stack keeps main's K split" cover the eight stacks (qkv 10,240 rows: 4, q 6,144: 4, against the 2,048-row view's 8). `Part` drops its unused `k0`, `Triple` its `k`/`k0`, and `Shard`/the family's private `cols`/`attentionCols`/`gdnCols`/`downCols` and the column-window code `prepare`/`packTiles` used them for are gone, with `config_test.zig`'s column-span assertions deleted and the row fields left as they were. `simd_qmm.call` takes the rank's `whole` width and `chunks(n, whole)` picks the chunk fence from it, so a rank's K tree is its own full matrix's while a one-Mac stack passes 0 and keeps exactly main's fence (the core test pins `chunks` on 6,144/12,288 and 5,120/10,240 at 8, and on 6,144 and 5,120 alone at 16, which is the pair's own-width split the `gn` forwarding is there to avoid). `quant_gpu.strided` forwards the `gn` of the linear it serves. `q27_attn_merge` takes the rank's head count (buffer 11) and strides a row by that many heads instead of 24 (`G * 4`), so the non-prompt multi-row rounds a served chat feeds every earlier reply through merge the rank's own rows; `attention.zig` binds `g.heads`. The buffered form is only what a pair runs. One Mac has its 24 heads at compile time, and main's kernel used them there (`G * 4`, and `PG * 4` in the prompt merge), so each site now has a second instance: `q27_attn_merge_single` and `q27_attn_prompt_merge_single` carry main's body verbatim — same reduction, same float arithmetic, main's indexing, no `heads` or `kv_heads` buffer — and the encoder takes a trailing `split` and picks `io[4]`/`prompt[1]` for a pair and `io[10]`/`prompt[2]` for one Mac (`decode_round.zig` passes `r.reduce != null`). The two instances compute the same values, so only the emitted code differs. A function constant would need `MTLFunctionConstantValues`, which the runtime library never creates (`PipelineBuild` would fail the contract's first gate), so the second compiled instance is the route that needs no new Metal machinery. The config gains its shard parse (`config_test.zig` covers it), the embedding table stays whole on every rank — only the head is sliced — with `embedBound` reading the table's bound rather than the family's `vocab_size`, and the session keeps the pair's pick as the round's token and surfaces `error.PairFailed` from the head seam's sentinel rather than returning `maxInt(u32)`. The seam's `Reduce` is threaded through `forward.encodeTp`/`session`, and the prompt attention kernel's signature follows the seam's reopened encoder.
…ith the peer Step 3 of 6, stacked on steps 1-2 (`qwen27/pair-seam`, `qwen27/rank-slice`) and main (`c39c09b9`); step 4 stacks on this commit. The serving path learns the pair: a rank reads its rank-local shard, the serve loop routes every seam through the link, and the head's pick is the reply. A pair refuses what it cannot keep: vision, a rank count the shard does not match, and a drafter whose state only rank 0 holds, and it fails closed when its watchdog cannot be started. it fails closed when its watchdog cannot be started. The slot accounting understands that both ranks hold the frame. A pair also keeps three serving rules. Its prefill chunk is capped at the seam window's rows (`pair.chunk`), because each chunk stages a seam and the window holds `batch_rows` of them; the host test pins that the served chunk times the widest seam fits `pairBytes()`. A sampled or grammar-constrained request is refused at the head of `prefillFn` (`error.PairGreedyOnly`) instead of being answered greedily and failing on the second token, with a name in `words()` and a host test. And neither rank resumes a slot in place: rank 1 resets the slot at each round's `begin` (`link.follow`), so `attach` turns `slot_resume` off for a pair (`lb.resumesInPlace`), or the ranks would part at the resumed offset; a host test pins the rule. A start-up step that fails after the watchdog is up is undone: the two threads live in `qwen27_link.Workers`, so `attach` sets their `quit`, stops following and joins the watchdog, and a host already started is stopped; `close` joins both before the pair it watched is freed, so neither thread reads a pair that `open` has already freed. `qwen27_link.zig` is the pair's link over the existing relay, and holds the watchdog and rank 1's follower in one `Workers`; `qwen27_pair.zig` holds the rank-local shard and the seam transport for the serve loop; main's `qwen27_costs.zig` times the window both ranks share; and `qwen27_served.zig`/`qwen27_slots.zig`/`qwen27_lanes.zig` the accounting a shard adds. GLM-5.3's transport is not touched: `pair.zig`/`pair.metal` are as on main, and no `glm_dsa` file changes in this series.
Step 4 of 6, stacked on steps 1-3 (`qwen27/pair-seam`, `qwen27/rank-slice`, `qwen27/pair-lifecycle`) and main (`c39c09b9`); step 5 stacks on this commit. `tf-qwen27-tp-check` is built by `zig build test` and launched by the suite. It needs `--model`, so CI's run prints `SKIP qwen27-tp-check: no --model DIR` and carries on, and a host with the checkpoint gets both stages. Stage 1 loads the model, then rank 1 through `loadShard`, and requires the same greedy ids, logits bytes and prompt-state digest; stage 2 starts two rank-local shards joined by `pair.Loopback`, a host-memory link, and requires both ranks to emit the same ids as the single Mac, with `pair_logits_byte_equal` and `pair_first_logits_difference: null` reported, and `bad_pick_at` and the seam give-up counters naming any loss. `--stage 1|2` narrows it and `--dump` prints each side's top two. `--decode-prompt` feeds the prompt through `Session.decodeChunk` in `--chunk N`-row rounds, an earlier reply's arithmetic rather than the prompt kernels: the multi-row round a served chat gives every earlier reply, which runs the tree merge (`q27_attn_merge_single` on one Mac) and not the prompt kernels' merge. Both stages use the same feed, and the receipt reports `prompt_arithmetic: "decode"` next to the chunk. `--chunk` may then reach 512, the served chunk (`model.promptRows()`); the pair's stages keep 128, the rows the seam window holds (`pair.stageBytes`), so `--chunk 512 --stage 2` is refused by name rather than panicking at the seam. Stage 1 at chunk 128 is where main's merge stride is exercised: it takes `q27_attn_merge` at `chunk` rows a round, so the single-Mac logits bytes are the reference the pair has to match. `zig/qwen27_core.zig` exports the pair and `zig/build/qwen27.zig` registers the program; `--forced-file` replays one side's picks on the other side for divergence localisation, and `--taps` hashes five layers' post-prompt residuals.
Step 5 of 6, stacked on steps 1-4 (`qwen27/pair-seam`, `qwen27/rank-slice`,
`qwen27/pair-lifecycle`, `qwen27/pair-tests`) and main (`c39c09b9`); step 6
stacks on this commit.
`tools/qwen27/pair_accept.py` replays the checked-in single-Mac oracle
against a served pair: every case is sent twice, both replies must be
identical, and their `token_sha` must equal the oracle's, with no case
exempt. `tools/qwen27/pair_supervise.sh` starts rank 1 on `$PEER` and
rank 0 locally, restarts both when either stops answering, stops only
the pids it started (never by pattern), and verifies the peer's host key
rather than turning the check off. `qwen38_27b_oracle.json` is the
single-Mac, undrafted, temperature-0 record they compare against.
The oracle records the GPU family it was taken on (`apple-m4` here), and
`pair_accept.py` refuses to replay it against a serve that reports a
different family, or one whose family it cannot read, instead of
comparing tokens across two GPUs as if they were the same machine. The
two-turn case (`chat-two-turn-128`) opens with the recorded `chat-128`
reply and replays the oracle's second turn, so the acceptance run covers
the multi-row `decodeChunk` rounds a chat takes, not just single-turn
prompts; `record_oracle.py` records it the same way.
`pair_supervise.sh`'s `PEER_STATE` default is now peer-relative (rank 1's
own `${TMPDIR:-/tmp}/tensorfold-pair`, expanded there) rather than rank
0's local directory, so a supervision run does not need rank 0 to know
rank 1's filesystem layout; a caller-set `PEER_STATE` is used as given,
and a peer TMPDIR that cannot be read stops the script with exit status
1 instead of carrying an empty path into `down`.
`up` returns each start-up step's failure itself
(`check_binaries || return 1`, `start_rank1 || return 1`,
`start_rank0 || return 1`): `watch` calls it as `if up`, and sh switches
`set -e` off inside a function an `if` tests, so a reported failure
alone would leave both ranks starting on different builds and a failed
start sitting out the whole `READY_TIMEOUT`.
Nothing in either script names a machine, a user path or an address:
every location is environment-overridable, and the shim's stop path is
the pidfile.
…fuse a pair serve Two review findings on the oracle and its recorder, and the pair's own side of the second one. `qwen38_27b_oracle.json` carried one top-level `runtime`, `tensorfold-native 1.1.2`, though `chat-two-turn-128` was recorded later. Each case now carries the runtime that recorded its own `token_sha`: `code-128`, `code-256`, `chat-128` and `chat-256` say `tensorfold-native 1.1.2`, and `chat-two-turn-128` says `tensorfold-native 1.1.7`, so a case names the exact binary that must reproduce it. The description says so. `pair_accept.py` compares `token_sha`, not the build, so its cases are unchanged. `record_oracle.py` reads the serve's `/health` before it sends any completion. It refuses (exit 2, a plain line on stderr) a serve whose `policy` line reports the two-Mac pair speed-up mode, because rank 0 of a pair passes the `chat-128` turn-1 check just as well, and an oracle must come from one Mac. It also refuses a serve whose `/health` is unreadable or carries no version string. The reported version becomes the new case's `runtime`, so the receipt names the build that produced it, and the turn-1 `chat-128` equality and decimal-token-id hash checks are unchanged. That refusal could not fire before this commit: `engine_api.Info.policy` existed and the server prints it under `/health`, but the qwen27 host left it empty, so a pair serve reported no policy line at all. `qwen27_served` now names the mode when it attaches a pair, and its host test pins the line, so a pair's `/health` carries "speed-up mode: a two-Mac pair, rank 0 answers greedy requests only" and the recorder refuses that serve. `pair_accept.py`'s warm case asked a pair for a resumed prompt state, but a pair keeps no prompt cache (its slots are one and its prompt states are two Macs', so `qwen27_served.attach` enables none), so that case failed by construction on any pair. It is now `cold`: it drives a second turn in the same conversation and passes when the pair reports no cached prompt tokens, pinning the documented "keeps no prompt cache" limitation instead of contradicting it.
2e3a3f8 to
ea66002
Compare
|
Thanks @akol1. The scripts and the /health policy line look ready. A pair's /health now names the two-Mac mode and one Mac's reports nothing new, the recorder refuses a pair serve, and the supervisor's start steps each fail on their own under |
What this changes
Step 5 of 6 of the two-Mac Qwen3.8-27B series, rebased onto
main(c39c09b9), stacked on step 4 (qwen27/pair-tests,cd510c64), on top of steps 2-3 (qwen27/rank-sliced43966c5,qwen27/pair-lifecycle6db850cf); step 1 (#1432) is merged, and the stack's first commit retargetsmain's cost-rule case test (3abfc26e). Two commits: the scripts, then the oracle's per-case build, the serve's/healthpolicy line they read, and the acceptance run's cold case. Step 6 stacks on this branch's tip.The scripts that make a two-Mac run reproducible, and the serve's half of the recorder's pair guard, with no machine name and no user path in them:
tools/qwen27/record_oracle.pyrecords a single-Mac acceptance oracle: five cases (a 128-token and a 256-token/v1/completions, a 128-token and a 256-token/v1/chat/completions, and a two-turn chat case that asks a question, answers it, then asks a follow-up about the same example), each at temperature 0 with"draft": false. Each case records the token sha, the build that recorded it (runtime), and thegpu_familyof the Mac it ran on:code-128,code-256,chat-128andchat-256saytensorfold-native 1.1.2, and the two-turn case, recorded later, saystensorfold-native 1.1.7, so a case names the exact binary that must reproduce it rather than one label for the file. The recorder reads the serve's/healthfirst and refuses (exit 2, a plain line on stderr) a serve whosepolicynames the two-Mac speed-up mode — rank 0 of a pair would pass the case's own turn-1 check just as well, and an oracle has to come from one Mac — and also one whose/healthis unreadable or carries no version string. The version it reports becomes the new case'sruntime, and the refusal happens before any completion request, so a pair serve never answers a recording./healthnow carries the line that refusal reads.qwen27_pair_flags.healthPolicyreturnsspeed-up mode: a two-Mac pair, rank 0 answers greedy requests onlyfor a pair and nothing for one Mac,LaneHost.inittakes it as.policy, and the server prints it under/health; a host test pins both halves. Before this commit the qwen27 host leftengine_api.Info.policyempty, so a pair serve reported no policy line and the recorder's refusal could not fire — the guard and the line it reads land together.tools/qwen27/pair_accept.py runreplays that oracle against a served pair: every case is sent--repeat 2times and both replies must be identical, and theirtoken_shamust equal the oracle's, with--onlylabelled as a subset rather than a full acceptance. It refuses an oracle whosegpu_familyit cannot match: it prints the recorded family, this Mac's family and the reason (before the M5 the bits come fromsimd_qmm, on the M5 from the lane kernels with their own K splits), and returns 2 before sending a request — the pair's exactness is exact on one family of arithmetic, so it will not compare across the boundary.coldbuilds a two-turn conversation from the oracle's chat fixture (its answer, then a follow-up about the same example) and passes when the pair reports no cached prompt tokens: a pair keeps no prompt cache (one slot, and each Mac holds only its own halves of its states), so a resumed-prompt case cannot pass on one, and this case pins that documented limitation instead of contradicting it.tools/qwen27/pair_supervise.sh up|down|status|watchstarts rank 1 on$PEERand rank 0 locally, waits for both to answer/health, restarts both when either stops answering, and stops only the pids it wrote to its pidfiles (never by command-line pattern). It verifies the peer's host key instead of turning the check off, and it refuses to start when the two binaries differ unlessALLOW_MISMATCH=1says the A/B is deliberate.PEER_STATEdefaults on rank 1 to rank 1's own${TMPDIR:-/tmp}/tensorfold-pair, so neither rank writes to the other's state directory.Receipt
ea66002975d254638304d3caf6979de621ff333f) onmain(c39c09b9fb5d539f600a171544fb5622fd65b363) plus steps 2-4; no MLX or PyTorch. The gates ran on an Apple M4 Pro (Mac16,11), Metal only. The two-Mac run quoted below was produced before the series was rebased onto thismain, from the series tree at that time:zig build nativethere produced sha2563a82766f4bffbc9f3bc3575f6dec6841b3e4274e324bff83755f96f70b81400e(the supervisor printedbinaries match: 3a82766f4bff), both Macs installed that binary (tensorfold-native 1.1.8), on two Macs of the same GPU family (apple-m4), Metal only, at the checkpoint revision22d8d538154e0e5b6f8dcb1fcc74a60a0798104c(4-bit affine, group 64). The second Mac is no longer available, so there is no fresh two-Mac run on the rebased heads: the receipt below records the pair path before the rebase, and the binary this commit builds is not that binary.tools/bench_concurrent.py --alone --serialortoken_shaagainst"draft": false:tools/qwen27/pair_accept.py run(defaults--repeat 2,--timeout 600) exited 0 with fivePASSlines andpair_accept: 5/5 cases match the oracle on http://127.0.0.1:4100; 0 did not; noFAILorSKIPline was printed. The five cases arecode-128atf5c9409cfe40,code-256at2b28aa2108de,chat-128atb69b0269917f,chat-256ata06fab348fbe, and the two-turn chat fixturechat-two-turn-128at7d156013fbdd, and each case ran twice with identicaltoken_shaand identical reply text. Both health endpoints named the same GPU generation and backend ("device": "Apple M4 Pro","backend": "metal"), both carried the pair's policy line ("policy": "speed-up mode: a two-Mac pair, rank 0 answers greedy requests only"), and the supervisor printedbinaries match: 3a82766f4bffbefore starting the ranks, so both Macs served the same bytes.pair_accept.py coldon that pair then reportscold: turn 1 prefill marked 0; turn 2 cached=0and passes withPASS cold: the pair kept no prompt cache, as its one slot over two Macs' states implies, exit 0: the pair accepts the two-turn chat case and pins the no-cache limitation the docs state. This is the recorded run from before the rebase, and the log it is quoted from is the one the supervisor andpair_accept.pywrote on this Mac then. The script the receipt ran is byte-identical to this head's —tools/qwen27/pair_accept.pyis untouched by the rebase — but the binary is not: the receipt's tree predates this rebase, and this head sits onc39c09b9, so a fresh two-Mac acceptance would answer from a different tree. On the rebased heads the single-Mac loopback gate (tf-qwen27-tp-check, step 4) and the host tests inzig build testare what run here, and a fresh two-Mac acceptance needs a working pair (the second Mac is gone).tools/bench_openai.py: n/a, these scripts are the harness for that measurement; the rates belong to the run above, not to this diff.pair_accept.pyrecords per-case prompt and decode rates in its JSON, but the two-Mac receipt above is the acceptance result, not a speed claim.zig build test:580/580 steps succeeded; 249/269 tests passed (20 skipped)(Zig does not re-count cached test steps, so these are the counts of what this checkout re-ran; every step succeeded), exit 0 — this is also what runs the host test that pins the pair's/healthpolicy line.zig build test-kernel-contract:20/20 steps succeeded; 114/114 tests passed, exit 0.zig build dev-tools:216/216 steps succeeded.zig build test-qwen27-served-host:7/7 steps succeeded; 44/45 tests passed (1 skipped), exit 0.zig build tf-qwen27-tp-check:42/42 steps succeeded.zig fmt --check build.zig zig: clean. Filtered contract runs: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 38 ms, limits 216 msandSUMMARY 4 kernels: 4 pass gates 1-4, 0 fail, 0 known findings; 0 controls: 0 red as planted, 0 missed; 0 limit problems; gates 34211 ms, limits 135 ms. Lean rule:425 problems, 425 on the base, 0 new(tools/zig/lean_new.py, exit 0). The oracle file's five cases and the family refusal strings are checked in and reproducible withrecord_oracle.pyandpair_accept.pyas described above.pair_accept.pyrun on the rebased heads — the second Mac is gone (the cluster was broken), so the acceptance above is the recorded pre-rebase run; the single-Mac loopback gate and the host tests cover the seam here. CUDA, HIP/RDNA, M1-M3, M5 and Windows are not in this room;pair_acceptrefuses the M5 because the oracle's family isapple-m4.Checklist
"draft": falseand both repeats must match, so a draft path that appears would fail the acceptance rather than pass it."draft": false, a resumed prompt equals a fresh one, and concurrent equals solo. — the acceptance sends each case twice and requires identical replies; the two-turn case is the chat continuation that reuses a reply's state, andcoldis the pair's resumed-prompt check: it drives the follow-up turn and passes when the pair reports no cached prompt tokens, the documented reason being that a pair keeps no prompt cache.SUMMARYline is in the receipt. — this commit adds no GPU kernel; it drives the pair. The filtered contract runs in the receipt are the regression check for the kernels the run covers.apple-m4Macs for the quoted run; no CUDA, HIP, M1-M3, M5 or Windows; the M5 is refused by the oracle's family check by design.pair_accept.pyprintspair_accept: <url> did not answer: ...and exits 2 when nothing answers, instead of a traceback, andrecord_oracle.pynames its own preconditions (no opening case, a turn with no text, a reply withouttoken_ids) the same way. The/healthpolicy line has a host test that fails if a pair reports no line or one Mac reports the pair's.lean: 425 problems, 425 on the base, 0 new.$PEER,$MODEL,$BINand$STATEare read from the environment; the oracle stores a GPU family, not a host;pair_supervise.shverifies the peer's host key and never disables the check.Stacked on:
qwen27/rank-slice,qwen27/pair-lifecycle,qwen27/pair-tests. This commit is step 5 of 6.