mlx-bun 0.0.4
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/LICENSE +21 -0
- package/README.md +362 -0
- package/THIRD_PARTY_LICENSES.md +41 -0
- package/bin/mlx-bun.mjs +32 -0
- package/docs/curve-designer.html +303 -0
- package/docs/design/adapters-end-to-end.md +201 -0
- package/docs/design/compat-cli-surface-design.md +227 -0
- package/docs/design/docs-reorg-plan.md +152 -0
- package/docs/design/hlg-sampling.md +340 -0
- package/docs/design/memory-system.md +339 -0
- package/docs/design/optimization_plan.md +344 -0
- package/docs/design/parallel-slots.md +515 -0
- package/docs/design/runtime-isolation.md +247 -0
- package/docs/design/segmented-backward-training.md +508 -0
- package/docs/investigations/chunk-finetune-experiment.md +84 -0
- package/docs/investigations/curve-bisector-routing.md +123 -0
- package/docs/investigations/curve-runs/bisector-route.json +98 -0
- package/docs/investigations/curve-runs/bisector-trace.json +1393 -0
- package/docs/investigations/curve-runs/curve-terrain.json +1179 -0
- package/docs/investigations/curve-runs/curve-terrain.svg +42 -0
- package/docs/investigations/curve-terrain.html +105 -0
- package/docs/investigations/expert-offload-single-user-moe.md +292 -0
- package/docs/investigations/hlg-ab.html +34 -0
- package/docs/investigations/hlg-explorer.html +238 -0
- package/docs/investigations/hlg-figs/cliff.svg +1 -0
- package/docs/investigations/hlg-figs/cross.svg +1 -0
- package/docs/investigations/hlg-figs/edges.svg +1 -0
- package/docs/investigations/hlg-figs/k20-grids.svg +1 -0
- package/docs/investigations/hlg-figs/k20-peaks.svg +1 -0
- package/docs/investigations/hlg-figs/lw-curve.svg +1 -0
- package/docs/investigations/hlg-figs/terrain.svg +1 -0
- package/docs/investigations/hlg-report.html +262 -0
- package/docs/investigations/hlg-runs/hlg-ab.json +149 -0
- package/docs/investigations/hlg-runs/hlg-blind-prompts.md +87 -0
- package/docs/investigations/hlg-runs/hlg-blind.json +107 -0
- package/docs/investigations/hlg-runs/hlg-cliff.json +197 -0
- package/docs/investigations/hlg-runs/hlg-cross.json +505 -0
- package/docs/investigations/hlg-runs/hlg-edge.json +388 -0
- package/docs/investigations/hlg-runs/hlg-eval1.json +25 -0
- package/docs/investigations/hlg-runs/hlg-frontier.json +86 -0
- package/docs/investigations/hlg-runs/hlg-grid.json +242 -0
- package/docs/investigations/hlg-runs/hlg-grid2.json +343 -0
- package/docs/investigations/hlg-runs/hlg-lw-coarse.json +110 -0
- package/docs/investigations/hlg-runs/hlg-map.json +383 -0
- package/docs/investigations/hlg-runs/run0-original.log +159 -0
- package/docs/investigations/hlg-runs/runA-recommended.log +131 -0
- package/docs/investigations/hlg-runs/runAA-cliff-cpm5.log +26 -0
- package/docs/investigations/hlg-runs/runAB-grid2-e4b.log +18 -0
- package/docs/investigations/hlg-runs/runAC-peak-e4b-partial.log +25 -0
- package/docs/investigations/hlg-runs/runAD-ab-samples.log +8 -0
- package/docs/investigations/hlg-runs/runAE-edge-e4b.log +51 -0
- package/docs/investigations/hlg-runs/runAF-logit-calibration.log +10 -0
- package/docs/investigations/hlg-runs/runAG-eval-config2.log +16 -0
- package/docs/investigations/hlg-runs/runB-gain-variants.log +192 -0
- package/docs/investigations/hlg-runs/runC-pivots.log +162 -0
- package/docs/investigations/hlg-runs/runD-oetf.log +132 -0
- package/docs/investigations/hlg-runs/runE-eotf.log +162 -0
- package/docs/investigations/hlg-runs/runF-shaper-working.log +86 -0
- package/docs/investigations/hlg-runs/runG-shaper-sweep.log +101 -0
- package/docs/investigations/hlg-runs/runH-wsweep.log +176 -0
- package/docs/investigations/hlg-runs/runI-asweep.log +176 -0
- package/docs/investigations/hlg-runs/runJ-knobmap.log +49 -0
- package/docs/investigations/hlg-runs/runK-frontier.log +19 -0
- package/docs/investigations/hlg-runs/runL-widemap.log +65 -0
- package/docs/investigations/hlg-runs/runM-frontier-emb.log +15 -0
- package/docs/investigations/hlg-runs/runN-verify.log +116 -0
- package/docs/investigations/hlg-runs/runO-frontier-seed5000.log +15 -0
- package/docs/investigations/hlg-runs/runP-grid-AxSm.log +15 -0
- package/docs/investigations/hlg-runs/runQ-grid-tgW.log +15 -0
- package/docs/investigations/hlg-runs/runR-blind-transcript.log +79 -0
- package/docs/investigations/hlg-runs/runS-cross-coherence.log +33 -0
- package/docs/investigations/hlg-runs/runT-cross-cpm5.log +33 -0
- package/docs/investigations/hlg-runs/runU-cross-12b.log +33 -0
- package/docs/investigations/hlg-runs/runV-lw-e4b.log +22 -0
- package/docs/investigations/hlg-runs/runW-lw-12b.log +22 -0
- package/docs/investigations/hlg-runs/runX-lw-cpm5.log +22 -0
- package/docs/investigations/hlg-runs/runY-cliff-e4b.log +26 -0
- package/docs/investigations/hlg-runs/runZ-cliff-12b.log +26 -0
- package/docs/investigations/hlg-sampling-investigation.md +970 -0
- package/docs/investigations/lab-build-journal.md +157 -0
- package/docs/investigations/pi-builtin-investigation.md +308 -0
- package/docs/investigations/starter-model-port-handoff.md +323 -0
- package/docs/planning/IDEAS.md +26 -0
- package/docs/planning/PRODUCT_ROADMAP.md +426 -0
- package/docs/planning/ResearchTopics.md +119 -0
- package/docs/planning/chunk-task-roadmap.md +77 -0
- package/docs/planning/journal.md +155 -0
- package/docs/reference/distribution.md +121 -0
- package/docs/reference/embedding.md +168 -0
- package/docs/reference/library-api.md +153 -0
- package/docs/reference/server-api.md +224 -0
- package/docs/reference/server-config.md +243 -0
- package/docs/segmented-backward-handoff.md +148 -0
- package/package.json +68 -0
- package/src/anthropic.ts +491 -0
- package/src/chat-template.ts +186 -0
- package/src/cli.ts +1075 -0
- package/src/config.ts +229 -0
- package/src/curve-sampler.ts +122 -0
- package/src/dataset/generators.ts +1087 -0
- package/src/dataset/index.ts +12 -0
- package/src/dataset/job.ts +81 -0
- package/src/dataset/llm.ts +71 -0
- package/src/dataset/registry.ts +528 -0
- package/src/download.ts +283 -0
- package/src/eval/README.md +86 -0
- package/src/eval/kl-prompts.ts +29 -0
- package/src/eval/kl.ts +294 -0
- package/src/eval/quality-db.ts +92 -0
- package/src/eval/runner.ts +102 -0
- package/src/eval/score.ts +49 -0
- package/src/eval/tasks/bfcl.ts +500 -0
- package/src/eval/tasks/gsm8k.ts +85 -0
- package/src/eval/tasks/hashhop.ts +109 -0
- package/src/eval/tasks/humaneval.ts +252 -0
- package/src/eval/tasks/ifeval.ts +354 -0
- package/src/eval/tasks/mmlu.ts +145 -0
- package/src/evaldb.ts +199 -0
- package/src/expert-offload-build.ts +77 -0
- package/src/expert-offload.ts +61 -0
- package/src/expert-trace.ts +67 -0
- package/src/fit.ts +254 -0
- package/src/generate.ts +451 -0
- package/src/harness-pi.ts +177 -0
- package/src/hf-push.ts +628 -0
- package/src/index.ts +22 -0
- package/src/jobs/db.ts +139 -0
- package/src/jobs/index.ts +29 -0
- package/src/jobs/job-entry.ts +106 -0
- package/src/jobs/runner.ts +304 -0
- package/src/jobs/sse.ts +114 -0
- package/src/jobs/types.ts +58 -0
- package/src/kv-store.ts +185 -0
- package/src/lora.ts +314 -0
- package/src/md-modules.d.ts +7 -0
- package/src/mlx/array.ts +237 -0
- package/src/mlx/autograd.ts +267 -0
- package/src/mlx/checkpoint.ts +116 -0
- package/src/mlx/compile.ts +158 -0
- package/src/mlx/custom-vjp.ts +140 -0
- package/src/mlx/ffi.ts +433 -0
- package/src/mlx/metal-kernel.ts +130 -0
- package/src/mlx/ops.ts +707 -0
- package/src/mmap.ts +90 -0
- package/src/model/batched-mask.ts +204 -0
- package/src/model/batched-rotating.ts +300 -0
- package/src/model/compiled-decode.ts +681 -0
- package/src/model/factory.ts +27 -0
- package/src/model/fingerprint.ts +33 -0
- package/src/model/flash-attention.ts +674 -0
- package/src/model/fused-decode-kernel.ts +305 -0
- package/src/model/fused-geglu-kernel.ts +87 -0
- package/src/model/gemma4-base.ts +1616 -0
- package/src/model/gemma4.ts +1012 -0
- package/src/model/generated/gemma4-12b.ts +526 -0
- package/src/model/generated/gemma4-26b.ts +500 -0
- package/src/model/generated/gemma4-e4b.ts +757 -0
- package/src/model/generated/index.ts +18 -0
- package/src/model/minicpm5.ts +278 -0
- package/src/model/qwen3-delta.ts +224 -0
- package/src/model/qwen3_5.ts +429 -0
- package/src/model/support.ts +46 -0
- package/src/native-pack.ts +152 -0
- package/src/pi-provider.ts +95 -0
- package/src/pi-terminal.ts +264 -0
- package/src/pi-web.ts +924 -0
- package/src/preflight.ts +123 -0
- package/src/prompt-cache.ts +113 -0
- package/src/quantize/allocator.ts +627 -0
- package/src/quantize/calibration.ts +180 -0
- package/src/quantize/config-writer.ts +132 -0
- package/src/quantize/index.ts +57 -0
- package/src/quantize/job.ts +136 -0
- package/src/quantize/quantizer.ts +462 -0
- package/src/quantize/safetensors-writer.ts +171 -0
- package/src/quantize/sensitivity.ts +234 -0
- package/src/registry.ts +336 -0
- package/src/responses.ts +774 -0
- package/src/safetensors.ts +158 -0
- package/src/sampler.ts +517 -0
- package/src/serve/batch-scheduler.ts +362 -0
- package/src/serve/generation-gateway.ts +160 -0
- package/src/server.ts +1891 -0
- package/src/spec/drafter.ts +261 -0
- package/src/spec/generate.ts +221 -0
- package/src/status-page.html +497 -0
- package/src/tokenizer.ts +38 -0
- package/src/tool-call.ts +242 -0
- package/src/train/dataset.ts +461 -0
- package/src/train/export.ts +32 -0
- package/src/train/forward.ts +229 -0
- package/src/train/index.ts +43 -0
- package/src/train/job.ts +148 -0
- package/src/train/lora-params.ts +194 -0
- package/src/train/loss.ts +342 -0
- package/src/train/merge.ts +195 -0
- package/src/train/optimizer.ts +176 -0
- package/src/train/rank.ts +101 -0
- package/src/train/segmented.ts +520 -0
- package/src/train/trainer.ts +600 -0
- package/src/tui.ts +161 -0
- package/src/vision/embedder.ts +168 -0
- package/src/vision/preprocess.ts +247 -0
- package/src/vision/prompt.ts +158 -0
- package/src/vision/siglip.ts +599 -0
- package/src/web/app.html +2471 -0
- package/src/web/skills/web-research/SKILL.md +44 -0
- package/src/web/skills.ts +59 -0
- package/src/web-tools.ts +426 -0
- package/src/weights.ts +91 -0
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
// Continuous-batching scheduler for `--batch N` serving (phase S2, the engine
|
|
2
|
+
// loop). Owns ONE running batch and drives it forward one decode step at a
|
|
3
|
+
// time, admitting waiting requests and evicting finished ones between steps —
|
|
4
|
+
// iteration-level (continuous) scheduling, not static batching. See
|
|
5
|
+
// docs/design/parallel-slots.md.
|
|
6
|
+
//
|
|
7
|
+
// The numerically-hard parts are verified primitives:
|
|
8
|
+
// - the batched FORWARD (per-row RoPE/mask) is bit-parity with mlx-lm B=N
|
|
9
|
+
// across all 4 models (tests/batched-decode-parity);
|
|
10
|
+
// - the dynamic-B FULL-attention ops mergeKVRows / filterKVRows match mlx-lm
|
|
11
|
+
// BatchKVCache (tests/batched-decode-parity), and the SLIDING-window
|
|
12
|
+
// BatchedRotatingCache (merge/filter/decode/make_mask incl. ring-wrap)
|
|
13
|
+
// matches mlx-lm BatchRotatingKVCache (tests/batched-rotating).
|
|
14
|
+
// This module is the ORCHESTRATION on top: admission, the step loop, per-row
|
|
15
|
+
// sampling + token accounting, eviction, and assembling each layer's batched
|
|
16
|
+
// cache by type. Gate: tests/batch-scheduler.test.ts (teacher-forced, KL).
|
|
17
|
+
//
|
|
18
|
+
// Per-layer cache types: a model interleaves full-attention layers (plain
|
|
19
|
+
// KVCache, wrapped per step in a BatchedDecodeMaskCache) and sliding-window
|
|
20
|
+
// layers (a persistent BatchedRotatingCache that is itself the batched cache +
|
|
21
|
+
// mask). Full layers share one leftPad/offset (all rows advance together); the
|
|
22
|
+
// rotating caches self-track per-row leftPad/offset as the ring wraps. The
|
|
23
|
+
// per-row absolute position stays consistent across both (full: offset-leftPad;
|
|
24
|
+
// rot: offsetArr) — see docs/design/parallel-slots.md.
|
|
25
|
+
//
|
|
26
|
+
// Bun-async, NO threads: a single detached driver loop owns the GPU for batched
|
|
27
|
+
// mode (an ExclusiveLock keeps the serial fallback off the GPU concurrently).
|
|
28
|
+
// Joins re-merge the whole batch; the keep-the-running-batch `extend`
|
|
29
|
+
// optimization is a later refinement.
|
|
30
|
+
|
|
31
|
+
import { MlxArray } from "../mlx/array";
|
|
32
|
+
import * as ops from "../mlx/ops";
|
|
33
|
+
import { clearCache } from "../mlx/ffi";
|
|
34
|
+
import { KVCache, RotatingKVCache, type Cache } from "../model/gemma4-base";
|
|
35
|
+
import { BatchedDecodeMaskCache, mergeKVRows, filterKVRows } from "../model/batched-mask";
|
|
36
|
+
import { BatchedRotatingCache } from "../model/batched-rotating";
|
|
37
|
+
import type { RuntimeModel } from "../model/factory";
|
|
38
|
+
|
|
39
|
+
/** A token sampler for one row: (logits [1,V], step) → token array [1] on
|
|
40
|
+
* device. Greedy is `(l) => ops.argmaxAxis(l, -1)`; richer closures fold in
|
|
41
|
+
* temperature / top-p / logits processors + history (built per request from
|
|
42
|
+
* its sampler options, exactly like generate()'s sampleStep). */
|
|
43
|
+
export type RowSampler = (logits1V: MlxArray, step: number) => MlxArray;
|
|
44
|
+
|
|
45
|
+
export interface BatchRequest {
|
|
46
|
+
promptIds: number[];
|
|
47
|
+
maxTokens: number;
|
|
48
|
+
eosTokenIds: number[];
|
|
49
|
+
sample: RowSampler;
|
|
50
|
+
/** Called per emitted (non-EOS) token, in order. Returning `false` halts this
|
|
51
|
+
* row (a decoded-text stop sequence fired) — matches generate()'s onToken
|
|
52
|
+
* contract. EOS terminates the row WITHOUT an onToken call. May be async;
|
|
53
|
+
* keep it cheap — it runs inline in the step loop. */
|
|
54
|
+
onToken: (token: number) => void | boolean | Promise<void | boolean>;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface BatchStats {
|
|
58
|
+
promptTokens: number;
|
|
59
|
+
generatedTokens: number;
|
|
60
|
+
/** Prompt tokens served from a pre-warmed cache. Always 0 in v1 (each row is
|
|
61
|
+
* solo-prefilled from scratch; prompt-cache reuse under batching is later). */
|
|
62
|
+
cachedTokens: number;
|
|
63
|
+
finishReason: "stop" | "length";
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
interface Row {
|
|
67
|
+
req: BatchRequest;
|
|
68
|
+
resolve: (s: BatchStats) => void;
|
|
69
|
+
reject: (e: unknown) => void;
|
|
70
|
+
current: number; // last emitted token, fed at the next step
|
|
71
|
+
generated: number; // tokens emitted so far (incl. a terminating EOS)
|
|
72
|
+
promptTokens: number;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Held while the batch is active; the serial fallback acquires the same lock. */
|
|
76
|
+
export interface ExclusiveLock {
|
|
77
|
+
acquire(): Promise<() => void>;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface BatchSchedulerOptions {
|
|
81
|
+
/** Max rows in the running batch (mlx-lm `--decode-concurrency`). */
|
|
82
|
+
maxBatch: number;
|
|
83
|
+
lock?: ExclusiveLock;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
type LayerInner = KVCache | BatchedRotatingCache;
|
|
87
|
+
type Row1 = { keys: MlxArray; values: MlxArray };
|
|
88
|
+
|
|
89
|
+
export class BatchScheduler {
|
|
90
|
+
#running: Row[] = [];
|
|
91
|
+
#inners: LayerInner[] | null = null; // per-layer batched KV; null when empty
|
|
92
|
+
#fullLeftPad: number[] = []; // per-row padding for FULL layers (rot self-tracks)
|
|
93
|
+
#pending: Row[] = [];
|
|
94
|
+
#looping = false;
|
|
95
|
+
#wake: (() => void) | null = null;
|
|
96
|
+
readonly #maxBatch: number;
|
|
97
|
+
readonly #lock: ExclusiveLock | undefined;
|
|
98
|
+
readonly #kinds: ("full" | "rot")[]; // per-layer attention type
|
|
99
|
+
readonly #rotMaxSize: number[]; // per-layer sliding window (rot layers only)
|
|
100
|
+
|
|
101
|
+
constructor(private readonly model: RuntimeModel, opts: BatchSchedulerOptions) {
|
|
102
|
+
this.#maxBatch = Math.max(1, Math.floor(opts.maxBatch));
|
|
103
|
+
this.#lock = opts.lock;
|
|
104
|
+
const proto = model.makeCache(); // fresh caches hold no buffers
|
|
105
|
+
this.#kinds = proto.map((c) => (c instanceof RotatingKVCache ? "rot" : "full"));
|
|
106
|
+
this.#rotMaxSize = proto.map((c) => (c instanceof RotatingKVCache ? c.maxSize : 0));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
get activeRows(): number {
|
|
110
|
+
return this.#running.length;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Submit a request; resolves when its row finishes (EOS, stop, or length). */
|
|
114
|
+
submit(req: BatchRequest): Promise<BatchStats> {
|
|
115
|
+
return new Promise<BatchStats>((resolve, reject) => {
|
|
116
|
+
this.#pending.push({
|
|
117
|
+
req, resolve, reject,
|
|
118
|
+
current: 0, generated: 0, promptTokens: req.promptIds.length,
|
|
119
|
+
});
|
|
120
|
+
this.#ensureLoop();
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
#ensureLoop(): void {
|
|
125
|
+
if (this.#wake) { this.#wake(); return; }
|
|
126
|
+
if (this.#looping) return;
|
|
127
|
+
this.#looping = true;
|
|
128
|
+
void this.#drive();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async #drive(): Promise<void> {
|
|
132
|
+
let release: (() => void) | null = null;
|
|
133
|
+
try {
|
|
134
|
+
while (true) {
|
|
135
|
+
if (!release && this.#lock && (this.#pending.length > 0 || this.#running.length > 0))
|
|
136
|
+
release = await this.#lock.acquire();
|
|
137
|
+
|
|
138
|
+
while (this.#pending.length > 0 && this.#running.length < this.#maxBatch) {
|
|
139
|
+
const row = this.#pending.shift()!;
|
|
140
|
+
try {
|
|
141
|
+
await this.#admit(row);
|
|
142
|
+
} catch (e) {
|
|
143
|
+
row.reject(e);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (this.#running.length === 0) {
|
|
147
|
+
if (this.#pending.length > 0) continue;
|
|
148
|
+
if (release) { release(); release = null; }
|
|
149
|
+
await new Promise<void>((r) => { this.#wake = r; });
|
|
150
|
+
this.#wake = null;
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
try {
|
|
154
|
+
await this.#step();
|
|
155
|
+
} catch (e) {
|
|
156
|
+
for (const row of this.#running) row.reject(e);
|
|
157
|
+
this.#applyFilter([]);
|
|
158
|
+
}
|
|
159
|
+
await new Promise<void>((r) => setImmediate(r));
|
|
160
|
+
}
|
|
161
|
+
} finally {
|
|
162
|
+
if (release) release();
|
|
163
|
+
this.#looping = false;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Solo-prefill a joining request, emit its first token, and (if it survives)
|
|
168
|
+
* re-merge it with the running batch (per-layer, by attention type). */
|
|
169
|
+
async #admit(row: Row): Promise<void> {
|
|
170
|
+
const solo = this.model.makeCache();
|
|
171
|
+
const ids = ops.fromInt32(row.req.promptIds, [1, row.req.promptIds.length]);
|
|
172
|
+
const h = this.model.forwardHidden(ids, solo);
|
|
173
|
+
ids.dispose();
|
|
174
|
+
const lg = this.model.logitsFromHidden(h);
|
|
175
|
+
h.dispose();
|
|
176
|
+
const [, Lp, V] = lg.shape as [number, number, number];
|
|
177
|
+
const last = lg.slice([0, Lp - 1, 0], [1, Lp, V]);
|
|
178
|
+
lg.dispose();
|
|
179
|
+
const last2 = ops.reshape(last, [1, V]);
|
|
180
|
+
last.dispose();
|
|
181
|
+
const tok = this.#readToken(row.req.sample(last2, 0));
|
|
182
|
+
last2.dispose();
|
|
183
|
+
row.generated = 1;
|
|
184
|
+
clearCache();
|
|
185
|
+
|
|
186
|
+
const stop = await this.#emit(row, tok);
|
|
187
|
+
if (stop !== "continue") {
|
|
188
|
+
for (const c of solo) c.dispose();
|
|
189
|
+
this.#finish(row, stop);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Re-merge the running batch + the new row, layer by layer.
|
|
194
|
+
const prev = this.#inners;
|
|
195
|
+
const prevPad = this.#fullLeftPad;
|
|
196
|
+
const B = this.#running.length;
|
|
197
|
+
const newInners: LayerInner[] = [];
|
|
198
|
+
let newFullPad = this.#fullLeftPad;
|
|
199
|
+
for (let layer = 0; layer < this.#kinds.length; layer++) {
|
|
200
|
+
const soloC = solo[layer] as KVCache | RotatingKVCache;
|
|
201
|
+
const [sk, sv] = soloC.temporalView();
|
|
202
|
+
const newRow: Row1 = { keys: sk, values: sv };
|
|
203
|
+
if (this.#kinds[layer] === "rot") {
|
|
204
|
+
const rows: Row1[] = [];
|
|
205
|
+
const offsets: number[] = [];
|
|
206
|
+
const prevRot = prev?.[layer] as BatchedRotatingCache | undefined;
|
|
207
|
+
if (prevRot) {
|
|
208
|
+
const [k0, v0] = prevRot.temporalView(); // [B,H,valid,D]
|
|
209
|
+
const [, H, valid, D] = k0.shape as [number, number, number, number];
|
|
210
|
+
const vD = v0.shape[3]!;
|
|
211
|
+
for (let b = 0; b < B; b++) {
|
|
212
|
+
const pad = Math.max(0, prevRot.leftPad[b]!);
|
|
213
|
+
rows.push({
|
|
214
|
+
keys: k0.slice([b, 0, pad, 0], [b + 1, H, valid, D]),
|
|
215
|
+
values: v0.slice([b, 0, pad, 0], [b + 1, H, valid, vD]),
|
|
216
|
+
});
|
|
217
|
+
offsets.push(prevRot.offsetArr[b]!);
|
|
218
|
+
}
|
|
219
|
+
k0.dispose(); v0.dispose();
|
|
220
|
+
}
|
|
221
|
+
rows.push(newRow);
|
|
222
|
+
offsets.push(soloC.offset);
|
|
223
|
+
newInners.push(BatchedRotatingCache.merge(rows, offsets, this.#rotMaxSize[layer]!));
|
|
224
|
+
for (const r of rows) { r.keys.dispose(); r.values.dispose(); }
|
|
225
|
+
} else {
|
|
226
|
+
const rows: Row1[] = [];
|
|
227
|
+
const prevFull = prev?.[layer] as KVCache | undefined;
|
|
228
|
+
if (prevFull) {
|
|
229
|
+
const [k0, v0] = prevFull.temporalView(); // [B,H,off,D]
|
|
230
|
+
const [, H, off, D] = k0.shape as [number, number, number, number];
|
|
231
|
+
const vD = v0.shape[3]!;
|
|
232
|
+
for (let b = 0; b < B; b++) {
|
|
233
|
+
const pad = prevPad[b]!;
|
|
234
|
+
rows.push({
|
|
235
|
+
keys: k0.slice([b, 0, pad, 0], [b + 1, H, off, D]),
|
|
236
|
+
values: v0.slice([b, 0, pad, 0], [b + 1, H, off, vD]),
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
k0.dispose(); v0.dispose();
|
|
240
|
+
}
|
|
241
|
+
rows.push(newRow);
|
|
242
|
+
const merged = mergeKVRows(rows);
|
|
243
|
+
newFullPad = merged.leftPad;
|
|
244
|
+
const c = new KVCache();
|
|
245
|
+
c.restoreState(merged.keys, merged.values, merged.width);
|
|
246
|
+
newInners.push(c);
|
|
247
|
+
for (const r of rows) { r.keys.dispose(); r.values.dispose(); }
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
if (prev) for (const c of prev) c.dispose();
|
|
251
|
+
for (const c of solo) c.dispose();
|
|
252
|
+
this.#inners = newInners;
|
|
253
|
+
this.#fullLeftPad = newFullPad;
|
|
254
|
+
this.#running.push(row);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** One batched decode step: forward all rows, sample per row, emit, evict. */
|
|
258
|
+
async #step(): Promise<void> {
|
|
259
|
+
const rows = this.#running;
|
|
260
|
+
const B = rows.length;
|
|
261
|
+
const inners = this.#inners!;
|
|
262
|
+
// Per-layer forward cache: rot layers use the persistent BatchedRotatingCache
|
|
263
|
+
// directly; full layers get a fresh BatchedDecodeMaskCache wrapper.
|
|
264
|
+
const fwd: Cache[] = inners.map((c) =>
|
|
265
|
+
c instanceof BatchedRotatingCache ? c : new BatchedDecodeMaskCache(c, B, this.#fullLeftPad, null),
|
|
266
|
+
);
|
|
267
|
+
let toks: number[];
|
|
268
|
+
try {
|
|
269
|
+
const ids = ops.fromInt32(rows.map((r) => r.current), [B, 1]);
|
|
270
|
+
const h = this.model.forwardHidden(ids, fwd);
|
|
271
|
+
ids.dispose();
|
|
272
|
+
const lg = this.model.logitsFromHidden(h); // [B,1,V]
|
|
273
|
+
h.dispose();
|
|
274
|
+
const V = lg.shape[2]!;
|
|
275
|
+
const sampled: MlxArray[] = [];
|
|
276
|
+
for (let b = 0; b < B; b++) {
|
|
277
|
+
const rl = lg.slice([b, 0, 0], [b + 1, 1, V]);
|
|
278
|
+
const rl2 = ops.reshape(rl, [1, V]);
|
|
279
|
+
rl.dispose();
|
|
280
|
+
sampled.push(rows[b]!.req.sample(rl2, rows[b]!.generated));
|
|
281
|
+
rl2.dispose();
|
|
282
|
+
}
|
|
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
|
+
}
|
|
293
|
+
clearCache();
|
|
294
|
+
|
|
295
|
+
const keep: number[] = [];
|
|
296
|
+
for (let b = 0; b < B; b++) {
|
|
297
|
+
const row = rows[b]!;
|
|
298
|
+
row.generated++;
|
|
299
|
+
const disp = await this.#emit(row, toks[b]!);
|
|
300
|
+
if (disp === "continue") keep.push(b);
|
|
301
|
+
else this.#finish(row, disp);
|
|
302
|
+
}
|
|
303
|
+
if (keep.length < B) this.#applyFilter(keep);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/** Account one sampled token for a row. Mirrors generate(): EOS terminates
|
|
307
|
+
* WITHOUT an onToken call; otherwise onToken(token) runs and `false` halts;
|
|
308
|
+
* reaching maxTokens ends with "length". Advances row.current on continue. */
|
|
309
|
+
async #emit(row: Row, token: number): Promise<"continue" | "stop" | "length"> {
|
|
310
|
+
if (row.req.eosTokenIds.includes(token)) return "stop";
|
|
311
|
+
const cont = await row.req.onToken(token);
|
|
312
|
+
if (cont === false) return "stop";
|
|
313
|
+
if (row.generated >= row.req.maxTokens) return "length";
|
|
314
|
+
row.current = token;
|
|
315
|
+
return "continue";
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
#finish(row: Row, reason: "stop" | "length"): void {
|
|
319
|
+
row.resolve({
|
|
320
|
+
promptTokens: row.promptTokens,
|
|
321
|
+
generatedTokens: row.generated,
|
|
322
|
+
cachedTokens: 0,
|
|
323
|
+
finishReason: reason,
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/** Evict rows not in `keep` (sorted ascending) from the batched KV. */
|
|
328
|
+
#applyFilter(keep: number[]): void {
|
|
329
|
+
const inners = this.#inners!;
|
|
330
|
+
if (keep.length === 0) {
|
|
331
|
+
for (const c of inners) c.dispose();
|
|
332
|
+
this.#inners = null;
|
|
333
|
+
this.#fullLeftPad = [];
|
|
334
|
+
this.#running = [];
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
const out: LayerInner[] = [];
|
|
338
|
+
for (const inner of inners) {
|
|
339
|
+
if (inner instanceof BatchedRotatingCache) {
|
|
340
|
+
inner.filter(keep); // in-place (mutates + drops rows + reduces padding)
|
|
341
|
+
out.push(inner);
|
|
342
|
+
} else {
|
|
343
|
+
const [k0, v0] = inner.temporalView();
|
|
344
|
+
const f = filterKVRows(k0, v0, keep);
|
|
345
|
+
k0.dispose(); v0.dispose();
|
|
346
|
+
const c = new KVCache();
|
|
347
|
+
c.restoreState(f.keys, f.values, inner.offset);
|
|
348
|
+
out.push(c);
|
|
349
|
+
inner.dispose();
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
this.#inners = out;
|
|
353
|
+
this.#fullLeftPad = keep.map((i) => this.#fullLeftPad[i]!);
|
|
354
|
+
this.#running = keep.map((i) => this.#running[i]!);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
#readToken(t: MlxArray): number {
|
|
358
|
+
const v = Math.round(t.toFloat32()[0]!);
|
|
359
|
+
t.dispose();
|
|
360
|
+
return v;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
// Generation gateway — the seam between the server's request handlers and the
|
|
2
|
+
// two execution lanes: the serial single-queue path (today's default) and the
|
|
3
|
+
// continuous-batching scheduler (`--batch N`). The handler builds an onToken
|
|
4
|
+
// closure (its own StopMatcher + tool router + SSE stream) and calls run();
|
|
5
|
+
// the gateway routes it to the right lane. Per-row SSE fan-out falls out for
|
|
6
|
+
// free — each request keeps its own onToken/stream and the scheduler just
|
|
7
|
+
// invokes the right row's onToken.
|
|
8
|
+
//
|
|
9
|
+
// The two lanes are MUTUALLY EXCLUSIVE on the GPU (and on shared model state
|
|
10
|
+
// like loraState, which generate() mutates per-generation assuming a serialized
|
|
11
|
+
// queue). One AsyncMutex enforces it: a serial run holds it for its duration;
|
|
12
|
+
// the scheduler holds it for its whole active period (first admit → batch
|
|
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.)
|
|
17
|
+
//
|
|
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
|
|
21
|
+
// (per-row logits processors are a later refinement) AND no user-fixed seed
|
|
22
|
+
// (reproducibility ⇒ solo, matching mlx-lm's _is_batchable). Temperature /
|
|
23
|
+
// top-p / top-k DO batch (each row samples with its own seed). Full-attention
|
|
24
|
+
// AND sliding-window (Gemma) models both batch — the scheduler assembles each
|
|
25
|
+
// layer's cache by type. Prompt-cache prefix reuse is bypassed under batching
|
|
26
|
+
// in v1 (cachedTokens = 0).
|
|
27
|
+
|
|
28
|
+
import { MlxArray } from "../mlx/array";
|
|
29
|
+
import type { RuntimeModel } from "../model/factory";
|
|
30
|
+
import type { GenerateOptions, GenerateStats } from "../generate";
|
|
31
|
+
import { makeSampler, toLogprobs } from "../sampler";
|
|
32
|
+
import { BatchScheduler } from "./batch-scheduler";
|
|
33
|
+
|
|
34
|
+
/** Async mutex: acquire() resolves to a release fn; releases run FIFO. */
|
|
35
|
+
class AsyncMutex {
|
|
36
|
+
#tail: Promise<void> = Promise.resolve();
|
|
37
|
+
acquire(): Promise<() => void> {
|
|
38
|
+
let release!: () => void;
|
|
39
|
+
const gate = new Promise<void>((r) => { release = r; });
|
|
40
|
+
const wait = this.#tail;
|
|
41
|
+
this.#tail = this.#tail.then(() => gate);
|
|
42
|
+
return wait.then(() => release);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export type Vision = { embeddings: MlxArray; imageMask: MlxArray };
|
|
47
|
+
|
|
48
|
+
/** Per-token sink: returning `false` halts this generation (stop sequence). */
|
|
49
|
+
export type OnToken = (token: number) => void | boolean | Promise<void | boolean>;
|
|
50
|
+
|
|
51
|
+
/** The serial lane — exactly today's runGeneration (prompt-cache reuse + the
|
|
52
|
+
* generate() pipeline). The gateway calls it under the mutex. */
|
|
53
|
+
export type SerialRun = (
|
|
54
|
+
promptIds: number[],
|
|
55
|
+
options: GenerateOptions & { stopSequences?: string[] },
|
|
56
|
+
onToken: OnToken,
|
|
57
|
+
vision?: Vision,
|
|
58
|
+
) => Promise<GenerateStats>;
|
|
59
|
+
|
|
60
|
+
/** What the batchable decision needs from a request (cheap to compute). */
|
|
61
|
+
export interface RequestShape {
|
|
62
|
+
hasVision: boolean;
|
|
63
|
+
hasAdapters: boolean;
|
|
64
|
+
hasRepetitionPenalty: boolean;
|
|
65
|
+
/** The user explicitly set `seed` (reproducibility) — not the random default. */
|
|
66
|
+
userSeed: boolean;
|
|
67
|
+
/** KV quantization is active (kvConfig/kvBits) — batched is bf16-only in v1. */
|
|
68
|
+
kvQuant: boolean;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export class GenerationGateway {
|
|
72
|
+
readonly #mutex = new AsyncMutex();
|
|
73
|
+
readonly #batch: number;
|
|
74
|
+
#scheduler: BatchScheduler | null = null;
|
|
75
|
+
|
|
76
|
+
constructor(
|
|
77
|
+
private readonly model: RuntimeModel,
|
|
78
|
+
batch: number,
|
|
79
|
+
private readonly serialRun: SerialRun,
|
|
80
|
+
) {
|
|
81
|
+
this.#batch = Math.max(1, Math.floor(batch));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** True if `--batch N` (N>1) is on (batchability is then per-request). */
|
|
85
|
+
get batchingEnabled(): boolean {
|
|
86
|
+
return this.#batch > 1;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Rows currently decoding in the batch (0 if no scheduler / idle). */
|
|
90
|
+
get activeRows(): number {
|
|
91
|
+
return this.#scheduler?.activeRows ?? 0;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Decide whether a request joins the batch or runs serially. */
|
|
95
|
+
willBatch(shape: RequestShape): boolean {
|
|
96
|
+
return (
|
|
97
|
+
this.batchingEnabled &&
|
|
98
|
+
!shape.hasVision &&
|
|
99
|
+
!shape.hasAdapters &&
|
|
100
|
+
!shape.hasRepetitionPenalty &&
|
|
101
|
+
!shape.userSeed &&
|
|
102
|
+
!shape.kvQuant
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Run one generation on the appropriate lane. onToken is invoked per emitted
|
|
107
|
+
* token (its `false` halts); resolves with stats when the generation ends. */
|
|
108
|
+
async run(
|
|
109
|
+
promptIds: number[],
|
|
110
|
+
options: GenerateOptions & { stopSequences?: string[] },
|
|
111
|
+
onToken: OnToken,
|
|
112
|
+
vision: Vision | undefined,
|
|
113
|
+
shape: RequestShape,
|
|
114
|
+
): Promise<GenerateStats> {
|
|
115
|
+
if (!this.willBatch(shape)) {
|
|
116
|
+
const release = await this.#mutex.acquire();
|
|
117
|
+
try {
|
|
118
|
+
return await this.serialRun(promptIds, options, onToken, vision);
|
|
119
|
+
} finally {
|
|
120
|
+
release();
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Per-row sampler, mirroring generate()'s sampleStep (no logits processors:
|
|
125
|
+
// repetition penalty was excluded by willBatch). Greedy (temperature 0) is
|
|
126
|
+
// argmax; temp>0 uses this request's own seed → independent per-row RNG.
|
|
127
|
+
const sampler = makeSampler(options);
|
|
128
|
+
const sample = (logits1V: MlxArray, step: number): MlxArray => {
|
|
129
|
+
const lp = toLogprobs(logits1V);
|
|
130
|
+
const tok = sampler(lp, step);
|
|
131
|
+
lp.dispose();
|
|
132
|
+
return tok;
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const st = await this.#ensureScheduler().submit({
|
|
136
|
+
promptIds,
|
|
137
|
+
maxTokens: options.maxTokens ?? 512,
|
|
138
|
+
eosTokenIds: options.eosTokenIds ?? this.model.config.eosTokenIds,
|
|
139
|
+
sample,
|
|
140
|
+
onToken,
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
promptTokens: st.promptTokens,
|
|
145
|
+
cachedTokens: st.cachedTokens,
|
|
146
|
+
generatedTokens: st.generatedTokens,
|
|
147
|
+
prefillMs: 0, decodeMs: 0, prefillTps: 0, decodeTps: 0,
|
|
148
|
+
cacheTokens: [],
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
#ensureScheduler(): BatchScheduler {
|
|
153
|
+
if (!this.#scheduler)
|
|
154
|
+
this.#scheduler = new BatchScheduler(this.model, {
|
|
155
|
+
maxBatch: this.#batch,
|
|
156
|
+
lock: { acquire: () => this.#mutex.acquire() },
|
|
157
|
+
});
|
|
158
|
+
return this.#scheduler;
|
|
159
|
+
}
|
|
160
|
+
}
|