@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.
Files changed (170) hide show
  1. package/dist/evermind/delta_wire.d.ts +10 -1
  2. package/dist/evermind/delta_wire.d.ts.map +1 -1
  3. package/dist/evermind/delta_wire.js +25 -3
  4. package/dist/evermind/delta_wire.js.map +1 -1
  5. package/dist/evermind/index.d.ts +4 -4
  6. package/dist/evermind/index.d.ts.map +1 -1
  7. package/dist/evermind/index.js +2 -2
  8. package/dist/evermind/index.js.map +1 -1
  9. package/dist/evermind/merge.d.ts +25 -0
  10. package/dist/evermind/merge.d.ts.map +1 -1
  11. package/dist/evermind/merge.js +30 -0
  12. package/dist/evermind/merge.js.map +1 -1
  13. package/dist/evermind/package.d.ts +9 -0
  14. package/dist/evermind/package.d.ts.map +1 -1
  15. package/dist/evermind/package.js +20 -0
  16. package/dist/evermind/package.js.map +1 -1
  17. package/dist/index.d.ts +6 -5
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +5 -3
  20. package/dist/index.js.map +1 -1
  21. package/dist/limbic/index.d.ts +3 -0
  22. package/dist/limbic/index.d.ts.map +1 -1
  23. package/dist/limbic/index.js +3 -0
  24. package/dist/limbic/index.js.map +1 -1
  25. package/dist/limbic/limbic_task.d.ts +22 -0
  26. package/dist/limbic/limbic_task.d.ts.map +1 -0
  27. package/dist/limbic/limbic_task.js +45 -0
  28. package/dist/limbic/limbic_task.js.map +1 -0
  29. package/dist/limbic/limbic_trainer.d.ts +17 -30
  30. package/dist/limbic/limbic_trainer.d.ts.map +1 -1
  31. package/dist/limbic/limbic_trainer.js +38 -144
  32. package/dist/limbic/limbic_trainer.js.map +1 -1
  33. package/dist/ml/classical/ewma.d.ts +32 -0
  34. package/dist/ml/classical/ewma.d.ts.map +1 -0
  35. package/dist/ml/classical/ewma.js +46 -0
  36. package/dist/ml/classical/ewma.js.map +1 -0
  37. package/dist/ml/classical/gbm.d.ts +42 -0
  38. package/dist/ml/classical/gbm.d.ts.map +1 -0
  39. package/dist/ml/classical/gbm.js +116 -0
  40. package/dist/ml/classical/gbm.js.map +1 -0
  41. package/dist/ml/classical/index.d.ts +48 -0
  42. package/dist/ml/classical/index.d.ts.map +1 -0
  43. package/dist/ml/classical/index.js +45 -0
  44. package/dist/ml/classical/index.js.map +1 -0
  45. package/dist/ml/classical/isolation_forest.d.ts +37 -0
  46. package/dist/ml/classical/isolation_forest.d.ts.map +1 -0
  47. package/dist/ml/classical/isolation_forest.js +72 -0
  48. package/dist/ml/classical/isolation_forest.js.map +1 -0
  49. package/dist/ml/classical/kmeans.d.ts +31 -0
  50. package/dist/ml/classical/kmeans.d.ts.map +1 -0
  51. package/dist/ml/classical/kmeans.js +93 -0
  52. package/dist/ml/classical/kmeans.js.map +1 -0
  53. package/dist/ml/classical/knn.d.ts +48 -0
  54. package/dist/ml/classical/knn.d.ts.map +1 -0
  55. package/dist/ml/classical/knn.js +68 -0
  56. package/dist/ml/classical/knn.js.map +1 -0
  57. package/dist/ml/classical/linear.d.ts +29 -0
  58. package/dist/ml/classical/linear.d.ts.map +1 -0
  59. package/dist/ml/classical/linear.js +114 -0
  60. package/dist/ml/classical/linear.js.map +1 -0
  61. package/dist/ml/classical/logistic.d.ts +39 -0
  62. package/dist/ml/classical/logistic.d.ts.map +1 -0
  63. package/dist/ml/classical/logistic.js +103 -0
  64. package/dist/ml/classical/logistic.js.map +1 -0
  65. package/dist/ml/classical/math.d.ts +26 -0
  66. package/dist/ml/classical/math.d.ts.map +1 -0
  67. package/dist/ml/classical/math.js +90 -0
  68. package/dist/ml/classical/math.js.map +1 -0
  69. package/dist/ml/classical/naive_bayes.d.ts +24 -0
  70. package/dist/ml/classical/naive_bayes.d.ts.map +1 -0
  71. package/dist/ml/classical/naive_bayes.js +67 -0
  72. package/dist/ml/classical/naive_bayes.js.map +1 -0
  73. package/dist/ml/classical/robust_z.d.ts +30 -0
  74. package/dist/ml/classical/robust_z.d.ts.map +1 -0
  75. package/dist/ml/classical/robust_z.js +46 -0
  76. package/dist/ml/classical/robust_z.js.map +1 -0
  77. package/dist/ml/dense_head.d.ts +33 -0
  78. package/dist/ml/dense_head.d.ts.map +1 -0
  79. package/dist/ml/dense_head.js +140 -0
  80. package/dist/ml/dense_head.js.map +1 -0
  81. package/dist/ml/features.d.ts +25 -0
  82. package/dist/ml/features.d.ts.map +1 -0
  83. package/dist/ml/features.js +46 -0
  84. package/dist/ml/features.js.map +1 -0
  85. package/dist/ml/federate.d.ts +70 -0
  86. package/dist/ml/federate.d.ts.map +1 -0
  87. package/dist/ml/federate.js +122 -0
  88. package/dist/ml/federate.js.map +1 -0
  89. package/dist/ml/head_trainer.d.ts +76 -0
  90. package/dist/ml/head_trainer.d.ts.map +1 -0
  91. package/dist/ml/head_trainer.js +182 -0
  92. package/dist/ml/head_trainer.js.map +1 -0
  93. package/dist/ml/index.d.ts +23 -0
  94. package/dist/ml/index.d.ts.map +1 -0
  95. package/dist/ml/index.js +11 -0
  96. package/dist/ml/index.js.map +1 -0
  97. package/dist/ml/registry.d.ts +20 -0
  98. package/dist/ml/registry.d.ts.map +1 -0
  99. package/dist/ml/registry.js +43 -0
  100. package/dist/ml/registry.js.map +1 -0
  101. package/dist/ml/run.d.ts +94 -0
  102. package/dist/ml/run.d.ts.map +1 -0
  103. package/dist/ml/run.js +130 -0
  104. package/dist/ml/run.js.map +1 -0
  105. package/dist/ml/tasks/experience.d.ts +90 -0
  106. package/dist/ml/tasks/experience.d.ts.map +1 -0
  107. package/dist/ml/tasks/experience.js +156 -0
  108. package/dist/ml/tasks/experience.js.map +1 -0
  109. package/dist/ml/tasks/index.d.ts +10 -0
  110. package/dist/ml/tasks/index.d.ts.map +1 -0
  111. package/dist/ml/tasks/index.js +28 -0
  112. package/dist/ml/tasks/index.js.map +1 -0
  113. package/dist/ml/tasks/local_success.d.ts +55 -0
  114. package/dist/ml/tasks/local_success.d.ts.map +1 -0
  115. package/dist/ml/tasks/local_success.js +77 -0
  116. package/dist/ml/tasks/local_success.js.map +1 -0
  117. package/dist/ml/tasks/run_anomaly.d.ts +51 -0
  118. package/dist/ml/tasks/run_anomaly.d.ts.map +1 -0
  119. package/dist/ml/tasks/run_anomaly.js +60 -0
  120. package/dist/ml/tasks/run_anomaly.js.map +1 -0
  121. package/dist/ml/train.d.ts +64 -0
  122. package/dist/ml/train.d.ts.map +1 -0
  123. package/dist/ml/train.js +101 -0
  124. package/dist/ml/train.js.map +1 -0
  125. package/dist/ml/types.d.ts +112 -0
  126. package/dist/ml/types.d.ts.map +1 -0
  127. package/dist/ml/types.js +23 -0
  128. package/dist/ml/types.js.map +1 -0
  129. package/dist/moe/moe_model.d.ts.map +1 -1
  130. package/dist/moe/moe_model.js +1 -16
  131. package/dist/moe/moe_model.js.map +1 -1
  132. package/dist/training/cpu_ops.d.ts +2 -0
  133. package/dist/training/cpu_ops.d.ts.map +1 -1
  134. package/dist/training/cpu_ops.js +16 -0
  135. package/dist/training/cpu_ops.js.map +1 -1
  136. package/package.json +1 -1
  137. package/src/evermind/delta_wire.ts +148 -114
  138. package/src/evermind/index.ts +35 -32
  139. package/src/evermind/merge.ts +53 -0
  140. package/src/evermind/package.ts +488 -464
  141. package/src/index.ts +15 -1
  142. package/src/limbic/index.ts +5 -0
  143. package/src/limbic/limbic_task.ts +62 -0
  144. package/src/limbic/limbic_trainer.ts +91 -258
  145. package/src/ml/classical/ewma.ts +68 -0
  146. package/src/ml/classical/gbm.ts +143 -0
  147. package/src/ml/classical/index.ts +76 -0
  148. package/src/ml/classical/isolation_forest.ts +88 -0
  149. package/src/ml/classical/kmeans.ts +102 -0
  150. package/src/ml/classical/knn.ts +98 -0
  151. package/src/ml/classical/linear.ts +119 -0
  152. package/src/ml/classical/logistic.ts +125 -0
  153. package/src/ml/classical/math.ts +93 -0
  154. package/src/ml/classical/naive_bayes.ts +77 -0
  155. package/src/ml/classical/robust_z.ts +65 -0
  156. package/src/ml/dense_head.ts +157 -0
  157. package/src/ml/features.ts +61 -0
  158. package/src/ml/federate.ts +160 -0
  159. package/src/ml/head_trainer.ts +268 -0
  160. package/src/ml/index.ts +57 -0
  161. package/src/ml/registry.ts +44 -0
  162. package/src/ml/run.ts +178 -0
  163. package/src/ml/tasks/experience.ts +214 -0
  164. package/src/ml/tasks/index.ts +30 -0
  165. package/src/ml/tasks/local_success.ts +118 -0
  166. package/src/ml/tasks/run_anomaly.ts +90 -0
  167. package/src/ml/train.ts +136 -0
  168. package/src/ml/types.ts +132 -0
  169. package/src/moe/moe_model.ts +1 -13
  170. package/src/training/cpu_ops.ts +141 -127
