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
|
@@ -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
|
|
15
|
-
// the batch
|
|
16
|
-
//
|
|
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
|
|
19
|
-
//
|
|
20
|
-
//
|
|
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
|
|
32
|
-
import {
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
|
129
|
-
// repetition
|
|
130
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
}
|