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.
Files changed (144) hide show
  1. package/README.md +112 -20
  2. package/docs/design/batching-perf-path.md +232 -0
  3. package/docs/design/batching-v2-plan.md +515 -0
  4. package/docs/design/bucketing-stage.md +196 -0
  5. package/docs/design/compat-cli-surface-design.md +11 -11
  6. package/docs/design/docs-reorg-plan.md +1 -1
  7. package/docs/design/dreaming-nightly-pipeline.md +291 -0
  8. package/docs/design/dspark-speculative-decoding.md +106 -0
  9. package/docs/design/generic-model-support.md +381 -0
  10. package/docs/design/memory-inference-path.md +422 -0
  11. package/docs/design/memory-synthesis.md +287 -0
  12. package/docs/design/minicpm5-decode-megakernel.md +484 -0
  13. package/docs/design/mlx-lm-tool-parity-plan.md +535 -0
  14. package/docs/design/omlx-adoption-map.md +90 -0
  15. package/docs/design/orpo-dynamic-lambda.md +41 -0
  16. package/docs/design/orpo-training.md +35 -5
  17. package/docs/design/parallel-slots.md +12 -1
  18. package/docs/design/parity-tier-dag.md +13 -5
  19. package/docs/design/segmented-backward-training.md +15 -0
  20. package/docs/design/spec-decode-larger-targets.md +229 -0
  21. package/docs/design/ssd-kv-cold-tier.md +158 -0
  22. package/docs/design/tauri-desktop-app.md +2 -2
  23. package/docs/design/the-dreaming-handoff.md +141 -0
  24. package/docs/design/the-dreaming-master-plan.md +446 -0
  25. package/docs/design/write-pipeline-entity.md +468 -0
  26. package/docs/investigations/curve-bisector-routing.md +1 -1
  27. package/docs/investigations/decode-roofline-lookagain.md +348 -0
  28. package/docs/investigations/dspark-handoff.md +58 -0
  29. package/docs/investigations/kernel-perf-review-2026-07.md +171 -0
  30. package/docs/planning/PRODUCT_ROADMAP.md +2 -2
  31. package/docs/planning/curve-sampler-research-plan.md +331 -0
  32. package/docs/planning/memory-docs-and-dag-plan.md +185 -0
  33. package/docs/planning/pi-integration-review.md +178 -0
  34. package/docs/planning/release-notes-v0.0.10.md +57 -0
  35. package/docs/planning/release-notes-v0.0.9.md +122 -0
  36. package/docs/planning/web-ui-pass-plan.md +213 -0
  37. package/docs/planning/website-readme-pass-plan.md +224 -0
  38. package/docs/reference/cli.md +255 -0
  39. package/docs/reference/embedding.md +7 -2
  40. package/docs/reference/library-api.md +22 -3
  41. package/docs/reference/memory.md +5 -5
  42. package/docs/reference/models.md +137 -0
  43. package/docs/reference/orpo-quickstart.md +6 -0
  44. package/docs/reference/server-api.md +187 -9
  45. package/docs/reference/server-config.md +43 -9
  46. package/docs/reference/training.md +31 -12
  47. package/package.json +1 -1
  48. package/src/anthropic.ts +22 -0
  49. package/src/assets/curve-designer.html +1 -1
  50. package/src/chat-template.ts +25 -7
  51. package/src/cli.ts +1077 -90
  52. package/src/config.ts +4 -1
  53. package/src/download.ts +99 -8
  54. package/src/embed.ts +77 -0
  55. package/src/eval/perplexity.ts +164 -0
  56. package/src/eval/runner.ts +15 -6
  57. package/src/eval/tasks/bfcl.ts +1 -1
  58. package/src/generate.ts +149 -18
  59. package/src/harness-pi.ts +43 -17
  60. package/src/index.ts +2 -0
  61. package/src/kv-store.ts +240 -98
  62. package/src/lora.ts +2 -2
  63. package/src/memory/article.ts +558 -0
  64. package/src/memory/chunk-validation.ts +150 -0
  65. package/src/memory/chunk.ts +305 -12
  66. package/src/memory/cluster.ts +344 -17
  67. package/src/memory/coverage.ts +134 -0
  68. package/src/memory/crosslink.ts +0 -0
  69. package/src/memory/db.ts +184 -45
  70. package/src/memory/entity.ts +203 -0
  71. package/src/memory/gate.ts +187 -0
  72. package/src/memory/history.ts +152 -0
  73. package/src/memory/ingest.ts +324 -11
  74. package/src/memory/model.ts +321 -0
  75. package/src/memory/normalize.ts +671 -0
  76. package/src/memory/parse.ts +22 -0
  77. package/src/memory/pipeline.ts +242 -30
  78. package/src/memory/prompts.ts +42 -0
  79. package/src/memory/query.ts +334 -0
  80. package/src/memory/reconcile.ts +734 -0
  81. package/src/memory/redirect.ts +108 -0
  82. package/src/memory/reindex.ts +325 -0
  83. package/src/memory/resolve.ts +307 -0
  84. package/src/memory/route.ts +272 -0
  85. package/src/memory/schedule.ts +6 -4
  86. package/src/memory/stages.ts +774 -0
  87. package/src/memory/synthesize.ts +1629 -20
  88. package/src/memory/tools.ts +446 -96
  89. package/src/memory/vault.ts +462 -35
  90. package/src/memory/wikify.ts +1057 -16
  91. package/src/mlx/array.ts +21 -0
  92. package/src/mlx/ffi.ts +3 -0
  93. package/src/mlx/ops.ts +72 -0
  94. package/src/model/compiled-decode.ts +43 -8
  95. package/src/model/factory.ts +29 -8
  96. package/src/model/flash-attention.ts +4 -2
  97. package/src/model/fused-decode-kernel.ts +14 -7
  98. package/src/model/fused-mlp-kernel.ts +169 -0
  99. package/src/model/fused-swiglu-kernel.ts +240 -0
  100. package/src/model/gemma4-base.ts +96 -7
  101. package/src/model/gemma4.ts +33 -2
  102. package/src/model/minicpm5.ts +106 -8
  103. package/src/model/qwen3-delta.ts +48 -1
  104. package/src/model/qwen3.ts +276 -0
  105. package/src/model/qwen3_5.ts +11 -2
  106. package/src/model/steel-linear-kernel.ts +96 -0
  107. package/src/model/support.ts +38 -8
  108. package/src/model/universal/archs.ts +332 -0
  109. package/src/model/universal/dense.ts +576 -0
  110. package/src/model/universal/modules.ts +163 -0
  111. package/src/model/universal/rope.ts +366 -0
  112. package/src/pi-provider.ts +1 -1
  113. package/src/pi-terminal.ts +29 -8
  114. package/src/pi-web.ts +19 -2
  115. package/src/prompt-cache.ts +55 -9
  116. package/src/quantize/config-writer.ts +1 -1
  117. package/src/registry.ts +219 -8
  118. package/src/responses.ts +23 -0
  119. package/src/sampler.ts +220 -9
  120. package/src/serve/batch-scheduler.ts +307 -83
  121. package/src/serve/generation-gateway.ts +156 -29
  122. package/src/server.ts +746 -73
  123. package/src/spec/drafter.ts +81 -49
  124. package/src/spec/dspark/data-dflash.ts +156 -0
  125. package/src/spec/dspark/data.ts +201 -0
  126. package/src/spec/dspark/generate-dflash.ts +193 -0
  127. package/src/spec/dspark/generate.ts +290 -0
  128. package/src/spec/dspark/loss.ts +213 -0
  129. package/src/spec/dspark/module-dflash.ts +361 -0
  130. package/src/spec/dspark/module.ts +540 -0
  131. package/src/spec/dspark/sample.ts +112 -0
  132. package/src/spec/generate.ts +40 -19
  133. package/src/ssd-cache.ts +213 -0
  134. package/src/status-page.html +1 -1
  135. package/src/tokenizer.ts +7 -0
  136. package/src/train/flash-cce.ts +81 -23
  137. package/src/train/fuse.ts +243 -0
  138. package/src/train/index.ts +3 -0
  139. package/src/train/job.ts +8 -0
  140. package/src/train/loss.ts +341 -40
  141. package/src/train/prefix-shared.ts +62 -11
  142. package/src/train/segmented.ts +215 -55
  143. package/src/train/trainer.ts +112 -38
  144. package/src/web/skills/memory/SKILL.md +40 -17
