@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,86 @@
1
+ /**
2
+ * export/protobuf.ts — a minimal, dependency-free protobuf (proto3) wire writer.
3
+ *
4
+ * Just enough of the encoding to emit an ONNX `ModelProto` (see {@link ./onnx}):
5
+ * varint, fixed32 (for float tensors), and length-delimited (string / bytes /
6
+ * nested message) fields. Keeping it tiny and self-contained preserves the
7
+ * engine's zero-runtime-dependency guarantee — no `protobufjs`, no `onnx` proto.
8
+ *
9
+ * Only non-negative integers are emitted (all ONNX ids here are ≥ 0), so the
10
+ * varint path uses plain division and stays correct up to 2^53.
11
+ */
12
+
13
+ const WIRE_VARINT = 0;
14
+ const WIRE_FIXED32 = 5;
15
+ const WIRE_LEN = 2;
16
+
17
+ /** Accumulates protobuf-encoded bytes for one message. */
18
+ export class ProtoWriter {
19
+ private readonly parts: number[] = [];
20
+
21
+ private byte(b: number): void {
22
+ this.parts.push(b & 0xff);
23
+ }
24
+
25
+ private rawVarint(value: number): void {
26
+ if (value < 0 || !Number.isFinite(value)) throw new Error(`ProtoWriter: bad varint ${value}`);
27
+ let v = value;
28
+ while (v >= 0x80) {
29
+ this.byte((v % 0x80) | 0x80);
30
+ v = Math.floor(v / 0x80);
31
+ }
32
+ this.byte(v);
33
+ }
34
+
35
+ private tag(field: number, wire: number): void {
36
+ this.rawVarint(field * 8 + wire);
37
+ }
38
+
39
+ private rawBytes(bytes: Uint8Array): void {
40
+ for (let i = 0; i < bytes.length; i++) this.byte(bytes[i]!);
41
+ }
42
+
43
+ /** A varint field (int32 / int64 / bool / enum). */
44
+ varint(field: number, value: number): void {
45
+ this.tag(field, WIRE_VARINT);
46
+ this.rawVarint(value);
47
+ }
48
+
49
+ /** A 32-bit IEEE-754 float field (wire type fixed32, little-endian). */
50
+ float(field: number, value: number): void {
51
+ this.tag(field, WIRE_FIXED32);
52
+ const buf = new ArrayBuffer(4);
53
+ new DataView(buf).setFloat32(0, value, true);
54
+ this.rawBytes(new Uint8Array(buf));
55
+ }
56
+
57
+ /** A length-delimited raw-bytes field. */
58
+ bytes(field: number, data: Uint8Array): void {
59
+ this.tag(field, WIRE_LEN);
60
+ this.rawVarint(data.length);
61
+ this.rawBytes(data);
62
+ }
63
+
64
+ /** A length-delimited UTF-8 string field. */
65
+ string(field: number, value: string): void {
66
+ this.bytes(field, new TextEncoder().encode(value));
67
+ }
68
+
69
+ /** A nested-message field (the sub-message encoded as length-delimited bytes). */
70
+ message(field: number, sub: ProtoWriter): void {
71
+ this.bytes(field, sub.finish());
72
+ }
73
+
74
+ /** The encoded bytes of this message. */
75
+ finish(): Uint8Array {
76
+ return Uint8Array.from(this.parts);
77
+ }
78
+ }
79
+
80
+ /** Pack a Float32Array into little-endian raw bytes (for TensorProto.raw_data). */
81
+ export function float32ToBytes(data: Float32Array): Uint8Array {
82
+ const out = new Uint8Array(data.length * 4);
83
+ const dv = new DataView(out.buffer);
84
+ for (let i = 0; i < data.length; i++) dv.setFloat32(i * 4, data[i]!, true);
85
+ return out;
86
+ }
@@ -0,0 +1,77 @@
1
+ /**
2
+ * export/safetensors.ts — export weights to the HF-native `.safetensors` format.
3
+ *
4
+ * Layout (the safetensors spec): an 8-byte little-endian u64 header length, then a
5
+ * JSON header mapping each tensor name → {dtype, shape, data_offsets:[start,end]}
6
+ * (offsets relative to the start of the byte buffer that follows the header), then
7
+ * the concatenated raw tensor bytes. Round-trips losslessly in F32; F16 halves the
8
+ * size. Loadable by `safetensors`, `transformers`, and transformers.js.
9
+ */
10
+
11
+ import type { EvermindLM } from "../lm/evermind_lm.js";
12
+ import { namedTensors, type NamedTensor } from "./tensors.js";
13
+ import { floatToFp16 } from "../utils/quantization.js";
14
+
15
+ export interface SafetensorsOptions {
16
+ /** Store tensors as float16 (half the size). Default false (float32). */
17
+ fp16?: boolean;
18
+ /** Extra `__metadata__` string entries (e.g. format provenance). */
19
+ metadata?: Record<string, string>;
20
+ }
21
+
22
+ function tensorBytes(data: Float32Array, fp16: boolean): Uint8Array {
23
+ if (!fp16) {
24
+ const out = new Uint8Array(data.length * 4);
25
+ const dv = new DataView(out.buffer);
26
+ for (let i = 0; i < data.length; i++) dv.setFloat32(i * 4, data[i]!, true);
27
+ return out;
28
+ }
29
+ const out = new Uint8Array(data.length * 2);
30
+ const dv = new DataView(out.buffer);
31
+ for (let i = 0; i < data.length; i++) dv.setUint16(i * 2, floatToFp16(data[i]!), true);
32
+ return out;
33
+ }
34
+
35
+ /** Serialise the given named tensors to a `.safetensors` byte buffer. */
36
+ export function tensorsToSafetensors(tensors: NamedTensor[], opts: SafetensorsOptions = {}): Uint8Array {
37
+ const fp16 = opts.fp16 ?? false;
38
+ const dtype = fp16 ? "F16" : "F32";
39
+ const bytesPer = fp16 ? 2 : 4;
40
+
41
+ const header: Record<string, unknown> = {};
42
+ if (opts.metadata) header.__metadata__ = opts.metadata;
43
+
44
+ const chunks: Uint8Array[] = [];
45
+ let offset = 0;
46
+ for (const t of tensors) {
47
+ const nbytes = t.data.length * bytesPer;
48
+ header[t.name] = { dtype, shape: t.shape, data_offsets: [offset, offset + nbytes] };
49
+ chunks.push(tensorBytes(t.data, fp16));
50
+ offset += nbytes;
51
+ }
52
+
53
+ let headerJson = JSON.stringify(header);
54
+ // safetensors requires the header be padded to an 8-byte boundary (with spaces).
55
+ const headerBytesRaw = new TextEncoder().encode(headerJson);
56
+ const pad = (8 - (headerBytesRaw.length % 8)) % 8;
57
+ if (pad > 0) headerJson += " ".repeat(pad);
58
+ const headerBytes = new TextEncoder().encode(headerJson);
59
+
60
+ const out = new Uint8Array(8 + headerBytes.length + offset);
61
+ new DataView(out.buffer).setBigUint64(0, BigInt(headerBytes.length), true);
62
+ out.set(headerBytes, 8);
63
+ let p = 8 + headerBytes.length;
64
+ for (const c of chunks) {
65
+ out.set(c, p);
66
+ p += c.length;
67
+ }
68
+ return out;
69
+ }
70
+
71
+ /** Export a trained LM's weights to `.safetensors`. */
72
+ export function exportSafetensors(lm: EvermindLM, opts: SafetensorsOptions = {}): Uint8Array {
73
+ return tensorsToSafetensors(namedTensors(lm), {
74
+ ...opts,
75
+ metadata: { format: "pt", producer: "builderforce-memory-engine", ...(opts.metadata ?? {}) },
76
+ });
77
+ }
@@ -0,0 +1,107 @@
1
+ /**
2
+ * export/tensors.ts — the canonical named-tensor view of an EvermindLM.
3
+ *
4
+ * Every export format (safetensors / ONNX / GGUF / HF) consumes the SAME named,
5
+ * shaped tensor list — defined ONCE here so the formats never disagree about a
6
+ * weight's name, shape, or order. The runtime is left untouched: we read the
7
+ * model purely through its public {@link EvermindLM.config} and
8
+ * {@link EvermindLM.parameters} (the AdamW-ordered flat buffers), and re-attach
9
+ * the names/shapes that order implies.
10
+ *
11
+ * Parameter order (must mirror EvermindLM.parameters / SharedExpertMoE.parameters):
12
+ * emb,
13
+ * per layer l: conv[l], normConv[l], normMoe[l],
14
+ * moe: router, shared.{w1,b1,w2,b2}, expert{e}.{w1,b1,w2,b2}…
15
+ */
16
+
17
+ import type { EvermindLM } from "../lm/evermind_lm.js";
18
+
19
+ /** A named, shaped weight tensor (row-major, flat f32). */
20
+ export interface NamedTensor {
21
+ name: string;
22
+ /** Row-major dimensions, e.g. [vocab, dModel]. */
23
+ shape: number[];
24
+ data: Float32Array;
25
+ }
26
+
27
+ /** The flat numeric architecture an export needs (a superset of the LM config). */
28
+ export interface EvermindArch {
29
+ vocabSize: number;
30
+ dModel: number;
31
+ numLayers: number;
32
+ convKernel: number;
33
+ hiddenDim: number;
34
+ numExperts: number;
35
+ topK: number;
36
+ }
37
+
38
+ /** Pull the architecture out of a live LM (or any config-shaped object). */
39
+ export function archOf(lm: EvermindLM): EvermindArch {
40
+ const c = lm.config;
41
+ return {
42
+ vocabSize: c.vocabSize,
43
+ dModel: c.dModel,
44
+ numLayers: c.numLayers,
45
+ convKernel: c.convKernel,
46
+ hiddenDim: c.hiddenDim,
47
+ numExperts: c.numExperts,
48
+ topK: c.topK,
49
+ };
50
+ }
51
+
52
+ /** A weight name + its expected shape, in canonical parameter order. */
53
+ export interface TensorSpec {
54
+ name: string;
55
+ shape: number[];
56
+ }
57
+
58
+ /** The name+shape of every parameter, in EvermindLM.parameters() order. */
59
+ export function evermindTensorSpec(a: EvermindArch): TensorSpec[] {
60
+ const { vocabSize: V, dModel: D, numLayers: L, convKernel: K, hiddenDim: H, numExperts: E } = a;
61
+ const specs: TensorSpec[] = [{ name: "token_embedding.weight", shape: [V, D] }];
62
+ const ffn = (prefix: string): TensorSpec[] => [
63
+ { name: `${prefix}.w1`, shape: [H, D] },
64
+ { name: `${prefix}.b1`, shape: [H] },
65
+ { name: `${prefix}.w2`, shape: [D, H] },
66
+ { name: `${prefix}.b2`, shape: [D] },
67
+ ];
68
+ for (let l = 0; l < L; l++) {
69
+ const p = `layers.${l}`;
70
+ specs.push({ name: `${p}.conv.weight`, shape: [D, K] });
71
+ specs.push({ name: `${p}.norm_conv.weight`, shape: [D] });
72
+ specs.push({ name: `${p}.norm_moe.weight`, shape: [D] });
73
+ specs.push({ name: `${p}.moe.router.weight`, shape: [E, D] });
74
+ specs.push(...ffn(`${p}.moe.shared`));
75
+ for (let e = 0; e < E; e++) specs.push(...ffn(`${p}.moe.experts.${e}`));
76
+ }
77
+ return specs;
78
+ }
79
+
80
+ /**
81
+ * The full named/shaped tensor list of a trained LM, zipping the deterministic
82
+ * {@link evermindTensorSpec} against the live {@link EvermindLM.parameters}. The
83
+ * data buffers are the model's own (no copy) — treat them as read-only.
84
+ */
85
+ export function namedTensors(lm: EvermindLM): NamedTensor[] {
86
+ const specs = evermindTensorSpec(archOf(lm));
87
+ const params = lm.parameters();
88
+ if (specs.length !== params.length) {
89
+ throw new Error(
90
+ `export/tensors: parameter-count drift (spec ${specs.length} vs model ${params.length}) — ` +
91
+ `EvermindLM.parameters() order changed; update evermindTensorSpec`,
92
+ );
93
+ }
94
+ return specs.map((s, i) => {
95
+ const numel = s.shape.reduce((n, d) => n * d, 1);
96
+ const data = params[i]!.data;
97
+ if (data.length !== numel) {
98
+ throw new Error(`export/tensors: shape mismatch for ${s.name} (expected ${numel}, got ${data.length})`);
99
+ }
100
+ return { name: s.name, shape: s.shape, data };
101
+ });
102
+ }
103
+
104
+ /** Total trainable scalar parameters (for sizing / model-card display). */
105
+ export function paramCount(lm: EvermindLM): number {
106
+ return lm.parameters().reduce((n, p) => n + p.data.length, 0);
107
+ }
package/src/index.ts CHANGED
@@ -61,6 +61,75 @@ export {
61
61
 
62
62
  export { BPETokenizer } from './tokenizer/bpe.js';
63
63
 
64
+ // ── Model export (the publishing step: ONNX / safetensors / GGUF / HF repo) ─────
65
+
66
+ export {
67
+ exportEvermind,
68
+ EXPORT_FORMATS,
69
+ exportSafetensors,
70
+ tensorsToSafetensors,
71
+ exportOnnx,
72
+ exportGguf,
73
+ configJson,
74
+ generationConfigJson,
75
+ tokenizerJson,
76
+ modelCardMarkdown,
77
+ namedTensors,
78
+ evermindTensorSpec,
79
+ archOf,
80
+ paramCount,
81
+ } from './export/index.js';
82
+ export type {
83
+ ExportFormat,
84
+ ExportFile,
85
+ ExportResult,
86
+ ExportOptions,
87
+ HfMeta,
88
+ NamedTensor,
89
+ EvermindArch,
90
+ TensorSpec,
91
+ } from './export/index.js';
92
+
93
+ // ── Benchmarking (held-out perplexity / accuracy / throughput + A/B) ──────────
94
+
95
+ export {
96
+ benchmarkModel,
97
+ benchmarkModelAsync,
98
+ benchmarkText,
99
+ compareModels,
100
+ compareReports,
101
+ corpusToSequences,
102
+ trainAndBenchmark,
103
+ argmax as benchArgmax,
104
+ topKIndices,
105
+ perplexity,
106
+ bitsPerToken,
107
+ LN2,
108
+ } from './bench/index.js';
109
+ export type {
110
+ LogitsModel,
111
+ AsyncLogitsModel,
112
+ BenchmarkOptions,
113
+ BenchmarkReport,
114
+ ComparisonReport,
115
+ TrainAndBenchmarkOptions,
116
+ TrainAndBenchmarkResult,
117
+ } from './bench/index.js';
118
+
119
+ // ── Checkpoint integrity (CRC-32 + trailer) ───────────────────────────────────
120
+
121
+ export { crc32, appendCrcTrailer, verifyCrcTrailer, CRC_TRAILER_MAGIC } from './utils/crc32.js';
122
+ export type { CrcCheck } from './utils/crc32.js';
123
+ export {
124
+ computeRowDelta,
125
+ applyRowDelta,
126
+ serializeRowDelta,
127
+ deserializeRowDelta,
128
+ diffCheckpoints,
129
+ applyCheckpointDiff,
130
+ } from './utils/delta.js';
131
+ export type { RowDelta } from './utils/delta.js';
132
+
64
133
  // ── Seeded RNG (reproducible weight init) ─────────────────────────────────────
65
134
 
66
135
  export { SeededRng, setInitSeed, randn, gaussianArray } from './utils/rng.js';
@@ -94,6 +163,7 @@ export {
94
163
  readBuffer,
95
164
  uploadBuffer,
96
165
  cdiv,
166
+ BufferPool,
97
167
  } from './utils/gpu_utils.js';
98
168
 
99
169
  // ── Quantization ──────────────────────────────────────────────────────────────
@@ -48,9 +48,11 @@ var<workgroup> wg_bu : array<f32, 256>; // B*u values
48
48
 
49
49
  // ---- Helpers ----
50
50
 
51
- // Softplus: numerically stable log(1 + exp(x))
51
+ // Softplus: numerically stable softplus(x) = log(1 + exp(x))
52
52
  fn softplus(x: f32) -> f32 {
53
- return log(1.0 + exp(x));
53
+ // Numerically stable: max(x,0) + log1p(exp(-|x|)). The naive log(1+exp(x))
54
+ // overflows to +Inf for x ≳ 88 (f32 exp range); this form never does. (EVM-8)
55
+ return max(x, 0.0) + log(1.0 + exp(-abs(x)));
54
56
  }
55
57
 
56
58
  // ZerO-Order Hold discretisation of continuous A, Δ:
@@ -251,7 +253,9 @@ struct ScanParams {
251
253
  @group(0) @binding(12) var<storage, read_write> du : array<f32>;
252
254
 
253
255
  fn softplus(x: f32) -> f32 {
254
- return log(1.0 + exp(x));
256
+ // Numerically stable: max(x,0) + log1p(exp(-|x|)). The naive log(1+exp(x))
257
+ // overflows to +Inf for x ≳ 88 (f32 exp range); this form never does. (EVM-8)
258
+ return max(x, 0.0) + log(1.0 + exp(-abs(x)));
255
259
  }
256
260
 
257
261
  fn softplus_grad(x: f32) -> f32 {
@@ -26,6 +26,8 @@ import { crossEntropyLoss, crossEntropyGrad } from "../training/autograd.js";
26
26
  import { AdamW, type AdamWOptions } from "../optim/adamw.js";
27
27
  import { SeededRng } from "../utils/rng.js";
28
28
  import { quantizeFp16, dequantizeFp16 } from "../utils/quantization.js";
29
+ import { appendCrcTrailer, verifyCrcTrailer } from "../utils/crc32.js";
30
+ import { computeRowDelta, applyRowDelta, serializeRowDelta, deserializeRowDelta } from "../utils/delta.js";
29
31
 
30
32
  export interface EvermindLMConfig {
31
33
  /** Vocabulary size (the only required field; everything else has a default). */
@@ -434,11 +436,22 @@ export class EvermindLM {
434
436
  }
435
437
  if (fp16) new Uint16Array(buf, headerBytes, total).set(quantizeFp16(flat));
436
438
  else new Float32Array(buf, headerBytes, total).set(flat);
437
- return buf;
439
+ // Append a CRC-32 trailer so a corrupt/truncated checkpoint is caught on load
440
+ // (backward-compatible: pre-CRC readers ignore the trailing bytes). (EVM-7)
441
+ return appendCrcTrailer(buf);
438
442
  }
439
443
 
440
- /** Load weights from an "EVL0" binary. Validates magic + dims. */
444
+ /** Load weights from an "EVL0" binary. Validates CRC (when present), magic + dims. */
441
445
  loadWeights(buffer: ArrayBuffer): void {
446
+ this._setFlat(this._readFlat(buffer));
447
+ }
448
+
449
+ /** Parse + validate an EVL0 checkpoint to a flat f32 param vector (no mutation). */
450
+ private _readFlat(buffer: ArrayBuffer): Float32Array {
451
+ const crc = verifyCrcTrailer(buffer);
452
+ if (crc.hasTrailer && !crc.ok) {
453
+ throw new Error("EvermindLM.loadWeights: checkpoint failed CRC integrity check (corrupt or truncated)");
454
+ }
442
455
  const head = new Uint32Array(buffer, 0, 9);
443
456
  if (head[0] !== MAGIC) throw new Error("EvermindLM.loadWeights: bad magic (not an EVL0 checkpoint)");
444
457
  const version = head[1]!;
@@ -453,19 +466,51 @@ export class EvermindLM {
453
466
  ) {
454
467
  throw new Error("EvermindLM.loadWeights: config mismatch with checkpoint");
455
468
  }
469
+ const total = this.parameters().reduce((n, p) => n + p.data.length, 0);
470
+ const headerBytes = 36;
471
+ return version === 2
472
+ ? dequantizeFp16(new Uint16Array(buffer, headerBytes, total))
473
+ : new Float32Array(buffer.slice(headerBytes, headerBytes + total * 4));
474
+ }
475
+
476
+ /** Flat concatenation of all params, canonical order (matches checkpoint layout). */
477
+ private _flat(): Float32Array {
456
478
  const params = this.parameters();
457
479
  const total = params.reduce((n, p) => n + p.data.length, 0);
458
- const headerBytes = 36;
459
- const flat =
460
- version === 2
461
- ? dequantizeFp16(new Uint16Array(buffer, headerBytes, total))
462
- : new Float32Array(buffer.slice(headerBytes, headerBytes + total * 4));
480
+ const flat = new Float32Array(total);
463
481
  let o = 0;
464
- for (const p of params) {
482
+ for (const p of params) { flat.set(p.data, o); o += p.data.length; }
483
+ return flat;
484
+ }
485
+
486
+ /** Distribute a flat param vector back into the model's parameters. */
487
+ private _setFlat(flat: Float32Array): void {
488
+ let o = 0;
489
+ for (const p of this.parameters()) {
465
490
  p.data.set(flat.subarray(o, o + p.data.length));
466
491
  o += p.data.length;
467
492
  }
468
493
  }
494
+
495
+ /**
496
+ * Export a SPARSE DELTA of the current weights against a base EVL0 checkpoint
497
+ * (EVM-6). Online WSLA updates only a few rows, so a delta persists kilobytes
498
+ * instead of rewriting the whole model. Reconstruct with {@link loadDelta}.
499
+ */
500
+ exportDelta(baseCheckpoint: ArrayBuffer, opts: { eps?: number } = {}): ArrayBuffer {
501
+ const base = this._readFlat(baseCheckpoint);
502
+ const current = this._flat();
503
+ // Row-granular when the param vector tiles evenly by the model width (the
504
+ // embedding dominates and is dModel-wide); else fall back to per-element.
505
+ const rowSize = current.length % this.config.dModel === 0 ? this.config.dModel : 1;
506
+ return serializeRowDelta(computeRowDelta(base, current, rowSize, opts.eps ?? 0));
507
+ }
508
+
509
+ /** Reconstruct weights from a base EVL0 checkpoint + a delta (EVM-6). */
510
+ loadDelta(baseCheckpoint: ArrayBuffer, delta: ArrayBuffer): void {
511
+ const base = this._readFlat(baseCheckpoint);
512
+ this._setFlat(applyRowDelta(base, deserializeRowDelta(delta)));
513
+ }
469
514
  }
470
515
 
471
516
  /** Minimal sequence trainer: AdamW over next-token cross-entropy. */
@@ -36,6 +36,7 @@ import { LINEAR_FORWARD_WGSL } from '../kernels/linear_projection.js';
36
36
  import { ACTIVATIONS_WGSL } from '../kernels/activations.js';
37
37
  import { gaussianArray, setInitSeed } from '../utils/rng.js';
38
38
  import { quantizeFp16, dequantizeFp16 } from '../utils/quantization.js';
39
+ import { appendCrcTrailer, verifyCrcTrailer } from '../utils/crc32.js';
39
40
 
40
41
  // ── Public types ──────────────────────────────────────────────────────────────
41
42
 
@@ -485,7 +486,9 @@ export class HybridMambaModel {
485
486
  }
486
487
  }
487
488
 
488
- return out;
489
+ // Append a CRC-32 trailer for integrity-on-load (backward compatible —
490
+ // length-bounded readers ignore the trailing bytes). (EVM-7)
491
+ return appendCrcTrailer(out);
489
492
  }
490
493
 
491
494
  /**
@@ -496,6 +499,11 @@ export class HybridMambaModel {
496
499
  * v3: identical layout to v2 but the data section is fp16 (dequantised on load).
497
500
  */
498
501
  async loadWeights(buffer: ArrayBuffer): Promise<void> {
502
+ // Verify the CRC trailer when present (corrupt/truncated guard, EVM-7).
503
+ const crc = verifyCrcTrailer(buffer);
504
+ if (crc.hasTrailer && !crc.ok) {
505
+ throw new Error('Invalid weight file: failed CRC integrity check (corrupt or truncated).');
506
+ }
499
507
  const view = new DataView(buffer);
500
508
  let off = 0;
501
509
 
@@ -0,0 +1,82 @@
1
+ /**
2
+ * utils/crc32.ts — checkpoint integrity (CRC-32) + a backward-compatible trailer.
3
+ *
4
+ * Checkpoints had no integrity check: a truncated or bit-rotted `.bin` would
5
+ * either throw deep in the parser or, worse, load corrupt weights silently. This
6
+ * adds a standard IEEE CRC-32 and a self-describing trailer that callers append
7
+ * on export and verify on load.
8
+ *
9
+ * The trailer is appended AFTER the existing payload as `[magic u32][crc u32]`
10
+ * (8 bytes, little-endian). Because every checkpoint reader consumes a
11
+ * length-bounded payload (header declares element counts) and ignores trailing
12
+ * bytes, an old reader still loads a trailer-bearing file — the trailer is
13
+ * purely additive. The CRC covers the payload bytes only (everything before the
14
+ * trailer).
15
+ */
16
+
17
+ /** Trailer sentinel: 'EVCR' (Evermind CRc), little-endian uint32. */
18
+ export const CRC_TRAILER_MAGIC = 0x52435645; // bytes 45 56 43 52 = "EVCR"
19
+ const TRAILER_BYTES = 8;
20
+
21
+ /** Precomputed CRC-32 (IEEE 802.3, reflected) lookup table. */
22
+ const CRC_TABLE: Uint32Array = (() => {
23
+ const t = new Uint32Array(256);
24
+ for (let n = 0; n < 256; n++) {
25
+ let c = n;
26
+ for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
27
+ t[n] = c >>> 0;
28
+ }
29
+ return t;
30
+ })();
31
+
32
+ /** IEEE CRC-32 of a byte buffer, returned as an unsigned 32-bit number. */
33
+ export function crc32(bytes: Uint8Array): number {
34
+ let crc = 0xffffffff;
35
+ for (let i = 0; i < bytes.length; i++) {
36
+ crc = CRC_TABLE[(crc ^ bytes[i]!) & 0xff]! ^ (crc >>> 8);
37
+ }
38
+ return (crc ^ 0xffffffff) >>> 0;
39
+ }
40
+
41
+ /** Return a new ArrayBuffer = `payload` followed by the CRC trailer. */
42
+ export function appendCrcTrailer(payload: ArrayBuffer): ArrayBuffer {
43
+ const body = new Uint8Array(payload);
44
+ const out = new ArrayBuffer(body.length + TRAILER_BYTES);
45
+ const u8 = new Uint8Array(out);
46
+ u8.set(body, 0);
47
+ const view = new DataView(out);
48
+ view.setUint32(body.length, CRC_TRAILER_MAGIC, true);
49
+ view.setUint32(body.length + 4, crc32(body), true);
50
+ return out;
51
+ }
52
+
53
+ export interface CrcCheck {
54
+ /** True when a recognised CRC trailer is present. */
55
+ hasTrailer: boolean;
56
+ /** True when no trailer is present, or the trailer's CRC matches the payload. */
57
+ ok: boolean;
58
+ /** The payload with any trailer stripped (safe to parse). */
59
+ body: ArrayBuffer;
60
+ }
61
+
62
+ /**
63
+ * Inspect a buffer for a CRC trailer. When present, verifies the CRC over the
64
+ * preceding payload. When absent (legacy file), reports `ok: true` and returns
65
+ * the buffer unchanged — so verification is opt-in by the writer, never a
66
+ * hard requirement that would reject pre-CRC checkpoints.
67
+ */
68
+ export function verifyCrcTrailer(buffer: ArrayBuffer): CrcCheck {
69
+ if (buffer.byteLength < TRAILER_BYTES) {
70
+ return { hasTrailer: false, ok: true, body: buffer };
71
+ }
72
+ const view = new DataView(buffer);
73
+ const bodyLen = buffer.byteLength - TRAILER_BYTES;
74
+ const magic = view.getUint32(bodyLen, true);
75
+ if (magic !== CRC_TRAILER_MAGIC) {
76
+ return { hasTrailer: false, ok: true, body: buffer };
77
+ }
78
+ const stored = view.getUint32(bodyLen + 4, true);
79
+ const body = buffer.slice(0, bodyLen);
80
+ const ok = crc32(new Uint8Array(body)) === stored;
81
+ return { hasTrailer: true, ok, body };
82
+ }