feat: distributed expert workers — rebased, fmt=6 rotation fixed, token-exact parity gate - #1036
Conversation
|
Just to add some context beyond the automated gate: I also ran this through a real multi-machine integration test, not just loopback. The setup was one coordinator and up to three expert workers, all separate physical Linux boxes, connected over a WireGuard mesh (Tailscale). Two of the workers were on a different tailnet from the coordinator, so the cross-tailnet path was exercised too. One worker was a hosted VPS with over 200 ms of round-trip latency. Everything CPU-only, no GPU. Results:
One observation worth flagging for the maintainer, not blocking: the "expert hit %" telemetry always reports 0.0% under the cluster path — cluster_moe_batch bypasses the local pin/LRU counters, so the hit-rate stat silently doesn't cover cluster-delegated experts. The worker's own cache is fine; it's just an unreported number. Happy to provide more detail or reproduce anything on request. |
|
Heads-up, and an apology: this went #1063 landed on A rebase on current Two things worth knowing while you're in there:
Sorry for the churn. Landing the registry before your branches was the right call for the project, but it does mean the cost of it fell on the people with open work. |
The cluster control plane re-implemented the HTTP layer and dropped the accrued security hardening. Reuse openai_server's machinery instead of copying it: - cluster._Handler now subclasses APIHandler, inheriting send_json/read_json, the DNS-rebinding Host guard (_check_host), the MAX_BODY cap and the cumulative _DeadlineReader, and cluster.ClusterServer subclasses APIServer for the MAX_CONNECTIONS / per-IP connection caps. - Hoist the /v1/cluster/* endpoint join into one _endpoint helper. - Extract require_model shared by need_model and need_worker_model so the two validators cannot drift (worker only needs config.json + the colibri binary).
…esolvable-expert error Match the coordinator's model_init in cluster_worker_run: honor COLI_MODEL_DIRS via st_init_multi so a worker resolves expert tensors split across drives. Surface a clear worker-side error instead of expert_load's fatal exit when an expert tensor can't be resolved: drop fatal=1 -> fatal=0 so expert_load_impl returns -1 (no st_die_missing/exit), pre-check the tensor resolution to name the missing tensor, and emit a [CLUSTER] message before closing the connection.
cluster_worker_run re-implemented the routed-expert FFN as a parallel copy of moe() and was missing the fmt=6 gate/up input pre-rotation, producing wrong numbers for E8/IQ3 experts. Reuse the #9 shared expert_ffn helper and apply the Q^T input rotation (slot->g.fmt==6) in place per item before the gate/up, matching moe()'s E8_XE.
…fter failed load)
…grouped int4) Add --fmt6/--fmt4 to make_glm_oracle.py. Both flags generate the tiny GLM MoE fixture with E8-compatible dims (hidden=256, moe_inter=256 — the E8 super-block is 256 weights/98B, so the default 128/32 cannot be E8-quantized) and quantize ONLY the routed experts, reusing iq3_pack (fmt=6) and convert_fp8_to_int4's quant_int4_grouped (fmt=4); shared/dense/attn stay f32. Weights are packed from the ORIGINAL weights while the reference (ref_glm.json, inside each fixture dir) is computed from the DEQUANTIZED weights, so the engine's decode reproduces the oracle token-exactly (32/32). This is the fixture the #7 parity gate needs to exercise the E8 activation pre-rotation (E8_XE / e8_rot_rows), which the default f32 oracle never touches. The f32 default path is unchanged (hidden=128/moe_inter=32, bit-identical output).
Extract a single quantize_routed(w, fmt) encoder shared by the round-trip (ref) and save paths so they cannot drift; compute fmt once at the top instead of re-deriving args.fmt6/args.fmt4 in three places; hoist the numpy/safetensors codec imports to module scope; name the magic constants (FMT6/FMT4/FMT6_SCALE_TAG/GROUPED_INT4_*). Output is byte-identical (verified fmt6/fmt4/default all 32/32).
Runs glm_tiny in teacher-forcing prefill twice (local CPU vs cluster-delegated) and asserts identical token signatures AND zero [ORACLE] mismatch lines in both runs — stronger than JustVugg#550, which only asserted equality. Covers fmt=6 (rotation-bearing, the bug arm) and fmt=4 (no-rotation control). Determinism pinned via the same binary and a shared numeric-env block; skips cleanly when fixtures are absent.
The cluster registry subclasses openai_server.APIHandler, which enforces the DNS-rebinding Host guard (#SEC-7). That guard correctly rejects non-loopback Host headers, but a cross-host expert worker registers from its LAN IP, so the MVP (coordinator + remote workers) 403'd at discovery -- a regression the hardening introduced relative to the original JustVugg#551. Plumb the same escape hatch coli serve already exposes (JustVugg#597): accept --allowed-host (append, default from COLI_ALLOWED_HOSTS) on 'coli cluster coordinator' and thread it through cmd_cluster_coordinator -> cluster.serve -> ClusterServer.allowed_hosts. Default stays loopback + bind address; the guard is unchanged unless the operator opts a host in.
The registry-rebase resolution of need_model resolved the engine (which reads config.json) before require_model's directory check, so a missing model dir reported "unsupported model: cannot read config.json" instead of the documented "model not found" (test_cli_output caught it). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
cluster.py was missing from the install target, so `coli cluster` and serve-time worker discovery would ImportError on an installed tree. The protocol test includes ../colibri.c but only depended on compat.h, so it did not rebuild when the engine changed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
No apology needed — this is exactly the churn a registry refactor is supposed to cause, and the new shape actually made the rebase easier. Landing #1063 first was the right call. The branch is rebased onto current dev (including #1067 — the first CI run here tripped over the same OLMoE/numpy loader crash your dev CI hit, so I picked up your fix) and the conflict flag has cleared. It went about as you predicted: all conflicts were in c/coli, and every one resolved to "read it from the registry":
One resolution was a judgment call rather than mechanical, so flagging it per your offer: need_worker_model now resolves the model's family and explicitly refuses non-GLM models ("cluster expert workers currently support the GLM engine only"). The old code hardcoded the GLM engine path, which for a non-GLM model is exactly the silent-fallthrough-to-GLM the registry now bans — and the worker loop only exists in colibri.c. If you'd rather express that as a capability flag on FamilyDescriptor instead of an id check, happy to rework it to whatever fits the registry's contract. Beyond the 13 rebased commits there are two small fixes that came out of post-rebase review:
Nothing load-bearing moved: the c/colibri.c feature hunks are byte-identical to the pre-rebase branch (verified by diffing the two diffs, not by eye), the worker still routes through the shared expert_ffn with the fmt=6 input pre-rotation intact, and locally everything is green — make check passes, the parity gate is token-exact 32/32 on both fmt=6 and fmt=4, and the COLIEX01 protocol and registry tests pass. |
Summary
This revives upstream #551 (distributed expert workers:
coli cluster coordinator/coli cluster worker, the COLIEX01 wire framing,CLUSTER_WORKERSenv gate) and makes it shippable. On top of the rebase it:cluster_worker_runre-implemented the expert FFN as a parallel copy ofmoe()and was missing the fmt=6 (E8/IQ3) gate/up input pre-rotation, so it produced wrong numbers for E8/IQ3-quantized experts. The worker now calls the sharedexpert_ffnhelper (refactor: extract shared routed-expert FFN helper (byte-identical) #1035) and applies the same input rotationmoe()does — the parallel copy is gone, so the drift that caused the bug is structurally impossible.cluster_init's connect bookkeeping was inverted, so the coordinator always exited "no expert workers reachable" even with a live worker.openai_server's handler (inheriting the DNS-rebinding guard and connection caps), and exposes the existing--allowed-hostescape hatch oncoli cluster coordinatorso a cross-host worker can register.Correctness gate
Byte-for-byte worker-vs-local equality, proven token-exact.
c/tests/test_cluster_sharding.pyrunsglm_tinyin teacher-forcing prefill twice — local CPU vs cluster-delegated — and asserts zero[ORACLE]mismatch lines in both runs and identical token signatures. It covers fmt=6 (E8/IQ3, the rotation-bearing format — the arm that fails pre-fix) and fmt=4 (int4-grouped, the no-rotation control). Determinism is pinned: same binary/ARCH for both sides, CPU-only (COLI_CUDA=COLI_METAL=COLI_VULKAN=0), and a fixed numeric-env block (IDOT=0 I4S=1 COLI_NO_FUSED_PAIR=1 SPEC_PIN=0 XEXP=0 I4_ACC512=0 I3_AVX512=0 DRAFT=0). The gate demonstrably fails on the pre-fix worker (fmt=6 delegated drops to 0/32 with 32 mismatches) and passes post-fix (32/32, zero mismatch). It skips cleanly when the gitignoredglm_tinyfixture is absent, and is wired into CI (cluster-parityjob regenerates the fixtures then runs the gate). A no-model C test (test_cluster_protocol.c) asserts byte-exact COLIEX01 framing in both directions.The oracle was run twice (local + delegated, union of both format arms). The default path is unchanged unless
CLUSTER_WORKERSis set — the cluster path is env-gated OFF and the single-machine build is behaviourally identical.RAM budget / expert cache interaction (#403)
Distribution does not double-count the RAM budget. The coordinator's
cap_for_ramenforces the budget against its own measured RSS; a worker is a separate process holding its own resident expert set, and its resident bytes never enter the coordinator's accounting. The worker's cache is deliberately minimal — one disk-backed slot per layer (demand=0) — so it holds no resident experts beyond what it is actively computing. The one operator-visible consequence is that fleet-wide RSS is the sum of per-box budgets (coordinator box plus each worker box), each enforced independently; there is no cross-machine accounting today, which is fine for the MVP but worth a follow-up if workers are ever co-located on the coordinator's box.Validation
make→ 0 warnings;make check→ C suiteALL PASS (0 failures), PythonRan 473 tests OK.[ORACLE]mismatch (and demonstrably fails pre-fix).test_cluster_protocolfalsifiability-verified (mutating a framing field fails).Compatibility
CLUSTER_WORKERS); single-machine path unchanged.COLI_MODEL_DIRSviast_init_multi(matches a sharded coordinator).--allowed-host(same escape hatch ascoli serve); the registry defaults to loopback + bind address. The raw TCP worker binds0.0.0.0with no auth, matching the trusted-LAN assumption of the data plane (transport is an interface; auth is a follow-up).Note
This PR depends on the helper refactor in #1035 (review that one first). CI may sit in
action_requiredfor a first-time contributor — would appreciate the workflow run being approved.