Skip to content

cluster: a v2 request sends the batch rows once, each expert naming its rows by index - #1970

Merged
JustVugg merged 5 commits into
JustVugg:devfrom
Avicennasis:p/cluster-rows-once
Oct 22, 2026
Merged

JustVugg merged 5 commits into
JustVugg:devfrom
Avicennasis:p/cluster-rows-once

Conversation

@Avicennasis

Copy link
Copy Markdown
Contributor

Stacked on #1964 (which is stacked on #1963): the change to review is the last commit, 7d36d9ab.

A v1 request carries a copy of the input row inside every item it routes to, so at decode with top-k the same row crosses the wire k times (GLM-5.2 decode, top-8, D=6144: eight copies of a 24 KB row per layer). In a v2 request the coordinator now sends S and the S batch rows once (in act, so q8 under q8), and each item is eid nr plus nr row indices; the worker gathers (nr ≤ S, idx < S, S ≤ 65536). Replies are unchanged. v1 keeps row copies, so default bytes are untouched. The shared-expert item is the index list 0..S-1.

Tests: test_v2_sends_the_rows_once pins the exact 104 bytes of a top-2 request where row 0 routes to experts 42 and 43 and row 1 to 42 only, shared item riding; the worker gathers and the rows accumulate exactly. tests/test_cluster_sharding.py now prints bytes per position: sent 4102 → 2080 B/position on the top-2 fixtures (1.97×, the remainder is indices, headers and the hello; GLM-5.2 decode would be ~8×), asserted as sent_v1 ≥ 0.9·K·sent_v2 with K from config.json; f32 parity and the byte-exact logits of the shared-expert run unchanged; the q8 drift gate goes from 2317 to 1758 B/position (4.67×) with mismatches still 0, since each row is q8-encoded once by the same encoder. make colibri tests/test_cluster_protocol 0 warnings, 9 protocol cases, 4 gate tests passed.

@Avicennasis

Copy link
Copy Markdown
Contributor Author

Restacked on #1964's new head: this branch is now dev 1509ea5b + #1964 (601dfca4) + this one commit, head d2fda03f, force-pushed with --force-with-lease on the reviewed 7d36d9ab (kept on the fork as the old head of p/cluster-rows-once-1509ea5's history).

The rebase had one conflict, in c/colibri.c: #1964's new commit moves the per-worker request/reply out of cluster_moe_batch into cluster_exchange() (a failed link refuses every later exchange until reconnect, see #1964), and this commit rewrites that same request: the batch rows once after n, each item naming its rows by index. Resolved by carrying the rows-once bytes into cluster_exchange (it takes x, S now) — the wire this PR describes is unchanged. The test file, test_cluster_sharding.py and docs/cluster.md replayed clean.

