@seanhogg/builderforce-memory-engine 2026.6.27 → 2026.6.30
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/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/lm/evermind_lm.d.ts +148 -0
- package/dist/lm/evermind_lm.d.ts.map +1 -0
- package/dist/lm/evermind_lm.js +479 -0
- package/dist/lm/evermind_lm.js.map +1 -0
- package/dist/lm/index.d.ts +6 -0
- package/dist/lm/index.d.ts.map +1 -0
- package/dist/lm/index.js +5 -0
- package/dist/lm/index.js.map +1 -0
- package/dist/model/attention_block.js +1 -1
- package/dist/model/attention_block.js.map +1 -1
- package/dist/model/mamba_model.js +1 -1
- package/dist/model/mamba_model.js.map +1 -1
- package/dist/moe/index.d.ts +10 -0
- package/dist/moe/index.d.ts.map +1 -0
- package/dist/moe/index.js +7 -0
- package/dist/moe/index.js.map +1 -0
- package/dist/moe/moe_model.d.ts +134 -0
- package/dist/moe/moe_model.d.ts.map +1 -0
- package/dist/moe/moe_model.js +415 -0
- package/dist/moe/moe_model.js.map +1 -0
- package/dist/moe/moe_package.d.ts +81 -0
- package/dist/moe/moe_package.d.ts.map +1 -0
- package/dist/moe/moe_package.js +157 -0
- package/dist/moe/moe_package.js.map +1 -0
- package/dist/moe/moe_trainer.d.ts +53 -0
- package/dist/moe/moe_trainer.d.ts.map +1 -0
- package/dist/moe/moe_trainer.js +93 -0
- package/dist/moe/moe_trainer.js.map +1 -0
- package/dist/optim/adamw.d.ts +32 -0
- package/dist/optim/adamw.d.ts.map +1 -0
- package/dist/optim/adamw.js +52 -0
- package/dist/optim/adamw.js.map +1 -0
- package/dist/tokenizer/bpe.d.ts +11 -0
- package/dist/tokenizer/bpe.d.ts.map +1 -1
- package/dist/tokenizer/bpe.js +78 -0
- package/dist/tokenizer/bpe.js.map +1 -1
- package/package.json +5 -3
- package/src/index.ts +28 -0
- package/src/lm/evermind_lm.ts +558 -0
- package/src/lm/index.ts +6 -0
- package/src/model/attention_block.ts +1 -1
- package/src/model/mamba_model.ts +1 -1
- package/src/moe/index.ts +23 -0
- package/src/moe/moe_model.ts +475 -0
- package/src/moe/moe_package.ts +205 -0
- package/src/moe/moe_trainer.ts +134 -0
- package/src/optim/adamw.ts +72 -0
- package/src/tokenizer/bpe.ts +77 -0
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* moe_trainer.ts — AdamW training loop for {@link SharedExpertMoE}.
|
|
3
|
+
*
|
|
4
|
+
* Makes "train your own Evermind AI" real: given labelled (input → target)
|
|
5
|
+
* samples, runs minibatch AdamW over the flat parameters with the load-balancing
|
|
6
|
+
* auxiliary loss mixed in (so the router spreads load instead of collapsing onto
|
|
7
|
+
* a few experts). Pure CPU, deterministic given a seeded model — the same loop a
|
|
8
|
+
* WebGPU optimiser kernel would accelerate.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { SharedExpertMoE } from "./moe_model.js";
|
|
12
|
+
import { AdamW } from "../optim/adamw.js";
|
|
13
|
+
|
|
14
|
+
export interface MoESample {
|
|
15
|
+
input: ArrayLike<number>;
|
|
16
|
+
target: ArrayLike<number>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface MoETrainOptions {
|
|
20
|
+
/** Learning rate. Default 0.01. */
|
|
21
|
+
lr?: number;
|
|
22
|
+
/** AdamW β1. Default 0.9. */
|
|
23
|
+
beta1?: number;
|
|
24
|
+
/** AdamW β2. Default 0.999. */
|
|
25
|
+
beta2?: number;
|
|
26
|
+
/** AdamW ε. Default 1e-8. */
|
|
27
|
+
eps?: number;
|
|
28
|
+
/** Decoupled weight decay. Default 0. */
|
|
29
|
+
weightDecay?: number;
|
|
30
|
+
/** Weight of the load-balancing auxiliary loss. Default 0.01. */
|
|
31
|
+
auxWeight?: number;
|
|
32
|
+
/** Minibatch size. Default = all samples (full batch). */
|
|
33
|
+
batchSize?: number;
|
|
34
|
+
/** Passes over the dataset. Default 1. */
|
|
35
|
+
epochs?: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface MoEEpochResult {
|
|
39
|
+
/** Mean per-sample task (MSE·½) loss over the epoch. */
|
|
40
|
+
loss: number;
|
|
41
|
+
/** Load-balancing auxiliary loss at the end of the epoch (≈1 balanced … E collapsed). */
|
|
42
|
+
auxLoss: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* AdamW optimiser over a model's flat parameter list. State (m, v) is keyed by
|
|
47
|
+
* parameter index and persists across {@link step} calls.
|
|
48
|
+
*/
|
|
49
|
+
export class MoETrainer {
|
|
50
|
+
private readonly adam: AdamW;
|
|
51
|
+
private readonly opt: Required<MoETrainOptions>;
|
|
52
|
+
|
|
53
|
+
constructor(
|
|
54
|
+
private readonly model: SharedExpertMoE,
|
|
55
|
+
options: MoETrainOptions = {},
|
|
56
|
+
) {
|
|
57
|
+
this.opt = {
|
|
58
|
+
lr: options.lr ?? 0.01,
|
|
59
|
+
beta1: options.beta1 ?? 0.9,
|
|
60
|
+
beta2: options.beta2 ?? 0.999,
|
|
61
|
+
eps: options.eps ?? 1e-8,
|
|
62
|
+
weightDecay: options.weightDecay ?? 0,
|
|
63
|
+
auxWeight: options.auxWeight ?? 0.01,
|
|
64
|
+
batchSize: options.batchSize ?? 0,
|
|
65
|
+
epochs: options.epochs ?? 1,
|
|
66
|
+
};
|
|
67
|
+
this.adam = new AdamW(model, this.opt);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Train for the configured epochs. Returns the per-epoch loss history. */
|
|
71
|
+
fit(samples: MoESample[]): MoEEpochResult[] {
|
|
72
|
+
const history: MoEEpochResult[] = [];
|
|
73
|
+
for (let e = 0; e < this.opt.epochs; e++) history.push(this.runEpoch(samples));
|
|
74
|
+
return history;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
private runEpoch(samples: MoESample[]): MoEEpochResult {
|
|
78
|
+
const batchSize = this.opt.batchSize > 0 ? this.opt.batchSize : samples.length;
|
|
79
|
+
const { numExperts, modelDim } = this.model.config;
|
|
80
|
+
let epochLoss = 0;
|
|
81
|
+
let lastAux = 0;
|
|
82
|
+
|
|
83
|
+
for (let start = 0; start < samples.length; start += batchSize) {
|
|
84
|
+
const batch = samples.slice(start, start + batchSize);
|
|
85
|
+
this.model.zeroGrad();
|
|
86
|
+
|
|
87
|
+
// Forward + task backward, retaining (x, probs) for the batch aux gradient.
|
|
88
|
+
const xs: Float32Array[] = [];
|
|
89
|
+
const probsList: Float32Array[] = [];
|
|
90
|
+
const counts = new Float32Array(numExperts);
|
|
91
|
+
let batchLoss = 0;
|
|
92
|
+
|
|
93
|
+
for (const s of batch) {
|
|
94
|
+
const f = this.model.forward(s.input);
|
|
95
|
+
const dOut = new Float32Array(modelDim);
|
|
96
|
+
for (let d = 0; d < modelDim; d++) {
|
|
97
|
+
const diff = f.output[d]! - (s.target[d] ?? 0);
|
|
98
|
+
dOut[d] = diff;
|
|
99
|
+
batchLoss += 0.5 * diff * diff;
|
|
100
|
+
}
|
|
101
|
+
this.model.backward(dOut, f.cache);
|
|
102
|
+
xs.push(f.cache.x);
|
|
103
|
+
probsList.push(f.route.probs);
|
|
104
|
+
for (const ex of f.route.experts) counts[ex] = counts[ex]! + 1;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Load-balancing aux gradient (batch-level): f = dispatch fractions.
|
|
108
|
+
const dispatched = counts.reduce((a, b) => a + b, 0) || 1;
|
|
109
|
+
const fVec = Float32Array.from(counts, (c) => c / dispatched);
|
|
110
|
+
const scale = (this.opt.auxWeight * numExperts) / batch.length;
|
|
111
|
+
for (let i = 0; i < xs.length; i++) {
|
|
112
|
+
this.model.auxGradStep(xs[i]!, probsList[i]!, fVec, scale);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Average the task gradient over the batch, then AdamW step.
|
|
116
|
+
this.scaleGradients(1 / batch.length);
|
|
117
|
+
this.adam.step();
|
|
118
|
+
|
|
119
|
+
epochLoss += batchLoss;
|
|
120
|
+
lastAux = numExperts * fVec.reduce((sum, f, e) => sum + f * (probsList.length
|
|
121
|
+
? probsList.reduce((s, p) => s + p[e]!, 0) / probsList.length
|
|
122
|
+
: 0), 0);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return { loss: epochLoss / Math.max(1, samples.length), auxLoss: lastAux };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
private scaleGradients(k: number): void {
|
|
129
|
+
if (k === 1) return;
|
|
130
|
+
for (const g of this.model.gradients()) {
|
|
131
|
+
for (let i = 0; i < g.data.length; i++) g.data[i] = g.data[i]! * k;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* adamw.ts — AdamW optimiser over a model's flat parameter list.
|
|
3
|
+
*
|
|
4
|
+
* Shared by every CPU-reference trainer in the engine (MoE FFN, the full
|
|
5
|
+
* EvermindLM) so the optimiser maths lives in exactly one place. Operates on any
|
|
6
|
+
* object exposing index-aligned `parameters()` / `gradients()` Float32Arrays.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export interface OptimParam {
|
|
10
|
+
data: Float32Array;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface OptimTarget {
|
|
14
|
+
parameters(): OptimParam[];
|
|
15
|
+
gradients(): OptimParam[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface AdamWOptions {
|
|
19
|
+
lr?: number;
|
|
20
|
+
beta1?: number;
|
|
21
|
+
beta2?: number;
|
|
22
|
+
eps?: number;
|
|
23
|
+
weightDecay?: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class AdamW {
|
|
27
|
+
private readonly m: Float32Array[] = [];
|
|
28
|
+
private readonly v: Float32Array[] = [];
|
|
29
|
+
private t = 0;
|
|
30
|
+
private readonly opt: Required<AdamWOptions>;
|
|
31
|
+
|
|
32
|
+
constructor(
|
|
33
|
+
private readonly target: OptimTarget,
|
|
34
|
+
options: AdamWOptions = {},
|
|
35
|
+
) {
|
|
36
|
+
this.opt = {
|
|
37
|
+
lr: options.lr ?? 0.01,
|
|
38
|
+
beta1: options.beta1 ?? 0.9,
|
|
39
|
+
beta2: options.beta2 ?? 0.999,
|
|
40
|
+
eps: options.eps ?? 1e-8,
|
|
41
|
+
weightDecay: options.weightDecay ?? 0,
|
|
42
|
+
};
|
|
43
|
+
for (const p of target.parameters()) {
|
|
44
|
+
this.m.push(new Float32Array(p.data.length));
|
|
45
|
+
this.v.push(new Float32Array(p.data.length));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** One optimiser step from the currently-accumulated gradients. */
|
|
50
|
+
step(): void {
|
|
51
|
+
this.t++;
|
|
52
|
+
const { lr, beta1, beta2, eps, weightDecay } = this.opt;
|
|
53
|
+
const params = this.target.parameters();
|
|
54
|
+
const grads = this.target.gradients();
|
|
55
|
+
const bc1 = 1 - Math.pow(beta1, this.t);
|
|
56
|
+
const bc2 = 1 - Math.pow(beta2, this.t);
|
|
57
|
+
for (let p = 0; p < params.length; p++) {
|
|
58
|
+
const w = params[p]!.data;
|
|
59
|
+
const g = grads[p]!.data;
|
|
60
|
+
const m = this.m[p]!;
|
|
61
|
+
const v = this.v[p]!;
|
|
62
|
+
for (let i = 0; i < w.length; i++) {
|
|
63
|
+
const gi = g[i]!;
|
|
64
|
+
m[i] = beta1 * m[i]! + (1 - beta1) * gi;
|
|
65
|
+
v[i] = beta2 * v[i]! + (1 - beta2) * gi * gi;
|
|
66
|
+
const mh = m[i]! / bc1;
|
|
67
|
+
const vh = v[i]! / bc2;
|
|
68
|
+
w[i] = w[i]! - lr * (mh / (Math.sqrt(vh) + eps) + weightDecay * w[i]!);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
package/src/tokenizer/bpe.ts
CHANGED
|
@@ -91,6 +91,83 @@ export class BPETokenizer {
|
|
|
91
91
|
this.padId = this.vocab.get(this.padToken) ?? null;
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
+
/**
|
|
95
|
+
* Learn a byte-level BPE vocabulary + merges from a text corpus, then load
|
|
96
|
+
* them into this tokenizer. Makes the tokenizer self-contained (no external
|
|
97
|
+
* vocab file): train on your data, then `encode`/`decode` round-trips real
|
|
98
|
+
* text and frequent sequences compress to single tokens. The base vocabulary
|
|
99
|
+
* always covers all 256 byte symbols, so any input is representable.
|
|
100
|
+
*/
|
|
101
|
+
train(corpus: string | string[], opts: { numMerges?: number; minPairFreq?: number } = {}): void {
|
|
102
|
+
const numMerges = opts.numMerges ?? 200;
|
|
103
|
+
const minPairFreq = opts.minPairFreq ?? 2;
|
|
104
|
+
const texts = Array.isArray(corpus) ? corpus : [corpus];
|
|
105
|
+
|
|
106
|
+
// Byte-encode each pre-token into a symbol sequence; count word frequencies.
|
|
107
|
+
const wordFreq = new Map<string, number>();
|
|
108
|
+
for (const text of texts) {
|
|
109
|
+
for (const word of text.match(PRE_TOKENIZE_RE) ?? []) {
|
|
110
|
+
const bytes = new TextEncoder().encode(word);
|
|
111
|
+
const byteStr = Array.from(bytes).map((b) => BYTE_ENCODER.get(b) ?? '?').join('');
|
|
112
|
+
wordFreq.set(byteStr, (wordFreq.get(byteStr) ?? 0) + 1);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
const words = [...wordFreq].map(([w, freq]) => ({ syms: [...w], freq }));
|
|
116
|
+
|
|
117
|
+
// Greedy BPE: repeatedly merge the most frequent adjacent symbol pair.
|
|
118
|
+
const merges: string[] = [];
|
|
119
|
+
for (let m = 0; m < numMerges; m++) {
|
|
120
|
+
const pairCounts = new Map<string, number>();
|
|
121
|
+
for (const { syms, freq } of words) {
|
|
122
|
+
for (let i = 0; i < syms.length - 1; i++) {
|
|
123
|
+
const pair = syms[i]! + ' ' + syms[i + 1]!;
|
|
124
|
+
pairCounts.set(pair, (pairCounts.get(pair) ?? 0) + freq);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
let best = '';
|
|
128
|
+
let bestCount = 0;
|
|
129
|
+
for (const [pair, count] of pairCounts) {
|
|
130
|
+
if (count > bestCount) {
|
|
131
|
+
best = pair;
|
|
132
|
+
bestCount = count;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (bestCount < minPairFreq) break;
|
|
136
|
+
merges.push(best);
|
|
137
|
+
const sp = best.indexOf(' ');
|
|
138
|
+
const a = best.slice(0, sp);
|
|
139
|
+
const b = best.slice(sp + 1);
|
|
140
|
+
const merged = a + b;
|
|
141
|
+
for (const w of words) {
|
|
142
|
+
const out: string[] = [];
|
|
143
|
+
for (let i = 0; i < w.syms.length; i++) {
|
|
144
|
+
if (i < w.syms.length - 1 && w.syms[i] === a && w.syms[i + 1] === b) {
|
|
145
|
+
out.push(merged);
|
|
146
|
+
i++;
|
|
147
|
+
} else {
|
|
148
|
+
out.push(w.syms[i]!);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
w.syms = out;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Assemble the vocabulary: specials, every byte symbol (full coverage), then merges.
|
|
156
|
+
const vocabObj: Record<string, number> = {};
|
|
157
|
+
let id = 0;
|
|
158
|
+
for (const special of [this.unkToken, this.bosToken, this.eosToken, this.padToken]) {
|
|
159
|
+
if (!(special in vocabObj)) vocabObj[special] = id++;
|
|
160
|
+
}
|
|
161
|
+
for (const ch of BYTE_ENCODER.values()) {
|
|
162
|
+
if (!(ch in vocabObj)) vocabObj[ch] = id++;
|
|
163
|
+
}
|
|
164
|
+
for (const pair of merges) {
|
|
165
|
+
const merged = pair.split(' ').join('');
|
|
166
|
+
if (!(merged in vocabObj)) vocabObj[merged] = id++;
|
|
167
|
+
}
|
|
168
|
+
this.loadFromObjects(vocabObj, merges);
|
|
169
|
+
}
|
|
170
|
+
|
|
94
171
|
loadFromObjects(vocabObj: Record<string, number>, mergeArr: string[]): void {
|
|
95
172
|
this.vocab = new Map(Object.entries(vocabObj).map(([k, v]) => [k, Number(v)]));
|
|
96
173
|
this.idToToken = new Map([...this.vocab].map(([k, v]) => [v, k]));
|