Skip to content

fabric stage: the protocol layer, boot description, round plan, rows layout and keep-alive, with golden bytes for every message (#2005 step 1b, stacked on #2012) - #2060

Merged
ashhart merged 7 commits into
ashhart:mainfrom
jschmied:stage-protocol
Nov 6, 2026

Conversation

@jschmied

@jschmied jschmied commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

Part of #2005 (step 1b, the protocol layer of section 6.4). On main 9200a3064 now that #2012 has landed: the six commits are this PR's own, head ecd50eaa5. The last one rewrites the three rows fixtures for #2012's merged TFST frame magic (2 bytes each).

Host code only: no engine and no GPU. It defines what a pipeline stage and rank 0 say to each other on #2012's link, and ships golden bytes of every message, so a CUDA and a Metal stage can be built separately against the same bytes.

What it adds (zig/src/fabric/stage/)

  • payload.zig: every description, ready and plan payload starts with an 8-byte prefix: magic 0x50534654 (its little-endian bytes read TFSP), a u16 protocol version, the message and a reserved byte. fabric: the pipeline stage link, a TCP ring that carries each round's plan and hidden rows, with a shared key (Linux and macOS, #2005 step 1) #2012's frame header stays at version 1.
  • boot.zig: a stage's boot description and its ready report, and rank 0's checks:
    • the description: rank and world, engine version, backend, KV dtype, half-open layer range, boundary (hidden width, row dtype, residual planes, drafter width), shapes (prompt chunk, max rows, --parallel, context), owned tap layers, and capacity (KV rows, recurrent states);
    • check refuses the first mismatch by its name: OtherRank, OtherVersion, OtherBackend, OtherKvDtype, OtherRange, OtherWidth, OtherDtype, OtherPlanes, OtherDraftWidth, OtherShapes, OtherTaps, ShortCapacity;
    • the layout check covers all stages together: RangeGap, RangeOverlap, RangeShort, RangeBeyond.
  • plan.zig:
    • the round plan: streams in ascending id order, each with an op (open, continue, close, cancel, resume), first position, rows, accepted count and resume point, plus the chunk cuts;
    • the rows frame's layout: plane-major rows in plan order, then the drafter's fp32 tap sums;
    • the bound checks on decode, each refused by its name. No length from the wire is trusted before it is checked against the payload's size and the stage's limits.
  • keepalive.zig: rank 0 sends an empty plan every 2 s when idle, and a stage that misses three declares the ring dead. Pure state, tested with injected time.
  • fixtures/: 47 golden files, each good message plus planted bad ones, and a README with every layout and the error each bad file must give. fixtures_test.zig re-encodes each good file byte for byte and checks that each bad one is refused with its named error.

The choices section 6.4 left open are written into RFC #2005, section 6.4, under "v0's decisions": half-open ranges, stream order, what a chunk cut and a resume point are, one engine version string, and the boot capacity floor (KV rows ≥ the context, recurrent states ≥ --parallel) that check enforces.

Checks (GB10)

  • zig build test-stage: all pass on ecd50eaa5. The runner reports 37/37 because it also counts two unnamed test {} blocks that only import the other files. test-stage-compile builds for Linux and macOS on aarch64.
  • lean: 0 new problems. zig build test, the CI set (test-kernel-contract test-golden test-server-security test-pull-security fuzz-cortheon) and git diff --check pass.

Since the review (de6c3bdae, after ThinkOffApp's questions on #2005): a plan's stream is 28 bytes and carries its slot's state generation (0 for an open; plan.sameGeneration refuses another as OtherGeneration); the keep-alive is due whenever 2 s pass without a plan, mid-round too; ready's warm means executable for the declared shapes. The plan fixtures are rewritten and plan_open_generation added. #2012's later commits came in with the rebase.

Mode byte in v0 (9f679f852, from #2115 as asked): byte 20 of a plan names the round's mode, 0 a window (every row's state recorded, as decode and a verify run) and 1 a prompt chunk, so a CUDA or Metal stage picks the same kernels as rank 0. A keep-alive is a window, and any other value is refused as BadMode. New fixtures plan_prompt, plan_bad_mode and plan_keepalive_mode; plan_bad_reserved now pokes byte 21.

@ashhart

ashhart commented Nov 3, 2026

Copy link
Copy Markdown
Owner

Thanks @jschmied, this is a careful protocol layer: every length is checked before it's used, every mismatch has its own error, and the golden files mean a CUDA and a Metal stage can be built separately against the same bytes.

I read commits 4 and 5 on current main, with #2012's three commits under them. Everything applies, the lean check is unchanged, and nothing outside fabric/stage imports it. A few small things:

We'll run zig build test-stage on a GB10 and on a Mac when it's ready to land after #2012.

@jschmied

jschmied commented Nov 3, 2026

Copy link
Copy Markdown
Contributor Author

Thanks @ashhart. All four are in, at 05a8853ab:

Checks on 05a8853ab (GB10): zig build test-stage passes and test-stage-compile builds for Linux and macOS; lean 424 on the base, 424 on the head, 0 new; zig build test, the CI set and git diff --check pass.

@ashhart

ashhart commented Nov 3, 2026

Copy link
Copy Markdown
Owner

Thanks @jschmied, all four are in. The magic's little-endian bytes now read TFSP, and none of the 43 fixtures still carries the old order. The capacity floor and the link to the v0 decisions are in, and so are the test counts. On the frame magic: yes, please fix it in #2012 before it lands, so both read the same way on the wire. Once #2012 is in and this is rebased onto it, we'll run test-stage on a GB10 and on a Mac and land it.

@jschmied

jschmied commented Nov 3, 2026

Copy link
Copy Markdown
Contributor Author

Thanks @ashhart. The frame magic is fixed in #2012, at fe6aeebc7:

  • The magic is now 0x54534654, so its bytes on the wire read TFST. The value I suggested above, 0x54535654, was wrong: its bytes read TVST.
  • The frame test now checks the first four bytes of an encoded header against "TFST", and it fails with the old value.
  • zig build test (which runs test-stage), the CI set and diff-check are green, and the lean check is unchanged (0 new).
  • Both PR bodies say this now.

Once #2012 lands, I'll rebase this PR onto it so it carries only its own commits.

@ashhart

ashhart commented Nov 4, 2026

Copy link
Copy Markdown
Owner

Thanks @jschmied. Nothing is left on this PR's own commits. One thing before it can land: CI's fmt job fails on fabric/stage/secret.zig and frame.zig. Those come from #2012, and #2012 shows the same failure, so a zig fmt there fixes both. Once #2012 lands and this is rebased down to its own three commits, we'll run test-stage on a GB10 and on a Mac and land it.

@ashhart

ashhart commented Nov 4, 2026

Copy link
Copy Markdown
Owner

Thanks @jschmied. The state generation is a good addition: a missed rollback now fails by name instead of as wrong rows. With the rewritten fixtures it's still nothing more to do on your side. Once #2012 lands and this is rebased down to its own commits, we'll run test-stage on a GB10 and on a Mac and land it. If #2115's mode byte belongs in v0, this is the PR to put it in, before v0 freezes.

jschmied added a commit to jschmied/TensorFold that referenced this pull request Nov 5, 2026
…nd tf-qwen35-cuda stage

cuda_stage.zig drives the stage link (ashhart#2012) with the protocol (ashhart#2060): at boot each stage's description and
ready report go round the ring and rank 0 checks every one and the layout; each round rank 0's plan goes round
(rank 0 checks the last stage's echo byte for byte), then the rows, h and r as bf16 planes. Slots is the same
code on every rank: open takes a free slot at generation 0, continue keeps the previous window's first
`accepted` rows (a keep advances the generation) and checks the plan's generation and position, close frees it;
resume is refused until the prompt cache spans stages. Rank 0's Lead predicts each entry's position and
generation, sends the plan, then applies it through Slots like every follower.

tf-qwen35-cuda <model> stage <out> runs one scenario (a two-chunk prompt, greedy steps, a second stream beside
it, drafted windows kept to 3, 5, 0 and 2 rows, a keep-alive) on one engine or a ring. Qwen3.8-27B MLX 4-bit on
one GB10: 2 stages (0:32, 32:64) and 3 (0:20, 20:44, 44:64) give the one engine's 48 logits rows byte for byte,
bf16 and fp8 KV; device bytes 16.98 GiB whole, 9.04 + 7.64 GiB in two, 6.61 + 6.03 + 5.22 GiB in three. A
follower killed mid-scenario ends rank 0 with Closed 0.4 s later.
@jschmied

jschmied commented Nov 5, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto main now that #2012 has landed. The PR is down to its own commits, on main 9200a3064, head ecd50eaa5:

On the GB10, on this head: test-stage 37/37. Lean 425 on the base and on the head, 0 new; zig build test RC 0; test-kernel-contract test-golden test-server-security test-pull-security fuzz-cortheon RC 0; git diff --check clean.

@jschmied
jschmied marked this pull request as ready for review November 5, 2026 10:23
@ashhart

ashhart commented Nov 5, 2026

Copy link
Copy Markdown
Owner

Thanks @jschmied for the clean rebase and the explained fixture bytes. test-stage on a GB10 and a Mac is next on our side.

@ashhart

ashhart commented Nov 6, 2026

Copy link
Copy Markdown
Owner

test-stage passes on both of our machines, main's tree against this PR's (ebd52a3d6):

Machine Main This PR
GB10 (aarch64 Linux) 20/20 tests, 3/3 steps 37/37 tests, 3/3 steps
Apple M5 Ultra 20/20 tests, 3/3 steps 37/37 tests, 3/3 steps

That's what we said we'd run, so it goes in the landing queue now. Thanks @jschmied.

@ashhart

ashhart commented Nov 6, 2026

Copy link
Copy Markdown
Owner

/atc exchange 857d45f91fc8134b6269482657215227afc86094a13ba4741e8fb2ba2e63d3d7

jschmied and others added 7 commits November 6, 2026 19:42
…y report, rank 0's round plan, the rows layout and the keep-alive, each mismatch refused by its name
…yte-stable and each refused by its name, for a CUDA or Metal stage
…ytes on the wire read TFSP; fixtures rewritten
…d the keep-alive beats mid-round too

RFC ashhart#2005 section 6.1 has a stage refuse rows whose generation is not its own, but the plan carried none. A stream is
now 28 bytes: the generation u32 at 24, 0 for an open (BadOpen otherwise), and plan.sameGeneration refuses another by
name (OtherGeneration). The keep-alive is due once 2 s pass without a plan, not only when idle, so a stage's long
forward elsewhere in the ring never reads as a dead ring; a stage already counts any frame as heard. Ready's warm
means executable for the declared shapes, not graphs captured. The plan fixtures are rewritten (140 bytes for the
round) and plan_open_generation added.
… prompt chunk, so a stage picks the engine's kernels

A Qwen stage runs a prompt chunk through the prompt GEMMs and a window through the one-row-exact path, and the
bits differ, so a follower must know which one rank 0 runs. A keep-alive is a window; any other mode byte is
BadMode. Fixtures plan_prompt, plan_bad_mode and plan_keepalive_mode added; plan_bad_reserved pokes byte 21.
@ashhart
ashhart merged commit 3499ad9 into ashhart:main Nov 6, 2026
7 checks passed
@ashhart

ashhart commented Nov 6, 2026

Copy link
Copy Markdown
Owner

Thank you @jschmied. This is merged into main with your commits under your own name, so you're listed as a TensorFold contributor. It ships in the next release, credited to you in the changelog and the release notes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants