Quazim0t0's picture
DaisyChain-Infer: project files + guide
30bafb7 verified
Raw
History Blame Contribute Delete
8.71 kB
// The wire protocol, as pure functions.
//
// Kept out of app.js on purpose: a codec that only exists inside a browser
// event handler cannot be tested, and the failures this protocol can have are
// precisely the ones that are invisible at runtime. A misread offset does not
// throw — it yields a plausible number, and a plausible number in an
// activation is a slightly wrong sentence nobody can attribute to anything.
// So the codec lives here and test_wire.js round-trips it.
//
// Sentinels continue DaisyChain-Web's numbering (it uses -2..-8), so a client
// pointed at the wrong server sees an unknown tag rather than a valid-looking
// message of the wrong kind.
(function (root) {
"use strict";
const FRAG = -5, // fragment of a large message (shared with DaisyChain-Web)
HELLO = -20, // capability report
ASSIGN = -21, // model identity + which layers you own (no weights)
READY = -22, // a stage has fetched its layers and is in the ring
ACT = -23, // hidden state moving to the next stage
TOKEN = -24, // a token was emitted
DONE = -25; // run finished
// The address of the head's return leg. Not a stage index: the head is both
// stage 0 and the terminus, so "next = 0" is ambiguous — outbound it means
// "stage 0, run your blocks", inbound it means "the lap is done". They need
// distinct addresses or the head re-runs its own blocks and the lap never
// closes. Negative, so it can never collide with a real stage index.
const RETURN = -1;
const enc = new TextEncoder(), dec = new TextDecoder();
function tagOf(buf) { return new Int32Array(buf, 0, 1)[0]; }
// ---- hello -----------------------------------------------------------------
function packHello(capacity, probeHash, backend) {
const nb = enc.encode(String(backend || "?").slice(0, 32));
const buf = new ArrayBuffer(16 + nb.length);
new Int32Array(buf, 0, 1)[0] = HELLO;
new Float32Array(buf, 4, 1)[0] = capacity;
new Uint32Array(buf, 8, 1)[0] = probeHash >>> 0;
new Int32Array(buf, 12, 1)[0] = nb.length;
new Uint8Array(buf, 16).set(nb);
return buf;
}
function unpackHello(buf) {
const n = new Int32Array(buf, 12, 1)[0];
if (n < 0 || 16 + n > buf.byteLength) throw new Error("hello: bad backend length");
return { capacity: new Float32Array(buf, 4, 1)[0],
probeHash: new Uint32Array(buf, 8, 1)[0],
backend: dec.decode(new Uint8Array(buf, 16, n)) };
}
// ---- assignment -------------------------------------------------------------
// Weights do NOT travel between peers. The head sends each device the model's
// identity and which layers it owns; the device then fetches exactly those
// tensors from the Hub itself, with its own credentials. So this message is
// small, and a token never crosses the wire.
//
// [i32 ASSIGN][utf8 JSON]
//
// JSON rather than packed ints because the payload is a model spec whose
// fields differ by architecture. It is validated on arrival — a malformed
// assignment must fail here, not three layers deep in a GEMM.
function packAssign(a) {
const bytes = enc.encode(JSON.stringify(a));
const buf = new ArrayBuffer(4 + bytes.length);
new Int32Array(buf, 0, 1)[0] = ASSIGN;
new Uint8Array(buf, 4).set(bytes);
return buf;
}
function unpackAssign(buf) {
let a;
try { a = JSON.parse(dec.decode(new Uint8Array(buf, 4))); }
catch (e) { throw new Error("assignment: payload is not valid JSON"); }
if (!a || typeof a.repo !== "string" || !a.repo)
throw new Error("assignment: no repo id");
if (!/^[\w.-]+\/[\w.-]+$/.test(a.repo))
throw new Error(`assignment: "${a.repo}" is not a valid repo id`);
if (!a.spec || !a.spec.layers || !a.spec.hidden)
throw new Error("assignment: incomplete model spec");
if (!Array.isArray(a.plan) || !a.plan.length)
throw new Error("assignment: no plan");
// A plan that does not cover every layer exactly once still generates
// fluent text — with a layer missing. It has to be refused at the door.
let cover = 0;
for (const s of a.plan) {
if (typeof s.lo !== "number" || typeof s.hi !== "number" || s.hi < s.lo || s.lo < 0 || s.hi > a.spec.layers)
throw new Error("assignment: stage range out of bounds");
cover += s.hi - s.lo;
}
if (cover !== a.spec.layers)
throw new Error(`assignment: stages cover ${cover} of ${a.spec.layers} layers`);
if (!a.plan[0].head) throw new Error("assignment: stage 0 must be the head");
if (typeof a.mine !== "number" || a.mine < 0 || a.mine >= a.plan.length)
throw new Error("assignment: no valid stage index for this device");
return a;
}
// ---- activation ------------------------------------------------------------
// [i32 ACT, seq, tokenIdx, nextIndex][u32 actHash, modelHash][f32 hidden]
// actHash is an integrity check on the payload; modelHash answers a different
// question — whether this activation belongs to the model I hold a slice of.
// Two rings running at once on one device is not hypothetical: reload the
// head with a different checkpoint and the old stages are still out there.
function packAct(seq, tokenIdx, nextIndex, hidden, hashF32, modelHash) {
const buf = new ArrayBuffer(24 + hidden.byteLength);
new Int32Array(buf, 0, 4).set([ACT, seq, tokenIdx, nextIndex]);
new Uint32Array(buf, 16, 2).set([hashF32(hidden) >>> 0, modelHash >>> 0]);
new Float32Array(buf, 24).set(hidden);
return buf;
}
function unpackAct(buf, hashF32) {
if (buf.byteLength < 24) throw new Error("act: truncated header");
const iv = new Int32Array(buf, 0, 4);
const [actHash, modelHash] = new Uint32Array(buf, 16, 2);
const hidden = new Float32Array(buf.slice(24));
if (hashF32 && (hashF32(hidden) >>> 0) !== actHash) throw new Error("act: payload failed its integrity hash");
return { seq: iv[1], tokenIdx: iv[2], nextIndex: iv[3], actHash, modelHash, hidden };
}
// ---- routing ---------------------------------------------------------------
// Where an activation goes after this stage, and how to read one that arrives.
// These are two lines each and they live here, as pure functions, only
// because getting them wrong is invisible everywhere else: the math is
// correct, the bytes are correct, and the lap simply never closes. Neither a
// numeric oracle nor a codec round-trip can see that — it is a property of
// the ROUTE, so it needs something that can walk one.
function routeAfter(plan, myIndex) {
const isLast = myIndex === plan.length - 1;
const next = isLast ? plan[0] : plan[myIndex + 1];
return { to: next.id, address: isLast ? RETURN : next.index, isReturn: isLast };
}
function classifyAct(nextIndex, myIndex) {
if (nextIndex === RETURN) return "return"; // the lap is finished
if (nextIndex === myIndex) return "mine"; // run my blocks, pass it on
return "other"; // not addressed to me
}
// ---- token / done ----------------------------------------------------------
function packToken(seq, id, total) {
const buf = new ArrayBuffer(16);
new Int32Array(buf).set([TOKEN, seq, id, total]);
return buf;
}
function unpackToken(buf) {
const iv = new Int32Array(buf, 0, 4);
return { seq: iv[1], id: iv[2], total: iv[3] };
}
function packDone(seq) { return new Int32Array([DONE, seq]).buffer; }
// ---- ready ------------------------------------------------------------------
function packReady(stageIndex, ok, note) {
const bytes = enc.encode(JSON.stringify({ stageIndex, ok: !!ok, note: String(note || "").slice(0, 300) }));
const buf = new ArrayBuffer(4 + bytes.length);
new Int32Array(buf, 0, 1)[0] = READY;
new Uint8Array(buf, 4).set(bytes);
return buf;
}
function unpackReady(buf) {
try { return JSON.parse(dec.decode(new Uint8Array(buf, 4))); }
catch (e) { throw new Error("ready: payload is not valid JSON"); }
}
const api = { FRAG, HELLO, ASSIGN, READY, ACT, TOKEN, DONE, RETURN, tagOf,
packHello, unpackHello, packAssign, unpackAssign, packReady, unpackReady,
packAct, unpackAct, packToken, unpackToken, packDone,
routeAfter, classifyAct };
if (typeof module !== "undefined" && module.exports) module.exports = api;
else root.Wire = api;
})(typeof self !== "undefined" ? self : this);