@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
@@ -23,6 +23,7 @@
23
23
  import { SharedExpertMoE } from "../moe/moe_model.js";
24
24
  import { crossEntropyLoss, crossEntropyGrad } from "../training/autograd.js";
25
25
  import { AdamW } from "../optim/adamw.js";
26
+ import { DynamicLossScaler, roundFp16 } from "../training/mixed_precision.js";
26
27
  import { SeededRng } from "../utils/rng.js";
27
28
  import { quantizeFp16, dequantizeFp16 } from "../utils/quantization.js";
28
29
  import { appendCrcTrailer, verifyCrcTrailer } from "../utils/crc32.js";
@@ -39,6 +40,15 @@ export const DEFAULT_LM_SEED = 0x45564c4d; // "EVLM"
39
40
  const MAGIC = 0x45564c30; // "EVL0"
40
41
  export class EvermindLM {
41
42
  config;
43
+ /**
44
+ * Token-positions pushed through a block since the last {@link resetStats}.
45
+ *
46
+ * The cost metric that matters for the tool-calling path: scoring N candidate
47
+ * tool names used to replay the WHOLE prompt N times, so this counted
48
+ * N x (prompt + candidate) x layers. With {@link stepDecode} the prompt is paid
49
+ * for once. Exposed so a benchmark can assert the saving rather than assume it.
50
+ */
51
+ _positionsEvaluated = 0;
42
52
  /** Tied token embedding / output head: vocabSize × dModel (row-major). */
43
53
  emb;
44
54
  gEmb;
@@ -112,65 +122,96 @@ export class EvermindLM {
112
122
  }
113
123
  }
114
124
  // ── Forward ────────────────────────────────────────────────────────────────