@@ -0,0 +1,136 @@
1
+ /**
2
+ * ml/train.ts — the ONE trainer seam for catalog tasks.
3
+ *
4
+ * Classical engines fit in closed form or by small iteration (`classical/`);
5
+ * head engines train a {@link DenseHead} through the shared {@link HeadTrainer}
6
+ * (GPU AdamW when a device is supplied, CPU AdamW otherwise). A task can name its
7
+ * own composite fit (`TrainSpec.fit`), or say that something else trains it
8
+ * (`TrainSpec.trainedBy`, e.g. `LimbicTrainer` for `limbic.affect`).
9
+ *
10
+ * Held-out loss is measured the same way everywhere ({@link mlEvalLoss}) — it is
11
+ * also what gates a federated merge.
12
+ */
13
+ import { fitClassical, type ClassicalOptions } from "./classical/index.js";
14
+ import { DenseHead } from "./dense_head.js";
15
+ import { extractFeatureMatrix, type TextEmbedder } from "./features.js";
16
+ import { HeadTrainer, type HeadTrainOptions } from "./head_trainer.js";
17
+ import type { MlModelStore } from "./run.js";
18
+ import { engineKey, type MlEngineRef, type MlPredictor, type MlSample, type MlTask, type MlTaskKind } from "./types.js";
19
+
20
+ /** One labelled example in input space. */
21
+ export interface MlTrainExample<I> {
22
+ input: I;
23
+ /** classify: class index; regress: target value; cluster/anomaly: ignored (use 0). */
24
+ label: number;
25
+ weight?: number;
26
+ }
27
+
28
+ export interface MlTrainOptions {
29
+ /** Engine to fit (default: the task's first trainable local engine). */
30
+ engine?: MlEngineRef;
31
+ embedder?: TextEmbedder;
32
+ device?: GPUDevice | null;
33
+ classical?: ClassicalOptions;
34
+ head?: { hiddenDim?: number; seed?: number; train?: HeadTrainOptions };
35
+ /** Overrides the task's `holdoutFraction`. 0 disables the held-out measurement. */
36
+ holdoutFraction?: number;
37
+ /** After measuring on the held-out tail, refit on everything. Default true. */
38
+ refitOnAll?: boolean;
39
+ /** Store the fitted predictor here under (task id, engine key). */
40
+ models?: MlModelStore;
41
+ }
42
+
43
+ export interface MlTrainResult {
44
+ predictor: MlPredictor;
45
+ engine: MlEngineRef;
46
+ /** Examples the final fit saw. */
47
+ samples: number;
48
+ /** Size of the held-out tail, and the model's loss on it (null when none was held out). */
49
+ holdout: number;
50
+ holdoutLoss: number | null;
51
+ }
52
+
53
+ /**
54
+ * Mean loss of `predictor` on `samples` for a task of `kind` — classify: weighted
55
+ * cross-entropy of the true class; regress: weighted ½·squared error. Null for
56
+ * kinds without a supervised loss, or an empty set.
57
+ */
58
+ export function mlEvalLoss(kind: MlTaskKind, predictor: MlPredictor, samples: readonly MlSample[]): number | null {
59
+ if (samples.length === 0 || (kind !== "classify" && kind !== "regress")) return null;
60
+ let sum = 0;
61
+ let wSum = 0;
62
+ for (const s of samples) {
63
+ const w = s.weight ?? 1;
64
+ const p = predictor.predict(s.x);
65
+ const loss = kind === "classify" ? -Math.log(Math.max(p.scores[s.y] ?? 0, 1e-12)) : 0.5 * (p.scores[0]! - s.y) ** 2;
66
+ sum += w * loss;
67
+ wSum += w;
68
+ }
69
+ return wSum > 0 ? sum / wSum : null;
70
+ }
71
+
72
+ /** The task's first local engine the seam can train. */
73
+ function defaultEngine(task: MlTask<any, any>): MlEngineRef {
74
+ const e = task.engines.find((r) => r.tier === "local" && (r.engine === "classical" || r.engine === "head"));
75
+ if (!e) throw new Error(`trainMlTask(${task.id}): no trainable local engine`);
76
+ return e;
77
+ }
78
+
79
+ async function fitEngine(task: MlTask<any, any>, ref: MlEngineRef, samples: MlSample[], opts: MlTrainOptions): Promise<MlPredictor> {
80
+ if (task.train?.fit) return task.train.fit(samples, ref);
81
+ if (ref.engine === "classical") return fitClassical(ref.algorithm, samples, opts.classical);
82
+ if (ref.engine === "head") {
83
+ const objective = task.kind === "classify" ? "softmax" : task.kind === "regress" ? "mse" : null;
84
+ if (!objective) throw new Error(`trainMlTask(${task.id}): a head engine trains classify or regress tasks only`);
85
+ const head = new DenseHead({
86
+ inputDim: samples[0]!.x.length,
87
+ hiddenDim: ref.head === "linear" ? 0 : (opts.head?.hiddenDim ?? 16),
88
+ outputDim: objective === "softmax" ? task.output.labels!.length : 1,
89
+ objective,
90
+ ...(opts.head?.seed != null ? { seed: opts.head.seed } : {}),
91
+ });
92
+ await new HeadTrainer(head, opts.device ?? null, `trainMlTask(${task.id})`).train(samples, opts.head?.train);
93
+ return head;
94
+ }
95
+ throw new Error(`trainMlTask(${task.id}): engine '${engineKey(ref)}' is not trained here`);
96
+ }
97
+
98
+ /**
99
+ * Fit task `task` on labelled examples. Returns null when there are fewer than
100
+ * the task's `minSamples` (it stays untrained). Throws for a task with no train
101
+ * spec, or one another trainer owns.
102
+ */
103
+ export async function trainMlTask<I>(
104
+ task: MlTask<I, any>,
105
+ examples: readonly MlTrainExample<I>[],
106
+ opts: MlTrainOptions = {},
107
+ ): Promise<MlTrainResult | null> {
108
+ const spec = task.train;
109
+ if (!spec) throw new Error(`trainMlTask(${task.id}): task is rule-based or pre-trained only`);
110
+ if (spec.trainedBy) throw new Error(`trainMlTask(${task.id}): trained by ${spec.trainedBy}`);
111
+ if (examples.length < spec.minSamples) return null;
112
+
113
+ const ref = opts.engine ?? defaultEngine(task);
114
+ const X = await extractFeatureMatrix(task.features, examples.map((e) => e.input), opts.embedder);
115
+ const samples: MlSample[] = examples.map((e, i) => ({ x: X[i]!, y: e.label, ...(e.weight != null ? { weight: e.weight } : {}) }));
116
+
117
+ // Hold out the chronological TAIL: examples arrive oldest first, and the question
118
+ // is always "does this generalise to what comes next".
119
+ const supervised = task.kind === "classify" || task.kind === "regress";
120
+ const frac = supervised ? (opts.holdoutFraction ?? spec.holdoutFraction ?? 0.2) : 0;
121
+ const nHold = Math.floor(samples.length * frac);
122
+ let holdoutLoss: number | null = null;
123
+ if (nHold >= 1 && samples.length - nHold >= 1) {
124
+ const train = samples.slice(0, samples.length - nHold);
125
+ const hold = samples.slice(samples.length - nHold);
126
+ const predictor = await fitEngine(task, ref, train, opts);
127
+ holdoutLoss = mlEvalLoss(task.kind, predictor, hold);
128
+ if (opts.refitOnAll === false) {
129
+ opts.models?.set(task.id, engineKey(ref), predictor);
130
+ return { predictor, engine: ref, samples: train.length, holdout: nHold, holdoutLoss };
131
+ }
132
+ }
133
+ const predictor = await fitEngine(task, ref, samples, opts);
134
+ opts.models?.set(task.id, engineKey(ref), predictor);
135
+ return { predictor, engine: ref, samples: samples.length, holdout: nHold >= 1 ? nHold : 0, holdoutLoss };
136
+ }
@@ -0,0 +1,132 @@
1
+ /**
2
+ * ml/types.ts — the ML task catalog contract (docs/ml-task-catalog.md §3).
3
+ *
4
+ * One bounded question Evermind answers without an LLM is ONE data row: how its
5
+ * input becomes a vector, which engines may answer it (in preference order), how
6
+ * it trains, and what its output means. Pure types — no vscode, no DOM, no network.
7
+ */
8
+
9
+ /** The shape of the question. */
10
+ export type MlTaskKind = "classify" | "regress" | "cluster" | "anomaly" | "rank";
11
+
12
+ /** Pure-TypeScript algorithms that run everywhere (`ml/classical/`). */
13
+ export type ClassicalAlgorithm =
14
+ | "knn"
15
+ | "naive-bayes"
16
+ | "logistic"
17
+ | "linear"
18
+ | "kmeans"
19
+ | "robust-z"
20
+ | "ewma"
21
+ | "isolation-forest"
22
+ | "gbm-small";
23
+
24
+ /** An engine that can answer a task. Cloud engines are a port the HOST supplies. */
25
+ export type MlEngineRef =
26
+ | { tier: "local"; engine: "classical"; algorithm: ClassicalAlgorithm }
27
+ | { tier: "local"; engine: "head"; head: "mlp" | "linear" }
28
+ | { tier: "local"; engine: "onnx"; model: string }
29
+ | { tier: "cloud"; engine: "workers-ai"; model: string };
30
+
31
+ /**
32
+ * Which engines a run may use — the person's inference setting.
33
+ * - `local-only`: local tiers only, never the network.
34
+ * - `hybrid`: local first, the cloud port when no local engine can answer (or the
35
+ * local answer is below the caller's confidence floor).
36
+ * - `specific`: exactly the engine the caller names.
37
+ */
38
+ export type InferenceMode = "local-only" | "hybrid" | "specific";
39
+
40
+ /** What an engine returns before the task decodes it. */
41
+ export interface MlPrediction {
42
+ /**
43
+ * classify: one probability per label; regress: the predicted value(s);
44
+ * cluster: `[clusterId]`; anomaly: `[score]` where ≥ 1 is anomalous;
45
+ * rank: one score per candidate.
46
+ */
47
+ scores: number[];
48
+ /** Calibrated confidence in [0, 1]. */
49
+ confidence: number;
50
+ }
51
+
52
+ /** A fitted model that answers on a feature vector. */
53
+ export interface MlPredictor {
54
+ predict(x: Float32Array): MlPrediction;
55
+ /** Trainable state as one flat vector — present on models that federate. */
56
+ exportParams?(): Float32Array;
57
+ /** Load a flat vector produced by {@link exportParams} (e.g. after a merge). */
58
+ importParams?(params: ArrayLike<number>): void;
59
+ }
60
+
61
+ /** How an input becomes a vector: an embedded text part, numeric fields, or both. */
62
+ export interface FeatureSpec<I> {
63
+ /** Text to embed (needs an embedder in the run context). */
64
+ text?: (input: I) => string;
65
+ /** Numeric fields, already on a stable scale (so federated weights mean the same thing everywhere). */
66
+ numeric?: (input: I) => number[];
67
+ /** Names of the numeric fields, index-aligned — documentation and Insights labels. */
68
+ numericNames?: readonly string[];
69
+ }
70
+
71
+ /** One labelled training example in feature space. */
72
+ export interface MlSample {
73
+ x: Float32Array;
74
+ /** classify: class index; regress: target value; cluster/anomaly: ignored. */
75
+ y: number;
76
+ /** Sample weight (default 1). */
77
+ weight?: number;
78
+ }
79
+
80
+ /** How a task trains. Absent on the task = rule-based or pre-trained only. */
81
+ export interface TrainSpec {
82
+ /** Fewer labelled examples than this and the task stays untrained. */
83
+ minSamples: number;
84
+ /** Fraction held out for the eval gate (default 0.2). */
85
+ holdoutFraction?: number;
86
+ /**
87
+ * A task-specific fit (e.g. a composite of two classical detectors). When absent
88
+ * the trainer fits the engine named by the run (`ml/train.ts`).
89
+ */
90
+ fit?: (samples: readonly MlSample[], engine: MlEngineRef) => MlPredictor;
91
+ /** Who trains it when not `ml/train.ts` (e.g. `LimbicTrainer` for `limbic.affect`). */
92
+ trainedBy?: string;
93
+ }
94
+
95
+ /** What a task's answer means. */
96
+ export interface OutputSpec<O> {
97
+ /** Label set for classify tasks (index-aligned with `MlPrediction.scores`). */
98
+ labels?: readonly string[];
99
+ /** Decode an engine prediction into the task's output. */
100
+ decode(p: MlPrediction): O;
101
+ }
102
+
103
+ /** One bounded question Evermind can answer without an LLM. DATA, not a class tree. */
104
+ export interface MlTask<I = unknown, O = unknown> {
105
+ kind: MlTaskKind;
106
+ /** Stable id, e.g. `experience.quality`. */
107
+ id: string;
108
+ /** One line on what it answers. */
109
+ description: string;
110
+ features: FeatureSpec<I>;
111
+ /** In preference order; filtered by the inference mode. */
112
+ engines: readonly MlEngineRef[];
113
+ train?: TrainSpec;
114
+ output: OutputSpec<O>;
115
+ }
116
+
117
+ /** Task ids: lowercase dotted segments, e.g. `run.local-success`. */
118
+ export const ML_TASK_ID_RE = /^[a-z][a-z0-9-]*(\.[a-z][a-z0-9-]*)+$/;
119
+
120
+ /** A stable string key for an engine ref, e.g. `local:classical:logistic`. */
121
+ export function engineKey(ref: MlEngineRef): string {
122
+ switch (ref.engine) {
123
+ case "classical":
124
+ return `local:classical:${ref.algorithm}`;
125
+ case "head":
126
+ return `local:head:${ref.head}`;
127
+ case "onnx":
128
+ return `local:onnx:${ref.model}`;
129
+ case "workers-ai":
130
+ return `cloud:workers-ai:${ref.model}`;
131
+ }
132
+ }
@@ -25,6 +25,7 @@
25
25
 
