SPIRA v2: entropy-patch tokenization + hierarchical dopamine-gated spiking cortex + hippocampal one-shot memory with replay consolidation
032a961 verified Download web/src/lib/spira/model.ts from thefinalboss/SPIRA: direct link, hf CLI and curl.
- Browser
- Download file 29.6 kB
-
https://huggingface.co/thefinalboss/SPIRA/resolve/main/web/src/lib/spira/model.ts
- Command line
-
hf download hf://thefinalboss/SPIRA/web/src/lib/spira/model.ts
-
curl -L -o model.ts https://huggingface.co/thefinalboss/SPIRA/resolve/main/web/src/lib/spira/model.ts
29.6 kB
| /** | |
| * 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<string, number>; | |
| 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<string, number>; | |
| } | |
| 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<bigint, number>(); | |
| // 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<bigint, number>(); | |
| // 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<string, number>; | |
| 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<bigint, number>(); | |
| const maxMatch = new Map<bigint, number>(); | |
| // 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<number>(); | |
| 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<SpiraModel> { | |
| 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; | |
| } | |