mlx-bun 0.0.8 → 0.0.9

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 (133) hide show
  1. package/README.md +112 -20
  2. package/docs/design/batching-v2-plan.md +515 -0
  3. package/docs/design/bucketing-stage.md +196 -0
  4. package/docs/design/compat-cli-surface-design.md +11 -11
  5. package/docs/design/docs-reorg-plan.md +1 -1
  6. package/docs/design/dreaming-nightly-pipeline.md +291 -0
  7. package/docs/design/dspark-speculative-decoding.md +106 -0
  8. package/docs/design/generic-model-support.md +381 -0
  9. package/docs/design/memory-inference-path.md +422 -0
  10. package/docs/design/memory-synthesis.md +287 -0
  11. package/docs/design/minicpm5-decode-megakernel.md +484 -0
  12. package/docs/design/mlx-lm-tool-parity-plan.md +535 -0
  13. package/docs/design/orpo-dynamic-lambda.md +41 -0
  14. package/docs/design/orpo-training.md +35 -5
  15. package/docs/design/parallel-slots.md +12 -1
  16. package/docs/design/parity-tier-dag.md +13 -5
  17. package/docs/design/spec-decode-larger-targets.md +229 -0
  18. package/docs/design/tauri-desktop-app.md +2 -2
  19. package/docs/design/the-dreaming-handoff.md +141 -0
  20. package/docs/design/the-dreaming-master-plan.md +446 -0
  21. package/docs/design/write-pipeline-entity.md +468 -0
  22. package/docs/investigations/curve-bisector-routing.md +1 -1
  23. package/docs/investigations/decode-roofline-lookagain.md +262 -0
  24. package/docs/investigations/dspark-handoff.md +58 -0
  25. package/docs/investigations/kernel-perf-review-2026-07.md +92 -0
  26. package/docs/planning/PRODUCT_ROADMAP.md +2 -2
  27. package/docs/planning/curve-sampler-research-plan.md +331 -0
  28. package/docs/planning/memory-docs-and-dag-plan.md +185 -0
  29. package/docs/planning/pi-integration-review.md +178 -0
  30. package/docs/planning/release-notes-v0.0.9.md +122 -0
  31. package/docs/planning/web-ui-pass-plan.md +213 -0
  32. package/docs/planning/website-readme-pass-plan.md +224 -0
  33. package/docs/reference/cli.md +255 -0
  34. package/docs/reference/embedding.md +7 -2
  35. package/docs/reference/library-api.md +22 -3
  36. package/docs/reference/memory.md +5 -5
  37. package/docs/reference/models.md +137 -0
  38. package/docs/reference/orpo-quickstart.md +6 -0
  39. package/docs/reference/server-api.md +187 -9
  40. package/docs/reference/server-config.md +40 -9
  41. package/docs/reference/training.md +25 -11
  42. package/package.json +1 -1
  43. package/src/anthropic.ts +22 -0
  44. package/src/assets/curve-designer.html +1 -1
  45. package/src/chat-template.ts +25 -7
  46. package/src/cli.ts +1029 -86
  47. package/src/config.ts +4 -1
  48. package/src/download.ts +99 -8
  49. package/src/embed.ts +77 -0
  50. package/src/eval/perplexity.ts +164 -0
  51. package/src/eval/runner.ts +15 -6
  52. package/src/eval/tasks/bfcl.ts +1 -1
  53. package/src/generate.ts +141 -18
  54. package/src/harness-pi.ts +43 -17
  55. package/src/index.ts +2 -0
  56. package/src/lora.ts +2 -2
  57. package/src/memory/article.ts +558 -0
  58. package/src/memory/chunk-validation.ts +150 -0
  59. package/src/memory/chunk.ts +305 -12
  60. package/src/memory/cluster.ts +344 -17
  61. package/src/memory/coverage.ts +134 -0
  62. package/src/memory/crosslink.ts +0 -0
  63. package/src/memory/db.ts +184 -45
  64. package/src/memory/entity.ts +203 -0
  65. package/src/memory/gate.ts +187 -0
  66. package/src/memory/history.ts +152 -0
  67. package/src/memory/ingest.ts +324 -11
  68. package/src/memory/model.ts +321 -0
  69. package/src/memory/normalize.ts +671 -0
  70. package/src/memory/parse.ts +22 -0
  71. package/src/memory/pipeline.ts +242 -30
  72. package/src/memory/prompts.ts +42 -0
  73. package/src/memory/query.ts +334 -0
  74. package/src/memory/reconcile.ts +734 -0
  75. package/src/memory/redirect.ts +108 -0
  76. package/src/memory/reindex.ts +325 -0
  77. package/src/memory/resolve.ts +307 -0
  78. package/src/memory/route.ts +272 -0
  79. package/src/memory/schedule.ts +6 -4
  80. package/src/memory/stages.ts +774 -0
  81. package/src/memory/synthesize.ts +1629 -20
  82. package/src/memory/tools.ts +446 -96
  83. package/src/memory/vault.ts +462 -35
  84. package/src/memory/wikify.ts +1057 -16
  85. package/src/mlx/ffi.ts +3 -0
  86. package/src/mlx/ops.ts +46 -0
  87. package/src/model/compiled-decode.ts +43 -8
  88. package/src/model/factory.ts +29 -8
  89. package/src/model/flash-attention.ts +4 -2
  90. package/src/model/fused-decode-kernel.ts +14 -7
  91. package/src/model/fused-mlp-kernel.ts +169 -0
  92. package/src/model/fused-swiglu-kernel.ts +240 -0
  93. package/src/model/gemma4-base.ts +62 -6
  94. package/src/model/gemma4.ts +33 -2
  95. package/src/model/minicpm5.ts +106 -8
  96. package/src/model/qwen3.ts +276 -0
  97. package/src/model/steel-linear-kernel.ts +96 -0
  98. package/src/model/support.ts +38 -8
  99. package/src/model/universal/archs.ts +332 -0
  100. package/src/model/universal/dense.ts +576 -0
  101. package/src/model/universal/modules.ts +163 -0
  102. package/src/model/universal/rope.ts +366 -0
  103. package/src/pi-provider.ts +1 -1
  104. package/src/pi-terminal.ts +29 -8
  105. package/src/pi-web.ts +19 -2
  106. package/src/quantize/config-writer.ts +1 -1
  107. package/src/registry.ts +215 -7
  108. package/src/responses.ts +23 -0
  109. package/src/sampler.ts +220 -9
  110. package/src/serve/batch-scheduler.ts +272 -80
  111. package/src/serve/generation-gateway.ts +86 -18
  112. package/src/server.ts +612 -49
  113. package/src/spec/drafter.ts +81 -49
  114. package/src/spec/dspark/data-dflash.ts +156 -0
  115. package/src/spec/dspark/data.ts +201 -0
  116. package/src/spec/dspark/generate-dflash.ts +193 -0
  117. package/src/spec/dspark/generate.ts +290 -0
  118. package/src/spec/dspark/loss.ts +213 -0
  119. package/src/spec/dspark/module-dflash.ts +361 -0
  120. package/src/spec/dspark/module.ts +540 -0
  121. package/src/spec/dspark/sample.ts +112 -0
  122. package/src/spec/generate.ts +40 -19
  123. package/src/status-page.html +1 -1
  124. package/src/tokenizer.ts +7 -0
  125. package/src/train/flash-cce.ts +70 -15
  126. package/src/train/fuse.ts +243 -0
  127. package/src/train/index.ts +3 -0
  128. package/src/train/job.ts +8 -0
  129. package/src/train/loss.ts +289 -30
  130. package/src/train/prefix-shared.ts +33 -5
  131. package/src/train/segmented.ts +67 -11
  132. package/src/train/trainer.ts +92 -28
  133. package/src/web/skills/memory/SKILL.md +40 -17
