mlx-bun 0.0.8 → 0.0.10
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 +112 -20
- package/docs/design/batching-perf-path.md +232 -0
- package/docs/design/batching-v2-plan.md +515 -0
- package/docs/design/bucketing-stage.md +196 -0
- package/docs/design/compat-cli-surface-design.md +11 -11
- package/docs/design/docs-reorg-plan.md +1 -1
- package/docs/design/dreaming-nightly-pipeline.md +291 -0
- package/docs/design/dspark-speculative-decoding.md +106 -0
- package/docs/design/generic-model-support.md +381 -0
- package/docs/design/memory-inference-path.md +422 -0
- package/docs/design/memory-synthesis.md +287 -0
- package/docs/design/minicpm5-decode-megakernel.md +484 -0
- package/docs/design/mlx-lm-tool-parity-plan.md +535 -0
- package/docs/design/omlx-adoption-map.md +90 -0
- package/docs/design/orpo-dynamic-lambda.md +41 -0
- package/docs/design/orpo-training.md +35 -5
- package/docs/design/parallel-slots.md +12 -1
- package/docs/design/parity-tier-dag.md +13 -5
- package/docs/design/segmented-backward-training.md +15 -0
- package/docs/design/spec-decode-larger-targets.md +229 -0
- package/docs/design/ssd-kv-cold-tier.md +158 -0
- package/docs/design/tauri-desktop-app.md +2 -2
- package/docs/design/the-dreaming-handoff.md +141 -0
- package/docs/design/the-dreaming-master-plan.md +446 -0
- package/docs/design/write-pipeline-entity.md +468 -0
- package/docs/investigations/curve-bisector-routing.md +1 -1
- package/docs/investigations/decode-roofline-lookagain.md +348 -0
- package/docs/investigations/dspark-handoff.md +58 -0
- package/docs/investigations/kernel-perf-review-2026-07.md +171 -0
- package/docs/planning/PRODUCT_ROADMAP.md +2 -2
- package/docs/planning/curve-sampler-research-plan.md +331 -0
- package/docs/planning/memory-docs-and-dag-plan.md +185 -0
- package/docs/planning/pi-integration-review.md +178 -0
- package/docs/planning/release-notes-v0.0.10.md +57 -0
- package/docs/planning/release-notes-v0.0.9.md +122 -0
- package/docs/planning/web-ui-pass-plan.md +213 -0
- package/docs/planning/website-readme-pass-plan.md +224 -0
- package/docs/reference/cli.md +255 -0
- package/docs/reference/embedding.md +7 -2
- package/docs/reference/library-api.md +22 -3
- package/docs/reference/memory.md +5 -5
- package/docs/reference/models.md +137 -0
- package/docs/reference/orpo-quickstart.md +6 -0
- package/docs/reference/server-api.md +187 -9
- package/docs/reference/server-config.md +43 -9
- package/docs/reference/training.md +31 -12
- package/package.json +1 -1
- package/src/anthropic.ts +22 -0
- package/src/assets/curve-designer.html +1 -1
- package/src/chat-template.ts +25 -7
- package/src/cli.ts +1077 -90
- package/src/config.ts +4 -1
- package/src/download.ts +99 -8
- package/src/embed.ts +77 -0
- package/src/eval/perplexity.ts +164 -0
- package/src/eval/runner.ts +15 -6
- package/src/eval/tasks/bfcl.ts +1 -1
- package/src/generate.ts +149 -18
- package/src/harness-pi.ts +43 -17
- package/src/index.ts +2 -0
- package/src/kv-store.ts +240 -98
- package/src/lora.ts +2 -2
- package/src/memory/article.ts +558 -0
- package/src/memory/chunk-validation.ts +150 -0
- package/src/memory/chunk.ts +305 -12
- package/src/memory/cluster.ts +344 -17
- package/src/memory/coverage.ts +134 -0
- package/src/memory/crosslink.ts +0 -0
- package/src/memory/db.ts +184 -45
- package/src/memory/entity.ts +203 -0
- package/src/memory/gate.ts +187 -0
- package/src/memory/history.ts +152 -0
- package/src/memory/ingest.ts +324 -11
- package/src/memory/model.ts +321 -0
- package/src/memory/normalize.ts +671 -0
- package/src/memory/parse.ts +22 -0
- package/src/memory/pipeline.ts +242 -30
- package/src/memory/prompts.ts +42 -0
- package/src/memory/query.ts +334 -0
- package/src/memory/reconcile.ts +734 -0
- package/src/memory/redirect.ts +108 -0
- package/src/memory/reindex.ts +325 -0
- package/src/memory/resolve.ts +307 -0
- package/src/memory/route.ts +272 -0
- package/src/memory/schedule.ts +6 -4
- package/src/memory/stages.ts +774 -0
- package/src/memory/synthesize.ts +1629 -20
- package/src/memory/tools.ts +446 -96
- package/src/memory/vault.ts +462 -35
- package/src/memory/wikify.ts +1057 -16
- package/src/mlx/array.ts +21 -0
- package/src/mlx/ffi.ts +3 -0
- package/src/mlx/ops.ts +72 -0
- package/src/model/compiled-decode.ts +43 -8
- package/src/model/factory.ts +29 -8
- package/src/model/flash-attention.ts +4 -2
- package/src/model/fused-decode-kernel.ts +14 -7
- package/src/model/fused-mlp-kernel.ts +169 -0
- package/src/model/fused-swiglu-kernel.ts +240 -0
- package/src/model/gemma4-base.ts +96 -7
- package/src/model/gemma4.ts +33 -2
- package/src/model/minicpm5.ts +106 -8
- package/src/model/qwen3-delta.ts +48 -1
- package/src/model/qwen3.ts +276 -0
- package/src/model/qwen3_5.ts +11 -2
- package/src/model/steel-linear-kernel.ts +96 -0
- package/src/model/support.ts +38 -8
- package/src/model/universal/archs.ts +332 -0
- package/src/model/universal/dense.ts +576 -0
- package/src/model/universal/modules.ts +163 -0
- package/src/model/universal/rope.ts +366 -0
- package/src/pi-provider.ts +1 -1
- package/src/pi-terminal.ts +29 -8
- package/src/pi-web.ts +19 -2
- package/src/prompt-cache.ts +55 -9
- package/src/quantize/config-writer.ts +1 -1
- package/src/registry.ts +219 -8
- package/src/responses.ts +23 -0
- package/src/sampler.ts +220 -9
- package/src/serve/batch-scheduler.ts +307 -83
- package/src/serve/generation-gateway.ts +156 -29
- package/src/server.ts +746 -73
- package/src/spec/drafter.ts +81 -49
- package/src/spec/dspark/data-dflash.ts +156 -0
- package/src/spec/dspark/data.ts +201 -0
- package/src/spec/dspark/generate-dflash.ts +193 -0
- package/src/spec/dspark/generate.ts +290 -0
- package/src/spec/dspark/loss.ts +213 -0
- package/src/spec/dspark/module-dflash.ts +361 -0
- package/src/spec/dspark/module.ts +540 -0
- package/src/spec/dspark/sample.ts +112 -0
- package/src/spec/generate.ts +40 -19
- package/src/ssd-cache.ts +213 -0
- package/src/status-page.html +1 -1
- package/src/tokenizer.ts +7 -0
- package/src/train/flash-cce.ts +81 -23
- package/src/train/fuse.ts +243 -0
- package/src/train/index.ts +3 -0
- package/src/train/job.ts +8 -0
- package/src/train/loss.ts +341 -40
- package/src/train/prefix-shared.ts +62 -11
- package/src/train/segmented.ts +215 -55
- package/src/train/trainer.ts +112 -38
- package/src/web/skills/memory/SKILL.md +40 -17
package/src/kv-store.ts
CHANGED
|
@@ -1,23 +1,38 @@
|
|
|
1
1
|
// KV-cache persistence: save prompt caches to disk, reload zero-copy.
|
|
2
|
+
// The serialization core of the SSD cold tier (docs/design/ssd-kv-cold-tier.md).
|
|
2
3
|
//
|
|
3
4
|
// File layout (every tensor PAGE-ALIGNED — the Phase 1 corollary: files
|
|
4
5
|
// we write can be mmap'd and handed to the GPU without copies):
|
|
5
|
-
// [magic "
|
|
6
|
+
// [magic "MLXBUNKV2\n"][u32 LE header length][u32 LE dataStart]
|
|
7
|
+
// [u64 LE header hash][JSON header][padding]
|
|
6
8
|
// [tensor data at 16 KiB-aligned offsets]
|
|
7
|
-
// Header: {
|
|
8
|
-
//
|
|
9
|
+
// Header: { formatVersion, modelId, configFingerprint, ns, tokenizerHash,
|
|
10
|
+
// createdAt, tokens, caches: [{ kind, offset, idx?, maxSize?,
|
|
11
|
+
// groupSize?, bits?, tensors: [{ off, bytes, shape, dtype, hash }] }] }
|
|
12
|
+
//
|
|
13
|
+
// v2 over v1: quantized cache kinds (the serving DEFAULT is kv_config
|
|
14
|
+
// quantization, which v1 could not persist), SSM kind (Qwen3.5 hybrid),
|
|
15
|
+
// invalidation metadata (configFingerprint covers the kv-quant scheme;
|
|
16
|
+
// tokenizerHash guards vocab drift; ns = adapter spec), per-tensor hashes
|
|
17
|
+
// (verified opt-in — eager verification would defeat lazy page fault-in),
|
|
18
|
+
// a header hash (always verified — cheap), and atomic tmp+fsync+rename
|
|
19
|
+
// writes. v1 files are not migrated: nothing shipped wrote them (the
|
|
20
|
+
// format was test/experiment-only) — they read as "not a v2 file" and the
|
|
21
|
+
// SSD tier deletes-and-regenerates.
|
|
9
22
|
//
|
|
10
23
|
// Reload mmaps copy-on-write (MAP_PRIVATE): if mlx ever donates one of
|
|
11
24
|
// these buffers, writes hit private pages, never the file.
|
|
12
25
|
|
|
13
|
-
import { openSync, writeSync, closeSync } from "node:fs";
|
|
26
|
+
import { openSync, writeSync, readSync, closeSync, fsyncSync, renameSync, rmSync } from "node:fs";
|
|
14
27
|
import { MmapFile } from "./mmap";
|
|
15
28
|
import { MlxArray } from "./mlx/array";
|
|
16
29
|
import type { Dtype } from "./mlx/ffi";
|
|
17
30
|
import * as ops from "./mlx/ops";
|
|
18
31
|
import {
|
|
19
|
-
type Cache, KVCache, RotatingKVCache,
|
|
20
|
-
|
|
32
|
+
type Cache, KVCache, RotatingKVCache,
|
|
33
|
+
QuantizedKVCache, RotatingQuantizedKVCache,
|
|
34
|
+
} from "./model/gemma4-base";
|
|
35
|
+
import { SSMCache } from "./model/qwen3-delta";
|
|
21
36
|
|
|
22
37
|
/** Contiguous raw bytes of (possibly a view of) an array. */
|
|
23
38
|
function contiguousBytes(a: MlxArray): Uint8Array {
|
|
@@ -27,120 +42,184 @@ function contiguousBytes(a: MlxArray): Uint8Array {
|
|
|
27
42
|
return bytes;
|
|
28
43
|
}
|
|
29
44
|
|
|
30
|
-
const MAGIC = "
|
|
45
|
+
const MAGIC = "MLXBUNKV2\n";
|
|
31
46
|
const ALIGN = 16384;
|
|
47
|
+
/** magic + u32 headerLen + u32 dataStart + u64 headerHash */
|
|
48
|
+
const PREFIX_LEN = MAGIC.length + 4 + 4 + 8;
|
|
49
|
+
|
|
50
|
+
const hash64 = (bytes: Uint8Array): string => Bun.hash(bytes).toString(16);
|
|
32
51
|
|
|
33
|
-
|
|
34
|
-
|
|
52
|
+
export type CacheKind = "kv" | "rotating" | "qkv" | "rotating-qkv" | "ssm";
|
|
53
|
+
|
|
54
|
+
interface TensorSlot {
|
|
55
|
+
off: number;
|
|
56
|
+
bytes: number;
|
|
57
|
+
shape: number[];
|
|
58
|
+
dtype: number;
|
|
59
|
+
hash: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface CacheHeaderEntry {
|
|
63
|
+
kind: CacheKind;
|
|
35
64
|
offset: number;
|
|
65
|
+
/** rotating variants: ring write index */
|
|
36
66
|
idx?: number;
|
|
67
|
+
/** rotating variants: window size */
|
|
37
68
|
maxSize?: number;
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
vBytes: number;
|
|
69
|
+
/** quantized variants */
|
|
70
|
+
groupSize?: number;
|
|
71
|
+
bits?: number;
|
|
72
|
+
/** kv/rotating: [k, v] · qkv/rotating-qkv: [kPacked, kScales, kBiases,
|
|
73
|
+
* vPacked, vScales, vBiases] · ssm: [conv, recurrent] */
|
|
74
|
+
tensors: TensorSlot[];
|
|
45
75
|
}
|
|
46
76
|
|
|
47
|
-
export interface
|
|
77
|
+
export interface KvSaveMeta {
|
|
78
|
+
modelId?: string;
|
|
79
|
+
/** configFingerprint(config) — covers every graph-shaping field incl.
|
|
80
|
+
* the kv-quant scheme, so a scheme flip invalidates naturally. */
|
|
81
|
+
configFingerprint?: string;
|
|
82
|
+
/** Adapter namespace (PromptCache ns — adapters joined with "+"). */
|
|
83
|
+
ns?: string;
|
|
84
|
+
/** sha256 of tokenizer.json: identical ids must mean identical text. */
|
|
85
|
+
tokenizerHash?: string;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface KvFileHeader extends KvSaveMeta {
|
|
89
|
+
formatVersion: 2;
|
|
90
|
+
createdAt: number;
|
|
48
91
|
tokens: number[];
|
|
49
92
|
caches: CacheHeaderEntry[];
|
|
50
93
|
}
|
|
51
94
|
|
|
52
95
|
const alignUp = (n: number) => Math.ceil(n / ALIGN) * ALIGN;
|
|
53
96
|
|
|
54
|
-
|
|
55
|
-
|
|
97
|
+
/** Snapshot one cache into header entry + blobs. Quantized full caches are
|
|
98
|
+
* sliced to the live [.., :offset, :] region (token axis — quantization
|
|
99
|
+
* groups run along the FEATURE dim, so a sequence-axis cut never splits a
|
|
100
|
+
* group); rotating caches persist the whole ring as-laid-out with ringIdx. */
|
|
101
|
+
function snapshotCache(c: Cache, dataOffset: number): { entry: CacheHeaderEntry; blobs: Uint8Array[]; next: number } {
|
|
102
|
+
const slots: TensorSlot[] = [];
|
|
56
103
|
const blobs: Uint8Array[] = [];
|
|
104
|
+
let cursor = dataOffset;
|
|
105
|
+
const push = (a: MlxArray, disposeAfter: boolean): void => {
|
|
106
|
+
const bytes = contiguousBytes(a);
|
|
107
|
+
const off = alignUp(cursor);
|
|
108
|
+
slots.push({ off, bytes: bytes.length, shape: a.shape, dtype: a.dtype, hash: hash64(bytes) });
|
|
109
|
+
blobs.push(bytes);
|
|
110
|
+
cursor = off + bytes.length;
|
|
111
|
+
if (disposeAfter) a.dispose();
|
|
112
|
+
};
|
|
113
|
+
const liveSlice = (a: MlxArray, upTo: number): MlxArray => {
|
|
114
|
+
const [B, H, , D] = a.shape as [number, number, number, number];
|
|
115
|
+
return a.slice([0, 0, 0, 0], [B, H, upTo, D]);
|
|
116
|
+
};
|
|
117
|
+
const pushTriple = (t: ops.QuantizedTensor, upTo: number | null): void => {
|
|
118
|
+
for (const a of [t.packed, t.scales, t.biases]) {
|
|
119
|
+
if (upTo === null) push(a, false);
|
|
120
|
+
else push(liveSlice(a, upTo), true);
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
let entry: CacheHeaderEntry;
|
|
125
|
+
if (c instanceof RotatingQuantizedKVCache) {
|
|
126
|
+
if (!c.keys || !c.values) throw new Error("cannot persist an empty cache");
|
|
127
|
+
pushTriple(c.keys, null);
|
|
128
|
+
pushTriple(c.values, null);
|
|
129
|
+
entry = { kind: "rotating-qkv", offset: c.offset, idx: c.ringIdx, maxSize: c.maxSize,
|
|
130
|
+
groupSize: c.groupSize, bits: c.bits, tensors: slots };
|
|
131
|
+
} else if (c instanceof QuantizedKVCache) {
|
|
132
|
+
if (!c.keys || !c.values) throw new Error("cannot persist an empty cache");
|
|
133
|
+
pushTriple(c.keys, c.offset);
|
|
134
|
+
pushTriple(c.values, c.offset);
|
|
135
|
+
entry = { kind: "qkv", offset: c.offset, groupSize: c.groupSize, bits: c.bits, tensors: slots };
|
|
136
|
+
} else if (c instanceof RotatingKVCache) {
|
|
137
|
+
if (!c.keys || !c.values) throw new Error("cannot persist an empty cache");
|
|
138
|
+
push(c.keys, false);
|
|
139
|
+
push(c.values, false);
|
|
140
|
+
entry = { kind: "rotating", offset: c.offset, idx: c.ringIdx, maxSize: c.maxSize, tensors: slots };
|
|
141
|
+
} else if (c instanceof SSMCache) {
|
|
142
|
+
if (!c.conv || !c.recurrent) throw new Error("cannot persist an empty cache");
|
|
143
|
+
push(c.conv, false);
|
|
144
|
+
push(c.recurrent, false);
|
|
145
|
+
entry = { kind: "ssm", offset: c.offset, tensors: slots };
|
|
146
|
+
} else if (c instanceof KVCache) {
|
|
147
|
+
if (!c.keys || !c.values) throw new Error("cannot persist an empty cache");
|
|
148
|
+
push(liveSlice(c.keys, c.offset), true);
|
|
149
|
+
push(liveSlice(c.values, c.offset), true);
|
|
150
|
+
entry = { kind: "kv", offset: c.offset, tensors: slots };
|
|
151
|
+
} else {
|
|
152
|
+
throw new Error("unknown cache type");
|
|
153
|
+
}
|
|
154
|
+
return { entry, blobs, next: cursor };
|
|
155
|
+
}
|
|
57
156
|
|
|
58
|
-
|
|
157
|
+
/** Persist `caches` (+ the exact token prefix they encode) to `path`.
|
|
158
|
+
* ATOMIC: written to `<path>.tmp`, fsync'd, renamed — a crash mid-write
|
|
159
|
+
* leaves only a .tmp orphan (ignored + reaped by SsdCacheStore.scan). */
|
|
160
|
+
export function saveKvCache(path: string, tokens: number[], caches: Cache[], meta: KvSaveMeta = {}): void {
|
|
161
|
+
const entries: CacheHeaderEntry[] = [];
|
|
162
|
+
const blobs: Uint8Array[] = [];
|
|
59
163
|
let dataOffset = 0; // relative; rebased after header is sized
|
|
60
164
|
for (const c of caches) {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
let maxSize: number | undefined;
|
|
66
|
-
if (c instanceof RotatingKVCache) {
|
|
67
|
-
kind = "rotating";
|
|
68
|
-
idx = c.ringIdx;
|
|
69
|
-
maxSize = c.maxSize;
|
|
70
|
-
if (!c.keys || !c.values) throw new Error("cannot persist an empty cache");
|
|
71
|
-
keys = c.keys;
|
|
72
|
-
values = c.values;
|
|
73
|
-
} else if (c instanceof KVCache) {
|
|
74
|
-
kind = "kv";
|
|
75
|
-
if (!c.keys || !c.values) throw new Error("cannot persist an empty cache");
|
|
76
|
-
// store only the live [.., :offset, :] region
|
|
77
|
-
const [B, H, , D] = c.keys.shape as [number, number, number, number];
|
|
78
|
-
const vD = c.values.shape[3]!;
|
|
79
|
-
keys = c.keys.slice([0, 0, 0, 0], [B, H, c.offset, D]);
|
|
80
|
-
values = c.values.slice([0, 0, 0, 0], [B, H, c.offset, vD]);
|
|
81
|
-
} else {
|
|
82
|
-
throw new Error("unknown cache type");
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
const kBytesArr = contiguousBytes(keys);
|
|
86
|
-
const vBytesArr = contiguousBytes(values);
|
|
87
|
-
const kOffset = alignUp(dataOffset);
|
|
88
|
-
const vOffset = alignUp(kOffset + kBytesArr.length);
|
|
89
|
-
dataOffset = vOffset + vBytesArr.length;
|
|
90
|
-
|
|
91
|
-
entries.push({
|
|
92
|
-
kind, offset: c.offset, idx, maxSize,
|
|
93
|
-
dtype: keys.dtype,
|
|
94
|
-
kShape: keys.shape, vShape: values.shape,
|
|
95
|
-
kOffset, vOffset,
|
|
96
|
-
kBytes: kBytesArr.length, vBytes: vBytesArr.length,
|
|
97
|
-
});
|
|
98
|
-
blobs.push(kBytesArr, vBytesArr);
|
|
99
|
-
if (c instanceof KVCache) {
|
|
100
|
-
keys.dispose();
|
|
101
|
-
values.dispose();
|
|
102
|
-
}
|
|
165
|
+
const s = snapshotCache(c, dataOffset);
|
|
166
|
+
entries.push(s.entry);
|
|
167
|
+
blobs.push(...s.blobs);
|
|
168
|
+
dataOffset = s.next;
|
|
103
169
|
}
|
|
104
170
|
|
|
105
|
-
const header: KvFileHeader = {
|
|
171
|
+
const header: KvFileHeader = {
|
|
172
|
+
formatVersion: 2, createdAt: Date.now(), ...meta, tokens, caches: entries,
|
|
173
|
+
};
|
|
106
174
|
const headerJson = new TextEncoder().encode(JSON.stringify(header));
|
|
107
|
-
const dataStart = alignUp(
|
|
175
|
+
const dataStart = alignUp(PREFIX_LEN + headerJson.length);
|
|
108
176
|
|
|
109
|
-
const
|
|
177
|
+
const tmp = `${path}.tmp`;
|
|
178
|
+
const fd = openSync(tmp, "w");
|
|
110
179
|
try {
|
|
111
180
|
const pre = new Uint8Array(dataStart);
|
|
112
181
|
pre.set(new TextEncoder().encode(MAGIC), 0);
|
|
113
182
|
const dv = new DataView(pre.buffer);
|
|
114
183
|
dv.setUint32(MAGIC.length, headerJson.length, true);
|
|
115
184
|
dv.setUint32(MAGIC.length + 4, dataStart, true);
|
|
116
|
-
|
|
185
|
+
dv.setBigUint64(MAGIC.length + 8, BigInt(Bun.hash(headerJson)), true);
|
|
186
|
+
pre.set(headerJson, PREFIX_LEN);
|
|
117
187
|
writeSync(fd, pre, 0, pre.length, 0);
|
|
118
188
|
|
|
119
189
|
let blobIdx = 0;
|
|
120
|
-
for (const e of entries)
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
} finally {
|
|
190
|
+
for (const e of entries)
|
|
191
|
+
for (const slot of e.tensors)
|
|
192
|
+
writeSync(fd, blobs[blobIdx++]!, 0, slot.bytes, dataStart + slot.off);
|
|
193
|
+
fsyncSync(fd);
|
|
194
|
+
} catch (err) {
|
|
126
195
|
closeSync(fd);
|
|
196
|
+
try { rmSync(tmp, { force: true }); } catch {}
|
|
197
|
+
throw err;
|
|
127
198
|
}
|
|
199
|
+
closeSync(fd);
|
|
200
|
+
renameSync(tmp, path);
|
|
128
201
|
}
|
|
129
202
|
|
|
130
|
-
/** Read only the header (cheap — for prefix matching across many files).
|
|
203
|
+
/** Read only the header (cheap — for prefix matching across many files).
|
|
204
|
+
* Always verifies the header hash; throws on any structural mismatch. */
|
|
131
205
|
export function readKvHeader(path: string): KvFileHeader & { dataStart: number } {
|
|
132
206
|
const fd = openSync(path, "r");
|
|
133
207
|
try {
|
|
134
|
-
const head = new Uint8Array(
|
|
135
|
-
|
|
208
|
+
const head = new Uint8Array(PREFIX_LEN);
|
|
209
|
+
readSync(fd, head, 0, head.length, 0);
|
|
136
210
|
if (new TextDecoder().decode(head.subarray(0, MAGIC.length)) !== MAGIC)
|
|
137
|
-
throw new Error(`${path}: not an mlx-bun KV cache file`);
|
|
211
|
+
throw new Error(`${path}: not an mlx-bun KV v2 cache file`);
|
|
138
212
|
const dv = new DataView(head.buffer);
|
|
139
213
|
const len = dv.getUint32(MAGIC.length, true);
|
|
140
214
|
const dataStart = dv.getUint32(MAGIC.length + 4, true);
|
|
215
|
+
const expectHash = dv.getBigUint64(MAGIC.length + 8, true);
|
|
141
216
|
const body = new Uint8Array(len);
|
|
142
|
-
|
|
217
|
+
readSync(fd, body, 0, len, PREFIX_LEN);
|
|
218
|
+
if (BigInt(Bun.hash(body)) !== expectHash)
|
|
219
|
+
throw new Error(`${path}: header hash mismatch (truncated or corrupt)`);
|
|
143
220
|
const header = JSON.parse(new TextDecoder().decode(body)) as KvFileHeader;
|
|
221
|
+
if (header.formatVersion !== 2)
|
|
222
|
+
throw new Error(`${path}: unsupported formatVersion ${header.formatVersion}`);
|
|
144
223
|
return { ...header, dataStart };
|
|
145
224
|
} finally {
|
|
146
225
|
closeSync(fd);
|
|
@@ -149,37 +228,100 @@ export function readKvHeader(path: string): KvFileHeader & { dataStart: number }
|
|
|
149
228
|
|
|
150
229
|
export interface LoadedKvCache {
|
|
151
230
|
tokens: number[];
|
|
231
|
+
header: KvFileHeader;
|
|
152
232
|
caches: Cache[];
|
|
153
233
|
/** Keep referenced as long as the caches live. */
|
|
154
234
|
mmap: MmapFile;
|
|
155
235
|
}
|
|
156
236
|
|
|
157
|
-
export
|
|
237
|
+
export interface KvLoadExpect {
|
|
238
|
+
/** Reject on metadata mismatch (pass what the server is running). */
|
|
239
|
+
configFingerprint?: string;
|
|
240
|
+
tokenizerHash?: string;
|
|
241
|
+
ns?: string;
|
|
242
|
+
/** Verify every tensor hash (reads all bytes — defeats lazy fault-in;
|
|
243
|
+
* off by default, `--ssd-cache-verify`). */
|
|
244
|
+
verify?: boolean;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** Reload zero-copy. `model` is anything with makeCache() — the entry count
|
|
248
|
+
* is validated against the DONOR cache list (model.layers.length was wrong
|
|
249
|
+
* for KV-shared models like e4b, whose makeCache() returns donors only). */
|
|
250
|
+
export function loadKvCache(
|
|
251
|
+
path: string,
|
|
252
|
+
model: { makeCache(): Cache[] },
|
|
253
|
+
expect: KvLoadExpect = {},
|
|
254
|
+
): LoadedKvCache {
|
|
158
255
|
const header = readKvHeader(path);
|
|
256
|
+
for (const key of ["configFingerprint", "tokenizerHash", "ns"] as const) {
|
|
257
|
+
if (expect[key] !== undefined && header[key] !== expect[key])
|
|
258
|
+
throw new Error(`${path}: ${key} mismatch (file ${header[key]}, expected ${expect[key]})`);
|
|
259
|
+
}
|
|
260
|
+
const proto = model.makeCache();
|
|
261
|
+
const cacheCount = proto.length;
|
|
262
|
+
for (const c of proto) c.dispose();
|
|
263
|
+
if (header.caches.length !== cacheCount)
|
|
264
|
+
throw new Error(`${path}: ${header.caches.length} cached layers but model has ${cacheCount}`);
|
|
265
|
+
|
|
159
266
|
const mmap = MmapFile.open(path, "cow");
|
|
160
267
|
const dataStart = header.dataStart;
|
|
268
|
+
const arr = (slot: TensorSlot): MlxArray => {
|
|
269
|
+
if (expect.verify) {
|
|
270
|
+
const view = mmap.view(dataStart + slot.off, slot.bytes);
|
|
271
|
+
if (hash64(view) !== slot.hash) {
|
|
272
|
+
mmap.unmap();
|
|
273
|
+
throw new Error(`${path}: tensor hash mismatch at offset ${slot.off}`);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
return MlxArray.fromPointer(mmap.pointer(dataStart + slot.off), slot.shape, slot.dtype as Dtype);
|
|
277
|
+
};
|
|
278
|
+
const triple = (slots: TensorSlot[], at: number): ops.QuantizedTensor =>
|
|
279
|
+
({ packed: arr(slots[at]!), scales: arr(slots[at + 1]!), biases: arr(slots[at + 2]!) });
|
|
161
280
|
|
|
162
281
|
const caches: Cache[] = [];
|
|
163
|
-
|
|
164
|
-
const
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
282
|
+
try {
|
|
283
|
+
for (const e of header.caches) {
|
|
284
|
+
const t = e.tensors;
|
|
285
|
+
switch (e.kind) {
|
|
286
|
+
case "kv": {
|
|
287
|
+
const c = new KVCache();
|
|
288
|
+
c.restoreState(arr(t[0]!), arr(t[1]!), e.offset);
|
|
289
|
+
caches.push(c);
|
|
290
|
+
break;
|
|
291
|
+
}
|
|
292
|
+
case "rotating": {
|
|
293
|
+
const c = new RotatingKVCache(e.maxSize!);
|
|
294
|
+
c.restoreState(arr(t[0]!), arr(t[1]!), e.offset, e.idx!);
|
|
295
|
+
caches.push(c);
|
|
296
|
+
break;
|
|
297
|
+
}
|
|
298
|
+
case "qkv": {
|
|
299
|
+
const c = new QuantizedKVCache(e.groupSize!, e.bits!);
|
|
300
|
+
c.restoreState(triple(t, 0), triple(t, 3), e.offset);
|
|
301
|
+
caches.push(c);
|
|
302
|
+
break;
|
|
303
|
+
}
|
|
304
|
+
case "rotating-qkv": {
|
|
305
|
+
const c = new RotatingQuantizedKVCache(e.maxSize!, e.groupSize!, e.bits!);
|
|
306
|
+
c.restoreState(triple(t, 0), triple(t, 3), e.offset, e.idx!);
|
|
307
|
+
caches.push(c);
|
|
308
|
+
break;
|
|
309
|
+
}
|
|
310
|
+
case "ssm": {
|
|
311
|
+
const c = new SSMCache();
|
|
312
|
+
c.conv = arr(t[0]!);
|
|
313
|
+
c.recurrent = arr(t[1]!);
|
|
314
|
+
c.offset = e.offset;
|
|
315
|
+
caches.push(c);
|
|
316
|
+
break;
|
|
317
|
+
}
|
|
318
|
+
default:
|
|
319
|
+
throw new Error(`${path}: unknown cache kind ${(e as { kind: string }).kind}`);
|
|
320
|
+
}
|
|
178
321
|
}
|
|
322
|
+
} catch (err) {
|
|
323
|
+
for (const c of caches) c.dispose();
|
|
324
|
+
throw err;
|
|
179
325
|
}
|
|
180
|
-
|
|
181
|
-
throw new Error(
|
|
182
|
-
`${path}: ${caches.length} cached layers but model has ${model.layers.length}`,
|
|
183
|
-
);
|
|
184
|
-
return { tokens: header.tokens, caches, mmap };
|
|
326
|
+
return { tokens: header.tokens, header, caches, mmap };
|
|
185
327
|
}
|
package/src/lora.ts
CHANGED
|
@@ -77,7 +77,7 @@ export function loadAdapterTensors(file: string): Map<string, MlxArray> {
|
|
|
77
77
|
* PEFT writes lora_alpha + r (scale = alpha / r). Reference: mount.py.
|
|
78
78
|
* `rsLora` (recorded at train time) means the effective per-layer scale is
|
|
79
79
|
* α/√rank — the caller divides by √(that layer's rank). */
|
|
80
|
-
async function readAdapterScale(dir: string): Promise<{ scale: number; rank: number | null; rsLora: boolean }> {
|
|
80
|
+
export async function readAdapterScale(dir: string): Promise<{ scale: number; rank: number | null; rsLora: boolean }> {
|
|
81
81
|
for (const name of ["optiq_lora_config.json", "adapter_config.json"]) {
|
|
82
82
|
const f = Bun.file(`${dir}/${name}`);
|
|
83
83
|
if (await f.exists()) {
|
|
@@ -94,7 +94,7 @@ async function readAdapterScale(dir: string): Promise<{ scale: number; rank: num
|
|
|
94
94
|
throw new Error(`no adapter_config.json in ${dir}`);
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
-
function adapterWeightsFile(dir: string): string {
|
|
97
|
+
export function adapterWeightsFile(dir: string): string {
|
|
98
98
|
for (const name of ["adapters.safetensors", "adapter_model.safetensors"]) {
|
|
99
99
|
const p = `${dir}/${name}`;
|
|
100
100
|
if (existsSync(p)) return p;
|