@seanhogg/builderforce-memory-engine 2026.6.31 → 2026.6.35

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 (90) hide show
  1. package/README.md +2 -0
  2. package/dist/bench/harness.d.ts +37 -0
  3. package/dist/bench/harness.d.ts.map +1 -0
  4. package/dist/bench/harness.js +171 -0
  5. package/dist/bench/harness.js.map +1 -0
  6. package/dist/bench/index.d.ts +11 -0
  7. package/dist/bench/index.d.ts.map +1 -0
  8. package/dist/bench/index.js +10 -0
  9. package/dist/bench/index.js.map +1 -0
  10. package/dist/bench/metrics.d.ts +36 -0
  11. package/dist/bench/metrics.d.ts.map +1 -0
  12. package/dist/bench/metrics.js +83 -0
  13. package/dist/bench/metrics.js.map +1 -0
  14. package/dist/bench/types.d.ts +119 -0
  15. package/dist/bench/types.d.ts.map +1 -0
  16. package/dist/bench/types.js +16 -0
  17. package/dist/bench/types.js.map +1 -0
  18. package/dist/export/gguf.d.ts +23 -0
  19. package/dist/export/gguf.d.ts.map +1 -0
  20. package/dist/export/gguf.js +158 -0
  21. package/dist/export/gguf.js.map +1 -0
  22. package/dist/export/hf.d.ts +32 -0
  23. package/dist/export/hf.d.ts.map +1 -0
  24. package/dist/export/hf.js +138 -0
  25. package/dist/export/hf.js.map +1 -0
  26. package/dist/export/index.d.ts +57 -0
  27. package/dist/export/index.d.ts.map +1 -0
  28. package/dist/export/index.js +85 -0
  29. package/dist/export/index.js.map +1 -0
  30. package/dist/export/onnx.d.ts +32 -0
  31. package/dist/export/onnx.d.ts.map +1 -0
  32. package/dist/export/onnx.js +265 -0
  33. package/dist/export/onnx.js.map +1 -0
  34. package/dist/export/protobuf.d.ts +34 -0
  35. package/dist/export/protobuf.d.ts.map +1 -0
  36. package/dist/export/protobuf.js +77 -0
  37. package/dist/export/protobuf.js.map +1 -0
  38. package/dist/export/safetensors.d.ts +22 -0
  39. package/dist/export/safetensors.d.ts.map +1 -0
  40. package/dist/export/safetensors.js +66 -0
  41. package/dist/export/safetensors.js.map +1 -0
  42. package/dist/export/tensors.d.ts +51 -0
  43. package/dist/export/tensors.d.ts.map +1 -0
  44. package/dist/export/tensors.js +76 -0
  45. package/dist/export/tensors.js.map +1 -0
  46. package/dist/index.d.ts +9 -1
  47. package/dist/index.d.ts.map +1 -1
  48. package/dist/index.js +8 -1
  49. package/dist/index.js.map +1 -1
  50. package/dist/kernels/selective_scan.d.ts.map +1 -1
  51. package/dist/kernels/selective_scan.js +7 -3
  52. package/dist/kernels/selective_scan.js.map +1 -1
  53. package/dist/lm/evermind_lm.d.ts +17 -1
  54. package/dist/lm/evermind_lm.d.ts.map +1 -1
  55. package/dist/lm/evermind_lm.js +49 -5
  56. package/dist/lm/evermind_lm.js.map +1 -1
  57. package/dist/model/mamba_model.d.ts.map +1 -1
  58. package/dist/model/mamba_model.js +9 -1
  59. package/dist/model/mamba_model.js.map +1 -1
  60. package/dist/utils/crc32.d.ts +37 -0
  61. package/dist/utils/crc32.d.ts.map +1 -0
  62. package/dist/utils/crc32.js +70 -0
  63. package/dist/utils/crc32.js.map +1 -0
  64. package/dist/utils/delta.d.ts +55 -0
  65. package/dist/utils/delta.d.ts.map +1 -0
  66. package/dist/utils/delta.js +148 -0
  67. package/dist/utils/delta.js.map +1 -0
  68. package/dist/utils/gpu_utils.d.ts +21 -0
  69. package/dist/utils/gpu_utils.d.ts.map +1 -1
  70. package/dist/utils/gpu_utils.js +64 -5
  71. package/dist/utils/gpu_utils.js.map +1 -1
  72. package/package.json +1 -1
  73. package/src/bench/harness.ts +219 -0
  74. package/src/bench/index.ts +37 -0
  75. package/src/bench/metrics.ts +98 -0
  76. package/src/bench/types.ts +121 -0
  77. package/src/export/gguf.ts +168 -0
  78. package/src/export/hf.ts +155 -0
  79. package/src/export/index.ts +128 -0
  80. package/src/export/onnx.ts +331 -0
  81. package/src/export/protobuf.ts +86 -0
  82. package/src/export/safetensors.ts +77 -0
  83. package/src/export/tensors.ts +107 -0
  84. package/src/index.ts +70 -0
  85. package/src/kernels/selective_scan.ts +7 -3
  86. package/src/lm/evermind_lm.ts +53 -8
  87. package/src/model/mamba_model.ts +9 -1
  88. package/src/utils/crc32.ts +82 -0
  89. package/src/utils/delta.ts +156 -0
  90. package/src/utils/gpu_utils.ts +60 -5
