@seanhogg/builderforce-memory-engine 2026.10.1 → 2026.10.3
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/evermind/adapt.d.ts +7 -0
- package/dist/evermind/adapt.d.ts.map +1 -1
- package/dist/evermind/adapt.js +1 -1
- package/dist/evermind/adapt.js.map +1 -1
- package/dist/evermind/delta_wire.d.ts +16 -1
- package/dist/evermind/delta_wire.d.ts.map +1 -1
- package/dist/evermind/delta_wire.js +22 -2
- package/dist/evermind/delta_wire.js.map +1 -1
- package/dist/evermind/index.d.ts +2 -2
- package/dist/evermind/index.d.ts.map +1 -1
- package/dist/evermind/index.js +1 -1
- package/dist/evermind/index.js.map +1 -1
- package/dist/evermind/merge.d.ts +5 -2
- package/dist/evermind/merge.d.ts.map +1 -1
- package/dist/evermind/merge.js +5 -2
- package/dist/evermind/merge.js.map +1 -1
- package/dist/evermind/ml_head.d.ts +28 -0
- package/dist/evermind/ml_head.d.ts.map +1 -0
- package/dist/evermind/ml_head.js +47 -0
- package/dist/evermind/ml_head.js.map +1 -0
- package/dist/evermind/package.d.ts +2 -2
- package/dist/evermind/package.d.ts.map +1 -1
- package/dist/evermind/package.js +1 -1
- package/dist/evermind/package.js.map +1 -1
- package/dist/experience/index.d.ts +2 -2
- package/dist/experience/index.d.ts.map +1 -1
- package/dist/experience/index.js.map +1 -1
- package/dist/experience/store.d.ts +19 -1
- package/dist/experience/store.d.ts.map +1 -1
- package/dist/experience/store.js +14 -2
- package/dist/experience/store.js.map +1 -1
- package/dist/experience/types.d.ts +18 -0
- package/dist/experience/types.d.ts.map +1 -1
- package/dist/experience/types.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/kernels/weight_update.d.ts.map +1 -1
- package/dist/kernels/weight_update.js +12 -0
- package/dist/kernels/weight_update.js.map +1 -1
- package/dist/limbic/limbic_model.d.ts.map +1 -1
- package/dist/limbic/limbic_model.js +1 -3
- package/dist/limbic/limbic_model.js.map +1 -1
- package/dist/ml/classical/anomaly_composite.d.ts +32 -0
- package/dist/ml/classical/anomaly_composite.d.ts.map +1 -0
- package/dist/ml/classical/anomaly_composite.js +29 -0
- package/dist/ml/classical/anomaly_composite.js.map +1 -0
- package/dist/ml/classical/ewma.d.ts +7 -1
- package/dist/ml/classical/ewma.d.ts.map +1 -1
- package/dist/ml/classical/ewma.js +6 -4
- package/dist/ml/classical/ewma.js.map +1 -1
- package/dist/ml/classical/gbm.d.ts.map +1 -1
- package/dist/ml/classical/gbm.js +2 -1
- package/dist/ml/classical/gbm.js.map +1 -1
- package/dist/ml/classical/index.d.ts +6 -2
- package/dist/ml/classical/index.d.ts.map +1 -1
- package/dist/ml/classical/index.js +5 -1
- package/dist/ml/classical/index.js.map +1 -1
- package/dist/ml/classical/kmeans.d.ts.map +1 -1
- package/dist/ml/classical/kmeans.js +23 -4
- package/dist/ml/classical/kmeans.js.map +1 -1
- package/dist/ml/classical/knn.d.ts +15 -5
- package/dist/ml/classical/knn.d.ts.map +1 -1
- package/dist/ml/classical/knn.js +28 -7
- package/dist/ml/classical/knn.js.map +1 -1
- package/dist/ml/classical/logistic.d.ts +1 -9
- package/dist/ml/classical/logistic.d.ts.map +1 -1
- package/dist/ml/classical/logistic.js +17 -9
- package/dist/ml/classical/logistic.js.map +1 -1
- package/dist/ml/classical/naive_bayes.d.ts +5 -1
- package/dist/ml/classical/naive_bayes.d.ts.map +1 -1
- package/dist/ml/classical/naive_bayes.js +8 -3
- package/dist/ml/classical/naive_bayes.js.map +1 -1
- package/dist/ml/classical/robust_z.d.ts +15 -1
- package/dist/ml/classical/robust_z.d.ts.map +1 -1
- package/dist/ml/classical/robust_z.js +6 -1
- package/dist/ml/classical/robust_z.js.map +1 -1
- package/dist/ml/eval_loss.d.ts +15 -0
- package/dist/ml/eval_loss.d.ts.map +1 -0
- package/dist/ml/eval_loss.js +20 -0
- package/dist/ml/eval_loss.js.map +1 -0
- package/dist/ml/federate.d.ts +11 -25
- package/dist/ml/federate.d.ts.map +1 -1
- package/dist/ml/federate.js +15 -38
- package/dist/ml/federate.js.map +1 -1
- package/dist/ml/head_trainer.d.ts +16 -2
- package/dist/ml/head_trainer.d.ts.map +1 -1
- package/dist/ml/head_trainer.js +64 -27
- package/dist/ml/head_trainer.js.map +1 -1
- package/dist/ml/run.d.ts +11 -2
- package/dist/ml/run.d.ts.map +1 -1
- package/dist/ml/run.js +56 -17
- package/dist/ml/run.js.map +1 -1
- package/dist/ml/tasks/experience.d.ts +95 -13
- package/dist/ml/tasks/experience.d.ts.map +1 -1
- package/dist/ml/tasks/experience.js +172 -20
- package/dist/ml/tasks/experience.js.map +1 -1
- package/dist/ml/tasks/run_anomaly.d.ts +37 -19
- package/dist/ml/tasks/run_anomaly.d.ts.map +1 -1
- package/dist/ml/tasks/run_anomaly.js +50 -31
- package/dist/ml/tasks/run_anomaly.js.map +1 -1
- package/dist/ml/train.d.ts +8 -7
- package/dist/ml/train.d.ts.map +1 -1
- package/dist/ml/train.js +21 -30
- package/dist/ml/train.js.map +1 -1
- package/dist/ml/types.d.ts +6 -3
- package/dist/ml/types.d.ts.map +1 -1
- package/dist/ml/types.js +3 -2
- package/dist/ml/types.js.map +1 -1
- package/dist/optim/adamw.d.ts +11 -5
- package/dist/optim/adamw.d.ts.map +1 -1
- package/dist/optim/adamw.js +19 -4
- package/dist/optim/adamw.js.map +1 -1
- package/package.json +1 -1
- package/src/evermind/adapt.ts +8 -2
- package/src/evermind/delta_wire.ts +173 -148
- package/src/evermind/index.ts +36 -35
- package/src/evermind/merge.ts +10 -4
- package/src/evermind/ml_head.ts +63 -0
- package/src/evermind/package.ts +488 -488
- package/src/experience/index.ts +2 -1
- package/src/experience/store.ts +35 -3
- package/src/experience/types.ts +19 -0
- package/src/index.ts +2 -0
- package/src/kernels/weight_update.ts +12 -0
- package/src/limbic/limbic_model.ts +1 -4
- package/src/limbic/limbic_trainer.ts +91 -91
- package/src/ml/classical/anomaly_composite.ts +46 -0
- package/src/ml/classical/ewma.ts +8 -2
- package/src/ml/classical/gbm.ts +144 -143
- package/src/ml/classical/index.ts +8 -2
- package/src/ml/classical/kmeans.ts +22 -4
- package/src/ml/classical/knn.ts +31 -8
- package/src/ml/classical/logistic.ts +9 -9
- package/src/ml/classical/math.ts +93 -93
- package/src/ml/classical/naive_bayes.ts +10 -3
- package/src/ml/classical/robust_z.ts +21 -2
- package/src/ml/eval_loss.ts +27 -0
- package/src/ml/federate.ts +22 -52
- package/src/ml/head_trainer.ts +66 -48
- package/src/ml/registry.ts +44 -44
- package/src/ml/run.ts +57 -13
- package/src/ml/tasks/experience.ts +222 -23
- package/src/ml/tasks/run_anomaly.ts +62 -37
- package/src/ml/train.ts +34 -31
- package/src/ml/types.ts +135 -132
- package/src/optim/adamw.ts +21 -5
- package/src/training/cpu_ops.ts +141 -141
package/src/ml/classical/math.ts
CHANGED
|
@@ -1,93 +1,93 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ml/classical/math.ts — the small vector helpers every classical algorithm shares.
|
|
3
|
-
* Pure, allocation-light, deterministic.
|
|
4
|
-
*/
|
|
5
|
-
import { SeededRng } from "../../utils/rng.js";
|
|
6
|
-
|
|
7
|
-
/** The engine's ONE sigmoid / softmax (`training/cpu_ops.ts`). */
|
|
8
|
-
export { sigmoid, softmax } from "../../training/cpu_ops.js";
|
|
9
|
-
|
|
10
|
-
export type Vec = ArrayLike<number>;
|
|
11
|
-
|
|
12
|
-
export function dot(a: Vec, b: Vec): number {
|
|
13
|
-
const n = Math.min(a.length, b.length);
|
|
14
|
-
let s = 0;
|
|
15
|
-
for (let i = 0; i < n; i++) s += a[i]! * b[i]!;
|
|
16
|
-
return s;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function norm(a: Vec): number {
|
|
20
|
-
return Math.sqrt(dot(a, a));
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/** Cosine similarity in [-1, 1]; 0 when either vector is zero. */
|
|
24
|
-
export function cosine(a: Vec, b: Vec): number {
|
|
25
|
-
const na = norm(a);
|
|
26
|
-
const nb = norm(b);
|
|
27
|
-
return na > 0 && nb > 0 ? dot(a, b) / (na * nb) : 0;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export function squaredDistance(a: Vec, b: Vec): number {
|
|
31
|
-
const n = Math.min(a.length, b.length);
|
|
32
|
-
let s = 0;
|
|
33
|
-
for (let i = 0; i < n; i++) {
|
|
34
|
-
const d = a[i]! - b[i]!;
|
|
35
|
-
s += d * d;
|
|
36
|
-
}
|
|
37
|
-
return s;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/** Median of a numeric list (copy-sorted). NaN for an empty list. */
|
|
41
|
-
export function median(values: readonly number[]): number {
|
|
42
|
-
if (values.length === 0) return Number.NaN;
|
|
43
|
-
const s = [...values].sort((a, b) => a - b);
|
|
44
|
-
const m = s.length >> 1;
|
|
45
|
-
return s.length % 2 ? s[m]! : (s[m - 1]! + s[m]!) / 2;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/** Index of the largest value (first on ties). */
|
|
49
|
-
export function argmax(values: ArrayLike<number>): number {
|
|
50
|
-
let best = 0;
|
|
51
|
-
for (let i = 1; i < values.length; i++) if (values[i]! > values[best]!) best = i;
|
|
52
|
-
return best;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/** Number of distinct classes implied by integer labels 0..k-1. */
|
|
56
|
-
export function classCount(labels: readonly number[]): number {
|
|
57
|
-
let k = 0;
|
|
58
|
-
for (const y of labels) {
|
|
59
|
-
if (!Number.isInteger(y) || y < 0) throw new Error(`class label must be a non-negative integer, got ${y}`);
|
|
60
|
-
k = Math.max(k, y + 1);
|
|
61
|
-
}
|
|
62
|
-
return Math.max(k, 2);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/** Feature dimension of a non-empty sample matrix; throws on ragged rows. */
|
|
66
|
-
export function featureDim(X: readonly Vec[]): number {
|
|
67
|
-
if (X.length === 0) throw new Error("no samples");
|
|
68
|
-
const d = X[0]!.length;
|
|
69
|
-
for (const x of X) if (x.length !== d) throw new Error(`ragged features: expected ${d}, got ${x.length}`);
|
|
70
|
-
return d;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/** Confidence of a probability vector: its top probability. */
|
|
74
|
-
export function topProbability(p: readonly number[]): number {
|
|
75
|
-
return p.length ? Math.max(...p) : 0;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/** A deterministic [0,1) source for algorithms that sample — the engine's ONE seeded RNG. */
|
|
79
|
-
export function seededUniform(seed: number): () => number {
|
|
80
|
-
const rng = new SeededRng(seed);
|
|
81
|
-
return () => rng.next();
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Average path length of an unsuccessful BST search over n points — the
|
|
86
|
-
* isolation-forest normaliser c(n).
|
|
87
|
-
*/
|
|
88
|
-
export function averagePathLength(n: number): number {
|
|
89
|
-
if (n <= 1) return 0;
|
|
90
|
-
if (n === 2) return 1;
|
|
91
|
-
const harmonic = Math.log(n - 1) + 0.5772156649;
|
|
92
|
-
return 2 * harmonic - (2 * (n - 1)) / n;
|
|
93
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* ml/classical/math.ts — the small vector helpers every classical algorithm shares.
|
|
3
|
+
* Pure, allocation-light, deterministic.
|
|
4
|
+
*/
|
|
5
|
+
import { SeededRng } from "../../utils/rng.js";
|
|
6
|
+
|
|
7
|
+
/** The engine's ONE sigmoid / softmax (`training/cpu_ops.ts`). */
|
|
8
|
+
export { sigmoid, softmax } from "../../training/cpu_ops.js";
|
|
9
|
+
|
|
10
|
+
export type Vec = ArrayLike<number>;
|
|
11
|
+
|
|
12
|
+
export function dot(a: Vec, b: Vec): number {
|
|
13
|
+
const n = Math.min(a.length, b.length);
|
|
14
|
+
let s = 0;
|
|
15
|
+
for (let i = 0; i < n; i++) s += a[i]! * b[i]!;
|
|
16
|
+
return s;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function norm(a: Vec): number {
|
|
20
|
+
return Math.sqrt(dot(a, a));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Cosine similarity in [-1, 1]; 0 when either vector is zero. */
|
|
24
|
+
export function cosine(a: Vec, b: Vec): number {
|
|
25
|
+
const na = norm(a);
|
|
26
|
+
const nb = norm(b);
|
|
27
|
+
return na > 0 && nb > 0 ? dot(a, b) / (na * nb) : 0;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function squaredDistance(a: Vec, b: Vec): number {
|
|
31
|
+
const n = Math.min(a.length, b.length);
|
|
32
|
+
let s = 0;
|
|
33
|
+
for (let i = 0; i < n; i++) {
|
|
34
|
+
const d = a[i]! - b[i]!;
|
|
35
|
+
s += d * d;
|
|
36
|
+
}
|
|
37
|
+
return s;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Median of a numeric list (copy-sorted). NaN for an empty list. */
|
|
41
|
+
export function median(values: readonly number[]): number {
|
|
42
|
+
if (values.length === 0) return Number.NaN;
|
|
43
|
+
const s = [...values].sort((a, b) => a - b);
|
|
44
|
+
const m = s.length >> 1;
|
|
45
|
+
return s.length % 2 ? s[m]! : (s[m - 1]! + s[m]!) / 2;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Index of the largest value (first on ties). */
|
|
49
|
+
export function argmax(values: ArrayLike<number>): number {
|
|
50
|
+
let best = 0;
|
|
51
|
+
for (let i = 1; i < values.length; i++) if (values[i]! > values[best]!) best = i;
|
|
52
|
+
return best;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Number of distinct classes implied by integer labels 0..k-1. */
|
|
56
|
+
export function classCount(labels: readonly number[]): number {
|
|
57
|
+
let k = 0;
|
|
58
|
+
for (const y of labels) {
|
|
59
|
+
if (!Number.isInteger(y) || y < 0) throw new Error(`class label must be a non-negative integer, got ${y}`);
|
|
60
|
+
k = Math.max(k, y + 1);
|
|
61
|
+
}
|
|
62
|
+
return Math.max(k, 2);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Feature dimension of a non-empty sample matrix; throws on ragged rows. */
|
|
66
|
+
export function featureDim(X: readonly Vec[]): number {
|
|
67
|
+
if (X.length === 0) throw new Error("no samples");
|
|
68
|
+
const d = X[0]!.length;
|
|
69
|
+
for (const x of X) if (x.length !== d) throw new Error(`ragged features: expected ${d}, got ${x.length}`);
|
|
70
|
+
return d;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Confidence of a probability vector: its top probability. */
|
|
74
|
+
export function topProbability(p: readonly number[]): number {
|
|
75
|
+
return p.length ? Math.max(...p) : 0;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** A deterministic [0,1) source for algorithms that sample — the engine's ONE seeded RNG. */
|
|
79
|
+
export function seededUniform(seed: number): () => number {
|
|
80
|
+
const rng = new SeededRng(seed);
|
|
81
|
+
return () => rng.next();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Average path length of an unsuccessful BST search over n points — the
|
|
86
|
+
* isolation-forest normaliser c(n).
|
|
87
|
+
*/
|
|
88
|
+
export function averagePathLength(n: number): number {
|
|
89
|
+
if (n <= 1) return 0;
|
|
90
|
+
if (n === 2) return 1;
|
|
91
|
+
const harmonic = Math.log(n - 1) + 0.5772156649;
|
|
92
|
+
return 2 * harmonic - (2 * (n - 1)) / n;
|
|
93
|
+
}
|
|
@@ -4,6 +4,10 @@
|
|
|
4
4
|
* Per class: a weighted prior, and an independent Gaussian per feature. Variance
|
|
5
5
|
* is smoothed by a fraction of the largest feature variance (as scikit-learn's
|
|
6
6
|
* `var_smoothing`), so a constant feature never divides by zero.
|
|
7
|
+
*
|
|
8
|
+
* A class with no training weight has prior −∞ and probability exactly 0: its
|
|
9
|
+
* Gaussian is a placeholder (mean 0, variance ≈ the smoothing floor), which would
|
|
10
|
+
* otherwise out-score every real class on an input near zero.
|
|
7
11
|
*/
|
|
8
12
|
import type { MlPrediction, MlPredictor, MlSample } from "../types.js";
|
|
9
13
|
import { classCount, featureDim, softmax, topProbability } from "./math.js";
|
|
@@ -21,11 +25,12 @@ export class NaiveBayesModel implements MlPredictor {
|
|
|
21
25
|
readonly variance: Float64Array[],
|
|
22
26
|
) {}
|
|
23
27
|
|
|
24
|
-
/** Per-class joint log-likelihood log P(c) + Σ log N(x_i | μ, σ²). */
|
|
28
|
+
/** Per-class joint log-likelihood log P(c) + Σ log N(x_i | μ, σ²); −∞ for an unseen class. */
|
|
25
29
|
jointLogLikelihood(x: ArrayLike<number>): number[] {
|
|
26
30
|
return this.mean.map((mu, c) => {
|
|
27
31
|
const v = this.variance[c]!;
|
|
28
32
|
let ll = this.logPrior[c]!;
|
|
33
|
+
if (ll === -Infinity) return ll;
|
|
29
34
|
for (let i = 0; i < mu.length; i++) {
|
|
30
35
|
const d = (x[i] ?? 0) - mu[i]!;
|
|
31
36
|
ll += -0.5 * Math.log(2 * Math.PI * v[i]!) - (d * d) / (2 * v[i]!);
|
|
@@ -71,7 +76,9 @@ export function fitNaiveBayes(samples: readonly MlSample[], opts: NaiveBayesOpti
|
|
|
71
76
|
for (let c = 0; c < k; c++) for (let i = 0; i < d; i++) variance[c]![i] = variance[c]![i]! + eps;
|
|
72
77
|
|
|
73
78
|
const total = wSum.reduce((a, b) => a + b, 0);
|
|
74
|
-
|
|
75
|
-
|
|
79
|
+
if (!(total > 0)) throw new Error("fitNaiveBayes: the samples carry no weight");
|
|
80
|
+
// An unseen class is masked out (−∞, so softmax gives it exactly 0). At least one
|
|
81
|
+
// class has weight, so softmax always has a finite maximum.
|
|
82
|
+
const logPrior = Float64Array.from(wSum, (w) => (w > 0 ? Math.log(w / total) : -Infinity));
|
|
76
83
|
return new NaiveBayesModel(logPrior, mean, variance);
|
|
77
84
|
}
|
|
@@ -4,7 +4,10 @@
|
|
|
4
4
|
* Per feature: z = |x − median| / (1.4826 · MAD). Median and MAD are not dragged
|
|
5
5
|
* by the very outliers being looked for, unlike mean and standard deviation. When
|
|
6
6
|
* MAD is 0 (more than half the history identical) the mean absolute deviation
|
|
7
|
-
* stands in, then
|
|
7
|
+
* stands in, then two floors: a fraction of |median| (relative) and `minScale`
|
|
8
|
+
* (absolute, in the feature's own units). Without the absolute one a history that
|
|
9
|
+
* is all zeros — no tool failures, ever — gets a scale of ~1e-9, and the first
|
|
10
|
+
* small non-zero value scores astronomically.
|
|
8
11
|
*
|
|
9
12
|
* Prediction: `scores[0] = max_i z_i / threshold` — ≥ 1 is anomalous.
|
|
10
13
|
*/
|
|
@@ -21,6 +24,21 @@ export interface RobustZOptions {
|
|
|
21
24
|
threshold?: number;
|
|
22
25
|
/** Smallest scale per feature, as a fraction of |median| (plus 1e-9). Default 0.01. */
|
|
23
26
|
minScaleFraction?: number;
|
|
27
|
+
/**
|
|
28
|
+
* Smallest scale per feature in absolute feature units: one value for every
|
|
29
|
+
* feature, or one per feature (a missing entry is 0). Default 0 — the algorithm
|
|
30
|
+
* cannot know the units; a task on a fixed scale sets it (`run.anomaly` does,
|
|
31
|
+
* per feature, because each of its metrics is normalised by a different reference).
|
|
32
|
+
*/
|
|
33
|
+
minScale?: MinScale;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** An absolute scale floor: one for every feature, or one per feature. */
|
|
37
|
+
export type MinScale = number | readonly number[];
|
|
38
|
+
|
|
39
|
+
/** The floor for feature `i` (a per-feature array's missing entry is 0). */
|
|
40
|
+
export function minScaleAt(minScale: MinScale, i: number): number {
|
|
41
|
+
return typeof minScale === "number" ? minScale : (minScale[i] ?? 0);
|
|
24
42
|
}
|
|
25
43
|
|
|
26
44
|
export class RobustZModel implements MlPredictor {
|
|
@@ -50,6 +68,7 @@ export function fitRobustZ(samples: readonly MlSample[], opts: RobustZOptions =
|
|
|
50
68
|
const X = samples.map((s) => s.x);
|
|
51
69
|
const d = featureDim(X);
|
|
52
70
|
const minFrac = opts.minScaleFraction ?? 0.01;
|
|
71
|
+
const minAbs: MinScale = opts.minScale ?? 0;
|
|
53
72
|
const center = new Float64Array(d);
|
|
54
73
|
const scale = new Float64Array(d);
|
|
55
74
|
for (let i = 0; i < d; i++) {
|
|
@@ -59,7 +78,7 @@ export function fitRobustZ(samples: readonly MlSample[], opts: RobustZOptions =
|
|
|
59
78
|
let s = MAD_TO_SD * median(dev);
|
|
60
79
|
if (!(s > 0)) s = MEANAD_TO_SD * (dev.reduce((a, b) => a + b, 0) / dev.length);
|
|
61
80
|
center[i] = med;
|
|
62
|
-
scale[i] = Math.max(s, minFrac * Math.abs(med) + 1e-9);
|
|
81
|
+
scale[i] = Math.max(s, minFrac * Math.abs(med) + 1e-9, minScaleAt(minAbs, i));
|
|
63
82
|
}
|
|
64
83
|
return new RobustZModel(center, scale, opts.threshold ?? 3.5);
|
|
65
84
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ml/eval_loss.ts — the ONE held-out loss for catalog models.
|
|
3
|
+
*
|
|
4
|
+
* What the trainer reports as `holdoutLoss`, what a federated merge is gated on, and
|
|
5
|
+
* what `LogisticModel.loss` returns. A leaf (types only), so a classical algorithm can
|
|
6
|
+
* use it without importing the trainer that imports the algorithms.
|
|
7
|
+
*/
|
|
8
|
+
import type { MlPredictor, MlSample, MlTaskKind } from "./types.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Mean loss of `predictor` on `samples` for a task of `kind` — classify: weighted
|
|
12
|
+
* cross-entropy of the true class; regress: weighted ½·squared error. Null for
|
|
13
|
+
* kinds without a supervised loss, or an empty (or zero-weight) set.
|
|
14
|
+
*/
|
|
15
|
+
export function mlEvalLoss(kind: MlTaskKind, predictor: MlPredictor, samples: readonly MlSample[]): number | null {
|
|
16
|
+
if (samples.length === 0 || (kind !== "classify" && kind !== "regress")) return null;
|
|
17
|
+
let sum = 0;
|
|
18
|
+
let wSum = 0;
|
|
19
|
+
for (const s of samples) {
|
|
20
|
+
const w = s.weight ?? 1;
|
|
21
|
+
const p = predictor.predict(s.x);
|
|
22
|
+
const loss = kind === "classify" ? -Math.log(Math.max(p.scores[s.y] ?? 0, 1e-12)) : 0.5 * (p.scores[0]! - s.y) ** 2;
|
|
23
|
+
sum += w * loss;
|
|
24
|
+
wSum += w;
|
|
25
|
+
}
|
|
26
|
+
return wSum > 0 ? sum / wSum : null;
|
|
27
|
+
}
|
package/src/ml/federate.ts
CHANGED
|
@@ -8,62 +8,26 @@
|
|
|
8
8
|
* `target: "ml:<task>"`, and the writer folds N of them with the existing FedAvg
|
|
9
9
|
* (`mergeCheckpointDiffsGated` → `mergeCheckpointDiffs`), refusing a merge whose
|
|
10
10
|
* held-out loss regresses. No second wire, no second merge.
|
|
11
|
+
*
|
|
12
|
+
* The head's manifest entry and checkpoint format live with the artifact
|
|
13
|
+
* (`evermind/ml_head.ts`); this module only imports evermind/, never the reverse.
|
|
11
14
|
*/
|
|
12
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
buildDeltaLearnPayload,
|
|
17
|
+
decodeDeltaPayload,
|
|
18
|
+
deltaTargetMismatch,
|
|
19
|
+
deltaUnusableReason,
|
|
20
|
+
mlHeadTarget,
|
|
21
|
+
type DeltaLearnPayload,
|
|
22
|
+
} from "../evermind/delta_wire.js";
|
|
13
23
|
import { mergeCheckpointDiffsGated, type GatedMergeResult, type MergeGateOptions } from "../evermind/merge.js";
|
|
24
|
+
import { headCheckpoint, headParams, type EvermindMlHead } from "../evermind/ml_head.js";
|
|
14
25
|
import type { EvermindModelPackage } from "../evermind/package.js";
|
|
15
26
|
import { base64ToBytes, bytesToBase64 } from "../utils/base64.js";
|
|
16
|
-
import { appendCrcTrailer, verifyCrcTrailer } from "../utils/crc32.js";
|
|
17
27
|
import { diffCheckpoints } from "../utils/delta.js";
|
|
18
28
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
* it rides in the manifest, and versioned, so it federates — a delta against it is
|
|
22
|
-
* `target: "ml:<task>"`, `baseVersion: version` on the delta wire.
|
|
23
|
-
*/
|
|
24
|
-
export interface EvermindMlHead {
|
|
25
|
-
/** Catalog task id, e.g. `run.local-success`. */
|
|
26
|
-
task: string;
|
|
27
|
-
/** Engine key, e.g. `local:classical:logistic`. */
|
|
28
|
-
engine: string;
|
|
29
|
-
/** Integer head version — what a federated delta for it is diffed against. */
|
|
30
|
-
version: number;
|
|
31
|
-
/** What a loader needs to rebuild the predictor (e.g. `{ dim, classes }`). */
|
|
32
|
-
shape: Record<string, number>;
|
|
33
|
-
/** base64 of the CRC-trailed f32 checkpoint of the head's flat parameters. */
|
|
34
|
-
params: string;
|
|
35
|
-
/** Held-out loss of this version, when measured. */
|
|
36
|
-
evalLoss?: number;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/** A head's flat parameters as a CRC-trailed f32 checkpoint. */
|
|
40
|
-
export function headCheckpoint(params: ArrayLike<number>): ArrayBuffer {
|
|
41
|
-
const body = new ArrayBuffer(params.length * 4);
|
|
42
|
-
new Float32Array(body).set(Array.from(params));
|
|
43
|
-
return appendCrcTrailer(body);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/** The flat parameters of a head checkpoint. Throws on a failed CRC. */
|
|
47
|
-
export function headParams(checkpoint: ArrayBuffer): Float32Array {
|
|
48
|
-
const crc = verifyCrcTrailer(checkpoint);
|
|
49
|
-
if (!crc.ok) throw new Error("headParams: checkpoint failed its CRC check");
|
|
50
|
-
if (crc.body.byteLength % 4 !== 0) throw new Error("headParams: checkpoint is not f32");
|
|
51
|
-
return new Float32Array(crc.body);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/** Why a manifest head entry is unusable, or null (the package's validate() calls this). */
|
|
55
|
-
export function mlHeadUnusableReason(h: EvermindMlHead): string | null {
|
|
56
|
-
if (!h || typeof h.task !== "string" || typeof h.engine !== "string") return "task and engine are required";
|
|
57
|
-
if (!Number.isInteger(h.version) || h.version < 0) return "version must be a non-negative integer";
|
|
58
|
-
try {
|
|
59
|
-
const crc = verifyCrcTrailer(base64ToBytes(h.params));
|
|
60
|
-
if (!crc.hasTrailer || !crc.ok) return "params checkpoint failed its CRC check";
|
|
61
|
-
if (crc.body.byteLength % 4 !== 0) return "params checkpoint is not f32";
|
|
62
|
-
} catch {
|
|
63
|
-
return "params is not base64";
|
|
64
|
-
}
|
|
65
|
-
return null;
|
|
66
|
-
}
|
|
29
|
+
export { headCheckpoint, headParams, mlHeadUnusableReason } from "../evermind/ml_head.js";
|
|
30
|
+
export type { EvermindMlHead } from "../evermind/ml_head.js";
|
|
67
31
|
|
|
68
32
|
/** A manifest entry for a head's current parameters. */
|
|
69
33
|
export function mlHeadEntry(
|
|
@@ -121,6 +85,11 @@ export interface MlHeadMergeOutcome {
|
|
|
121
85
|
* target, stale base or a malformed diff costs only itself), FedAvg the rest, and
|
|
122
86
|
* keep the result only if `score` (held-out loss of a parameter vector, lower is
|
|
123
87
|
* better) does not regress.
|
|
88
|
+
*
|
|
89
|
+
* Fails CLOSED: `requireEval` defaults to true here, so a round `score` cannot judge
|
|
90
|
+
* (null — e.g. `mlEvalLoss` for an anomaly/cluster head or an empty holdout — or a
|
|
91
|
+
* non-finite loss) is refused. A caller that federates an unscorable head on purpose
|
|
92
|
+
* passes `{ requireEval: false }` explicitly.
|
|
124
93
|
*/
|
|
125
94
|
export function mergeMlHeadDeltas(
|
|
126
95
|
base: EvermindMlHead,
|
|
@@ -134,7 +103,8 @@ export function mergeMlHeadDeltas(
|
|
|
134
103
|
const weights: number[] = [];
|
|
135
104
|
const rejected: { index: number; reason: string }[] = [];
|
|
136
105
|
payloads.forEach((p, index) => {
|
|
137
|
-
|
|
106
|
+
const wrongTarget = deltaTargetMismatch(p.target, target);
|
|
107
|
+
if (wrongTarget) return void rejected.push({ index, reason: wrongTarget });
|
|
138
108
|
if (p.baseVersion !== base.version) return void rejected.push({ index, reason: `stale base version ${p.baseVersion} (head is ${base.version})` });
|
|
139
109
|
let diff: ArrayBuffer;
|
|
140
110
|
try {
|
|
@@ -148,7 +118,7 @@ export function mergeMlHeadDeltas(
|
|
|
148
118
|
weights.push(p.weight ?? 1);
|
|
149
119
|
});
|
|
150
120
|
|
|
151
|
-
const merge = mergeCheckpointDiffsGated(baseCp, diffs, weights, (cp) => score(headParams(cp)), opts);
|
|
121
|
+
const merge = mergeCheckpointDiffsGated(baseCp, diffs, weights, (cp) => score(headParams(cp)), { ...opts, requireEval: opts.requireEval ?? true });
|
|
152
122
|
if (!merge.accepted) return { head: base, merge, rejected };
|
|
153
123
|
const head: EvermindMlHead = {
|
|
154
124
|
...base,
|
package/src/ml/head_trainer.ts
CHANGED
|
@@ -5,8 +5,13 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Full-batch gradient descent with AdamW. When a GPUDevice is supplied the
|
|
7
7
|
* optimiser step runs on the GPU via the shared WEIGHT_UPDATE_WGSL kernel; with
|
|
8
|
-
* no device it
|
|
9
|
-
* everywhere (CI, Node without
|
|
8
|
+
* no device it runs the kernel's CPU mirror, `adamwUpdateInPlace` (`optim/adamw.ts`,
|
|
9
|
+
* NaN/Inf guard included), so training works everywhere (CI, Node without
|
|
10
|
+
* @webgpu/node, a laptop with no GPU). Neither path bounds the step (maxDelta 0).
|
|
11
|
+
*
|
|
12
|
+
* A non-finite gradient element is skipped by both paths — its moments and weight
|
|
13
|
+
* are left as they were, so the next finite step trains it again — and counted in
|
|
14
|
+
* {@link HeadTrainer.skippedNonFinite}, so a caller can see a run that diverged.
|
|
10
15
|
*
|
|
11
16
|
* The head owns its forward/backward and its loss (MSE for regression, BCE /
|
|
12
17
|
* softmax cross-entropy for classification); this file owns only the loop:
|
|
@@ -23,6 +28,7 @@ import {
|
|
|
23
28
|
cdiv,
|
|
24
29
|
} from "../utils/gpu_utils.js";
|
|
25
30
|
import { WEIGHT_UPDATE_WGSL } from "../kernels/weight_update.js";
|
|
31
|
+
import { adamwUpdateInPlace, type AdamWKernelStep } from "../optim/adamw.js";
|
|
26
32
|
|
|
27
33
|
/** A named trainable tensor (flat row-major Float32Array). */
|
|
28
34
|
export interface HeadParam {
|
|
@@ -63,29 +69,19 @@ interface AdamMoment {
|
|
|
63
69
|
v: Float32Array;
|
|
64
70
|
}
|
|
65
71
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
weightDecay: number;
|
|
69
|
-
beta1: number;
|
|
70
|
-
beta2: number;
|
|
71
|
-
eps: number;
|
|
72
|
-
beta1_t: number;
|
|
73
|
-
beta2_t: number;
|
|
74
|
-
}
|
|
72
|
+
/** Byte size of WEIGHT_UPDATE_WGSL's `AdamParams` uniform: 1 u32 + 8 f32 + 3 f32 pad = 48. */
|
|
73
|
+
const ADAM_PARAMS_BYTES = 48;
|
|
75
74
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
beta1_t: number,
|
|
84
|
-
beta2_t: number,
|
|
85
|
-
): ArrayBuffer {
|
|
86
|
-
const buf = new ArrayBuffer(32);
|
|
75
|
+
/**
|
|
76
|
+
* Pack `AdamParams` exactly as WEIGHT_UPDATE_WGSL declares it:
|
|
77
|
+
* `num_elements: u32, lr, beta1, beta2, eps, weight_decay, beta1_t, beta2_t,
|
|
78
|
+
* max_delta, _pad0, _pad1, _pad2` (all f32 after the first) — 48 bytes.
|
|
79
|
+
*/
|
|
80
|
+
function packAdamParams(numElements: number, hp: AdamWKernelStep): ArrayBuffer {
|
|
81
|
+
const buf = new ArrayBuffer(ADAM_PARAMS_BYTES);
|
|
87
82
|
new Uint32Array(buf, 0, 1).set([numElements]);
|
|
88
|
-
|
|
83
|
+
// Pads stay 0 (fresh ArrayBuffer).
|
|
84
|
+
new Float32Array(buf, 4, 8).set([hp.lr, hp.beta1, hp.beta2, hp.eps, hp.weightDecay, hp.beta1_t, hp.beta2_t, hp.maxDelta]);
|
|
89
85
|
return buf;
|
|
90
86
|
}
|
|
91
87
|
|
|
@@ -94,6 +90,7 @@ export class HeadTrainer<S> {
|
|
|
94
90
|
readonly device: GPUDevice | null;
|
|
95
91
|
private _moments: AdamMoment[] | null = null;
|
|
96
92
|
private _step = 0;
|
|
93
|
+
private _skippedNonFinite = 0;
|
|
97
94
|
private readonly _adamwPipeline: GPUComputePipeline | null;
|
|
98
95
|
|
|
99
96
|
constructor(
|
|
@@ -109,6 +106,19 @@ export class HeadTrainer<S> {
|
|
|
109
106
|
: null;
|
|
110
107
|
}
|
|
111
108
|
|
|
109
|
+
/**
|
|
110
|
+
* Parameter elements whose gradient was non-finite (NaN/±Inf) and whose update was
|
|
111
|
+
* therefore skipped, summed over every step this trainer has taken. 0 on a healthy run.
|
|
112
|
+
*/
|
|
113
|
+
get skippedNonFinite(): number {
|
|
114
|
+
return this._skippedNonFinite;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Whether any step had to skip a non-finite gradient element. */
|
|
118
|
+
get diverged(): boolean {
|
|
119
|
+
return this._skippedNonFinite > 0;
|
|
120
|
+
}
|
|
121
|
+
|
|
112
122
|
/** Whether the optimiser step runs on the GPU. */
|
|
113
123
|
get gpuTraining(): boolean {
|
|
114
124
|
return this.device != null && this._adamwPipeline != null;
|
|
@@ -159,7 +169,7 @@ export class HeadTrainer<S> {
|
|
|
159
169
|
this._step++;
|
|
160
170
|
const beta1_t = Math.pow(beta1, this._step);
|
|
161
171
|
const beta2_t = Math.pow(beta2, this._step);
|
|
162
|
-
const hp:
|
|
172
|
+
const hp: AdamWKernelStep = { lr: learningRate, weightDecay, beta1, beta2, eps, beta1_t, beta2_t, maxDelta: 0 };
|
|
163
173
|
if (this.gpuTraining) {
|
|
164
174
|
await this._adamwStepGpu(grads, hp);
|
|
165
175
|
} else {
|
|
@@ -181,42 +191,31 @@ export class HeadTrainer<S> {
|
|
|
181
191
|
return total / samples.length;
|
|
182
192
|
}
|
|
183
193
|
|
|
184
|
-
|
|
194
|
+
/** AdamW on the CPU via the kernel's shared mirror (`adamwUpdateInPlace`), which counts its skips. */
|
|
195
|
+
private _adamwStepCpu(grads: HeadParam[], hp: AdamWKernelStep): void {
|
|
185
196
|
const params = this.head.parameters();
|
|
186
|
-
const { learningRate: lr, weightDecay: wd, beta1, beta2, eps, beta1_t, beta2_t } = hp;
|
|
187
197
|
for (let pi = 0; pi < params.length; pi++) {
|
|
188
|
-
const p = params[pi]!.data;
|
|
189
|
-
const g = grads[pi]!.data;
|
|
190
198
|
const mom = this._moments![pi]!;
|
|
191
|
-
|
|
192
|
-
const gi = g[i]!;
|
|
193
|
-
mom.m[i] = beta1 * mom.m[i]! + (1 - beta1) * gi;
|
|
194
|
-
mom.v[i] = beta2 * mom.v[i]! + (1 - beta2) * gi * gi;
|
|
195
|
-
const mHat = mom.m[i]! / (1 - beta1_t);
|
|
196
|
-
const vHat = mom.v[i]! / (1 - beta2_t);
|
|
197
|
-
p[i] = p[i]! * (1 - lr * wd) - (lr * mHat) / (Math.sqrt(vHat) + eps);
|
|
198
|
-
}
|
|
199
|
+
this._skippedNonFinite += adamwUpdateInPlace(params[pi]!.data, grads[pi]!.data, mom.m, mom.v, hp);
|
|
199
200
|
}
|
|
200
201
|
}
|
|
201
202
|
|
|
202
203
|
/** AdamW on the GPU via the shared WEIGHT_UPDATE_WGSL kernel. Awaited per step. */
|
|
203
|
-
private async _adamwStepGpu(grads: HeadParam[], hp:
|
|
204
|
+
private async _adamwStepGpu(grads: HeadParam[], hp: AdamWKernelStep): Promise<void> {
|
|
204
205
|
const device = this.device!;
|
|
205
206
|
const pipeline = this._adamwPipeline!;
|
|
206
207
|
const params = this.head.parameters();
|
|
207
|
-
const { learningRate: lr, weightDecay: wd, beta1, beta2, eps, beta1_t, beta2_t } = hp;
|
|
208
208
|
|
|
209
209
|
for (let pi = 0; pi < params.length; pi++) {
|
|
210
210
|
const p = params[pi]!;
|
|
211
211
|
const mom = this._moments![pi]!;
|
|
212
|
+
// The kernel skips these elements too; it cannot count, so count them here.
|
|
213
|
+
this._skippedNonFinite += countNonFinite(grads[pi]!.data);
|
|
212
214
|
const paramBuf = createStorageBuffer(device, p.data, true);
|
|
213
215
|
const gradBuf = createStorageBuffer(device, grads[pi]!.data, false);
|
|
214
216
|
const mBuf = createStorageBuffer(device, mom.m, true);
|
|
215
217
|
const vBuf = createStorageBuffer(device, mom.v, true);
|
|
216
|
-
const uni = createUniformBuffer(
|
|
217
|
-
device,
|
|
218
|
-
packAdamParams(p.numel, lr, beta1, beta2, eps, wd, beta1_t, beta2_t),
|
|
219
|
-
);
|
|
218
|
+
const uni = createUniformBuffer(device, packAdamParams(p.numel, hp));
|
|
220
219
|
const bg = createBindGroup(device, pipeline, [uni, paramBuf, gradBuf, mBuf, vBuf]);
|
|
221
220
|
dispatchKernel(device, pipeline, bg, [cdiv(p.numel, 256), 1, 1]);
|
|
222
221
|
|
|
@@ -233,16 +232,35 @@ export class HeadTrainer<S> {
|
|
|
233
232
|
}
|
|
234
233
|
}
|
|
235
234
|
|
|
236
|
-
/**
|
|
235
|
+
/** Elements of `a` that are NaN or ±Inf. */
|
|
236
|
+
function countNonFinite(a: Float32Array): number {
|
|
237
|
+
let n = 0;
|
|
238
|
+
for (let i = 0; i < a.length; i++) if (!Number.isFinite(a[i]!)) n++;
|
|
239
|
+
return n;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Clip grads to `maxNorm` in place; returns the PRE-clip L2 norm (the signal — NaN
|
|
244
|
+
* or Inf when any element is). The clip itself uses the norm of the FINITE elements:
|
|
245
|
+
* those are skipped by the optimiser anyway, and one NaN must not exempt the rest
|
|
246
|
+
* from clipping.
|
|
247
|
+
*/
|
|
237
248
|
function clipGradients(grads: HeadParam[], maxNorm: number): number {
|
|
238
249
|
let normSq = 0;
|
|
239
|
-
|
|
240
|
-
const
|
|
241
|
-
|
|
242
|
-
|
|
250
|
+
let finiteSq = 0;
|
|
251
|
+
for (const g of grads) {
|
|
252
|
+
for (let i = 0; i < g.data.length; i++) {
|
|
253
|
+
const sq = g.data[i]! * g.data[i]!;
|
|
254
|
+
normSq += sq;
|
|
255
|
+
if (Number.isFinite(sq)) finiteSq += sq;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
const finiteNorm = Math.sqrt(finiteSq);
|
|
259
|
+
if (finiteNorm > maxNorm && finiteNorm > 0) {
|
|
260
|
+
const scale = maxNorm / finiteNorm;
|
|
243
261
|
for (const g of grads) for (let i = 0; i < g.data.length; i++) g.data[i]! *= scale;
|
|
244
262
|
}
|
|
245
|
-
return
|
|
263
|
+
return Math.sqrt(normSq);
|
|
246
264
|
}
|
|
247
265
|
|
|
248
266
|
/** Concatenate a head's parameters into one flat vector (the federation unit). */
|