@@ -0,0 +1,321 @@
1
+ // mlx-bun memory — local-model call seam for The Dreaming.
2
+ //
3
+ // ONE base model drives the whole synthesis pipeline: Gemma-4-e4b (the OptiQ
4
+ // 4-bit checkpoint). Each stage (chunk / entity / route / section / synthesis /
5
+ // editor) renders a {system?, user} message array through the SAME chat-template
6
+ // path the server uses (server.ts:977-980), so a stage that was SFT'd with a
7
+ // system turn (the `memory-chunk` chunk adapter) decodes ON-distribution — the
8
+ // trained `<|turn>system…<turn|>` block is present, not dropped.
9
+ //
10
+ // Two seams, ONE shared model:
11
+ // - callLocal(stage, input) — one call, bit-exact greedy (the eval
12
+ // runner's raw-forward decode), the safe
13
+ // fallback used by every single-call site.
14
+ // - callLocalBatch(stage, inputs) — N independent calls for ONE stage, routed
15
+ // through the in-process continuous-batching
16
+ // BatchScheduler when batch>1, else looped on
17
+ // the bit-exact greedy path. Order-preserving.
18
+ //
19
+ // A lazy module-level runtime holds ONE RuntimeModel (via loadTaskModel), the
20
+ // `memory-chunk` adapter mounted once, and ONE BatchScheduler — consolidating the
21
+ // old per-(stage,adapter) TaskModel instances into a single shared model (reuse
22
+ // AND batching in one move). Stages NEVER re-implement prompt→ids; they pass
23
+ // {system?, user} and this seam renders+encodes+BOS-dedupes identically to the
24
+ // server and to the trainer's prompt-region render (dataset.ts:98-101).
25
+
26
+ import { existsSync, readdirSync } from "node:fs";
27
+ import { loadTaskModel, greedyDecodeBitExact, type TaskModel } from "../eval/runner";
28
+ import type { ChatMessage, ChatTemplate } from "../chat-template";
29
+ import type { LoadedTokenizer } from "../tokenizer";
30
+ import { BatchScheduler, type RowSampler } from "../serve/batch-scheduler";
31
+ import * as ops from "../mlx/ops";
32
+ import type { MlxArray } from "../mlx/array";
33
+
34
+ const HF_HUB = `${process.env.HOME}/.cache/huggingface/hub`;
35
+ const E4B_REPO = "models--mlx-community--gemma-4-e4b-it-OptiQ-4bit";
36
+
37
+ /** Resolve the e4b snapshot dir by globbing snapshots/ for the one carrying
38
+ * config.json — so a freshly-downloaded model needs no hardcoded commit hash.
39
+ * Returns an _unresolved sentinel (precheck → STOP) until the download lands.
40
+ * Mirrors tests/paths.ts hfSnapshot / SNAPSHOT_E4B, inlined to keep src self-
41
+ * contained (no src→tests dependency). */
42
+ function resolveE4bSnapshot(): string {
43
+ const base = `${HF_HUB}/${E4B_REPO}/snapshots`;
44
+ try {
45
+ for (const snap of readdirSync(base))
46
+ if (existsSync(`${base}/${snap}/config.json`)) return `${base}/${snap}`;
47
+ } catch {
48
+ /* not downloaded yet */
49
+ }
50
+ return `${base}/_unresolved`;
51
+ }
52
+
53
+ export const MODEL_ID = resolveE4bSnapshot();
54
+
55
+ /** Per-stage adapter dir, or undefined when none is symlinked (run base). Only
56
+ * the `chunk` stage has a trained adapter on disk today (`memory-chunk`). */
57
+ export function adapterDirFor(stage: string): string | undefined {
58
+ const dir = `${process.env.HOME}/.cache/mlx-bun/adapters/memory-${stage}`;
59
+ return existsSync(dir) ? dir : undefined;
60
+ }
61
+
62
+ // ---------------------------------------------------------------------------
63
+ // Templating — system vs user per stage
64
+ // ---------------------------------------------------------------------------
65
+
66
+ /** Token budget for EVERY memory model call. A finished answer stops at EOS on
67
+ * its own; a maxTokens cap can only ever truncate an UNFINISHED answer — there is
68
+ * no case where capping output improves it (a one-word verdict already stops; a
69
+ * long section that gets cut is corrupted). This is a single high backstop against
70
+ * a pathological non-terminating decode, set far above any real output. Never set
71
+ * a per-call cap below this. */
72
+ export const MAX_OUTPUT_TOKENS = 64_000;
73
+
74
+ /** A stage's model input: a SYSTEM turn (instruction/policy) plus the USER turn
75
+ * (the content to operate on). `system` is OPTIONAL — when omitted, the stage's
76
+ * default system (below) is applied; pass it explicitly to override (the chunk
77
+ * stage passes its EXACT trained system, CHUNK_SYSTEM, so the only trained
78
+ * adapter decodes byte-for-byte on-distribution). */
79
+ export interface LocalInput {
80
+ system?: string;
81
+ user: string;
82
+ }
83
+
84
+ /** Default per-stage SYSTEM turn, applied when `LocalInput.system` is omitted.
85
+ *
86
+ * The `chunk` stage is intentionally ABSENT here: it supplies its exact trained
87
+ * system (CHUNK_SYSTEM in chunk.ts) explicitly, the load-bearing correctness fix.
88
+ * The base-stage systems are a QUALITY split for the instruct model (which honors
89
+ * a system turn) — there is no trained adapter to match byte-for-byte, so these
90
+ * are concise directives that reinforce each prompt's existing instructions. */
91
+ const DEFAULT_STAGE_SYSTEM: Record<string, string> = {
92
+ entity:
93
+ "You are an entity extractor. Read ONE conversation chunk and list the " +
94
+ "canonical wiki-title names it is about, one per line — nothing else.",
95
+ route: "You answer only 'yes' or 'no'.",
96
+ section:
97
+ "You route a conversation chunk to an article section. Follow the " +
98
+ "instructions in the message and answer concisely with no preamble.",
99
+ synthesis:
100
+ "You are a careful encyclopedia editor. Follow the editorial instructions " +
101
+ "in the message exactly and output only the requested article text.",
102
+ editor:
103
+ "You are a careful encyclopedia section editor. Follow the editorial " +
104
+ "instructions in the message exactly and output only the requested text.",
105
+ };
106
+
107
+ /** Build the {system?, user} message array for a stage — system resolved from the
108
+ * explicit override, else the stage default (absent ⇒ user-only, today's shape). */
109
+ export function memoryMessages(stage: string, input: LocalInput): ChatMessage[] {
110
+ const system = input.system ?? DEFAULT_STAGE_SYSTEM[stage];
111
+ const msgs: ChatMessage[] = [];
112
+ if (system) msgs.push({ role: "system", content: system });
113
+ msgs.push({ role: "user", content: input.user });
114
+ return msgs;
115
+ }
116
+
117
+ /** Render a stage input to prompt ids through the model's chat template, exactly
118
+ * like the server (server.ts:977-980) and the trainer's prompt-region render
119
+ * (dataset.ts:98-101): render([system?,user], addGenerationPrompt:true) →
120
+ * encode (the template already emits the BOS) → strip a duplicate leading BOS.
121
+ * Pure w.r.t. the GPU (tokenizer + template only), so the parity test can call
122
+ * it without loading the model. */
123
+ export function memoryPromptIds(
124
+ stage: string,
125
+ input: LocalInput,
126
+ tokenizer: LoadedTokenizer,
127
+ template: ChatTemplate,
128
+ ): number[] {
129
+ const text = template.render(memoryMessages(stage, input), { addGenerationPrompt: true });
130
+ const ids = tokenizer.encode(text); // template emits <bos>; default add_special
131
+ const bos = tokenizer.bosTokenId;
132
+ return ids.length >= 2 && ids[0] === bos && ids[1] === bos ? ids.slice(1) : ids;
133
+ }
134
+
135
+ // ---------------------------------------------------------------------------
136
+ // Shared runtime — one model, one adapter mount, one scheduler
137
+ // ---------------------------------------------------------------------------
138
+
139
+ /** Async mutex: acquire() resolves to a release fn; releases run FIFO. Serializes
140
+ * memory generation ops so the global `loraState.active` is owned exclusively for
141
+ * one op's lifetime (set for the chunk batch, reset after). */
142
+ class AsyncMutex {
143
+ #tail: Promise<void> = Promise.resolve();
144
+ acquire(): Promise<() => void> {
145
+ let release!: () => void;
146
+ const gate = new Promise<void>((r) => { release = r; });
147
+ const wait = this.#tail;
148
+ this.#tail = this.#tail.then(() => gate);
149
+ return wait.then(() => release);
150
+ }
151
+ }
152
+
153
+ interface MemoryRuntime {
154
+ tm: TaskModel;
155
+ template: ChatTemplate;
156
+ scheduler: BatchScheduler;
157
+ /** True once the `memory-chunk` adapter is mounted on the shared model. */
158
+ hasChunkAdapter: boolean;
159
+ }
160
+
161
+ /** Max rows in a memory batch (continuous-batching width). 1 disables batching
162
+ * (callLocalBatch then loops on the bit-exact greedy fallback).
163
+ * Default 1 (serial): batching measured 1.7-1.9x SLOWER for the real
164
+ * extract/chunk workload (heterogeneous prefills pad) and can diverge on
165
+ * near-ties — see docs/design/memory-inference-path.md "Verification results".
166
+ * Opt back in with MLX_BUN_MEMORY_BATCH=8 if a length-bucketed scheduler
167
+ * lands. (Decision: Josh, 2026-07-01.) */
168
+ export function memoryBatchSize(): number {
169
+ const n = Number(process.env.MLX_BUN_MEMORY_BATCH ?? "1");
170
+ return Number.isFinite(n) && n >= 1 ? Math.floor(n) : 1;
171
+ }
172
+
173
+ function requireModel(): void {
174
+ if (!existsSync(`${MODEL_ID}/config.json`)) {
175
+ throw new Error(
176
+ `memory: Gemma-4-e4b is not downloaded (looked under ${HF_HUB}/${E4B_REPO}/snapshots). ` +
177
+ `Fetch it first:\n HF_HUB_DISABLE_XET=1 hf download mlx-community/gemma-4-e4b-it-OptiQ-4bit`,
178
+ );
179
+ }
180
+ }
181
+
182
+ let runtimePromise: Promise<MemoryRuntime> | null = null;
183
+ const opLock = new AsyncMutex(); // serializes whole memory ops (loraState ownership)
184
+ const schedLock = new AsyncMutex(); // the scheduler's exclusive-GPU lock
185
+
186
+ /** Lazily load the ONE shared model + mount memory-chunk + build the scheduler.
187
+ * Never auto-downloads; throws (with the fetch hint) until the snapshot lands. */
188
+ async function getRuntime(): Promise<MemoryRuntime> {
189
+ if (!runtimePromise) {
190
+ runtimePromise = (async () => {
191
+ requireModel();
192
+ const tm = await loadTaskModel(MODEL_ID); // base e4b, no adapter
193
+ if (!tm.template) throw new Error(`memory: ${MODEL_ID} has no chat template`);
194
+ let hasChunkAdapter = false;
195
+ const chunkDir = adapterDirFor("chunk");
196
+ if (chunkDir) {
197
+ // Mount ONCE; activation is per-call via loraState.active (see decodeGreedy
198
+ // / batchDecode). Mounting alone runs base — it must be activated.
199
+ const { AdapterManager } = await import("../lora");
200
+ await new AdapterManager(tm.model).mount("memory-chunk", chunkDir);
201
+ hasChunkAdapter = true;
202
+ }
203
+ const scheduler = new BatchScheduler(tm.model, {
204
+ maxBatch: memoryBatchSize(),
205
+ lock: { acquire: () => schedLock.acquire() },
206
+ });
207
+ return { tm, template: tm.template, scheduler, hasChunkAdapter };
208
+ })();
209
+ }
210
+ return runtimePromise;
211
+ }
212
+
213
+ /** The adapters to activate for a stage. Only `chunk` is adapter-bound (and only
214
+ * when `memory-chunk` is mounted); every base stage runs with `active = []`. */
215
+ function activeFor(stage: string, rt: MemoryRuntime): string[] {
216
+ return stage === "chunk" && rt.hasChunkAdapter ? ["memory-chunk"] : [];
217
+ }
218
+
219
+ /** One bit-exact greedy decode for a stage (raw-forward argmax, runner parity).
220
+ * Sets the stage's adapter for the call and resets `active` to [] after. */
221
+ function decodeGreedy(rt: MemoryRuntime, stage: string, input: LocalInput, maxTokens: number): string {
222
+ const active = activeFor(stage, rt);
223
+ rt.tm.model.loraState.active = active;
224
+ const ids = memoryPromptIds(stage, input, rt.tm.tokenizer, rt.template);
225
+ try {
226
+ return greedyDecodeBitExact(
227
+ { ...rt.tm, activeAdapters: active.length ? active : undefined },
228
+ ids,
229
+ maxTokens,
230
+ );
231
+ } finally {
232
+ rt.tm.model.loraState.active = [];
233
+ }
234
+ }
235
+
236
+ /** Batched decode for N independent inputs of ONE stage. Sets the stage adapter
237
+ * globally for the whole batch (the scheduler reads `loraState.active` per
238
+ * forward, so all rows get it uniformly — a batch is necessarily single-adapter),
239
+ * submits every row to the shared scheduler with a greedy per-row sampler, and
240
+ * resets `active` after all rows finish. Order-preserving: out[i] ↔ inputs[i].
241
+ *
242
+ * Decode-path caveat: this samples via forwardHidden/logitsFromHidden (the
243
+ * scheduler), which can diverge from the runner's raw-forward greedy on near-ties
244
+ * past ~32 tokens. For the JSON/binary memory stages (validated by
245
+ * well-formedness) this is acceptable; the single-call fallback stays bit-exact. */
246
+ async function batchDecode(
247
+ rt: MemoryRuntime,
248
+ stage: string,
249
+ inputs: LocalInput[],
250
+ maxTokens: number,
251
+ ): Promise<string[]> {
252
+ const active = activeFor(stage, rt);
253
+ rt.tm.model.loraState.active = active;
254
+ const eos = rt.tm.config.eosTokenIds;
255
+ const greedySample: RowSampler = (logits1V: MlxArray) => ops.argmaxAxis(logits1V, -1);
256
+ try {
257
+ return await Promise.all(
258
+ inputs.map((inp) => {
259
+ const ids = memoryPromptIds(stage, inp, rt.tm.tokenizer, rt.template);
260
+ const toks: number[] = [];
261
+ return rt.scheduler
262
+ .submit({
263
+ promptIds: ids,
264
+ maxTokens,
265
+ eosTokenIds: eos,
266
+ sample: greedySample,
267
+ onToken: (t) => { toks.push(t); },
268
+ })
269
+ .then(() => rt.tm.tokenizer.decode(toks, true));
270
+ }),
271
+ );
272
+ } finally {
273
+ rt.tm.model.loraState.active = [];
274
+ }
275
+ }
276
+
277
+ // ---------------------------------------------------------------------------
278
+ // Public seams
279
+ // ---------------------------------------------------------------------------
280
+
281
+ /** Call the local model for a pipeline `stage` with a {system?, user} input.
282
+ * Renders through the shared chat-template path, runs the MODEL precheck, and
283
+ * decodes ONE completion on the bit-exact greedy path (the safe fallback used by
284
+ * every single-call site). NEVER auto-downloads. */
285
+ export async function callLocal(
286
+ stage: string,
287
+ input: LocalInput,
288
+ opts?: { maxTokens?: number },
289
+ ): Promise<string> {
290
+ const rt = await getRuntime();
291
+ const release = await opLock.acquire();
292
+ try {
293
+ return decodeGreedy(rt, stage, input, opts?.maxTokens ?? 256);
294
+ } finally {
295
+ release();
296
+ }
297
+ }
298
+
299
+ /** N independent calls for ONE stage (homogeneous adapter by construction).
300
+ * Routed through the shared in-process BatchScheduler when batch>1 AND N>1, else
301
+ * looped on the bit-exact greedy fallback. Order-preserving: out[i] ↔ inputs[i]. */
302
+ export async function callLocalBatch(
303
+ stage: string,
304
+ inputs: LocalInput[],
305
+ opts?: { maxTokens?: number },
306
+ ): Promise<string[]> {
307
+ if (inputs.length === 0) return [];
308
+ const rt = await getRuntime();
309
+ const maxTokens = opts?.maxTokens ?? 256;
310
+ const release = await opLock.acquire();
311
+ try {
312
+ if (inputs.length === 1 || memoryBatchSize() <= 1) {
313
+ const out: string[] = [];
314
+ for (const inp of inputs) out.push(decodeGreedy(rt, stage, inp, maxTokens));
315
+ return out;
316
+ }
317
+ return await batchDecode(rt, stage, inputs, maxTokens);
318
+ } finally {
319
+ release();
320
+ }
321
+ }