@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/dist/lm/evermind_lm.js
CHANGED
|
@@ -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
|
-
/**
|
|
116
|
-
|
|
117
|
-
const { dModel
|
|
118
|
-
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
-
|
|
170
|
-
x = afterMoe;
|
|
186
|
+
afterConv[c] = afterConv[c] + acc;
|
|
171
187
|
}
|
|
172
|
-
|
|
173
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
189
|
-
*
|
|
190
|
-
*
|
|
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
|
-
|
|
193
|
-
const { dModel,
|
|
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 =
|
|
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
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
const
|
|
243
|
-
for (let
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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
|
-
|
|
249
|
-
|
|
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] +
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
|
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.
|
|
456
|
-
|
|
457
|
-
this.
|
|
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
|