@@ -11,13 +11,22 @@
11
11
  // queue). One AsyncMutex enforces it: a serial run holds it for its duration;
12
12
  // the scheduler holds it for its whole active period (first admit → batch
13
13
  // empties). So batched requests run concurrently with EACH OTHER (the point),
14
- // but never alongside a serial-lane generation. A non-batchable request drains
15
- // the batch (waits for it to empty), then runs solo; queued batchable requests
16
- // wait for that solo run to finish. (mlx-lm's drain behavior.)
14
+ // but never alongside a serial-lane generation. A non-batchable request DRAINS
15
+ // the batch: while any serial-lane request is waiting, the scheduler stops
16
+ // admitting new rows (admissionHeld), finishes the running ones, and releases
17
+ // the mutex; the serial request runs solo, then kick() resumes admission.
18
+ // (mlx-lm's drain_batch semantics — without this, sustained batchable traffic
19
+ // starves the serial lane forever.) Non-generation GPU/model-state work (the
20
+ // curve endpoints, adapter mount/unmount) goes through runExclusive() so there
21
+ // is exactly ONE mutual-exclusion domain.
17
22
  //
18
- // v1 batchable gate (the rest → serial): batch>1 AND bf16 KV (no kv-quant — the
19
- // batched scheduler runs bf16; mixed-precision-KV batching is the novel-combo
20
- // L2 follow-up) AND no vision AND no LoRA adapters AND no repetition penalty
23
+ // v1 batchable gate (the rest → serial): batch>1 AND every model cache is a
24
+ // dynamic-B-capable type (KVCache | RotatingKVCache — mirrors mlx-lm
25
+ // server.py's all-caches-have-merge check; Qwen3.5's SSMCache has no
26
+ // merge/filter/temporalView, so hybrid models route serial until the ArraysCache
27
+ // port — batching-v2-plan item h) AND bf16 KV (no kv-quant — the batched
28
+ // scheduler runs bf16; mixed-precision-KV batching is the novel-combo L2
29
+ // follow-up) AND no vision AND no LoRA adapters AND no repetition penalty
21
30
  // (per-row logits processors are a later refinement) AND no user-fixed seed
