mlx-bun 0.0.7 → 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.
- package/README.md +112 -20
- 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/diffusion-gemma-port.md +602 -0
- 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/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/spec-decode-larger-targets.md +229 -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 +262 -0
- package/docs/investigations/dspark-handoff.md +58 -0
- package/docs/investigations/kernel-perf-review-2026-07.md +92 -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.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 +40 -9
- package/docs/reference/training.md +25 -11
- 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 +1029 -86
- package/src/config.ts +54 -3
- package/src/diffusion/diffusion-generate.ts +436 -0
- package/src/download.ts +99 -8
- package/src/embed.ts +77 -0
- package/src/eval/perplexity.ts +164 -0
- package/src/eval/runner.ts +19 -7
- package/src/eval/tasks/bfcl.ts +1 -1
- package/src/generate.ts +196 -19
- package/src/harness-pi.ts +43 -17
- package/src/index.ts +2 -0
- 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/ffi.ts +11 -0
- package/src/mlx/ops.ts +113 -0
- package/src/model/compiled-decode.ts +43 -8
- package/src/model/diffusion-gemma.ts +1026 -0
- package/src/model/factory.ts +35 -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 +62 -6
- package/src/model/gemma4.ts +33 -2
- package/src/model/minicpm5.ts +106 -8
- package/src/model/qwen3.ts +276 -0
- package/src/model/steel-linear-kernel.ts +96 -0
- package/src/model/support.ts +45 -7
- 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/quantize/config-writer.ts +1 -1
- package/src/registry.ts +215 -7
- package/src/responses.ts +23 -0
- package/src/sampler.ts +220 -9
- package/src/serve/batch-scheduler.ts +272 -80
- package/src/serve/generation-gateway.ts +90 -18
- package/src/server.ts +643 -50
- 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/status-page.html +1 -1
- package/src/tokenizer.ts +7 -0
- package/src/train/diffusion-lora.ts +216 -0
- package/src/train/flash-cce.ts +70 -15
- 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 +289 -30
- package/src/train/prefix-shared.ts +33 -5
- package/src/train/segmented.ts +67 -11
- package/src/train/trainer.ts +92 -28
- package/src/vision/diffusion-vision.ts +620 -0
- package/src/web/skills/memory/SKILL.md +40 -17
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// loop). Owns ONE running batch and drives it forward one decode step at a
|
|
3
3
|
// time, admitting waiting requests and evicting finished ones between steps —
|
|
4
4
|
// iteration-level (continuous) scheduling, not static batching. See
|
|
5
|
-
// docs/design/parallel-slots.md.
|
|
5
|
+
// docs/design/parallel-slots.md and docs/design/batching-v2-plan.md.
|
|
6
6
|
//
|
|
7
7
|
// The numerically-hard parts are verified primitives:
|
|
8
8
|
// - the batched FORWARD (per-row RoPE/mask) is bit-parity with mlx-lm B=N
|
|
@@ -21,12 +21,35 @@
|
|
|
21
21
|
// mask). Full layers share one leftPad/offset (all rows advance together); the
|
|
22
22
|
// rotating caches self-track per-row leftPad/offset as the ring wraps. The
|
|
23
23
|
// per-row absolute position stays consistent across both (full: offset-leftPad;
|
|
24
|
-
// rot: offsetArr) — see docs/design/parallel-slots.md.
|
|
24
|
+
// rot: offsetArr) — see docs/design/parallel-slots.md. Models whose caches
|
|
25
|
+
// don't support the dynamic-B ops (e.g. Qwen3.5's SSMCache) never reach this
|
|
26
|
+
// scheduler — GenerationGateway.willBatch gates on cache capability.
|
|
27
|
+
//
|
|
28
|
+
// Engine mechanics (the serial decode loop's hygiene, transplanted —
|
|
29
|
+
// batching-v2-plan step 3):
|
|
30
|
+
// - PIPELINED decode: each step builds the NEXT step's graph from the
|
|
31
|
+
// still-unread sampled-token array (asyncEval), then reads the PREVIOUS
|
|
32
|
+
// step's tokens while the new step computes — mlx-lm GenerationBatch._step.
|
|
33
|
+
// The pipeline is flushed (read + emit) before a join merges, so admission
|
|
34
|
+
// never has to reconcile an in-flight token array with a new row.
|
|
35
|
+
// - clearCache every 256 steps (serial's cadence; mlx-lm batched uses 512),
|
|
36
|
+
// not every step — per-step clears trashed the buffer pool each token.
|
|
37
|
+
// - CHUNKED, INTERLEAVED admission: a joiner prefills prefillChunkSize
|
|
38
|
+
// tokens per loop iteration with one batch decode step run in between, so
|
|
39
|
+
// running rows stall at most one chunk per joiner (mlx-lm interleaves the
|
|
40
|
+
// same way), and the prefill transient stays bounded.
|
|
41
|
+
// - Failure containment: one row's onToken throwing evicts THAT row (its
|
|
42
|
+
// promise rejects); siblings keep decoding (mlx-lm `remove` semantics). A
|
|
43
|
+
// forward/sampling error still drops the whole batch (can't be attributed
|
|
44
|
+
// to a row).
|
|
25
45
|
//
|
|
26
46
|
// Bun-async, NO threads: a single detached driver loop owns the GPU for batched
|
|
27
47
|
// mode (an ExclusiveLock keeps the serial fallback off the GPU concurrently).
|
|
48
|
+
// When `admissionHeld` reports a waiting serial-lane request, the loop stops
|
|
49
|
+
// admitting, finishes the running rows, and releases the lock so the serial
|
|
50
|
+
// request runs (mlx-lm's drain_batch) — resumed via kick().
|
|
28
51
|
// Joins re-merge the whole batch; the keep-the-running-batch `extend`
|
|
29
|
-
// optimization is a later refinement.
|
|
52
|
+
// optimization is a later refinement (batching-v2-plan item a).
|
|
30
53
|
|
|
31
54
|
import { MlxArray } from "../mlx/array";
|
|
32
55
|
import * as ops from "../mlx/ops";
|
|
@@ -36,10 +59,15 @@ import { BatchedDecodeMaskCache, mergeKVRows, filterKVRows } from "../model/batc
|
|
|
36
59
|
import { BatchedRotatingCache } from "../model/batched-rotating";
|
|
37
60
|
import type { RuntimeModel } from "../model/factory";
|
|
38
61
|
|
|
62
|
+
/** Decode-pipeline kill switch (read once at load, like the serial loop's
|
|
63
|
+
* MLX_BUN_COMPILED_DECODE): 1 ⇒ read each step's tokens synchronously. */
|
|
64
|
+
const NO_PIPELINE = process.env.MLX_BUN_BATCH_NO_PIPELINE === "1";
|
|
65
|
+
|
|
39
66
|
/** A token sampler for one row: (logits [1,V], step) → token array [1] on
|
|
40
67
|
* device. Greedy is `(l) => ops.argmaxAxis(l, -1)`; richer closures fold in
|
|
41
68
|
* temperature / top-p / logits processors + history (built per request from
|
|
42
|
-
* its sampler options, exactly like generate()'s sampleStep).
|
|
69
|
+
* its sampler options, exactly like generate()'s sampleStep). Called once per
|
|
70
|
+
* step 0..maxTokens-1 for this row (never beyond maxTokens). */
|
|
43
71
|
export type RowSampler = (logits1V: MlxArray, step: number) => MlxArray;
|
|
44
72
|
|
|
45
73
|
export interface BatchRequest {
|
|
@@ -49,8 +77,9 @@ export interface BatchRequest {
|
|
|
49
77
|
sample: RowSampler;
|
|
50
78
|
/** Called per emitted (non-EOS) token, in order. Returning `false` halts this
|
|
51
79
|
* row (a decoded-text stop sequence fired) — matches generate()'s onToken
|
|
52
|
-
* contract. EOS terminates the row WITHOUT an onToken call.
|
|
53
|
-
*
|
|
80
|
+
* contract. EOS terminates the row WITHOUT an onToken call. Throwing evicts
|
|
81
|
+
* THIS row only (its submit promise rejects; siblings continue). May be
|
|
82
|
+
* async; keep it cheap — it runs inline in the step loop. */
|
|
54
83
|
onToken: (token: number) => void | boolean | Promise<void | boolean>;
|
|
55
84
|
}
|
|
56
85
|
|
|
@@ -67,11 +96,20 @@ interface Row {
|
|
|
67
96
|
req: BatchRequest;
|
|
68
97
|
resolve: (s: BatchStats) => void;
|
|
69
98
|
reject: (e: unknown) => void;
|
|
70
|
-
current: number; // last emitted token, fed at the next step
|
|
99
|
+
current: number; // last emitted token, fed at the next step (pipeline cold)
|
|
71
100
|
generated: number; // tokens emitted so far (incl. a terminating EOS)
|
|
101
|
+
sampled: number; // sample() calls so far (leads `generated` by 1 in-pipeline)
|
|
72
102
|
promptTokens: number;
|
|
73
103
|
}
|
|
74
104
|
|
|
105
|
+
/** A joiner mid-prefill: `pos` prompt tokens are already in `solo`. Advanced
|
|
106
|
+
* one chunk per loop iteration, interleaved with batch decode steps. */
|
|
107
|
+
interface PrefillState {
|
|
108
|
+
row: Row;
|
|
109
|
+
solo: Cache[];
|
|
110
|
+
pos: number;
|
|
111
|
+
}
|
|
112
|
+
|
|
75
113
|
/** Held while the batch is active; the serial fallback acquires the same lock. */
|
|
76
114
|
export interface ExclusiveLock {
|
|
77
115
|
acquire(): Promise<() => void>;
|
|
@@ -81,6 +119,12 @@ export interface BatchSchedulerOptions {
|
|
|
81
119
|
/** Max rows in the running batch (mlx-lm `--decode-concurrency`). */
|
|
82
120
|
maxBatch: number;
|
|
83
121
|
lock?: ExclusiveLock;
|
|
122
|
+
/** Drain signal: while true, no NEW rows are admitted (running rows finish,
|
|
123
|
+
* the lock is released) so a waiting serial-lane request can run — mlx-lm's
|
|
124
|
+
* drain_batch. Pair with kick() when it flips back to false. */
|
|
125
|
+
admissionHeld?: () => boolean;
|
|
126
|
+
/** Joiner prefill chunk length (default 2048, the serial loop's constant). */
|
|
127
|
+
prefillChunkSize?: number;
|
|
84
128
|
}
|
|
85
129
|
|
|
86
130
|
type LayerInner = KVCache | BatchedRotatingCache;
|
|
@@ -91,19 +135,29 @@ export class BatchScheduler {
|
|
|
91
135
|
#inners: LayerInner[] | null = null; // per-layer batched KV; null when empty
|
|
92
136
|
#fullLeftPad: number[] = []; // per-row padding for FULL layers (rot self-tracks)
|
|
93
137
|
#pending: Row[] = [];
|
|
138
|
+
#prefill: PrefillState | null = null; // the (single) joiner mid-prefill
|
|
139
|
+
/** Sampled-but-unread token array [B], aligned with #running — the decode
|
|
140
|
+
* pipeline register. Filtered/disposed alongside the batched KV. */
|
|
141
|
+
#pendingToks: MlxArray | null = null;
|
|
142
|
+
#steps = 0; // decode-step counter (clearCache cadence)
|
|
94
143
|
#looping = false;
|
|
95
144
|
#wake: (() => void) | null = null;
|
|
96
145
|
readonly #maxBatch: number;
|
|
97
146
|
readonly #lock: ExclusiveLock | undefined;
|
|
147
|
+
readonly #admissionHeld: (() => boolean) | undefined;
|
|
148
|
+
readonly #prefillChunkSize: number;
|
|
98
149
|
readonly #kinds: ("full" | "rot")[]; // per-layer attention type
|
|
99
150
|
readonly #rotMaxSize: number[]; // per-layer sliding window (rot layers only)
|
|
100
151
|
|
|
101
152
|
constructor(private readonly model: RuntimeModel, opts: BatchSchedulerOptions) {
|
|
102
153
|
this.#maxBatch = Math.max(1, Math.floor(opts.maxBatch));
|
|
103
154
|
this.#lock = opts.lock;
|
|
155
|
+
this.#admissionHeld = opts.admissionHeld;
|
|
156
|
+
this.#prefillChunkSize = Math.max(1, Math.floor(opts.prefillChunkSize ?? 2048));
|
|
104
157
|
const proto = model.makeCache(); // fresh caches hold no buffers
|
|
105
158
|
this.#kinds = proto.map((c) => (c instanceof RotatingKVCache ? "rot" : "full"));
|
|
106
159
|
this.#rotMaxSize = proto.map((c) => (c instanceof RotatingKVCache ? c.maxSize : 0));
|
|
160
|
+
for (const c of proto) c.dispose();
|
|
107
161
|
}
|
|
108
162
|
|
|
109
163
|
get activeRows(): number {
|
|
@@ -115,12 +169,17 @@ export class BatchScheduler {
|
|
|
115
169
|
return new Promise<BatchStats>((resolve, reject) => {
|
|
116
170
|
this.#pending.push({
|
|
117
171
|
req, resolve, reject,
|
|
118
|
-
current: 0, generated: 0, promptTokens: req.promptIds.length,
|
|
172
|
+
current: 0, generated: 0, sampled: 0, promptTokens: req.promptIds.length,
|
|
119
173
|
});
|
|
120
174
|
this.#ensureLoop();
|
|
121
175
|
});
|
|
122
176
|
}
|
|
123
177
|
|
|
178
|
+
/** Wake the driver loop (e.g. after admissionHeld flips back to false). */
|
|
179
|
+
kick(): void {
|
|
180
|
+
this.#ensureLoop();
|
|
181
|
+
}
|
|
182
|
+
|
|
124
183
|
#ensureLoop(): void {
|
|
125
184
|
if (this.#wake) { this.#wake(); return; }
|
|
126
185
|
if (this.#looping) return;
|
|
@@ -132,29 +191,58 @@ export class BatchScheduler {
|
|
|
132
191
|
let release: (() => void) | null = null;
|
|
133
192
|
try {
|
|
134
193
|
while (true) {
|
|
135
|
-
|
|
136
|
-
|
|
194
|
+
const held = this.#admissionHeld?.() === true;
|
|
195
|
+
// Work = running rows, an in-flight prefill (finish it even under
|
|
196
|
+
// drain — the row is already half-admitted), or admissible pending.
|
|
197
|
+
const hasWork =
|
|
198
|
+
this.#running.length > 0 ||
|
|
199
|
+
this.#prefill !== null ||
|
|
200
|
+
(!held && this.#pending.length > 0);
|
|
201
|
+
if (!hasWork) {
|
|
202
|
+
if (release) { release(); release = null; } // let the serial lane in
|
|
203
|
+
await new Promise<void>((r) => { this.#wake = r; });
|
|
204
|
+
this.#wake = null;
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
if (!release && this.#lock) release = await this.#lock.acquire();
|
|
208
|
+
|
|
209
|
+
// Start at most one joiner prefill (drain-aware); it advances one
|
|
210
|
+
// chunk per iteration, interleaved with the decode step below.
|
|
211
|
+
if (!this.#prefill && !held &&
|
|
212
|
+
this.#pending.length > 0 && this.#running.length < this.#maxBatch)
|
|
213
|
+
this.#prefill = { row: this.#pending.shift()!, solo: this.model.makeCache(), pos: 0 };
|
|
137
214
|
|
|
138
|
-
|
|
139
|
-
const
|
|
215
|
+
if (this.#prefill) {
|
|
216
|
+
const p = this.#prefill;
|
|
140
217
|
try {
|
|
141
|
-
await this.#
|
|
218
|
+
if (await this.#prefillChunk(p)) this.#prefill = null;
|
|
142
219
|
} catch (e) {
|
|
143
|
-
row
|
|
220
|
+
// Prefill/admission failure is per-row: reject the joiner, keep
|
|
221
|
+
// the running batch.
|
|
222
|
+
this.#prefill = null;
|
|
223
|
+
for (const c of p.solo) c.dispose();
|
|
224
|
+
p.row.reject(e);
|
|
144
225
|
}
|
|
145
226
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
227
|
+
|
|
228
|
+
// Burst admission: when a joiner just COMPLETED and more batchable
|
|
229
|
+
// requests are queued, admit them before the next decode step (the
|
|
230
|
+
// admit-all-then-step grouping the mlx-lm goldens gate). Long prompts
|
|
231
|
+
// still interleave: a mid-prefill joiner leaves #prefill non-null, so
|
|
232
|
+
// the loop falls through to run a decode step between chunks.
|
|
233
|
+
if (this.#prefill === null && !held &&
|
|
234
|
+
this.#pending.length > 0 && this.#running.length < this.#maxBatch)
|
|
151
235
|
continue;
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
236
|
+
|
|
237
|
+
if (this.#running.length > 0) {
|
|
238
|
+
try {
|
|
239
|
+
await this.#step();
|
|
240
|
+
} catch (e) {
|
|
241
|
+
// A forward/sampling error can't be attributed to one row — drop
|
|
242
|
+
// the batch. (A row's onToken error is contained in #emitRows.)
|
|
243
|
+
for (const row of this.#running) row.reject(e);
|
|
244
|
+
this.#applyFilter([]);
|
|
245
|
+
}
|
|
158
246
|
}
|
|
159
247
|
await new Promise<void>((r) => setImmediate(r));
|
|
160
248
|
}
|
|
@@ -164,40 +252,69 @@ export class BatchScheduler {
|
|
|
164
252
|
}
|
|
165
253
|
}
|
|
166
254
|
|
|
167
|
-
/**
|
|
168
|
-
*
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
const
|
|
255
|
+
/** Advance a joiner's solo prefill by one chunk. Non-final chunks forward +
|
|
256
|
+
* eval the cache and return false (the caller interleaves a decode step).
|
|
257
|
+
* The final chunk samples token 0, emits it, and — if the row survives —
|
|
258
|
+
* merges it into the running batch; returns true (admission complete). */
|
|
259
|
+
async #prefillChunk(p: PrefillState): Promise<boolean> {
|
|
260
|
+
const prompt = p.row.req.promptIds;
|
|
261
|
+
if (prompt.length - p.pos > this.#prefillChunkSize) {
|
|
262
|
+
const chunk = prompt.slice(p.pos, p.pos + this.#prefillChunkSize);
|
|
263
|
+
const ids = ops.fromInt32(chunk, [1, chunk.length]);
|
|
264
|
+
const h = this.model.forwardHidden(ids, p.solo);
|
|
265
|
+
ids.dispose();
|
|
266
|
+
h.dispose(); // logits never computed for non-final chunks
|
|
267
|
+
ops.evalAll(p.solo.flatMap((c) => c.state()));
|
|
268
|
+
clearCache(); // serial prefill's per-chunk clear (generate.ts)
|
|
269
|
+
p.pos += this.#prefillChunkSize;
|
|
270
|
+
return false;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Final chunk: slice the LAST hidden position BEFORE the lm_head — running
|
|
274
|
+
// logitsFromHidden on the whole [1,Lp,H] hidden materializes a [1,Lp,V]
|
|
275
|
+
// transient (~4.3 GB bf16 at Gemma V=262k, 8k prompt). Same reorder as the
|
|
276
|
+
// serial path (generate.ts prefill).
|
|
277
|
+
const chunk = prompt.slice(p.pos);
|
|
278
|
+
const ids = ops.fromInt32(chunk, [1, chunk.length]);
|
|
279
|
+
const h = this.model.forwardHidden(ids, p.solo);
|
|
173
280
|
ids.dispose();
|
|
174
|
-
const
|
|
281
|
+
const [, Lc, H] = h.shape as [number, number, number];
|
|
282
|
+
const hLast = h.slice([0, Lc - 1, 0], [1, Lc, H]);
|
|
175
283
|
h.dispose();
|
|
176
|
-
const
|
|
177
|
-
|
|
284
|
+
const lg = this.model.logitsFromHidden(hLast); // [1,1,V]
|
|
285
|
+
hLast.dispose();
|
|
286
|
+
const V = lg.shape[2]!;
|
|
287
|
+
const last2 = ops.reshape(lg, [1, V]);
|
|
178
288
|
lg.dispose();
|
|
179
|
-
const
|
|
180
|
-
last.dispose();
|
|
181
|
-
const tok = this.#readToken(row.req.sample(last2, 0));
|
|
289
|
+
const tok = this.#readToken(p.row.req.sample(last2, 0));
|
|
182
290
|
last2.dispose();
|
|
183
|
-
row.
|
|
184
|
-
|
|
291
|
+
p.row.sampled = 1;
|
|
292
|
+
p.row.generated = 1;
|
|
293
|
+
clearCache(); // drop the prefill transients (serial's token-0 clear)
|
|
185
294
|
|
|
186
|
-
const stop = await this.#emit(row, tok);
|
|
295
|
+
const stop = await this.#emit(p.row, tok);
|
|
187
296
|
if (stop !== "continue") {
|
|
188
|
-
for (const c of solo) c.dispose();
|
|
189
|
-
this.#finish(row, stop);
|
|
190
|
-
return;
|
|
297
|
+
for (const c of p.solo) c.dispose();
|
|
298
|
+
this.#finish(p.row, stop);
|
|
299
|
+
return true;
|
|
191
300
|
}
|
|
301
|
+
await this.#mergeJoiner(p);
|
|
302
|
+
return true;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/** Merge a fully-prefilled joiner with the running batch, layer by layer
|
|
306
|
+
* (re-merge; `extend` is the later refinement). Flushes the decode pipeline
|
|
307
|
+
* first so the row set is settled and the next step starts cold. */
|
|
308
|
+
async #mergeJoiner(p: PrefillState): Promise<void> {
|
|
309
|
+
await this.#flushPipeline();
|
|
192
310
|
|
|
193
|
-
// Re-merge the running batch + the new row, layer by layer.
|
|
194
311
|
const prev = this.#inners;
|
|
195
312
|
const prevPad = this.#fullLeftPad;
|
|
196
313
|
const B = this.#running.length;
|
|
197
314
|
const newInners: LayerInner[] = [];
|
|
198
315
|
let newFullPad = this.#fullLeftPad;
|
|
199
316
|
for (let layer = 0; layer < this.#kinds.length; layer++) {
|
|
200
|
-
const soloC = solo[layer] as KVCache | RotatingKVCache;
|
|
317
|
+
const soloC = p.solo[layer] as KVCache | RotatingKVCache;
|
|
201
318
|
const [sk, sv] = soloC.temporalView();
|
|
202
319
|
const newRow: Row1 = { keys: sk, values: sv };
|
|
203
320
|
if (this.#kinds[layer] === "rot") {
|
|
@@ -248,55 +365,120 @@ export class BatchScheduler {
|
|
|
248
365
|
}
|
|
249
366
|
}
|
|
250
367
|
if (prev) for (const c of prev) c.dispose();
|
|
251
|
-
for (const c of solo) c.dispose();
|
|
368
|
+
for (const c of p.solo) c.dispose();
|
|
252
369
|
this.#inners = newInners;
|
|
253
370
|
this.#fullLeftPad = newFullPad;
|
|
254
|
-
this.#running.push(row);
|
|
371
|
+
this.#running.push(p.row);
|
|
255
372
|
}
|
|
256
373
|
|
|
257
|
-
/** One batched decode step
|
|
374
|
+
/** One PIPELINED batched decode step (mlx-lm GenerationBatch._step):
|
|
375
|
+
* 1. forward all rows from the UNREAD pending token array (or, pipeline
|
|
376
|
+
* cold, from the rows' last emitted tokens), sample each live row's next
|
|
377
|
+
* token on its [1,V] slice, asyncEval the new [B] token array;
|
|
378
|
+
* 2. THEN sync-read the previous step's tokens (overlapping the readback
|
|
379
|
+
* with this step's compute), emit them, and evict finished rows.
|
|
380
|
+
* Rows that finish get one extra harmless KV write from the already-built
|
|
381
|
+
* step; filter drops the row (mlx-lm behaves identically). Length-finished
|
|
382
|
+
* rows are known in advance and are NOT sampled (placeholder slot). */
|
|
258
383
|
async #step(): Promise<void> {
|
|
259
384
|
const rows = this.#running;
|
|
260
385
|
const B = rows.length;
|
|
261
386
|
const inners = this.#inners!;
|
|
262
|
-
|
|
263
|
-
//
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
const
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
387
|
+
|
|
388
|
+
// A row is live if it still needs tokens sampled; a row whose pending
|
|
389
|
+
// unread token is its last (sampled == maxTokens) only awaits emission.
|
|
390
|
+
const anyLive = rows.some((r) => r.sampled < r.req.maxTokens);
|
|
391
|
+
let nextToks: MlxArray | null = null;
|
|
392
|
+
if (anyLive) {
|
|
393
|
+
// Per-layer forward cache: rot layers use the persistent
|
|
394
|
+
// BatchedRotatingCache directly; full layers get a fresh
|
|
395
|
+
// BatchedDecodeMaskCache wrapper.
|
|
396
|
+
const fwd: Cache[] = inners.map((c) =>
|
|
397
|
+
c instanceof BatchedRotatingCache ? c : new BatchedDecodeMaskCache(c, B, this.#fullLeftPad, null),
|
|
398
|
+
);
|
|
399
|
+
try {
|
|
400
|
+
const ids = this.#pendingToks
|
|
401
|
+
? ops.reshape(this.#pendingToks, [B, 1]) // feed the unread tokens
|
|
402
|
+
: ops.fromInt32(rows.map((r) => r.current), [B, 1]); // pipeline cold
|
|
403
|
+
const h = this.model.forwardHidden(ids, fwd);
|
|
404
|
+
ids.dispose();
|
|
405
|
+
const lg = this.model.logitsFromHidden(h); // [B,1,V]
|
|
406
|
+
h.dispose();
|
|
407
|
+
const V = lg.shape[2]!;
|
|
408
|
+
const sampled: MlxArray[] = [];
|
|
409
|
+
for (let b = 0; b < B; b++) {
|
|
410
|
+
const row = rows[b]!;
|
|
411
|
+
if (row.sampled >= row.req.maxTokens) {
|
|
412
|
+
// Length-doomed row: evicted right after the emission below ever
|
|
413
|
+
// uses this slot as input — placeholder keeps the [B] alignment.
|
|
414
|
+
sampled.push(ops.fromInt32([0], [1]));
|
|
415
|
+
continue;
|
|
416
|
+
}
|
|
417
|
+
const rl = lg.slice([b, 0, 0], [b + 1, 1, V]);
|
|
418
|
+
const rl2 = ops.reshape(rl, [1, V]);
|
|
419
|
+
rl.dispose();
|
|
420
|
+
sampled.push(row.req.sample(rl2, row.sampled));
|
|
421
|
+
row.sampled++;
|
|
422
|
+
rl2.dispose();
|
|
423
|
+
}
|
|
424
|
+
lg.dispose();
|
|
425
|
+
nextToks = ops.concatAxis(sampled, 0); // [B]
|
|
426
|
+
for (const t of sampled) t.dispose();
|
|
427
|
+
ops.asyncEvalAll([nextToks]); // dispatch; read NEXT iteration
|
|
428
|
+
} finally {
|
|
429
|
+
// Free the step's RoPE arrays; do NOT dispose (full wrappers would free
|
|
430
|
+
// their persistent inner; rot caches persist across steps).
|
|
431
|
+
for (const c of fwd) (c as { releaseRopeArr?: () => void }).releaseRopeArr?.();
|
|
282
432
|
}
|
|
283
|
-
lg.dispose();
|
|
284
|
-
const tokArr = ops.concatAxis(sampled, 0); // [B]
|
|
285
|
-
for (const t of sampled) t.dispose();
|
|
286
|
-
toks = [...tokArr.toFloat32()].map((x) => Math.round(x));
|
|
287
|
-
tokArr.dispose();
|
|
288
|
-
} finally {
|
|
289
|
-
// Free the step's RoPE arrays; do NOT dispose (full wrappers would free
|
|
290
|
-
// their persistent inner; rot caches persist across steps).
|
|
291
|
-
for (const c of fwd) (c as { releaseRopeArr?: () => void }).releaseRopeArr?.();
|
|
292
433
|
}
|
|
293
|
-
|
|
434
|
+
this.#steps++;
|
|
435
|
+
if (this.#steps % 256 === 0) clearCache(); // serial's cadence, not per-step
|
|
294
436
|
|
|
437
|
+
// Read + emit the PREVIOUS step's tokens while the new step computes.
|
|
438
|
+
const prev = this.#pendingToks;
|
|
439
|
+
this.#pendingToks = nextToks;
|
|
440
|
+
// Kill switch / A-B lever (house style, cf. MLX_BUN_COMPILED_DECODE=0):
|
|
441
|
+
// MLX_BUN_BATCH_NO_PIPELINE=1 reads THIS step's tokens synchronously —
|
|
442
|
+
// set from process start `prev` is always null, so the flush below IS the
|
|
443
|
+
// whole phase 2. Same math either way (pipelining is scheduling).
|
|
444
|
+
if (NO_PIPELINE) {
|
|
445
|
+
await this.#flushPipeline();
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
if (prev) {
|
|
449
|
+
const toks = [...prev.toFloat32()].map((x) => Math.round(x));
|
|
450
|
+
prev.dispose();
|
|
451
|
+
await this.#emitRows(toks); // also filters #pendingToks on eviction
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/** Read out the pipeline register (if any): emit its tokens and evict
|
|
456
|
+
* finished rows, leaving the pipeline cold. Called before a join merges. */
|
|
457
|
+
async #flushPipeline(): Promise<void> {
|
|
458
|
+
const prev = this.#pendingToks;
|
|
459
|
+
if (!prev) return;
|
|
460
|
+
this.#pendingToks = null;
|
|
461
|
+
const toks = [...prev.toFloat32()].map((x) => Math.round(x));
|
|
462
|
+
prev.dispose();
|
|
463
|
+
await this.#emitRows(toks);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/** Emit one read-back token per running row; evict finished rows. A row's
|
|
467
|
+
* onToken throwing rejects THAT row and evicts it — siblings continue. */
|
|
468
|
+
async #emitRows(toks: number[]): Promise<void> {
|
|
469
|
+
const rows = this.#running;
|
|
470
|
+
const B = rows.length;
|
|
295
471
|
const keep: number[] = [];
|
|
296
472
|
for (let b = 0; b < B; b++) {
|
|
297
473
|
const row = rows[b]!;
|
|
298
474
|
row.generated++;
|
|
299
|
-
|
|
475
|
+
let disp: "continue" | "stop" | "length";
|
|
476
|
+
try {
|
|
477
|
+
disp = await this.#emit(row, toks[b]!);
|
|
478
|
+
} catch (e) {
|
|
479
|
+
row.reject(e); // containment: this row only (mlx-lm `remove`)
|
|
480
|
+
continue; // not kept → evicted by the filter below
|
|
481
|
+
}
|
|
300
482
|
if (disp === "continue") keep.push(b);
|
|
301
483
|
else this.#finish(row, disp);
|
|
302
484
|
}
|
|
@@ -324,7 +506,8 @@ export class BatchScheduler {
|
|
|
324
506
|
});
|
|
325
507
|
}
|
|
326
508
|
|
|
327
|
-
/** Evict rows not in `keep` (sorted ascending) from the batched KV
|
|
509
|
+
/** Evict rows not in `keep` (sorted ascending) from the batched KV and the
|
|
510
|
+
* pipeline register. */
|
|
328
511
|
#applyFilter(keep: number[]): void {
|
|
329
512
|
const inners = this.#inners!;
|
|
330
513
|
if (keep.length === 0) {
|
|
@@ -332,6 +515,8 @@ export class BatchScheduler {
|
|
|
332
515
|
this.#inners = null;
|
|
333
516
|
this.#fullLeftPad = [];
|
|
334
517
|
this.#running = [];
|
|
518
|
+
this.#pendingToks?.dispose();
|
|
519
|
+
this.#pendingToks = null;
|
|
335
520
|
return;
|
|
336
521
|
}
|
|
337
522
|
const out: LayerInner[] = [];
|
|
@@ -352,6 +537,13 @@ export class BatchScheduler {
|
|
|
352
537
|
this.#inners = out;
|
|
353
538
|
this.#fullLeftPad = keep.map((i) => this.#fullLeftPad[i]!);
|
|
354
539
|
this.#running = keep.map((i) => this.#running[i]!);
|
|
540
|
+
if (this.#pendingToks) {
|
|
541
|
+
const idx = ops.fromInt32(keep, [keep.length]);
|
|
542
|
+
const next = ops.takeAxis(this.#pendingToks, idx, 0);
|
|
543
|
+
idx.dispose();
|
|
544
|
+
this.#pendingToks.dispose();
|
|
545
|
+
this.#pendingToks = next;
|
|
546
|
+
}
|
|
355
547
|
}
|
|
356
548
|
|
|
357
549
|
#readToken(t: MlxArray): number {
|