// Model sharding — the piece DaisyChain-Train does not have. // // DaisyChain-Train pools COMPUTE: every node holds a full replica, so a model // bigger than one machine cannot be trained. Inference is where that limit can // be lifted, because a forward pass is a chain: layer l needs layer l-1's // OUTPUT, never its WEIGHTS. So the layers live on different machines and the // activation travels instead. // // And because safetensors gives every tensor an exact byte range, each device // fetches only its own layers straight from the Hub. No device — not even the // head — ever holds the whole model. That is what makes the pooling real // rather than a redistribution of something one machine already had to load. // // The ring shape is forced by weight tying. When lm_head is tied to the // embedding table, the largest tensor in the model is needed at BOTH ends: to // embed the prompt and to produce the logits. Copying it to the last stage // would hand back most of the memory just pooled, so the last stage returns // its hidden state to the head, which owns the embedding once and does both // ends of the pass. (function (root) { "use strict"; // ---- the plan --------------------------------------------------------------- // Stage 0 is the HEAD: embeddings, final norm, lm_head. Every stage may also // own a contiguous run of layers, apportioned by MEASURED capacity — the // same self-calibrating idea as cluster.py's capacity_score, except what is // balanced is layers per device rather than batch per device. // // Largest-remainder apportionment, so the plan is a pure function of the // capacity report: every device derives the same plan from the same inputs // and the plan never has to be trusted, only compared. function planStages(spec, caps) { const n = caps.length; if (n < 1) throw new Error("no devices to plan across"); const total = caps.reduce((a, d) => a + Math.max(1e-6, d.capacity), 0); const exact = caps.map(d => spec.layers * Math.max(1e-6, d.capacity) / total); const floor = exact.map(Math.floor); let left = spec.layers - floor.reduce((a, b) => a + b, 0); const order = exact.map((e, i) => [e - floor[i], i]).sort((a, b) => b[0] - a[0] || a[1] - b[1]); for (let i = 0; i < order.length && left > 0; i++, left--) floor[order[i][1]]++; const stages = []; let lo = 0; for (let i = 0; i < n; i++) { const hi = lo + floor[i]; stages.push({ id: caps[i].id, index: i, lo, hi, head: i === 0, backend: caps[i].backend, capacity: caps[i].capacity }); lo = hi; } // A device with no layers is only worth a hop if it is the head, which has // real work (embed + unembed). Anyone else empty is dropped. return stages.filter(s => s.head || s.hi > s.lo).map((s, i) => ({ ...s, index: i })); } // What a stage will cost to hold, computed from the file's own header BEFORE // any weight bytes move — so a device can see whether its slice fits before // spending the bandwidth finding out. function stageBytes(spec, available, st, ArchMod) { const names = ArchMod.tensorsFor(spec, available, st); let n = 0; for (const name of names) n += available.get(name).elems * 4; // f32 in memory return n; } // ---- arranging fetched tensors into what the forward pass wants ------------- // Two shape conventions have to be reconciled here, and getting it wrong is // silent: torch.nn.Linear stores weights (out, in) while the GEMM wants // (in, out), whereas GPT-2's Conv1D already stores (in, out). A wrong // transpose does not throw — it produces a model that generates confident // nonsense — so the layout comes from the spec and is applied once, at load. function toKN(w, outDim, inDim, layout) { if (layout === "in_out") return w; // already k x n const out = new Float32Array(w.length); for (let o = 0; o < outDim; o++) for (let i = 0; i < inDim; i++) out[i * outDim + o] = w[o * inDim + i]; return out; } function stageWeights(spec, st, got, ArchMod) { const resolve = ArchMod.resolver(new Map([...got.keys()].map(k => [k, true]))); const pick = (name, opt) => { const r = resolve(name, opt); return r ? got.get(r) : null; }; const C = spec.hidden, L = spec.weightLayout; const w = { layers: [] }; if (st.head) { const h = ArchMod.headTensors(spec); w.emb = pick(h.emb); // (vocab, C) — used as a lookup, not a GEMM w.pos = pick(h.pos, true); w.nrmF = pick(h.nrmF, true); w.nrmFb = pick(h.nrmFb, true); // lm_head as k x n = (C, vocab). When tied, that is embᵀ. const lm = pick(h.lmHead, true); w.lmHead = lm ? toKN(lm, spec.vocab, C, L) : transposeEmb(w.emb, spec.vocab, C); if (!w.nrmF) w.nrmF = new Float32Array(C).fill(1); // a model without a final norm weight } const qDim = spec.heads * spec.headDim, kvDim = spec.kvHeads * spec.headDim; for (let l = st.lo; l < st.hi; l++) { const t = ArchMod.layerTensors(spec, l); const ly = { nrm1: pick(t.nrm1), nrm1b: pick(t.nrm1b, true), nrm2: pick(t.nrm2), nrm2b: pick(t.nrm2b, true) }; if (spec.qkvFused) { ly.Wqkv = toKN(pick(t.Wqkv), 3 * C, C, L); ly.bqkv = pick(t.bqkv, true); } else { ly.Wq = toKN(pick(t.Wq), qDim, C, L); ly.bq = pick(t.bq, true); ly.Wk = toKN(pick(t.Wk), kvDim, C, L); ly.bk = pick(t.bk, true); ly.Wv = toKN(pick(t.Wv), kvDim, C, L); ly.bv = pick(t.bv, true); } ly.Wo = toKN(pick(t.Wo), C, qDim, L); ly.bo = pick(t.bo, true); if (spec.gated) { ly.Wgate = toKN(pick(t.Wgate), spec.inter, C, L); ly.Wup = toKN(pick(t.Wup), spec.inter, C, L); } else { ly.Wfc = toKN(pick(t.Wfc), spec.inter, C, L); ly.bfc = pick(t.bfc, true); } ly.Wdown = toKN(pick(t.Wdown), C, spec.inter, L); ly.bdown = pick(t.bdown, true); w.layers.push(ly); } return w; } function transposeEmb(emb, vocab, C) { const out = new Float32Array(emb.length); for (let v = 0; v < vocab; v++) for (let c = 0; c < C; c++) out[c * vocab + v] = emb[v * C + c]; return out; } // ---- hashes ----------------------------------------------------------------- // FNV-1a over raw bytes, identical to the function DaisyChain-Web hashes // replicas with, so hashes stay comparable across the projects. function fnv1a(bytes) { let h = 0x811c9dc5; for (let i = 0; i < bytes.length; i++) { h ^= bytes[i]; h = Math.imul(h, 0x01000193); } return h >>> 0; } function hashF32(a) { return fnv1a(new Uint8Array(a.buffer, a.byteOffset, a.byteLength)); } // The model fingerprint every stage repeats in its status. Derived from the // repo id, revision and the tensor index — NOT from the weights, because no // device reads all of them. It answers "are we all running the same model?", // which is the question that matters when stages hold disjoint pieces. function modelFingerprint(repo, revision, tensors) { const parts = [repo, revision || "main"]; for (const name of [...tensors.keys()].sort()) { const t = tensors.get(name); parts.push(`${name}:${t.dtype}:${t.shape.join("x")}:${t.start}:${t.end}`); } return fnv1a(new TextEncoder().encode(parts.join("|"))); } const api = { planStages, stageBytes, stageWeights, toKN, transposeEmb, fnv1a, hashF32, modelFingerprint }; if (typeof module !== "undefined" && module.exports) module.exports = api; else root.Shard = api; })(typeof self !== "undefined" ? self : this);