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/spec/generate.ts
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
// Speculative decoding loop — port of optiq runtime/spec/runtime.py
|
|
2
2
|
// (γ-draft / verify-in-one-pass / accept-longest-prefix / rollback).
|
|
3
3
|
//
|
|
4
|
-
// Correctness
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
4
|
+
// Correctness oracle: OPTIQ's spec_generate, NOT stock decode. Assistant-
|
|
5
|
+
// drafter speculation is an optiq feature (mlx-lm has only generic
|
|
6
|
+
// two-model spec, and can't drive a KV-borrowing drafter). Both optiq AND
|
|
7
|
+
// mlx-lm BATCH the verify lm-head (one matmul over the whole γ+1 window,
|
|
8
|
+
// then argmax per position) — so neither is bit-exact to stock token-at-
|
|
9
|
+
// a-time decode; they diverge from it at bf16 knife-edges. We do the same
|
|
10
|
+
// (picksBatched): greedy spec is bit-exact to optiq's spec, and agrees
|
|
11
|
+
// with stock only on tie-free prompts (where batched == per-position).
|
|
12
|
+
// An earlier version verified per-position to stay bit-exact to STOCK —
|
|
13
|
+
// that matched no real oracle and cost an extra γ× read of the lm-head
|
|
14
|
+
// weight per step; superseded. See docs/design/spec-decode-larger-targets.md.
|
|
9
15
|
//
|
|
10
16
|
// Limitation (same as the reference): partial-accept rollback requires
|
|
11
17
|
// trimmable caches; RotatingKVCache loses trimability once its ring
|
|
@@ -14,7 +20,6 @@
|
|
|
14
20
|
import { MlxArray } from "../mlx/array";
|
|
15
21
|
import * as ops from "../mlx/ops";
|
|
16
22
|
import { Gemma4Model, type Cache } from "../model/gemma4";
|
|
17
|
-
import { toLogprobs } from "../sampler";
|
|
18
23
|
import type { GemmaAssistantDrafter } from "./drafter";
|
|
19
24
|
|
|
20
25
|
export interface SpecOptions {
|
|
@@ -39,12 +44,8 @@ export interface SpecResult {
|
|
|
39
44
|
stats: SpecStats;
|
|
40
45
|
}
|
|
41
46
|
|
|
42
|
-
/**
|
|
43
|
-
*
|
|
44
|
-
* decode step), then logprobs argmax over [1, V]. Computing the lm-head
|
|
45
|
-
* per position (instead of batched over the verify window) keeps every
|
|
46
|
-
* pick in the same kernel shapes as stock decode — batched lm-head
|
|
47
|
-
* rounds differently and flips bf16 knife-edges. */
|
|
47
|
+
/** Single greedy pick from one hidden position [1,1,H] (lm-head + argmax).
|
|
48
|
+
* Used for the NON-speculative prefill token (matches stock there). */
|
|
48
49
|
function pickFromHidden(model: Gemma4Model, hidden: MlxArray, pos: number): number {
|
|
49
50
|
const H = hidden.shape[2]!;
|
|
50
51
|
const hSl = hidden.slice([0, pos, 0], [1, pos + 1, H]);
|
|
@@ -53,15 +54,36 @@ function pickFromHidden(model: Gemma4Model, hidden: MlxArray, pos: number): numb
|
|
|
53
54
|
const V = logits.shape[2]!;
|
|
54
55
|
const flat = ops.reshape(logits, [1, V]);
|
|
55
56
|
logits.dispose();
|
|
56
|
-
const
|
|
57
|
+
const am = ops.argmaxAxis(flat, -1);
|
|
57
58
|
flat.dispose();
|
|
58
|
-
const am = ops.argmaxAxis(lp, -1);
|
|
59
|
-
lp.dispose();
|
|
60
59
|
const t = ops.itemUint32(am);
|
|
61
60
|
am.dispose();
|
|
62
61
|
return t;
|
|
63
62
|
}
|
|
64
63
|
|
|
64
|
+
/** optiq-faithful batched verify: ONE lm-head over the whole γ+1 window
|
|
65
|
+
* (reads the lm-head weight ONCE, as optiq and mlx-lm both do), then
|
|
66
|
+
* argmax per position. This is what makes greedy spec bit-exact to
|
|
67
|
+
* optiq's spec_generate. argmax is invariant to the logsumexp shift and
|
|
68
|
+
* to the monotone final-logit softcap, so raw-logit argmax here picks the
|
|
69
|
+
* same token optiq does. */
|
|
70
|
+
function picksBatched(model: Gemma4Model, hidden: MlxArray, count: number): number[] {
|
|
71
|
+
const logits = model.logitsFromHidden(hidden); // [1, L, V] — one matmul
|
|
72
|
+
const V = logits.shape[2]!;
|
|
73
|
+
const out: number[] = [];
|
|
74
|
+
for (let k = 0; k < count; k++) {
|
|
75
|
+
const sl = logits.slice([0, k, 0], [1, k + 1, V]);
|
|
76
|
+
const flat = ops.reshape(sl, [1, V]);
|
|
77
|
+
sl.dispose();
|
|
78
|
+
const am = ops.argmaxAxis(flat, -1);
|
|
79
|
+
flat.dispose();
|
|
80
|
+
out.push(ops.itemUint32(am));
|
|
81
|
+
am.dispose();
|
|
82
|
+
}
|
|
83
|
+
logits.dispose();
|
|
84
|
+
return out;
|
|
85
|
+
}
|
|
86
|
+
|
|
65
87
|
/** Donor caches: the target's LAST sliding and LAST full cache owners
|
|
66
88
|
* (port of optiq kv_view.find_donor_layers). */
|
|
67
89
|
function donorIndices(model: Gemma4Model): { sliding: number; full: number } {
|
|
@@ -163,10 +185,9 @@ export function specGenerate(
|
|
|
163
185
|
vIds.dispose();
|
|
164
186
|
stats.targetCalls++;
|
|
165
187
|
|
|
166
|
-
// 2c. accept the longest matching prefix
|
|
167
|
-
// see
|
|
168
|
-
const gt
|
|
169
|
-
for (let k = 0; k <= gamma; k++) gt.push(pickFromHidden(model, vHidden, k));
|
|
188
|
+
// 2c. accept the longest matching prefix. Batched verify lm-head
|
|
189
|
+
// (optiq-faithful — see picksBatched).
|
|
190
|
+
const gt = picksBatched(model, vHidden, gamma + 1);
|
|
170
191
|
let kAccept = 0;
|
|
171
192
|
while (kAccept < gamma && drafts[kAccept] === gt[kAccept]) kAccept++;
|
|
172
193
|
stats.accepted += kAccept;
|
package/src/ssd-cache.ts
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
// SSD cold tier for the prompt/KV cache (docs/design/ssd-kv-cold-tier.md).
|
|
2
|
+
//
|
|
3
|
+
// Files ARE the database: no sidecar index. Layout
|
|
4
|
+
// <dir>/<configFingerprint>/<nsHash>/<uuid>.mlxkv
|
|
5
|
+
// so two models (or two kv-quant schemes of one model) share a directory
|
|
6
|
+
// without ever colliding, and an incompatible fingerprint dir is simply
|
|
7
|
+
// ignored (never deleted — it may belong to the user's other model).
|
|
8
|
+
// Startup recovery = a header-only scan of OUR fingerprint dir: header hash
|
|
9
|
+
// verified (cheap), corrupt or metadata-mismatched files unlinked, `.tmp`
|
|
10
|
+
// write orphans reaped. LRU state = file mtime (bumped on hit); the byte
|
|
11
|
+
// cap is enforced at write time by oldest-mtime eviction. A cache is always
|
|
12
|
+
// droppable: every failure path degrades to "no hit" or "not stored" — the
|
|
13
|
+
// tier must never take serving down (oMLX paged_ssd_cache lesson, Apache-2.0
|
|
14
|
+
// idea port; ours spills whole prefix entries, not content-hashed blocks —
|
|
15
|
+
// see the design doc's D1 for why).
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
existsSync, mkdirSync, readdirSync, rmSync, statSync, utimesSync,
|
|
19
|
+
} from "node:fs";
|
|
20
|
+
import { join } from "node:path";
|
|
21
|
+
import { randomUUID } from "node:crypto";
|
|
22
|
+
import { commonPrefixLength } from "./prompt-cache";
|
|
23
|
+
import {
|
|
24
|
+
saveKvCache, loadKvCache, readKvHeader,
|
|
25
|
+
type KvSaveMeta, type KvLoadExpect, type LoadedKvCache,
|
|
26
|
+
} from "./kv-store";
|
|
27
|
+
import type { Cache } from "./model/gemma4-base";
|
|
28
|
+
|
|
29
|
+
export interface SsdIndexEntry {
|
|
30
|
+
path: string;
|
|
31
|
+
ns: string;
|
|
32
|
+
tokens: number[];
|
|
33
|
+
bytes: number;
|
|
34
|
+
mtimeMs: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface SsdStoreOptions {
|
|
38
|
+
dir: string;
|
|
39
|
+
maxBytes: number;
|
|
40
|
+
/** Server identity — written into every file, enforced on scan/load. */
|
|
41
|
+
configFingerprint: string;
|
|
42
|
+
tokenizerHash: string;
|
|
43
|
+
modelId: string;
|
|
44
|
+
/** Verify every tensor hash on load (--ssd-cache-verify). */
|
|
45
|
+
verify?: boolean;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const nsHash = (ns: string): string =>
|
|
49
|
+
ns === "" ? "base" : Bun.hash(ns).toString(16);
|
|
50
|
+
|
|
51
|
+
export class SsdCacheStore {
|
|
52
|
+
readonly #opts: SsdStoreOptions;
|
|
53
|
+
readonly #root: string; // <dir>/<configFingerprint>
|
|
54
|
+
#index: SsdIndexEntry[] = [];
|
|
55
|
+
#warnedWriteFailure = false;
|
|
56
|
+
stats = { restores: 0, spills: 0, restoreMsLast: 0 };
|
|
57
|
+
|
|
58
|
+
constructor(opts: SsdStoreOptions) {
|
|
59
|
+
this.#opts = opts;
|
|
60
|
+
this.#root = join(opts.dir, opts.configFingerprint);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
get entries(): number {
|
|
64
|
+
return this.#index.length;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
get totalBytes(): number {
|
|
68
|
+
return this.#index.reduce((a, e) => a + e.bytes, 0);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
get maxBytes(): number {
|
|
72
|
+
return this.#opts.maxBytes;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Startup recovery: rebuild the in-memory index from disk. Corrupt
|
|
76
|
+
* headers and metadata mismatches are unlinked; `.tmp` orphans reaped.
|
|
77
|
+
* Only OUR fingerprint dir is touched. Returns entries indexed. */
|
|
78
|
+
scan(): number {
|
|
79
|
+
this.#index = [];
|
|
80
|
+
if (!existsSync(this.#root)) return 0;
|
|
81
|
+
for (const nsDir of readdirSync(this.#root)) {
|
|
82
|
+
const nsPath = join(this.#root, nsDir);
|
|
83
|
+
let files: string[];
|
|
84
|
+
try { files = readdirSync(nsPath); } catch { continue; }
|
|
85
|
+
for (const f of files) {
|
|
86
|
+
const path = join(nsPath, f);
|
|
87
|
+
if (f.endsWith(".tmp")) {
|
|
88
|
+
// A crash mid-write: the rename never happened. Ours by
|
|
89
|
+
// construction (uuid names), safe to reap.
|
|
90
|
+
try { rmSync(path, { force: true }); } catch {}
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (!f.endsWith(".mlxkv")) continue;
|
|
94
|
+
try {
|
|
95
|
+
const h = readKvHeader(path); // verifies the header hash
|
|
96
|
+
if (
|
|
97
|
+
h.configFingerprint !== this.#opts.configFingerprint ||
|
|
98
|
+
h.tokenizerHash !== this.#opts.tokenizerHash
|
|
99
|
+
)
|
|
100
|
+
throw new Error("metadata mismatch");
|
|
101
|
+
const st = statSync(path);
|
|
102
|
+
this.#index.push({
|
|
103
|
+
path, ns: h.ns ?? "", tokens: h.tokens, bytes: st.size, mtimeMs: st.mtimeMs,
|
|
104
|
+
});
|
|
105
|
+
} catch {
|
|
106
|
+
try { rmSync(path, { force: true }); } catch {}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return this.#index.length;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Best stored entry for `prompt` in `ns`: the longest common prefix
|
|
114
|
+
* (capped at prompt.length−1 — at least one token must forward). An
|
|
115
|
+
* entry LONGER than the matched prefix is still returned (the divergent-
|
|
116
|
+
* tail pattern: same long context, different question) — the caller
|
|
117
|
+
* trims after restore when every cache kind is trimmable, and falls
|
|
118
|
+
* back to a fresh prefill when not (ring post-wrap, SSM). Pure index
|
|
119
|
+
* lookup; no I/O. */
|
|
120
|
+
find(prompt: number[], ns = ""): { entry: SsdIndexEntry; prefixLen: number } | null {
|
|
121
|
+
let best: SsdIndexEntry | null = null;
|
|
122
|
+
let bestLen = 0;
|
|
123
|
+
for (const e of this.#index) {
|
|
124
|
+
if (e.ns !== ns) continue;
|
|
125
|
+
const p = Math.min(commonPrefixLength(e.tokens, prompt), prompt.length - 1);
|
|
126
|
+
if (p === 0 || p <= bestLen) continue;
|
|
127
|
+
bestLen = p;
|
|
128
|
+
best = e;
|
|
129
|
+
}
|
|
130
|
+
return best ? { entry: best, prefixLen: bestLen } : null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Restore an entry zero-copy (COW mmap; pages fault in lazily). Bumps
|
|
134
|
+
* LRU mtime. On ANY failure the file is dropped and null returned. */
|
|
135
|
+
restore(entry: SsdIndexEntry, model: { makeCache(): Cache[] }): LoadedKvCache | null {
|
|
136
|
+
const t0 = performance.now();
|
|
137
|
+
try {
|
|
138
|
+
const expect: KvLoadExpect = {
|
|
139
|
+
configFingerprint: this.#opts.configFingerprint,
|
|
140
|
+
tokenizerHash: this.#opts.tokenizerHash,
|
|
141
|
+
ns: entry.ns,
|
|
142
|
+
verify: this.#opts.verify,
|
|
143
|
+
};
|
|
144
|
+
const loaded = loadKvCache(entry.path, model, expect);
|
|
145
|
+
const now = new Date();
|
|
146
|
+
try { utimesSync(entry.path, now, now); entry.mtimeMs = now.getTime(); } catch {}
|
|
147
|
+
this.stats.restores++;
|
|
148
|
+
this.stats.restoreMsLast = performance.now() - t0;
|
|
149
|
+
return loaded;
|
|
150
|
+
} catch {
|
|
151
|
+
this.remove(entry.path);
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Persist an entry. Synchronous (the tier calls it on the idle serial
|
|
157
|
+
* lane between requests); atomic via kv-store's tmp+fsync+rename. An
|
|
158
|
+
* entry bigger than the cap is refused; disk/write failure is a warn-once
|
|
159
|
+
* soft-fail — serving never stalls on the cold tier. Existing entries in
|
|
160
|
+
* the same ns whose tokens are a PREFIX of the new entry are superseded
|
|
161
|
+
* (the agent-conversation pattern: one growing entry, not N generations
|
|
162
|
+
* of it). Returns true if stored. */
|
|
163
|
+
store(tokens: number[], caches: Cache[], ns = ""): boolean {
|
|
164
|
+
const dir = join(this.#root, nsHash(ns));
|
|
165
|
+
const path = join(dir, `${randomUUID()}.mlxkv`);
|
|
166
|
+
try {
|
|
167
|
+
mkdirSync(dir, { recursive: true });
|
|
168
|
+
const meta: KvSaveMeta = {
|
|
169
|
+
modelId: this.#opts.modelId,
|
|
170
|
+
configFingerprint: this.#opts.configFingerprint,
|
|
171
|
+
tokenizerHash: this.#opts.tokenizerHash,
|
|
172
|
+
ns,
|
|
173
|
+
};
|
|
174
|
+
saveKvCache(path, tokens, caches, meta);
|
|
175
|
+
const st = statSync(path);
|
|
176
|
+
if (st.size > this.#opts.maxBytes) {
|
|
177
|
+
rmSync(path, { force: true });
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
// Supersede shadowed ancestors of this prefix (same ns).
|
|
181
|
+
for (const e of [...this.#index]) {
|
|
182
|
+
if (e.ns !== ns || e.tokens.length >= tokens.length) continue;
|
|
183
|
+
if (commonPrefixLength(e.tokens, tokens) === e.tokens.length) this.remove(e.path);
|
|
184
|
+
}
|
|
185
|
+
this.#index.push({ path, ns, tokens, bytes: st.size, mtimeMs: Date.now() });
|
|
186
|
+
this.stats.spills++;
|
|
187
|
+
this.evictToCap();
|
|
188
|
+
return true;
|
|
189
|
+
} catch (err) {
|
|
190
|
+
try { rmSync(path, { force: true }); rmSync(`${path}.tmp`, { force: true }); } catch {}
|
|
191
|
+
if (!this.#warnedWriteFailure) {
|
|
192
|
+
this.#warnedWriteFailure = true;
|
|
193
|
+
console.warn(`[ssd-cache] store failed (disk full or unwritable?) — cold tier disabled for this entry: ${(err as Error).message}`);
|
|
194
|
+
}
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** Enforce the byte cap: unlink oldest-mtime entries until under it. */
|
|
200
|
+
evictToCap(): void {
|
|
201
|
+
while (this.totalBytes > this.#opts.maxBytes && this.#index.length > 0) {
|
|
202
|
+
let oldest = 0;
|
|
203
|
+
for (let i = 1; i < this.#index.length; i++)
|
|
204
|
+
if (this.#index[i]!.mtimeMs < this.#index[oldest]!.mtimeMs) oldest = i;
|
|
205
|
+
this.remove(this.#index[oldest]!.path);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
remove(path: string): void {
|
|
210
|
+
try { rmSync(path, { force: true }); } catch {}
|
|
211
|
+
this.#index = this.#index.filter((e) => e.path !== path);
|
|
212
|
+
}
|
|
213
|
+
}
|
package/src/status-page.html
CHANGED
|
@@ -285,7 +285,7 @@
|
|
|
285
285
|
<thead><tr><th>Command</th><th style="text-align:left">What it does</th></tr></thead>
|
|
286
286
|
<tbody>
|
|
287
287
|
<tr><td><code>mlx-bun pi</code></td><td style="text-align:left">Agent session on a local model — picks/downloads the right model, serves, drops into pi. Flags: <code>--query</code>, <code>--port</code>; everything else passes to pi (<code>-p</code>, <code>--continue</code>, <code>@file</code>…)</td></tr>
|
|
288
|
-
<tr><td><code>mlx-bun serve <query></code></td><td style="text-align:left">Persistent server + this page. <code>--port
|
|
288
|
+
<tr><td><code>mlx-bun serve <query></code></td><td style="text-align:left">Persistent server + this page. <code>--port 8080</code>, <code>--memory-budget GB</code></td></tr>
|
|
289
289
|
<tr><td><code>mlx-bun get <org/repo></code></td><td style="text-align:left">Resumable, sha256-verified model download</td></tr>
|
|
290
290
|
<tr><td><code>mlx-bun ls</code> / <code>fit</code></td><td style="text-align:left">List downloaded models / will-it-fit assessment (<code>--ctx</code>, <code>--skus</code>)</td></tr>
|
|
291
291
|
<tr><td><code>mlx-bun harness pi</code></td><td style="text-align:left">Register this server as a provider in your own pi install (<code>--remove</code> undoes)</td></tr>
|
package/src/tokenizer.ts
CHANGED
|
@@ -11,6 +11,10 @@ export interface LoadedTokenizer {
|
|
|
11
11
|
* you get a corrupting double-BOS `<s><s>…`). */
|
|
12
12
|
encode(text: string, addSpecialTokens?: boolean): number[];
|
|
13
13
|
decode(ids: number[], skipSpecialTokens?: boolean): string;
|
|
14
|
+
/** Token id → raw vocab token string (mlx-lm's convert_ids_to_tokens —
|
|
15
|
+
* the undecoded piece, e.g. "▁Hello", used in logprobs responses).
|
|
16
|
+
* Falls back to decode() for ids outside the base vocab (added tokens). */
|
|
17
|
+
idToToken(id: number): string;
|
|
14
18
|
readonly bosTokenId: number | null;
|
|
15
19
|
readonly eosTokenId: number | null;
|
|
16
20
|
}
|
|
@@ -36,6 +40,9 @@ export async function loadTokenizer(modelDir: string): Promise<LoadedTokenizer>
|
|
|
36
40
|
decode: (ids, skipSpecialTokens = false) =>
|
|
37
41
|
// python's decode([]) === ""; the JS lib throws on empty input
|
|
38
42
|
ids.length === 0 ? "" : tok.decode(ids, { skip_special_tokens: skipSpecialTokens }),
|
|
43
|
+
idToToken: (id) =>
|
|
44
|
+
(tok as unknown as { id_to_token(id: number): string | undefined })
|
|
45
|
+
.id_to_token(id) ?? tok.decode([id], { skip_special_tokens: false }),
|
|
39
46
|
bosTokenId: idOf("bos_token"),
|
|
40
47
|
eosTokenId: idOf("eos_token"),
|
|
41
48
|
};
|
package/src/train/flash-cce.ts
CHANGED
|
@@ -723,20 +723,28 @@ const BWD_SG_SOURCE = String.raw`
|
|
|
723
723
|
}
|
|
724
724
|
`;
|
|
725
725
|
|
|
726
|
-
// Apple-CCE coeff filter epsilon (phase-2 per-tile skip). DEFAULT
|
|
727
|
-
//
|
|
728
|
-
//
|
|
729
|
-
//
|
|
730
|
-
//
|
|
731
|
-
//
|
|
732
|
-
|
|
726
|
+
// Apple-CCE coeff filter epsilon (phase-2 per-tile skip). DEFAULT ON at 1e-5
|
|
727
|
+
// (flipped 2026-07-02 — the kernel-perf-review backlog #1 flip). The old
|
|
728
|
+
// 0.66%→2.7% dh-accuracy scare was on RANDOM targets (flat softmax, the filter's
|
|
729
|
+
// worst case — see PLAN.md "filter-on-real-data"); the real-data gate
|
|
730
|
+
// (scripts/experiments/flash-cce-filter-realdata.ts) measured, on REAL chunk-ORPO
|
|
731
|
+
// data (M1 Max, 2026-07-02): CPM5 eps 1e-5 = 0.343% dh for 1.41×; e4b = 0.158% dh
|
|
732
|
+
// for 1.70× — and the teacher-forced full-logits fidelity shows the filter's added
|
|
733
|
+
// error is far below the flash-vs-full fp-reassociation scatter that already
|
|
734
|
+
// exists (CPM5 0.850→0.913% relnorm, e4b 1.207→1.220%, cosine ≥0.99993 both).
|
|
735
|
+
// (The ~3.5× figure floating around older notes was the retired SG kernel, not
|
|
736
|
+
// steel.) MLX_BUN_CCE_BWD_FILTER_EPS=0 restores exact gradients — eps "0"
|
|
737
|
+
// compiles the filter OUT (#if CCE_BWD_FILTER 0), the identical pre-flip kernel.
|
|
738
|
+
const BWD_FILTER_EPS = process.env.MLX_BUN_CCE_BWD_FILTER_EPS ?? "1e-5";
|
|
733
739
|
// blockMax vocab-block early-exit eps (skip whole cold (token-block,vocab-block) programs
|
|
734
|
-
// — attacks BOTH phases). DEFAULT
|
|
735
|
-
// blocks whose max prob < eps AND which lack the target → ~0
|
|
736
|
-
//
|
|
737
|
-
//
|
|
738
|
-
// with
|
|
739
|
-
|
|
740
|
+
// — attacks BOTH phases). DEFAULT ON at 1e-5 (flipped 2026-07-02). It is ~LOSSLESS when
|
|
741
|
+
// it fires (only skips blocks whose max prob < eps AND which lack the target → ~0
|
|
742
|
+
// contribution). The M=512 synthetic probe found nothing cold (pure overhead), but the
|
|
743
|
+
// real-data gate proved real text DOES go cold: alone it's 1.23× (CPM5) / 2.02× (e4b)
|
|
744
|
+
// at ≤0.004% dh; COMBINED with the coeff filter at 1e-5/1e-5 the backward is 1.71×
|
|
745
|
+
// (CPM5) / 3.16× (e4b) vs exact. Fires only when the caller supplies blockMax (the
|
|
746
|
+
// production head does); MLX_BUN_CCE_BWD_BLOCK_EPS=0 disables (compiled out).
|
|
747
|
+
const BWD_BLOCK_EPS = process.env.MLX_BUN_CCE_BWD_BLOCK_EPS ?? "1e-5";
|
|
740
748
|
const _bwdSgKernels = new Map<string, MetalKernel>();
|
|
741
749
|
function bwdSgKernel(dimTiles: number, filterEps: string, blockEps: string): MetalKernel {
|
|
742
750
|
const filterOn = Number.parseFloat(filterEps) > 0 ? 1 : 0;
|
|
@@ -1005,6 +1013,22 @@ function fwdSteelKernel(gs: number, bits: number): MetalKernel {
|
|
|
1005
1013
|
return k;
|
|
1006
1014
|
}
|
|
1007
1015
|
|
|
1016
|
+
// One-time dispatch-route warnings. A failed tiling guard silently reroutes the
|
|
1017
|
+
// whole flash head onto the legacy kernels (~5× slower class: e4b M=512 backward
|
|
1018
|
+
// 754 → 3687+ ms) with no observable signal — the exact "forward passes are
|
|
1019
|
+
// flag-gated, verify in code" hazard. Name the failed guard once per route so a
|
|
1020
|
+
// new model port that misses steel is visible in the training log.
|
|
1021
|
+
const _steelFallbackWarned = new Set<string>();
|
|
1022
|
+
function warnSteelFallback(which: "forward" | "backward", detail: string, fallback: string): void {
|
|
1023
|
+
if (_steelFallbackWarned.has(which)) return;
|
|
1024
|
+
_steelFallbackWarned.add(which);
|
|
1025
|
+
console.warn(
|
|
1026
|
+
`[flash-cce] ${which}: steel kernel guard failed (${detail}) — dispatching the ` +
|
|
1027
|
+
`${fallback} instead (~5x-slower class). If this model should be steel-capable, ` +
|
|
1028
|
+
`check the tiling guards in flash-cce.ts.`,
|
|
1029
|
+
);
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1008
1032
|
export function flashCceForward(
|
|
1009
1033
|
hResp: MlxArray, head: FlashCceHead, targets: number[],
|
|
1010
1034
|
): { logp: MlxArray; lse: MlxArray; blockMax: MlxArray } {
|
|
@@ -1039,6 +1063,15 @@ export function flashCceForward(
|
|
|
1039
1063
|
const blockV0 = Math.ceil(V / NBLK);
|
|
1040
1064
|
const useSteel = process.env.MLX_BUN_CCE_NOSTEEL !== "1" && H % 32 === 0 && V % 32 === 0 && blockV0 % 32 === 0;
|
|
1041
1065
|
const useSimd = !useSteel && USE_SIMDGROUP_FWD && H % SG_BD === 0;
|
|
1066
|
+
if (!useSteel && process.env.MLX_BUN_CCE_NOSTEEL !== "1") {
|
|
1067
|
+
// NOTE blockV0 = ceil(V/NBLK) can violate %32 even when V%32==0 (e.g. V=50304)
|
|
1068
|
+
const fails = [
|
|
1069
|
+
H % 32 !== 0 ? `H%32!=0 (H=${H})` : null,
|
|
1070
|
+
V % 32 !== 0 ? `V%32!=0 (V=${V})` : null,
|
|
1071
|
+
blockV0 % 32 !== 0 ? `blockV0%32!=0 (blockV0=${blockV0}, V=${V}, NBLK=${NBLK})` : null,
|
|
1072
|
+
].filter(Boolean).join(", ");
|
|
1073
|
+
warnSteelFallback("forward", fails, useSimd ? "simdgroup kernel" : "scalar/lane kernel");
|
|
1074
|
+
}
|
|
1042
1075
|
const useLane = !useSteel && !useSimd && process.env.MLX_BUN_CCE_LANE === "1" && WPR % 32 === 0;
|
|
1043
1076
|
const kernel = useSteel ? fwdSteelKernel(GS, head.bits) : useSimd ? fwdSgKernel() : useLane ? fwdLaneKernel() : fwdKernel();
|
|
1044
1077
|
const tokTile = useSteel ? STEEL_BM : useSimd ? SG_BB : BLOCK_B;
|
|
@@ -1103,13 +1136,13 @@ export function flashCceForward(
|
|
|
1103
1136
|
* (no atomics). Caller owns the result. */
|
|
1104
1137
|
export function flashCceBackward(
|
|
1105
1138
|
hResp: MlxArray, head: FlashCceHead, targets: number[], lse: MlxArray, cot: number[],
|
|
1106
|
-
// Apple-CCE coeff filter eps.
|
|
1107
|
-
//
|
|
1108
|
-
//
|
|
1109
|
-
// does this, since synthetic-flat softmax has no negligible rows to skip).
|
|
1139
|
+
// Apple-CCE coeff filter eps. Defaults to BWD_FILTER_EPS — ON at 1e-5 since
|
|
1140
|
+
// 2026-07-02 (real-data gates in BWD_FILTER_EPS's note). Pass "0" explicitly
|
|
1141
|
+
// for guaranteed-exact gradients (the parity gate does).
|
|
1110
1142
|
filterEps: string = BWD_FILTER_EPS,
|
|
1111
1143
|
// The forward's `blockMax [M, NBLK]` enables the vocab-block early exit (skips
|
|
1112
|
-
// whole cold programs — attacks
|
|
1144
|
+
// whole cold programs — attacks BOTH phases); ON at 1e-5 by default when
|
|
1145
|
+
// blockMax is supplied. Omit blockMax (or set blockEps "0") for exact.
|
|
1113
1146
|
blockMax?: MlxArray, blockEps: string = BWD_BLOCK_EPS,
|
|
1114
1147
|
): MlxArray {
|
|
1115
1148
|
const M = hResp.shape[0]!;
|
|
@@ -1119,7 +1152,10 @@ export function flashCceBackward(
|
|
|
1119
1152
|
const GS = head.groupSize;
|
|
1120
1153
|
const WPR = head.w.shape[1]!;
|
|
1121
1154
|
if (H > H_MAX) throw new Error(`flashCceBackward: hidden ${H} > H_MAX ${H_MAX}`);
|
|
1122
|
-
|
|
1155
|
+
// NOTE: H % TG is a LEGACY-SCALAR-kernel requirement only (it owns DPER = H/TG
|
|
1156
|
+
// dims per thread — floor division, so a remainder would leave dims silently
|
|
1157
|
+
// zero). The steel/simdgroup kernels have their own (weaker) guards below; the
|
|
1158
|
+
// throw is applied only when dispatch actually lands on the scalar kernel.
|
|
1123
1159
|
|
|
1124
1160
|
const hF = hResp.dtype === Dtype.float32 ? hResp : hResp.astype(Dtype.float32);
|
|
1125
1161
|
const scF = head.scales.dtype === Dtype.float32 ? head.scales : head.scales.astype(Dtype.float32);
|
|
@@ -1133,10 +1169,12 @@ export function flashCceBackward(
|
|
|
1133
1169
|
|
|
1134
1170
|
// STEEL is the DEFAULT (verbatim MLX BlockMMA GEMM for both phases, [M,V]-free) when
|
|
1135
1171
|
// the tiling is clean (H,V,blockV all %32 AND H % HTw for the H-tiled accumulator).
|
|
1136
|
-
// MLX_BUN_CCE_BWD_NOSTEEL=1 falls back to the old kernels. The steel backward
|
|
1137
|
-
//
|
|
1138
|
-
//
|
|
1139
|
-
//
|
|
1172
|
+
// MLX_BUN_CCE_BWD_NOSTEEL=1 falls back to the old kernels. The steel backward carries
|
|
1173
|
+
// the SAME Apple-CCE skips as the SG kernel — coeff filter (phase-2 per-tile) +
|
|
1174
|
+
// blockMax vocab-block early exit (whole-program) — both compiled out at eps=0.
|
|
1175
|
+
// BOTH skips default ON at 1e-5 (2026-07-02 flip; measured gates in the
|
|
1176
|
+
// BWD_FILTER_EPS / BWD_BLOCK_EPS notes). MLX_BUN_CCE_BWD_{FILTER,BLOCK}_EPS=0
|
|
1177
|
+
// restores exact gradients (identical pre-flip kernels).
|
|
1140
1178
|
const blockV0 = Math.ceil(V / NBLK);
|
|
1141
1179
|
// HTw == group_size for the phase-2 QuantizedBlockLoader (one group per H-tile); needs
|
|
1142
1180
|
// H % GS == 0 (always, GR*GS=H) and GS % 32 == 0 (TNv = GS/32 frags per H-tile integral).
|
|
@@ -1144,6 +1182,26 @@ export function flashCceBackward(
|
|
|
1144
1182
|
// simdgroup_matrix phase-2 GEMM is the next tier (5512→3913 ms, 1.41×) when the H
|
|
1145
1183
|
// tiling is clean; MLX_BUN_CCE_BWD_SCALAR=1 forces the float4 scalar kernel.
|
|
1146
1184
|
const useSg = !useSteel && process.env.MLX_BUN_CCE_BWD_SCALAR !== "1" && H % (SGN * 8) === 0 && H % SG_BD === 0;
|
|
1185
|
+
if (!useSteel && process.env.MLX_BUN_CCE_BWD_NOSTEEL !== "1") {
|
|
1186
|
+
// NOTE blockV0 = ceil(V/NBLK) can violate %32 even when V%32==0 (e.g. V=50304)
|
|
1187
|
+
const fails = [
|
|
1188
|
+
H % 32 !== 0 ? `H%32!=0 (H=${H})` : null,
|
|
1189
|
+
V % 32 !== 0 ? `V%32!=0 (V=${V})` : null,
|
|
1190
|
+
blockV0 % 32 !== 0 ? `blockV0%32!=0 (blockV0=${blockV0}, V=${V}, NBLK=${NBLK})` : null,
|
|
1191
|
+
H % GS !== 0 ? `H%GS!=0 (H=${H}, GS=${GS})` : null,
|
|
1192
|
+
GS % 32 !== 0 ? `GS%32!=0 (GS=${GS})` : null,
|
|
1193
|
+
].filter(Boolean).join(", ");
|
|
1194
|
+
warnSteelFallback("backward", fails, useSg ? "simdgroup kernel" : "scalar kernel");
|
|
1195
|
+
}
|
|
1196
|
+
// Legacy scalar kernel: each thread owns DPER = H/TG dims (floor) — a remainder
|
|
1197
|
+
// would leave the last H%TG dims silently zero, so it hard-requires H % TG == 0.
|
|
1198
|
+
// Only enforced when dispatch actually lands there (steel/SG have weaker guards).
|
|
1199
|
+
if (!useSteel && !useSg && H % TG !== 0)
|
|
1200
|
+
throw new Error(
|
|
1201
|
+
`flashCceBackward: hidden ${H} not divisible by TG ${TG} — required by the legacy ` +
|
|
1202
|
+
`scalar kernel, which is the selected route because the steel/simdgroup tiling ` +
|
|
1203
|
+
`guards also failed for this shape (see the [flash-cce] warning above).`,
|
|
1204
|
+
);
|
|
1147
1205
|
// Block-skip is on (either kernel) only when blockMax is supplied AND blockEps>0 (the
|
|
1148
1206
|
// kernel reads it as an extra input). The coeff filter needs no extra input (just eps).
|
|
1149
1207
|
const useBlockSkip = (useSteel || useSg) && blockMax != null && Number.parseFloat(blockEps) > 0;
|