/** * SPIRA — Spiking Plastic Recurrent Architecture, TypeScript inference port. * * Loads the JSON artifact exported by `spira.export_deploy` and provides: * - EPT tokenization (entropy patches) * - LIF reservoir simulation (weights regenerated from seed, bit-identical) * - delta-rule readout scoring (int8-quantized W_out, mean-centered trace) * - hippocampal associative memory with hybrid addressing: * primary full-key hash (pattern separation / capacity) + * leave-one-out hashes (CA3-style completion under corruption) * - closed-loop generation and ONE-SHOT episodic teaching (in browser) * * Everything runs on plain CPU in the browser — no GPU, no server. */ import { Mulberry32, fnv1a64, inputNeurons, laneKey32, splitmix64 } from "./hashing"; import { EPT, Patch } from "./ept"; const b64ToU8 = (b64: string): Uint8Array => { const bin = atob(b64); const u8 = new Uint8Array(bin.length); for (let i = 0; i < bin.length; i++) u8[i] = bin.charCodeAt(i); return u8; }; export interface SpiraDeploy { format: string; config: Record; ept: { budget_bits: number; min_len: number; max_len: number; alpha: number; log2p_b64: string }; reservoir: { w_rec_rows: string; w_rec_cols: string; w_rec_vals_b64: string; w_rec_n: number }; readout: { shape: [number, number]; vocab_ids_hex: string[]; w_out_q_b64: string; w_out_scale_b64: string; }; hier?: { n_g: number; win: number; lam_g: number; tau_trace_g: number; target_rate_g: number; eta_homeo_g: number; beta: number; fusion_scale: number; w_ing_b64: string; w_recg_b64: string; w_outg_q_b64: string; w_outg_scale_b64: string; }; am: { n_entries: number; packed_b64: string; entry_bytes: number }; registry: { n: number; ids_b64: string; lens_b64: string; buffer_b64: string }; core_state?: { x_mean_b64: string; thr_b64: string; rhat_b64: string; x_mean_g_b64?: string; thr_g_b64?: string; rhat_g_b64?: string; }; fusion?: { scale: number; sigma_fast?: number; sigma_slow?: number }; meta: Record; } export interface TeachResult { tokens: number; newPatches: number; associations: number; ms: number; } const rotl = (x: number, r: number) => ((x << r) | (x >>> (32 - r))) >>> 0; export class SpiraModel { ept: EPT; // dimensions dIn: number; kIn: number; nRes: number; nOut: number; vocabSize: number; // reservoir wIn!: Float32Array; wRec!: Float32Array; lam: number; baseThr: number; thrMin: number; thrMax: number; refrac: number; tauTrace: number; etaHomeo: number; targetRate: number; // state v!: Float32Array; thr!: Float32Array; rhat!: Float32Array; refr!: Int8Array; x!: Float32Array; xPrev!: Float32Array; xMean!: Float32Array; s!: Float32Array; // readout wOutQ!: Int8Array; wOutScale!: Float32Array; vocabIds: bigint[] = []; colOf = new Map(); // fusion betaAm: number; amOverride: number; temperature: number; topK: number; // hippocampus (hybrid addressing) amSize: number; amM: number; amMinLanes: number; amKeys!: Uint32Array; amVals!: BigUint64Array; amCounts!: Uint8Array; amUsed!: Uint8Array; // registry regIds: bigint[] = []; regLens: number[] = []; regOffsets: number[] = []; regBuffer!: Uint8Array; regIndexOf = new Map(); // hierarchical slow area (G) — v2 hier: boolean; nG = 0; win = 0; lamG = 0; tauTraceG = 0; targetRateG = 0; etaHomeoG = 0; beta = 0; fusionScale = 1; wInG!: Float32Array; wRecG!: Float32Array; wOutGQ!: Int8Array; wOutGScale!: Float32Array; vG!: Float32Array; thrG!: Float32Array; rhatG!: Float32Array; xG!: Float32Array; xMeanG!: Float32Array; sG!: Float32Array; winSpikes!: Float32Array; uSlow!: Float32Array; winCount = 0; meta: Record; constructor(art: SpiraDeploy) { const c = art.config; this.dIn = c.d_in; this.kIn = c.k_in; this.nRes = c.n_res; this.vocabSize = c.vocab_size; this.nOut = this.vocabSize + 1; this.lam = c.lam; this.baseThr = c.base_thr; this.thrMin = c.thr_min; this.thrMax = c.thr_max; this.refrac = c.refrac; this.tauTrace = c.tau_trace; this.etaHomeo = c.eta_homeo; this.targetRate = c.target_rate; this.betaAm = c.beta_am; this.amOverride = c.am_override; this.temperature = c.temperature; this.topK = c.top_k; this.amSize = c.am_size; this.amM = c.am_m; this.amMinLanes = c.am_min_lanes; this.meta = art.meta; // ---- EPT ---- const log2p = new Float32Array(b64ToU8(art.ept.log2p_b64).buffer); this.ept = new EPT(log2p, art.ept.budget_bits, art.ept.min_len, art.ept.max_len); // ---- reservoir: regenerate input weights from the seed ---- this.buildW_in(c.seed, c.in_fanout, c.rec_fanout, c.inh_frac, c.w_in_scale, c.w_rec_scale); this.buildW_rec(art.reservoir); // ---- readout ---- this.wOutQ = new Int8Array(b64ToU8(art.readout.w_out_q_b64).buffer); this.wOutScale = new Float32Array(b64ToU8(art.readout.w_out_scale_b64).buffer); this.vocabIds = art.readout.vocab_ids_hex.map((h) => BigInt(h)); this.vocabIds.forEach((id, col) => this.colOf.set(id, col)); // ---- hippocampus ---- this.amKeys = new Uint32Array(this.amSize * this.amM); this.amVals = new BigUint64Array(this.amSize); this.amCounts = new Uint8Array(this.amSize); this.amUsed = new Uint8Array(this.amSize); this.loadAmEntries(art.am); // ---- registry ---- this.loadRegistry(art.registry); // ---- hierarchical slow area (v2) ---- this.hier = !!(art.hier && art.config.hierarchy); if (this.hier && art.hier) { const h = art.hier; this.nG = h.n_g; this.win = h.win; this.lamG = h.lam_g; this.tauTraceG = h.tau_trace_g; this.targetRateG = h.target_rate_g; this.etaHomeoG = h.eta_homeo_g; this.beta = h.beta; this.fusionScale = h.fusion_scale; this.wInG = new Float32Array(b64ToU8(h.w_ing_b64).buffer); this.wRecG = new Float32Array(b64ToU8(h.w_recg_b64).buffer); this.wOutGQ = new Int8Array(b64ToU8(h.w_outg_q_b64).buffer); this.wOutGScale = new Float32Array(b64ToU8(h.w_outg_scale_b64).buffer); } // ---- learned homeostatic state ---- this.resetCore(); if (art.core_state) { this.xMean = new Float32Array(b64ToU8(art.core_state.x_mean_b64).buffer); this.thr = new Float32Array(b64ToU8(art.core_state.thr_b64).buffer); this.rhat = new Float32Array(b64ToU8(art.core_state.rhat_b64).buffer); if (this.hier && art.core_state.x_mean_g_b64) { this.xMeanG = new Float32Array(b64ToU8(art.core_state.x_mean_g_b64).buffer); this.thrG = new Float32Array(b64ToU8(art.core_state.thr_g_b64).buffer); this.rhatG = new Float32Array(b64ToU8(art.core_state.rhat_g_b64).buffer); } } } // ------------------------------------------------------------ build/load private buildW_in(seed: number, inFanout: number, recFanout: number, inhFrac: number, wInScale: number, wRecScale: number) { const rng = new Mulberry32(seed); this.wIn = new Float32Array(this.dIn * this.nRes); for (let i = 0; i < this.dIn; i++) { for (let f = 0; f < inFanout; f++) { const t = Math.floor(rng.nextFloat() * this.nRes); this.wIn[i * this.nRes + t] += rng.nextWeight(wInScale); } } const wRec = new Float32Array(this.nRes * this.nRes); const wRecMax = 3 * wRecScale; const lo = new Float32Array(this.nRes); const hi = new Float32Array(this.nRes); for (let j = 0; j < this.nRes; j++) { const isInh = rng.nextFloat() < inhFrac; const sign = isInh ? -1 : 1; lo[j] = isInh ? -wRecMax : 0; hi[j] = isInh ? 0 : wRecMax; for (let f = 0; f < recFanout; f++) { const t = Math.floor(rng.nextFloat() * this.nRes); wRec[j * this.nRes + t] += sign * Math.abs(rng.nextWeight(wRecScale)); } } for (let j = 0; j < this.nRes; j++) { for (let t = 0; t < this.nRes; t++) { const w = wRec[j * this.nRes + t]; wRec[j * this.nRes + t] = Math.min(Math.max(w, lo[j]), hi[j]); } } this._wRecBase = wRec; } private _wRecBase!: Float32Array; private buildW_rec(res: SpiraDeploy["reservoir"]) { const wRec = this._wRecBase.slice(); const rows = new Int32Array(b64ToU8(res.w_rec_rows).buffer); const cols = new Int32Array(b64ToU8(res.w_rec_cols).buffer); const vals = new Float32Array(b64ToU8(res.w_rec_vals_b64).buffer); for (let i = 0; i < res.w_rec_n; i++) { wRec[rows[i] * this.nRes + cols[i]] = vals[i]; } this.wRec = wRec; } // ---- hippocampal addressing: primary + leave-one-out ---- private primaryAddr(k: Uint32Array): number { const z = (k[0] ^ rotl(k[1], 8) ^ rotl(k[2], 16) ^ rotl(k[3], 24)) >>> 0; return Number(splitmix64(BigInt(z)) % BigInt(this.amSize)); } private looAddr(k: Uint32Array, l: number): number { const a: number[] = []; for (let i = 0; i < this.amM; i++) if (i !== l) a.push(k[i]); const z = (a[0] ^ rotl(a[1], 13) ^ rotl(a[2], 26)) >>> 0; return Number( splitmix64(BigInt(z) ^ BigInt(0x9e3779b9 + l * 0x85ebca6b)) % BigInt(this.amSize) ); } private loadAmEntries(am: SpiraDeploy["am"]) { const packed = b64ToU8(am.packed_b64); const m = this.amM; const eb = am.entry_bytes; const dv = new DataView(packed.buffer); for (let i = 0; i < am.n_entries; i++) { const off = i * eb; const key: number[] = []; for (let l = 0; l < m; l++) key.push(dv.getUint32(off + l * 4, true)); const val = dv.getBigUint64(off + 16, true); const count = packed[off + 24]; const k = Uint32Array.from(key); // primary slot const p = this.primaryAddr(k); for (let l2 = 0; l2 < m; l2++) this.amKeys[p * m + l2] = k[l2]; this.amVals[p] = val; this.amCounts[p] = count; this.amUsed[p] = 1; // leave-one-out completion slots for (let l = 0; l < m; l++) { const a = this.looAddr(k, l); if (!this.amUsed[a]) { for (let l2 = 0; l2 < m; l2++) this.amKeys[a * m + l2] = k[l2]; this.amVals[a] = val; this.amCounts[a] = count; this.amUsed[a] = 1; } } } } private loadRegistry(reg: SpiraDeploy["registry"]) { const ids = new BigUint64Array(b64ToU8(reg.ids_b64).buffer); const lens = new Uint16Array(b64ToU8(reg.lens_b64).buffer); this.regBuffer = b64ToU8(reg.buffer_b64); let off = 0; for (let i = 0; i < reg.n; i++) { const id = ids[i]; this.regIds.push(id); this.regLens.push(lens[i]); this.regOffsets.push(off); this.regIndexOf.set(id, i); off += lens[i]; } } // ------------------------------------------------------------- reservoir resetCore() { this.v = new Float32Array(this.nRes); this.thr = new Float32Array(this.nRes).fill(this.baseThr); this.rhat = new Float32Array(this.nRes).fill(this.targetRate); this.refr = new Int8Array(this.nRes); this.x = new Float32Array(this.nRes); this.xPrev = new Float32Array(this.nRes); this.xMean = new Float32Array(this.nRes); this.s = new Float32Array(this.nRes); if (this.hier) { this.vG = new Float32Array(this.nG); this.thrG = new Float32Array(this.nG).fill(this.baseThr); this.rhatG = new Float32Array(this.nG).fill(this.targetRateG); this.xG = new Float32Array(this.nG); this.xMeanG = new Float32Array(this.nG); this.sG = new Float32Array(this.nG); this.winSpikes = new Float32Array(this.nRes); this.uSlow = new Float32Array(this.nOut); this.winCount = 0; } } /** Reset transients, KEEP learned homeostatic scalars (before generation). */ resetDynamics() { const { thr, rhat, xMean } = this; let thrG: Float32Array | undefined, rhatG: Float32Array | undefined, xMeanG: Float32Array | undefined; if (this.hier) { thrG = this.thrG; rhatG = this.rhatG; xMeanG = this.xMeanG; } this.resetCore(); this.thr = thr; this.rhat = rhat; this.xMean = xMean; if (this.hier && thrG && rhatG && xMeanG) { this.thrG = thrG; this.rhatG = rhatG; this.xMeanG = xMeanG; } } /** Advance one tick with the given active input neurons; returns trace x. */ step(inputIdx: number[]): Float32Array { const n = this.nRes; const I = new Float32Array(n); for (const idx of inputIdx) { const base = idx * n; for (let j = 0; j < n; j++) I[j] += this.wIn[base + j]; } for (let j = 0; j < n; j++) { const sj = this.s[j]; if (sj !== 0) { const base = j * n; for (let t = 0; t < n; t++) I[t] += this.wRec[base + t] * sj; } } const v = this.v, thr = this.thr, refr = this.refr, rhat = this.rhat; const spikes = new Float32Array(n); for (let j = 0; j < n; j++) { let vj = this.lam * v[j] + I[j]; if (refr[j] !== 0) vj = 0; if (vj >= thr[j] && refr[j] === 0) { spikes[j] = 1; v[j] = 0; } else { v[j] = vj; } if (refr[j] > 0) refr[j]--; if (spikes[j]) refr[j] = this.refrac; rhat[j] += 0.02 * (spikes[j] - rhat[j]); thr[j] += this.etaHomeo * (rhat[j] - this.targetRate); if (thr[j] < this.thrMin) thr[j] = this.thrMin; else if (thr[j] > this.thrMax) thr[j] = this.thrMax; } this.s = spikes; if (this.hier) { const ws = this.winSpikes; for (let j = 0; j < n; j++) ws[j] += spikes[j]; } this.xPrev = this.x; const x = this.x; for (let j = 0; j < n; j++) x[j] = this.tauTrace * x[j] + spikes[j]; // slow EMA of the trace (homeostatic scaling analogue; readout centering) const xm = this.xMean; for (let j = 0; j < n; j++) xm[j] += 0.001 * (x[j] - xm[j]); return x; } /** * Advance the slow area one slow step (one window of `win` fast ticks). * Input: the pooled fast spike volley, sign-preserved through Dale's law * of the presynaptic fast neuron. Output: the cached slow readout uSlow, * used as a variance-matched top-down prior with gain beta. */ stepG() { const nG = this.nG; const I = new Float32Array(nG); const inv = 1 / this.win; for (let j = 0; j < this.nRes; j++) { const c = this.winSpikes[j] * inv; if (c === 0) continue; const base = j * nG; for (let g = 0; g < nG; g++) I[g] += c * this.wInG[base + g]; } for (let j = 0; j < nG; j++) { const sj = this.sG[j]; if (sj !== 0) { const base = j * nG; for (let g = 0; g < nG; g++) I[g] += this.wRecG[base + g] * sj; } } const v = this.vG, thr = this.thrG, rhat = this.rhatG, x = this.xG, xm = this.xMeanG; const spikes = new Float32Array(nG); for (let j = 0; j < nG; j++) { const vj = this.lamG * v[j] + I[j]; if (vj >= thr[j]) { spikes[j] = 1; v[j] = 0; } else { v[j] = vj; } rhat[j] += 0.05 * (spikes[j] - rhat[j]); thr[j] += this.etaHomeoG * (rhat[j] - this.targetRateG); if (thr[j] < this.thrMin) thr[j] = this.thrMin; else if (thr[j] > this.thrMax) thr[j] = this.thrMax; x[j] = this.tauTraceG * x[j] + spikes[j]; xm[j] += 0.001 * (x[j] - xm[j]); } this.sG = spikes; this.winSpikes.fill(0); // cached slow readout (int8-quantized, per-column scale) const nOut = this.nOut; const raw = new Float32Array(nOut); for (let j = 0; j < nG; j++) { const xj = x[j] - xm[j]; if (xj === 0) continue; const base = j * nOut; for (let c = 0; c < nOut; c++) raw[c] += xj * this.wOutGQ[base + c]; } const u = this.uSlow; for (let c = 0; c < nOut; c++) u[c] = raw[c] * this.wOutGScale[c]; } /** Feed one token: fast step (+ slow step at window boundaries). */ feed(pid: bigint): Float32Array { const x = this.step(inputNeurons(pid, this.dIn, this.kIn)); if (this.hier) { this.winCount++; if (this.winCount % this.win === 0) this.stepG(); } return x; } /** Behavioural scores: fast readout + variance-matched top-down prior. */ scores(x: Float32Array): Float32Array { const nRes = this.nRes, nOut = this.nOut; const raw = new Float32Array(nOut); for (let j = 0; j < nRes; j++) { const xj = x[j] - this.xMean[j]; if (xj === 0) continue; const base = j * nOut; for (let c = 0; c < nOut; c++) raw[c] += xj * this.wOutQ[base + c]; } const u = new Float32Array(nOut); if (this.hier) { const bs = this.beta * this.fusionScale; for (let c = 0; c < nOut; c++) u[c] = raw[c] * this.wOutScale[c] + bs * this.uSlow[c]; } else { for (let c = 0; c < nOut; c++) u[c] = raw[c] * this.wOutScale[c]; } return u; } // ------------------------------------------------------------ hippocampus private writeAm(laneKeys: Uint32Array, val: bigint) { const m = this.amM; // primary slot: exact-context capacity const p = this.primaryAddr(laneKeys); if (!this.amUsed[p]) { for (let l = 0; l < m; l++) this.amKeys[p * m + l] = laneKeys[l]; this.amVals[p] = val; this.amCounts[p] = 1; this.amUsed[p] = 1; } else { let same = true; for (let l = 0; l < m; l++) if (this.amKeys[p * m + l] !== laneKeys[l]) { same = false; break; } if (same) { if (this.amVals[p] === val) { if (this.amCounts[p] < 255) this.amCounts[p]++; } else { if (this.amCounts[p] > 1) this.amCounts[p]--; else { this.amVals[p] = val; this.amCounts[p] = 1; } } } else if (this.amCounts[p] <= 1) { for (let l = 0; l < m; l++) this.amKeys[p * m + l] = laneKeys[l]; this.amVals[p] = val; this.amCounts[p] = 1; } } // leave-one-out completion slots for (let l = 0; l < m; l++) { const a = this.looAddr(laneKeys, l); if (!this.amUsed[a]) { for (let l2 = 0; l2 < m; l2++) this.amKeys[a * m + l2] = laneKeys[l2]; this.amVals[a] = val; this.amCounts[a] = 1; this.amUsed[a] = 1; } } } /** Query the hippocampus: (patchId, weight) or null. Two-tier voting. */ queryAm(laneKeys: Uint32Array): { val: bigint; weight: number } | null { const m = this.amM; const votes = new Map(); const maxMatch = new Map(); // primary probe: exact full-key retrieval const p = this.primaryAddr(laneKeys); if (this.amUsed[p]) { let same = true; for (let l = 0; l < m; l++) if (this.amKeys[p * m + l] !== laneKeys[l]) { same = false; break; } if (same) { const v = this.amVals[p]; const w = m * (1 + Math.log1p(this.amCounts[p])); votes.set(v, (votes.get(v) ?? 0) + w); maxMatch.set(v, m); } } // leave-one-out probes: corrupted-cue completion for (let l = 0; l < m; l++) { const a = this.looAddr(laneKeys, l); if (!this.amUsed[a]) continue; let match = 0; for (let l2 = 0; l2 < m; l2++) if (this.amKeys[a * m + l2] === laneKeys[l2]) match++; if (match >= this.amMinLanes) { const v = this.amVals[a]; const w = match * (1 + Math.log1p(this.amCounts[a])); votes.set(v, (votes.get(v) ?? 0) + w); if (match > (maxMatch.get(v) ?? 0)) maxMatch.set(v, match); } } if (!votes.size) return null; let top = 0; for (const mm of maxMatch.values()) if (mm > top) top = mm; let bestV: bigint | null = null; let bestW = 0; for (const [v, w] of votes) { const eff = top === m ? (maxMatch.get(v) === m ? w : -1) : w; if (eff >= 0 && eff > bestW) { bestW = eff; bestV = v; } } if (bestV === null) return null; return { val: bestV, weight: bestW }; } private laneKeysOf(ids: bigint[]): Uint32Array { const m = this.amM; const out = new Uint32Array(m); for (let l = 0; l < m; l++) out[l] = laneKey32(ids[ids.length - m + l], l); return out; } // -------------------------------------------------------------- registry bytesOfId(id: bigint): Uint8Array | null { const idx = this.regIndexOf.get(id); if (idx === undefined) return null; const off = this.regOffsets[idx]; return this.regBuffer.slice(off, off + this.regLens[idx]); } private registerPatches(patches: Patch[]): number { let added = 0; for (const p of patches) { if (!this.regIndexOf.has(p.id)) { const idx = this.regIds.length; this.regIds.push(p.id); const off = this.regOffsets.length ? this.regOffsets[this.regOffsets.length - 1] + this.regLens[this.regLens.length - 1] : 0; this.regOffsets.push(off); this.regLens.push(p.bytes.length); this.regIndexOf.set(p.id, idx); const newBuf = new Uint8Array(this.regBuffer.length + p.bytes.length); newBuf.set(this.regBuffer); newBuf.set(p.bytes, this.regBuffer.length); this.regBuffer = newBuf; added++; } } return added; } // ------------------------------------------------------------- one-shot /** ONE-SHOT episodic teaching: one pass, no gradients, no repetition. */ teach(text: string | Uint8Array): TeachResult { const t0 = performance.now(); const data = typeof text === "string" ? new TextEncoder().encode(text) : text; const patches = this.ept.segment(data); const ids = patches.map((p) => p.id); const newPatches = this.registerPatches(patches); const m = this.amM; let associations = 0; for (let r = 0; r + m < ids.length; r++) { const laneKeys = new Uint32Array(m); for (let l = 0; l < m; l++) laneKeys[l] = laneKey32(ids[r + l], l); this.writeAm(laneKeys, ids[r + m]); associations++; } return { tokens: ids.length, newPatches, associations, ms: performance.now() - t0 }; } /** * Retrieve the memorized continuation of a context (multi-patch replay), * with optional corruption: `corruptSlots` entries of the 4-slot context * are replaced by random registered patches before querying (pattern * completion test). Repeats the completion `steps` times, sliding the * context window. */ recall(context: string, corruptSlots = 0, rngSeed = 11, steps = 8): { continuation: string; weight: number; } | null { const data = new TextEncoder().encode(context); const patches = this.ept.segment(data); if (patches.length < this.amM) return null; const ids = patches.map((p) => p.id); const enc = new TextEncoder(); // initial (optionally corrupted) lane keys const laneKeys = this.laneKeysOf(ids); if (corruptSlots > 0 && this.regIds.length > 8) { let seed = rngSeed >>> 0; const rand = () => { seed ^= seed << 13; seed >>>= 0; seed ^= seed >>> 17; seed ^= seed << 5; seed >>>= 0; return seed / 4294967296; }; const slots = new Set(); let guard = 0; while (slots.size < Math.min(corruptSlots, this.amM) && guard++ < 32) { slots.add(Math.floor(rand() * this.amM) % this.amM); } for (const slot of slots) { const randIdx = Math.floor(rand() * this.regIds.length) % this.regIds.length; laneKeys[slot] = laneKey32(this.regIds[randIdx], slot); } } const hit = this.queryAm(laneKeys); if (!hit) return null; const first = this.bytesOfId(hit.val); if (!first) return null; // greedy multi-patch replay through the memory let out: Uint8Array = first; let weight = hit.weight; const ctx = ids.slice(); ctx.push(hit.val); for (let s = 1; s < steps; s++) { const lk = this.laneKeysOf(ctx); const h = this.queryAm(lk); if (!h) break; const b = this.bytesOfId(h.val); if (!b) break; const merged = new Uint8Array(out.length + b.length); merged.set(out); merged.set(b, out.length); out = merged; weight = Math.min(weight, h.weight); ctx.push(h.val); } return { continuation: new TextDecoder().decode(out), weight }; } // ------------------------------------------------------------ generation /** Closed-loop generation: predicted patches are fed back as spikes. */ generate(prompt: string, opts?: { nTokens?: number; temperature?: number; topK?: number; useAm?: boolean; seed?: number; onToken?: (step: number, bytes: Uint8Array) => void; }): string { const nTokens = opts?.nTokens ?? 120; const temperature = opts?.temperature ?? this.temperature; const topK = opts?.topK ?? this.topK; const useAm = opts?.useAm ?? true; let seed = opts?.seed ?? 12345; const rand = () => { seed ^= seed << 13; seed >>>= 0; seed ^= seed >>> 17; seed ^= seed << 5; seed >>>= 0; return seed / 4294967296; }; this.resetDynamics(); const data = new TextEncoder().encode(prompt); const patches = this.ept.segment(data); if (!patches.length) return prompt; const ids: bigint[] = patches.map((p) => p.id); const out: number[] = Array.from(data); for (const pid of ids) { this.feed(pid); } for (let step = 0; step < nTokens; step++) { const x = this.x; let u = this.scores(x); let bypassId: bigint | null = null; if (useAm && ids.length >= this.amM) { const hit = this.queryAm(this.laneKeysOf(ids)); if (hit) { const col = this.colOf.get(hit.val); if (col !== undefined) { u = u.slice(); u[col] += this.betaAm * hit.weight; } else if (hit.weight >= this.amOverride && this.bytesOfId(hit.val)) { bypassId = hit.val; } } } let chosenId: bigint; if (bypassId !== null) { chosenId = bypassId; } else { // top-k selection by a single partial scan (k is small) const k = Math.max(1, Math.min(topK, this.vocabSize)); const top: number[] = []; const topScores: number[] = []; for (let c = 0; c < this.vocabSize; c++) { const sc = u[c]; if (topScores.length < k) { top.push(c); topScores.push(sc); // keep the small list sorted descending for (let i = topScores.length - 1; i > 0 && topScores[i] > topScores[i - 1]; i--) { const ts = topScores[i]; topScores[i] = topScores[i - 1]; topScores[i - 1] = ts; const ti = top[i]; top[i] = top[i - 1]; top[i - 1] = ti; } } else if (sc > topScores[k - 1]) { top[k - 1] = c; topScores[k - 1] = sc; for (let i = k - 1; i > 0 && topScores[i] > topScores[i - 1]; i--) { const ts = topScores[i]; topScores[i] = topScores[i - 1]; topScores[i - 1] = ts; const ti = top[i]; top[i] = top[i - 1]; top[i - 1] = ti; } } } const maxS = topScores[0]; const z = top.map((c) => (u[c] - maxS) / Math.max(temperature, 1e-6)); const exps = z.map(Math.exp); const tot = exps.reduce((a, b) => a + b, 0); let r = rand() * tot; let pick = top[0]; for (let i = 0; i < top.length; i++) { r -= exps[i]; if (r <= 0) { pick = top[i]; break; } } chosenId = this.vocabIds[pick]; } const b = this.bytesOfId(chosenId); if (!b) break; for (const byte of b) out.push(byte); ids.push(chosenId); this.feed(chosenId); opts?.onToken?.(step, b); } return new TextDecoder().decode(new Uint8Array(out)); } // ------------------------------------------------------------- utilities surpriseProfile(text: string): { surprise: Float32Array; patches: Patch[] } { const data = new TextEncoder().encode(text); return { surprise: this.ept.surprise(data), patches: this.ept.segment(data) }; } stats() { const slowAmortized = this.hier ? (this.nG * (this.vocabSize + 1) + this.nRes * this.nG + this.nG * this.nG) / this.win : 0; const macs = this.kIn * this.nRes + this.nRes * this.nRes + this.nRes * (this.vocabSize + 1) + slowAmortized; return { neurons: this.nRes, inputNeurons: this.dIn, readout: this.vocabSize + 1, hierarchy: this.hier ? { neurons: this.nG, window: this.win, beta: this.beta } : null, synapses: { input: this.dIn * 8, recurrent: this.nRes * this.nRes, readout: this.nRes * (this.vocabSize + 1), slowReadout: this.hier ? this.nG * (this.vocabSize + 1) : 0, }, macsPerToken: macs, registry: this.regIds.length, amEntries: this.amUsed.reduce((a, b) => a + b, 0), }; } } let _model: SpiraModel | null = null; /** Lazy singleton loader (the artifact is ~29 MB — fetched once). */ export async function loadSpira(): Promise { if (_model) return _model; const res = await fetch("/spira_deploy.json"); if (!res.ok) throw new Error(`artifact fetch failed: ${res.status}`); const art = (await res.json()) as SpiraDeploy; _model = new SpiraModel(art); return _model; }