File size: 8,707 Bytes
30bafb7 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 | // 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);
|