@seanhogg/builderforce-memory-engine 2026.7.11 → 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.
@@ -0,0 +1,67 @@
1
+ /**
2
+ * mixed_precision.ts — mixed-precision training discipline (AMP) for the CPU
3
+ * reference, faithful to the cookbook's recipe:
4
+ *
5
+ * • fp16 compute, fp32 MASTER weights. The model's Float32Array params ARE the
6
+ * master copy; the forward runs on an fp16-rounded view, so tiny weight
7
+ * updates still accumulate in fp32 instead of vanishing.
8
+ * • Dynamic LOSS SCALING. fp16 gradients underflow to zero below ~6e-5; scaling
9
+ * the loss by a large factor S lifts them into fp16 range, then we unscale by
10
+ * S before the optimiser step. On overflow (inf/nan) we skip the step and back
11
+ * the scale off; after a clean streak we grow it. This is the mechanism that
12
+ * makes fp16 training numerically stable.
13
+ *
14
+ * The payoff is memory/throughput: activations and gradients at half precision.
15
+ * On CPU we can't reclaim the bytes, so this module models the NUMERICS exactly
16
+ * (fp16 rounding + loss scaling) — the property the WGSL path will inherit — and
17
+ * its tests prove a small gradient survives WITH scaling and underflows WITHOUT.
18
+ */
19
+ /** Round a value to what fp16 storage would preserve (half-precision simulation). */
20
+ export declare function roundFp16(x: number): number;
21
+ /** Round every element of a copy to fp16 precision (leaves the input untouched). */
22
+ export declare function fp16View(a: Float32Array): Float32Array;
23
+ export interface LossScalerOptions {
24
+ /** Initial scale. Default 2^14 = 16384. */
25
+ initScale?: number;
26
+ /** Multiply the scale by this after `growthInterval` clean steps. Default 2. */
27
+ growthFactor?: number;
28
+ /** Multiply the scale by this on overflow. Default 0.5. */
29
+ backoffFactor?: number;
30
+ /** Clean steps between growth attempts. Default 200. */
31
+ growthInterval?: number;
32
+ /** Floor for the scale so it can't collapse to zero. Default 1. */
33
+ minScale?: number;
34
+ }
35
+ /**
36
+ * Dynamic loss scaler — the standard AMP controller. Scale the loss/grads up
37
+ * before backward, {@link check} them for overflow, {@link unscale} the good ones,
38
+ * and {@link update} the scale based on whether this step overflowed.
39
+ */
40
+ export declare class DynamicLossScaler {
41
+ private _scale;
42
+ private readonly growthFactor;
43
+ private readonly backoffFactor;
44
+ private readonly growthInterval;
45
+ private readonly minScale;
46
+ private cleanStreak;
47
+ private _overflows;
48
+ private _skipped;
49
+ constructor(opts?: LossScalerOptions);
50
+ get scale(): number;
51
+ get overflows(): number;
52
+ get skipped(): number;
53
+ /** True if any gradient is inf/nan (an fp16 overflow) — such a step must be skipped. */
54
+ check(grads: {
55
+ data: Float32Array;
56
+ }[]): boolean;
57
+ /** Divide gradients back down by the current scale, in place. */
58
+ unscale(grads: {
59
+ data: Float32Array;
60
+ }[]): void;
61
+ /**
62
+ * Advance the scale controller. Pass whether this step overflowed. Returns
63
+ * `true` if the optimiser step should PROCEED, `false` if it must be skipped.
64
+ */
65
+ update(overflow: boolean): boolean;
66
+ }
67
+ //# sourceMappingURL=mixed_precision.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mixed_precision.d.ts","sourceRoot":"","sources":["../../src/training/mixed_precision.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAIH,qFAAqF;AACrF,wBAAgB,SAAS,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAE3C;AAED,oFAAoF;AACpF,wBAAgB,QAAQ,CAAC,CAAC,EAAE,YAAY,GAAG,YAAY,CAItD;AAED,MAAM,WAAW,iBAAiB;IAChC,2CAA2C;IAC3C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,gFAAgF;IAChF,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,2DAA2D;IAC3D,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,wDAAwD;IACxD,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,mEAAmE;IACnE,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;;GAIG;AACH,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IACtC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAS;IACvC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAS;IACxC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAS;IAClC,OAAO,CAAC,WAAW,CAAK;IACxB,OAAO,CAAC,UAAU,CAAK;IACvB,OAAO,CAAC,QAAQ,CAAK;gBAET,IAAI,GAAE,iBAAsB;IAQxC,IAAI,KAAK,IAAI,MAAM,CAElB;IACD,IAAI,SAAS,IAAI,MAAM,CAEtB;IACD,IAAI,OAAO,IAAI,MAAM,CAEpB;IAED,wFAAwF;IACxF,KAAK,CAAC,KAAK,EAAE;QAAE,IAAI,EAAE,YAAY,CAAA;KAAE,EAAE,GAAG,OAAO;IAW/C,iEAAiE;IACjE,OAAO,CAAC,KAAK,EAAE;QAAE,IAAI,EAAE,YAAY,CAAA;KAAE,EAAE,GAAG,IAAI;IAQ9C;;;OAGG;IACH,MAAM,CAAC,QAAQ,EAAE,OAAO,GAAG,OAAO;CAcnC"}
@@ -0,0 +1,101 @@
1
+ /**
2
+ * mixed_precision.ts — mixed-precision training discipline (AMP) for the CPU
3
+ * reference, faithful to the cookbook's recipe:
4
+ *
5
+ * • fp16 compute, fp32 MASTER weights. The model's Float32Array params ARE the
6
+ * master copy; the forward runs on an fp16-rounded view, so tiny weight
7
+ * updates still accumulate in fp32 instead of vanishing.
8
+ * • Dynamic LOSS SCALING. fp16 gradients underflow to zero below ~6e-5; scaling
9
+ * the loss by a large factor S lifts them into fp16 range, then we unscale by
10
+ * S before the optimiser step. On overflow (inf/nan) we skip the step and back
11
+ * the scale off; after a clean streak we grow it. This is the mechanism that
12
+ * makes fp16 training numerically stable.
13
+ *
14
+ * The payoff is memory/throughput: activations and gradients at half precision.
15
+ * On CPU we can't reclaim the bytes, so this module models the NUMERICS exactly
16
+ * (fp16 rounding + loss scaling) — the property the WGSL path will inherit — and
17
+ * its tests prove a small gradient survives WITH scaling and underflows WITHOUT.
18
+ */
19
+ import { floatToFp16, fp16ToFloat } from "../utils/quantization.js";
20
+ /** Round a value to what fp16 storage would preserve (half-precision simulation). */
21
+ export function roundFp16(x) {
22
+ return fp16ToFloat(floatToFp16(x));
23
+ }
24
+ /** Round every element of a copy to fp16 precision (leaves the input untouched). */
25
+ export function fp16View(a) {
26
+ const out = new Float32Array(a.length);
27
+ for (let i = 0; i < a.length; i++)
28
+ out[i] = roundFp16(a[i]);
29
+ return out;
30
+ }
31
+ /**
32
+ * Dynamic loss scaler — the standard AMP controller. Scale the loss/grads up
33
+ * before backward, {@link check} them for overflow, {@link unscale} the good ones,
34
+ * and {@link update} the scale based on whether this step overflowed.
35
+ */
36
+ export class DynamicLossScaler {
37
+ _scale;
38
+ growthFactor;
39
+ backoffFactor;
40
+ growthInterval;
41
+ minScale;
42
+ cleanStreak = 0;
43
+ _overflows = 0;
44
+ _skipped = 0;
45
+ constructor(opts = {}) {
46
+ this._scale = opts.initScale ?? 16384;
47
+ this.growthFactor = opts.growthFactor ?? 2;
48
+ this.backoffFactor = opts.backoffFactor ?? 0.5;
49
+ this.growthInterval = opts.growthInterval ?? 200;
50
+ this.minScale = opts.minScale ?? 1;
51
+ }
52
+ get scale() {
53
+ return this._scale;
54
+ }
55
+ get overflows() {
56
+ return this._overflows;
57
+ }
58
+ get skipped() {
59
+ return this._skipped;
60
+ }
61
+ /** True if any gradient is inf/nan (an fp16 overflow) — such a step must be skipped. */
62
+ check(grads) {
63
+ for (const g of grads) {
64
+ const a = g.data;
65
+ for (let i = 0; i < a.length; i++) {
66
+ const x = a[i];
67
+ if (!Number.isFinite(x))
68
+ return true;
69
+ }
70
+ }
71
+ return false;
72
+ }
73
+ /** Divide gradients back down by the current scale, in place. */
74
+ unscale(grads) {
75
+ const inv = 1 / this._scale;
76
+ for (const g of grads) {
77
+ const a = g.data;
78
+ for (let i = 0; i < a.length; i++)
79
+ a[i] = a[i] * inv;
80
+ }
81
+ }
82
+ /**
83
+ * Advance the scale controller. Pass whether this step overflowed. Returns
84
+ * `true` if the optimiser step should PROCEED, `false` if it must be skipped.
85
+ */
86
+ update(overflow) {
87
+ if (overflow) {
88
+ this._overflows++;
89
+ this._skipped++;
90
+ this._scale = Math.max(this.minScale, this._scale * this.backoffFactor);
91
+ this.cleanStreak = 0;
92
+ return false;
93
+ }
94
+ if (++this.cleanStreak >= this.growthInterval) {
95
+ this._scale *= this.growthFactor;
96
+ this.cleanStreak = 0;
97
+ }
98
+ return true;
99
+ }
100
+ }
101
+ //# sourceMappingURL=mixed_precision.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mixed_precision.js","sourceRoot":"","sources":["../../src/training/mixed_precision.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seanhogg/builderforce-memory-engine",
3
- "version": "2026.7.11",
3
+ "version": "2026.7.14",
4
4
  "description": "BuilderForce Agent Memory — engine layer. TypeScript/WGSL Mamba SSM kernels, model blocks, autograd, training, and BPE tokenizer (zero runtime deps, WebGPU-native).",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.js",
