@seanhogg/builderforce-memory-engine 2026.6.35 → 2026.7.11
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 +30 -2
- package/dist/codec/evermind_video.d.ts +35 -0
- package/dist/codec/evermind_video.d.ts.map +1 -0
- package/dist/codec/evermind_video.js +37 -0
- package/dist/codec/evermind_video.js.map +1 -0
- package/dist/codec/image_rvq.d.ts +28 -0
- package/dist/codec/image_rvq.d.ts.map +1 -0
- package/dist/codec/image_rvq.js +39 -0
- package/dist/codec/image_rvq.js.map +1 -0
- package/dist/codec/index.d.ts +13 -0
- package/dist/codec/index.d.ts.map +1 -0
- package/dist/codec/index.js +11 -0
- package/dist/codec/index.js.map +1 -0
- package/dist/codec/multimodal_vocab.d.ts +79 -0
- package/dist/codec/multimodal_vocab.d.ts.map +1 -0
- package/dist/codec/multimodal_vocab.js +96 -0
- package/dist/codec/multimodal_vocab.js.map +1 -0
- package/dist/codec/video_rvq.d.ts +116 -0
- package/dist/codec/video_rvq.d.ts.map +1 -0
- package/dist/codec/video_rvq.js +450 -0
- package/dist/codec/video_rvq.js.map +1 -0
- package/dist/import/evermind.d.ts +48 -0
- package/dist/import/evermind.d.ts.map +1 -0
- package/dist/import/evermind.js +111 -0
- package/dist/import/evermind.js.map +1 -0
- package/dist/import/index.d.ts +11 -0
- package/dist/import/index.d.ts.map +1 -0
- package/dist/import/index.js +10 -0
- package/dist/import/index.js.map +1 -0
- package/dist/import/safetensors.d.ts +13 -0
- package/dist/import/safetensors.d.ts.map +1 -0
- package/dist/import/safetensors.js +64 -0
- package/dist/import/safetensors.js.map +1 -0
- package/dist/index.d.ts +8 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/kernels/selective_scan.d.ts.map +1 -1
- package/dist/kernels/selective_scan.js +9 -3
- package/dist/kernels/selective_scan.js.map +1 -1
- package/dist/kernels/ssd.d.ts.map +1 -1
- package/dist/kernels/ssd.js +8 -2
- package/dist/kernels/ssd.js.map +1 -1
- package/dist/kernels/weight_update.d.ts.map +1 -1
- package/dist/kernels/weight_update.js +22 -3
- package/dist/kernels/weight_update.js.map +1 -1
- package/dist/limbic/index.d.ts +2 -2
- package/dist/limbic/index.d.ts.map +1 -1
- package/dist/limbic/index.js +1 -1
- package/dist/limbic/index.js.map +1 -1
- package/dist/limbic/regions.d.ts +31 -0
- package/dist/limbic/regions.d.ts.map +1 -1
- package/dist/limbic/regions.js +34 -0
- package/dist/limbic/regions.js.map +1 -1
- package/dist/model/mamba_model.d.ts +17 -0
- package/dist/model/mamba_model.d.ts.map +1 -1
- package/dist/model/mamba_model.js +27 -0
- package/dist/model/mamba_model.js.map +1 -1
- package/dist/moe/index.d.ts +1 -1
- package/dist/moe/index.d.ts.map +1 -1
- package/dist/moe/moe_package.d.ts +40 -2
- package/dist/moe/moe_package.d.ts.map +1 -1
- package/dist/moe/moe_package.js +90 -7
- package/dist/moe/moe_package.js.map +1 -1
- package/dist/tokenizer/bpe.d.ts +37 -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/trainer.d.ts +34 -2
- package/dist/training/trainer.d.ts.map +1 -1
- package/dist/training/trainer.js +76 -26
- package/dist/training/trainer.js.map +1 -1
- package/dist/utils/quantization.d.ts.map +1 -1
- package/dist/utils/quantization.js +4 -1
- package/dist/utils/quantization.js.map +1 -1
- package/package.json +1 -1
- package/src/codec/evermind_video.ts +55 -0
- package/src/codec/image_rvq.ts +50 -0
- package/src/codec/index.ts +16 -0
- package/src/codec/multimodal_vocab.ts +115 -0
- package/src/codec/video_rvq.ts +496 -0
- package/src/import/evermind.ts +128 -0
- package/src/import/index.ts +11 -0
- package/src/import/safetensors.ts +67 -0
- package/src/index.ts +35 -0
- package/src/kernels/selective_scan.ts +9 -3
- package/src/kernels/ssd.ts +8 -2
- package/src/kernels/weight_update.ts +22 -3
- package/src/limbic/index.ts +2 -1
- package/src/limbic/regions.ts +60 -0
- package/src/model/mamba_model.ts +27 -0
- package/src/moe/index.ts +1 -0
- package/src/moe/moe_package.ts +107 -6
- package/src/tokenizer/bpe.ts +59 -0
- package/src/training/trainer.ts +92 -28
- package/src/utils/quantization.ts +4 -1
package/src/training/trainer.ts
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
createComputePipeline,
|
|
10
10
|
createBindGroup,
|
|
11
11
|
dispatchKernel,
|
|
12
|
+
readBuffer,
|
|
12
13
|
cdiv,
|
|
13
14
|
} from '../utils/gpu_utils.js';
|
|
14
15
|
|
|
@@ -29,9 +30,26 @@ export interface TrainOptions {
|
|
|
29
30
|
beta2?: number;
|
|
30
31
|
eps?: number;
|
|
31
32
|
wsla?: boolean;
|
|
32
|
-
|
|
33
|
+
/** Trust region: max |Δθ| per optimizer step. 0 disables. Defaults to
|
|
34
|
+
* {@link WSLA_MAX_DELTA} in WSLA (write-through) mode, else 0 (full training). */
|
|
35
|
+
maxDelta?: number;
|
|
36
|
+
/** Read back the pre-clip gradient L2 norm each step (the `grad_norm_reduce`
|
|
37
|
+
* kernel already computes it — this just maps it to the CPU). OFF by default: the
|
|
38
|
+
* readback is a GPU sync point that would slow a long finetune. `adapt()` turns it
|
|
39
|
+
* ON so the norm reaches {@link AdaptResult} as an instability signal. */
|
|
40
|
+
trackGradNorm?: boolean;
|
|
41
|
+
onEpochEnd?: ((epoch: number, loss: number, gradNorm?: number) => void) | null;
|
|
33
42
|
}
|
|
34
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Default per-step trust region for WSLA / write-through adaptation. Small
|
|
46
|
+
* enough that a single `adapt()` nudges the narrow params without lurching, so
|
|
47
|
+
* repeated adapts stay stable (and any that regress are cheaply rolled back by
|
|
48
|
+
* the session). Full-training callers pass `maxDelta: 0` (or omit it in
|
|
49
|
+
* non-WSLA mode) for unbounded steps.
|
|
50
|
+
*/
|
|
51
|
+
export const WSLA_MAX_DELTA = 0.05;
|
|
52
|
+
|
|
35
53
|
interface AdamMoments {
|
|
36
54
|
m: GPUBuffer;
|
|
37
55
|
v: GPUBuffer;
|
|
@@ -45,13 +63,22 @@ interface AdamHyperparams {
|
|
|
45
63
|
eps: number;
|
|
46
64
|
beta1_t: number;
|
|
47
65
|
beta2_t: number;
|
|
66
|
+
maxDelta: number;
|
|
48
67
|
}
|
|
49
68
|
|
|
50
69
|
export class MambaTrainer {
|
|
51
70
|
model: HybridMambaModel;
|
|
52
71
|
tokenizer: BPETokenizer | null;
|
|
53
72
|
device: GPUDevice;
|
|
54
|
-
|
|
73
|
+
/**
|
|
74
|
+
* Adam moments keyed by parameter NAME (not array index). Name-keying is what
|
|
75
|
+
* lets WSLA toggle safely: a narrow write-through step updates only the
|
|
76
|
+
* `layer{i}.wXProj/bXProj` subset, a full fine-tune updates everything, and
|
|
77
|
+
* both reuse the SAME `m`/`v` buffers per parameter. Index-keyed moments (the
|
|
78
|
+
* old scheme) silently misaligned the moment with the wrong parameter the
|
|
79
|
+
* moment the trainable set changed shape — corrupting the update.
|
|
80
|
+
*/
|
|
81
|
+
private _moments: Map<string, AdamMoments>;
|
|
55
82
|
private _step: number;
|
|
56
83
|
private _adamwPipeline: GPUComputePipeline;
|
|
57
84
|
private _clipReducePipeline: GPUComputePipeline;
|
|
@@ -62,7 +89,7 @@ export class MambaTrainer {
|
|
|
62
89
|
this.tokenizer = tokenizer;
|
|
63
90
|
this.device = model.device;
|
|
64
91
|
|
|
65
|
-
this._moments =
|
|
92
|
+
this._moments = new Map();
|
|
66
93
|
this._step = 0;
|
|
67
94
|
|
|
68
95
|
this._adamwPipeline = createComputePipeline(this.device, WEIGHT_UPDATE_WGSL, 'adamw_update');
|
|
@@ -70,12 +97,17 @@ export class MambaTrainer {
|
|
|
70
97
|
this._clipScalePipeline = createComputePipeline(this.device, GRAD_CLIP_WGSL, 'grad_clip_scale');
|
|
71
98
|
}
|
|
72
99
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
100
|
+
/** Get-or-create the Adam first/second moments for a parameter, by name. */
|
|
101
|
+
private _momentFor(p: BlockParam): AdamMoments {
|
|
102
|
+
let mom = this._moments.get(p.name);
|
|
103
|
+
if (!mom) {
|
|
104
|
+
mom = {
|
|
105
|
+
m: createEmptyStorageBuffer(this.device, p.numel * 4, false),
|
|
106
|
+
v: createEmptyStorageBuffer(this.device, p.numel * 4, false),
|
|
107
|
+
};
|
|
108
|
+
this._moments.set(p.name, mom);
|
|
109
|
+
}
|
|
110
|
+
return mom;
|
|
79
111
|
}
|
|
80
112
|
|
|
81
113
|
async train(input: string | number[], opts: TrainOptions = {}): Promise<number[]> {
|
|
@@ -90,8 +122,11 @@ export class MambaTrainer {
|
|
|
90
122
|
beta2 = 0.999,
|
|
91
123
|
eps = 1e-8,
|
|
92
124
|
wsla = false,
|
|
125
|
+
trackGradNorm = false,
|
|
93
126
|
onEpochEnd = null,
|
|
94
127
|
} = opts;
|
|
128
|
+
// Trust region defaults ON for write-through (WSLA) adapts, OFF otherwise.
|
|
129
|
+
const maxDelta = opts.maxDelta ?? (wsla ? WSLA_MAX_DELTA : 0);
|
|
95
130
|
|
|
96
131
|
if (wsla) this.model.setWSLAMode(true);
|
|
97
132
|
|
|
@@ -117,27 +152,33 @@ export class MambaTrainer {
|
|
|
117
152
|
throw new Error('Input is too short to form any training chunk.');
|
|
118
153
|
}
|
|
119
154
|
|
|
120
|
-
|
|
155
|
+
// Moments are created lazily per-parameter (name-keyed) on first touch —
|
|
156
|
+
// no upfront allocation needed, and it stays correct as WSLA narrows the
|
|
157
|
+
// trainable set.
|
|
121
158
|
|
|
122
159
|
const epochLosses: number[] = [];
|
|
123
160
|
|
|
124
161
|
for (let epoch = 0; epoch < epochs; epoch++) {
|
|
125
162
|
let epochLoss = 0;
|
|
163
|
+
let epochGradNorm = 0;
|
|
126
164
|
let numSteps = 0;
|
|
127
165
|
|
|
128
166
|
for (const { inputs, targets } of chunks) {
|
|
129
|
-
const loss = await this._trainStep(
|
|
167
|
+
const { loss, gradNorm } = await this._trainStep(
|
|
130
168
|
inputs, targets, batchSize,
|
|
131
|
-
{ learningRate, maxGradNorm, weightDecay, beta1, beta2, eps, wsla }
|
|
169
|
+
{ learningRate, maxGradNorm, weightDecay, beta1, beta2, eps, wsla, maxDelta, trackGradNorm }
|
|
132
170
|
);
|
|
133
171
|
epochLoss += loss;
|
|
172
|
+
if (gradNorm != null) epochGradNorm += gradNorm;
|
|
134
173
|
numSteps++;
|
|
135
174
|
}
|
|
136
175
|
|
|
137
176
|
const avgLoss = epochLoss / numSteps;
|
|
138
177
|
epochLosses.push(avgLoss);
|
|
139
178
|
|
|
140
|
-
|
|
179
|
+
// Mean pre-clip grad norm this epoch (only measured when trackGradNorm is on).
|
|
180
|
+
const avgGradNorm = trackGradNorm ? epochGradNorm / numSteps : undefined;
|
|
181
|
+
if (onEpochEnd) onEpochEnd(epoch + 1, avgLoss, avgGradNorm);
|
|
141
182
|
}
|
|
142
183
|
|
|
143
184
|
if (wsla) this.model.setWSLAMode(false);
|
|
@@ -148,9 +189,9 @@ export class MambaTrainer {
|
|
|
148
189
|
inputs: number[],
|
|
149
190
|
targets: number[],
|
|
150
191
|
batch: number,
|
|
151
|
-
hyperparams: TrainOptions & { learningRate: number; maxGradNorm: number; weightDecay: number; beta1: number; beta2: number; eps: number }
|
|
152
|
-
): Promise<number> {
|
|
153
|
-
const { learningRate, maxGradNorm, weightDecay, beta1, beta2, eps } = hyperparams;
|
|
192
|
+
hyperparams: TrainOptions & { learningRate: number; maxGradNorm: number; weightDecay: number; beta1: number; beta2: number; eps: number; maxDelta: number; trackGradNorm?: boolean }
|
|
193
|
+
): Promise<{ loss: number; gradNorm: number | null }> {
|
|
194
|
+
const { learningRate, maxGradNorm, weightDecay, beta1, beta2, eps, maxDelta, trackGradNorm } = hyperparams;
|
|
154
195
|
|
|
155
196
|
this._step++;
|
|
156
197
|
const seqLen = inputs.length;
|
|
@@ -177,21 +218,24 @@ export class MambaTrainer {
|
|
|
177
218
|
|
|
178
219
|
const dLogitsBuf = createStorageBuffer(this.device, dLogits, false);
|
|
179
220
|
|
|
180
|
-
await this._clipGradients(dLogitsBuf, dLogits.length, maxGradNorm);
|
|
221
|
+
const gradNorm = await this._clipGradients(dLogitsBuf, dLogits.length, maxGradNorm, !!trackGradNorm);
|
|
181
222
|
|
|
182
|
-
|
|
223
|
+
// Only the trainable set is updated. Under WSLA (write-through) that is
|
|
224
|
+
// the narrow per-layer subset and the backbone (incl. every A_log) stays
|
|
225
|
+
// frozen — the guarantee that repeated adapts can't destabilise the SSM.
|
|
226
|
+
const params = this.model.getTrainableParams();
|
|
183
227
|
const beta1_t = Math.pow(beta1, this._step);
|
|
184
228
|
const beta2_t = Math.pow(beta2, this._step);
|
|
185
229
|
|
|
186
230
|
await this._adamwStep(
|
|
187
231
|
params, [dLogitsBuf],
|
|
188
|
-
{ learningRate, weightDecay, beta1, beta2, eps, beta1_t, beta2_t }
|
|
232
|
+
{ learningRate, weightDecay, beta1, beta2, eps, beta1_t, beta2_t, maxDelta }
|
|
189
233
|
);
|
|
190
234
|
|
|
191
235
|
dLogitsBuf.destroy();
|
|
192
236
|
gpuLogits.destroy();
|
|
193
237
|
|
|
194
|
-
return loss;
|
|
238
|
+
return { loss, gradNorm };
|
|
195
239
|
}
|
|
196
240
|
|
|
197
241
|
private async _adamwStep(
|
|
@@ -199,7 +243,7 @@ export class MambaTrainer {
|
|
|
199
243
|
gradBufs: GPUBuffer[],
|
|
200
244
|
hp: AdamHyperparams
|
|
201
245
|
): Promise<void> {
|
|
202
|
-
const { learningRate, weightDecay, beta1, beta2, eps, beta1_t, beta2_t } = hp;
|
|
246
|
+
const { learningRate, weightDecay, beta1, beta2, eps, beta1_t, beta2_t, maxDelta } = hp;
|
|
203
247
|
|
|
204
248
|
for (let i = 0; i < params.length; i++) {
|
|
205
249
|
const p = params[i]!;
|
|
@@ -207,16 +251,17 @@ export class MambaTrainer {
|
|
|
207
251
|
|
|
208
252
|
if (!gradBuf || gradBuf.size < p.numel * 4) continue;
|
|
209
253
|
|
|
254
|
+
const mom = this._momentFor(p);
|
|
210
255
|
const paramsBuf = createUniformBuffer(this.device, packAdamParams(
|
|
211
|
-
p.numel, learningRate, beta1, beta2, eps, weightDecay, beta1_t, beta2_t
|
|
256
|
+
p.numel, learningRate, beta1, beta2, eps, weightDecay, beta1_t, beta2_t, maxDelta
|
|
212
257
|
));
|
|
213
258
|
|
|
214
259
|
const bg = createBindGroup(this.device, this._adamwPipeline, [
|
|
215
260
|
paramsBuf,
|
|
216
261
|
p.buf,
|
|
217
262
|
gradBuf,
|
|
218
|
-
|
|
219
|
-
|
|
263
|
+
mom.m,
|
|
264
|
+
mom.v,
|
|
220
265
|
]);
|
|
221
266
|
|
|
222
267
|
dispatchKernel(this.device, this._adamwPipeline, bg,
|
|
@@ -226,7 +271,14 @@ export class MambaTrainer {
|
|
|
226
271
|
}
|
|
227
272
|
}
|
|
228
273
|
|
|
229
|
-
|
|
274
|
+
/**
|
|
275
|
+
* Clip gradients to `maxNorm` in place. The reduce kernel writes Σg² into
|
|
276
|
+
* `normSqBuf`; the scale kernel then rescales the grads. When `trackNorm` is set
|
|
277
|
+
* we read Σg² back and return the true pre-clip L2 norm (√Σg²) — the kernel
|
|
278
|
+
* already computed it, so this is only a small buffer readback (a GPU sync,
|
|
279
|
+
* hence opt-in). Returns null when not tracking.
|
|
280
|
+
*/
|
|
281
|
+
private async _clipGradients(gradBuf: GPUBuffer, numel: number, maxNorm: number, trackNorm = false): Promise<number | null> {
|
|
230
282
|
const normSqBuf = createEmptyStorageBuffer(this.device, 4, true);
|
|
231
283
|
this.device.queue.writeBuffer(normSqBuf, 0, new Float32Array([0.0]));
|
|
232
284
|
|
|
@@ -240,6 +292,14 @@ export class MambaTrainer {
|
|
|
240
292
|
dispatchKernel(this.device, this._clipReducePipeline, bg1,
|
|
241
293
|
[cdiv(numel, 256), 1, 1]);
|
|
242
294
|
|
|
295
|
+
// Read Σg² AFTER the reduce (the scale kernel only reads it, never overwrites),
|
|
296
|
+
// so the returned norm is the pre-clip magnitude — the instability signal.
|
|
297
|
+
let gradNorm: number | null = null;
|
|
298
|
+
if (trackNorm) {
|
|
299
|
+
const out = await readBuffer(this.device, normSqBuf, 4);
|
|
300
|
+
gradNorm = Math.sqrt(Math.max(0, out[0] ?? 0));
|
|
301
|
+
}
|
|
302
|
+
|
|
243
303
|
const bg2 = createBindGroup(this.device, this._clipScalePipeline,
|
|
244
304
|
[pBuf, gradBuf, normSqBuf]);
|
|
245
305
|
dispatchKernel(this.device, this._clipScalePipeline, bg2,
|
|
@@ -247,6 +307,7 @@ export class MambaTrainer {
|
|
|
247
307
|
|
|
248
308
|
pBuf.destroy();
|
|
249
309
|
normSqBuf.destroy();
|
|
310
|
+
return gradNorm;
|
|
250
311
|
}
|
|
251
312
|
|
|
252
313
|
async evaluate(input: string | number[]): Promise<number> {
|
|
@@ -300,10 +361,13 @@ function buildChunks(ids: number[], seqLen: number): Array<{inputs: number[], ta
|
|
|
300
361
|
|
|
301
362
|
function packAdamParams(
|
|
302
363
|
numElements: number, lr: number, beta1: number, beta2: number,
|
|
303
|
-
eps: number, weightDecay: number, beta1_t: number, beta2_t: number
|
|
364
|
+
eps: number, weightDecay: number, beta1_t: number, beta2_t: number,
|
|
365
|
+
maxDelta: number
|
|
304
366
|
): ArrayBuffer {
|
|
305
|
-
|
|
367
|
+
// 48 bytes: u32 + 8×f32 = 36, padded to a 16-byte multiple for the uniform
|
|
368
|
+
// layout (matches AdamParams' _pad0.._pad2 in WEIGHT_UPDATE_WGSL).
|
|
369
|
+
const buf = new ArrayBuffer(48);
|
|
306
370
|
new Uint32Array(buf, 0, 1).set([numElements]);
|
|
307
|
-
new Float32Array(buf, 4,
|
|
371
|
+
new Float32Array(buf, 4, 8).set([lr, beta1, beta2, eps, weightDecay, beta1_t, beta2_t, maxDelta]);
|
|
308
372
|
return buf;
|
|
309
373
|
}
|
|
@@ -54,7 +54,10 @@ export function fp16ToFloat(val: number): number {
|
|
|
54
54
|
const mantissa = val & 0x3FF;
|
|
55
55
|
|
|
56
56
|
if (exponent === 0) {
|
|
57
|
-
|
|
57
|
+
// Subnormal: value = mantissa/1024 × 2^-14 (no implicit leading 1, min
|
|
58
|
+
// exponent -14). The 2^-14 scale was missing, so subnormals (|x| < ~6.1e-5)
|
|
59
|
+
// decoded ~16384× too large — corrupting every fp16 round-trip of tiny weights.
|
|
60
|
+
const f = (mantissa / 1024.0) * Math.pow(2, -14);
|
|
58
61
|
return sign ? -f : f;
|
|
59
62
|
}
|
|
60
63
|
|