@@ -0,0 +1,155 @@
1
+ /**
2
+ * export/hf.ts — the Hugging Face repository sidecar files.
3
+ *
4
+ * The JSON/markdown that turns raw weights into an ingestible HF model repo:
5
+ * • config.json — architecture config (model_type "evermind")
6
+ * • generation_config.json — default decoding params
7
+ * • tokenizer.json — the byte-level BPE in HF `tokenizers` format
8
+ * • README.md — the model card (YAML front-matter + body)
9
+ *
10
+ * The tokenizer uses the same GPT-2 byte→unicode alphabet as {@link BPETokenizer}
11
+ * (a ByteLevel pre-tokenizer/decoder), so HF `tokenizers` / transformers.js
12
+ * reproduce our encode/decode.
13
+ */
14
+
15
+ import type { EvermindLM } from "../lm/evermind_lm.js";
16
+ import type { BPETokenizer } from "../tokenizer/bpe.js";
17
+ import { archOf, paramCount } from "./tensors.js";
18
+
19
+ export interface HfMeta {
20
+ name?: string;
21
+ version?: string;
22
+ license?: string;
23
+ author?: string;
24
+ description?: string;
25
+ tags?: string[];
26
+ }
27
+
28
+ /** HF `config.json` describing the Evermind architecture. */
29
+ export function configJson(lm: EvermindLM): Record<string, unknown> {
30
+ const a = archOf(lm);
31
+ return {
32
+ model_type: "evermind",
33
+ architectures: ["EvermindForCausalLM"],
34
+ vocab_size: a.vocabSize,
35
+ d_model: a.dModel,
36
+ hidden_size: a.dModel,
37
+ num_hidden_layers: a.numLayers,
38
+ conv_kernel: a.convKernel,
39
+ intermediate_size: a.hiddenDim,
40
+ num_experts: a.numExperts,
41
+ num_experts_per_tok: a.topK,
42
+ rms_norm_eps: 1e-5,
43
+ tie_word_embeddings: true,
44
+ torch_dtype: "float32",
45
+ transformers_version: "4.0.0",
46
+ };
47
+ }
48
+
49
+ /** HF `generation_config.json` — greedy by default, matching the reference. */
50
+ export function generationConfigJson(): Record<string, unknown> {
51
+ return { do_sample: false, temperature: 1.0, max_new_tokens: 64 };
52
+ }
53
+
54
+ /** HF `tokenizer.json` (the `tokenizers` fast-tokenizer format) for the BPE. */
55
+ export function tokenizerJson(tok: BPETokenizer): Record<string, unknown> {
56
+ const vocab: Record<string, number> = {};
57
+ for (const [token, id] of tok.vocab) vocab[token] = id;
58
+ const merges = [...tok.merges.keys()]; // already "a b" space-separated pairs
59
+
60
+ const added: { id: number; content: string }[] = [];
61
+ for (const content of [tok.unkToken, tok.bosToken, tok.eosToken, tok.padToken]) {
62
+ const id = tok.vocab.get(content);
63
+ if (id !== undefined && !added.some((t) => t.id === id)) added.push({ id, content });
64
+ }
65
+
66
+ return {
67
+ version: "1.0",
68
+ truncation: null,
69
+ padding: null,
70
+ added_tokens: added.map((t) => ({
71
+ id: t.id,
72
+ content: t.content,
73
+ single_word: false,
74
+ lstrip: false,
75
+ rstrip: false,
76
+ normalized: false,
77
+ special: true,
78
+ })),
79
+ normalizer: null,
80
+ pre_tokenizer: { type: "ByteLevel", add_prefix_space: false, trim_offsets: true, use_regex: true },
81
+ post_processor: { type: "ByteLevel", add_prefix_space: true, trim_offsets: false, use_regex: true },
82
+ decoder: { type: "ByteLevel", add_prefix_space: true, trim_offsets: true, use_regex: true },
83
+ model: {
84
+ type: "BPE",
85
+ dropout: null,
86
+ unk_token: null,
87
+ continuing_subword_prefix: null,
88
+ end_of_word_suffix: null,
89
+ fuse_unk: false,
90
+ byte_fallback: false,
91
+ vocab,
92
+ merges,
93
+ },
94
+ };
95
+ }
96
+
97
+ /** HF model card (`README.md`) with YAML front-matter. */
98
+ export function modelCardMarkdown(lm: EvermindLM, meta: HfMeta = {}): string {
99
+ const a = archOf(lm);
100
+ const params = paramCount(lm);
101
+ const name = meta.name ?? "Evermind";
102
+ const license = meta.license ?? "mit";
103
+ const tags = ["evermind", "ssm", "mixture-of-experts", "text-generation", ...(meta.tags ?? [])];
104
+ const front = [
105
+ "---",
106
+ `license: ${license}`,
107
+ "library_name: builderforce-memory",
108
+ "pipeline_tag: text-generation",
109
+ "tags:",
110
+ ...tags.map((t) => ` - ${t}`),
111
+ "---",
112
+ ].join("\n");
113
+
114
+ const body = `
115
+ # ${name}
116
+
117
+ ${meta.description ?? "A self-updating SSM language model built with the BuilderForce Evermind engine."}
118
+
119
+ An **Evermind** model: a shared-expert hybrid SSM (depthwise causal conv token-mixer
120
+ + top-k Mixture-of-Experts channel-mixer, tied embeddings). Trained and run natively
121
+ in TypeScript/WebGPU; this repository is an export for the Hugging Face ecosystem.
122
+
123
+ ## Architecture
124
+
125
+ | field | value |
126
+ | --- | --- |
127
+ | vocab size | ${a.vocabSize} |
128
+ | d_model | ${a.dModel} |
129
+ | layers | ${a.numLayers} |
130
+ | conv kernel | ${a.convKernel} |
131
+ | FFN hidden | ${a.hiddenDim} |
132
+ | experts (top-k) | ${a.numExperts} (${a.topK}) |
133
+ | parameters | ${params.toLocaleString("en-US")} |
134
+
135
+ ## Files
136
+
137
+ - \`model.safetensors\` — weights (HF-native, F32).
138
+ - \`model.onnx\` — runnable graph (\`input_ids\` → \`logits\`); load with onnxruntime / transformers.js.
139
+ - \`config.json\`, \`generation_config.json\`, \`tokenizer.json\` — HF sidecars.
140
+ - \`model.gguf\` — GGUF container (custom architecture; for GGUF tooling).
141
+ - \`model.evermind\` — the native package; runs in \`@seanhogg/builderforce-memory\` (full self-update).
142
+
143
+ ## Usage (ONNX, no KV-cache — re-run full context per step)
144
+
145
+ \`\`\`js
146
+ import * as ort from "onnxruntime-web";
147
+ const session = await ort.InferenceSession.create("model.onnx");
148
+ const ids = BigInt64Array.from([/* token ids */].map(BigInt));
149
+ const input_ids = new ort.Tensor("int64", ids, [1, ids.length]);
150
+ const { logits } = await session.run({ input_ids });
151
+ \`\`\`
152
+ ${meta.author ? `\n## Author\n\n${meta.author}\n` : ""}`;
153
+
154
+ return `${front}\n${body}`;
155
+ }
@@ -0,0 +1,128 @@
1
+ /**
2
+ * export/index.ts — the model-export registry.
3
+ *
4
+ * One entry point — {@link exportEvermind} — turns a trained {@link EvermindLM}
5
+ * (+ its tokenizer) into a named set of files in a requested format. The runtime
6
+ * is untouched: export reads the model through its public surface only. This is
7
+ * the *publishing* seam — training/serving stay in the WebGPU/TS runtime; export
8
+ * is offered as a separate step that emits portable artifacts (ONNX, safetensors,
9
+ * GGUF) and a complete Hugging Face repo bundle.
10
+ *
11
+ * Formats:
12
+ * • "safetensors" → model.safetensors
13
+ * • "onnx" → model.onnx
14
+ * • "gguf" → model.gguf
15
+ * • "huggingface" → a full HF repo (safetensors + onnx + gguf + config +
16
+ * tokenizer.json + generation_config + README), ready to push
17
+ */
18
+
19
+ import type { EvermindLM } from "../lm/evermind_lm.js";
20
+ import type { BPETokenizer } from "../tokenizer/bpe.js";
21
+ import { paramCount } from "./tensors.js";
22
+ import { exportSafetensors } from "./safetensors.js";
23
+ import { exportOnnx } from "./onnx.js";
24
+ import { exportGguf } from "./gguf.js";
25
+ import { configJson, generationConfigJson, tokenizerJson, modelCardMarkdown, type HfMeta } from "./hf.js";
26
+
27
+ export type ExportFormat = "safetensors" | "onnx" | "gguf" | "huggingface";
28
+
29
+ /** One emitted file. `data` is bytes (binary) or a string (text). */
30
+ export interface ExportFile {
31
+ path: string;
32
+ data: Uint8Array | string;
33
+ contentType: string;
34
+ }
35
+
36
+ export interface ExportResult {
37
+ format: ExportFormat;
38
+ files: ExportFile[];
39
+ /** Total trainable scalar parameters of the exported model. */
40
+ paramCount: number;
41
+ }
42
+
43
+ export interface ExportOptions extends HfMeta {
44
+ /** Store weights as float16 where the format supports it. Default false. */
45
+ fp16?: boolean;
46
+ }
47
+
48
+ /** Catalog of formats for UI pickers (id + human label + extension). */
49
+ export const EXPORT_FORMATS: { id: ExportFormat; label: string; description: string; ext: string }[] = [
50
+ { id: "huggingface", label: "Hugging Face repo", description: "Full publishable repo: safetensors + ONNX + GGUF + config + tokenizer + model card", ext: "/" },
51
+ { id: "onnx", label: "ONNX", description: "Runnable graph for onnxruntime / transformers.js (input_ids → logits)", ext: ".onnx" },
52
+ { id: "safetensors", label: "Safetensors", description: "HF-native weight format (lossless F32 / half-size F16)", ext: ".safetensors" },
53
+ { id: "gguf", label: "GGUF", description: "llama.cpp container (custom architecture; for GGUF tooling)", ext: ".gguf" },
54
+ ];
55
+
56
+ const JSON_CT = "application/json";
57
+
58
+ function jsonFile(path: string, obj: unknown): ExportFile {
59
+ return { path, data: JSON.stringify(obj, null, 2), contentType: JSON_CT };
60
+ }
61
+
62
+ /**
63
+ * Export a trained model in the requested format. `tokenizer` is required for the
64
+ * "huggingface" bundle (to emit tokenizer.json); the weight-only formats ignore it.
65
+ */
66
+ export function exportEvermind(
67
+ lm: EvermindLM,
68
+ format: ExportFormat,
69
+ opts: ExportOptions = {},
70
+ tokenizer?: BPETokenizer,
71
+ ): ExportResult {
72
+ const params = paramCount(lm);
73
+ const base = (): ExportResult => ({ format, files: [], paramCount: params });
74
+
75
+ switch (format) {
76
+ case "safetensors": {
77
+ const r = base();
78
+ r.files.push({
79
+ path: "model.safetensors",
80
+ data: exportSafetensors(lm, { fp16: opts.fp16 }),
81
+ contentType: "application/octet-stream",
82
+ });
83
+ return r;
84
+ }
85
+ case "onnx": {
86
+ const r = base();
87
+ r.files.push({
88
+ path: "model.onnx",
89
+ data: exportOnnx(lm, { producerVersion: opts.version }),
90
+ contentType: "application/octet-stream",
91
+ });
92
+ return r;
93
+ }
94
+ case "gguf": {
95
+ const r = base();
96
+ r.files.push({
97
+ path: "model.gguf",
98
+ data: exportGguf(lm, { name: opts.name, fp16: opts.fp16 }),
99
+ contentType: "application/octet-stream",
100
+ });
101
+ return r;
102
+ }
103
+ case "huggingface": {
104
+ if (!tokenizer) throw new Error("exportEvermind('huggingface'): a tokenizer is required for tokenizer.json");
105
+ const r = base();
106
+ r.files.push(
107
+ { path: "model.safetensors", data: exportSafetensors(lm, { fp16: opts.fp16 }), contentType: "application/octet-stream" },
108
+ { path: "model.onnx", data: exportOnnx(lm, { producerVersion: opts.version }), contentType: "application/octet-stream" },
109
+ { path: "model.gguf", data: exportGguf(lm, { name: opts.name, fp16: opts.fp16 }), contentType: "application/octet-stream" },
110
+ jsonFile("config.json", configJson(lm)),
111
+ jsonFile("generation_config.json", generationConfigJson()),
112
+ jsonFile("tokenizer.json", tokenizerJson(tokenizer)),
113
+ { path: "README.md", data: modelCardMarkdown(lm, opts), contentType: "text/markdown" },
114
+ );
115
+ return r;
116
+ }
117
+ default:
118
+ throw new Error(`exportEvermind: unknown format "${String(format)}"`);
119
+ }
120
+ }
121
+
122
+ export { exportSafetensors, tensorsToSafetensors } from "./safetensors.js";
123
+ export { exportOnnx } from "./onnx.js";
124
+ export { exportGguf } from "./gguf.js";
125
+ export { configJson, generationConfigJson, tokenizerJson, modelCardMarkdown } from "./hf.js";
126
+ export type { HfMeta } from "./hf.js";
127
+ export { namedTensors, evermindTensorSpec, archOf, paramCount } from "./tensors.js";
128
+ export type { NamedTensor, EvermindArch, TensorSpec } from "./tensors.js";
@@ -0,0 +1,331 @@
1
+ /**
2
+ * export/onnx.ts — export a trained {@link EvermindLM} to a runnable ONNX graph.
3
+ *
4
+ * Produces a standard causal-LM ONNX `ModelProto`: input `input_ids` [batch,seq]
5
+ * (int64) → output `logits` [batch,seq,vocab] (float). No KV cache — the model is
6
+ * stateless and recomputes the full context each step, so a host (onnxruntime-web,
7
+ * onnxruntime-node, or transformers.js driving the generation loop) re-runs the
8
+ * whole sequence per token. The graph reproduces the CPU reference forward
9
+ * EXACTLY:
10
+ *
11
+ * • embedding → Gather(emb, input_ids)
12
+ * • RMSNorm → Mul/ReduceMean/Add/Sqrt/Div/Mul (eps = 1e-5)
13
+ * • causal → Transpose → Conv(group=D, pads=[K-1,0], kernel reversed) → Transpose
14
+ * depthwise conv (so out[t] = Σⱼ ker[j]·x[t−j], matching the reference tap order)
15
+ * • MoE → router MatMul → TopK → Softmax → ScatterElements (dense top-k mask)
16
+ * → shared FFN + Σₑ combineₑ · expertₑ(x) (numerically identical to
17
+ * the sparse reference: non-selected experts get combine 0)
18
+ * • tied head → MatMul(x, embᵀ)
19
+ *
20
+ * Opset 18 / IR v8. Weights are emitted as raw little-endian f32 initializers that
21
+ * are byte-identical to {@link namedTensors} (matmul weights are transposed by a
22
+ * graph node at runtime, not duplicated on disk).
23
+ *
24
+ * Zero dependencies — the protobuf is hand-encoded by {@link ./protobuf}.
25
+ */
26
+
27
+ import type { EvermindLM } from "../lm/evermind_lm.js";
28
+ import { archOf, namedTensors, type EvermindArch, type NamedTensor } from "./tensors.js";
29
+ import { ProtoWriter, float32ToBytes } from "./protobuf.js";
30
+
31
+ const OPSET = 18;
32
+ const IR_VERSION = 8;
33
+ const ELEM_FLOAT = 1;
34
+ const ELEM_INT64 = 7;
35
+
36
+ // AttributeProto.AttributeType
37
+ const ATTR_FLOAT = 1;
38
+ const ATTR_INT = 2;
39
+ const ATTR_STRING = 3;
40
+ const ATTR_INTS = 7;
41
+
42
+ type Attr =
43
+ | { kind: "i"; name: string; value: number }
44
+ | { kind: "ints"; name: string; value: number[] }
45
+ | { kind: "f"; name: string; value: number }
46
+ | { kind: "s"; name: string; value: string };
47
+
48
+ interface NodeDef {
49
+ op: string;
50
+ inputs: string[];
51
+ outputs: string[];
52
+ name: string;
53
+ attrs: Attr[];
54
+ }
55
+
56
+ interface InitDef {
57
+ name: string;
58
+ dims: number[];
59
+ dataType: number;
60
+ raw?: Uint8Array;
61
+ int64?: number[];
62
+ }
63
+
64
+ /** Accumulates a single ONNX GraphProto (nodes + initializers) with auto-naming. */
65
+ class GraphBuilder {
66
+ readonly nodes: NodeDef[] = [];
67
+ readonly inits: InitDef[] = [];
68
+ private uid = 0;
69
+
70
+ /** A fresh intermediate value name. */
71
+ tmp(hint: string): string {
72
+ return `${hint}_${this.uid++}`;
73
+ }
74
+
75
+ initFloat(name: string, dims: number[], data: Float32Array): string {
76
+ this.inits.push({ name, dims, dataType: ELEM_FLOAT, raw: float32ToBytes(data) });
77
+ return name;
78
+ }
79
+
80
+ initInt64(name: string, dims: number[], values: number[]): string {
81
+ this.inits.push({ name, dims, dataType: ELEM_INT64, int64: values });
82
+ return name;
83
+ }
84
+
85
+ node(op: string, inputs: string[], outputs: string[], attrs: Attr[] = []): string[] {
86
+ this.nodes.push({ op, inputs, outputs, name: `${op}_${this.uid++}`, attrs });
87
+ return outputs;
88
+ }
89
+
90
+ /** Single-output convenience: returns the output value name. */
91
+ op(op: string, inputs: string[], hint: string, attrs: Attr[] = []): string {
92
+ const out = this.tmp(hint);
93
+ this.node(op, inputs, [out], attrs);
94
+ return out;
95
+ }
96
+ }
97
+
98
+ /** Transpose a 2-D row-major matrix [rows, cols] → [cols, rows]. */
99
+ function transpose2d(data: Float32Array, rows: number, cols: number): Float32Array {
100
+ const out = new Float32Array(data.length);
101
+ for (let r = 0; r < rows; r++) for (let c = 0; c < cols; c++) out[c * rows + r] = data[r * cols + c]!;
102
+ return out;
103
+ }
104
+
105
+ /** RMSNorm(x, gain) as graph ops. Returns the normalised value name. */
106
+ function rmsNorm(g: GraphBuilder, x: string, gainInit: string, eps: string, axes: string): string {
107
+ const sq = g.op("Mul", [x, x], "rms_sq");
108
+ const ms = g.op("ReduceMean", [sq, axes], "rms_mean", [{ kind: "i", name: "keepdims", value: 1 }]);
109
+ const msE = g.op("Add", [ms, eps], "rms_eps");
110
+ const r = g.op("Sqrt", [msE], "rms_r");
111
+ const div = g.op("Div", [x, r], "rms_div");
112
+ return g.op("Mul", [div, gainInit], "rms_y");
113
+ }
114
+
115
+ /** A 2-layer FFN expert: y = (relu(x·W1ᵀ + b1))·W2ᵀ + b2. Weights transposed at runtime. */
116
+ function ffn(
117
+ g: GraphBuilder,
118
+ x: string,
119
+ w1: string,
120
+ b1: string,
121
+ w2: string,
122
+ b2: string,
123
+ ): string {
124
+ const w1t = g.op("Transpose", [w1], "w1t", [{ kind: "ints", name: "perm", value: [1, 0] }]);
125
+ const pre = g.op("Add", [g.op("MatMul", [x, w1t], "ffn_mm1"), b1], "ffn_pre");
126
+ const h = g.op("Relu", [pre], "ffn_h");
127
+ const w2t = g.op("Transpose", [w2], "w2t", [{ kind: "ints", name: "perm", value: [1, 0] }]);
128
+ return g.op("Add", [g.op("MatMul", [h, w2t], "ffn_mm2"), b2], "ffn_y");
129
+ }
130
+
131
+ /** Build the ONNX bytes for a trained LM. */
132
+ export function exportOnnx(lm: EvermindLM, opts: { producerName?: string; producerVersion?: string } = {}): Uint8Array {
133
+ const arch = archOf(lm);
134
+ const tensors = namedTensors(lm);
135
+ const byName = new Map<string, NamedTensor>(tensors.map((t) => [t.name, t]));
136
+ const get = (name: string): NamedTensor => {
137
+ const t = byName.get(name);
138
+ if (!t) throw new Error(`export/onnx: missing tensor ${name}`);
139
+ return t;
140
+ };
141
+
142
+ const g = new GraphBuilder();
143
+ const { vocabSize: V, dModel: D, numLayers: L, convKernel: K, numExperts: E, topK } = arch;
144
+
145
+ // Shared scalar/axis initializers.
146
+ const emb = g.initFloat("token_embedding.weight", [V, D], get("token_embedding.weight").data);
147
+ const eps = g.initFloat("rms_eps", [1], new Float32Array([1e-5]));
148
+ const axesLast = g.initInt64("axes_last", [1], [2]); // feature axis of [B,S,*]
149
+ const topkK = g.initInt64("topk_k", [1], [topK]);
150
+
151
+ // input_ids → embedded [B,S,D]
152
+ let x = g.op("Gather", [emb, "input_ids"], "embedded", [{ kind: "i", name: "axis", value: 0 }]);
153
+
154
+ for (let l = 0; l < L; l++) {
155
+ const p = `layers.${l}`;
156
+
157
+ // ── Causal depthwise conv sub-block ──────────────────────────────────────
158
+ const nConv = g.initFloat(`${p}.norm_conv.weight`, [D], get(`${p}.norm_conv.weight`).data);
159
+ const normedC = rmsNorm(g, x, nConv, eps, axesLast);
160
+ // Conv weight: reference ker[c,j] → ONNX W[c,0,K-1-j] (reverse taps), shape [D,1,K].
161
+ const ker = get(`${p}.conv.weight`).data; // [D,K]
162
+ const cw = new Float32Array(D * K);
163
+ for (let c = 0; c < D; c++) for (let j = 0; j < K; j++) cw[c * K + (K - 1 - j)] = ker[c * K + j]!;
164
+ const convW = g.initFloat(`${p}.conv.onnx_weight`, [D, 1, K], cw);
165
+ const xt = g.op("Transpose", [normedC], "conv_in", [{ kind: "ints", name: "perm", value: [0, 2, 1] }]); // [B,D,S]
166
+ const cOut = g.op("Conv", [xt, convW], "conv_out", [
167
+ { kind: "i", name: "group", value: D },
168
+ { kind: "ints", name: "kernel_shape", value: [K] },
169
+ { kind: "ints", name: "pads", value: [K - 1, 0] },
170
+ { kind: "ints", name: "strides", value: [1] },
171
+ { kind: "ints", name: "dilations", value: [1] },
172
+ ]);
173
+ const cOutT = g.op("Transpose", [cOut], "conv_back", [{ kind: "ints", name: "perm", value: [0, 2, 1] }]); // [B,S,D]
174
+ const afterConv = g.op("Add", [x, cOutT], "after_conv");
175
+
176
+ // ── MoE channel-mixer sub-block ──────────────────────────────────────────
177
+ const nMoe = g.initFloat(`${p}.norm_moe.weight`, [D], get(`${p}.norm_moe.weight`).data);
178
+ const normedM = rmsNorm(g, afterConv, nMoe, eps, axesLast);
179
+
180
+ const wr = g.initFloat(`${p}.moe.router.weight`, [E, D], get(`${p}.moe.router.weight`).data);
181
+ const wrT = g.op("Transpose", [wr], "router_t", [{ kind: "ints", name: "perm", value: [1, 0] }]); // [D,E]
182
+ const routerLogits = g.op("MatMul", [normedM, wrT], "router_logits"); // [B,S,E]
183
+ const topkV = g.tmp("topk_v");
184
+ const topkI = g.tmp("topk_i");
185
+ g.node("TopK", [routerLogits, topkK], [topkV, topkI], [
186
+ { kind: "i", name: "axis", value: 2 },
187
+ { kind: "i", name: "largest", value: 1 },
188
+ { kind: "i", name: "sorted", value: 1 },
189
+ ]);
190
+ const gates = g.op("Softmax", [topkV], "gates", [{ kind: "i", name: "axis", value: 2 }]); // [B,S,k]
191
+ const zeros = g.op("Sub", [routerLogits, routerLogits], "zeros"); // [B,S,E]
192
+ const combine = g.op("ScatterElements", [zeros, topkI, gates], "combine", [{ kind: "i", name: "axis", value: 2 }]); // [B,S,E]
193
+
194
+ // shared expert (always active)
195
+ const sharedY = ffn(
196
+ g,
197
+ normedM,
198
+ g.initFloat(`${p}.moe.shared.w1`, [arch.hiddenDim, D], get(`${p}.moe.shared.w1`).data),
199
+ g.initFloat(`${p}.moe.shared.b1`, [arch.hiddenDim], get(`${p}.moe.shared.b1`).data),
200
+ g.initFloat(`${p}.moe.shared.w2`, [D, arch.hiddenDim], get(`${p}.moe.shared.w2`).data),
201
+ g.initFloat(`${p}.moe.shared.b2`, [D], get(`${p}.moe.shared.b2`).data),
202
+ );
203
+
204
+ // per-expert combine weight: split [B,S,E] → E × [B,S,1]
205
+ const splitOuts = Array.from({ length: E }, (_, e) => g.tmp(`combine_${e}`));
206
+ g.node("Split", [combine], splitOuts, [
207
+ { kind: "i", name: "axis", value: 2 },
208
+ { kind: "i", name: "num_outputs", value: E },
209
+ ]);
210
+
211
+ let moeOut = sharedY;
212
+ for (let e = 0; e < E; e++) {
213
+ const ep = `${p}.moe.experts.${e}`;
214
+ const ye = ffn(
215
+ g,
216
+ normedM,
217
+ g.initFloat(`${ep}.w1`, [arch.hiddenDim, D], get(`${ep}.w1`).data),
218
+ g.initFloat(`${ep}.b1`, [arch.hiddenDim], get(`${ep}.b1`).data),
219
+ g.initFloat(`${ep}.w2`, [D, arch.hiddenDim], get(`${ep}.w2`).data),
220
+ g.initFloat(`${ep}.b2`, [D], get(`${ep}.b2`).data),
221
+ );
222
+ const weighted = g.op("Mul", [splitOuts[e]!, ye], "expert_w"); // [B,S,1]·[B,S,D] broadcast
223
+ moeOut = g.op("Add", [moeOut, weighted], "moe_acc");
224
+ }
225
+
226
+ x = g.op("Add", [afterConv, moeOut], "after_moe");
227
+ }
228
+
229
+ // Tied head: logits = x · embᵀ.
230
+ const embT = g.op("Transpose", [emb], "emb_t", [{ kind: "ints", name: "perm", value: [1, 0] }]); // [D,V]
231
+ g.node("MatMul", [x, embT], ["logits"]);
232
+
233
+ return encodeModel(g, arch, opts);
234
+ }
235
+
236
+ // ── protobuf encoding ──────────────────────────────────────────────────────────
237
+
238
+ function attrProto(a: Attr): ProtoWriter {
239
+ const w = new ProtoWriter();
240
+ w.string(1, a.name);
241
+ switch (a.kind) {
242
+ case "i":
243
+ w.varint(20, ATTR_INT);
244
+ w.varint(3, a.value);
245
+ break;
246
+ case "ints":
247
+ w.varint(20, ATTR_INTS);
248
+ for (const v of a.value) w.varint(8, v); // repeated (unpacked) int64
249
+ break;
250
+ case "f":
251
+ w.varint(20, ATTR_FLOAT);
252
+ w.float(2, a.value);
253
+ break;
254
+ case "s":
255
+ w.varint(20, ATTR_STRING);
256
+ w.string(4, a.value);
257
+ break;
258
+ }
259
+ return w;
260
+ }
261
+
262
+ function nodeProto(n: NodeDef): ProtoWriter {
263
+ const w = new ProtoWriter();
264
+ for (const i of n.inputs) w.string(1, i);
265
+ for (const o of n.outputs) w.string(2, o);
266
+ w.string(3, n.name);
267
+ w.string(4, n.op);
268
+ for (const a of n.attrs) w.message(5, attrProto(a));
269
+ return w;
270
+ }
271
+
272
+ function tensorProto(t: InitDef): ProtoWriter {
273
+ const w = new ProtoWriter();
274
+ for (const d of t.dims) w.varint(1, d); // repeated int64 dims
275
+ w.varint(2, t.dataType);
276
+ w.string(8, t.name);
277
+ if (t.int64) for (const v of t.int64) w.varint(7, v); // int64_data (unpacked)
278
+ if (t.raw) w.bytes(9, t.raw); // raw_data (TensorProto field 9)
279
+ return w;
280
+ }
281
+
282
+ function dimParam(name: string): ProtoWriter {
283
+ const w = new ProtoWriter();
284
+ w.string(2, name); // Dimension.dim_param
285
+ return w;
286
+ }
287
+ function dimValue(v: number): ProtoWriter {
288
+ const w = new ProtoWriter();
289
+ w.varint(1, v); // Dimension.dim_value
290
+ return w;
291
+ }
292
+
293
+ /** ValueInfoProto for a tensor of the given element type and (symbolic) dims. */
294
+ function valueInfo(name: string, elemType: number, dims: (number | string)[]): ProtoWriter {
295
+ const shape = new ProtoWriter();
296
+ for (const d of dims) shape.message(1, typeof d === "string" ? dimParam(d) : dimValue(d));
297
+ const tensorType = new ProtoWriter();
298
+ tensorType.varint(1, elemType); // elem_type
299
+ tensorType.message(2, shape); // shape
300
+ const type = new ProtoWriter();
301
+ type.message(1, tensorType); // tensor_type
302
+ const vi = new ProtoWriter();
303
+ vi.string(1, name);
304
+ vi.message(2, type);
305
+ return vi;
306
+ }
307
+
308
+ function encodeModel(
309
+ g: GraphBuilder,
310
+ arch: EvermindArch,
311
+ opts: { producerName?: string; producerVersion?: string },
312
+ ): Uint8Array {
313
+ const graph = new ProtoWriter();
314
+ for (const n of g.nodes) graph.message(1, nodeProto(n));
315
+ graph.string(2, "evermind");
316
+ for (const t of g.inits) graph.message(5, tensorProto(t));
317
+ graph.message(11, valueInfo("input_ids", ELEM_INT64, ["batch", "seq"]));
318
+ graph.message(12, valueInfo("logits", ELEM_FLOAT, ["batch", "seq", arch.vocabSize]));
319
+
320
+ const opset = new ProtoWriter();
321
+ opset.string(1, ""); // default domain
322
+ opset.varint(2, OPSET);
323
+
324
+ const model = new ProtoWriter();
325
+ model.varint(1, IR_VERSION);
326
+ model.string(2, opts.producerName ?? "builderforce-memory-engine");
327
+ model.string(3, opts.producerVersion ?? "evermind");
328
+ model.message(7, graph);
329
+ model.message(8, opset);
330
+ return model.finish();
331
+ }