mlx-bun 0.0.10 → 0.0.11
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 +66 -21
- package/THIRD_PARTY_LICENSES.md +1 -0
- package/docs/archive/mlx-bun-lab-report.html +275 -0
- package/docs/archive/wwdc-mlx-bun.html +848 -0
- package/docs/design/audio-input-plan.md +356 -0
- package/docs/design/batching-perf-path.md +58 -35
- package/docs/design/batching-v2-plan.md +23 -9
- package/docs/design/bucketing-stage.md +2 -1
- package/docs/design/compat-cli-surface-design.md +4 -3
- package/docs/design/decode-speed-program.md +172 -0
- package/docs/design/docs-reorg-plan.md +1 -1
- package/docs/design/dspark-serving-program.md +371 -0
- package/docs/design/dspark-speculative-decoding.md +167 -5
- package/docs/design/faithful-l1-consolidation.md +310 -0
- package/docs/design/generic-model-support.md +8 -5
- package/docs/design/grammar-spec-batching-integration.md +301 -0
- package/docs/design/minicpm5-decode-megakernel.md +16 -11
- package/docs/design/mlx-lm-tool-parity-plan.md +11 -1
- package/docs/design/omlx-adoption-map.md +14 -6
- package/docs/design/orpo-training.md +34 -30
- package/docs/design/paged-kv-cache.md +214 -0
- package/docs/design/parallel-slots.md +82 -49
- package/docs/design/parity-tier-dag.md +43 -33
- package/docs/design/repo-cleanup-plan.md +189 -0
- package/docs/design/runtime-isolation.md +19 -3
- package/docs/design/segmented-backward-training.md +3 -2
- package/docs/design/ssd-kv-cold-tier.md +130 -0
- package/docs/design/structured-output.md +484 -0
- package/docs/design/the-dreaming-master-plan.md +2 -3
- package/docs/design/turboquant-kv.md +235 -0
- package/docs/design/unified-engine-frontier-plan.md +774 -0
- package/docs/design/web-chat-beat-matrix.md +315 -0
- package/docs/design/web-chat-redesign.md +1112 -0
- package/docs/investigations/dspark-handoff.md +182 -16
- package/docs/investigations/lab-build-journal.md +2 -2
- package/docs/investigations/openwiki-evaluation.md +249 -0
- package/docs/planning/IDEAS.md +14 -0
- package/docs/planning/PRODUCT_ROADMAP.md +2 -3
- package/docs/planning/chat-ui-vision-memory-first.md +199 -0
- package/docs/planning/chat-ui-vision.md +50 -0
- package/docs/planning/memory-docs-and-dag-plan.md +15 -6
- package/docs/planning/release-notes-v0.0.11.md +239 -0
- package/docs/reference/cli.md +35 -16
- package/docs/reference/distribution.md +13 -6
- package/docs/reference/embedding.md +35 -14
- package/docs/reference/features-matrix.md +97 -0
- package/docs/reference/library-api.md +30 -9
- package/docs/reference/memory.md +26 -6
- package/docs/reference/models.md +11 -1
- package/docs/reference/orpo-quickstart.md +3 -4
- package/docs/reference/server-api.md +634 -22
- package/docs/reference/server-config.md +250 -121
- package/docs/reference/training.md +14 -15
- package/package.json +5 -3
- package/src/anthropic.ts +13 -0
- package/src/audio/conformer.ts +647 -0
- package/src/audio/decode.ts +133 -0
- package/src/audio/features.ts +202 -0
- package/src/audio/transcode.ts +67 -0
- package/src/cli.ts +356 -105
- package/src/config.ts +49 -2
- package/src/download.ts +40 -0
- package/src/eval/README.md +7 -6
- package/src/eval/kl.ts +59 -3
- package/src/eval/quality-db.ts +2 -2
- package/src/eval/runner.ts +3 -3
- package/src/expert-offload.ts +15 -2
- package/src/flags.ts +13 -0
- package/src/generate.ts +330 -24
- package/src/grammar.ts +585 -0
- package/src/hub-rest.ts +242 -0
- package/src/kv-store.ts +442 -88
- package/src/lora.ts +8 -0
- package/src/md-modules.d.ts +42 -0
- package/src/media-fetch.ts +284 -0
- package/src/memory/rest.ts +245 -0
- package/src/memory/vault.ts +74 -0
- package/src/mlx/array.ts +106 -19
- package/src/mlx/autograd.ts +2 -2
- package/src/mlx/ffi.ts +27 -3
- package/src/mlx/metal-kernel.ts +7 -4
- package/src/mlx/ops.ts +153 -23
- package/src/mlx/turboquant-ops.ts +831 -0
- package/src/mlx/turboquant-tables.ts +237 -0
- package/src/model/batched-mask.ts +62 -1
- package/src/model/batched-quant.ts +206 -0
- package/src/model/batched-rotating-quant.ts +346 -0
- package/src/model/batched-rotating.ts +46 -1
- package/src/model/compiled-decode.ts +4 -15
- package/src/model/factory.ts +10 -4
- package/src/model/gemma4-base.ts +363 -39
- package/src/model/gemma4.ts +122 -33
- package/src/model/generated/gemma4-12b.ts +9 -13
- package/src/model/generated/gemma4-26b.ts +10 -5
- package/src/model/generated/gemma4-e4b.ts +14 -9
- package/src/model/minicpm5.ts +58 -102
- package/src/model/paged-kv.ts +253 -0
- package/src/model/qwen3-delta.ts +76 -15
- package/src/model/qwen3-moe.ts +404 -0
- package/src/model/qwen3.ts +9 -8
- package/src/model/qwen3_5.ts +88 -23
- package/src/model/support.ts +12 -1
- package/src/model/universal/dense.ts +40 -5
- package/src/model/universal/rope.ts +36 -21
- package/src/pi-web.ts +1299 -82
- package/src/preflight.ts +27 -8
- package/src/prompt-cache.ts +293 -33
- package/src/quantize/quantizer.ts +16 -1
- package/src/registry.ts +57 -2
- package/src/serve/batch-scheduler.ts +965 -72
- package/src/serve/generation-gateway.ts +214 -11
- package/src/serve/isolate.ts +417 -0
- package/src/serve/lane-registry.ts +44 -0
- package/src/serve/session-search.ts +326 -0
- package/src/server.ts +1380 -135
- package/src/spec/assistant-source.ts +150 -0
- package/src/spec/deepspec-source.ts +143 -0
- package/src/spec/dflash-source.ts +124 -0
- package/src/spec/dspark/ab-stats.ts +136 -0
- package/src/spec/dspark/calibration.ts +112 -0
- package/src/spec/dspark/deepspec-module.ts +801 -0
- package/src/spec/dspark/generate-dflash.ts +72 -20
- package/src/spec/dspark/loader.ts +23 -0
- package/src/spec/dspark/module-dflash.ts +278 -36
- package/src/spec/dspark/quantize-drafter.ts +91 -0
- package/src/spec/generate.ts +59 -25
- package/src/spec/ngram-source.ts +132 -0
- package/src/spec/serve-loop.ts +536 -0
- package/src/spec/source.ts +93 -0
- package/src/spec/two-model.ts +171 -0
- package/src/ssd-cache.ts +107 -35
- package/src/tokenizer.ts +288 -2
- package/src/tool-approvals.ts +131 -0
- package/src/tool-call.ts +408 -20
- package/src/train/flash-cce.ts +2 -2
- package/src/train/job.ts +0 -2
- package/src/train/trainer.ts +1 -4
- package/src/vision/prompt.ts +217 -48
- package/src/web/app.html +1184 -1789
- package/src/web/app.js +5667 -0
- package/src/web/icon.svg +13 -0
- package/src/web/manifest.webmanifest +24 -0
- package/src/web/src/adapters-panel.ts +288 -0
- package/src/web/src/api.ts +57 -0
- package/src/web/src/assistant.ts +485 -0
- package/src/web/src/chat.ts +1050 -0
- package/src/web/src/composer.ts +959 -0
- package/src/web/src/dataset.ts +132 -0
- package/src/web/src/finetune.ts +267 -0
- package/src/web/src/hub.ts +428 -0
- package/src/web/src/main.ts +98 -0
- package/src/web/src/markdown.ts +0 -0
- package/src/web/src/memory-panel.ts +572 -0
- package/src/web/src/model-picker.ts +159 -0
- package/src/web/src/palette.ts +346 -0
- package/src/web/src/protocol.ts +62 -0
- package/src/web/src/quantize.ts +167 -0
- package/src/web/src/rag.ts +274 -0
- package/src/web/src/sessions.ts +469 -0
- package/src/web/src/shell.ts +804 -0
- package/src/web/src/status.ts +252 -0
- package/src/web/src/ui-catalog.ts +92 -0
- package/src/web/sw.js +71 -0
- package/src/web/vendor/README.md +61 -0
- package/src/web/vendor/hljs-LICENSE +29 -0
- package/src/web/vendor/hljs-theme.css +59 -0
- package/src/web/vendor/hljs.js +26 -0
- package/src/model/fused-decode-kernel.ts +0 -312
- package/src/model/fused-geglu-kernel.ts +0 -198
- package/src/model/fused-mlp-kernel.ts +0 -169
- package/src/model/fused-swiglu-kernel.ts +0 -240
- package/src/model/steel-linear-kernel.ts +0 -96
- package/src/status-page.html +0 -497
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// Quantize a DeepSpec-format drafter checkpoint (Phase 1a of the DSpark
|
|
2
|
+
// serving program, docs/design/dspark-serving-program.md).
|
|
3
|
+
//
|
|
4
|
+
// Thin policy layer over the house quantizer (src/quantize): the generic
|
|
5
|
+
// shape-eligibility rule already captures every 2-D matmul weight — the
|
|
6
|
+
// layer projections, fc, lm_head, markov_w2, and the two gather tables
|
|
7
|
+
// (embed_tokens, markov_w1: quantized-embedding gather-dequant at load,
|
|
8
|
+
// mlx QuantizedEmbedding semantics). The drafter-specific overlay:
|
|
9
|
+
//
|
|
10
|
+
// KEEP bf16: confidence_head.* — threshold-sensitive (the truncation
|
|
11
|
+
// comparison runs on its sigmoid; borderline flips change proposal
|
|
12
|
+
// lengths) and tiny (~4k params), so quantizing it buys nothing.
|
|
13
|
+
// Norms / layer_scalar / biases are 1-D and never shape-eligible.
|
|
14
|
+
//
|
|
15
|
+
// Drafter numerics only move ACCEPTANCE, never correctness (the target
|
|
16
|
+
// verifies every draft) — the quality gate for any scheme swept through
|
|
17
|
+
// here is the Phase-1c acceptance A/B, not a KL battery.
|
|
18
|
+
|
|
19
|
+
import { quantizeModelDir, type QuantizeResult } from "../../quantize";
|
|
20
|
+
import { DeepspecDrafter } from "./deepspec-module";
|
|
21
|
+
import { MlxArray } from "../../mlx/array";
|
|
22
|
+
import { Dtype } from "../../mlx/ffi";
|
|
23
|
+
|
|
24
|
+
/** Module bases kept full-precision regardless of shape eligibility. */
|
|
25
|
+
const KEEP_BF16 = [/^confidence_head\./];
|
|
26
|
+
|
|
27
|
+
/** The drafter per-tensor policy: quantize everything shape-eligible
|
|
28
|
+
* except the KEEP_BF16 set. */
|
|
29
|
+
export function drafterQuantizePredicate(base: string): boolean {
|
|
30
|
+
return !KEEP_BF16.some((re) => re.test(base));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface QuantizeDrafterOptions {
|
|
34
|
+
bits: 4 | 8;
|
|
35
|
+
groupSize: 32 | 64;
|
|
36
|
+
/** Skip the post-write load smoke (tests exercise loading separately). */
|
|
37
|
+
skipLoadSmoke?: boolean;
|
|
38
|
+
onProgress?: (stage: string, message: string, progress: number) => void;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Quantize the DeepSpec drafter at `srcDir` into `outDir` and smoke the
|
|
43
|
+
* result: reload through DeepspecDrafter (quantized detection + graph
|
|
44
|
+
* build) and eval one projectContext row through the quantized fc path.
|
|
45
|
+
*/
|
|
46
|
+
export async function quantizeDrafterDir(
|
|
47
|
+
srcDir: string,
|
|
48
|
+
outDir: string,
|
|
49
|
+
opts: QuantizeDrafterOptions,
|
|
50
|
+
): Promise<QuantizeResult> {
|
|
51
|
+
const raw = (await Bun.file(`${srcDir}/config.json`).json()) as Record<string, any>;
|
|
52
|
+
if (raw.architectures?.[0] !== "Gemma4DSparkModel")
|
|
53
|
+
throw new Error(
|
|
54
|
+
`quantizeDrafterDir: expected a DeepSpec checkpoint (architectures[0]==="Gemma4DSparkModel"), got ${JSON.stringify(raw.architectures)} — for regular models use \`mlx-bun quantize\``,
|
|
55
|
+
);
|
|
56
|
+
if (raw.quantization ?? raw.quantization_config)
|
|
57
|
+
throw new Error("quantizeDrafterDir: source is already quantized — start from the bf16 checkpoint");
|
|
58
|
+
|
|
59
|
+
const result = await quantizeModelDir(
|
|
60
|
+
srcDir,
|
|
61
|
+
outDir,
|
|
62
|
+
{
|
|
63
|
+
bits: opts.bits,
|
|
64
|
+
groupSize: opts.groupSize,
|
|
65
|
+
quantizePredicate: drafterQuantizePredicate,
|
|
66
|
+
},
|
|
67
|
+
opts.onProgress ? (e) => opts.onProgress!(e.stage, e.message, e.progress) : undefined,
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
if (!opts.skipLoadSmoke) {
|
|
71
|
+
// Load smoke: quantized detection + one real quantized matmul (fc) so a
|
|
72
|
+
// wrong spec/layout fails HERE, not at serve time.
|
|
73
|
+
const d = await DeepspecDrafter.load(outDir);
|
|
74
|
+
try {
|
|
75
|
+
const width = d.tapLayers.length * d.hidden;
|
|
76
|
+
const zeros = MlxArray.fromFloat32(new Float32Array(width), [1, 1, width]);
|
|
77
|
+
const x = zeros.astype(Dtype.bfloat16);
|
|
78
|
+
zeros.dispose();
|
|
79
|
+
const proj = d.projectContext(x);
|
|
80
|
+
x.dispose();
|
|
81
|
+
const got = proj.shape;
|
|
82
|
+
proj.dispose();
|
|
83
|
+
if (got.length !== 3 || got[2] !== d.hidden)
|
|
84
|
+
throw new Error(`load smoke: projectContext shape ${JSON.stringify(got)}, expected [1,1,${d.hidden}]`);
|
|
85
|
+
} finally {
|
|
86
|
+
d.dispose();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return result;
|
|
91
|
+
}
|
package/src/spec/generate.ts
CHANGED
|
@@ -128,18 +128,36 @@ export function specGenerate(
|
|
|
128
128
|
full: (caches[donors.full] as never as { temporalView(): [MlxArray, MlxArray] }).temporalView(),
|
|
129
129
|
});
|
|
130
130
|
|
|
131
|
+
// Round/prefill scratch tensors, hoisted so the finally frees them on a
|
|
132
|
+
// mid-round throw (the 2f not-trimmable throw, or any mlx op) — try-body
|
|
133
|
+
// locals are invisible to the finally. Each is nulled at its normal
|
|
134
|
+
// disposal, so the finally never double-frees. Same discipline as
|
|
135
|
+
// src/spec/serve-loop.ts / generate-dflash.ts (2026-07-06 review fix);
|
|
136
|
+
// see memory [[mlx-inline-slice-leak-pattern]].
|
|
137
|
+
let prefillH: MlxArray | null = null;
|
|
138
|
+
let lastHidden: MlxArray | null = null;
|
|
139
|
+
let vHidden: MlxArray | null = null;
|
|
140
|
+
|
|
131
141
|
try {
|
|
132
|
-
// 1. prefill
|
|
142
|
+
// 1. prefill — FULL-prompt single forward, deliberately. Per-oracle
|
|
143
|
+
// convention (2026-07-07 re-anchor): mlx-lm's speculative path drains to
|
|
144
|
+
// len-1 with no step-0 (the SERVE loop, whose oracle it is, now mirrors
|
|
145
|
+
// that — src/spec/serve-loop.ts); THIS loop's oracle is optiq
|
|
146
|
+
// spec_generate, which prefills the whole prompt in one forward and
|
|
147
|
+
// samples token0 from it (runtime/spec/runtime.py:162-170, read from the
|
|
148
|
+
// installed oracle venv) — the 2026-06-14 bit-exact gate was established
|
|
149
|
+
// against exactly that shape. Do not tail-split here.
|
|
133
150
|
const t0 = performance.now();
|
|
134
151
|
const ids = ops.fromInt32(promptTokens, [1, promptTokens.length]);
|
|
135
|
-
|
|
152
|
+
prefillH = model.forwardHidden(ids, caches);
|
|
136
153
|
ids.dispose();
|
|
137
154
|
stats.targetCalls++;
|
|
138
|
-
const H =
|
|
139
|
-
const Lp =
|
|
140
|
-
let next = pickFromHidden(model,
|
|
141
|
-
|
|
142
|
-
|
|
155
|
+
const H = prefillH.shape[2]!;
|
|
156
|
+
const Lp = prefillH.shape[1]!;
|
|
157
|
+
let next = pickFromHidden(model, prefillH, Lp - 1);
|
|
158
|
+
lastHidden = prefillH.slice([0, Lp - 1, 0], [1, Lp, H]);
|
|
159
|
+
prefillH.dispose();
|
|
160
|
+
prefillH = null;
|
|
143
161
|
stats.prefillMs = performance.now() - t0;
|
|
144
162
|
|
|
145
163
|
// EOS convention: an EOS id stops generation and counts toward
|
|
@@ -158,30 +176,38 @@ export function specGenerate(
|
|
|
158
176
|
const position = caches[0]!.offset - 1;
|
|
159
177
|
const shared = readDonors();
|
|
160
178
|
|
|
161
|
-
// 2a. draft γ tokens against the frozen donor views
|
|
179
|
+
// 2a. draft γ tokens against the frozen donor views. try/finally so a
|
|
180
|
+
// mid-draft throw still frees the chained hiddens + donor views + the
|
|
181
|
+
// in-flight emb (assistant-source.ts draft() discipline).
|
|
162
182
|
const drafts: number[] = [];
|
|
163
183
|
let dTok = next;
|
|
164
|
-
let dHid = lastHidden
|
|
184
|
+
let dHid = lastHidden!; // borrowed for k=0
|
|
165
185
|
const ownedHiddens: MlxArray[] = [];
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
186
|
+
let emb: MlxArray | null = null;
|
|
187
|
+
try {
|
|
188
|
+
for (let k = 0; k < gamma; k++) {
|
|
189
|
+
emb = embedScaled(dTok);
|
|
190
|
+
const step = drafter.forward(emb, dHid, shared, position + k);
|
|
191
|
+
emb.dispose();
|
|
192
|
+
emb = null;
|
|
193
|
+
drafts.push(step.token);
|
|
194
|
+
stats.drafted++;
|
|
195
|
+
ownedHiddens.push(step.nextHidden);
|
|
196
|
+
dTok = step.token;
|
|
197
|
+
dHid = step.nextHidden;
|
|
198
|
+
}
|
|
199
|
+
} finally {
|
|
200
|
+
emb?.dispose();
|
|
201
|
+
for (const a of ownedHiddens) a.dispose();
|
|
202
|
+
for (const [k, v] of [shared.sliding, shared.full]) {
|
|
203
|
+
k.dispose();
|
|
204
|
+
v.dispose();
|
|
205
|
+
}
|
|
180
206
|
}
|
|
181
207
|
|
|
182
208
|
// 2b. verify all γ drafts (+ the pending token) in one forward
|
|
183
209
|
const vIds = ops.fromInt32([next, ...drafts], [1, gamma + 1]);
|
|
184
|
-
|
|
210
|
+
vHidden = model.forwardHidden(vIds, caches);
|
|
185
211
|
vIds.dispose();
|
|
186
212
|
stats.targetCalls++;
|
|
187
213
|
|
|
@@ -199,6 +225,7 @@ export function specGenerate(
|
|
|
199
225
|
stats.emitted++;
|
|
200
226
|
if (isEos || stats.emitted >= maxTokens) {
|
|
201
227
|
vHidden.dispose();
|
|
228
|
+
vHidden = null;
|
|
202
229
|
break outer;
|
|
203
230
|
}
|
|
204
231
|
}
|
|
@@ -221,18 +248,25 @@ export function specGenerate(
|
|
|
221
248
|
|
|
222
249
|
if (emitIsEos || stats.emitted >= maxTokens) {
|
|
223
250
|
vHidden.dispose();
|
|
251
|
+
vHidden = null;
|
|
224
252
|
break;
|
|
225
253
|
}
|
|
226
254
|
|
|
227
255
|
// 2g. chain state
|
|
228
256
|
next = emit;
|
|
229
|
-
lastHidden
|
|
257
|
+
lastHidden!.dispose();
|
|
230
258
|
lastHidden = vHidden.slice([0, kAccept, 0], [1, kAccept + 1, H]);
|
|
231
259
|
vHidden.dispose();
|
|
260
|
+
vHidden = null;
|
|
232
261
|
}
|
|
233
262
|
stats.decodeMs = performance.now() - tDecode;
|
|
234
263
|
lastHidden.dispose();
|
|
264
|
+
lastHidden = null;
|
|
235
265
|
} finally {
|
|
266
|
+
// Survivors of a mid-round throw (each nulled at its normal disposal).
|
|
267
|
+
prefillH?.dispose();
|
|
268
|
+
lastHidden?.dispose();
|
|
269
|
+
vHidden?.dispose();
|
|
236
270
|
for (const c of caches) c.dispose();
|
|
237
271
|
}
|
|
238
272
|
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// NgramSource — MODEL-FREE prompt-lookup speculative drafting behind the
|
|
2
|
+
// DraftSource seam (src/spec/source.ts). No weights, no caches, no tokenizer:
|
|
3
|
+
// drafts are copied from the request's own token stream. When the trailing
|
|
4
|
+
// k-gram of (prompt + emitted-so-far) has occurred earlier in that stream,
|
|
5
|
+
// the tokens that followed the earlier occurrence are proposed as the draft;
|
|
6
|
+
// the shared verify/accept executor (src/spec/serve-loop.ts) makes the result
|
|
7
|
+
// LOSSLESS by construction (drafts are only proposals — the target's own
|
|
8
|
+
// samples decide every emitted token, at any temperature; only the acceptance
|
|
9
|
+
// rate moves). Best case: agentic/RAG/code-edit traffic that re-emits spans
|
|
10
|
+
// already present in context.
|
|
11
|
+
//
|
|
12
|
+
// Prior art (ported, not invented): "Prompt Lookup Decoding" (Apoorv Saxena,
|
|
13
|
+
// github.com/apoorvumang/prompt-lookup-decoding) and vLLM's `ngram` proposer
|
|
14
|
+
// (vllm/v1 spec_decode ngram_proposer) — both match the LONGEST k-gram first
|
|
15
|
+
// (k = max..min) and take the FIRST occurrence scanning left-to-right; we
|
|
16
|
+
// follow that exactly. Defaults mirror Saxena (max_ngram_size=3,
|
|
17
|
+
// num_pred_tokens=10 → our --num-draft-tokens default for this kind).
|
|
18
|
+
//
|
|
19
|
+
// Token-history reconstruction (the one subtle part): the seam hands sources
|
|
20
|
+
// the prompt at prefill() and only the FEED tail each round — never the full
|
|
21
|
+
// emitted stream — so this source rebuilds it from the feed/commit discipline
|
|
22
|
+
// (same information two-model keeps in its draft KV):
|
|
23
|
+
// - prefill: history = prompt to len-1 under the oracle tail-split shape
|
|
24
|
+
// (the last prompt token arrives as the first round's feed = [pending]);
|
|
25
|
+
// full prompt under the legacy shape (token0 arrives as its feed).
|
|
26
|
+
// - draft(feed): history += feed. feed is [correction] after a rejected
|
|
27
|
+
// round, or [lastDraft, bonus] after an all-accept round (mlx-lm's
|
|
28
|
+
// re-feed rule, generate.py:645-648) — so the all-accept round's LAST
|
|
29
|
+
// draft is deliberately NOT pushed at commit (it arrives here instead).
|
|
30
|
+
// - commit(d, kAccept): history += drafts[0 .. min(kAccept, d-1)) — the
|
|
31
|
+
// accepted drafts except that re-fed last one. Rejected tips never enter
|
|
32
|
+
// history; the correction token arrives via the next feed.
|
|
33
|
+
// Invariant at matching time (just after the feed push): history ==
|
|
34
|
+
// prompt + every emitted token, ending at the pending/anchor token.
|
|
35
|
+
//
|
|
36
|
+
// A round with no match returns [] — the serve loop's d=0 semantics degrade
|
|
37
|
+
// it to one plain target step (bit-equivalent to non-spec decode), so ngram
|
|
38
|
+
// speculation is structurally never worse than plain decode by more than the
|
|
39
|
+
// JS scan (~µs against 30k-token histories).
|
|
40
|
+
|
|
41
|
+
import { flagOn } from "../flags";
|
|
42
|
+
import type { DraftProvider, DraftSource } from "./source";
|
|
43
|
+
|
|
44
|
+
export interface NgramOptions {
|
|
45
|
+
/** Longest suffix k-gram tried first (Saxena max_ngram_size). */
|
|
46
|
+
max?: number;
|
|
47
|
+
/** Shortest k-gram tried before giving up (vLLM prompt_lookup_min). */
|
|
48
|
+
min?: number;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export class NgramProvider implements DraftProvider {
|
|
52
|
+
readonly id = "ngram";
|
|
53
|
+
readonly weightsBytes = 0;
|
|
54
|
+
readonly max: number;
|
|
55
|
+
readonly min: number;
|
|
56
|
+
|
|
57
|
+
constructor(opts: NgramOptions = {}) {
|
|
58
|
+
this.max = Math.max(1, opts.max ?? 3);
|
|
59
|
+
this.min = Math.max(1, Math.min(opts.min ?? 1, this.max));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
open(_opts: Parameters<DraftProvider["open"]>[0]): DraftSource {
|
|
63
|
+
// Sampler and target both ignored: proposals come from lookup, not a
|
|
64
|
+
// model, so drafting is sampler-independent (like the assistant source —
|
|
65
|
+
// correctness holds at any temperature, only acceptance moves).
|
|
66
|
+
return new NgramSource(this.max, this.min);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
dispose(): void {}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
class NgramSource implements DraftSource {
|
|
73
|
+
readonly weightsBytes = 0;
|
|
74
|
+
#hist: number[] = [];
|
|
75
|
+
#lastDrafts: number[] = [];
|
|
76
|
+
|
|
77
|
+
constructor(
|
|
78
|
+
private readonly max: number,
|
|
79
|
+
private readonly min: number,
|
|
80
|
+
) {}
|
|
81
|
+
|
|
82
|
+
/** Test hook — the reconstructed prompt+emitted stream (see header). */
|
|
83
|
+
get history(): readonly number[] {
|
|
84
|
+
return this.#hist;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
prefill(promptIds: number[]): void {
|
|
88
|
+
// Mirror the serve loop's prefill shape (two-model.ts does the same): under
|
|
89
|
+
// the oracle tail split the last prompt token is never prefilled — it IS
|
|
90
|
+
// the first feed. Legacy shape (kill switch / 1-token prompt): full prompt,
|
|
91
|
+
// and the sampled token0 arrives as the first feed.
|
|
92
|
+
const tailSplit = flagOn("MLX_BUN_PREFILL_TAIL_SPLIT", true);
|
|
93
|
+
const upTo = tailSplit && promptIds.length > 1 ? promptIds.length - 1 : promptIds.length;
|
|
94
|
+
this.#hist = promptIds.slice(0, upTo);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
draft(feed: number[], n: number, _stepBase: number): number[] {
|
|
98
|
+
this.#hist.push(...feed);
|
|
99
|
+
this.#lastDrafts = this.#propose(n);
|
|
100
|
+
return this.#lastDrafts;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
commit(d: number, kAccept: number): void {
|
|
104
|
+
// Accepted drafts join history, EXCEPT an all-accept round's last draft —
|
|
105
|
+
// the serve loop re-feeds it ([lastDraft, bonus]) and the next draft()'s
|
|
106
|
+
// feed push would double it (see header). d=0 rounds push nothing.
|
|
107
|
+
const upTo = Math.max(0, Math.min(kAccept, d - 1));
|
|
108
|
+
for (let i = 0; i < upTo; i++) this.#hist.push(this.#lastDrafts[i]!);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Longest-k-first, first-occurrence prompt lookup (Saxena/vLLM order):
|
|
112
|
+
* find the trailing k-gram earlier in history, propose what followed it. */
|
|
113
|
+
#propose(n: number): number[] {
|
|
114
|
+
const h = this.#hist;
|
|
115
|
+
const L = h.length;
|
|
116
|
+
const kTop = Math.min(this.max, L - 1);
|
|
117
|
+
for (let k = kTop; k >= this.min; k--) {
|
|
118
|
+
const tailAt = L - k;
|
|
119
|
+
// First occurrence, left to right; i + k < L both excludes the trailing
|
|
120
|
+
// self-match and guarantees at least one continuation token.
|
|
121
|
+
search: for (let i = 0; i + k < L; i++) {
|
|
122
|
+
for (let j = 0; j < k; j++) {
|
|
123
|
+
if (h[i + j] !== h[tailAt + j]) continue search;
|
|
124
|
+
}
|
|
125
|
+
return h.slice(i + k, i + k + n);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return [];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
dispose(): void {}
|
|
132
|
+
}
|