@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,219 @@
1
+ /**
2
+ * bench/harness.ts — run a benchmark and A/B two models.
3
+ *
4
+ * The harness drives a model over held-out token sequences, scores next-token
5
+ * prediction with {@link metrics}, and produces a {@link BenchmarkReport}. It
6
+ * also offers a one-call {@link trainAndBenchmark} that builds a fresh
7
+ * `EvermindLM` from a corpus, holds out a slice, trains on the rest, and scores
8
+ * — the path the Studio uses to benchmark a model in the browser.
9
+ */
10
+
11
+ import { EvermindLM, EvermindLMTrainer } from "../lm/evermind_lm.js";
12
+ import { BPETokenizer } from "../tokenizer/bpe.js";
13
+ import { SeededRng } from "../utils/rng.js";
14
+ import { newAccumulator, scoreInto, perplexity, bitsPerToken } from "./metrics.js";
15
+ import type {
16
+ AsyncLogitsModel,
17
+ BenchmarkOptions,
18
+ BenchmarkReport,
19
+ ComparisonReport,
20
+ LogitsModel,
21
+ TrainAndBenchmarkOptions,
22
+ TrainAndBenchmarkResult,
23
+ } from "./types.js";
24
+
25
+ /** A monotonic clock, preferring `performance.now()` when present. */
26
+ function defaultNow(): number {
27
+ const g = globalThis as { performance?: { now(): number } };
28
+ return typeof g.performance?.now === "function" ? g.performance.now() : Date.now();
29
+ }
30
+
31
+ /** Next-token targets for a sequence: tokens[1..T-1] predicted from positions 0..T-2. */
32
+ function nextTokenTargets(seq: number[]): number[] {
33
+ return seq.slice(1);
34
+ }
35
+
36
+ function finalize(
37
+ acc: { tokens: number; ceSum: number; top1Hits: number; topKHits: number },
38
+ sequences: number,
39
+ topK: number,
40
+ elapsedMs: number | undefined,
41
+ ): BenchmarkReport {
42
+ const meanCe = acc.tokens > 0 ? acc.ceSum / acc.tokens : 0;
43
+ const report: BenchmarkReport = {
44
+ sequences,
45
+ tokens: acc.tokens,
46
+ crossEntropy: meanCe,
47
+ perplexity: perplexity(meanCe),
48
+ bitsPerToken: bitsPerToken(meanCe),
49
+ top1Accuracy: acc.tokens > 0 ? acc.top1Hits / acc.tokens : 0,
50
+ topKAccuracy: acc.tokens > 0 ? acc.topKHits / acc.tokens : 0,
51
+ topK,
52
+ };
53
+ if (elapsedMs !== undefined) {
54
+ report.elapsedMs = elapsedMs;
55
+ report.tokensPerSecond = elapsedMs > 0 ? (acc.tokens / elapsedMs) * 1000 : 0;
56
+ }
57
+ return report;
58
+ }
59
+
60
+ /**
61
+ * Benchmark a model (synchronous forward) over held-out token sequences.
62
+ * Sequences shorter than 2 tokens have no prediction target and are skipped.
63
+ */
64
+ export function benchmarkModel(
65
+ model: LogitsModel,
66
+ sequences: number[][],
67
+ opts: BenchmarkOptions = {},
68
+ ): BenchmarkReport {
69
+ const topK = opts.topK ?? 5;
70
+ const measure = opts.measureLatency ?? true;
71
+ const now = opts.now ?? defaultNow;
72
+ const acc = newAccumulator();
73
+ let scored = 0;
74
+ let elapsed = 0;
75
+ for (const seq of sequences) {
76
+ if (seq.length < 2) continue;
77
+ const t0 = measure ? now() : 0;
78
+ const { logits } = model.forward(seq);
79
+ if (measure) elapsed += now() - t0;
80
+ scoreInto(acc, logits, nextTokenTargets(seq), topK);
81
+ scored++;
82
+ }
83
+ return finalize(acc, scored, topK, measure ? elapsed : undefined);
84
+ }
85
+
86
+ /** Benchmark a model whose forward pass is asynchronous (e.g. a WebGPU backend). */
87
+ export async function benchmarkModelAsync(
88
+ model: AsyncLogitsModel,
89
+ sequences: number[][],
90
+ opts: BenchmarkOptions = {},
91
+ ): Promise<BenchmarkReport> {
92
+ const topK = opts.topK ?? 5;
93
+ const measure = opts.measureLatency ?? true;
94
+ const now = opts.now ?? defaultNow;
95
+ const acc = newAccumulator();
96
+ let scored = 0;
97
+ let elapsed = 0;
98
+ for (const seq of sequences) {
99
+ if (seq.length < 2) continue;
100
+ const t0 = measure ? now() : 0;
101
+ const { logits } = await model.forward(seq);
102
+ if (measure) elapsed += now() - t0;
103
+ scoreInto(acc, logits, nextTokenTargets(seq), topK);
104
+ scored++;
105
+ }
106
+ return finalize(acc, scored, topK, measure ? elapsed : undefined);
107
+ }
108
+
109
+ /** A/B two models on the same eval set. Lower perplexity wins (the primary metric). */
110
+ export function compareModels(
111
+ candidate: LogitsModel,
112
+ baseline: LogitsModel,
113
+ sequences: number[][],
114
+ opts: BenchmarkOptions = {},
115
+ ): ComparisonReport {
116
+ return compareReports(benchmarkModel(candidate, sequences, opts), benchmarkModel(baseline, sequences, opts));
117
+ }
118
+
119
+ /** Build a {@link ComparisonReport} from two already-computed scorecards. */
120
+ export function compareReports(candidate: BenchmarkReport, baseline: BenchmarkReport): ComparisonReport {
121
+ const perplexityDelta = candidate.perplexity - baseline.perplexity;
122
+ const perplexityRatio = baseline.perplexity > 0 ? candidate.perplexity / baseline.perplexity : Infinity;
123
+ const top1Delta = candidate.top1Accuracy - baseline.top1Accuracy;
124
+ // A small relative band counts as a tie so noise doesn't flip the verdict.
125
+ const tieBand = 0.005;
126
+ let winner: ComparisonReport["winner"];
127
+ if (Math.abs(perplexityRatio - 1) <= tieBand) winner = "tie";
128
+ else winner = perplexityDelta < 0 ? "candidate" : "baseline";
129
+ const pct = ((1 - perplexityRatio) * 100).toFixed(1);
130
+ const summary =
131
+ winner === "tie"
132
+ ? `Tie: perplexity ${candidate.perplexity.toFixed(2)} vs ${baseline.perplexity.toFixed(2)}`
133
+ : winner === "candidate"
134
+ ? `Candidate wins: ${pct}% lower perplexity (${candidate.perplexity.toFixed(2)} vs ${baseline.perplexity.toFixed(2)})`
135
+ : `Baseline wins: candidate ${(-Number(pct)).toFixed(1)}% higher perplexity (${candidate.perplexity.toFixed(2)} vs ${baseline.perplexity.toFixed(2)})`;
136
+ return { candidate, baseline, perplexityDelta, perplexityRatio, top1Delta, winner, summary };
137
+ }
138
+
139
+ /** Split a corpus into next-token training sequences (one per sentence). */
140
+ export function corpusToSequences(corpus: string, codec: { encode(text: string): number[] }): number[][] {
141
+ return corpus
142
+ .split(/(?<=\.)\s+/)
143
+ .map((s) => codec.encode(s.trim()))
144
+ .filter((ids) => ids.length >= 2);
145
+ }
146
+
147
+ /** Benchmark an existing model against a raw-text corpus (tokenized per sentence). */
148
+ export function benchmarkText(
149
+ model: LogitsModel,
150
+ codec: { encode(text: string): number[] },
151
+ corpus: string,
152
+ opts: BenchmarkOptions = {},
153
+ ): BenchmarkReport {
154
+ return benchmarkModel(model, corpusToSequences(corpus, codec), opts);
155
+ }
156
+
157
+ /** Deterministic Fisher–Yates shuffle (in place) driven by a seeded RNG. */
158
+ function shuffle<T>(arr: T[], rng: SeededRng): T[] {
159
+ for (let i = arr.length - 1; i > 0; i--) {
160
+ const j = Math.floor(rng.next() * (i + 1));
161
+ const tmp = arr[i]!;
162
+ arr[i] = arr[j]!;
163
+ arr[j] = tmp;
164
+ }
165
+ return arr;
166
+ }
167
+
168
+ /**
169
+ * Train a fresh `EvermindLM` on a corpus and benchmark it on a held-out slice —
170
+ * the canonical "build + score a model from text" path. A real benchmark must
171
+ * score data the model never trained on, so `heldOutRatio` is clamped to leave
172
+ * at least one eval sequence and one train sequence.
173
+ */
174
+ export function trainAndBenchmark(
175
+ corpus: string,
176
+ opts: TrainAndBenchmarkOptions = {},
177
+ ): TrainAndBenchmarkResult {
178
+ const seed = opts.seed ?? 7;
179
+ const tok = new BPETokenizer();
180
+ tok.train(corpus, { numMerges: opts.numMerges ?? 100 });
181
+
182
+ const all = corpusToSequences(corpus, tok);
183
+ if (all.length < 2) {
184
+ throw new Error(
185
+ `corpus produced ${all.length} trainable sequence(s); need at least 2 to hold out an eval split (add more sentences)`,
186
+ );
187
+ }
188
+
189
+ // Deterministic split: shuffle by seed, reserve heldOutRatio for eval.
190
+ const order = shuffle([...all], new SeededRng((seed >>> 0) || 1));
191
+ const ratio = Math.min(0.9, Math.max(0.05, opts.heldOutRatio ?? 0.25));
192
+ let evalCount = Math.round(order.length * ratio);
193
+ evalCount = Math.max(1, Math.min(order.length - 1, evalCount));
194
+ const evalSeqs = order.slice(0, evalCount);
195
+ const trainSeqs = order.slice(evalCount);
196
+
197
+ const model = new EvermindLM({
198
+ vocabSize: tok.vocabSize,
199
+ dModel: opts.dModel ?? 32,
200
+ numLayers: opts.numLayers ?? 2,
201
+ hiddenDim: opts.hiddenDim ?? 48,
202
+ seed,
203
+ });
204
+ const epochs = opts.epochs ?? 30;
205
+ const history = new EvermindLMTrainer(model, { lr: opts.lr ?? 0.03, epochs }).fit(trainSeqs);
206
+
207
+ const report = benchmarkModel(model, evalSeqs, { topK: opts.topK ?? 5, measureLatency: true });
208
+ const sample = model.generateText(opts.prompt ?? "The", tok, { maxNewTokens: 8, temperature: 0 });
209
+
210
+ return {
211
+ ...report,
212
+ trainSequences: trainSeqs.length,
213
+ evalSequences: evalSeqs.length,
214
+ initialTrainLoss: history[0] ?? 0,
215
+ finalTrainLoss: history.at(-1) ?? 0,
216
+ vocabSize: tok.vocabSize,
217
+ sample,
218
+ };
219
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * bench — the Evermind benchmarking harness.
3
+ *
4
+ * Measures a trained model on held-out data (perplexity, bits-per-token,
5
+ * next-token accuracy, throughput), A/Bs two models, and offers a one-call
6
+ * train-and-score path the Studio drives in the browser.
7
+ */
8
+
9
+ export {
10
+ benchmarkModel,
11
+ benchmarkModelAsync,
12
+ benchmarkText,
13
+ compareModels,
14
+ compareReports,
15
+ corpusToSequences,
16
+ trainAndBenchmark,
17
+ } from "./harness.js";
18
+
19
+ export {
20
+ argmax,
21
+ topKIndices,
22
+ perplexity,
23
+ bitsPerToken,
24
+ newAccumulator,
25
+ scoreInto,
26
+ LN2,
27
+ } from "./metrics.js";
28
+
29
+ export type {
30
+ LogitsModel,
31
+ AsyncLogitsModel,
32
+ BenchmarkOptions,
33
+ BenchmarkReport,
34
+ ComparisonReport,
35
+ TrainAndBenchmarkOptions,
36
+ TrainAndBenchmarkResult,
37
+ } from "./types.js";
@@ -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
+ }