@seanhogg/builderforce-memory-engine 2026.6.30 → 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.
- package/README.md +2 -0
- package/dist/bench/harness.d.ts +37 -0
- package/dist/bench/harness.d.ts.map +1 -0
- package/dist/bench/harness.js +171 -0
- package/dist/bench/harness.js.map +1 -0
- package/dist/bench/index.d.ts +11 -0
- package/dist/bench/index.d.ts.map +1 -0
- package/dist/bench/index.js +10 -0
- package/dist/bench/index.js.map +1 -0
- package/dist/bench/metrics.d.ts +36 -0
- package/dist/bench/metrics.d.ts.map +1 -0
- package/dist/bench/metrics.js +83 -0
- package/dist/bench/metrics.js.map +1 -0
- package/dist/bench/types.d.ts +119 -0
- package/dist/bench/types.d.ts.map +1 -0
- package/dist/bench/types.js +16 -0
- package/dist/bench/types.js.map +1 -0
- package/dist/export/gguf.d.ts +23 -0
- package/dist/export/gguf.d.ts.map +1 -0
- package/dist/export/gguf.js +158 -0
- package/dist/export/gguf.js.map +1 -0
- package/dist/export/hf.d.ts +32 -0
- package/dist/export/hf.d.ts.map +1 -0
- package/dist/export/hf.js +138 -0
- package/dist/export/hf.js.map +1 -0
- package/dist/export/index.d.ts +57 -0
- package/dist/export/index.d.ts.map +1 -0
- package/dist/export/index.js +85 -0
- package/dist/export/index.js.map +1 -0
- package/dist/export/onnx.d.ts +32 -0
- package/dist/export/onnx.d.ts.map +1 -0
- package/dist/export/onnx.js +265 -0
- package/dist/export/onnx.js.map +1 -0
- package/dist/export/protobuf.d.ts +34 -0
- package/dist/export/protobuf.d.ts.map +1 -0
- package/dist/export/protobuf.js +77 -0
- package/dist/export/protobuf.js.map +1 -0
- package/dist/export/safetensors.d.ts +22 -0
- package/dist/export/safetensors.d.ts.map +1 -0
- package/dist/export/safetensors.js +66 -0
- package/dist/export/safetensors.js.map +1 -0
- package/dist/export/tensors.d.ts +51 -0
- package/dist/export/tensors.d.ts.map +1 -0
- package/dist/export/tensors.js +76 -0
- package/dist/export/tensors.js.map +1 -0
- package/dist/index.d.ts +9 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -1
- package/dist/index.js.map +1 -1
- package/dist/kernels/selective_scan.d.ts.map +1 -1
- package/dist/kernels/selective_scan.js +7 -3
- package/dist/kernels/selective_scan.js.map +1 -1
- package/dist/lm/evermind_lm.d.ts +17 -1
- package/dist/lm/evermind_lm.d.ts.map +1 -1
- package/dist/lm/evermind_lm.js +49 -5
- package/dist/lm/evermind_lm.js.map +1 -1
- package/dist/model/mamba_model.d.ts.map +1 -1
- package/dist/model/mamba_model.js +9 -1
- package/dist/model/mamba_model.js.map +1 -1
- package/dist/utils/crc32.d.ts +37 -0
- package/dist/utils/crc32.d.ts.map +1 -0
- package/dist/utils/crc32.js +70 -0
- package/dist/utils/crc32.js.map +1 -0
- package/dist/utils/delta.d.ts +37 -0
- package/dist/utils/delta.d.ts.map +1 -0
- package/dist/utils/delta.js +108 -0
- package/dist/utils/delta.js.map +1 -0
- package/dist/utils/gpu_utils.d.ts +21 -0
- package/dist/utils/gpu_utils.d.ts.map +1 -1
- package/dist/utils/gpu_utils.js +64 -5
- package/dist/utils/gpu_utils.js.map +1 -1
- package/package.json +1 -1
- package/src/bench/harness.ts +219 -0
- package/src/bench/index.ts +37 -0
- package/src/bench/metrics.ts +98 -0
- package/src/bench/types.ts +121 -0
- package/src/export/gguf.ts +168 -0
- package/src/export/hf.ts +155 -0
- package/src/export/index.ts +128 -0
- package/src/export/onnx.ts +331 -0
- package/src/export/protobuf.ts +86 -0
- package/src/export/safetensors.ts +77 -0
- package/src/export/tensors.ts +107 -0
- package/src/index.ts +63 -0
- package/src/kernels/selective_scan.ts +7 -3
- package/src/lm/evermind_lm.ts +53 -8
- package/src/model/mamba_model.ts +9 -1
- package/src/utils/crc32.ts +82 -0
- package/src/utils/delta.ts +117 -0
- package/src/utils/gpu_utils.ts +60 -5
|
@@ -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
|
-
|
|
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
|
-
|
|
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 {
|
package/src/lm/evermind_lm.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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. */
|
package/src/model/mamba_model.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* utils/delta.ts — sparse, row-granular weight deltas for online checkpoints.
|
|
3
|
+
*
|
|
4
|
+
* Online adaptation (WSLA) restricts the trainable set to a few
|
|
5
|
+
* selective-projection ROWS, yet each `adapt()` previously rewrote the WHOLE
|
|
6
|
+
* model to disk. That is wasteful in both size and I/O: 99% of the bytes are
|
|
7
|
+
* identical to the base checkpoint. A delta persists only the rows that actually
|
|
8
|
+
* changed, as a diff against a base, so an online update writes kilobytes
|
|
9
|
+
* instead of megabytes.
|
|
10
|
+
*
|
|
11
|
+
* Format (little-endian), CRC-trailed via {@link appendCrcTrailer}:
|
|
12
|
+
* magic 'EVD0' u32 | version u32 | rowSize u32 | nRows u32 |
|
|
13
|
+
* rowIndex[nRows] u32 | float32 row data[nRows * rowSize]
|
|
14
|
+
*
|
|
15
|
+
* The base + delta reconstruct the exact current weights; a row counts as
|
|
16
|
+
* "changed" if any element differs beyond an optional epsilon.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { appendCrcTrailer, verifyCrcTrailer } from './crc32.js';
|
|
20
|
+
|
|
21
|
+
const DELTA_MAGIC = 0x30445645; // 'EVD0'
|
|
22
|
+
const DELTA_VERSION = 1;
|
|
23
|
+
|
|
24
|
+
export interface RowDelta {
|
|
25
|
+
/** Width of one row (elements). `base.length` must be a multiple of it. */
|
|
26
|
+
rowSize: number;
|
|
27
|
+
/** Indices (row-major) of the rows that changed. */
|
|
28
|
+
rows: number[];
|
|
29
|
+
/** Concatenated new values for the changed rows (rows.length * rowSize). */
|
|
30
|
+
data: Float32Array;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Compute the row-sparse delta of `current` vs `base` (same length + layout).
|
|
35
|
+
* A row is included when any element differs by more than `eps` (default 0).
|
|
36
|
+
*/
|
|
37
|
+
export function computeRowDelta(
|
|
38
|
+
base: Float32Array,
|
|
39
|
+
current: Float32Array,
|
|
40
|
+
rowSize: number,
|
|
41
|
+
eps = 0,
|
|
42
|
+
): RowDelta {
|
|
43
|
+
if (base.length !== current.length) {
|
|
44
|
+
throw new Error(`computeRowDelta: length mismatch (base ${base.length}, current ${current.length})`);
|
|
45
|
+
}
|
|
46
|
+
if (rowSize <= 0 || base.length % rowSize !== 0) {
|
|
47
|
+
throw new Error(`computeRowDelta: base length ${base.length} is not a multiple of rowSize ${rowSize}`);
|
|
48
|
+
}
|
|
49
|
+
const nRowsTotal = base.length / rowSize;
|
|
50
|
+
const rows: number[] = [];
|
|
51
|
+
for (let r = 0; r < nRowsTotal; r++) {
|
|
52
|
+
const off = r * rowSize;
|
|
53
|
+
let changed = false;
|
|
54
|
+
for (let c = 0; c < rowSize; c++) {
|
|
55
|
+
if (Math.abs(current[off + c]! - base[off + c]!) > eps) { changed = true; break; }
|
|
56
|
+
}
|
|
57
|
+
if (changed) rows.push(r);
|
|
58
|
+
}
|
|
59
|
+
const data = new Float32Array(rows.length * rowSize);
|
|
60
|
+
rows.forEach((r, i) => data.set(current.subarray(r * rowSize, r * rowSize + rowSize), i * rowSize));
|
|
61
|
+
return { rowSize, rows, data };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Apply a delta onto a copy of `base`, returning the reconstructed weights. */
|
|
65
|
+
export function applyRowDelta(base: Float32Array, delta: RowDelta): Float32Array {
|
|
66
|
+
if (base.length % delta.rowSize !== 0) {
|
|
67
|
+
throw new Error(`applyRowDelta: base length ${base.length} is not a multiple of rowSize ${delta.rowSize}`);
|
|
68
|
+
}
|
|
69
|
+
const out = Float32Array.from(base);
|
|
70
|
+
delta.rows.forEach((r, i) => {
|
|
71
|
+
const dst = r * delta.rowSize;
|
|
72
|
+
if (dst + delta.rowSize > out.length) {
|
|
73
|
+
throw new Error(`applyRowDelta: row ${r} out of range for base of ${out.length} elements`);
|
|
74
|
+
}
|
|
75
|
+
out.set(delta.data.subarray(i * delta.rowSize, i * delta.rowSize + delta.rowSize), dst);
|
|
76
|
+
});
|
|
77
|
+
return out;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Serialize a delta to a CRC-trailed binary. */
|
|
81
|
+
export function serializeRowDelta(delta: RowDelta): ArrayBuffer {
|
|
82
|
+
const nRows = delta.rows.length;
|
|
83
|
+
const headerEls = 4; // magic, version, rowSize, nRows
|
|
84
|
+
const headerBytes = headerEls * 4;
|
|
85
|
+
const rowIdxBytes = nRows * 4;
|
|
86
|
+
const dataBytes = delta.data.length * 4;
|
|
87
|
+
const buf = new ArrayBuffer(headerBytes + rowIdxBytes + dataBytes);
|
|
88
|
+
const view = new DataView(buf);
|
|
89
|
+
view.setUint32(0, DELTA_MAGIC, true);
|
|
90
|
+
view.setUint32(4, DELTA_VERSION, true);
|
|
91
|
+
view.setUint32(8, delta.rowSize, true);
|
|
92
|
+
view.setUint32(12, nRows, true);
|
|
93
|
+
let off = headerBytes;
|
|
94
|
+
for (const r of delta.rows) { view.setUint32(off, r, true); off += 4; }
|
|
95
|
+
new Float32Array(buf, off, delta.data.length).set(delta.data);
|
|
96
|
+
return appendCrcTrailer(buf);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Parse a binary produced by {@link serializeRowDelta}; verifies CRC + magic. */
|
|
100
|
+
export function deserializeRowDelta(buffer: ArrayBuffer): RowDelta {
|
|
101
|
+
const crc = verifyCrcTrailer(buffer);
|
|
102
|
+
if (crc.hasTrailer && !crc.ok) {
|
|
103
|
+
throw new Error('deserializeRowDelta: failed CRC integrity check (corrupt or truncated)');
|
|
104
|
+
}
|
|
105
|
+
const body = crc.body;
|
|
106
|
+
const view = new DataView(body);
|
|
107
|
+
if (view.getUint32(0, true) !== DELTA_MAGIC) {
|
|
108
|
+
throw new Error('deserializeRowDelta: bad magic (not an EVD0 delta)');
|
|
109
|
+
}
|
|
110
|
+
const rowSize = view.getUint32(8, true);
|
|
111
|
+
const nRows = view.getUint32(12, true);
|
|
112
|
+
let off = 16;
|
|
113
|
+
const rows: number[] = [];
|
|
114
|
+
for (let i = 0; i < nRows; i++) { rows.push(view.getUint32(off, true)); off += 4; }
|
|
115
|
+
const data = new Float32Array(body.slice(off, off + nRows * rowSize * 4));
|
|
116
|
+
return { rowSize, rows, data };
|
|
117
|
+
}
|
package/src/utils/gpu_utils.ts
CHANGED
|
@@ -10,6 +10,60 @@ const COPY_SRC: number = _gpu.GPUBufferUsage?.COPY_SRC ?? 0x04;
|
|
|
10
10
|
const COPY_DST: number = _gpu.GPUBufferUsage?.COPY_DST ?? 0x08;
|
|
11
11
|
const MAP_READ: number = _gpu.GPUBufferUsage?.MAP_READ ?? 0x01;
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* Shape-keyed GPUBuffer pool (EVM-8). Hot paths (e.g. `readBuffer` staging)
|
|
15
|
+
* allocated and destroyed a GPUBuffer on EVERY call — costly under load. The
|
|
16
|
+
* pool reuses buffers keyed by (byteSize, usage): `acquire` hands back a free
|
|
17
|
+
* buffer of that shape or creates one; `release` returns it for reuse. Buffers
|
|
18
|
+
* of one shape are interchangeable, so reuse is always safe.
|
|
19
|
+
*/
|
|
20
|
+
export class BufferPool {
|
|
21
|
+
private readonly _free = new Map<string, GPUBuffer[]>();
|
|
22
|
+
constructor(private readonly device: GPUDevice) {}
|
|
23
|
+
|
|
24
|
+
private _key(byteSize: number, usage: number): string {
|
|
25
|
+
return `${byteSize}:${usage}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** A free buffer of this shape, or a freshly created one. */
|
|
29
|
+
acquire(byteSize: number, usage: number): GPUBuffer {
|
|
30
|
+
const list = this._free.get(this._key(byteSize, usage));
|
|
31
|
+
const reused = list?.pop();
|
|
32
|
+
return reused ?? this.device.createBuffer({ size: byteSize, usage });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Return a buffer to the pool for later reuse (do not destroy it yourself). */
|
|
36
|
+
release(buffer: GPUBuffer, byteSize: number, usage: number): void {
|
|
37
|
+
const key = this._key(byteSize, usage);
|
|
38
|
+
let list = this._free.get(key);
|
|
39
|
+
if (!list) { list = []; this._free.set(key, list); }
|
|
40
|
+
list.push(buffer);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Number of pooled (free) buffers across all shapes. */
|
|
44
|
+
get freeCount(): number {
|
|
45
|
+
let n = 0;
|
|
46
|
+
for (const list of this._free.values()) n += list.length;
|
|
47
|
+
return n;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Destroy every pooled buffer and empty the pool. */
|
|
51
|
+
clear(): void {
|
|
52
|
+
for (const list of this._free.values()) {
|
|
53
|
+
for (const b of list) b.destroy();
|
|
54
|
+
}
|
|
55
|
+
this._free.clear();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** One staging-buffer pool per device, used by {@link readBuffer}. */
|
|
60
|
+
const _stagingPools = new WeakMap<GPUDevice, BufferPool>();
|
|
61
|
+
function stagingPool(device: GPUDevice): BufferPool {
|
|
62
|
+
let pool = _stagingPools.get(device);
|
|
63
|
+
if (!pool) { pool = new BufferPool(device); _stagingPools.set(device, pool); }
|
|
64
|
+
return pool;
|
|
65
|
+
}
|
|
66
|
+
|
|
13
67
|
export interface InitWebGPUOptions {
|
|
14
68
|
powerPreference?: 'high-performance' | 'low-power';
|
|
15
69
|
}
|
|
@@ -93,10 +147,11 @@ export function createUniformBuffer(device: GPUDevice, data: ArrayBuffer | Array
|
|
|
93
147
|
|
|
94
148
|
export async function readBuffer(device: GPUDevice, srcBuffer: GPUBuffer, byteSize: number): Promise<Float32Array> {
|
|
95
149
|
const MAP_READ_FLAG: number = _gpu.GPUMapMode?.READ ?? 0x01;
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
150
|
+
// EVM-8: reuse a pooled staging buffer instead of allocating + destroying one
|
|
151
|
+
// per call. After unmap a staging buffer is fully reusable for the next read.
|
|
152
|
+
const stagingUsage = MAP_READ | COPY_DST;
|
|
153
|
+
const pool = stagingPool(device);
|
|
154
|
+
const stagingBuffer = pool.acquire(byteSize, stagingUsage);
|
|
100
155
|
|
|
101
156
|
const encoder = device.createCommandEncoder();
|
|
102
157
|
encoder.copyBufferToBuffer(srcBuffer, 0, stagingBuffer, 0, byteSize);
|
|
@@ -105,7 +160,7 @@ export async function readBuffer(device: GPUDevice, srcBuffer: GPUBuffer, byteSi
|
|
|
105
160
|
await stagingBuffer.mapAsync(MAP_READ_FLAG);
|
|
106
161
|
const result = new Float32Array(stagingBuffer.getMappedRange().slice(0));
|
|
107
162
|
stagingBuffer.unmap();
|
|
108
|
-
|
|
163
|
+
pool.release(stagingBuffer, byteSize, stagingUsage);
|
|
109
164
|
return result;
|
|
110
165
|
}
|
|
111
166
|
|