@seanhogg/builderforce-memory-engine 2026.7.12 → 2026.9.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (196) hide show
  1. package/README.md +14 -0
  2. package/dist/bench/adaptation.d.ts +57 -0
  3. package/dist/bench/adaptation.d.ts.map +1 -0
  4. package/dist/bench/adaptation.js +167 -0
  5. package/dist/bench/adaptation.js.map +1 -0
  6. package/dist/bench/harness.d.ts +2 -0
  7. package/dist/bench/harness.d.ts.map +1 -1
  8. package/dist/bench/harness.js +1 -1
  9. package/dist/bench/harness.js.map +1 -1
  10. package/dist/bench/index.d.ts +5 -3
  11. package/dist/bench/index.d.ts.map +1 -1
  12. package/dist/bench/index.js +4 -2
  13. package/dist/bench/index.js.map +1 -1
  14. package/dist/bench/types.d.ts +88 -0
  15. package/dist/bench/types.d.ts.map +1 -1
  16. package/dist/evermind/adapt.d.ts +40 -0
  17. package/dist/evermind/adapt.d.ts.map +1 -0
  18. package/dist/evermind/adapt.js +25 -0
  19. package/dist/evermind/adapt.js.map +1 -0
  20. package/dist/evermind/delta_wire.d.ts +43 -0
  21. package/dist/evermind/delta_wire.d.ts.map +1 -0
  22. package/dist/evermind/delta_wire.js +94 -0
  23. package/dist/evermind/delta_wire.js.map +1 -0
  24. package/dist/evermind/eval.d.ts +31 -0
  25. package/dist/evermind/eval.d.ts.map +1 -0
  26. package/dist/evermind/eval.js +36 -0
  27. package/dist/evermind/eval.js.map +1 -0
  28. package/dist/evermind/index.d.ts +16 -0
  29. package/dist/evermind/index.d.ts.map +1 -0
  30. package/dist/evermind/index.js +11 -0
  31. package/dist/evermind/index.js.map +1 -0
  32. package/dist/evermind/merge.d.ts +21 -0
  33. package/dist/evermind/merge.d.ts.map +1 -0
  34. package/dist/evermind/merge.js +79 -0
  35. package/dist/evermind/merge.js.map +1 -0
  36. package/dist/{moe/moe_package.d.ts → evermind/package.d.ts} +55 -5
  37. package/dist/evermind/package.d.ts.map +1 -0
  38. package/dist/{moe/moe_package.js → evermind/package.js} +118 -19
  39. package/dist/evermind/package.js.map +1 -0
  40. package/dist/import/foreign/adapter.d.ts +59 -0
  41. package/dist/import/foreign/adapter.d.ts.map +1 -0
  42. package/dist/import/foreign/adapter.js +76 -0
  43. package/dist/import/foreign/adapter.js.map +1 -0
  44. package/dist/import/foreign/apply.d.ts +23 -0
  45. package/dist/import/foreign/apply.d.ts.map +1 -0
  46. package/dist/import/foreign/apply.js +43 -0
  47. package/dist/import/foreign/apply.js.map +1 -0
  48. package/dist/import/foreign/codestral_mamba.d.ts +57 -0
  49. package/dist/import/foreign/codestral_mamba.d.ts.map +1 -0
  50. package/dist/import/foreign/codestral_mamba.js +174 -0
  51. package/dist/import/foreign/codestral_mamba.js.map +1 -0
  52. package/dist/import/foreign/falcon_mamba.d.ts +51 -0
  53. package/dist/import/foreign/falcon_mamba.d.ts.map +1 -0
  54. package/dist/import/foreign/falcon_mamba.js +131 -0
  55. package/dist/import/foreign/falcon_mamba.js.map +1 -0
  56. package/dist/import/foreign/index.d.ts +21 -0
  57. package/dist/import/foreign/index.d.ts.map +1 -0
  58. package/dist/import/foreign/index.js +18 -0
  59. package/dist/import/foreign/index.js.map +1 -0
  60. package/dist/import/foreign/plan.d.ts +122 -0
  61. package/dist/import/foreign/plan.d.ts.map +1 -0
  62. package/dist/import/foreign/plan.js +171 -0
  63. package/dist/import/foreign/plan.js.map +1 -0
  64. package/dist/import/foreign/registry.d.ts +35 -0
  65. package/dist/import/foreign/registry.d.ts.map +1 -0
  66. package/dist/import/foreign/registry.js +89 -0
  67. package/dist/import/foreign/registry.js.map +1 -0
  68. package/dist/import/index.d.ts +7 -0
  69. package/dist/import/index.d.ts.map +1 -1
  70. package/dist/import/index.js +7 -0
  71. package/dist/import/index.js.map +1 -1
  72. package/dist/index.d.ts +29 -12
  73. package/dist/index.d.ts.map +1 -1
  74. package/dist/index.js +24 -9
  75. package/dist/index.js.map +1 -1
  76. package/dist/kernels/selective_scan.d.ts.map +1 -1
  77. package/dist/kernels/selective_scan.js +15 -9
  78. package/dist/kernels/selective_scan.js.map +1 -1
  79. package/dist/kernels/slice.d.ts +26 -0
  80. package/dist/kernels/slice.d.ts.map +1 -0
  81. package/dist/kernels/slice.js +52 -0
  82. package/dist/kernels/slice.js.map +1 -0
  83. package/dist/kernels/weight_update.d.ts.map +1 -1
  84. package/dist/kernels/weight_update.js +30 -4
  85. package/dist/kernels/weight_update.js.map +1 -1
  86. package/dist/lm/evermind_lm.d.ts +180 -4
  87. package/dist/lm/evermind_lm.d.ts.map +1 -1
  88. package/dist/lm/evermind_lm.js +433 -116
  89. package/dist/lm/evermind_lm.js.map +1 -1
  90. package/dist/lm/index.d.ts +8 -2
  91. package/dist/lm/index.d.ts.map +1 -1
  92. package/dist/lm/index.js +6 -1
  93. package/dist/lm/index.js.map +1 -1
  94. package/dist/lm/text_embedder.d.ts +55 -0
  95. package/dist/lm/text_embedder.d.ts.map +1 -0
  96. package/dist/lm/text_embedder.js +62 -0
  97. package/dist/lm/text_embedder.js.map +1 -0
  98. package/dist/model/attention_block.d.ts.map +1 -1
  99. package/dist/model/attention_block.js +9 -5
  100. package/dist/model/attention_block.js.map +1 -1
  101. package/dist/model/mamba1_block.d.ts.map +1 -1
  102. package/dist/model/mamba1_block.js +28 -12
  103. package/dist/model/mamba1_block.js.map +1 -1
  104. package/dist/model/mamba2_block.d.ts.map +1 -1
  105. package/dist/model/mamba2_block.js +14 -9
  106. package/dist/model/mamba2_block.js.map +1 -1
  107. package/dist/model/mamba3_block.d.ts.map +1 -1
  108. package/dist/model/mamba3_block.js +14 -9
  109. package/dist/model/mamba3_block.js.map +1 -1
  110. package/dist/moe/index.d.ts +0 -2
  111. package/dist/moe/index.d.ts.map +1 -1
  112. package/dist/moe/index.js +0 -1
  113. package/dist/moe/index.js.map +1 -1
  114. package/dist/optim/adamw.d.ts +58 -1
  115. package/dist/optim/adamw.d.ts.map +1 -1
  116. package/dist/optim/adamw.js +69 -4
  117. package/dist/optim/adamw.js.map +1 -1
  118. package/dist/tokenizer/bpe.d.ts +28 -0
  119. package/dist/tokenizer/bpe.d.ts.map +1 -1
  120. package/dist/tokenizer/bpe.js +47 -0
  121. package/dist/tokenizer/bpe.js.map +1 -1
  122. package/dist/training/cpu_ops.d.ts +44 -0
  123. package/dist/training/cpu_ops.d.ts.map +1 -0
  124. package/dist/training/cpu_ops.js +111 -0
  125. package/dist/training/cpu_ops.js.map +1 -0
  126. package/dist/training/lora.d.ts +153 -0
  127. package/dist/training/lora.d.ts.map +1 -0
  128. package/dist/training/lora.js +324 -0
  129. package/dist/training/lora.js.map +1 -0
  130. package/dist/training/mamba1_cpu.d.ts +109 -0
  131. package/dist/training/mamba1_cpu.d.ts.map +1 -0
  132. package/dist/training/mamba1_cpu.js +316 -0
  133. package/dist/training/mamba1_cpu.js.map +1 -0
  134. package/dist/training/mixed_precision.d.ts +67 -0
  135. package/dist/training/mixed_precision.d.ts.map +1 -0
  136. package/dist/training/mixed_precision.js +101 -0
  137. package/dist/training/mixed_precision.js.map +1 -0
  138. package/dist/training/model_cpu.d.ts +100 -0
  139. package/dist/training/model_cpu.d.ts.map +1 -0
  140. package/dist/training/model_cpu.js +252 -0
  141. package/dist/training/model_cpu.js.map +1 -0
  142. package/dist/training/trainer.d.ts +156 -16
  143. package/dist/training/trainer.d.ts.map +1 -1
  144. package/dist/training/trainer.js +327 -101
  145. package/dist/training/trainer.js.map +1 -1
  146. package/dist/utils/base64.d.ts +12 -0
  147. package/dist/utils/base64.d.ts.map +1 -0
  148. package/dist/utils/base64.js +27 -0
  149. package/dist/utils/base64.js.map +1 -0
  150. package/package.json +3 -1
  151. package/src/bench/adaptation.ts +180 -0
  152. package/src/bench/harness.ts +1 -1
  153. package/src/bench/index.ts +13 -2
  154. package/src/bench/types.ts +93 -0
  155. package/src/evermind/adapt.ts +61 -0
  156. package/src/evermind/delta_wire.ts +114 -0
  157. package/src/evermind/eval.ts +62 -0
  158. package/src/evermind/index.ts +33 -0
  159. package/src/evermind/merge.ts +106 -0
  160. package/src/{moe/moe_package.ts → evermind/package.ts} +148 -17
  161. package/src/import/foreign/adapter.ts +106 -0
  162. package/src/import/foreign/apply.ts +54 -0
  163. package/src/import/foreign/codestral_mamba.ts +224 -0
  164. package/src/import/foreign/falcon_mamba.ts +173 -0
  165. package/src/import/foreign/index.ts +37 -0
  166. package/src/import/foreign/plan.ts +294 -0
  167. package/src/import/foreign/registry.ts +126 -0
  168. package/src/import/index.ts +30 -0
  169. package/src/index.ts +104 -13
  170. package/src/kernels/selective_scan.ts +15 -9
  171. package/src/kernels/slice.ts +64 -0
  172. package/src/kernels/weight_update.ts +30 -4
  173. package/src/lm/evermind_lm.ts +470 -116
  174. package/src/lm/index.ts +12 -2
  175. package/src/lm/text_embedder.ts +84 -0
  176. package/src/model/attention_block.ts +9 -5
  177. package/src/model/mamba1_block.ts +28 -12
  178. package/src/model/mamba2_block.ts +14 -9
  179. package/src/model/mamba3_block.ts +14 -9
  180. package/src/moe/index.ts +0 -9
  181. package/src/optim/adamw.ts +115 -7
  182. package/src/tokenizer/bpe.ts +55 -0
  183. package/src/training/cpu_ops.ts +127 -0
  184. package/src/training/lora.ts +365 -0
  185. package/src/training/mamba1_cpu.ts +421 -0
  186. package/src/training/mixed_precision.ts +119 -0
  187. package/src/training/model_cpu.ts +360 -0
  188. package/src/training/trainer.ts +397 -154
  189. package/src/utils/base64.ts +28 -0
  190. package/dist/model/mamba_block.d.ts +0 -64
  191. package/dist/model/mamba_block.d.ts.map +0 -1
  192. package/dist/model/mamba_block.js +0 -303
  193. package/dist/model/mamba_block.js.map +0 -1
  194. package/dist/moe/moe_package.d.ts.map +0 -1
  195. package/dist/moe/moe_package.js.map +0 -1
  196. package/src/model/mamba_block.ts +0 -401
