@seanhogg/builderforce-memory-engine 2026.9.34 → 2026.10.1
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/delta_wire.d.ts +10 -1
- package/dist/evermind/delta_wire.d.ts.map +1 -1
- package/dist/evermind/delta_wire.js +25 -3
- package/dist/evermind/delta_wire.js.map +1 -1
- package/dist/evermind/index.d.ts +4 -4
- package/dist/evermind/index.d.ts.map +1 -1
- package/dist/evermind/index.js +2 -2
- package/dist/evermind/index.js.map +1 -1
- package/dist/evermind/merge.d.ts +25 -0
- package/dist/evermind/merge.d.ts.map +1 -1
- package/dist/evermind/merge.js +30 -0
- package/dist/evermind/merge.js.map +1 -1
- package/dist/evermind/package.d.ts +9 -0
- package/dist/evermind/package.d.ts.map +1 -1
- package/dist/evermind/package.js +20 -0
- package/dist/evermind/package.js.map +1 -1
- package/dist/index.d.ts +6 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -3
- package/dist/index.js.map +1 -1
- package/dist/limbic/index.d.ts +3 -0
- package/dist/limbic/index.d.ts.map +1 -1
- package/dist/limbic/index.js +3 -0
- package/dist/limbic/index.js.map +1 -1
- package/dist/limbic/limbic_task.d.ts +22 -0
- package/dist/limbic/limbic_task.d.ts.map +1 -0
- package/dist/limbic/limbic_task.js +45 -0
- package/dist/limbic/limbic_task.js.map +1 -0
- package/dist/limbic/limbic_trainer.d.ts +17 -30
- package/dist/limbic/limbic_trainer.d.ts.map +1 -1
- package/dist/limbic/limbic_trainer.js +38 -144
- package/dist/limbic/limbic_trainer.js.map +1 -1
- package/dist/ml/classical/ewma.d.ts +32 -0
- package/dist/ml/classical/ewma.d.ts.map +1 -0
- package/dist/ml/classical/ewma.js +46 -0
- package/dist/ml/classical/ewma.js.map +1 -0
- package/dist/ml/classical/gbm.d.ts +42 -0
- package/dist/ml/classical/gbm.d.ts.map +1 -0
- package/dist/ml/classical/gbm.js +116 -0
- package/dist/ml/classical/gbm.js.map +1 -0
- package/dist/ml/classical/index.d.ts +48 -0
- package/dist/ml/classical/index.d.ts.map +1 -0
- package/dist/ml/classical/index.js +45 -0
- package/dist/ml/classical/index.js.map +1 -0
- package/dist/ml/classical/isolation_forest.d.ts +37 -0
- package/dist/ml/classical/isolation_forest.d.ts.map +1 -0
- package/dist/ml/classical/isolation_forest.js +72 -0
- package/dist/ml/classical/isolation_forest.js.map +1 -0
- package/dist/ml/classical/kmeans.d.ts +31 -0
- package/dist/ml/classical/kmeans.d.ts.map +1 -0
- package/dist/ml/classical/kmeans.js +93 -0
- package/dist/ml/classical/kmeans.js.map +1 -0
- package/dist/ml/classical/knn.d.ts +48 -0
- package/dist/ml/classical/knn.d.ts.map +1 -0
- package/dist/ml/classical/knn.js +68 -0
- package/dist/ml/classical/knn.js.map +1 -0
- package/dist/ml/classical/linear.d.ts +29 -0
- package/dist/ml/classical/linear.d.ts.map +1 -0
- package/dist/ml/classical/linear.js +114 -0
- package/dist/ml/classical/linear.js.map +1 -0
- package/dist/ml/classical/logistic.d.ts +39 -0
- package/dist/ml/classical/logistic.d.ts.map +1 -0
- package/dist/ml/classical/logistic.js +103 -0
- package/dist/ml/classical/logistic.js.map +1 -0
- package/dist/ml/classical/math.d.ts +26 -0
- package/dist/ml/classical/math.d.ts.map +1 -0
- package/dist/ml/classical/math.js +90 -0
- package/dist/ml/classical/math.js.map +1 -0
- package/dist/ml/classical/naive_bayes.d.ts +24 -0
- package/dist/ml/classical/naive_bayes.d.ts.map +1 -0
- package/dist/ml/classical/naive_bayes.js +67 -0
- package/dist/ml/classical/naive_bayes.js.map +1 -0
- package/dist/ml/classical/robust_z.d.ts +30 -0
- package/dist/ml/classical/robust_z.d.ts.map +1 -0
- package/dist/ml/classical/robust_z.js +46 -0
- package/dist/ml/classical/robust_z.js.map +1 -0
- package/dist/ml/dense_head.d.ts +33 -0
- package/dist/ml/dense_head.d.ts.map +1 -0
- package/dist/ml/dense_head.js +140 -0
- package/dist/ml/dense_head.js.map +1 -0
- package/dist/ml/features.d.ts +25 -0
- package/dist/ml/features.d.ts.map +1 -0
- package/dist/ml/features.js +46 -0
- package/dist/ml/features.js.map +1 -0
- package/dist/ml/federate.d.ts +70 -0
- package/dist/ml/federate.d.ts.map +1 -0
- package/dist/ml/federate.js +122 -0
- package/dist/ml/federate.js.map +1 -0
- package/dist/ml/head_trainer.d.ts +76 -0
- package/dist/ml/head_trainer.d.ts.map +1 -0
- package/dist/ml/head_trainer.js +182 -0
- package/dist/ml/head_trainer.js.map +1 -0
- package/dist/ml/index.d.ts +23 -0
- package/dist/ml/index.d.ts.map +1 -0
- package/dist/ml/index.js +11 -0
- package/dist/ml/index.js.map +1 -0
- package/dist/ml/registry.d.ts +20 -0
- package/dist/ml/registry.d.ts.map +1 -0
- package/dist/ml/registry.js +43 -0
- package/dist/ml/registry.js.map +1 -0
- package/dist/ml/run.d.ts +94 -0
- package/dist/ml/run.d.ts.map +1 -0
- package/dist/ml/run.js +130 -0
- package/dist/ml/run.js.map +1 -0
- package/dist/ml/tasks/experience.d.ts +90 -0
- package/dist/ml/tasks/experience.d.ts.map +1 -0
- package/dist/ml/tasks/experience.js +156 -0
- package/dist/ml/tasks/experience.js.map +1 -0
- package/dist/ml/tasks/index.d.ts +10 -0
- package/dist/ml/tasks/index.d.ts.map +1 -0
- package/dist/ml/tasks/index.js +28 -0
- package/dist/ml/tasks/index.js.map +1 -0
- package/dist/ml/tasks/local_success.d.ts +55 -0
- package/dist/ml/tasks/local_success.d.ts.map +1 -0
- package/dist/ml/tasks/local_success.js +77 -0
- package/dist/ml/tasks/local_success.js.map +1 -0
- package/dist/ml/tasks/run_anomaly.d.ts +51 -0
- package/dist/ml/tasks/run_anomaly.d.ts.map +1 -0
- package/dist/ml/tasks/run_anomaly.js +60 -0
- package/dist/ml/tasks/run_anomaly.js.map +1 -0
- package/dist/ml/train.d.ts +64 -0
- package/dist/ml/train.d.ts.map +1 -0
- package/dist/ml/train.js +101 -0
- package/dist/ml/train.js.map +1 -0
- package/dist/ml/types.d.ts +112 -0
- package/dist/ml/types.d.ts.map +1 -0
- package/dist/ml/types.js +23 -0
- package/dist/ml/types.js.map +1 -0
- package/dist/moe/moe_model.d.ts.map +1 -1
- package/dist/moe/moe_model.js +1 -16
- package/dist/moe/moe_model.js.map +1 -1
- package/dist/training/cpu_ops.d.ts +2 -0
- package/dist/training/cpu_ops.d.ts.map +1 -1
- package/dist/training/cpu_ops.js +16 -0
- package/dist/training/cpu_ops.js.map +1 -1
- package/package.json +1 -1
- package/src/evermind/delta_wire.ts +148 -114
- package/src/evermind/index.ts +35 -32
- package/src/evermind/merge.ts +53 -0
- package/src/evermind/package.ts +488 -464
- package/src/index.ts +15 -1
- package/src/limbic/index.ts +5 -0
- package/src/limbic/limbic_task.ts +62 -0
- package/src/limbic/limbic_trainer.ts +91 -258
- package/src/ml/classical/ewma.ts +68 -0
- package/src/ml/classical/gbm.ts +143 -0
- package/src/ml/classical/index.ts +76 -0
- package/src/ml/classical/isolation_forest.ts +88 -0
- package/src/ml/classical/kmeans.ts +102 -0
- package/src/ml/classical/knn.ts +98 -0
- package/src/ml/classical/linear.ts +119 -0
- package/src/ml/classical/logistic.ts +125 -0
- package/src/ml/classical/math.ts +93 -0
- package/src/ml/classical/naive_bayes.ts +77 -0
- package/src/ml/classical/robust_z.ts +65 -0
- package/src/ml/dense_head.ts +157 -0
- package/src/ml/features.ts +61 -0
- package/src/ml/federate.ts +160 -0
- package/src/ml/head_trainer.ts +268 -0
- package/src/ml/index.ts +57 -0
- package/src/ml/registry.ts +44 -0
- package/src/ml/run.ts +178 -0
- package/src/ml/tasks/experience.ts +214 -0
- package/src/ml/tasks/index.ts +30 -0
- package/src/ml/tasks/local_success.ts +118 -0
- package/src/ml/tasks/run_anomaly.ts +90 -0
- package/src/ml/train.ts +136 -0
- package/src/ml/types.ts +132 -0
- package/src/moe/moe_model.ts +1 -13
- package/src/training/cpu_ops.ts +141 -127
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ml/federate.ts — trained ML heads federate like the model's own weights.
|
|
3
|
+
*
|
|
4
|
+
* A head's trained state is ONE flat f32 vector (`MlPredictor.exportParams`),
|
|
5
|
+
* stored as a CRC-trailed checkpoint so it is the same shape the delta pipeline
|
|
6
|
+
* already handles: a learner diffs its trained copy against the version it
|
|
7
|
+
* started from (`diffCheckpoints`), ships it on the existing delta wire with
|
|
8
|
+
* `target: "ml:<task>"`, and the writer folds N of them with the existing FedAvg
|
|
9
|
+
* (`mergeCheckpointDiffsGated` → `mergeCheckpointDiffs`), refusing a merge whose
|
|
10
|
+
* held-out loss regresses. No second wire, no second merge.
|
|
11
|
+
*/
|
|
12
|
+
import { buildDeltaLearnPayload, decodeDeltaPayload, deltaUnusableReason, mlHeadTarget, type DeltaLearnPayload } from "../evermind/delta_wire.js";
|
|
13
|
+
import { mergeCheckpointDiffsGated, type GatedMergeResult, type MergeGateOptions } from "../evermind/merge.js";
|
|
14
|
+
import type { EvermindModelPackage } from "../evermind/package.js";
|
|
15
|
+
import { base64ToBytes, bytesToBase64 } from "../utils/base64.js";
|
|
16
|
+
import { appendCrcTrailer, verifyCrcTrailer } from "../utils/crc32.js";
|
|
17
|
+
import { diffCheckpoints } from "../utils/delta.js";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* One trained ML-catalog head carried in a model's manifest (`mlHeads`): small, so
|
|
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
|
+
}
|
|
67
|
+
|
|
68
|
+
/** A manifest entry for a head's current parameters. */
|
|
69
|
+
export function mlHeadEntry(
|
|
70
|
+
task: string,
|
|
71
|
+
engine: string,
|
|
72
|
+
version: number,
|
|
73
|
+
params: ArrayLike<number>,
|
|
74
|
+
shape: Record<string, number> = {},
|
|
75
|
+
evalLoss?: number,
|
|
76
|
+
): EvermindMlHead {
|
|
77
|
+
return {
|
|
78
|
+
task,
|
|
79
|
+
engine,
|
|
80
|
+
version,
|
|
81
|
+
shape,
|
|
82
|
+
params: bytesToBase64(headCheckpoint(params)),
|
|
83
|
+
...(evalLoss != null && Number.isFinite(evalLoss) ? { evalLoss } : {}),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** The entry's flat parameters. */
|
|
88
|
+
export function mlHeadParams(entry: EvermindMlHead): Float32Array {
|
|
89
|
+
return headParams(base64ToBytes(entry.params));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** The head a package carries for `task`, if any. */
|
|
93
|
+
export function packageMlHead(pkg: EvermindModelPackage, task: string): EvermindMlHead | undefined {
|
|
94
|
+
return pkg.manifest.mlHeads?.find((h) => h.task === task);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** `pkg` with `entry` as its head for `entry.task` (replacing the old one). */
|
|
98
|
+
export function withPackageMlHead(pkg: EvermindModelPackage, entry: EvermindMlHead): EvermindModelPackage {
|
|
99
|
+
return pkg.withMlHeads([...(pkg.manifest.mlHeads ?? []).filter((h) => h.task !== entry.task), entry]);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* The delta-wire payload for a locally trained head: its new parameters diffed
|
|
104
|
+
* against the version it started from. The caller owns the size guard, as for LM deltas.
|
|
105
|
+
*/
|
|
106
|
+
export function buildMlHeadDelta(base: EvermindMlHead, trained: ArrayLike<number>, weight?: number, label?: string): DeltaLearnPayload {
|
|
107
|
+
const diff = diffCheckpoints(base64ToBytes(base.params), headCheckpoint(trained));
|
|
108
|
+
return buildDeltaLearnPayload(diff, base.version, weight, label ?? `ml head ${base.task}`, mlHeadTarget(base.task));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export interface MlHeadMergeOutcome {
|
|
112
|
+
/** The head after the round: version + 1 when accepted, the base unchanged when refused. */
|
|
113
|
+
head: EvermindMlHead;
|
|
114
|
+
merge: GatedMergeResult;
|
|
115
|
+
/** Contributions left out of the batch, by index, with why. */
|
|
116
|
+
rejected: { index: number; reason: string }[];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* One federated round for a head: validate each contribution ON ITS OWN (wrong
|
|
121
|
+
* target, stale base or a malformed diff costs only itself), FedAvg the rest, and
|
|
122
|
+
* keep the result only if `score` (held-out loss of a parameter vector, lower is
|
|
123
|
+
* better) does not regress.
|
|
124
|
+
*/
|
|
125
|
+
export function mergeMlHeadDeltas(
|
|
126
|
+
base: EvermindMlHead,
|
|
127
|
+
payloads: readonly DeltaLearnPayload[],
|
|
128
|
+
score: (params: Float32Array) => number | null,
|
|
129
|
+
opts: MergeGateOptions = {},
|
|
130
|
+
): MlHeadMergeOutcome {
|
|
131
|
+
const baseCp = base64ToBytes(base.params);
|
|
132
|
+
const target = mlHeadTarget(base.task);
|
|
133
|
+
const diffs: ArrayBuffer[] = [];
|
|
134
|
+
const weights: number[] = [];
|
|
135
|
+
const rejected: { index: number; reason: string }[] = [];
|
|
136
|
+
payloads.forEach((p, index) => {
|
|
137
|
+
if (p.target !== target) return void rejected.push({ index, reason: `target ${p.target ?? "(model)"} is not ${target}` });
|
|
138
|
+
if (p.baseVersion !== base.version) return void rejected.push({ index, reason: `stale base version ${p.baseVersion} (head is ${base.version})` });
|
|
139
|
+
let diff: ArrayBuffer;
|
|
140
|
+
try {
|
|
141
|
+
diff = decodeDeltaPayload(p);
|
|
142
|
+
} catch (error) {
|
|
143
|
+
return void rejected.push({ index, reason: `diff is not base64: ${error instanceof Error ? error.message : String(error)}` });
|
|
144
|
+
}
|
|
145
|
+
const why = deltaUnusableReason(diff, baseCp);
|
|
146
|
+
if (why) return void rejected.push({ index, reason: why });
|
|
147
|
+
diffs.push(diff);
|
|
148
|
+
weights.push(p.weight ?? 1);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
const merge = mergeCheckpointDiffsGated(baseCp, diffs, weights, (cp) => score(headParams(cp)), opts);
|
|
152
|
+
if (!merge.accepted) return { head: base, merge, rejected };
|
|
153
|
+
const head: EvermindMlHead = {
|
|
154
|
+
...base,
|
|
155
|
+
version: base.version + 1,
|
|
156
|
+
params: bytesToBase64(merge.checkpoint),
|
|
157
|
+
...(merge.newLoss != null ? { evalLoss: merge.newLoss } : {}),
|
|
158
|
+
};
|
|
159
|
+
return { head, merge, rejected };
|
|
160
|
+
}
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ml/head_trainer.ts — HeadTrainer: the ONE gradient trainer for a small head over
|
|
3
|
+
* an embedding (extracted from the limbic affect trainer, which is now a thin
|
|
4
|
+
* registration over it).
|
|
5
|
+
*
|
|
6
|
+
* Full-batch gradient descent with AdamW. When a GPUDevice is supplied the
|
|
7
|
+
* optimiser step runs on the GPU via the shared WEIGHT_UPDATE_WGSL kernel; with
|
|
8
|
+
* no device it uses a numerically-equivalent CPU AdamW, so training works
|
|
9
|
+
* everywhere (CI, Node without @webgpu/node, a laptop with no GPU).
|
|
10
|
+
*
|
|
11
|
+
* The head owns its forward/backward and its loss (MSE for regression, BCE /
|
|
12
|
+
* softmax cross-entropy for classification); this file owns only the loop:
|
|
13
|
+
* zero → accumulate per sample → average → clip → AdamW step.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
createUniformBuffer,
|
|
18
|
+
createStorageBuffer,
|
|
19
|
+
createComputePipeline,
|
|
20
|
+
createBindGroup,
|
|
21
|
+
dispatchKernel,
|
|
22
|
+
readBuffer,
|
|
23
|
+
cdiv,
|
|
24
|
+
} from "../utils/gpu_utils.js";
|
|
25
|
+
import { WEIGHT_UPDATE_WGSL } from "../kernels/weight_update.js";
|
|
26
|
+
|
|
27
|
+
/** A named trainable tensor (flat row-major Float32Array). */
|
|
28
|
+
export interface HeadParam {
|
|
29
|
+
name: string;
|
|
30
|
+
data: Float32Array;
|
|
31
|
+
numel: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** What a head must expose to be trained by {@link HeadTrainer}. */
|
|
35
|
+
export interface TrainableHead<S> {
|
|
36
|
+
/** Trainable parameters, in a stable order. */
|
|
37
|
+
parameters(): HeadParam[];
|
|
38
|
+
/** Gradient buffers, index-aligned with {@link parameters}. */
|
|
39
|
+
gradients(): HeadParam[];
|
|
40
|
+
zeroGrad(): void;
|
|
41
|
+
/** Accumulate gradients for one sample into {@link gradients}; returns its loss. */
|
|
42
|
+
accumulate(sample: S): number;
|
|
43
|
+
/** Forward-only loss of one sample. */
|
|
44
|
+
loss(sample: S): number;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface HeadTrainOptions {
|
|
48
|
+
learningRate?: number;
|
|
49
|
+
epochs?: number;
|
|
50
|
+
weightDecay?: number;
|
|
51
|
+
beta1?: number;
|
|
52
|
+
beta2?: number;
|
|
53
|
+
eps?: number;
|
|
54
|
+
/** Max global gradient L2 norm before the optimiser step. Default 1.0. */
|
|
55
|
+
maxGradNorm?: number;
|
|
56
|
+
/** Per-epoch hook. `gradNorm` is the pre-clip gradient L2 norm this epoch (the
|
|
57
|
+
* instability early-warning) — computed CPU-side so it's always available. */
|
|
58
|
+
onEpochEnd?: ((epoch: number, loss: number, gradNorm?: number) => void) | null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
interface AdamMoment {
|
|
62
|
+
m: Float32Array;
|
|
63
|
+
v: Float32Array;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
interface AdamHyper {
|
|
67
|
+
learningRate: number;
|
|
68
|
+
weightDecay: number;
|
|
69
|
+
beta1: number;
|
|
70
|
+
beta2: number;
|
|
71
|
+
eps: number;
|
|
72
|
+
beta1_t: number;
|
|
73
|
+
beta2_t: number;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function packAdamParams(
|
|
77
|
+
numElements: number,
|
|
78
|
+
lr: number,
|
|
79
|
+
beta1: number,
|
|
80
|
+
beta2: number,
|
|
81
|
+
eps: number,
|
|
82
|
+
weightDecay: number,
|
|
83
|
+
beta1_t: number,
|
|
84
|
+
beta2_t: number,
|
|
85
|
+
): ArrayBuffer {
|
|
86
|
+
const buf = new ArrayBuffer(32);
|
|
87
|
+
new Uint32Array(buf, 0, 1).set([numElements]);
|
|
88
|
+
new Float32Array(buf, 4, 7).set([lr, beta1, beta2, eps, weightDecay, beta1_t, beta2_t]);
|
|
89
|
+
return buf;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export class HeadTrainer<S> {
|
|
93
|
+
readonly head: TrainableHead<S>;
|
|
94
|
+
readonly device: GPUDevice | null;
|
|
95
|
+
private _moments: AdamMoment[] | null = null;
|
|
96
|
+
private _step = 0;
|
|
97
|
+
private readonly _adamwPipeline: GPUComputePipeline | null;
|
|
98
|
+
|
|
99
|
+
constructor(
|
|
100
|
+
head: TrainableHead<S>,
|
|
101
|
+
device: GPUDevice | null = null,
|
|
102
|
+
/** Name used in error messages (the registration that owns this trainer). */
|
|
103
|
+
private readonly label: string = "HeadTrainer",
|
|
104
|
+
) {
|
|
105
|
+
this.head = head;
|
|
106
|
+
this.device = device;
|
|
107
|
+
this._adamwPipeline = device
|
|
108
|
+
? createComputePipeline(device, WEIGHT_UPDATE_WGSL, "adamw_update")
|
|
109
|
+
: null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Whether the optimiser step runs on the GPU. */
|
|
113
|
+
get gpuTraining(): boolean {
|
|
114
|
+
return this.device != null && this._adamwPipeline != null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private _initMoments(): void {
|
|
118
|
+
if (this._moments) return;
|
|
119
|
+
this._moments = this.head.parameters().map((p) => ({
|
|
120
|
+
m: new Float32Array(p.numel),
|
|
121
|
+
v: new Float32Array(p.numel),
|
|
122
|
+
}));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Train on `samples` for `epochs` passes; returns the per-epoch mean loss.
|
|
127
|
+
* Full-batch: grads accumulate over every sample, are averaged, clipped, then
|
|
128
|
+
* applied once per epoch.
|
|
129
|
+
*/
|
|
130
|
+
async train(samples: S[], opts: HeadTrainOptions = {}): Promise<number[]> {
|
|
131
|
+
if (samples.length === 0) throw new Error(`${this.label}.train: no samples`);
|
|
132
|
+
const {
|
|
133
|
+
learningRate = 0.05,
|
|
134
|
+
epochs = 50,
|
|
135
|
+
weightDecay = 0.0,
|
|
136
|
+
beta1 = 0.9,
|
|
137
|
+
beta2 = 0.999,
|
|
138
|
+
eps = 1e-8,
|
|
139
|
+
maxGradNorm = 1.0,
|
|
140
|
+
onEpochEnd = null,
|
|
141
|
+
} = opts;
|
|
142
|
+
|
|
143
|
+
this._initMoments();
|
|
144
|
+
const losses: number[] = [];
|
|
145
|
+
|
|
146
|
+
for (let epoch = 0; epoch < epochs; epoch++) {
|
|
147
|
+
this.head.zeroGrad();
|
|
148
|
+
let epochLoss = 0;
|
|
149
|
+
for (const s of samples) epochLoss += this.head.accumulate(s);
|
|
150
|
+
|
|
151
|
+
const grads = this.head.gradients();
|
|
152
|
+
const invN = 1 / samples.length;
|
|
153
|
+
for (const g of grads) {
|
|
154
|
+
for (let i = 0; i < g.data.length; i++) g.data[i]! *= invN;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const gradNorm = clipGradients(grads, maxGradNorm);
|
|
158
|
+
|
|
159
|
+
this._step++;
|
|
160
|
+
const beta1_t = Math.pow(beta1, this._step);
|
|
161
|
+
const beta2_t = Math.pow(beta2, this._step);
|
|
162
|
+
const hp: AdamHyper = { learningRate, weightDecay, beta1, beta2, eps, beta1_t, beta2_t };
|
|
163
|
+
if (this.gpuTraining) {
|
|
164
|
+
await this._adamwStepGpu(grads, hp);
|
|
165
|
+
} else {
|
|
166
|
+
this._adamwStepCpu(grads, hp);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const avg = epochLoss / samples.length;
|
|
170
|
+
losses.push(avg);
|
|
171
|
+
if (onEpochEnd) onEpochEnd(epoch + 1, avg, gradNorm);
|
|
172
|
+
}
|
|
173
|
+
return losses;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Mean loss over samples (no weight update). 0 for an empty set. */
|
|
177
|
+
evaluate(samples: S[]): number {
|
|
178
|
+
if (samples.length === 0) return 0;
|
|
179
|
+
let total = 0;
|
|
180
|
+
for (const s of samples) total += this.head.loss(s);
|
|
181
|
+
return total / samples.length;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
private _adamwStepCpu(grads: HeadParam[], hp: AdamHyper): void {
|
|
185
|
+
const params = this.head.parameters();
|
|
186
|
+
const { learningRate: lr, weightDecay: wd, beta1, beta2, eps, beta1_t, beta2_t } = hp;
|
|
187
|
+
for (let pi = 0; pi < params.length; pi++) {
|
|
188
|
+
const p = params[pi]!.data;
|
|
189
|
+
const g = grads[pi]!.data;
|
|
190
|
+
const mom = this._moments![pi]!;
|
|
191
|
+
for (let i = 0; i < p.length; i++) {
|
|
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
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** AdamW on the GPU via the shared WEIGHT_UPDATE_WGSL kernel. Awaited per step. */
|
|
203
|
+
private async _adamwStepGpu(grads: HeadParam[], hp: AdamHyper): Promise<void> {
|
|
204
|
+
const device = this.device!;
|
|
205
|
+
const pipeline = this._adamwPipeline!;
|
|
206
|
+
const params = this.head.parameters();
|
|
207
|
+
const { learningRate: lr, weightDecay: wd, beta1, beta2, eps, beta1_t, beta2_t } = hp;
|
|
208
|
+
|
|
209
|
+
for (let pi = 0; pi < params.length; pi++) {
|
|
210
|
+
const p = params[pi]!;
|
|
211
|
+
const mom = this._moments![pi]!;
|
|
212
|
+
const paramBuf = createStorageBuffer(device, p.data, true);
|
|
213
|
+
const gradBuf = createStorageBuffer(device, grads[pi]!.data, false);
|
|
214
|
+
const mBuf = createStorageBuffer(device, mom.m, true);
|
|
215
|
+
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
|
+
);
|
|
220
|
+
const bg = createBindGroup(device, pipeline, [uni, paramBuf, gradBuf, mBuf, vBuf]);
|
|
221
|
+
dispatchKernel(device, pipeline, bg, [cdiv(p.numel, 256), 1, 1]);
|
|
222
|
+
|
|
223
|
+
p.data.set((await readBuffer(device, paramBuf, p.numel * 4)).subarray(0, p.numel));
|
|
224
|
+
mom.m.set((await readBuffer(device, mBuf, p.numel * 4)).subarray(0, p.numel));
|
|
225
|
+
mom.v.set((await readBuffer(device, vBuf, p.numel * 4)).subarray(0, p.numel));
|
|
226
|
+
|
|
227
|
+
paramBuf.destroy();
|
|
228
|
+
gradBuf.destroy();
|
|
229
|
+
mBuf.destroy();
|
|
230
|
+
vBuf.destroy();
|
|
231
|
+
uni.destroy();
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** Clip grads to `maxNorm` in place; returns the PRE-clip L2 norm (the signal). */
|
|
237
|
+
function clipGradients(grads: HeadParam[], maxNorm: number): number {
|
|
238
|
+
let normSq = 0;
|
|
239
|
+
for (const g of grads) for (let i = 0; i < g.data.length; i++) normSq += g.data[i]! * g.data[i]!;
|
|
240
|
+
const norm = Math.sqrt(normSq);
|
|
241
|
+
if (norm > maxNorm && norm > 0) {
|
|
242
|
+
const scale = maxNorm / norm;
|
|
243
|
+
for (const g of grads) for (let i = 0; i < g.data.length; i++) g.data[i]! *= scale;
|
|
244
|
+
}
|
|
245
|
+
return norm;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** Concatenate a head's parameters into one flat vector (the federation unit). */
|
|
249
|
+
export function flattenParams(params: readonly HeadParam[]): Float32Array {
|
|
250
|
+
const out = new Float32Array(params.reduce((n, p) => n + p.numel, 0));
|
|
251
|
+
let o = 0;
|
|
252
|
+
for (const p of params) {
|
|
253
|
+
out.set(p.data, o);
|
|
254
|
+
o += p.numel;
|
|
255
|
+
}
|
|
256
|
+
return out;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** Write a flat vector back into a head's parameters (inverse of {@link flattenParams}). */
|
|
260
|
+
export function assignParams(params: readonly HeadParam[], flat: ArrayLike<number>): void {
|
|
261
|
+
const total = params.reduce((n, p) => n + p.numel, 0);
|
|
262
|
+
if (flat.length !== total) throw new Error(`assignParams: expected ${total} values, got ${flat.length}`);
|
|
263
|
+
let o = 0;
|
|
264
|
+
for (const p of params) {
|
|
265
|
+
for (let i = 0; i < p.numel; i++) p.data[i] = flat[o + i]!;
|
|
266
|
+
o += p.numel;
|
|
267
|
+
}
|
|
268
|
+
}
|
package/src/ml/index.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ml — the ML task catalog (docs/ml-task-catalog.md): small, purpose-built models
|
|
3
|
+
* for bounded questions, answered on the person's own machine without a GPU or an
|
|
4
|
+
* LLM call. ONE registry, ONE runner, ONE trainer seam; trained heads federate on
|
|
5
|
+
* the existing delta wire. Pure and portable: no vscode, no DOM, no network.
|
|
6
|
+
*/
|
|
7
|
+
export type {
|
|
8
|
+
MlTask,
|
|
9
|
+
MlTaskKind,
|
|
10
|
+
MlEngineRef,
|
|
11
|
+
ClassicalAlgorithm,
|
|
12
|
+
InferenceMode,
|
|
13
|
+
MlPrediction,
|
|
14
|
+
MlPredictor,
|
|
15
|
+
FeatureSpec,
|
|
16
|
+
MlSample,
|
|
17
|
+
TrainSpec,
|
|
18
|
+
OutputSpec,
|
|
19
|
+
} from "./types.js";
|
|
20
|
+
export { engineKey, ML_TASK_ID_RE } from "./types.js";
|
|
21
|
+
|
|
22
|
+
export { MlTaskRegistry } from "./registry.js";
|
|
23
|
+
export {
|
|
24
|
+
runMlTask,
|
|
25
|
+
candidateEngines,
|
|
26
|
+
InMemoryMlModels,
|
|
27
|
+
MlDecisionLog,
|
|
28
|
+
MlEngineUnavailableError,
|
|
29
|
+
} from "./run.js";
|
|
30
|
+
export type { MlRunContext, MlResult, MlModelStore, MlCloudPort, MlDecision, MlDecisionSink, MlSkipReason } from "./run.js";
|
|
31
|
+
|
|
32
|
+
export { trainMlTask, mlEvalLoss } from "./train.js";
|
|
33
|
+
export type { MlTrainExample, MlTrainOptions, MlTrainResult } from "./train.js";
|
|
34
|
+
|
|
35
|
+
export { HeadTrainer, flattenParams, assignParams } from "./head_trainer.js";
|
|
36
|
+
export type { HeadParam, TrainableHead, HeadTrainOptions } from "./head_trainer.js";
|
|
37
|
+
export { DenseHead } from "./dense_head.js";
|
|
38
|
+
export type { DenseHeadConfig } from "./dense_head.js";
|
|
39
|
+
|
|
40
|
+
export { extractFeatures, extractFeatureMatrix, logScale, clampRatio } from "./features.js";
|
|
41
|
+
|
|
42
|
+
export * from "./classical/index.js";
|
|
43
|
+
|
|
44
|
+
export {
|
|
45
|
+
headCheckpoint,
|
|
46
|
+
headParams,
|
|
47
|
+
mlHeadEntry,
|
|
48
|
+
mlHeadParams,
|
|
49
|
+
mlHeadUnusableReason,
|
|
50
|
+
packageMlHead,
|
|
51
|
+
withPackageMlHead,
|
|
52
|
+
buildMlHeadDelta,
|
|
53
|
+
mergeMlHeadDeltas,
|
|
54
|
+
} from "./federate.js";
|
|
55
|
+
export type { EvermindMlHead, MlHeadMergeOutcome } from "./federate.js";
|
|
56
|
+
|
|
57
|
+
export * from "./tasks/index.js";
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ml/registry.ts — the ONE registry of ML tasks, keyed by task id.
|
|
3
|
+
*
|
|
4
|
+
* A new task is a new row, not a new module. The registry only stores and looks
|
|
5
|
+
* up rows; running lives in `run.ts`, training in `train.ts`.
|
|
6
|
+
*/
|
|
7
|
+
import { ML_TASK_ID_RE, type MlTask } from "./types.js";
|
|
8
|
+
|
|
9
|
+
export class MlTaskRegistry {
|
|
10
|
+
private readonly _tasks = new Map<string, MlTask<any, any>>();
|
|
11
|
+
|
|
12
|
+
/** Register a row. Throws on a malformed id, a duplicate id, or a row with no engine. */
|
|
13
|
+
register<I, O>(task: MlTask<I, O>): MlTask<I, O> {
|
|
14
|
+
if (!ML_TASK_ID_RE.test(task.id)) throw new Error(`MlTaskRegistry: malformed task id '${task.id}'`);
|
|
15
|
+
if (this._tasks.has(task.id)) throw new Error(`MlTaskRegistry: task '${task.id}' is already registered`);
|
|
16
|
+
if (task.engines.length === 0) throw new Error(`MlTaskRegistry: task '${task.id}' lists no engine`);
|
|
17
|
+
if (task.kind === "classify" && !(task.output.labels && task.output.labels.length >= 2)) {
|
|
18
|
+
throw new Error(`MlTaskRegistry: classify task '${task.id}' needs at least two output labels`);
|
|
19
|
+
}
|
|
20
|
+
this._tasks.set(task.id, task);
|
|
21
|
+
return task;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** The row for `id`, or undefined. */
|
|
25
|
+
get<I = unknown, O = unknown>(id: string): MlTask<I, O> | undefined {
|
|
26
|
+
return this._tasks.get(id) as MlTask<I, O> | undefined;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** The row for `id`; throws when it is not registered. */
|
|
30
|
+
require<I = unknown, O = unknown>(id: string): MlTask<I, O> {
|
|
31
|
+
const t = this.get<I, O>(id);
|
|
32
|
+
if (!t) throw new Error(`MlTaskRegistry: unknown task '${id}'`);
|
|
33
|
+
return t;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
has(id: string): boolean {
|
|
37
|
+
return this._tasks.has(id);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Every row, in registration order. */
|
|
41
|
+
list(): MlTask<any, any>[] {
|
|
42
|
+
return [...this._tasks.values()];
|
|
43
|
+
}
|
|
44
|
+
}
|