export const DEFAULT_TIMING = {
beat: 400, drop: 1600, collect: 2000, takeover: 3200, };
export function policyFor(n, T = DEFAULT_TIMING) {
const beat = Math.round(T.beat * Math.max(1, n / 4));
return {
n,
quorum: n <= 3 ? n : Math.max(2, Math.ceil(n * 0.6)),
beat,
drop: Math.max(T.drop, beat * 4),
takeover: Math.max(T.takeover, beat * 8),
collect: Math.min(T.collect * 3, Math.round(T.collect * (1 + n / 16))),
};
}
function makeYield() {
if (typeof MessageChannel === "undefined") {
return () => new Promise((r) => setTimeout(r, 0));
}
const channel = new MessageChannel();
const waiting = [];
channel.port1.onmessage = () => waiting.shift()?.();
channel.port1.unref?.();
channel.port2.unref?.();
return () =>
new Promise((resolve) => {
waiting.push(resolve);
channel.port2.postMessage(0);
});
}
export function createRank({
id, bus, rank, post = () => {}, model = "?", backend = "cpu", precision = "f32",
timing = {},
}) {
const T = { ...DEFAULT_TIMING, ...timing };
const now = () => (typeof performance !== "undefined" ? performance.now() : Date.now());
const send = (msg) => bus.postMessage(msg);
const bucketLen = rank.bucket_len;
let running = false; let ready = false; let step = 0;
let lr = 0.05;
let looping = false;
let pending = null; let lastOpen = 0;
let joining = null;
let disposed = false;
let contributing = false; let lastSamples = 0; let roundsFrom = null; const yieldToQueue = makeYield();
let fault = null;
const members = new Map(); const stat = { steps: 0, samples: 0, bytesAt: 0, loss: NaN, since: now() };
let policy = policyFor(1, T);
const wireBytes = () => bus.bytes?.() ?? 0;
function live() {
const t = now();
for (const [k, m] of members) if (t - m.seen > policy.drop) members.delete(k);
return members;
}
function cohortIds() {
const ids = running && ready ? [id] : [];
for (const [k, m] of live()) if (m.ready && m.running) ids.push(k);
return ids.sort((a, b) => a - b);
}
function leaderId() {
let best = running && ready ? id : null;
for (const [k, m] of live()) {
if (!(m.ready && m.running)) continue;
if (best === null || k < best) best = k;
}
return best;
}
let deferUntil = 0;
const isLeader = () =>
running && ready && leaderId() === id && now() >= deferUntil;
function announce() {
send({ t: "hello", from: id, batch: rank.batch, ready, running, step, model, backend });
}
function beat() {
send({ t: "beat", from: id, batch: rank.batch, ready, running, step, backend });
}
async function driveRound() {
const ids = cohortIds();
const batches = new Map([[id, rank.batch]]);
for (const k of ids) if (k !== id) batches.set(k, members.get(k)?.batch ?? rank.batch);
const total = [...batches.values()].reduce((a, b) => a + b, 0);
const assign = {};
let offset = 0;
for (const k of ids) {
assign[k] = (step * total + offset) >>> 0;
offset += batches.get(k);
}
const got = new Map();
let settle = null;
const collected = ids.length > 1
? new Promise((resolve) => {
settle = resolve;
pending = {
step,
want: new Set(ids),
quorum: Math.min(ids.length, policy.quorum),
got,
resolve,
};
})
: null;
send({
t: "round",
from: id,
step,
lr,
assign,
params: rank.params(),
loss: stat.loss,
samples: lastSamples,
cohort: ids.length,
});
lastOpen = now();
const mine = await rank.grads(assign[id]);
got.set(id, { loss: mine[0], g: mine.subarray(1), samples: rank.batch });
if (collected) {
if (pending && got.size >= pending.quorum) settle();
else if (pending) pending.timer = setTimeout(settle, policy.collect);
await collected;
if (pending?.timer) clearTimeout(pending.timer);
pending = null;
}
const avg = new Float32Array(bucketLen);
let samples = 0, loss = 0;
for (const e of got.values()) samples += e.samples;
for (const e of got.values()) {
const w = e.samples / samples;
loss += w * e.loss;
for (let i = 0; i < bucketLen; i++) avg[i] += w * e.g[i];
}
rank.apply(avg, lr);
const published = bus.quantize?.(rank.params());
if (published) rank.set_params(published);
step += 1;
stat.steps += 1;
stat.samples += samples;
stat.loss = loss;
lastSamples = samples;
report(got.size);
}
function adopt(m) {
rank.set_params(m.params);
step = m.step;
ready = true;
stat.steps += 1;
stat.samples += m.samples ?? 0;
if (Number.isFinite(m.loss)) stat.loss = m.loss;
if (joining) { clearInterval(joining); joining = null; }
report(m.cohort);
}
async function loop() {
if (looping || disposed) return;
looping = true;
try {
while (running && ready && isLeader() && !disposed) {
for (let waited = 0; waited < 20 && (bus.pressure?.() ?? 0) > 1; waited++) {
await new Promise((r) => setTimeout(r, policy.beat));
}
try {
await driveRound();
fault = null;
} catch (e) {
fault = String(e?.message ?? e);
if (pending) { clearTimeout(pending.timer); pending.resolve(); pending = null; }
report();
await new Promise((r) => setTimeout(r, policy.beat));
}
await yieldToQueue(); }
} finally {
looping = false;
if (!running && ready && !disposed) publish();
}
}
function report(cohortSize) {
const dt = (now() - stat.since) / 1000;
post({
t: "state",
id: id.toString(16).padStart(8, "0"),
leader: isLeader(),
ready,
running,
step,
cohort: cohortSize ?? Math.max(1, cohortIds().length),
loss: stat.loss,
stepsPerSec: dt > 0 ? stat.steps / dt : 0,
samplesPerSec: dt > 0 ? stat.samples / dt : 0,
bytesPerSec: dt > 0 ? (wireBytes() - stat.bytesAt) / dt : 0,
peers: [...live().entries()].map(([k, m]) => ({
id: k.toString(16).padStart(8, "0"),
backend: m.backend ?? "?",
})),
bucket: bucketLen,
batch: rank.batch,
model,
backend,
precision,
fault,
policy: { n: policy.n, quorum: policy.quorum, beat: policy.beat },
});
if (dt > 2) {
stat.steps = 0;
stat.samples = 0;
stat.bytesAt = wireBytes();
stat.since = now();
}
}
function onMessage(m) {
if (disposed || !m || m.from === id) return;
if (m.to !== undefined && m.to !== id) return;
const peer = members.get(m.from) ?? {};
members.set(m.from, {
...peer,
seen: now(),
batch: m.batch ?? peer.batch,
ready: m.ready ?? peer.ready,
running: m.running ?? peer.running,
step: m.step ?? peer.step,
backend: m.backend ?? peer.backend,
});
switch (m.t) {
case "hello":
beat();
break;
case "round": {
lastOpen = now();
if (looping || pending) {
if (m.from < id) standDown();
break;
}
const lead = leaderId();
if (lead !== null && m.from > lead) break; if (m.from === roundsFrom && m.step < step) break; roundsFrom = m.from;
adopt(m);
if (!running) break;
if (m.assign?.[id] === undefined) break; contribute(m);
break;
}
case "grad":
if (pending && m.step === pending.step && pending.want.has(m.from)) {
pending.got.set(m.from, { loss: m.loss, g: m.g, samples: m.samples });
if (pending.got.size >= pending.quorum) pending.resolve();
}
break;
}
}
async function contribute(m) {
if (contributing) return; contributing = true;
try {
const out = await rank.grads(m.assign[id]);
if (m.step !== step) return;
send({
t: "grad", from: id, to: m.from, step: m.step,
loss: out[0], samples: rank.batch, g: out.subarray(1),
});
fault = null;
} catch (e) {
fault = String(e?.message ?? e);
report();
} finally {
contributing = false;
}
}
function publish() {
send({
t: "round",
from: id,
step,
lr,
assign: {},
params: rank.params(),
loss: stat.loss,
samples: lastSamples,
cohort: cohortIds().length,
});
}
function standDown() {
if (pending) { clearTimeout(pending.timer); pending.resolve(); pending = null; }
deferUntil = now() + T.takeover;
announce();
report();
}
function beginJoining() {
if (joining) clearInterval(joining);
let tries = 0;
joining = setInterval(() => {
if (disposed || ready) { clearInterval(joining); joining = null; return; }
const peers = [...live().entries()];
if (!peers.some(([, m]) => m.ready) && ++tries >= 2) {
const lowest = peers.reduce((lo, [k]) => Math.min(lo, k), id);
if (lowest === id) {
ready = true;
clearInterval(joining); joining = null;
report();
if (running) loop();
return;
}
}
announce();
}, T.beat);
}
let heart = null;
const tick = () => {
if (disposed) return;
policy = policyFor(live().size + 1, T);
beat();
if (running && ready && isLeader() && !looping && !pending
&& now() - lastOpen > policy.takeover) {
lastOpen = now();
loop();
}
report();
heart = setTimeout(tick, policy.beat);
};
heart = setTimeout(tick, T.beat);
bus.onmessage = (ev) => onMessage(ev.data ?? ev);
return {
start(nextLr) {
if (nextLr !== undefined) lr = nextLr;
running = true;
announce();
if (!ready) beginJoining();
else loop();
report();
},
stop() {
const wasLeader = isLeader();
running = false;
if (wasLeader && ready && !looping) publish();
beat();
report();
},
setLr(nextLr) { lr = nextLr; },
dispose() {
disposed = true;
running = false;
clearTimeout(heart);
if (joining) clearInterval(joining);
if (pending) { clearTimeout(pending.timer); pending.resolve(); pending = null; }
},
get state() { return { id, ready, running, step, leader: isLeader(), cohort: cohortIds() }; },
};
}