@seanhogg/builderforce-memory-engine 2026.6.31 → 2026.6.34

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 +37 -0
  65. package/dist/utils/delta.d.ts.map +1 -0
  66. package/dist/utils/delta.js +108 -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 +63 -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 +117 -0
  90. package/src/utils/gpu_utils.ts +60 -5
@@ -0,0 +1,98 @@
1
+ /**
2
+ * bench/metrics.ts — pure scoring primitives over per-position logits.
3
+ *
4
+ * These are the building blocks the harness composes. They are exact, allocation
5
+ * -light, and numerically stable (cross-entropy reuses the engine's stable
6
+ * log-sum-exp). Nothing here trains or mutates the model — benchmarking is a
7
+ * read-only forward pass over held-out data.
8
+ */
9
+
10
+ import { crossEntropyLoss } from "../training/autograd.js";
11
+
12
+ /** Natural log of 2 — used to convert nats → bits. */
13
+ export const LN2 = Math.LN2;
14
+
15
+ /**
16
+ * Indices of the `k` largest logits, descending. O(V·k) — fine for the small
17
+ * top-k used in accuracy scoring; avoids a full sort of the vocab per position.
18
+ */
19
+ export function topKIndices(logits: Float32Array, k: number): number[] {
20
+ const n = logits.length;
21
+ const kk = Math.max(1, Math.min(k, n));
22
+ const idx: number[] = [];
23
+ const used = new Uint8Array(n);
24
+ for (let s = 0; s < kk; s++) {
25
+ let best = -1;
26
+ let bestVal = -Infinity;
27
+ for (let i = 0; i < n; i++) {
28
+ if (used[i]) continue;
29
+ const v = logits[i]!;
30
+ if (v > bestVal) {
31
+ bestVal = v;
32
+ best = i;
33
+ }
34
+ }
35
+ if (best < 0) break;
36
+ used[best] = 1;
37
+ idx.push(best);
38
+ }
39
+ return idx;
40
+ }
41
+
42
+ /** Index of the single largest logit (argmax). */
43
+ export function argmax(logits: Float32Array): number {
44
+ let best = 0;
45
+ let bestVal = -Infinity;
46
+ for (let i = 0; i < logits.length; i++) {
47
+ if (logits[i]! > bestVal) {
48
+ bestVal = logits[i]!;
49
+ best = i;
50
+ }
51
+ }
52
+ return best;
53
+ }
54
+
55
+ /** Perplexity from a mean cross-entropy (nats). */
56
+ export function perplexity(meanCrossEntropy: number): number {
57
+ return Math.exp(meanCrossEntropy);
58
+ }
59
+
60
+ /** Bits-per-token from a mean cross-entropy (nats). */
61
+ export function bitsPerToken(meanCrossEntropy: number): number {
62
+ return meanCrossEntropy / LN2;
63
+ }
64
+
65
+ /** Accumulator over predicted positions — cross-entropy + top-1/top-k hits. */
66
+ export interface ScoreAccumulator {
67
+ tokens: number;
68
+ ceSum: number;
69
+ top1Hits: number;
70
+ topKHits: number;
71
+ }
72
+
73
+ export function newAccumulator(): ScoreAccumulator {
74
+ return { tokens: 0, ceSum: 0, top1Hits: 0, topKHits: 0 };
75
+ }
76
+
77
+ /**
78
+ * Score one (logits, targets) pair into the accumulator. `logits[t]` predicts
79
+ * `targets[t]`; positions with an out-of-range target are skipped. The caller
80
+ * aligns logits/targets (next-token: logits[0..T-2] predict tokens[1..T-1]).
81
+ */
82
+ export function scoreInto(
83
+ acc: ScoreAccumulator,
84
+ logits: Float32Array[],
85
+ targets: number[],
86
+ k: number,
87
+ ): void {
88
+ const n = Math.min(logits.length, targets.length);
89
+ for (let t = 0; t < n; t++) {
90
+ const target = targets[t]!;
91
+ const lg = logits[t]!;
92
+ if (target < 0 || target >= lg.length) continue;
93
+ acc.ceSum += crossEntropyLoss(lg, target);
94
+ if (argmax(lg) === target) acc.top1Hits++;
95
+ if (topKIndices(lg, k).includes(target)) acc.topKHits++;
96
+ acc.tokens++;
97
+ }
98
+ }
@@ -0,0 +1,121 @@
1
+ /**
2
+ * bench/types.ts — public types for the Evermind benchmarking harness.
3
+ *
4
+ * Benchmarking answers the question the marketplace (and the "beats a frozen
5
+ * LLM" thesis) actually rests on: *how good is this model?* The harness measures
6
+ * a trained model on held-out data with the standard language-model yardsticks —
7
+ * perplexity, bits-per-token, and next-token accuracy — plus generation
8
+ * throughput, and can A/B two models (e.g. a quantized vs full-precision build,
9
+ * or a freshly-adapted vs prior checkpoint).
10
+ *
11
+ * The metrics operate on any object that can produce per-position logits from a
12
+ * token sequence — the engine's `EvermindLM` satisfies {@link LogitsModel}
13
+ * directly — so the harness is model-agnostic and dependency-free.
14
+ */
15
+
16
+ /** The minimal surface a model must expose to be benchmarked: per-position logits. */
17
+ export interface LogitsModel {
18
+ /** Run the model over a token sequence; returns one logit vector per position. */
19
+ forward(tokens: number[]): { logits: Float32Array[] };
20
+ }
21
+
22
+ /** A model whose forward pass is asynchronous (e.g. a WebGPU backend). */
23
+ export interface AsyncLogitsModel {
24
+ forward(tokens: number[]): Promise<{ logits: Float32Array[] }>;
25
+ }
26
+
27
+ /** Knobs for a benchmark run. */
28
+ export interface BenchmarkOptions {
29
+ /** k for top-k next-token accuracy. Default 5. */
30
+ topK?: number;
31
+ /** Measure forward throughput (tokens/sec). Default true. */
32
+ measureLatency?: boolean;
33
+ /**
34
+ * Monotonic clock in milliseconds, injectable for deterministic tests.
35
+ * Defaults to `performance.now()` when available, else `Date.now()`.
36
+ */
37
+ now?: () => number;
38
+ }
39
+
40
+ /** The scorecard a benchmark run produces. */
41
+ export interface BenchmarkReport {
42
+ /** Number of evaluated sequences. */
43
+ sequences: number;
44
+ /** Number of predicted positions (next-token targets) scored. */
45
+ tokens: number;
46
+ /** Mean next-token cross-entropy, in nats. Lower is better. */
47
+ crossEntropy: number;
48
+ /** Perplexity = exp(crossEntropy). Lower is better; 1.0 is perfect. */
49
+ perplexity: number;
50
+ /** Bits per token = crossEntropy / ln(2). Lower is better. */
51
+ bitsPerToken: number;
52
+ /** Fraction of positions where the argmax prediction was correct (0..1). */
53
+ top1Accuracy: number;
54
+ /** Fraction of positions where the true token was in the top-k (0..1). */
55
+ topKAccuracy: number;
56
+ /** The k used for {@link topKAccuracy}. */
57
+ topK: number;
58
+ /** Forward throughput in tokens/sec, when {@link BenchmarkOptions.measureLatency}. */
59
+ tokensPerSecond?: number;
60
+ /** Wall-clock spent in the forward passes (ms), when measured. */
61
+ elapsedMs?: number;
62
+ }
63
+
64
+ /** The result of A/B-ing two models on the same eval set. */
65
+ export interface ComparisonReport {
66
+ candidate: BenchmarkReport;
67
+ baseline: BenchmarkReport;
68
+ /** candidate.perplexity − baseline.perplexity (negative = candidate better). */
69
+ perplexityDelta: number;
70
+ /** candidate.perplexity / baseline.perplexity (<1 = candidate better). */
71
+ perplexityRatio: number;
72
+ /** candidate.top1Accuracy − baseline.top1Accuracy (positive = candidate better). */
73
+ top1Delta: number;
74
+ /** Which model won on perplexity (the primary metric). */
75
+ winner: "candidate" | "baseline" | "tie";
76
+ /** Human-readable one-liner. */
77
+ summary: string;
78
+ }
79
+
80
+ /** Options for {@link trainAndBenchmark}: train a fresh EvermindLM, then score it. */
81
+ export interface TrainAndBenchmarkOptions {
82
+ /** BPE merges to learn for the tokenizer. Default 100. */
83
+ numMerges?: number;
84
+ /** Model channel dimension. Default 32. */
85
+ dModel?: number;
86
+ /** Number of (conv + MoE) blocks. Default 2. */
87
+ numLayers?: number;
88
+ /** MoE expert FFN hidden width. Default 48. */
89
+ hiddenDim?: number;
90
+ /** Training epochs. Default 30. */
91
+ epochs?: number;
92
+ /** AdamW learning rate. Default 0.03. */
93
+ lr?: number;
94
+ /** Deterministic seed (model init + held-out split). Default 7. */
95
+ seed?: number;
96
+ /**
97
+ * Fraction of sequences reserved for evaluation (never trained on). Default
98
+ * 0.25. A real benchmark must score held-out data, so this is enforced > 0.
99
+ */
100
+ heldOutRatio?: number;
101
+ /** k for top-k accuracy. Default 5. */
102
+ topK?: number;
103
+ /** Prompt used to capture a qualitative generation sample. Default "The". */
104
+ prompt?: string;
105
+ }
106
+
107
+ /** {@link trainAndBenchmark} result: the held-out scorecard plus training context. */
108
+ export interface TrainAndBenchmarkResult extends BenchmarkReport {
109
+ /** Sequences used for training. */
110
+ trainSequences: number;
111
+ /** Held-out sequences used for evaluation. */
112
+ evalSequences: number;
113
+ /** Final-epoch mean training loss (nats). */
114
+ finalTrainLoss: number;
115
+ /** First-epoch mean training loss (nats) — pairs with final to show the drop. */
116
+ initialTrainLoss: number;
117
+ /** Learned tokenizer vocabulary size. */
118
+ vocabSize: number;
119
+ /** A short greedy generation from {@link TrainAndBenchmarkOptions.prompt}. */
120
+ sample: string;
121
+ }
@@ -0,0 +1,168 @@
1
+ /**
2
+ * export/gguf.ts — export weights to a GGUF v3 container (the llama.cpp format).
3
+ *
4
+ * Emits a spec-valid GGUF file: magic + version, KV metadata (architecture, dims,
5
+ * expert counts…), aligned tensor-info table, then aligned tensor data. ggml
6
+ * stores dimensions innermost-first, so row-major shapes are reversed here.
7
+ *
8
+ * NOTE: GGUF is a *container*. llama.cpp can only RUN a file whose
9
+ * `general.architecture` it has a graph for; "evermind" is custom, so this file
10
+ * is a valid, inspectable artifact (gguf tooling reads it) but won't execute in
11
+ * stock llama.cpp until the architecture is upstreamed. It exists so the same
12
+ * weights can travel through GGUF-native tooling. For an executable export use
13
+ * {@link ./onnx} or {@link ./safetensors}.
14
+ */
15
+
16
+ import type { EvermindLM } from "../lm/evermind_lm.js";
17
+ import { archOf, namedTensors, type NamedTensor } from "./tensors.js";
18
+ import { floatToFp16 } from "../utils/quantization.js";
19
+
20
+ const GGUF_MAGIC = 0x46554747; // "GGUF" little-endian
21
+ const GGUF_VERSION = 3;
22
+ const ALIGNMENT = 32;
23
+
24
+ // GGUF metadata value types
25
+ const T_UINT32 = 4;
26
+ const T_STRING = 8;
27
+ // ggml tensor types
28
+ const GGML_F32 = 0;
29
+ const GGML_F16 = 1;
30
+
31
+ export interface GgufOptions {
32
+ name?: string;
33
+ /** Store tensors as float16 (ggml type F16). Default false (F32). */
34
+ fp16?: boolean;
35
+ }
36
+
37
+ /** A growable little-endian byte buffer. */
38
+ class ByteBuf {
39
+ private buf = new Uint8Array(1024);
40
+ private len = 0;
41
+ private ensure(n: number): void {
42
+ if (this.len + n <= this.buf.length) return;
43
+ let cap = this.buf.length * 2;
44
+ while (cap < this.len + n) cap *= 2;
45
+ const next = new Uint8Array(cap);
46
+ next.set(this.buf.subarray(0, this.len));
47
+ this.buf = next;
48
+ }
49
+ u32(v: number): void {
50
+ this.ensure(4);
51
+ new DataView(this.buf.buffer).setUint32(this.len, v, true);
52
+ this.len += 4;
53
+ }
54
+ u64(v: number): void {
55
+ this.ensure(8);
56
+ new DataView(this.buf.buffer).setBigUint64(this.len, BigInt(v), true);
57
+ this.len += 8;
58
+ }
59
+ raw(bytes: Uint8Array): void {
60
+ this.ensure(bytes.length);
61
+ this.buf.set(bytes, this.len);
62
+ this.len += bytes.length;
63
+ }
64
+ /** Pad with zeros until the length is a multiple of `align`. */
65
+ alignTo(align: number): void {
66
+ const pad = (align - (this.len % align)) % align;
67
+ if (pad > 0) {
68
+ this.ensure(pad);
69
+ this.len += pad;
70
+ }
71
+ }
72
+ string(s: string): void {
73
+ const bytes = new TextEncoder().encode(s);
74
+ this.u64(bytes.length);
75
+ this.raw(bytes);
76
+ }
77
+ get length(): number {
78
+ return this.len;
79
+ }
80
+ bytes(): Uint8Array {
81
+ return this.buf.subarray(0, this.len);
82
+ }
83
+ }
84
+
85
+ function tensorBytes(data: Float32Array, fp16: boolean): Uint8Array {
86
+ if (!fp16) {
87
+ const out = new Uint8Array(data.length * 4);
88
+ const dv = new DataView(out.buffer);
89
+ for (let i = 0; i < data.length; i++) dv.setFloat32(i * 4, data[i]!, true);
90
+ return out;
91
+ }
92
+ const out = new Uint8Array(data.length * 2);
93
+ const dv = new DataView(out.buffer);
94
+ for (let i = 0; i < data.length; i++) dv.setUint16(i * 2, floatToFp16(data[i]!), true);
95
+ return out;
96
+ }
97
+
98
+ /** Export a trained LM to GGUF bytes. */
99
+ export function exportGguf(lm: EvermindLM, opts: GgufOptions = {}): Uint8Array {
100
+ const fp16 = opts.fp16 ?? false;
101
+ const a = archOf(lm);
102
+ const tensors = namedTensors(lm);
103
+ const ggmlType = fp16 ? GGML_F16 : GGML_F32;
104
+ const bytesPer = fp16 ? 2 : 4;
105
+
106
+ const kvU32: [string, number][] = [
107
+ ["evermind.vocab_size", a.vocabSize],
108
+ ["evermind.embedding_length", a.dModel],
109
+ ["evermind.block_count", a.numLayers],
110
+ ["evermind.conv_kernel", a.convKernel],
111
+ ["evermind.feed_forward_length", a.hiddenDim],
112
+ ["evermind.expert_count", a.numExperts],
113
+ ["evermind.expert_used_count", a.topK],
114
+ ["general.alignment", ALIGNMENT],
115
+ ];
116
+ const kvStr: [string, string][] = [
117
+ ["general.architecture", "evermind"],
118
+ ["general.name", opts.name ?? "Evermind"],
119
+ ];
120
+
121
+ // Header: magic, version, tensor_count, metadata_kv_count, KVs.
122
+ const head = new ByteBuf();
123
+ head.u32(GGUF_MAGIC);
124
+ head.u32(GGUF_VERSION);
125
+ head.u64(tensors.length);
126
+ head.u64(kvU32.length + kvStr.length);
127
+ for (const [k, v] of kvStr) {
128
+ head.string(k);
129
+ head.u32(T_STRING);
130
+ head.string(v);
131
+ }
132
+ for (const [k, v] of kvU32) {
133
+ head.string(k);
134
+ head.u32(T_UINT32);
135
+ head.u32(v);
136
+ }
137
+
138
+ // Tensor-info table. Offsets are relative to the start of the (aligned) tensor
139
+ // data section; each tensor starts at an ALIGNMENT boundary within it.
140
+ const dims = (t: NamedTensor): number[] => [...t.shape].reverse(); // ggml: innermost first
141
+ let dataOffset = 0;
142
+ const offsets: number[] = [];
143
+ for (const t of tensors) {
144
+ offsets.push(dataOffset);
145
+ const nbytes = t.data.length * bytesPer;
146
+ dataOffset += nbytes;
147
+ dataOffset += (ALIGNMENT - (dataOffset % ALIGNMENT)) % ALIGNMENT;
148
+ }
149
+ tensors.forEach((t, i) => {
150
+ head.string(t.name);
151
+ const d = dims(t);
152
+ head.u32(d.length);
153
+ for (const dim of d) head.u64(dim);
154
+ head.u32(ggmlType);
155
+ head.u64(offsets[i]!);
156
+ });
157
+
158
+ // Pad header to the alignment boundary, then write aligned tensor data.
159
+ head.alignTo(ALIGNMENT);
160
+ const dataStart = head.length;
161
+ for (let i = 0; i < tensors.length; i++) {
162
+ // Re-establish alignment relative to the data section start.
163
+ const want = dataStart + offsets[i]!;
164
+ while (head.length < want) head.raw(new Uint8Array(1));
165
+ head.raw(tensorBytes(tensors[i]!.data, fp16));
166
+ }
167
+ return head.bytes();
168
+ }
@@ -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";