26
26
  import { SeededRng } from "../utils/rng.js";
27
27
  import { quantizeFp16, dequantizeFp16 } from "../utils/quantization.js";
28
+ import { softmax } from "../training/cpu_ops.js";
28
29
 
29
30
  export interface MoEConfig {
30
31
  /** Model (token) dimension — FFN input/output width. Default 64. */
@@ -462,16 +463,3 @@ export class SharedExpertMoE {
462
463
  }
463
464
  }
464
465
 
465
- /** Numerically-stable softmax over a flat array. */
466
- function softmax(logits: Float32Array): Float32Array {
467
- let max = -Infinity;
468
- for (let i = 0; i < logits.length; i++) if (logits[i]! > max) max = logits[i]!;
469
- const out = new Float32Array(logits.length);
470
- let sum = 0;
471
- for (let i = 0; i < logits.length; i++) {
472
- out[i] = Math.exp(logits[i]! - max);
473
- sum += out[i]!;
474
- }
475
- for (let i = 0; i < logits.length; i++) out[i] = out[i]! / sum;
476
- return out;
477
- }
@@ -1,127 +1,141 @@
1
- /**
2
- * cpu_ops.ts — the small set of exact CPU primitives the reference forward and
3
- * backward passes share (linear, RMSNorm, SiLU, softplus).
4
- *
5
- * Each one mirrors a WGSL kernel exactly, so a gradient computed here is the
6
- * gradient of the function the GPU actually evaluates. They are deliberately
7
- * plain loops over `Float32Array`: correctness first, and small enough that the
8
- * finite-difference check in `tests/backprop.test.ts` is meaningful.
9
- */
10
-
11
- /** Epsilon used by `rmsnorm_forward` in `ACTIVATIONS_WGSL`. */
12
- export const RMSNORM_EPS = 1e-6;
13
-
14
- export function sigmoid(x: number): number {
15
- return 1 / (1 + Math.exp(-x));
16
- }
17
-
18
- /** SiLU(x) = x * sigmoid(x) — mirrors `silu_forward`. */
19
- export function silu(x: number): number {
20
- return x / (1 + Math.exp(-x));
21
- }
22
-
23
- /** d/dx SiLU(x) = sigmoid(x) * (1 + x * (1 - sigmoid(x))). */
24
- export function siluGrad(x: number): number {
25
- const s = sigmoid(x);
26
- return s * (1 + x * (1 - s));
27
- }
28
-
29
- /** Numerically stable softplus — mirrors the kernel's `max(x,0) + log1p(exp(-|x|))`. */
30
- export function softplus(x: number): number {
31
- return Math.max(x, 0) + Math.log1p(Math.exp(-Math.abs(x)));
32
- }
33
-
34
- /**
35
- * Y = X @ W^T + b, mirroring `linear_forward`.
36
- *
37
- * @param X (M, K)
38
- * @param W (N, K) row-major
39
- * @param b (N,)
40
- * @param out (M, N) — written, not accumulated
41
- */
42
- export function linearForward(
43
- X: Float32Array, W: Float32Array, b: Float32Array,
44
- M: number, K: number, N: number, out: Float32Array,
45
- ): void {
46
- for (let r = 0; r < M; r++) {
47
- const xo = r * K;
48
- const yo = r * N;
49
- for (let n = 0; n < N; n++) {
50
- let acc = b[n]!;
51
- const wo = n * K;
52
- for (let k = 0; k < K; k++) acc += X[xo + k]! * W[wo + k]!;
53
- out[yo + n] = acc;
54
- }
55
- }
56
- }
57
-
58
- /**
59
- * Backward of {@link linearForward}. `dX` is written; `dW` and `db` are
60
- * ACCUMULATED into (so one parameter can collect gradient from several call
61
- * sites without a temporary).
62
- */
63
- export function linearBackward(
64
- dY: Float32Array, X: Float32Array, W: Float32Array,
65
- M: number, K: number, N: number,
66
- dX: Float32Array, dW: Float32Array, db: Float32Array,
67
- ): void {
68
- dX.fill(0);
69
- for (let r = 0; r < M; r++) {
70
- const xo = r * K;
71
- const yo = r * N;
72
- for (let n = 0; n < N; n++) {
73
- const g = dY[yo + n]!;
74
- if (g === 0) continue;
75
- db[n] = db[n]! + g;
76
- const wo = n * K;
77
- for (let k = 0; k < K; k++) {
78
- dX[xo + k] = dX[xo + k]! + g * W[wo + k]!;
79
- dW[wo + k] = dW[wo + k]! + g * X[xo + k]!;
80
- }
81
- }
82
- }
83
- }
84
-
85
- /**
86
- * RMSNorm forward — mirrors `rmsnorm_forward`:
87
- * inv[r] = 1 / sqrt(mean(x[r]²) + eps); y[r,i] = x[r,i] * inv[r] * w[i]
88
- */
89
- export function rmsNormForward(
90
- x: Float32Array, w: Float32Array, M: number, D: number,
91
- out: Float32Array, inv: Float32Array, eps: number = RMSNORM_EPS,
92
- ): void {
93
- for (let r = 0; r < M; r++) {
94
- const base = r * D;
95
- let sq = 0;
96
- for (let i = 0; i < D; i++) { const v = x[base + i]!; sq += v * v; }
97
- const iv = 1 / Math.sqrt(sq / D + eps);
98
- inv[r] = iv;
99
- for (let i = 0; i < D; i++) out[base + i] = x[base + i]! * iv * w[i]!;
100
- }
101
- }
102
-
103
- /**
104
- * RMSNorm backward. `dx` and `dw` are ACCUMULATED into — `dx` in particular is
105
- * usually the residual path, which already carries a gradient.
106
- */
107
- export function rmsNormBackward(
108
- dY: Float32Array, x: Float32Array, w: Float32Array, inv: Float32Array,
109
- M: number, D: number,
110
- dx: Float32Array, dw: Float32Array, _eps: number = RMSNORM_EPS,
111
- ): void {
112
- for (let r = 0; r < M; r++) {
113
- const base = r * D;
114
- const iv = inv[r]!;
115
- let dot = 0; // Σ_i (dY[r,i] * w[i]) * x[r,i]
116
- for (let i = 0; i < D; i++) {
117
- const g = dY[base + i]! * w[i]!;
118
- dot += g * x[base + i]!;
119
- dw[i] = dw[i]! + dY[base + i]! * x[base + i]! * iv;
120
- }
121
- const scale = (iv * iv * iv) / D;
122
- for (let i = 0; i < D; i++) {
123
- const g = dY[base + i]! * w[i]!;
124
- dx[base + i] = dx[base + i]! + g * iv - scale * dot * x[base + i]!;
125
- }
126
- }
127
- }
1
+ /**
2
+ * cpu_ops.ts — the small set of exact CPU primitives the reference forward and
3
+ * backward passes share (linear, RMSNorm, SiLU, softplus).
4
+ *
5
+ * Each one mirrors a WGSL kernel exactly, so a gradient computed here is the
6
+ * gradient of the function the GPU actually evaluates. They are deliberately
7
+ * plain loops over `Float32Array`: correctness first, and small enough that the
8
+ * finite-difference check in `tests/backprop.test.ts` is meaningful.
9
+ */
10
+
11
+ /** Epsilon used by `rmsnorm_forward` in `ACTIVATIONS_WGSL`. */
12
+ export const RMSNORM_EPS = 1e-6;
13
+
14
+ export function sigmoid(x: number): number {
15
+ return 1 / (1 + Math.exp(-x));
16
+ }
17
+
18
+ /** Numerically-stable softmax over a flat array. */
19
+ export function softmax(logits: ArrayLike<number>): Float32Array {
20
+ let max = -Infinity;
21
+ for (let i = 0; i < logits.length; i++) if (logits[i]! > max) max = logits[i]!;
22
+ const out = new Float32Array(logits.length);
23
+ let sum = 0;
24
+ for (let i = 0; i < logits.length; i++) {
25
+ out[i] = Math.exp(logits[i]! - max);
26
+ sum += out[i]!;
27
+ }
28
+ for (let i = 0; i < logits.length; i++) out[i] = out[i]! / sum;
29
+ return out;
30
+ }
31
+
32
+ /** SiLU(x) = x * sigmoid(x) — mirrors `silu_forward`. */
33
+ export function silu(x: number): number {
34
+ return x / (1 + Math.exp(-x));
35
+ }
36
+
37
+ /** d/dx SiLU(x) = sigmoid(x) * (1 + x * (1 - sigmoid(x))). */
38
+ export function siluGrad(x: number): number {
39
+ const s = sigmoid(x);
40
+ return s * (1 + x * (1 - s));
41
+ }
42
+
43
+ /** Numerically stable softplus — mirrors the kernel's `max(x,0) + log1p(exp(-|x|))`. */
44
+ export function softplus(x: number): number {
45
+ return Math.max(x, 0) + Math.log1p(Math.exp(-Math.abs(x)));
46
+ }
47
+
48
+ /**
49
+ * Y = X @ W^T + b, mirroring `linear_forward`.
50
+ *
51
+ * @param X (M, K)
52
+ * @param W (N, K) row-major
53
+ * @param b (N,)
54
+ * @param out (M, N) — written, not accumulated
55
+ */
56
+ export function linearForward(
57
+ X: Float32Array, W: Float32Array, b: Float32Array,
58
+ M: number, K: number, N: number, out: Float32Array,
59
+ ): void {
60
+ for (let r = 0; r < M; r++) {
61
+ const xo = r * K;
62
+ const yo = r * N;
63
+ for (let n = 0; n < N; n++) {
64
+ let acc = b[n]!;
65
+ const wo = n * K;
66
+ for (let k = 0; k < K; k++) acc += X[xo + k]! * W[wo + k]!;
67
+ out[yo + n] = acc;
68
+ }
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Backward of {@link linearForward}. `dX` is written; `dW` and `db` are
74
+ * ACCUMULATED into (so one parameter can collect gradient from several call
75
+ * sites without a temporary).
76
+ */
77
+ export function linearBackward(
78
+ dY: Float32Array, X: Float32Array, W: Float32Array,
79
+ M: number, K: number, N: number,
80
+ dX: Float32Array, dW: Float32Array, db: Float32Array,
81
+ ): void {
82
+ dX.fill(0);
83
+ for (let r = 0; r < M; r++) {
84
+ const xo = r * K;
85
+ const yo = r * N;
86
+ for (let n = 0; n < N; n++) {
87
+ const g = dY[yo + n]!;
88
+ if (g === 0) continue;
89
+ db[n] = db[n]! + g;
90
+ const wo = n * K;
91
+ for (let k = 0; k < K; k++) {
92
+ dX[xo + k] = dX[xo + k]! + g * W[wo + k]!;
93
+ dW[wo + k] = dW[wo + k]! + g * X[xo + k]!;
94
+ }
95
+ }
96
+ }
97
+ }
98
+
99
+ /**
100
+ * RMSNorm forward — mirrors `rmsnorm_forward`:
101
+ * inv[r] = 1 / sqrt(mean(x[r]²) + eps); y[r,i] = x[r,i] * inv[r] * w[i]
102
+ */
103
+ export function rmsNormForward(
104
+ x: Float32Array, w: Float32Array, M: number, D: number,
105
+ out: Float32Array, inv: Float32Array, eps: number = RMSNORM_EPS,
106
+ ): void {
107
+ for (let r = 0; r < M; r++) {
108
+ const base = r * D;
109
+ let sq = 0;
110
+ for (let i = 0; i < D; i++) { const v = x[base + i]!; sq += v * v; }
111
+ const iv = 1 / Math.sqrt(sq / D + eps);
112
+ inv[r] = iv;
113
+ for (let i = 0; i < D; i++) out[base + i] = x[base + i]! * iv * w[i]!;
114
+ }
115
+ }
116
+
117
+ /**
118
+ * RMSNorm backward. `dx` and `dw` are ACCUMULATED into — `dx` in particular is
119
+ * usually the residual path, which already carries a gradient.
120
+ */
121
+ export function rmsNormBackward(
122
+ dY: Float32Array, x: Float32Array, w: Float32Array, inv: Float32Array,
123
+ M: number, D: number,
124
+ dx: Float32Array, dw: Float32Array, _eps: number = RMSNORM_EPS,
125
+ ): void {
126
+ for (let r = 0; r < M; r++) {
127
+ const base = r * D;
128
+ const iv = inv[r]!;
129
+ let dot = 0; // Σ_i (dY[r,i] * w[i]) * x[r,i]
130
+ for (let i = 0; i < D; i++) {
131
+ const g = dY[base + i]! * w[i]!;
132
+ dot += g * x[base + i]!;
133
+ dw[i] = dw[i]! + dY[base + i]! * x[base + i]! * iv;
134
+ }
135
+ const scale = (iv * iv * iv) / D;
136
+ for (let i = 0; i < D; i++) {
137
+ const g = dY[base + i]! * w[i]!;
138
+ dx[base + i] = dx[base + i]! + g * iv - scale * dot * x[base + i]!;
139
+ }
140
+ }
141
+ }