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

Draft
jschmied wants to merge 6 commits into
ashhart:mainfrom
jschmied:stage-protocol
Draft

jschmied wants to merge 6 commits into
ashhart:mainfrom
jschmied:stage-protocol

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). Draft, stacked on #2012: the first three commits are #2012's (c55fd8d3c, 442f61776, c68421b38), and the last three are this PR. #2012 has since gained fe6aeebc7 (its frame magic now reads TFST on the wire). Once #2012 lands, I'll rebase this PR onto it so it carries only its own commits.

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/: 43 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)

…und's plan and hidden rows (Linux and macOS)

zig/src/fabric/stage is the first step of ashhart#2005: each rank listens on its own address only, connects to the next
rank and accepts the one before. A hello checks that both ends of every link agree on the world, on being
neighbours, and on the checkpoint's and layout's fingerprint. Frames carry a 32-byte header (magic, version, kind,
round, rows, payload bytes) and every send and receive has a deadline. A frame of another kind or round, or past the
receiver's buffer, is refused, and any failed call leaves the link broken, since its stream may be mid-frame. A rank
that fails closes both links, so its neighbours see it and the ring ends as one.

It uses libc sockets only, with no CUDA or Metal module. fabric/tp's control plane imports cuda through config.zig,
so this does not reuse it. TF_STAGE_RANK, TF_STAGE_WORLD, TF_STAGE_PEERS and TF_STAGE_TIMEOUT_S name a rank's
settings.

`zig build test-stage` runs rings of two and three ranks on loopback threads, and `test-stage-compile` builds the
tests for aarch64 Linux and macOS.
… any frame

A stage rank opens a port, so a ring needs a key: TF_STAGE_SECRET_FILE names a file of at least 16 bytes that only
its owner may read, and the key is its SHA-256. When a link forms, both ends send a fresh nonce from /dev/urandom,
then an HMAC-SHA256 over a label, their role (connector or acceptor), rank, world, the fingerprint and both nonces.
Each end checks the other's proof in constant time, so the key never crosses the wire and a proof can't be
replayed. A rank without the key is refused by both its neighbours (error.BadSecret), and a stranger that connects
and talks first is refused at its first frame.

This authenticates the ring when it forms. It doesn't encrypt, and it doesn't protect each frame after that, so
it's for a trusted network.
…wildcard listen and a key file over 4 KiB are refused
…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
@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.

…ytes on the wire read TFSP; fixtures rewritten
@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.

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