@@ -24,6 +24,7 @@
24
24
  import { SharedExpertMoE } from "../moe/moe_model.js";
25
25
  import { crossEntropyLoss, crossEntropyGrad } from "../training/autograd.js";
26
26
  import { AdamW, type AdamWOptions } from "../optim/adamw.js";
27
+ import { DynamicLossScaler, roundFp16, type LossScalerOptions } from "../training/mixed_precision.js";
27
28
  import { SeededRng } from "../utils/rng.js";
28
29
  import { quantizeFp16, dequantizeFp16 } from "../utils/quantization.js";
29
30
  import { appendCrcTrailer, verifyCrcTrailer } from "../utils/crc32.js";
@@ -101,9 +102,37 @@ export interface LMGenerateOptions {
101
102
  stopToken?: number;
102
103
  }
103
104
 
105
+ /**
106
+ * Incremental decode state — everything a continuation needs from the prefix.
107
+ *
108
+ * The model's only cross-position dependency is the depthwise causal conv, whose
109
+ * receptive field is `convKernel` positions, so the entire "KV cache" is the
110
+ * trailing `convKernel - 1` normalised conv inputs per layer. That makes an exact
111
+ * prefix cache cheap: process a prompt once, then extend it with each candidate
112
+ * continuation for the cost of the candidate alone.
113
+ */
114
+ export interface EvermindLMDecodeState {
115
+ /** [layer][slot] the trailing `convKernel - 1` RMSNormed conv inputs, oldest first. */
116
+ convWindow: Float32Array[][];
117
+ /** Positions consumed so far. */
118
+ length: number;
119
+ /** Logits at the LAST consumed position — what predicts the next token. */
120
+ lastLogits: Float32Array | null;
121
+ }
122
+
104
123
  export class EvermindLM {
105
124
  readonly config: Required<Omit<EvermindLMConfig, "seed">>;
106
125
 
126
+ /**
127
+ * Token-positions pushed through a block since the last {@link resetStats}.
128
+ *
129
+ * The cost metric that matters for the tool-calling path: scoring N candidate
130
+ * tool names used to replay the WHOLE prompt N times, so this counted
131
+ * N x (prompt + candidate) x layers. With {@link stepDecode} the prompt is paid
132
+ * for once. Exposed so a benchmark can assert the saving rather than assume it.
133
+ */
134
+ private _positionsEvaluated = 0;
135
+
107
136
  /** Tied token embedding / output head: vocabSize × dModel (row-major). */
108
137
  emb: Float32Array;
109
138
  private gEmb: Float32Array;
@@ -181,68 +210,104 @@ export class EvermindLM {
181
210
 
182
211
  // ── Forward ────────────────────────────────────────────────────────────────
183
212
 
184
- /** Run the model over a token sequence; returns per-position logits + a cache. */
185
- forward(tokens: number[]): { logits: Float32Array[]; cache: ForwardCache } {
186
- const { dModel, convKernel, numLayers, vocabSize } = this.config;
187
- const T = tokens.length;
188
-
189
- // Embed.
190
- let x: Float32Array[] = tokens.map((tok) => {
213
+ /** Embed a token sequence into per-position channel vectors. */
214
+ private _embed(tokens: number[]): Float32Array[] {
215
+ const { dModel } = this.config;
216
+ return tokens.map((tok) => {
191
217
  const row = new Float32Array(dModel);
192
218
  const off = tok * dModel;
193
219
  for (let c = 0; c < dModel; c++) row[c] = this.emb[off + c]!;
194
220
  return row;
195
221
  });
222
+ }
196
223
 
197
- const layers: LayerCache[] = [];
198
- for (let l = 0; l < numLayers; l++) {
199
- const layerIn = x;
200
- const ker = this.conv[l]!;
201
- const nConv = this.nConv[l]!;
202
- const nMoe = this.nMoe[l]!;
203
-
204
- // Pre-norm → depthwise causal conv → residual.
205
- const normedConv: Float32Array[] = [];
206
- const rmsConv: number[] = [];
207
- for (let t = 0; t < T; t++) {
208
- const { y, r } = rmsNorm(layerIn[t]!, nConv);
209
- normedConv.push(y);
210
- rmsConv.push(r);
211
- }
212
- const afterConv: Float32Array[] = [];
213
- for (let t = 0; t < T; t++) {
214
- const out = Float32Array.from(layerIn[t]!); // residual base
215
- for (let c = 0; c < dModel; c++) {
216
- let acc = 0;
217
- for (let j = 0; j < convKernel; j++) {
218
- const ti = t - j;
219
- if (ti >= 0) acc += ker[c * convKernel + j]! * normedConv[ti]![c]!;
220
- }
221
- out[c] = out[c]! + acc;
222
- }
223
- afterConv.push(out);
224
- }
224
+ /**
225
+ * One (conv + MoE) block: pre-norm → depthwise causal conv → residual, then
226
+ * pre-norm → MoE channel mixer → residual. Returns the block output and the
227
+ * activation cache its backward needs. Isolating this is what lets
228
+ * {@link lossAndBackwardCheckpointed} recompute a layer's activations on demand
229
+ * instead of retaining every layer's cache at once.
230
+ */
231
+ private _forwardLayer(l: number, layerIn: Float32Array[]): { afterMoe: Float32Array[]; cache: LayerCache } {
232
+ const T = layerIn.length;
233
+ const window: Float32Array[] = [];
225
234
 
226
- // Pre-norm → MoE channel mixer → residual.
227
- const rmsMoe: number[] = [];
228
- const moeCache: MoECacheLike[] = [];
229
- const afterMoe: Float32Array[] = [];
230
- for (let t = 0; t < T; t++) {
231
- const { y, r } = rmsNorm(afterConv[t]!, nMoe);
232
- rmsMoe.push(r);
233
- const out = Float32Array.from(afterConv[t]!); // residual base
234
- const mr = this.moe[l]!.forward(y);
235
- for (let c = 0; c < dModel; c++) out[c] = out[c]! + mr.output[c]!;
236
- afterMoe.push(out);
237
- moeCache.push(mr.cache as unknown as MoECacheLike);
238
- }
235
+ const normedConv: Float32Array[] = [];
236
+ const rmsConv: number[] = [];
237
+ const afterConv: Float32Array[] = [];
238
+ const rmsMoe: number[] = [];
239
+ const moeCache: MoECacheLike[] = [];
240
+ const afterMoe: Float32Array[] = [];
239
241
 
240
- layers.push({ layerIn, normedConv, rmsConv, afterConv, rmsMoe, moeCache });
241
- x = afterMoe;
242
+ for (let t = 0; t < T; t++) {
243
+ const st = this._stepLayerPosition(l, layerIn[t]!, window);
244
+ normedConv.push(st.normed);
245
+ rmsConv.push(st.rmsConv);
246
+ afterConv.push(st.afterConv);
247
+ rmsMoe.push(st.rmsMoe);
248
+ moeCache.push(st.moeCache);
249
+ afterMoe.push(st.out);
242
250
  }
251
+ return { afterMoe, cache: { layerIn, normedConv, rmsConv, afterConv, rmsMoe, moeCache } };
252
+ }
243
253
 
244
- // Tied head: logits_t[v] = x_t · emb[v].
245
- const logits: Float32Array[] = x.map((xt) => {
254
+ /**
255
+ * ONE position through one (conv + MoE) block, given the trailing conv window.
256
+ *
257
+ * The single place the block's maths lives: the batch path
258
+ * ({@link _forwardLayer}, which needs the full activation cache for backward)
259
+ * and the incremental path ({@link stepDecode}, which needs none of it) both
260
+ * run this, so they cannot drift apart. `window` holds the previous
261
+ * `convKernel - 1` RMSNormed conv inputs, OLDEST FIRST, and is advanced in
262
+ * place — a shorter window is exactly the causal zero-padding at the start of
263
+ * a sequence.
264
+ */
265
+ private _stepLayerPosition(
266
+ l: number,
267
+ xt: Float32Array,
268
+ window: Float32Array[],
269
+ ): { normed: Float32Array; rmsConv: number; afterConv: Float32Array; rmsMoe: number; moeCache: MoECacheLike; out: Float32Array } {
270
+ const { dModel, convKernel } = this.config;
271
+ const ker = this.conv[l]!;
272
+
273
+ const { y: normed, r: rConv } = rmsNorm(xt, this.nConv[l]!);
274
+ const afterConv = Float32Array.from(xt); // residual base
275
+ for (let c = 0; c < dModel; c++) {
276
+ let acc = ker[c * convKernel]! * normed[c]!;
277
+ for (let j = 1; j < convKernel; j++) {
278
+ const prev = window[window.length - j]; // position t - j, or undefined before the start
279
+ if (prev) acc += ker[c * convKernel + j]! * prev[c]!;
280
+ }
281
+ afterConv[c] = afterConv[c]! + acc;
282
+ }
283
+ window.push(normed);
284
+ while (window.length > convKernel - 1) window.shift();
285
+
286
+ const { y: normedMoe, r: rMoe } = rmsNorm(afterConv, this.nMoe[l]!);
287
+ const out = Float32Array.from(afterConv); // residual base
288
+ const mr = this.moe[l]!.forward(normedMoe);
289
+ for (let c = 0; c < dModel; c++) out[c] = out[c]! + mr.output[c]!;
290
+
291
+ this._positionsEvaluated++;
292
+ return {
293
+ normed, rmsConv: rConv, afterConv, rmsMoe: rMoe,
294
+ moeCache: mr.cache as unknown as MoECacheLike, out,
295
+ };
296
+ }
297
+
298
+ /** One token's embedding row (a copy — callers write into it). */
299
+ private _embedOne(tok: number): Float32Array {
300
+ const { dModel } = this.config;
301
+ const row = new Float32Array(dModel);
302
+ const off = tok * dModel;
303
+ for (let c = 0; c < dModel; c++) row[c] = this.emb[off + c]!;
304
+ return row;
305
+ }
306
+
307
+ /** Tied output head: logits_t[v] = x_t · emb[v]. */
308
+ private _head(x: Float32Array[]): Float32Array[] {
309
+ const { dModel, vocabSize } = this.config;
310
+ return x.map((xt) => {
246
311
  const lg = new Float32Array(vocabSize);
247
312
  for (let v = 0; v < vocabSize; v++) {
248
313
  let acc = 0;
@@ -252,34 +317,94 @@ export class EvermindLM {
252
317
  }
253
318
  return lg;
254
319
  });
320
+ }
255
321
 
256
- return { logits, cache: { tokens, layers, finalX: x } };
322
+ /** Run the model over a token sequence; returns per-position logits + a cache. */
323
+ forward(tokens: number[]): { logits: Float32Array[]; cache: ForwardCache } {
324
+ let x = this._embed(tokens);
325
+ const layers: LayerCache[] = [];
326
+ for (let l = 0; l < this.config.numLayers; l++) {
327
+ const { afterMoe, cache } = this._forwardLayer(l, x);
328
+ layers.push(cache);
329
+ x = afterMoe;
330
+ }
331
+ return { logits: this._head(x), cache: { tokens, layers, finalX: x } };
332
+ }
333
+
334
+ /**
335
+ * Produces a single fixed-length embedding vector for a token sequence — the
336
+ * CPU counterpart of `HybridMambaModel.embed()`, to the same contract.
337
+ *
338
+ * Runs the full block stack and takes the final hidden state — i.e. exactly the
339
+ * state the tied LM head consumes in {@link forward} — then mean-pools across
340
+ * sequence positions and L2-normalises. The result has length `dModel`, so
341
+ * cosine similarity between two embeddings reduces to a dot product.
342
+ *
343
+ * Like the GPU path it skips the (expensive) head projection: it only needs the
344
+ * `dModel`-wide hidden state, not `vocabSize` logits. Unlike the GPU path it
345
+ * needs no WebGPU device at all, which is what lets a headless host (the stdio
346
+ * MCP server) rank recall by SSM embedding instead of word overlap.
347
+ *
348
+ * Streamed one position at a time through the SHARED block implementation
349
+ * ({@link _stepLayerPosition}, the same routine {@link forward} and
350
+ * {@link stepDecode} run), so it can never drift from them and it retains no
351
+ * per-position activation cache.
352
+ *
353
+ * The embedding reflects whatever the model currently knows — an untrained
354
+ * model behaves like a random projection of the token embeddings (still
355
+ * lexically discriminative), and the representation sharpens as the model is
356
+ * adapted/distilled.
357
+ */
358
+ embed(tokenIds: number[] | Uint32Array): Float32Array {
359
+ const { dModel, numLayers } = this.config;
360
+ const out = new Float32Array(dModel);
361
+ const seqLen = tokenIds.length;
362
+ if (seqLen === 0) return out;
363
+
364
+ // One rolling conv window per layer — a shorter window at the start of the
365
+ // sequence IS the causal zero-padding, exactly as in stepDecode.
366
+ const windows: Float32Array[][] = Array.from({ length: numLayers }, () => [] as Float32Array[]);
367
+ for (let t = 0; t < seqLen; t++) {
368
+ let x = this._embedOne(tokenIds[t]!);
369
+ for (let l = 0; l < numLayers; l++) x = this._stepLayerPosition(l, x, windows[l]!).out;
370
+ for (let c = 0; c < dModel; c++) out[c] = out[c]! + x[c]!;
371
+ }
372
+
373
+ // Mean-pool across positions.
374
+ for (let c = 0; c < dModel; c++) out[c] = out[c]! / seqLen;
375
+
376
+ // L2-normalise so cosine similarity reduces to a dot product.
377
+ let norm = 0;
378
+ for (let c = 0; c < dModel; c++) norm += out[c]! * out[c]!;
379
+ norm = Math.sqrt(norm) || 1;
380
+ for (let c = 0; c < dModel; c++) out[c] = out[c]! / norm;
381
+ return out;
382
+ }
383
+
384
+ /** Text-level embedding: encode with `codec`, then {@link embed}. */
385
+ embedText(text: string, codec: TextCodec): Float32Array {
386
+ return this.embed(codec.encode(text));
257
387
  }
258
388
 
259
389
  // ── Loss + backward ──────────────────────────────────────────────────────────
260
390
 
261
391
  /**
262
- * Next-token cross-entropy over the sequence (predict tokens[t+1] from
263
- * position t), accumulating exact gradients. Returns the mean loss. Call
264
- * {@link zeroGrad} before and an optimiser step after.
392
+ * Head + tied-embedding gradient. Accumulates dL/d(head→emb) into gEmb and
393
+ * returns the mean next-token loss plus dL/d(finalX). Shared by the full and
394
+ * checkpointed backward paths so the head maths lives in one place.
265
395
  */
266
- lossAndBackward(tokens: number[]): number {
267
- const { dModel, convKernel, numLayers, vocabSize } = this.config;
396
+ private _headBackward(tokens: number[], logits: Float32Array[], finalX: Float32Array[]): { loss: number; dX: Float32Array[] } {
397
+ const { dModel, vocabSize } = this.config;
268
398
  const T = tokens.length;
269
- if (T < 2) return 0;
270
- const { logits, cache } = this.forward(tokens);
271
-
272
399
  const predPositions = T - 1; // positions 0..T-2 predict the next token
273
400
  const inv = 1 / predPositions;
274
-
275
- // dL/d(finalX_t) and head gradient into the tied embedding.
276
401
  const dX: Float32Array[] = Array.from({ length: T }, () => new Float32Array(dModel));
277
402
  let loss = 0;
278
403
  for (let t = 0; t < predPositions; t++) {
279
404
  const target = tokens[t + 1]!;
280
405
  loss += crossEntropyLoss(logits[t]!, target) * inv;
281
406
  const dLogit = crossEntropyGrad(logits[t]!, target); // probs - onehot
282
- const xt = cache.finalX[t]!;
407
+ const xt = finalX[t]!;
283
408
  for (let v = 0; v < vocabSize; v++) {
284
409
  const g = dLogit[v]! * inv;
285
410
  if (g === 0) continue;
@@ -290,62 +415,186 @@ export class EvermindLM {
290
415
  }
291
416
  }
292
417
  }
418
+ return { loss, dX };
419
+ }
293
420
 
294
- // Backprop through layers in reverse.
295
- for (let l = numLayers - 1; l >= 0; l--) {
296
- const lc = cache.layers[l]!;
297
- const ker = this.conv[l]!;
298
- const gker = this.gConv[l]!;
299
- const nConv = this.nConv[l]!;
300
- const gNConv = this.gNConv[l]!;
301
- const nMoe = this.nMoe[l]!;
302
- const gNMoe = this.gNMoe[l]!;
303
-
304
- // MoE sub-block: afterMoe = afterConv + MoE(RMSNorm(afterConv, nMoe)).
305
- const dAfterConv: Float32Array[] = [];
306
- for (let t = 0; t < T; t++) {
307
- const dMoeNormed = this.moe[l]!.backward(dX[t]!, lc.moeCache[t] as never);
308
- const { dx, dgain } = rmsNormBackward(dMoeNormed, lc.afterConv[t]!, lc.rmsMoe[t]!, nMoe);
309
- for (let c = 0; c < dModel; c++) gNMoe[c] = gNMoe[c]! + dgain[c]!;
310
- const d = Float32Array.from(dX[t]!); // residual passthrough
311
- for (let c = 0; c < dModel; c++) d[c] = d[c]! + dx[c]!;
312
- dAfterConv.push(d);
313
- }
421
+ /**
422
+ * Backward through one (conv + MoE) block given dL/d(block output) and the
423
+ * block's activation cache. Accumulates conv/norm/MoE gradients and returns
424
+ * dL/d(block input). The inverse of {@link _forwardLayer}.
425
+ */
426
+ private _backwardLayer(l: number, dOut: Float32Array[], lc: LayerCache): Float32Array[] {
427
+ const { dModel, convKernel } = this.config;
428
+ const T = dOut.length;
429
+ const ker = this.conv[l]!;
430
+ const gker = this.gConv[l]!;
431
+ const nConv = this.nConv[l]!;
432
+ const gNConv = this.gNConv[l]!;
433
+ const nMoe = this.nMoe[l]!;
434
+ const gNMoe = this.gNMoe[l]!;
435
+
436
+ // MoE sub-block: afterMoe = afterConv + MoE(RMSNorm(afterConv, nMoe)).
437
+ const dAfterConv: Float32Array[] = [];
438
+ for (let t = 0; t < T; t++) {
439
+ const dMoeNormed = this.moe[l]!.backward(dOut[t]!, lc.moeCache[t] as never);
440
+ const { dx, dgain } = rmsNormBackward(dMoeNormed, lc.afterConv[t]!, lc.rmsMoe[t]!, nMoe);
441
+ for (let c = 0; c < dModel; c++) gNMoe[c] = gNMoe[c]! + dgain[c]!;
442
+ const d = Float32Array.from(dOut[t]!); // residual passthrough
443
+ for (let c = 0; c < dModel; c++) d[c] = d[c]! + dx[c]!;
444
+ dAfterConv.push(d);
445
+ }
314
446
 
315
- // Conv sub-block: afterConv = layerIn + conv(RMSNorm(layerIn, nConv)).
316
- const dNormedConv: Float32Array[] = Array.from({ length: T }, () => new Float32Array(dModel));
317
- const dLayerIn: Float32Array[] = dAfterConv.map((v) => Float32Array.from(v)); // residual passthrough
318
- for (let t = 0; t < T; t++) {
319
- for (let c = 0; c < dModel; c++) {
320
- const dmix = dAfterConv[t]![c]!;
321
- if (dmix === 0) continue;
322
- for (let j = 0; j < convKernel; j++) {
323
- const ti = t - j;
324
- if (ti < 0) continue;
325
- gker[c * convKernel + j] = gker[c * convKernel + j]! + dmix * lc.normedConv[ti]![c]!;
326
- dNormedConv[ti]![c] = dNormedConv[ti]![c]! + dmix * ker[c * convKernel + j]!;
327
- }
447
+ // Conv sub-block: afterConv = layerIn + conv(RMSNorm(layerIn, nConv)).
448
+ const dNormedConv: Float32Array[] = Array.from({ length: T }, () => new Float32Array(dModel));
449
+ const dLayerIn: Float32Array[] = dAfterConv.map((v) => Float32Array.from(v)); // residual passthrough
450
+ for (let t = 0; t < T; t++) {
451
+ for (let c = 0; c < dModel; c++) {
452
+ const dmix = dAfterConv[t]![c]!;
453
+ if (dmix === 0) continue;
454
+ for (let j = 0; j < convKernel; j++) {
455
+ const ti = t - j;
456
+ if (ti < 0) continue;
457
+ gker[c * convKernel + j] = gker[c * convKernel + j]! + dmix * lc.normedConv[ti]![c]!;
458
+ dNormedConv[ti]![c] = dNormedConv[ti]![c]! + dmix * ker[c * convKernel + j]!;
328
459
  }
329
460
  }
330
- for (let t = 0; t < T; t++) {
331
- const { dx, dgain } = rmsNormBackward(dNormedConv[t]!, lc.layerIn[t]!, lc.rmsConv[t]!, nConv);
332
- for (let c = 0; c < dModel; c++) {
333
- gNConv[c] = gNConv[c]! + dgain[c]!;
334
- dLayerIn[t]![c] = dLayerIn[t]![c]! + dx[c]!;
335
- }
461
+ }
462
+ for (let t = 0; t < T; t++) {
463
+ const { dx, dgain } = rmsNormBackward(dNormedConv[t]!, lc.layerIn[t]!, lc.rmsConv[t]!, nConv);
464
+ for (let c = 0; c < dModel; c++) {
465
+ gNConv[c] = gNConv[c]! + dgain[c]!;
466
+ dLayerIn[t]![c] = dLayerIn[t]![c]! + dx[c]!;
336
467
  }
337
- for (let t = 0; t < T; t++) dX[t] = dLayerIn[t]!;
338
468
  }
469
+ return dLayerIn;
470
+ }
339
471
 
340
- // Embedding lookup: dX at layer-0 input flows into the row for token_t.
341
- for (let t = 0; t < T; t++) {
472
+ /** Embedding lookup: dL/d(layer-0 input) flows into the row for each token. */
473
+ private _embedBackward(tokens: number[], dIn: Float32Array[]): void {
474
+ const { dModel } = this.config;
475
+ for (let t = 0; t < tokens.length; t++) {
342
476
  const off = tokens[t]! * dModel;
343
- for (let c = 0; c < dModel; c++) this.gEmb[off + c] = this.gEmb[off + c]! + dX[t]![c]!;
477
+ for (let c = 0; c < dModel; c++) this.gEmb[off + c] = this.gEmb[off + c]! + dIn[t]![c]!;
344
478
  }
479
+ }
480
+
481
+ /**
482
+ * Next-token cross-entropy over the sequence (predict tokens[t+1] from
483
+ * position t), accumulating exact gradients. Returns the mean loss. Call
484
+ * {@link zeroGrad} before and an optimiser step after.
485
+ */
486
+ lossAndBackward(tokens: number[]): number {
487
+ if (tokens.length < 2) return 0;
488
+ const { logits, cache } = this.forward(tokens);
489
+ const { loss, dX } = this._headBackward(tokens, logits, cache.finalX);
490
+ let d = dX;
491
+ for (let l = this.config.numLayers - 1; l >= 0; l--) d = this._backwardLayer(l, d, cache.layers[l]!);
492
+ this._embedBackward(tokens, d);
493
+ return loss;
494
+ }
345
495
 
496
+ /**
497
+ * Activation-checkpointed backward — numerically identical gradients to
498
+ * {@link lossAndBackward}, but retains only the per-LAYER inputs during the
499
+ * forward instead of every layer's full activation cache. Each layer's
500
+ * activations are RECOMPUTED (a cheap extra forward) when its backward runs,
501
+ * so peak activation memory is one layer's cache, not all of them. This is the
502
+ * memory-for-compute trade the cookbook pairs with FSDP to fit longer
503
+ * sequences / bigger models on a constrained device.
504
+ */
505
+ lossAndBackwardCheckpointed(tokens: number[]): number {
506
+ if (tokens.length < 2) return 0;
507
+ let x = this._embed(tokens);
508
+ const layerInputs: Float32Array[][] = [];
509
+ for (let l = 0; l < this.config.numLayers; l++) {
510
+ layerInputs.push(x); // keep only the input; drop the cache
511
+ x = this._forwardLayer(l, x).afterMoe;
512
+ }
513
+ const logits = this._head(x);
514
+ const { loss, dX } = this._headBackward(tokens, logits, x);
515
+ let d = dX;
516
+ for (let l = this.config.numLayers - 1; l >= 0; l--) {
517
+ const { cache } = this._forwardLayer(l, layerInputs[l]!); // recompute this layer's activations
518
+ d = this._backwardLayer(l, d, cache);
519
+ }
520
+ this._embedBackward(tokens, d);
346
521
  return loss;
347
522
  }
348
523
 
524
+ // ── Incremental decoding (prefix cache) ──────────────────────────────────────
525
+
526
+ /** Positions pushed through a block since the last {@link resetStats}. */
527
+ get positionsEvaluated(): number { return this._positionsEvaluated; }
528
+ resetStats(): void { this._positionsEvaluated = 0; }
529
+
530
+ /** An empty decode state — the start of a sequence. */
531
+ newDecodeState(): EvermindLMDecodeState {
532
+ return {
533
+ convWindow: Array.from({ length: this.config.numLayers }, () => [] as Float32Array[]),
534
+ length: 0,
535
+ lastLogits: null,
536
+ };
537
+ }
538
+
539
+ /** Deep copy, so one prefix state can seed many independent continuations. */
540
+ cloneDecodeState(state: EvermindLMDecodeState): EvermindLMDecodeState {
541
+ return {
542
+ convWindow: state.convWindow.map((w) => w.map((v) => Float32Array.from(v))),
543
+ length: state.length,
544
+ lastLogits: state.lastLogits ? Float32Array.from(state.lastLogits) : null,
545
+ };
546
+ }
547
+
548
+ /**
549
+ * Consume `tokens` from `state`, returning the per-position logits and the
550
+ * ADVANCED state. `state` is not mutated — pass {@link cloneDecodeState} output
551
+ * or a fresh state; this clones internally so the caller's prefix stays reusable.
552
+ *
553
+ * Numerically identical to {@link forward} over the concatenated sequence: the
554
+ * shared {@link _stepLayerPosition} is the only implementation of the block.
555
+ */
556
+ stepDecode(state: EvermindLMDecodeState, tokens: number[]): { logits: Float32Array[]; state: EvermindLMDecodeState } {
557
+ const next = this.cloneDecodeState(state);
558
+ const logits: Float32Array[] = [];
559
+ for (const tok of tokens) {
560
+ let x = this._embedOne(tok);
561
+ for (let l = 0; l < this.config.numLayers; l++) {
562
+ x = this._stepLayerPosition(l, x, next.convWindow[l]!).out;
563
+ }
564
+ const lg = this._head([x])[0]!;
565
+ logits.push(lg);
566
+ next.lastLogits = lg;
567
+ next.length++;
568
+ }
569
+ return { logits, state: next };
570
+ }
571
+
572
+ /** Run a prompt once and keep the state, so continuations are cheap. */
573
+ prefixState(tokens: number[]): EvermindLMDecodeState {
574
+ return this.stepDecode(this.newDecodeState(), tokens).state;
575
+ }
576
+
577
+ /**
578
+ * Mean log-probability the model assigns to `continuation` following a cached
579
+ * prefix — the tool-decision vote, without replaying the prompt.
580
+ *
581
+ * Returns `-Infinity` for an empty continuation, and requires a prefix with at
582
+ * least one position (there must be a position to predict the first
583
+ * continuation token FROM).
584
+ */
585
+ scoreContinuation(prefix: EvermindLMDecodeState, continuation: number[]): number {
586
+ if (continuation.length === 0) return -Infinity;
587
+ if (!prefix.lastLogits) return -Infinity;
588
+ // The last prefix position predicts continuation[0]; each continuation
589
+ // position then predicts the next one, so the final one is never scored.
590
+ const { logits } = this.stepDecode(prefix, continuation.slice(0, -1));
591
+ let total = logProbOfToken(prefix.lastLogits, continuation[0]!);
592
+ for (let i = 1; i < continuation.length; i++) {
593
+ total += logProbOfToken(logits[i - 1]!, continuation[i]!);
594
+ }
595
+ return total / continuation.length;
596
+ }
597
+
349
598
  // ── Generation ───────────────────────────────────────────────────────────────
350
599
 
351
600
  /**
@@ -358,19 +607,24 @@ export class EvermindLM {
358
607
  return codec.decode(this.generate(codec.encode(prompt), opts));
359
608
  }
360
609
 
361
- /** Greedy / temperature-sampled autoregressive generation. Returns NEW token ids. */
610
+ /**
611
+ * Greedy / temperature-sampled autoregressive generation. Returns NEW token ids.
612
+ *
613
+ * Carries an incremental decode state, so the prompt is processed ONCE and each
614
+ * new token costs one position. The previous implementation re-ran the whole
615
+ * sequence per token, making generation quadratic in the prompt length.
616
+ */
362
617
  generate(prompt: number[], opts: LMGenerateOptions): number[] {
363
618
  const temperature = opts.temperature ?? 0;
364
619
  const rng = temperature > 0 ? new SeededRng((opts.seed ?? 1) >>> 0 || 1) : null;
365
- const tokens = [...prompt];
620
+ let state = this.stepDecode(this.newDecodeState(), prompt.length > 0 ? prompt : [0]).state;
366
621
  const produced: number[] = [];
367
622
  for (let n = 0; n < opts.maxNewTokens; n++) {
368
- const { logits } = this.forward(tokens.length > 0 ? tokens : [0]);
369
- const last = logits[logits.length - 1]!;
623
+ const last = state.lastLogits!;
370
624
  const next = rng ? sampleTemperature(last, temperature, rng) : argmax(last);
371
625
  produced.push(next);
372
- tokens.push(next);
373
626
  if (opts.stopToken !== undefined && next === opts.stopToken) break;
627
+ state = this.stepDecode(state, [next]).state;
374
628
  }
375
629
  return produced;
376
630
  }
@@ -513,29 +767,114 @@ export class EvermindLM {
513
767
  }
514
768
  }
515
769
 
770
+ export interface EvermindLMTrainOptions extends AdamWOptions {
771
+ epochs?: number;
772
+ /**
773
+ * Gradient accumulation: average gradients over this many sequences (micro-
774
+ * batches) before each optimiser step, for a larger effective batch on a
775
+ * memory-constrained device. Default 1.
776
+ */
777
+ accumSteps?: number;
778
+ /**
779
+ * Use activation checkpointing (recompute layer activations in backward) to
780
+ * cap peak activation memory. Identical gradients, a little extra compute.
781
+ * Default false.
782
+ */
783
+ checkpoint?: boolean;
784
+ /**
785
+ * Mixed-precision training: fp16-rounded gradients with dynamic loss scaling
786
+ * over fp32 master weights (the model params). Overflowing steps are skipped
787
+ * and the scale backs off. Default false.
788
+ */
789
+ mixedPrecision?: boolean | LossScalerOptions;
790
+ }
791
+
516
792
  /** Minimal sequence trainer: AdamW over next-token cross-entropy. */
517
793
  export class EvermindLMTrainer {
518
794
  private readonly adam: AdamW;
795
+ private readonly scaler: DynamicLossScaler | null;
796
+ private readonly grads: { data: Float32Array }[];
519
797
  constructor(
520
798
  private readonly model: EvermindLM,
521
- private readonly opts: AdamWOptions & { epochs?: number } = {},
799
+ private readonly opts: EvermindLMTrainOptions = {},
522
800
  ) {
523
801
  this.adam = new AdamW(model, opts);
802
+ this.grads = model.gradients();
803
+ this.scaler = opts.mixedPrecision
804
+ ? new DynamicLossScaler(typeof opts.mixedPrecision === "object" ? opts.mixedPrecision : {})
805
+ : null;
806
+ }
807
+
808
+ /** The dynamic loss scaler (mixed-precision mode only) — exposes scale/overflow stats. */
809
+ get lossScaler(): DynamicLossScaler | null {
810
+ return this.scaler;
811
+ }
812
+
813
+ private _backward(seq: number[]): number {
814
+ return this.opts.checkpoint ? this.model.lossAndBackwardCheckpointed(seq) : this.model.lossAndBackward(seq);
815
+ }
816
+
817
+ /** Divide accumulated gradients by `n` (accumulation averaging), in place. */
818
+ private _scaleGrads(n: number): void {
819
+ if (n === 1) return;
820
+ const inv = 1 / n;
821
+ for (const g of this.grads) for (let i = 0; i < g.data.length; i++) g.data[i] = g.data[i]! * inv;
822
+ }
823
+
824
+ /** Round accumulated gradients to fp16 precision (mixed-precision simulation), in place. */
825
+ private _fp16Grads(): void {
826
+ for (const g of this.grads) for (let i = 0; i < g.data.length; i++) g.data[i] = roundFp16(g.data[i]!);
524
827
  }
828
+
829
+ /** Multiply accumulated gradients by `s`, in place. */
830
+ private _mulGrads(s: number): void {
831
+ if (s === 1) return;
832
+ for (const g of this.grads) for (let i = 0; i < g.data.length; i++) g.data[i] = g.data[i]! * s;
833
+ }
834
+
525
835
  /** Train on a set of token sequences; returns per-epoch mean loss. */
526
836
  fit(sequences: number[][]): number[] {
527
837
  const epochs = this.opts.epochs ?? 1;
838
+ const accum = Math.max(1, this.opts.accumSteps ?? 1);
528
839
  const history: number[] = [];
529
840
  for (let e = 0; e < epochs; e++) {
530
841
  let total = 0;
531
842
  let n = 0;
843
+ let pending = 0;
844
+ // The scale is fixed for the duration of one accumulation window, so scale-up
845
+ // (per sequence) and unscale (at flush) always use the SAME factor even as the
846
+ // controller grows/backs it off between windows.
847
+ let windowScale = this.scaler ? this.scaler.scale : 1;
848
+ this.model.zeroGrad();
849
+ const flush = () => {
850
+ if (pending === 0) return;
851
+ this._scaleGrads(pending); // accumulation averaging
852
+ if (this.scaler) {
853
+ this._fp16Grads(); // half-precision gradients
854
+ const overflow = this.scaler.check(this.grads);
855
+ if (this.scaler.update(overflow)) {
856
+ this._mulGrads(1 / windowScale); // unscale by the SAME factor we scaled by
857
+ this.adam.step();
858
+ } // else: skip the step, scale has backed off
859
+ windowScale = this.scaler.scale; // next window uses the updated scale
860
+ } else {
861
+ this.adam.step();
862
+ }
863
+ this.model.zeroGrad();
864
+ pending = 0;
865
+ };
532
866
  for (const seq of sequences) {
533
867
  if (seq.length < 2) continue;
534
- this.model.zeroGrad();
535
- total += this.model.lossAndBackward(seq);
536
- this.adam.step();
868
+ if (pending === 0 && this.scaler) windowScale = this.scaler.scale;
869
+ const loss = this._backward(seq);
870
+ // Emulate scaling the loss before backward: the gradient is linear in the
871
+ // loss, so scaling the just-produced gradients by the loss scale is equivalent.
872
+ this._mulGrads(windowScale);
873
+ total += loss;
537
874
  n++;
875
+ if (++pending >= accum) flush();
538
876
  }
877
+ flush();
539
878
  history.push(n > 0 ? total / n : 0);
540
879
  }
541
880
  return history;
@@ -601,3 +940,18 @@ function sampleTemperature(logits: Float32Array, temperature: number, rng: Seede
601
940
  }
602
941
  return probs.length - 1;
603
942
  }
943
+
944
+ /**
945
+ * Log-probability the model assigned to `id` at a position, from that position's
946
+ * raw logits. Log-softmax computed max-shifted so a long-tail logit cannot
947
+ * overflow `exp`.
948
+ */
949
+ export function logProbOfToken(row: Float32Array, id: number): number {
950
+ let max = -Infinity;
951
+ for (let i = 0; i < row.length; i++) { const v = row[i]!; if (v > max) max = v; }
952
+ if (!Number.isFinite(max)) return -Infinity;
953
+ let sum = 0;
954
+ for (let i = 0; i < row.length; i++) sum += Math.exp(row[i]! - max);
955
+ const logit = id >= 0 && id < row.length ? row[id]! : -Infinity;
956
+ return logit - (max + Math.log(sum));
957
+ }