@seanhogg/builderforce-memory-engine 2026.7.12 → 2026.7.14
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/dist/index.d.ts +6 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/lm/evermind_lm.d.ts +70 -3
- package/dist/lm/evermind_lm.d.ts.map +1 -1
- package/dist/lm/evermind_lm.js +242 -110
- package/dist/lm/evermind_lm.js.map +1 -1
- package/dist/lm/index.d.ts +5 -1
- package/dist/lm/index.d.ts.map +1 -1
- package/dist/lm/index.js +3 -0
- package/dist/lm/index.js.map +1 -1
- package/dist/optim/adamw.d.ts +26 -1
- package/dist/optim/adamw.d.ts.map +1 -1
- package/dist/optim/adamw.js +35 -4
- package/dist/optim/adamw.js.map +1 -1
- 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/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/package.json +1 -1
- package/src/index.ts +8 -2
- package/src/lm/evermind_lm.ts +254 -110
- package/src/lm/index.ts +7 -1
- package/src/optim/adamw.ts +55 -7
- package/src/training/lora.ts +365 -0
- package/src/training/mixed_precision.ts +119 -0
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";
|
|
@@ -181,68 +182,71 @@ export class EvermindLM {
|
|
|
181
182
|
|
|
182
183
|
// ── Forward ────────────────────────────────────────────────────────────────
|
|
183
184
|
|
|
184
|
-
/**
|
|
185
|
-
|
|
186
|
-
const { dModel
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
// Embed.
|
|
190
|
-
let x: Float32Array[] = tokens.map((tok) => {
|
|
185
|
+
/** Embed a token sequence into per-position channel vectors. */
|
|
186
|
+
private _embed(tokens: number[]): Float32Array[] {
|
|
187
|
+
const { dModel } = this.config;
|
|
188
|
+
return tokens.map((tok) => {
|
|
191
189
|
const row = new Float32Array(dModel);
|
|
192
190
|
const off = tok * dModel;
|
|
193
191
|
for (let c = 0; c < dModel; c++) row[c] = this.emb[off + c]!;
|
|
194
192
|
return row;
|
|
195
193
|
});
|
|
194
|
+
}
|
|
196
195
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
196
|
+
/**
|
|
197
|
+
* One (conv + MoE) block: pre-norm → depthwise causal conv → residual, then
|
|
198
|
+
* pre-norm → MoE channel mixer → residual. Returns the block output and the
|
|
199
|
+
* activation cache its backward needs. Isolating this is what lets
|
|
200
|
+
* {@link lossAndBackwardCheckpointed} recompute a layer's activations on demand
|
|
201
|
+
* instead of retaining every layer's cache at once.
|
|
202
|
+
*/
|
|
203
|
+
private _forwardLayer(l: number, layerIn: Float32Array[]): { afterMoe: Float32Array[]; cache: LayerCache } {
|
|
204
|
+
const { dModel, convKernel } = this.config;
|
|
205
|
+
const T = layerIn.length;
|
|
206
|
+
const ker = this.conv[l]!;
|
|
207
|
+
const nConv = this.nConv[l]!;
|
|
208
|
+
const nMoe = this.nMoe[l]!;
|
|
209
|
+
|
|
210
|
+
const normedConv: Float32Array[] = [];
|
|
211
|
+
const rmsConv: number[] = [];
|
|
212
|
+
for (let t = 0; t < T; t++) {
|
|
213
|
+
const { y, r } = rmsNorm(layerIn[t]!, nConv);
|
|
214
|
+
normedConv.push(y);
|
|
215
|
+
rmsConv.push(r);
|
|
216
|
+
}
|
|
217
|
+
const afterConv: Float32Array[] = [];
|
|
218
|
+
for (let t = 0; t < T; t++) {
|
|
219
|
+
const out = Float32Array.from(layerIn[t]!); // residual base
|
|
220
|
+
for (let c = 0; c < dModel; c++) {
|
|
221
|
+
let acc = 0;
|
|
222
|
+
for (let j = 0; j < convKernel; j++) {
|
|
223
|
+
const ti = t - j;
|
|
224
|
+
if (ti >= 0) acc += ker[c * convKernel + j]! * normedConv[ti]![c]!;
|
|
222
225
|
}
|
|
223
|
-
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
// Pre-norm → MoE channel mixer → residual.
|
|
227
|
-
const rmsMoe: number[] = [];
|
|
228
|
-
const moeCache: MoECacheLike[] = [];
|
|
229
|
-
const afterMoe: Float32Array[] = [];
|
|
230
|
-
for (let t = 0; t < T; t++) {
|
|
231
|
-
const { y, r } = rmsNorm(afterConv[t]!, nMoe);
|
|
232
|
-
rmsMoe.push(r);
|
|
233
|
-
const out = Float32Array.from(afterConv[t]!); // residual base
|
|
234
|
-
const mr = this.moe[l]!.forward(y);
|
|
235
|
-
for (let c = 0; c < dModel; c++) out[c] = out[c]! + mr.output[c]!;
|
|
236
|
-
afterMoe.push(out);
|
|
237
|
-
moeCache.push(mr.cache as unknown as MoECacheLike);
|
|
226
|
+
out[c] = out[c]! + acc;
|
|
238
227
|
}
|
|
228
|
+
afterConv.push(out);
|
|
229
|
+
}
|
|
239
230
|
|
|
240
|
-
|
|
241
|
-
|
|
231
|
+
const rmsMoe: number[] = [];
|
|
232
|
+
const moeCache: MoECacheLike[] = [];
|
|
233
|
+
const afterMoe: Float32Array[] = [];
|
|
234
|
+
for (let t = 0; t < T; t++) {
|
|
235
|
+
const { y, r } = rmsNorm(afterConv[t]!, nMoe);
|
|
236
|
+
rmsMoe.push(r);
|
|
237
|
+
const out = Float32Array.from(afterConv[t]!); // residual base
|
|
238
|
+
const mr = this.moe[l]!.forward(y);
|
|
239
|
+
for (let c = 0; c < dModel; c++) out[c] = out[c]! + mr.output[c]!;
|
|
240
|
+
afterMoe.push(out);
|
|
241
|
+
moeCache.push(mr.cache as unknown as MoECacheLike);
|
|
242
242
|
}
|
|
243
|
+
return { afterMoe, cache: { layerIn, normedConv, rmsConv, afterConv, rmsMoe, moeCache } };
|
|
244
|
+
}
|
|
243
245
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
+
/** Tied output head: logits_t[v] = x_t · emb[v]. */
|
|
247
|
+
private _head(x: Float32Array[]): Float32Array[] {
|
|
248
|
+
const { dModel, vocabSize } = this.config;
|
|
249
|
+
return x.map((xt) => {
|
|
246
250
|
const lg = new Float32Array(vocabSize);
|
|
247
251
|
for (let v = 0; v < vocabSize; v++) {
|
|
248
252
|
let acc = 0;
|
|
@@ -252,34 +256,39 @@ export class EvermindLM {
|
|
|
252
256
|
}
|
|
253
257
|
return lg;
|
|
254
258
|
});
|
|
259
|
+
}
|
|
255
260
|
|
|
256
|
-
|
|
261
|
+
/** Run the model over a token sequence; returns per-position logits + a cache. */
|
|
262
|
+
forward(tokens: number[]): { logits: Float32Array[]; cache: ForwardCache } {
|
|
263
|
+
let x = this._embed(tokens);
|
|
264
|
+
const layers: LayerCache[] = [];
|
|
265
|
+
for (let l = 0; l < this.config.numLayers; l++) {
|
|
266
|
+
const { afterMoe, cache } = this._forwardLayer(l, x);
|
|
267
|
+
layers.push(cache);
|
|
268
|
+
x = afterMoe;
|
|
269
|
+
}
|
|
270
|
+
return { logits: this._head(x), cache: { tokens, layers, finalX: x } };
|
|
257
271
|
}
|
|
258
272
|
|
|
259
273
|
// ── Loss + backward ──────────────────────────────────────────────────────────
|
|
260
274
|
|
|
261
275
|
/**
|
|
262
|
-
*
|
|
263
|
-
*
|
|
264
|
-
*
|
|
276
|
+
* Head + tied-embedding gradient. Accumulates dL/d(head→emb) into gEmb and
|
|
277
|
+
* returns the mean next-token loss plus dL/d(finalX). Shared by the full and
|
|
278
|
+
* checkpointed backward paths so the head maths lives in one place.
|
|
265
279
|
*/
|
|
266
|
-
|
|
267
|
-
const { dModel,
|
|
280
|
+
private _headBackward(tokens: number[], logits: Float32Array[], finalX: Float32Array[]): { loss: number; dX: Float32Array[] } {
|
|
281
|
+
const { dModel, vocabSize } = this.config;
|
|
268
282
|
const T = tokens.length;
|
|
269
|
-
if (T < 2) return 0;
|
|
270
|
-
const { logits, cache } = this.forward(tokens);
|
|
271
|
-
|
|
272
283
|
const predPositions = T - 1; // positions 0..T-2 predict the next token
|
|
273
284
|
const inv = 1 / predPositions;
|
|
274
|
-
|
|
275
|
-
// dL/d(finalX_t) and head gradient into the tied embedding.
|
|
276
285
|
const dX: Float32Array[] = Array.from({ length: T }, () => new Float32Array(dModel));
|
|
277
286
|
let loss = 0;
|
|
278
287
|
for (let t = 0; t < predPositions; t++) {
|
|
279
288
|
const target = tokens[t + 1]!;
|
|
280
289
|
loss += crossEntropyLoss(logits[t]!, target) * inv;
|
|
281
290
|
const dLogit = crossEntropyGrad(logits[t]!, target); // probs - onehot
|
|
282
|
-
const xt =
|
|
291
|
+
const xt = finalX[t]!;
|
|
283
292
|
for (let v = 0; v < vocabSize; v++) {
|
|
284
293
|
const g = dLogit[v]! * inv;
|
|
285
294
|
if (g === 0) continue;
|
|
@@ -290,59 +299,109 @@ export class EvermindLM {
|
|
|
290
299
|
}
|
|
291
300
|
}
|
|
292
301
|
}
|
|
302
|
+
return { loss, dX };
|
|
303
|
+
}
|
|
293
304
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
}
|
|
305
|
+
/**
|
|
306
|
+
* Backward through one (conv + MoE) block given dL/d(block output) and the
|
|
307
|
+
* block's activation cache. Accumulates conv/norm/MoE gradients and returns
|
|
308
|
+
* dL/d(block input). The inverse of {@link _forwardLayer}.
|
|
309
|
+
*/
|
|
310
|
+
private _backwardLayer(l: number, dOut: Float32Array[], lc: LayerCache): Float32Array[] {
|
|
311
|
+
const { dModel, convKernel } = this.config;
|
|
312
|
+
const T = dOut.length;
|
|
313
|
+
const ker = this.conv[l]!;
|
|
314
|
+
const gker = this.gConv[l]!;
|
|
315
|
+
const nConv = this.nConv[l]!;
|
|
316
|
+
const gNConv = this.gNConv[l]!;
|
|
317
|
+
const nMoe = this.nMoe[l]!;
|
|
318
|
+
const gNMoe = this.gNMoe[l]!;
|
|
319
|
+
|
|
320
|
+
// MoE sub-block: afterMoe = afterConv + MoE(RMSNorm(afterConv, nMoe)).
|
|
321
|
+
const dAfterConv: Float32Array[] = [];
|
|
322
|
+
for (let t = 0; t < T; t++) {
|
|
323
|
+
const dMoeNormed = this.moe[l]!.backward(dOut[t]!, lc.moeCache[t] as never);
|
|
324
|
+
const { dx, dgain } = rmsNormBackward(dMoeNormed, lc.afterConv[t]!, lc.rmsMoe[t]!, nMoe);
|
|
325
|
+
for (let c = 0; c < dModel; c++) gNMoe[c] = gNMoe[c]! + dgain[c]!;
|
|
326
|
+
const d = Float32Array.from(dOut[t]!); // residual passthrough
|
|
327
|
+
for (let c = 0; c < dModel; c++) d[c] = d[c]! + dx[c]!;
|
|
328
|
+
dAfterConv.push(d);
|
|
329
|
+
}
|
|
314
330
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
}
|
|
331
|
+
// Conv sub-block: afterConv = layerIn + conv(RMSNorm(layerIn, nConv)).
|
|
332
|
+
const dNormedConv: Float32Array[] = Array.from({ length: T }, () => new Float32Array(dModel));
|
|
333
|
+
const dLayerIn: Float32Array[] = dAfterConv.map((v) => Float32Array.from(v)); // residual passthrough
|
|
334
|
+
for (let t = 0; t < T; t++) {
|
|
335
|
+
for (let c = 0; c < dModel; c++) {
|
|
336
|
+
const dmix = dAfterConv[t]![c]!;
|
|
337
|
+
if (dmix === 0) continue;
|
|
338
|
+
for (let j = 0; j < convKernel; j++) {
|
|
339
|
+
const ti = t - j;
|
|
340
|
+
if (ti < 0) continue;
|
|
341
|
+
gker[c * convKernel + j] = gker[c * convKernel + j]! + dmix * lc.normedConv[ti]![c]!;
|
|
342
|
+
dNormedConv[ti]![c] = dNormedConv[ti]![c]! + dmix * ker[c * convKernel + j]!;
|
|
328
343
|
}
|
|
329
344
|
}
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
345
|
+
}
|
|
346
|
+
for (let t = 0; t < T; t++) {
|
|
347
|
+
const { dx, dgain } = rmsNormBackward(dNormedConv[t]!, lc.layerIn[t]!, lc.rmsConv[t]!, nConv);
|
|
348
|
+
for (let c = 0; c < dModel; c++) {
|
|
349
|
+
gNConv[c] = gNConv[c]! + dgain[c]!;
|
|
350
|
+
dLayerIn[t]![c] = dLayerIn[t]![c]! + dx[c]!;
|
|
336
351
|
}
|
|
337
|
-
for (let t = 0; t < T; t++) dX[t] = dLayerIn[t]!;
|
|
338
352
|
}
|
|
353
|
+
return dLayerIn;
|
|
354
|
+
}
|
|
339
355
|
|
|
340
|
-
|
|
341
|
-
|
|
356
|
+
/** Embedding lookup: dL/d(layer-0 input) flows into the row for each token. */
|
|
357
|
+
private _embedBackward(tokens: number[], dIn: Float32Array[]): void {
|
|
358
|
+
const { dModel } = this.config;
|
|
359
|
+
for (let t = 0; t < tokens.length; t++) {
|
|
342
360
|
const off = tokens[t]! * dModel;
|
|
343
|
-
for (let c = 0; c < dModel; c++) this.gEmb[off + c] = this.gEmb[off + c]! +
|
|
361
|
+
for (let c = 0; c < dModel; c++) this.gEmb[off + c] = this.gEmb[off + c]! + dIn[t]![c]!;
|
|
344
362
|
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Next-token cross-entropy over the sequence (predict tokens[t+1] from
|
|
367
|
+
* position t), accumulating exact gradients. Returns the mean loss. Call
|
|
368
|
+
* {@link zeroGrad} before and an optimiser step after.
|
|
369
|
+
*/
|
|
370
|
+
lossAndBackward(tokens: number[]): number {
|
|
371
|
+
if (tokens.length < 2) return 0;
|
|
372
|
+
const { logits, cache } = this.forward(tokens);
|
|
373
|
+
const { loss, dX } = this._headBackward(tokens, logits, cache.finalX);
|
|
374
|
+
let d = dX;
|
|
375
|
+
for (let l = this.config.numLayers - 1; l >= 0; l--) d = this._backwardLayer(l, d, cache.layers[l]!);
|
|
376
|
+
this._embedBackward(tokens, d);
|
|
377
|
+
return loss;
|
|
378
|
+
}
|
|
345
379
|
|
|
380
|
+
/**
|
|
381
|
+
* Activation-checkpointed backward — numerically identical gradients to
|
|
382
|
+
* {@link lossAndBackward}, but retains only the per-LAYER inputs during the
|
|
383
|
+
* forward instead of every layer's full activation cache. Each layer's
|
|
384
|
+
* activations are RECOMPUTED (a cheap extra forward) when its backward runs,
|
|
385
|
+
* so peak activation memory is one layer's cache, not all of them. This is the
|
|
386
|
+
* memory-for-compute trade the cookbook pairs with FSDP to fit longer
|
|
387
|
+
* sequences / bigger models on a constrained device.
|
|
388
|
+
*/
|
|
389
|
+
lossAndBackwardCheckpointed(tokens: number[]): number {
|
|
390
|
+
if (tokens.length < 2) return 0;
|
|
391
|
+
let x = this._embed(tokens);
|
|
392
|
+
const layerInputs: Float32Array[][] = [];
|
|
393
|
+
for (let l = 0; l < this.config.numLayers; l++) {
|
|
394
|
+
layerInputs.push(x); // keep only the input; drop the cache
|
|
395
|
+
x = this._forwardLayer(l, x).afterMoe;
|
|
396
|
+
}
|
|
397
|
+
const logits = this._head(x);
|
|
398
|
+
const { loss, dX } = this._headBackward(tokens, logits, x);
|
|
399
|
+
let d = dX;
|
|
400
|
+
for (let l = this.config.numLayers - 1; l >= 0; l--) {
|
|
401
|
+
const { cache } = this._forwardLayer(l, layerInputs[l]!); // recompute this layer's activations
|
|
402
|
+
d = this._backwardLayer(l, d, cache);
|
|
403
|
+
}
|
|
404
|
+
this._embedBackward(tokens, d);
|
|
346
405
|
return loss;
|
|
347
406
|
}
|
|
348
407
|
|
|
@@ -513,29 +572,114 @@ export class EvermindLM {
|
|
|
513
572
|
}
|
|
514
573
|
}
|
|
515
574
|
|
|
575
|
+
export interface EvermindLMTrainOptions extends AdamWOptions {
|
|
576
|
+
epochs?: number;
|
|
577
|
+
/**
|
|
578
|
+
* Gradient accumulation: average gradients over this many sequences (micro-
|
|
579
|
+
* batches) before each optimiser step, for a larger effective batch on a
|
|
580
|
+
* memory-constrained device. Default 1.
|
|
581
|
+
*/
|
|
582
|
+
accumSteps?: number;
|
|
583
|
+
/**
|
|
584
|
+
* Use activation checkpointing (recompute layer activations in backward) to
|
|
585
|
+
* cap peak activation memory. Identical gradients, a little extra compute.
|
|
586
|
+
* Default false.
|
|
587
|
+
*/
|
|
588
|
+
checkpoint?: boolean;
|
|
589
|
+
/**
|
|
590
|
+
* Mixed-precision training: fp16-rounded gradients with dynamic loss scaling
|
|
591
|
+
* over fp32 master weights (the model params). Overflowing steps are skipped
|
|
592
|
+
* and the scale backs off. Default false.
|
|
593
|
+
*/
|
|
594
|
+
mixedPrecision?: boolean | LossScalerOptions;
|
|
595
|
+
}
|
|
596
|
+
|
|
516
597
|
/** Minimal sequence trainer: AdamW over next-token cross-entropy. */
|
|
517
598
|
export class EvermindLMTrainer {
|
|
518
599
|
private readonly adam: AdamW;
|
|
600
|
+
private readonly scaler: DynamicLossScaler | null;
|
|
601
|
+
private readonly grads: { data: Float32Array }[];
|
|
519
602
|
constructor(
|
|
520
603
|
private readonly model: EvermindLM,
|
|
521
|
-
private readonly opts:
|
|
604
|
+
private readonly opts: EvermindLMTrainOptions = {},
|
|
522
605
|
) {
|
|
523
606
|
this.adam = new AdamW(model, opts);
|
|
607
|
+
this.grads = model.gradients();
|
|
608
|
+
this.scaler = opts.mixedPrecision
|
|
609
|
+
? new DynamicLossScaler(typeof opts.mixedPrecision === "object" ? opts.mixedPrecision : {})
|
|
610
|
+
: null;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/** The dynamic loss scaler (mixed-precision mode only) — exposes scale/overflow stats. */
|
|
614
|
+
get lossScaler(): DynamicLossScaler | null {
|
|
615
|
+
return this.scaler;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
private _backward(seq: number[]): number {
|
|
619
|
+
return this.opts.checkpoint ? this.model.lossAndBackwardCheckpointed(seq) : this.model.lossAndBackward(seq);
|
|
524
620
|
}
|
|
621
|
+
|
|
622
|
+
/** Divide accumulated gradients by `n` (accumulation averaging), in place. */
|
|
623
|
+
private _scaleGrads(n: number): void {
|
|
624
|
+
if (n === 1) return;
|
|
625
|
+
const inv = 1 / n;
|
|
626
|
+
for (const g of this.grads) for (let i = 0; i < g.data.length; i++) g.data[i] = g.data[i]! * inv;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/** Round accumulated gradients to fp16 precision (mixed-precision simulation), in place. */
|
|
630
|
+
private _fp16Grads(): void {
|
|
631
|
+
for (const g of this.grads) for (let i = 0; i < g.data.length; i++) g.data[i] = roundFp16(g.data[i]!);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
/** Multiply accumulated gradients by `s`, in place. */
|
|
635
|
+
private _mulGrads(s: number): void {
|
|
636
|
+
if (s === 1) return;
|
|
637
|
+
for (const g of this.grads) for (let i = 0; i < g.data.length; i++) g.data[i] = g.data[i]! * s;
|
|
638
|
+
}
|
|
639
|
+
|
|
525
640
|
/** Train on a set of token sequences; returns per-epoch mean loss. */
|
|
526
641
|
fit(sequences: number[][]): number[] {
|
|
527
642
|
const epochs = this.opts.epochs ?? 1;
|
|
643
|
+
const accum = Math.max(1, this.opts.accumSteps ?? 1);
|
|
528
644
|
const history: number[] = [];
|
|
529
645
|
for (let e = 0; e < epochs; e++) {
|
|
530
646
|
let total = 0;
|
|
531
647
|
let n = 0;
|
|
648
|
+
let pending = 0;
|
|
649
|
+
// The scale is fixed for the duration of one accumulation window, so scale-up
|
|
650
|
+
// (per sequence) and unscale (at flush) always use the SAME factor even as the
|
|
651
|
+
// controller grows/backs it off between windows.
|
|
652
|
+
let windowScale = this.scaler ? this.scaler.scale : 1;
|
|
653
|
+
this.model.zeroGrad();
|
|
654
|
+
const flush = () => {
|
|
655
|
+
if (pending === 0) return;
|
|
656
|
+
this._scaleGrads(pending); // accumulation averaging
|
|
657
|
+
if (this.scaler) {
|
|
658
|
+
this._fp16Grads(); // half-precision gradients
|
|
659
|
+
const overflow = this.scaler.check(this.grads);
|
|
660
|
+
if (this.scaler.update(overflow)) {
|
|
661
|
+
this._mulGrads(1 / windowScale); // unscale by the SAME factor we scaled by
|
|
662
|
+
this.adam.step();
|
|
663
|
+
} // else: skip the step, scale has backed off
|
|
664
|
+
windowScale = this.scaler.scale; // next window uses the updated scale
|
|
665
|
+
} else {
|
|
666
|
+
this.adam.step();
|
|
667
|
+
}
|
|
668
|
+
this.model.zeroGrad();
|
|
669
|
+
pending = 0;
|
|
670
|
+
};
|
|
532
671
|
for (const seq of sequences) {
|
|
533
672
|
if (seq.length < 2) continue;
|
|
534
|
-
this.
|
|
535
|
-
|
|
536
|
-
|
|
673
|
+
if (pending === 0 && this.scaler) windowScale = this.scaler.scale;
|
|
674
|
+
const loss = this._backward(seq);
|
|
675
|
+
// Emulate scaling the loss before backward: the gradient is linear in the
|
|
676
|
+
// loss, so scaling the just-produced gradients by the loss scale is equivalent.
|
|
677
|
+
this._mulGrads(windowScale);
|
|
678
|
+
total += loss;
|
|
537
679
|
n++;
|
|
680
|
+
if (++pending >= accum) flush();
|
|
538
681
|
}
|
|
682
|
+
flush();
|
|
539
683
|
history.push(n > 0 ? total / n : 0);
|
|
540
684
|
}
|
|
541
685
|
return history;
|
package/src/lm/index.ts
CHANGED
|
@@ -3,4 +3,10 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
export { EvermindLM, EvermindLMTrainer, DEFAULT_LM_CONFIG, DEFAULT_LM_SEED } from "./evermind_lm.js";
|
|
6
|
-
export type { EvermindLMConfig, LMGenerateOptions, TextCodec } from "./evermind_lm.js";
|
|
6
|
+
export type { EvermindLMConfig, LMGenerateOptions, TextCodec, EvermindLMTrainOptions } from "./evermind_lm.js";
|
|
7
|
+
|
|
8
|
+
// PEFT / efficient-training toolkit (LoRA, QLoRA, mixed precision).
|
|
9
|
+
export { LoRAAdapter, EvermindLMLoRA, quantizeBase } from "../training/lora.js";
|
|
10
|
+
export type { LoRAConfig, LoRAFitOptions, BaseQuant } from "../training/lora.js";
|
|
11
|
+
export { DynamicLossScaler, roundFp16, fp16View } from "../training/mixed_precision.js";
|
|
12
|
+
export type { LossScalerOptions } from "../training/mixed_precision.js";
|
package/src/optim/adamw.ts
CHANGED
|
@@ -15,19 +15,42 @@ export interface OptimTarget {
|
|
|
15
15
|
gradients(): OptimParam[];
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Optimizer-state sharding (ZeRO-1 analog). When set, this optimiser instance
|
|
20
|
+
* OWNS only parameter tensors where `paramIndex % count === index`, and only
|
|
21
|
+
* allocates the AdamW moments (m, v — 2× the parameter bytes, the dominant cost
|
|
22
|
+
* of full fine-tuning per the cookbook's memory accounting) for its shard.
|
|
23
|
+
*
|
|
24
|
+
* Run `count` instances over the SAME model, one per `index`, and the union of
|
|
25
|
+
* their steps equals one unsharded step — but each holds only 1/count of the
|
|
26
|
+
* optimizer state. That is exactly how FSDP/ZeRO fits a larger model on the same
|
|
27
|
+
* hardware; here it is the seam a multi-device trainer partitions on, and even
|
|
28
|
+
* single-process it caps resident moment memory.
|
|
29
|
+
*/
|
|
30
|
+
export interface ShardSpec {
|
|
31
|
+
/** This shard's rank, 0-based. */
|
|
32
|
+
index: number;
|
|
33
|
+
/** Total number of shards. */
|
|
34
|
+
count: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
18
37
|
export interface AdamWOptions {
|
|
19
38
|
lr?: number;
|
|
20
39
|
beta1?: number;
|
|
21
40
|
beta2?: number;
|
|
22
41
|
eps?: number;
|
|
23
42
|
weightDecay?: number;
|
|
43
|
+
/** Optimizer-state sharding. Omit for the full (single-owner) optimiser. */
|
|
44
|
+
shard?: ShardSpec;
|
|
24
45
|
}
|
|
25
46
|
|
|
26
47
|
export class AdamW {
|
|
27
|
-
|
|
28
|
-
private readonly
|
|
48
|
+
// Per-parameter moments, or null for tensors this shard does not own (no memory).
|
|
49
|
+
private readonly m: (Float32Array | null)[] = [];
|
|
50
|
+
private readonly v: (Float32Array | null)[] = [];
|
|
29
51
|
private t = 0;
|
|
30
|
-
private readonly opt: Required<AdamWOptions
|
|
52
|
+
private readonly opt: Required<Omit<AdamWOptions, "shard">>;
|
|
53
|
+
private readonly shard: ShardSpec | null;
|
|
31
54
|
|
|
32
55
|
constructor(
|
|
33
56
|
private readonly target: OptimTarget,
|
|
@@ -40,13 +63,29 @@ export class AdamW {
|
|
|
40
63
|
eps: options.eps ?? 1e-8,
|
|
41
64
|
weightDecay: options.weightDecay ?? 0,
|
|
42
65
|
};
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
66
|
+
const shard = options.shard ?? null;
|
|
67
|
+
if (shard && (shard.count <= 0 || shard.index < 0 || shard.index >= shard.count)) {
|
|
68
|
+
throw new Error(`AdamW: invalid shard ${shard.index}/${shard.count}`);
|
|
69
|
+
}
|
|
70
|
+
this.shard = shard;
|
|
71
|
+
const params = target.parameters();
|
|
72
|
+
for (let p = 0; p < params.length; p++) {
|
|
73
|
+
if (this._owns(p)) {
|
|
74
|
+
this.m.push(new Float32Array(params[p]!.data.length));
|
|
75
|
+
this.v.push(new Float32Array(params[p]!.data.length));
|
|
76
|
+
} else {
|
|
77
|
+
this.m.push(null);
|
|
78
|
+
this.v.push(null);
|
|
79
|
+
}
|
|
46
80
|
}
|
|
47
81
|
}
|
|
48
82
|
|
|
49
|
-
/**
|
|
83
|
+
/** Whether this shard owns (and updates) parameter tensor `p`. */
|
|
84
|
+
private _owns(p: number): boolean {
|
|
85
|
+
return this.shard === null || p % this.shard.count === this.shard.index;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** One optimiser step from the currently-accumulated gradients (owned tensors only). */
|
|
50
89
|
step(): void {
|
|
51
90
|
this.t++;
|
|
52
91
|
const { lr, beta1, beta2, eps, weightDecay } = this.opt;
|
|
@@ -55,6 +94,7 @@ export class AdamW {
|
|
|
55
94
|
const bc1 = 1 - Math.pow(beta1, this.t);
|
|
56
95
|
const bc2 = 1 - Math.pow(beta2, this.t);
|
|
57
96
|
for (let p = 0; p < params.length; p++) {
|
|
97
|
+
if (!this._owns(p)) continue;
|
|
58
98
|
const w = params[p]!.data;
|
|
59
99
|
const g = grads[p]!.data;
|
|
60
100
|
const m = this.m[p]!;
|
|
@@ -69,4 +109,12 @@ export class AdamW {
|
|
|
69
109
|
}
|
|
70
110
|
}
|
|
71
111
|
}
|
|
112
|
+
|
|
113
|
+
/** Bytes of optimizer state this shard holds (m + v) — 1/count of the full state. */
|
|
114
|
+
stateBytes(): number {
|
|
115
|
+
let n = 0;
|
|
116
|
+
for (const arr of this.m) if (arr) n += arr.byteLength;
|
|
117
|
+
for (const arr of this.v) if (arr) n += arr.byteLength;
|
|
118
|
+
return n;
|
|
119
|
+
}
|
|
72
120
|
}
|