115
- /** Run the model over a token sequence; returns per-position logits + a cache. */
116
- forward(tokens) {
117
- const { dModel, convKernel, numLayers, vocabSize } = this.config;
118
- const T = tokens.length;
119
- // Embed.
120
- let x = tokens.map((tok) => {
125
+ /** Embed a token sequence into per-position channel vectors. */
126
+ _embed(tokens) {
127
+ const { dModel } = this.config;
128
+ return tokens.map((tok) => {
121
129
  const row = new Float32Array(dModel);
122
130
  const off = tok * dModel;
123
131
  for (let c = 0; c < dModel; c++)
124
132
  row[c] = this.emb[off + c];
125
133
  return row;
126
134
  });
127
- const layers = [];
128
- for (let l = 0; l < numLayers; l++) {
129
- const layerIn = x;
130
- const ker = this.conv[l];
131
- const nConv = this.nConv[l];
132
- const nMoe = this.nMoe[l];
133
- // Pre-norm → depthwise causal conv → residual.
134
- const normedConv = [];
135
- const rmsConv = [];
136
- for (let t = 0; t < T; t++) {
137
- const { y, r } = rmsNorm(layerIn[t], nConv);
138
- normedConv.push(y);
139
- rmsConv.push(r);
140
- }
141
- const afterConv = [];
142
- for (let t = 0; t < T; t++) {
143
- const out = Float32Array.from(layerIn[t]); // residual base
144
- for (let c = 0; c < dModel; c++) {
145
- let acc = 0;
146
- for (let j = 0; j < convKernel; j++) {
147
- const ti = t - j;
148
- if (ti >= 0)
149
- acc += ker[c * convKernel + j] * normedConv[ti][c];
150
- }
151
- out[c] = out[c] + acc;
152
- }
153
- afterConv.push(out);
154
- }
155
- // Pre-norm → MoE channel mixer → residual.
156
- const rmsMoe = [];
157
- const moeCache = [];
158
- const afterMoe = [];
159
- for (let t = 0; t < T; t++) {
160
- const { y, r } = rmsNorm(afterConv[t], nMoe);
161
- rmsMoe.push(r);
162
- const out = Float32Array.from(afterConv[t]); // residual base
163
- const mr = this.moe[l].forward(y);
164
- for (let c = 0; c < dModel; c++)
165
- out[c] = out[c] + mr.output[c];
166
- afterMoe.push(out);
167
- moeCache.push(mr.cache);
135
+ }
136
+ /**
137
+ * One (conv + MoE) block: pre-norm → depthwise causal conv → residual, then
138
+ * pre-norm → MoE channel mixer → residual. Returns the block output and the
139
+ * activation cache its backward needs. Isolating this is what lets
140
+ * {@link lossAndBackwardCheckpointed} recompute a layer's activations on demand
141
+ * instead of retaining every layer's cache at once.
142
+ */
143
+ _forwardLayer(l, layerIn) {
144
+ const T = layerIn.length;
145
+ const window = [];
146
+ const normedConv = [];
147
+ const rmsConv = [];
148
+ const afterConv = [];
149
+ const rmsMoe = [];
150
+ const moeCache = [];
151
+ const afterMoe = [];
152
+ for (let t = 0; t < T; t++) {
153
+ const st = this._stepLayerPosition(l, layerIn[t], window);
154
+ normedConv.push(st.normed);
155
+ rmsConv.push(st.rmsConv);
156
+ afterConv.push(st.afterConv);
157
+ rmsMoe.push(st.rmsMoe);
158
+ moeCache.push(st.moeCache);
159
+ afterMoe.push(st.out);
160
+ }
161
+ return { afterMoe, cache: { layerIn, normedConv, rmsConv, afterConv, rmsMoe, moeCache } };
162
+ }
163
+ /**
164
+ * ONE position through one (conv + MoE) block, given the trailing conv window.
165
+ *
166
+ * The single place the block's maths lives: the batch path
167
+ * ({@link _forwardLayer}, which needs the full activation cache for backward)
168
+ * and the incremental path ({@link stepDecode}, which needs none of it) both
169
+ * run this, so they cannot drift apart. `window` holds the previous
170
+ * `convKernel - 1` RMSNormed conv inputs, OLDEST FIRST, and is advanced in
171
+ * place — a shorter window is exactly the causal zero-padding at the start of
172
+ * a sequence.
173
+ */
174
+ _stepLayerPosition(l, xt, window) {
175
+ const { dModel, convKernel } = this.config;
176
+ const ker = this.conv[l];
177
+ const { y: normed, r: rConv } = rmsNorm(xt, this.nConv[l]);
178
+ const afterConv = Float32Array.from(xt); // residual base
179
+ for (let c = 0; c < dModel; c++) {
180
+ let acc = ker[c * convKernel] * normed[c];
181
+ for (let j = 1; j < convKernel; j++) {
182
+ const prev = window[window.length - j]; // position t - j, or undefined before the start
183
+ if (prev)
184
+ acc += ker[c * convKernel + j] * prev[c];
168
185
  }
169
- layers.push({ layerIn, normedConv, rmsConv, afterConv, rmsMoe, moeCache });
170
- x = afterMoe;
186
+ afterConv[c] = afterConv[c] + acc;
171
187
  }
172
- // Tied head: logits_t[v] = x_t · emb[v].
173
- const logits = x.map((xt) => {
188
+ window.push(normed);
189
+ while (window.length > convKernel - 1)
190
+ window.shift();
191
+ const { y: normedMoe, r: rMoe } = rmsNorm(afterConv, this.nMoe[l]);
192
+ const out = Float32Array.from(afterConv); // residual base
193
+ const mr = this.moe[l].forward(normedMoe);
194
+ for (let c = 0; c < dModel; c++)
195
+ out[c] = out[c] + mr.output[c];
196
+ this._positionsEvaluated++;
197
+ return {
198
+ normed, rmsConv: rConv, afterConv, rmsMoe: rMoe,
199
+ moeCache: mr.cache, out,
200
+ };
201
+ }
202
+ /** One token's embedding row (a copy — callers write into it). */
203
+ _embedOne(tok) {
204
+ const { dModel } = this.config;
205
+ const row = new Float32Array(dModel);
206
+ const off = tok * dModel;
207
+ for (let c = 0; c < dModel; c++)
208
+ row[c] = this.emb[off + c];
209
+ return row;
210
+ }
211
+ /** Tied output head: logits_t[v] = x_t · emb[v]. */
212
+ _head(x) {
213
+ const { dModel, vocabSize } = this.config;
214
+ return x.map((xt) => {
174
215
  const lg = new Float32Array(vocabSize);
175
216
  for (let v = 0; v < vocabSize; v++) {
176
217
  let acc = 0;
@@ -181,30 +222,92 @@ export class EvermindLM {
181
222
  }
182
223
  return lg;
183
224
  });
184
- return { logits, cache: { tokens, layers, finalX: x } };
225
+ }
226
+ /** Run the model over a token sequence; returns per-position logits + a cache. */
227
+ forward(tokens) {
228
+ let x = this._embed(tokens);
229
+ const layers = [];
230
+ for (let l = 0; l < this.config.numLayers; l++) {
231
+ const { afterMoe, cache } = this._forwardLayer(l, x);
232
+ layers.push(cache);
233
+ x = afterMoe;
234
+ }
235
+ return { logits: this._head(x), cache: { tokens, layers, finalX: x } };
236
+ }
237
+ /**
238
+ * Produces a single fixed-length embedding vector for a token sequence — the
239
+ * CPU counterpart of `HybridMambaModel.embed()`, to the same contract.
240
+ *
241
+ * Runs the full block stack and takes the final hidden state — i.e. exactly the
242
+ * state the tied LM head consumes in {@link forward} — then mean-pools across
243
+ * sequence positions and L2-normalises. The result has length `dModel`, so
244
+ * cosine similarity between two embeddings reduces to a dot product.
245
+ *
246
+ * Like the GPU path it skips the (expensive) head projection: it only needs the
247
+ * `dModel`-wide hidden state, not `vocabSize` logits. Unlike the GPU path it
248
+ * needs no WebGPU device at all, which is what lets a headless host (the stdio
249
+ * MCP server) rank recall by SSM embedding instead of word overlap.
250
+ *
251
+ * Streamed one position at a time through the SHARED block implementation
252
+ * ({@link _stepLayerPosition}, the same routine {@link forward} and
253
+ * {@link stepDecode} run), so it can never drift from them and it retains no
254
+ * per-position activation cache.
255
+ *
256
+ * The embedding reflects whatever the model currently knows — an untrained
257
+ * model behaves like a random projection of the token embeddings (still
258
+ * lexically discriminative), and the representation sharpens as the model is
259
+ * adapted/distilled.
260
+ */
261
+ embed(tokenIds) {
262
+ const { dModel, numLayers } = this.config;
263
+ const out = new Float32Array(dModel);
264
+ const seqLen = tokenIds.length;
265
+ if (seqLen === 0)
266
+ return out;
267
+ // One rolling conv window per layer — a shorter window at the start of the
268
+ // sequence IS the causal zero-padding, exactly as in stepDecode.
269
+ const windows = Array.from({ length: numLayers }, () => []);
270
+ for (let t = 0; t < seqLen; t++) {
271
+ let x = this._embedOne(tokenIds[t]);
272
+ for (let l = 0; l < numLayers; l++)
273
+ x = this._stepLayerPosition(l, x, windows[l]).out;
274
+ for (let c = 0; c < dModel; c++)
275
+ out[c] = out[c] + x[c];
276
+ }
277
+ // Mean-pool across positions.
278
+ for (let c = 0; c < dModel; c++)
279
+ out[c] = out[c] / seqLen;
280
+ // L2-normalise so cosine similarity reduces to a dot product.
281
+ let norm = 0;
282
+ for (let c = 0; c < dModel; c++)
283
+ norm += out[c] * out[c];
284
+ norm = Math.sqrt(norm) || 1;
285
+ for (let c = 0; c < dModel; c++)
286
+ out[c] = out[c] / norm;
287
+ return out;
288
+ }
289
+ /** Text-level embedding: encode with `codec`, then {@link embed}. */
290
+ embedText(text, codec) {
291
+ return this.embed(codec.encode(text));
185
292
  }
186
293
  // ── Loss + backward ──────────────────────────────────────────────────────────
187
294
  /**
188
- * Next-token cross-entropy over the sequence (predict tokens[t+1] from
189
- * position t), accumulating exact gradients. Returns the mean loss. Call
190
- * {@link zeroGrad} before and an optimiser step after.
295
+ * Head + tied-embedding gradient. Accumulates dL/d(head→emb) into gEmb and
296
+ * returns the mean next-token loss plus dL/d(finalX). Shared by the full and
297
+ * checkpointed backward paths so the head maths lives in one place.
191
298
  */
192
- lossAndBackward(tokens) {
193
- const { dModel, convKernel, numLayers, vocabSize } = this.config;
299
+ _headBackward(tokens, logits, finalX) {
300
+ const { dModel, vocabSize } = this.config;
194
301
  const T = tokens.length;
195
- if (T < 2)
196
- return 0;
197
- const { logits, cache } = this.forward(tokens);
198
302
  const predPositions = T - 1; // positions 0..T-2 predict the next token
199
303
  const inv = 1 / predPositions;
200
- // dL/d(finalX_t) and head gradient into the tied embedding.
201
304
  const dX = Array.from({ length: T }, () => new Float32Array(dModel));
202
305
  let loss = 0;
203
306
  for (let t = 0; t < predPositions; t++) {
204
307
  const target = tokens[t + 1];
205
308
  loss += crossEntropyLoss(logits[t], target) * inv;
206
309
  const dLogit = crossEntropyGrad(logits[t], target); // probs - onehot
207
- const xt = cache.finalX[t];
310
+ const xt = finalX[t];
208
311
  for (let v = 0; v < vocabSize; v++) {
209
312
  const g = dLogit[v] * inv;
210
313
  if (g === 0)
@@ -216,62 +319,182 @@ export class EvermindLM {
216
319
  }
217
320
  }
218
321
  }
219
- // Backprop through layers in reverse.
220
- for (let l = numLayers - 1; l >= 0; l--) {
221
- const lc = cache.layers[l];
222
- const ker = this.conv[l];
223
- const gker = this.gConv[l];
224
- const nConv = this.nConv[l];
225
- const gNConv = this.gNConv[l];
226
- const nMoe = this.nMoe[l];
227
- const gNMoe = this.gNMoe[l];
228
- // MoE sub-block: afterMoe = afterConv + MoE(RMSNorm(afterConv, nMoe)).
229
- const dAfterConv = [];
230
- for (let t = 0; t < T; t++) {
231
- const dMoeNormed = this.moe[l].backward(dX[t], lc.moeCache[t]);
232
- const { dx, dgain } = rmsNormBackward(dMoeNormed, lc.afterConv[t], lc.rmsMoe[t], nMoe);
233
- for (let c = 0; c < dModel; c++)
234
- gNMoe[c] = gNMoe[c] + dgain[c];
235
- const d = Float32Array.from(dX[t]); // residual passthrough
236
- for (let c = 0; c < dModel; c++)
237
- d[c] = d[c] + dx[c];
238
- dAfterConv.push(d);
239
- }
240
- // Conv sub-block: afterConv = layerIn + conv(RMSNorm(layerIn, nConv)).
241
- const dNormedConv = Array.from({ length: T }, () => new Float32Array(dModel));
242
- const dLayerIn = dAfterConv.map((v) => Float32Array.from(v)); // residual passthrough
243
- for (let t = 0; t < T; t++) {
244
- for (let c = 0; c < dModel; c++) {
245
- const dmix = dAfterConv[t][c];
246
- if (dmix === 0)
322
+ return { loss, dX };
323
+ }
324
+ /**
325
+ * Backward through one (conv + MoE) block given dL/d(block output) and the
326
+ * block's activation cache. Accumulates conv/norm/MoE gradients and returns
327
+ * dL/d(block input). The inverse of {@link _forwardLayer}.
328
+ */
329
+ _backwardLayer(l, dOut, lc) {
330
+ const { dModel, convKernel } = this.config;
331
+ const T = dOut.length;
332
+ const ker = this.conv[l];
333
+ const gker = this.gConv[l];
334
+ const nConv = this.nConv[l];
335
+ const gNConv = this.gNConv[l];
336
+ const nMoe = this.nMoe[l];
337
+ const gNMoe = this.gNMoe[l];
338
+ // MoE sub-block: afterMoe = afterConv + MoE(RMSNorm(afterConv, nMoe)).
339
+ const dAfterConv = [];
340
+ for (let t = 0; t < T; t++) {
341
+ const dMoeNormed = this.moe[l].backward(dOut[t], lc.moeCache[t]);
342
+ const { dx, dgain } = rmsNormBackward(dMoeNormed, lc.afterConv[t], lc.rmsMoe[t], nMoe);
343
+ for (let c = 0; c < dModel; c++)
344
+ gNMoe[c] = gNMoe[c] + dgain[c];
345
+ const d = Float32Array.from(dOut[t]); // residual passthrough
346
+ for (let c = 0; c < dModel; c++)
347
+ d[c] = d[c] + dx[c];
348
+ dAfterConv.push(d);
349
+ }
350
+ // Conv sub-block: afterConv = layerIn + conv(RMSNorm(layerIn, nConv)).
351
+ const dNormedConv = Array.from({ length: T }, () => new Float32Array(dModel));
352
+ const dLayerIn = dAfterConv.map((v) => Float32Array.from(v)); // residual passthrough
353
+ for (let t = 0; t < T; t++) {
354
+ for (let c = 0; c < dModel; c++) {
355
+ const dmix = dAfterConv[t][c];
356
+ if (dmix === 0)
357
+ continue;
358
+ for (let j = 0; j < convKernel; j++) {
359
+ const ti = t - j;
360
+ if (ti < 0)
247
361
  continue;
248
- for (let j = 0; j < convKernel; j++) {
249
- const ti = t - j;
250
- if (ti < 0)
251
- continue;
252
- gker[c * convKernel + j] = gker[c * convKernel + j] + dmix * lc.normedConv[ti][c];
253
- dNormedConv[ti][c] = dNormedConv[ti][c] + dmix * ker[c * convKernel + j];
254
- }
255
- }
256
- }
257
- for (let t = 0; t < T; t++) {
258
- const { dx, dgain } = rmsNormBackward(dNormedConv[t], lc.layerIn[t], lc.rmsConv[t], nConv);
259
- for (let c = 0; c < dModel; c++) {
260
- gNConv[c] = gNConv[c] + dgain[c];
261
- dLayerIn[t][c] = dLayerIn[t][c] + dx[c];
362
+ gker[c * convKernel + j] = gker[c * convKernel + j] + dmix * lc.normedConv[ti][c];
363
+ dNormedConv[ti][c] = dNormedConv[ti][c] + dmix * ker[c * convKernel + j];
262
364
  }
263
365
  }
264
- for (let t = 0; t < T; t++)
265
- dX[t] = dLayerIn[t];
266
366
  }
267
- // Embedding lookup: dX at layer-0 input flows into the row for token_t.
268
367
  for (let t = 0; t < T; t++) {
368
+ const { dx, dgain } = rmsNormBackward(dNormedConv[t], lc.layerIn[t], lc.rmsConv[t], nConv);
369
+ for (let c = 0; c < dModel; c++) {
370
+ gNConv[c] = gNConv[c] + dgain[c];
371
+ dLayerIn[t][c] = dLayerIn[t][c] + dx[c];
372
+ }
373
+ }
374
+ return dLayerIn;
375
+ }
376
+ /** Embedding lookup: dL/d(layer-0 input) flows into the row for each token. */
377
+ _embedBackward(tokens, dIn) {
378
+ const { dModel } = this.config;
379
+ for (let t = 0; t < tokens.length; t++) {
269
380
  const off = tokens[t] * dModel;
270
381
  for (let c = 0; c < dModel; c++)
271
- this.gEmb[off + c] = this.gEmb[off + c] + dX[t][c];
382
+ this.gEmb[off + c] = this.gEmb[off + c] + dIn[t][c];
383
+ }
384
+ }
385
+ /**
386
+ * Next-token cross-entropy over the sequence (predict tokens[t+1] from
387
+ * position t), accumulating exact gradients. Returns the mean loss. Call
388
+ * {@link zeroGrad} before and an optimiser step after.
389
+ */
390
+ lossAndBackward(tokens) {
391
+ if (tokens.length < 2)
392
+ return 0;
393
+ const { logits, cache } = this.forward(tokens);
394
+ const { loss, dX } = this._headBackward(tokens, logits, cache.finalX);
395
+ let d = dX;
396
+ for (let l = this.config.numLayers - 1; l >= 0; l--)
397
+ d = this._backwardLayer(l, d, cache.layers[l]);
398
+ this._embedBackward(tokens, d);
399
+ return loss;
400
+ }
401
+ /**
402
+ * Activation-checkpointed backward — numerically identical gradients to
403
+ * {@link lossAndBackward}, but retains only the per-LAYER inputs during the
404
+ * forward instead of every layer's full activation cache. Each layer's
405
+ * activations are RECOMPUTED (a cheap extra forward) when its backward runs,
406
+ * so peak activation memory is one layer's cache, not all of them. This is the
407
+ * memory-for-compute trade the cookbook pairs with FSDP to fit longer
408
+ * sequences / bigger models on a constrained device.
409
+ */
410
+ lossAndBackwardCheckpointed(tokens) {
411
+ if (tokens.length < 2)
412
+ return 0;
413
+ let x = this._embed(tokens);
414
+ const layerInputs = [];
415
+ for (let l = 0; l < this.config.numLayers; l++) {
416
+ layerInputs.push(x); // keep only the input; drop the cache
417
+ x = this._forwardLayer(l, x).afterMoe;
272
418
  }
419
+ const logits = this._head(x);
420
+ const { loss, dX } = this._headBackward(tokens, logits, x);
421
+ let d = dX;
422
+ for (let l = this.config.numLayers - 1; l >= 0; l--) {
423
+ const { cache } = this._forwardLayer(l, layerInputs[l]); // recompute this layer's activations
424
+ d = this._backwardLayer(l, d, cache);
425
+ }
426
+ this._embedBackward(tokens, d);
273
427
  return loss;
274
428
  }
429
+ // ── Incremental decoding (prefix cache) ──────────────────────────────────────
430
+ /** Positions pushed through a block since the last {@link resetStats}. */
431
+ get positionsEvaluated() { return this._positionsEvaluated; }
432
+ resetStats() { this._positionsEvaluated = 0; }
433
+ /** An empty decode state — the start of a sequence. */
434
+ newDecodeState() {
435
+ return {
436
+ convWindow: Array.from({ length: this.config.numLayers }, () => []),
437
+ length: 0,
438
+ lastLogits: null,
439
+ };
440
+ }
441
+ /** Deep copy, so one prefix state can seed many independent continuations. */
442
+ cloneDecodeState(state) {
443
+ return {
444
+ convWindow: state.convWindow.map((w) => w.map((v) => Float32Array.from(v))),
445
+ length: state.length,
446
+ lastLogits: state.lastLogits ? Float32Array.from(state.lastLogits) : null,
447
+ };
448
+ }
449
+ /**
450
+ * Consume `tokens` from `state`, returning the per-position logits and the
451
+ * ADVANCED state. `state` is not mutated — pass {@link cloneDecodeState} output
452
+ * or a fresh state; this clones internally so the caller's prefix stays reusable.
453
+ *
454
+ * Numerically identical to {@link forward} over the concatenated sequence: the
455
+ * shared {@link _stepLayerPosition} is the only implementation of the block.
456
+ */
457
+ stepDecode(state, tokens) {
458
+ const next = this.cloneDecodeState(state);
459
+ const logits = [];
460
+ for (const tok of tokens) {
461
+ let x = this._embedOne(tok);
462
+ for (let l = 0; l < this.config.numLayers; l++) {
463
+ x = this._stepLayerPosition(l, x, next.convWindow[l]).out;
464
+ }
465
+ const lg = this._head([x])[0];
466
+ logits.push(lg);
467
+ next.lastLogits = lg;
468
+ next.length++;
469
+ }
470
+ return { logits, state: next };
471
+ }
472
+ /** Run a prompt once and keep the state, so continuations are cheap. */
473
+ prefixState(tokens) {
474
+ return this.stepDecode(this.newDecodeState(), tokens).state;
475
+ }
476
+ /**
477
+ * Mean log-probability the model assigns to `continuation` following a cached
478
+ * prefix — the tool-decision vote, without replaying the prompt.
479
+ *
480
+ * Returns `-Infinity` for an empty continuation, and requires a prefix with at
481
+ * least one position (there must be a position to predict the first
482
+ * continuation token FROM).
483
+ */
484
+ scoreContinuation(prefix, continuation) {
485
+ if (continuation.length === 0)
486
+ return -Infinity;
487
+ if (!prefix.lastLogits)
488
+ return -Infinity;
489
+ // The last prefix position predicts continuation[0]; each continuation
490
+ // position then predicts the next one, so the final one is never scored.
491
+ const { logits } = this.stepDecode(prefix, continuation.slice(0, -1));
492
+ let total = logProbOfToken(prefix.lastLogits, continuation[0]);
493
+ for (let i = 1; i < continuation.length; i++) {
494
+ total += logProbOfToken(logits[i - 1], continuation[i]);
495
+ }
496
+ return total / continuation.length;
497
+ }
275
498
  // ── Generation ───────────────────────────────────────────────────────────────
276
499
  /**
277
500
  * Text-level generation: encode the prompt, generate, decode. `codec` is any
@@ -282,20 +505,25 @@ export class EvermindLM {
282
505
  generateText(prompt, codec, opts) {
283
506
  return codec.decode(this.generate(codec.encode(prompt), opts));
284
507
  }
285
- /** Greedy / temperature-sampled autoregressive generation. Returns NEW token ids. */
508
+ /**
509
+ * Greedy / temperature-sampled autoregressive generation. Returns NEW token ids.
510
+ *
511
+ * Carries an incremental decode state, so the prompt is processed ONCE and each
512
+ * new token costs one position. The previous implementation re-ran the whole
513
+ * sequence per token, making generation quadratic in the prompt length.
514
+ */
286
515
  generate(prompt, opts) {
287
516
  const temperature = opts.temperature ?? 0;
288
517
  const rng = temperature > 0 ? new SeededRng((opts.seed ?? 1) >>> 0 || 1) : null;
289
- const tokens = [...prompt];
518
+ let state = this.stepDecode(this.newDecodeState(), prompt.length > 0 ? prompt : [0]).state;
290
519
  const produced = [];
291
520
  for (let n = 0; n < opts.maxNewTokens; n++) {
292
- const { logits } = this.forward(tokens.length > 0 ? tokens : [0]);
293
- const last = logits[logits.length - 1];
521
+ const last = state.lastLogits;
294
522
  const next = rng ? sampleTemperature(last, temperature, rng) : argmax(last);
295
523
  produced.push(next);
296
- tokens.push(next);
297
524
  if (opts.stopToken !== undefined && next === opts.stopToken)
298
525
  break;
526
+ state = this.stepDecode(state, [next]).state;
299
527
  }
300
528
  return produced;
301
529
  }
@@ -437,26 +665,95 @@ export class EvermindLMTrainer {
437
665
  model;
438
666
  opts;
439
667
  adam;
668
+ scaler;
669
+ grads;
440
670
  constructor(model, opts = {}) {
441
671
  this.model = model;
442
672
  this.opts = opts;
443
673
  this.adam = new AdamW(model, opts);
674
+ this.grads = model.gradients();
675
+ this.scaler = opts.mixedPrecision
676
+ ? new DynamicLossScaler(typeof opts.mixedPrecision === "object" ? opts.mixedPrecision : {})
677
+ : null;
678
+ }
679
+ /** The dynamic loss scaler (mixed-precision mode only) — exposes scale/overflow stats. */
680
+ get lossScaler() {
681
+ return this.scaler;
682
+ }
683
+ _backward(seq) {
684
+ return this.opts.checkpoint ? this.model.lossAndBackwardCheckpointed(seq) : this.model.lossAndBackward(seq);
685
+ }
686
+ /** Divide accumulated gradients by `n` (accumulation averaging), in place. */
687
+ _scaleGrads(n) {
688
+ if (n === 1)
689
+ return;
690
+ const inv = 1 / n;
691
+ for (const g of this.grads)
692
+ for (let i = 0; i < g.data.length; i++)
693
+ g.data[i] = g.data[i] * inv;
694
+ }
695
+ /** Round accumulated gradients to fp16 precision (mixed-precision simulation), in place. */
696
+ _fp16Grads() {
697
+ for (const g of this.grads)
698
+ for (let i = 0; i < g.data.length; i++)
699
+ g.data[i] = roundFp16(g.data[i]);
700
+ }
701
+ /** Multiply accumulated gradients by `s`, in place. */
702
+ _mulGrads(s) {
703
+ if (s === 1)
704
+ return;
705
+ for (const g of this.grads)
706
+ for (let i = 0; i < g.data.length; i++)
707
+ g.data[i] = g.data[i] * s;
444
708
  }
445
709
  /** Train on a set of token sequences; returns per-epoch mean loss. */
446
710
  fit(sequences) {
447
711
  const epochs = this.opts.epochs ?? 1;
712
+ const accum = Math.max(1, this.opts.accumSteps ?? 1);
448
713
  const history = [];
449
714
  for (let e = 0; e < epochs; e++) {
450
715
  let total = 0;
451
716
  let n = 0;
717
+ let pending = 0;
718
+ // The scale is fixed for the duration of one accumulation window, so scale-up
719
+ // (per sequence) and unscale (at flush) always use the SAME factor even as the
720
+ // controller grows/backs it off between windows.
721
+ let windowScale = this.scaler ? this.scaler.scale : 1;
722
+ this.model.zeroGrad();
723
+ const flush = () => {
724
+ if (pending === 0)
725
+ return;
726
+ this._scaleGrads(pending); // accumulation averaging
727
+ if (this.scaler) {
728
+ this._fp16Grads(); // half-precision gradients
729
+ const overflow = this.scaler.check(this.grads);
730
+ if (this.scaler.update(overflow)) {
731
+ this._mulGrads(1 / windowScale); // unscale by the SAME factor we scaled by
732
+ this.adam.step();
733
+ } // else: skip the step, scale has backed off
734
+ windowScale = this.scaler.scale; // next window uses the updated scale
735
+ }
736
+ else {
737
+ this.adam.step();
738
+ }
739
+ this.model.zeroGrad();
740
+ pending = 0;
741
+ };
452
742
  for (const seq of sequences) {
453
743
  if (seq.length < 2)
454
744
  continue;
455
- this.model.zeroGrad();
456
- total += this.model.lossAndBackward(seq);
457
- this.adam.step();
745
+ if (pending === 0 && this.scaler)
746
+ windowScale = this.scaler.scale;
747
+ const loss = this._backward(seq);
748
+ // Emulate scaling the loss before backward: the gradient is linear in the
749
+ // loss, so scaling the just-produced gradients by the loss scale is equivalent.
750
+ this._mulGrads(windowScale);
751
+ total += loss;
458
752
  n++;
753
+ if (++pending >= accum)
754
+ flush();
459
755
  }
756
+ flush();
460
757
  history.push(n > 0 ? total / n : 0);
461
758
  }
462
759
  return history;
@@ -520,4 +817,24 @@ function sampleTemperature(logits, temperature, rng) {
520
817
  }
521
818
  return probs.length - 1;
522
819
  }
820
+ /**
821
+ * Log-probability the model assigned to `id` at a position, from that position's
822
+ * raw logits. Log-softmax computed max-shifted so a long-tail logit cannot
823
+ * overflow `exp`.
824
+ */
825
+ export function logProbOfToken(row, id) {
826
+ let max = -Infinity;
827
+ for (let i = 0; i < row.length; i++) {
828
+ const v = row[i];
829
+ if (v > max)
830
+ max = v;
831
+ }
832
+ if (!Number.isFinite(max))
833
+ return -Infinity;
834
+ let sum = 0;
835
+ for (let i = 0; i < row.length; i++)
836
+ sum += Math.exp(row[i] - max);
837
+ const logit = id >= 0 && id < row.length ? row[id] : -Infinity;
838
+ return logit - (max + Math.log(sum));
839
+ }
523
840
  //# sourceMappingURL=evermind_lm.js.map