package/src/index.ts CHANGED
@@ -41,9 +41,15 @@ export type {
41
41
 
42
42
  // ── EvermindLM (the generative model) + AdamW ──────────────────────────────────
43
43
  export { EvermindLM, EvermindLMTrainer, DEFAULT_LM_CONFIG, DEFAULT_LM_SEED } from './lm/index.js';
44
- export type { EvermindLMConfig, LMGenerateOptions, TextCodec } from './lm/index.js';
44
+ export type { EvermindLMConfig, LMGenerateOptions, TextCodec, EvermindLMTrainOptions } from './lm/index.js';
45
45
  export { AdamW } from './optim/adamw.js';
46
- export type { AdamWOptions, OptimTarget, OptimParam } from './optim/adamw.js';
46
+ export type { AdamWOptions, OptimTarget, OptimParam, ShardSpec } from './optim/adamw.js';
47
+
48
+ // PEFT / efficient-training toolkit (LoRA, QLoRA, mixed precision, checkpointing).
49
+ export { LoRAAdapter, EvermindLMLoRA, quantizeBase } from './training/lora.js';
50
+ export type { LoRAConfig, LoRAFitOptions, BaseQuant } from './training/lora.js';
51
+ export { DynamicLossScaler, roundFp16, fp16View } from './training/mixed_precision.js';
52
+ export type { LossScalerOptions } from './training/mixed_precision.js';
47
53
 
48
54
  // ── Training ──────────────────────────────────────────────────────────────────
49
55
 
@@ -46,7 +46,10 @@ export interface LimbicTrainOptions {
46
46
  eps?: number;
47
47
  /** Max global gradient L2 norm before the optimiser step. Default 1.0. */
48
48
  maxGradNorm?: number;
49
- onEpochEnd?: ((epoch: number, loss: number) => void) | null;
49
+ /** Per-epoch hook. `gradNorm` is the pre-clip gradient L2 norm this epoch (the
50
+ * instability early-warning) — computed CPU-side so it's always available, unlike
51
+ * the GPU trainer where it's opt-in. */
52
+ onEpochEnd?: ((epoch: number, loss: number, gradNorm?: number) => void) | null;
50
53
  }
51
54
 
52
55
  interface AdamMoment {
@@ -146,7 +149,7 @@ export class LimbicTrainer {
146
149
  for (let i = 0; i < g.data.length; i++) g.data[i]! *= invN;
147
150
  }
148
151
 
149
- this._clipGradients(grads, maxGradNorm);
152
+ const gradNorm = this._clipGradients(grads, maxGradNorm);
150
153
 
151
154
  this._step++;
152
155
  const beta1_t = Math.pow(beta1, this._step);
@@ -160,7 +163,7 @@ export class LimbicTrainer {
160
163
 
161
164
  const avg = epochLoss / samples.length;
162
165
  losses.push(avg);
163
- if (onEpochEnd) onEpochEnd(epoch + 1, avg);
166
+ if (onEpochEnd) onEpochEnd(epoch + 1, avg, gradNorm);
164
167
  }
165
168
  return losses;
166
169
  }
@@ -184,7 +187,8 @@ export class LimbicTrainer {
184
187
  return total / samples.length;
185
188
  }
186
189
 
187
- private _clipGradients(grads: LimbicParam[], maxNorm: number): void {
190
+ /** Clip grads to `maxNorm` in place; returns the PRE-clip L2 norm (the signal). */
191
+ private _clipGradients(grads: LimbicParam[], maxNorm: number): number {
188
192
  let normSq = 0;
189
193
  for (const g of grads) for (let i = 0; i < g.data.length; i++) normSq += g.data[i]! * g.data[i]!;
190
194
  const norm = Math.sqrt(normSq);
@@ -192,6 +196,7 @@ export class LimbicTrainer {
192
196
  const scale = maxNorm / norm;
193
197
  for (const g of grads) for (let i = 0; i < g.data.length; i++) g.data[i]! *= scale;
194
198
  }
199
+ return norm;
195
200
  }
196
201
 
197
202
  private _adamwStepCpu(