Ran here on the same Linux box, against dev 1509ea5b built the same way:

  • make -C c check: dev 193 C test binaries, 0 FAILED, Python Ran 2464 tests ... OK (skipped=214); this branch 193 C binaries, 0 FAILED, Ran 2466 tests ... OK (skipped=216) (+2 are this PR's own sharding tests, skipped in that run because the tree had no fixtures yet). Same five runtime warnings as dev, 0 compiler warnings.
  • tests/test_cluster_protocol: ok (3 of 3 runs) — including cluster: v2 hello that refuses v1 workers by name, and the shared expert on the worker #1964's failed-link cases on the rows-once wire.
  • tests.test_cluster_sharding with regenerated glm_tiny_fmt6/glm_tiny_fmt4 fixtures (torch 2.14.1+cpu, transformers 5.19.0): Ran 4 tests ... OK — fmt6/fmt4 parity token-exact with the shared item on the wire, and the two q8 drift gates (oracle mismatches f32=0 q8=0); the gate prints the saving: sent bytes 4102 → 2080 B/position (1.97x at top-2) for rows-once, 1758 B/position (4.67x) with q8 on top.
  • Not run: Metal/Vulkan paths; Windows (transport compiled out).

The push should restart the cancelled matrix.

@JustVugg

Copy link
Copy Markdown
Owner

Not in the batch of 2026-10-16 (#2112): see the note on #1963 about rebasing the stack on dev after it lands.

…tocol v2

The expert-worker protocol moves every routed row as raw f32 both ways, so on
a LAN the transfer, not the expert FFN, bounds a delegated layer.
distributed-llama syncs its activations as blocks of 32 int8 behind one scale
for a ~4x cut. Same idea here, behind an env switch and off by default: the
cluster parity gate is token-exact and int8 rows are not.

- COLI_CLUSTER_ACT=q8: the coordinator speaks a v2 header (magic, version 2,
  act, layer, D, I, n) whose rows are ceil(D/32) f32 block scales followed by
  D int8 -- qrow_i8 per block, the engine's own activation rounding, so no new
  quantizer -- and the worker answers in the same format. f32, the default,
  sends the v1 bytes unchanged, so a cluster of mixed binaries keeps working.
- The worker accepts v1 and v2, names the versions it speaks when it refuses
  one, and refuses an unknown act word by name. A coordinator whose v2 request
  meets a closed socket names the worker and the COLI_CLUSTER_ACT=f32 escape
  hatch instead of just "failed". The worker's response-header writes free the
  batch when the peer is gone mid-header, as a bad request already did; the
  act word had widened that leak.
- The coordinator reports bytes sent and received at exit, so the cut is
  measurable: on the tiny GLM oracle, 8203 -> 2316 B/position (3.54x) with the
  same argmax at every position.
- test_cluster_protocol: the q8 codec byte for byte against qrow_i8 (partial
  last block, zero row, decode within half a block scale), a v2 q8 round trip
  over a socketpair, and a v2 header with act=f32 still carrying raw rows.
test_cluster_sharding's f32 gate is unchanged: the same assertions, now on
shared _expert_worker/_coordinator helpers. ClusterQ8DriftTest runs the
delegated oracle with COLI_CLUSTER_ACT=q8 on both fixtures and asserts that
the f32 delegation stays exact, that the q8 run scores the same positions and
mismatches at most Q8_MAX_MISMATCHES of them (measured 0 on fmt6 and fmt4;
the bound carries no slack, raise it with a measurement), and that q8 moves
less than a third of f32's bytes; it prints bytes per position for the record.
…ert on the worker

On top of the q8 protocol v2 (COLI_CLUSTER_ACT=q8, header magic version act
layer D I n): the default still speaks v1 bytes, so a cluster of mixed engine
builds keeps working. The coordinator speaks v2 when it needs q8 rows or the
shared expert, and v2 gains two words v1 has none for:

- hello: an empty v2 request (n==0, act as the coordinator will speak it) at
  connect time, sent only when v2 will be spoken at all. A worker built from
  an older engine closes on version 2, and cluster_init refuses it by name
  ("expert worker HOST:PORT closed on the COLIEX01 v2 hello: it runs an older
  engine (v1); rebuild the worker, or run this coordinator without
  COLI_CLUSTER_ACT=q8 and CLUSTER_SHARED=1") before any token is generated. A
  worker answers the hello before its model checks, with an empty response in
  the same act; its refusals of an unknown version or act are unchanged.
- item eid 0xFFFFFFFF: the layer's shared expert. With CLUSTER_SHARED=1 the
  coordinator adds one item of every row to the layer's first routed request,
  on the worker layer%n picks, in the request's act; the worker loads the
  three shared matrices once (qt_load + qt_planarize, the coordinator's own
  resident branch) and runs FASE E's CPU sequence (matmul_qt, siluf,
  matmul_qt); the coordinator adds the rows where FASE E adds its own, so
  with f32 rows the f32 order of the sum does not move. Off by default.
  GLM-5.2: 2.83 B params a token off the coordinator. A v2 request may carry
  65 items (64 routed + the shared one); v1 stays at 64.

DUMP=<path> (raw f32 logits rows) was compiled only under COLI_VULKAN; it is
now on every build so the cluster gate can compare runs byte for byte.

Tests:
- tests/test_cluster_protocol.c: the hello against a v1 worker (refused, by
  name and version, read back from stderr) and a v2 worker (accepted); the
  real cluster_moe_batch over a socketpair with the shared item (a v2 request)
  and without it (v1 bytes, the default).
- tests/test_cluster_sharding.py: the f32 parity gate gains a third run with
  CLUSTER_SHARED=1 that must match the local run's teacher-forcing signature,
  show zero oracle mismatches, and match the local run's logits bytes; the
  worker's stderr must name every sparse layer it loaded the shared expert
  for. A 0.1 % scale on the worker's shared output flips no token on the tiny
  fixtures but fails the byte comparison; zeroing it fails the oracle on
  27/32 and 30/32 positions. The q8 drift gate is unchanged; its transfer
  summary now includes the hello's 56 bytes per worker per run.
…nnect

The coordinator's exchange with a worker -- the request header and items,
then the reply's header and items -- is one function now, cluster_exchange,
and a transport or protocol failure in it (a short read, EOF mid-reply, a
magic, version, act, status, item id or row count that is not the one
asked for) marks that worker's link failed. Every later exchange on a
failed link returns -1 before touching the socket: nothing is sent, nothing
is read, so the bytes a worker still sends after a failed exchange -- the
rest of a reply the coordinator stopped reading, or a well-formed reply
behind it -- are never read as the answer to a later request. A link comes
back only by reconnecting. The hello's refusals mark the link the same way.

cluster_moe_batch is unchanged in what it does: it builds the items, calls
the exchange, and on -1 prints the worker by name and exits, as before.
The rule is ds4's after a one-sided transport failure (antirez/ds4
f0962e3): refuse everything on the link, never consume drained bytes as a
reply.

tests/test_cluster_protocol.c pins it over a socketpair in two shapes. A
worker whose reply header claims two items for a one-item request, with a
well-formed 52-byte reply queued right behind it: the first exchange
returns -1, the second returns -1 at once and the 52 bytes are still on
the link, unread, and the worker saw no second request. A worker whose
reply stops after the first of two rows and shuts its write side: the
same, with nothing pending. Without the link guard the second exchange
reads the queued reply as its own (the test fails on rc == -1); with it,
8 of 8 runs pass. 0 warnings.
…ts rows by index

A v1 request copies a row into every item that routes it: at decode with
top-K the same row crosses the wire K times (GLM-5.2, top-8: eight copies
of a 24 KB row per layer). A v2 request now carries S and the batch's S rows
once, in `act`, then per item eid, nr and nr row indices (u32, network
order); the worker gathers each item's rows from the batch. Replies are per
item as before. v1 requests keep their copies, so the default bytes are
unchanged; the shared-expert item names every row by index instead of
carrying S more copies, and the coordinator no longer copies rows per item
for v2 either (cluster_item's copy_inputs).

Measured on the tiny GLM oracle (32 positions, top-2, D=256), bytes the
coordinator sends per position: v1 4102, v2 2080 (1.97x; the rest is
indices and headers). q8 in total: 2317 -> 1758 B/position (f32 8203,
4.67x), the same 0 oracle mismatches.

tests/test_cluster_protocol: the exact 104 bytes of a two-expert, top-2 v2
request where row 0 routes to both experts and row 1 to one, the shared
item riding (header, S, the two rows once, three index lists); the worker
gathers and the rows accumulate exactly. The q8 round trip and the
shared-expert test move to the new layout. tests/test_cluster_sharding:
f32 parity and q8 drift unchanged; the parity gate prints the bytes sent
per position for v1 and v2 and requires the v1/v2 ratio to be at least
0.9 K.
@JustVugg
JustVugg merged commit e1ff5cd into JustVugg:dev Oct 22, 2026
115 checks passed
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