@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.
- package/README.md +14 -0
- package/dist/bench/adaptation.d.ts +57 -0
- package/dist/bench/adaptation.d.ts.map +1 -0
- package/dist/bench/adaptation.js +167 -0
- package/dist/bench/adaptation.js.map +1 -0
- package/dist/bench/harness.d.ts +2 -0
- package/dist/bench/harness.d.ts.map +1 -1
- package/dist/bench/harness.js +1 -1
- package/dist/bench/harness.js.map +1 -1
- package/dist/bench/index.d.ts +5 -3
- package/dist/bench/index.d.ts.map +1 -1
- package/dist/bench/index.js +4 -2
- package/dist/bench/index.js.map +1 -1
- package/dist/bench/types.d.ts +88 -0
- package/dist/bench/types.d.ts.map +1 -1
- package/dist/evermind/adapt.d.ts +40 -0
- package/dist/evermind/adapt.d.ts.map +1 -0
- package/dist/evermind/adapt.js +25 -0
- package/dist/evermind/adapt.js.map +1 -0
- package/dist/evermind/delta_wire.d.ts +43 -0
- package/dist/evermind/delta_wire.d.ts.map +1 -0
- package/dist/evermind/delta_wire.js +94 -0
- package/dist/evermind/delta_wire.js.map +1 -0
- package/dist/evermind/eval.d.ts +31 -0
- package/dist/evermind/eval.d.ts.map +1 -0
- package/dist/evermind/eval.js +36 -0
- package/dist/evermind/eval.js.map +1 -0
- package/dist/evermind/index.d.ts +16 -0
- package/dist/evermind/index.d.ts.map +1 -0
- package/dist/evermind/index.js +11 -0
- package/dist/evermind/index.js.map +1 -0
- package/dist/evermind/merge.d.ts +21 -0
- package/dist/evermind/merge.d.ts.map +1 -0
- package/dist/evermind/merge.js +79 -0
- package/dist/evermind/merge.js.map +1 -0
- package/dist/{moe/moe_package.d.ts → evermind/package.d.ts} +55 -5
- package/dist/evermind/package.d.ts.map +1 -0
- package/dist/{moe/moe_package.js → evermind/package.js} +118 -19
- package/dist/evermind/package.js.map +1 -0
- package/dist/import/foreign/adapter.d.ts +59 -0
- package/dist/import/foreign/adapter.d.ts.map +1 -0
- package/dist/import/foreign/adapter.js +76 -0
- package/dist/import/foreign/adapter.js.map +1 -0
- package/dist/import/foreign/apply.d.ts +23 -0
- package/dist/import/foreign/apply.d.ts.map +1 -0
- package/dist/import/foreign/apply.js +43 -0
- package/dist/import/foreign/apply.js.map +1 -0
- package/dist/import/foreign/codestral_mamba.d.ts +57 -0
- package/dist/import/foreign/codestral_mamba.d.ts.map +1 -0
- package/dist/import/foreign/codestral_mamba.js +174 -0
- package/dist/import/foreign/codestral_mamba.js.map +1 -0
- package/dist/import/foreign/falcon_mamba.d.ts +51 -0
- package/dist/import/foreign/falcon_mamba.d.ts.map +1 -0
- package/dist/import/foreign/falcon_mamba.js +131 -0
- package/dist/import/foreign/falcon_mamba.js.map +1 -0
- package/dist/import/foreign/index.d.ts +21 -0
- package/dist/import/foreign/index.d.ts.map +1 -0
- package/dist/import/foreign/index.js +18 -0
- package/dist/import/foreign/index.js.map +1 -0
- package/dist/import/foreign/plan.d.ts +122 -0
- package/dist/import/foreign/plan.d.ts.map +1 -0
- package/dist/import/foreign/plan.js +171 -0
- package/dist/import/foreign/plan.js.map +1 -0
- package/dist/import/foreign/registry.d.ts +35 -0
- package/dist/import/foreign/registry.d.ts.map +1 -0
- package/dist/import/foreign/registry.js +89 -0
- package/dist/import/foreign/registry.js.map +1 -0
- package/dist/import/index.d.ts +7 -0
- package/dist/import/index.d.ts.map +1 -1
- package/dist/import/index.js +7 -0
- package/dist/import/index.js.map +1 -1
- package/dist/index.d.ts +29 -12
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +24 -9
- package/dist/index.js.map +1 -1
- package/dist/kernels/selective_scan.d.ts.map +1 -1
- package/dist/kernels/selective_scan.js +15 -9
- package/dist/kernels/selective_scan.js.map +1 -1
- package/dist/kernels/slice.d.ts +26 -0
- package/dist/kernels/slice.d.ts.map +1 -0
- package/dist/kernels/slice.js +52 -0
- package/dist/kernels/slice.js.map +1 -0
- package/dist/kernels/weight_update.d.ts.map +1 -1
- package/dist/kernels/weight_update.js +30 -4
- package/dist/kernels/weight_update.js.map +1 -1
- package/dist/lm/evermind_lm.d.ts +180 -4
- package/dist/lm/evermind_lm.d.ts.map +1 -1
- package/dist/lm/evermind_lm.js +433 -116
- package/dist/lm/evermind_lm.js.map +1 -1
- package/dist/lm/index.d.ts +8 -2
- package/dist/lm/index.d.ts.map +1 -1
- package/dist/lm/index.js +6 -1
- package/dist/lm/index.js.map +1 -1
- package/dist/lm/text_embedder.d.ts +55 -0
- package/dist/lm/text_embedder.d.ts.map +1 -0
- package/dist/lm/text_embedder.js +62 -0
- package/dist/lm/text_embedder.js.map +1 -0
- package/dist/model/attention_block.d.ts.map +1 -1
- package/dist/model/attention_block.js +9 -5
- package/dist/model/attention_block.js.map +1 -1
- package/dist/model/mamba1_block.d.ts.map +1 -1
- package/dist/model/mamba1_block.js +28 -12
- package/dist/model/mamba1_block.js.map +1 -1
- package/dist/model/mamba2_block.d.ts.map +1 -1
- package/dist/model/mamba2_block.js +14 -9
- package/dist/model/mamba2_block.js.map +1 -1
- package/dist/model/mamba3_block.d.ts.map +1 -1
- package/dist/model/mamba3_block.js +14 -9
- package/dist/model/mamba3_block.js.map +1 -1
- package/dist/moe/index.d.ts +0 -2
- package/dist/moe/index.d.ts.map +1 -1
- package/dist/moe/index.js +0 -1
- package/dist/moe/index.js.map +1 -1
- package/dist/optim/adamw.d.ts +58 -1
- package/dist/optim/adamw.d.ts.map +1 -1
- package/dist/optim/adamw.js +69 -4
- package/dist/optim/adamw.js.map +1 -1
- package/dist/tokenizer/bpe.d.ts +28 -0
- package/dist/tokenizer/bpe.d.ts.map +1 -1
- package/dist/tokenizer/bpe.js +47 -0
- package/dist/tokenizer/bpe.js.map +1 -1
- package/dist/training/cpu_ops.d.ts +44 -0
- package/dist/training/cpu_ops.d.ts.map +1 -0
- package/dist/training/cpu_ops.js +111 -0
- package/dist/training/cpu_ops.js.map +1 -0
- package/dist/training/lora.d.ts +153 -0
- package/dist/training/lora.d.ts.map +1 -0
- package/dist/training/lora.js +324 -0
- package/dist/training/lora.js.map +1 -0
- package/dist/training/mamba1_cpu.d.ts +109 -0
- package/dist/training/mamba1_cpu.d.ts.map +1 -0
- package/dist/training/mamba1_cpu.js +316 -0
- package/dist/training/mamba1_cpu.js.map +1 -0
- package/dist/training/mixed_precision.d.ts +67 -0
- package/dist/training/mixed_precision.d.ts.map +1 -0
- package/dist/training/mixed_precision.js +101 -0
- package/dist/training/mixed_precision.js.map +1 -0
- package/dist/training/model_cpu.d.ts +100 -0
- package/dist/training/model_cpu.d.ts.map +1 -0
- package/dist/training/model_cpu.js +252 -0
- package/dist/training/model_cpu.js.map +1 -0
- package/dist/training/trainer.d.ts +156 -16
- package/dist/training/trainer.d.ts.map +1 -1
- package/dist/training/trainer.js +327 -101
- package/dist/training/trainer.js.map +1 -1
- package/dist/utils/base64.d.ts +12 -0
- package/dist/utils/base64.d.ts.map +1 -0
- package/dist/utils/base64.js +27 -0
- package/dist/utils/base64.js.map +1 -0
- package/package.json +3 -1
- package/src/bench/adaptation.ts +180 -0
- package/src/bench/harness.ts +1 -1
- package/src/bench/index.ts +13 -2
- package/src/bench/types.ts +93 -0
- package/src/evermind/adapt.ts +61 -0
- package/src/evermind/delta_wire.ts +114 -0
- package/src/evermind/eval.ts +62 -0
- package/src/evermind/index.ts +33 -0
- package/src/evermind/merge.ts +106 -0
- package/src/{moe/moe_package.ts → evermind/package.ts} +148 -17
- package/src/import/foreign/adapter.ts +106 -0
- package/src/import/foreign/apply.ts +54 -0
- package/src/import/foreign/codestral_mamba.ts +224 -0
- package/src/import/foreign/falcon_mamba.ts +173 -0
- package/src/import/foreign/index.ts +37 -0
- package/src/import/foreign/plan.ts +294 -0
- package/src/import/foreign/registry.ts +126 -0
- package/src/import/index.ts +30 -0
- package/src/index.ts +104 -13
- package/src/kernels/selective_scan.ts +15 -9
- package/src/kernels/slice.ts +64 -0
- package/src/kernels/weight_update.ts +30 -4
- package/src/lm/evermind_lm.ts +470 -116
- package/src/lm/index.ts +12 -2
- package/src/lm/text_embedder.ts +84 -0
- package/src/model/attention_block.ts +9 -5
- package/src/model/mamba1_block.ts +28 -12
- package/src/model/mamba2_block.ts +14 -9
- package/src/model/mamba3_block.ts +14 -9
- package/src/moe/index.ts +0 -9
- package/src/optim/adamw.ts +115 -7
- package/src/tokenizer/bpe.ts +55 -0
- package/src/training/cpu_ops.ts +127 -0
- package/src/training/lora.ts +365 -0
- package/src/training/mamba1_cpu.ts +421 -0
- package/src/training/mixed_precision.ts +119 -0
- package/src/training/model_cpu.ts +360 -0
- package/src/training/trainer.ts +397 -154
- package/src/utils/base64.ts +28 -0
- package/dist/model/mamba_block.d.ts +0 -64
- package/dist/model/mamba_block.d.ts.map +0 -1
- package/dist/model/mamba_block.js +0 -303
- package/dist/model/mamba_block.js.map +0 -1
- package/dist/moe/moe_package.d.ts.map +0 -1
- package/dist/moe/moe_package.js.map +0 -1
- package/src/model/mamba_block.ts +0 -401
package/src/lm/evermind_lm.ts
CHANGED
|
@@ -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
|
-
/**
|
|
185
|
-
|
|
186
|
-
const { dModel
|
|
187
|
-
|
|
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
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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
|
-
|
|
241
|
-
|
|
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
|
-
|
|
245
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
263
|
-
*
|
|
264
|
-
*
|
|
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
|
-
|
|
267
|
-
const { dModel,
|
|
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 =
|
|
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
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
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
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
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
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
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
|
-
|
|
341
|
-
|
|
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]! +
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
|
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:
|
|
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.
|
|
535
|
-
|
|
536
|
-
|
|
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
|
+
}
|