22
31
  // (reproducibility ⇒ solo, matching mlx-lm's _is_batchable). Temperature /
23
32
  // top-p / top-k DO batch (each row samples with its own seed). Full-attention
@@ -26,10 +35,13 @@
26
35
  // in v1 (cachedTokens = 0).
27
36
 
28
37
  import { MlxArray } from "../mlx/array";
38
+ import * as ops from "../mlx/ops";
29
39
  import type { RuntimeModel } from "../model/factory";
30
40
  import { DiffusionGemmaModel } from "../model/diffusion-gemma";
31
- import type { GenerateOptions, GenerateStats } from "../generate";
32
- import { makeSampler, toLogprobs } from "../sampler";
41
+ import { KVCache, RotatingKVCache } from "../model/gemma4-base";
42
+ import { SSMCache } from "../model/qwen3-delta";
43
+ import type { GenerateOptions, GenerateStats, TokenLogprobs } from "../generate";
44
+ import { makeSampler, makeLogitsProcessors, toLogprobs } from "../sampler";
33
45
  import { BatchScheduler } from "./batch-scheduler";
34
46
 
35
47
  /** Async mutex: acquire() resolves to a release fn; releases run FIFO. */
@@ -46,8 +58,13 @@ class AsyncMutex {
46
58
 
47
59
  export type Vision = { embeddings: MlxArray; imageMask: MlxArray };
48
60
 
49
- /** Per-token sink: returning `false` halts this generation (stop sequence). */
50
- export type OnToken = (token: number) => void | boolean | Promise<void | boolean>;
61
+ /** Per-token sink: returning `false` halts this generation (stop sequence).
62
+ * `logprobs` is only populated on the serial lane when the request asked for
63
+ * logprobs capture (GenerateOptions.logprobs / topLogprobs). */
64
+ export type OnToken = (
65
+ token: number,
66
+ logprobs?: TokenLogprobs,
67
+ ) => void | boolean | Promise<void | boolean>;
51
68
 
52
69
  /** The serial lane — exactly today's runGeneration (prompt-cache reuse + the
53
70
  * generate() pipeline). The gateway calls it under the mutex. */
@@ -67,12 +84,29 @@ export interface RequestShape {
67
84
  userSeed: boolean;
68
85
  /** KV quantization is active (kvConfig/kvBits) — batched is bf16-only in v1. */
69
86
  kvQuant: boolean;
87
+ /** Any of the mlx-lm sampler/processor extensions is active: min_p, XTC,
88
+ * logit_bias, presence/frequency penalty. Safe v1: they ALL route to the
89
+ * serial lane alongside repetition penalty. min_p/XTC are per-row samplers
90
+ * and could batch (the batched lane already builds a per-row sampler);
91
+ * keeping them serial until the batched path grows per-row logits
92
+ * processors keeps one gate for the whole family. */
93
+ hasLogitsExtras: boolean;
94
+ /** The request asked for logprobs/top_logprobs capture. Batch-lane logprobs
95
+ * are deferred (the scheduler's per-row sampler doesn't capture or read
96
+ * back logprob arrays yet), so these route to the serial lane like the
97
+ * other mlx-lm request extensions above. */
98
+ wantsLogprobs: boolean;
70
99
  }
71
100
 
72
101
  export class GenerationGateway {
73
102
  readonly #mutex = new AsyncMutex();
74
103
  readonly #batch: number;
75
104
  #scheduler: BatchScheduler | null = null;
105
+ /** Serial-lane requests waiting for / holding the mutex. While > 0 the
106
+ * scheduler pauses admission (drain) so they can't be starved. */
107
+ #serialWaiters = 0;
108
+ /** Lazy, memoized: can this model's caches do the dynamic-B ops? */
109
+ #cacheBatchable: boolean | null = null;
76
110
 
77
111
  constructor(
78
112
  private readonly model: RuntimeModel,
@@ -92,21 +126,69 @@ export class GenerationGateway {
92
126
  return this.#scheduler?.activeRows ?? 0;
93
127
  }
94
128
 
129
+ /** Cache-capability gate (mirrors mlx-lm server.py's all-caches-have-merge
130
+ * check): the scheduler's dynamic-B ops (temporalView/merge/filter) exist
131
+ * on KVCache, RotatingKVCache, and SSMCache (hybrid gated-DeltaNet models
132
+ * — Qwen3.5 — batch via SSMCache.mergeRows/filter; MLX_BUN_BATCH_SSM=0 is
133
+ * the kill switch back to serial routing). */
134
+ #modelCachesBatchable(): boolean {
135
+ if (this.#cacheBatchable === null) {
136
+ const ssmOk = process.env.MLX_BUN_BATCH_SSM !== "0";
137
+ const proto = this.model.makeCache(); // fresh caches hold no buffers
138
+ this.#cacheBatchable = proto.every(
139
+ (c) =>
140
+ c instanceof KVCache ||
141
+ c instanceof RotatingKVCache ||
142
+ (ssmOk && c instanceof SSMCache),
143
+ );
144
+ for (const c of proto) c.dispose();
145
+ }
146
+ return this.#cacheBatchable;
147
+ }
148
+
95
149
  /** Decide whether a request joins the batch or runs serially. */
96
150
  willBatch(shape: RequestShape): boolean {
97
151
  // DiffusionGemma is non-autoregressive — the batch scheduler assumes the AR
98
152
  // KV-cache decode path, so it always runs serially through generate().
99
153
  if (this.model instanceof DiffusionGemmaModel) return false;
154
+ if (!this.batchingEnabled) return false;
155
+ if (!this.#modelCachesBatchable()) return false;
156
+ // repetitionPenalty / logits extras (min_p, XTC, logit_bias,
157
+ // presence/frequency penalties) batch: the per-row sampler folds in the
158
+ // same makeSampler/makeLogitsProcessors closures the serial lane runs,
159
+ // over a per-row device-side token history. (Load-bearing beyond opt-in
160
+ // knobs: some models — Qwen3.5 — ship a default repetition penalty in
161
+ // generation_config.json, which used to route EVERY request serial.)
100
162
  return (
101
- this.batchingEnabled &&
102
163
  !shape.hasVision &&
103
164
  !shape.hasAdapters &&
104
- !shape.hasRepetitionPenalty &&
165
+ !shape.wantsLogprobs &&
105
166
  !shape.userSeed &&
106
167
  !shape.kvQuant
107
168
  );
108
169
  }
109
170
 
171
+ /** Run `fn` with exclusive ownership of the GPU + shared model state (the
172
+ * serial lane's lock). THE single mutual-exclusion domain: the serial
173
+ * generation path, the curve /generate + /signal endpoints, and adapter
174
+ * mount/unmount all come through here, so nothing runs concurrently with
175
+ * batched decode steps (or with each other). Registers as a serial waiter
176
+ * so the scheduler drains (stops admitting) until `fn` has run. */
177
+ async runExclusive<T>(fn: () => Promise<T>): Promise<T> {
178
+ this.#serialWaiters++;
179
+ try {
180
+ const release = await this.#mutex.acquire();
181
+ try {
182
+ return await fn();
183
+ } finally {
184
+ release();
185
+ }
186
+ } finally {
187
+ this.#serialWaiters--;
188
+ if (this.#serialWaiters === 0) this.#scheduler?.kick(); // resume admission
189
+ }
190
+ }
191
+
110
192
  /** Run one generation on the appropriate lane. onToken is invoked per emitted
111
193
  * token (its `false` halts); resolves with stats when the generation ends. */
112
194
  async run(
@@ -117,32 +199,74 @@ export class GenerationGateway {
117
199
  shape: RequestShape,
118
200
  ): Promise<GenerateStats> {
119
201
  if (!this.willBatch(shape)) {
120
- const release = await this.#mutex.acquire();
121
- try {
122
- return await this.serialRun(promptIds, options, onToken, vision);
123
- } finally {
124
- release();
125
- }
202
+ // Serial decode is an unbroken microtask chain (FFI + generator
203
+ // resumes): without a periodic macrotask hop the event loop starves for
204
+ // the WHOLE generation — /stats, /health, and new-connection accepts
205
+ // stall until the request finishes (measured 2.5 s on a 512-token cpm5
206
+ // run). Rate-limited to ≥25 ms — the SSE flush hop's budget, ~0.1% of
207
+ // decode — and applied HERE so every serial caller (streaming or not,
208
+ // all four API surfaces) is covered. The batch lane needs nothing: its
209
+ // drive loop yields per step (measured /stats ~8 ms mid-generation).
210
+ let lastHop = performance.now();
211
+ const hop = (): Promise<void> => new Promise<void>((r) => setImmediate(r));
212
+ const hoppingOnToken: OnToken = (token, lp) => {
213
+ const r = onToken(token, lp);
214
+ if (r === false) return false;
215
+ if (performance.now() - lastHop < 25) return r;
216
+ lastHop = performance.now();
217
+ if (r instanceof Promise) return r.then((v) => (v === false ? false : hop().then(() => v)));
218
+ return hop().then(() => r);
219
+ };
220
+ return this.runExclusive(() => this.serialRun(promptIds, options, hoppingOnToken, vision));
126
221
  }
127
222
 
128
- // Per-row sampler, mirroring generate()'s sampleStep (no logits processors:
129
- // repetition penalty was excluded by willBatch). Greedy (temperature 0) is
130
- // argmax; temp>0 uses this request's own seed → independent per-row RNG.
223
+ // Per-row sampler, mirroring generate()'s sampleStep: logits processors
224
+ // (logit_bias / repetition / presence / frequency penalties — the same
225
+ // device-side closures the serial lane runs) fold in over a per-row token
226
+ // history seeded with the prompt, then the sampler. Greedy (temperature 0)
227
+ // is argmax; temp>0 uses this request's own seed → independent per-row RNG.
131
228
  const sampler = makeSampler(options);
229
+ const processors = makeLogitsProcessors(options);
230
+ // Device-side history (prompt + generated), maintained only when a
231
+ // processor needs it — generate()'s pushHistory, one per row. The sample
232
+ // closure is called once per sampled token in order, so extending it
233
+ // inside the closure keeps history exact.
234
+ let history: MlxArray | null = null;
235
+ if (processors.length > 0) history = ops.fromInt32(promptIds, [promptIds.length]);
132
236
  const sample = (logits1V: MlxArray, step: number): MlxArray => {
133
- const lp = toLogprobs(logits1V);
237
+ let cur = logits1V; // caller-owned; only intermediates are disposed here
238
+ for (const p of processors) {
239
+ const next = p(history, cur);
240
+ if (cur !== logits1V) cur.dispose();
241
+ cur = next;
242
+ }
243
+ const lp = toLogprobs(cur);
244
+ if (cur !== logits1V) cur.dispose();
134
245
  const tok = sampler(lp, step);
135
246
  lp.dispose();
247
+ if (history) {
248
+ const t1 = ops.reshape(tok, [1]);
249
+ const prev = history;
250
+ history = ops.concatAxis([prev, t1], 0);
251
+ prev.dispose();
252
+ t1.dispose();
253
+ }
136
254
  return tok;
137
255
  };
138
256
 
139
- const st = await this.#ensureScheduler().submit({
140
- promptIds,
141
- maxTokens: options.maxTokens ?? 512,
142
- eosTokenIds: options.eosTokenIds ?? this.model.config.eosTokenIds,
143
- sample,
144
- onToken,
145
- });
257
+ let st;
258
+ try {
259
+ st = await this.#ensureScheduler().submit({
260
+ promptIds,
261
+ maxTokens: options.maxTokens ?? 512,
262
+ eosTokenIds: options.eosTokenIds ?? this.model.config.eosTokenIds,
263
+ sample,
264
+ onToken,
265
+ });
266
+ } finally {
267
+ history?.dispose();
268
+ history = null;
269
+ }
146
270
 
147
271
  return {
148
272
  promptTokens: st.promptTokens,
@@ -158,6 +282,9 @@ export class GenerationGateway {
158
282
  this.#scheduler = new BatchScheduler(this.model, {
159
283
  maxBatch: this.#batch,
160
284
  lock: { acquire: () => this.#mutex.acquire() },
285
+ // Drain: pause admission while any serial-lane request waits; the
286
+ // runExclusive finally-block kick()s the loop back awake.
287
+ admissionHeld: () => this.#serialWaiters > 0,
161
288
  });
162
289
  return this.#scheduler;
163
290
  }