@seanhogg/builderforce-memory-engine 2026.9.29 → 2026.9.31
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/export/onnx.d.ts.map +1 -1
- package/dist/export/onnx.js +0 -8
- package/dist/export/onnx.js.map +1 -1
- package/dist/import/foreign/falcon_mamba.d.ts +15 -2
- package/dist/import/foreign/falcon_mamba.d.ts.map +1 -1
- package/dist/import/foreign/falcon_mamba.js +53 -36
- package/dist/import/foreign/falcon_mamba.js.map +1 -1
- package/dist/import/foreign/index.d.ts +5 -2
- package/dist/import/foreign/index.d.ts.map +1 -1
- package/dist/import/foreign/index.js +4 -2
- package/dist/import/foreign/index.js.map +1 -1
- package/dist/import/foreign/infer.d.ts +20 -0
- package/dist/import/foreign/infer.d.ts.map +1 -0
- package/dist/import/foreign/infer.js +58 -0
- package/dist/import/foreign/infer.js.map +1 -0
- package/dist/import/foreign/registry.d.ts.map +1 -1
- package/dist/import/foreign/registry.js +2 -1
- package/dist/import/foreign/registry.js.map +1 -1
- package/dist/import/foreign/vocab.d.ts +19 -0
- package/dist/import/foreign/vocab.d.ts.map +1 -0
- package/dist/import/foreign/vocab.js +40 -0
- package/dist/import/foreign/vocab.js.map +1 -0
- package/dist/import/index.d.ts +2 -2
- package/dist/import/index.d.ts.map +1 -1
- package/dist/import/index.js +1 -1
- package/dist/import/index.js.map +1 -1
- package/dist/import/safetensors.d.ts +1 -1
- package/dist/import/safetensors.js +11 -2
- package/dist/import/safetensors.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/moe/moe_model.d.ts +2 -0
- package/dist/moe/moe_model.d.ts.map +1 -1
- package/dist/moe/moe_model.js +9 -7
- package/dist/moe/moe_model.js.map +1 -1
- package/dist/moe/moe_trainer.d.ts.map +1 -1
- package/dist/moe/moe_trainer.js +5 -8
- package/dist/moe/moe_trainer.js.map +1 -1
- package/dist/utils/rng.d.ts +7 -3
- package/dist/utils/rng.d.ts.map +1 -1
- package/dist/utils/rng.js +10 -6
- package/dist/utils/rng.js.map +1 -1
- package/package.json +1 -1
- package/src/export/onnx.ts +0 -7
- package/src/import/foreign/falcon_mamba.ts +63 -40
- package/src/import/foreign/index.ts +5 -2
- package/src/import/foreign/infer.ts +62 -0
- package/src/import/foreign/registry.ts +2 -1
- package/src/import/foreign/vocab.ts +50 -0
- package/src/import/index.ts +5 -0
- package/src/import/safetensors.ts +9 -2
- package/src/index.ts +6 -1
- package/src/moe/moe_model.ts +9 -7
- package/src/moe/moe_trainer.ts +5 -8
- package/src/utils/rng.ts +10 -6
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* import/foreign/infer.ts — a Mamba-1 checkpoint's config, read off its tensors.
|
|
3
|
+
*
|
|
4
|
+
* `state-spaces/mamba-*` repositories publish `model.safetensors` with a
|
|
5
|
+
* `config.json` in the original Mamba-SSM vocabulary (`d_model`, `n_layer`, …)
|
|
6
|
+
* that no adapter reads, and a dropped safetensors file often arrives with no
|
|
7
|
+
* config at all. Every geometry fact the port needs is in the tensor shapes, so
|
|
8
|
+
* this derives the Hugging Face `MambaConfig` the {@link mambaAdapter} expects
|
|
9
|
+
* instead of anyone restating the derivation.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { NamedTensor } from "../../export/tensors.js";
|
|
13
|
+
import type { ForeignConfig } from "./adapter.js";
|
|
14
|
+
import { normaliseSourceName } from "./plan.js";
|
|
15
|
+
|
|
16
|
+
function fail(detail: string): never {
|
|
17
|
+
throw new Error(`import/foreign: cannot read a Mamba-1 config from these tensors — ${detail}`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The `MambaForCausalLM` config implied by a Mamba-1 checkpoint's tensors:
|
|
22
|
+
* embedding `[vocab, d_model]`, per-layer `in_proj [2·d_inner, d_model]`,
|
|
23
|
+
* `conv1d [d_inner, (1,) d_conv]`, `A_log [d_inner, d_state]` and
|
|
24
|
+
* `x_proj [dt_rank + 2·d_state, d_inner]`.
|
|
25
|
+
*/
|
|
26
|
+
export function mambaConfigFromTensors(tensors: readonly NamedTensor[]): ForeignConfig {
|
|
27
|
+
const byName = new Map(tensors.map((t) => [normaliseSourceName(t.name), t]));
|
|
28
|
+
const shape = (name: string) => byName.get(name)?.shape;
|
|
29
|
+
|
|
30
|
+
const emb = shape("backbone.embeddings.weight") ?? shape("backbone.embedding.weight");
|
|
31
|
+
if (!emb || emb.length !== 2) fail("no backbone.embedding(s).weight [vocab, d_model]");
|
|
32
|
+
const [vocabSize, dModel] = emb as [number, number];
|
|
33
|
+
|
|
34
|
+
let numLayers = 0;
|
|
35
|
+
while (byName.has(`backbone.layers.${numLayers}.mixer.in_proj.weight`)) numLayers++;
|
|
36
|
+
if (numLayers === 0) fail("no backbone.layers.N.mixer.in_proj.weight");
|
|
37
|
+
|
|
38
|
+
const m = "backbone.layers.0.mixer";
|
|
39
|
+
const inProj = shape(`${m}.in_proj.weight`)!;
|
|
40
|
+
const conv = shape(`${m}.conv1d.weight`);
|
|
41
|
+
const aLog = shape(`${m}.A_log`);
|
|
42
|
+
const xProj = shape(`${m}.x_proj.weight`);
|
|
43
|
+
if (!conv || !aLog || !xProj) fail(`layer 0 is missing conv1d.weight, A_log or x_proj.weight`);
|
|
44
|
+
|
|
45
|
+
const dInner = inProj[0]! / 2;
|
|
46
|
+
const dState = aLog[1]!;
|
|
47
|
+
const dConv = conv[conv.length - 1]!;
|
|
48
|
+
const dtRank = xProj[0]! - 2 * dState;
|
|
49
|
+
if (!Number.isInteger(dInner) || dtRank <= 0) fail(`inconsistent shapes (in_proj ${inProj.join("×")}, x_proj ${xProj.join("×")})`);
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
model_type: "mamba",
|
|
53
|
+
architectures: ["MambaForCausalLM"],
|
|
54
|
+
vocab_size: vocabSize,
|
|
55
|
+
hidden_size: dModel,
|
|
56
|
+
num_hidden_layers: numLayers,
|
|
57
|
+
intermediate_size: dInner,
|
|
58
|
+
state_size: dState,
|
|
59
|
+
conv_kernel: dConv,
|
|
60
|
+
time_step_rank: dtRank,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
@@ -12,12 +12,13 @@ import type { NamedTensor } from "../../export/tensors.js";
|
|
|
12
12
|
import type { HybridMambaModelConfig } from "../../model/mamba_model.js";
|
|
13
13
|
import { safetensorsToTensors } from "../safetensors.js";
|
|
14
14
|
import { executePortPlan, type PortDiscard, type PortedTensors, type SynthesisedTarget } from "./plan.js";
|
|
15
|
-
import { falconMambaAdapter } from "./falcon_mamba.js";
|
|
15
|
+
import { falconMambaAdapter, mambaAdapter } from "./falcon_mamba.js";
|
|
16
16
|
import { codestralMambaAdapter } from "./codestral_mamba.js";
|
|
17
17
|
import type { ForeignConfig, ForeignMambaAdapter } from "./adapter.js";
|
|
18
18
|
|
|
19
19
|
/** Every foreign SSM architecture this engine can warm-start from. */
|
|
20
20
|
export const FOREIGN_MAMBA_ADAPTERS: readonly ForeignMambaAdapter[] = [
|
|
21
|
+
mambaAdapter,
|
|
21
22
|
falconMambaAdapter,
|
|
22
23
|
codestralMambaAdapter,
|
|
23
24
|
];
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* import/foreign/vocab.ts — fit a ported checkpoint to a different vocabulary size.
|
|
3
|
+
*
|
|
4
|
+
* A ported model keeps its source tokenizer's vocabulary. To pair it with another
|
|
5
|
+
* tokenizer the embedding table (tied to the LM head) must have that tokenizer's row
|
|
6
|
+
* count: surplus rows are dropped, missing rows are drawn small and random so the
|
|
7
|
+
* new tokens start near the embedding cloud's centre. The new rows are recorded on
|
|
8
|
+
* the port as synthesised, never passed off as ported.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { SeededRng, gaussianArray } from "../../utils/rng.js";
|
|
12
|
+
import type { PortedCheckpoint } from "./registry.js";
|
|
13
|
+
|
|
14
|
+
export interface ResizeVocabOptions {
|
|
15
|
+
/** Standard deviation of new rows. Default 0.02, the usual embedding init. */
|
|
16
|
+
std?: number;
|
|
17
|
+
/** Seed for the new rows, so the same resize produces the same checkpoint. Default 1. */
|
|
18
|
+
seed?: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** `ported` with an embedding of `vocabSize` rows and a model config to match. */
|
|
22
|
+
export function resizePortedVocab(ported: PortedCheckpoint, vocabSize: number, opts: ResizeVocabOptions = {}): PortedCheckpoint {
|
|
23
|
+
if (!Number.isInteger(vocabSize) || vocabSize <= 0) {
|
|
24
|
+
throw new Error(`import/foreign: vocabulary size must be a positive integer, got ${vocabSize}`);
|
|
25
|
+
}
|
|
26
|
+
const from = ported.modelConfig.vocabSize;
|
|
27
|
+
if (vocabSize === from) return ported;
|
|
28
|
+
|
|
29
|
+
const emb = ported.weights.get("embedding");
|
|
30
|
+
if (!emb) throw new Error('import/foreign: the port has no "embedding" to resize');
|
|
31
|
+
const dModel = ported.modelConfig.dModel;
|
|
32
|
+
|
|
33
|
+
const resized = new Float32Array(vocabSize * dModel);
|
|
34
|
+
resized.set(emb.subarray(0, Math.min(from, vocabSize) * dModel));
|
|
35
|
+
const synthesisedTargets = [...ported.synthesisedTargets];
|
|
36
|
+
if (vocabSize > from) {
|
|
37
|
+
const std = opts.std ?? 0.02;
|
|
38
|
+
const rng = new SeededRng(opts.seed ?? 1);
|
|
39
|
+
resized.set(gaussianArray((vocabSize - from) * dModel, std, () => rng.next()), from * dModel);
|
|
40
|
+
synthesisedTargets.push({
|
|
41
|
+
target: "embedding",
|
|
42
|
+
value: std,
|
|
43
|
+
reason: `rows ${from}..${vocabSize - 1} drawn from N(0, ${std}²) to reach a ${vocabSize}-token vocabulary`,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const weights = new Map(ported.weights);
|
|
48
|
+
weights.set("embedding", resized);
|
|
49
|
+
return { ...ported, weights, synthesisedTargets, modelConfig: { ...ported.modelConfig, vocabSize } };
|
|
50
|
+
}
|
package/src/import/index.ts
CHANGED
|
@@ -25,7 +25,11 @@ export {
|
|
|
25
25
|
executePortPlan,
|
|
26
26
|
normaliseSourceName,
|
|
27
27
|
falconMambaAdapter,
|
|
28
|
+
mambaAdapter,
|
|
29
|
+
mamba1Adapter,
|
|
28
30
|
codestralMambaAdapter,
|
|
31
|
+
mambaConfigFromTensors,
|
|
32
|
+
resizePortedVocab,
|
|
29
33
|
} from "./foreign/index.js";
|
|
30
34
|
export type {
|
|
31
35
|
ForeignConfig,
|
|
@@ -38,4 +42,5 @@ export type {
|
|
|
38
42
|
PortedCheckpoint,
|
|
39
43
|
PortedTensors,
|
|
40
44
|
SynthesisedTarget,
|
|
45
|
+
ResizeVocabOptions,
|
|
41
46
|
} from "./foreign/index.js";
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* The exact inverse of {@link ../export/safetensors.ts}: parse the 8-byte
|
|
5
5
|
* little-endian u64 header length, the JSON header (name → {dtype, shape,
|
|
6
6
|
* data_offsets}), then decode each tensor's raw bytes into a Float32Array.
|
|
7
|
-
* F32 and
|
|
7
|
+
* F32, F16 and BF16 are supported (both halves are widened to f32 on read). This is the reader
|
|
8
8
|
* half of the warm-start / weight-port path — export was one-directional before.
|
|
9
9
|
*/
|
|
10
10
|
|
|
@@ -63,5 +63,12 @@ function decodeTensor(seg: Uint8Array, dtype: string, numel: number, name: strin
|
|
|
63
63
|
for (let i = 0; i < numel; i++) data[i] = fp16ToFloat(dv.getUint16(i * 2, true));
|
|
64
64
|
return data;
|
|
65
65
|
}
|
|
66
|
-
|
|
66
|
+
if (d === "BF16" || d === "BFLOAT16") {
|
|
67
|
+
// bfloat16 is the top half of an f32: shift it back into place.
|
|
68
|
+
if (seg.length < numel * 2) throw new Error(`safetensors: "${name}" BF16 bytes ${seg.length} < ${numel * 2}`);
|
|
69
|
+
const bits = new Uint32Array(data.buffer);
|
|
70
|
+
for (let i = 0; i < numel; i++) bits[i] = dv.getUint16(i * 2, true) << 16;
|
|
71
|
+
return data;
|
|
72
|
+
}
|
|
73
|
+
throw new Error(`safetensors: unsupported dtype "${dtype}" for "${name}" (expected F32, F16 or BF16)`);
|
|
67
74
|
}
|
package/src/index.ts
CHANGED
|
@@ -181,7 +181,7 @@ export {
|
|
|
181
181
|
} from './import/index.js';
|
|
182
182
|
export type { ImportOptions } from './import/index.js';
|
|
183
183
|
|
|
184
|
-
// ── Foreign weight port (Falcon-Mamba / Codestral-Mamba → HybridMambaModel)
|
|
184
|
+
// ── Foreign weight port (Mamba / Falcon-Mamba / Codestral-Mamba → HybridMambaModel) ──
|
|
185
185
|
// Transformer checkpoints are NOT portable — distillation is the only route, and
|
|
186
186
|
// `foreignMambaAdapterFor` rejects them saying so.
|
|
187
187
|
|
|
@@ -194,7 +194,11 @@ export {
|
|
|
194
194
|
executePortPlan,
|
|
195
195
|
normaliseSourceName,
|
|
196
196
|
falconMambaAdapter,
|
|
197
|
+
mambaAdapter,
|
|
198
|
+
mamba1Adapter,
|
|
197
199
|
codestralMambaAdapter,
|
|
200
|
+
mambaConfigFromTensors,
|
|
201
|
+
resizePortedVocab,
|
|
198
202
|
} from './import/index.js';
|
|
199
203
|
export type {
|
|
200
204
|
ForeignConfig,
|
|
@@ -207,6 +211,7 @@ export type {
|
|
|
207
211
|
PortedCheckpoint,
|
|
208
212
|
PortedTensors,
|
|
209
213
|
SynthesisedTarget,
|
|
214
|
+
ResizeVocabOptions,
|
|
210
215
|
} from './import/index.js';
|
|
211
216
|
|
|
212
217
|
// ── Benchmarking (held-out perplexity / accuracy / throughput + A/B) ──────────
|
package/src/moe/moe_model.ts
CHANGED
|
@@ -188,18 +188,20 @@ export class LoadBalanceAccumulator {
|
|
|
188
188
|
for (const e of route.experts) this.counts[e] = this.counts[e]! + 1;
|
|
189
189
|
for (let e = 0; e < this.numExperts; e++) this.probSum[e] = this.probSum[e]! + route.probs[e]!;
|
|
190
190
|
}
|
|
191
|
+
/** `f_e`: the fraction of all dispatches that went to each expert (sums to 1 once anything is observed). */
|
|
192
|
+
dispatchFractions(): Float32Array {
|
|
193
|
+
const dispatched = this.counts.reduce((a, b) => a + b, 0) || 1; // = tokens·topK
|
|
194
|
+
return Float32Array.from(this.counts, (c) => c / dispatched);
|
|
195
|
+
}
|
|
191
196
|
/** The load-balance loss over everything observed so far (0 if no tokens). */
|
|
192
197
|
loss(): number {
|
|
193
198
|
if (this.tokens === 0) return 0;
|
|
194
|
-
const
|
|
195
|
-
const dispatched = this.counts.reduce((a, b) => a + b, 0) || 1; // = tokens·topK
|
|
199
|
+
const f = this.dispatchFractions();
|
|
196
200
|
let sum = 0;
|
|
197
|
-
for (let e = 0; e <
|
|
198
|
-
|
|
199
|
-
const p = this.probSum[e]! / this.tokens; // mean router prob for e
|
|
200
|
-
sum += f * p;
|
|
201
|
+
for (let e = 0; e < this.numExperts; e++) {
|
|
202
|
+
sum += f[e]! * (this.probSum[e]! / this.tokens); // f_e · mean router prob for e
|
|
201
203
|
}
|
|
202
|
-
return
|
|
204
|
+
return this.numExperts * sum;
|
|
203
205
|
}
|
|
204
206
|
}
|
|
205
207
|
|
package/src/moe/moe_trainer.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* WebGPU optimiser kernel would accelerate.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { SharedExpertMoE } from "./moe_model.js";
|
|
11
|
+
import { LoadBalanceAccumulator, SharedExpertMoE } from "./moe_model.js";
|
|
12
12
|
import { AdamW } from "../optim/adamw.js";
|
|
13
13
|
|
|
14
14
|
export interface MoESample {
|
|
@@ -87,7 +87,7 @@ export class MoETrainer {
|
|
|
87
87
|
// Forward + task backward, retaining (x, probs) for the batch aux gradient.
|
|
88
88
|
const xs: Float32Array[] = [];
|
|
89
89
|
const probsList: Float32Array[] = [];
|
|
90
|
-
const
|
|
90
|
+
const balance = new LoadBalanceAccumulator(numExperts);
|
|
91
91
|
let batchLoss = 0;
|
|
92
92
|
|
|
93
93
|
for (const s of batch) {
|
|
@@ -101,12 +101,11 @@ export class MoETrainer {
|
|
|
101
101
|
this.model.backward(dOut, f.cache);
|
|
102
102
|
xs.push(f.cache.x);
|
|
103
103
|
probsList.push(f.route.probs);
|
|
104
|
-
|
|
104
|
+
balance.observe(f.route);
|
|
105
105
|
}
|
|
106
106
|
|
|
107
107
|
// Load-balancing aux gradient (batch-level): f = dispatch fractions.
|
|
108
|
-
const
|
|
109
|
-
const fVec = Float32Array.from(counts, (c) => c / dispatched);
|
|
108
|
+
const fVec = balance.dispatchFractions();
|
|
110
109
|
const scale = (this.opt.auxWeight * numExperts) / batch.length;
|
|
111
110
|
for (let i = 0; i < xs.length; i++) {
|
|
112
111
|
this.model.auxGradStep(xs[i]!, probsList[i]!, fVec, scale);
|
|
@@ -117,9 +116,7 @@ export class MoETrainer {
|
|
|
117
116
|
this.adam.step();
|
|
118
117
|
|
|
119
118
|
epochLoss += batchLoss;
|
|
120
|
-
lastAux =
|
|
121
|
-
? probsList.reduce((s, p) => s + p[e]!, 0) / probsList.length
|
|
122
|
-
: 0), 0);
|
|
119
|
+
lastAux = balance.loss();
|
|
123
120
|
}
|
|
124
121
|
|
|
125
122
|
return { loss: epochLoss / Math.max(1, samples.length), auxLoss: lastAux };
|
package/src/utils/rng.ts
CHANGED
|
@@ -50,16 +50,20 @@ export function setInitSeed(seed: number | undefined): void {
|
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
/**
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
53
|
+
/**
|
|
54
|
+
* Box–Muller Gaussian sample. Draws from the active init source unless `next` names
|
|
55
|
+
* another uniform [0,1) source — a caller-owned {@link SeededRng}, say, when the draw
|
|
56
|
+
* must be reproducible without touching the process-wide seed.
|
|
57
|
+
*/
|
|
58
|
+
export function randn(std = 1, next: () => number = _next): number {
|
|
59
|
+
const u1 = Math.max(next(), 1e-12);
|
|
60
|
+
const u2 = next();
|
|
57
61
|
return std * Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
|
|
58
62
|
}
|
|
59
63
|
|
|
60
64
|
/** Returns a Float32Array of `n` Gaussian samples with the given standard deviation. */
|
|
61
|
-
export function gaussianArray(n: number, std: number): Float32Array {
|
|
65
|
+
export function gaussianArray(n: number, std: number, next?: () => number): Float32Array {
|
|
62
66
|
const a = new Float32Array(n);
|
|
63
|
-
for (let i = 0; i < n; i++) a[i] = randn(std);
|
|
67
|
+
for (let i = 0; i < n; i++) a[i] = randn(std, next);
|
|
64
68
|
return a;
|
|
65
69
|
}
|