mlx-bun 0.0.10 → 0.0.12
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 +107 -32
- package/THIRD_PARTY_LICENSES.md +10 -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/colibri-glm52-port.md +1479 -0
- 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/pre-colibri-stabilization.md +329 -0
- 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/colibri-oracle-pin.md +1284 -0
- package/docs/investigations/dspark-handoff.md +182 -16
- package/docs/investigations/indexshare-performance-spike.md +357 -0
- 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/planning/release-notes-v0.0.12.md +80 -0
- package/docs/reference/cli.md +63 -18
- package/docs/reference/distribution.md +37 -6
- package/docs/reference/embedding.md +40 -16
- package/docs/reference/features-matrix.md +99 -0
- package/docs/reference/library-api.md +69 -20
- package/docs/reference/memory.md +26 -6
- package/docs/reference/models.md +60 -3
- package/docs/reference/orpo-quickstart.md +3 -4
- package/docs/reference/server-api.md +717 -24
- package/docs/reference/server-config.md +265 -129
- package/docs/reference/training.md +24 -16
- package/package.json +5 -3
- package/src/anthropic.ts +67 -44
- 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/chat-template.ts +155 -10
- package/src/cli.ts +483 -113
- package/src/config.ts +116 -24
- package/src/diffusion/diffusion-generate.ts +14 -8
- package/src/download.ts +143 -12
- package/src/eval/README.md +7 -6
- package/src/eval/ifeval.ts +63 -120
- package/src/eval/kl.ts +59 -3
- package/src/eval/quality-db.ts +2 -2
- package/src/eval/runner.ts +17 -8
- package/src/eval/tasks/ifeval.ts +167 -31
- package/src/expert-io.ts +391 -0
- package/src/expert-offload.ts +15 -2
- package/src/expert-residency.ts +1134 -0
- package/src/expert-usage.ts +415 -0
- package/src/flags.ts +13 -0
- package/src/generate.ts +438 -35
- package/src/grammar.ts +585 -0
- package/src/hf-push.ts +1 -1
- package/src/hub-rest.ts +242 -0
- package/src/index.ts +14 -2
- package/src/jobs/runner.ts +37 -18
- package/src/kv-store.ts +586 -93
- package/src/lora.ts +85 -14
- package/src/md-modules.d.ts +42 -0
- package/src/media-fetch.ts +426 -0
- package/src/memory/rest.ts +245 -0
- package/src/memory/vault.ts +74 -0
- package/src/mlx/array.ts +149 -19
- package/src/mlx/autograd.ts +53 -16
- package/src/mlx/checkpoint.ts +46 -13
- package/src/mlx/custom-vjp.ts +83 -35
- 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 +103 -6
- package/src/model/gemma4-base.ts +395 -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/glm52-atlas.ts +511 -0
- package/src/model/glm52-cache.ts +706 -0
- package/src/model/glm52-config.ts +262 -0
- package/src/model/glm52-container.ts +305 -0
- package/src/model/glm52-coupling.ts +388 -0
- package/src/model/glm52-dsa.ts +602 -0
- package/src/model/glm52-expert-layout.ts +253 -0
- package/src/model/glm52-layout.ts +152 -0
- package/src/model/glm52-memory.ts +473 -0
- package/src/model/glm52-mla.ts +786 -0
- package/src/model/glm52-moe.ts +223 -0
- package/src/model/glm52-pilot.ts +537 -0
- package/src/model/glm52-reference.ts +445 -0
- package/src/model/glm52-residency.ts +557 -0
- package/src/model/glm52-resident-weights.ts +135 -0
- package/src/model/glm52-streamed-experts.ts +453 -0
- package/src/model/glm52-streamed-metal.ts +610 -0
- package/src/model/glm52-weights.ts +503 -0
- package/src/model/glm52.ts +1153 -0
- 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 +19 -1
- package/src/model/universal/dense.ts +40 -5
- package/src/model/universal/rope.ts +36 -21
- package/src/native/expert_io.c +523 -0
- package/src/native-pack.ts +6 -4
- 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/responses.ts +62 -49
- package/src/serve/batch-scheduler.ts +1050 -78
- package/src/serve/generation-gateway.ts +252 -24
- 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 +1701 -175
- 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/glm52-mtp-source.ts +392 -0
- package/src/spec/ngram-source.ts +132 -0
- package/src/spec/serve-loop.ts +617 -0
- package/src/spec/source.ts +118 -0
- package/src/spec/two-model.ts +171 -0
- package/src/ssd-cache.ts +111 -35
- package/src/tokenizer.ts +288 -2
- package/src/tool-approvals.ts +131 -0
- package/src/tool-call.ts +438 -22
- package/src/train/flash-cce.ts +2 -2
- package/src/train/job.ts +0 -12
- package/src/train/lora-params.ts +17 -6
- package/src/train/trainer.ts +60 -30
- package/src/vision/prompt.ts +217 -48
- package/src/web/app.html +1197 -1791
- package/src/web/app.js +5694 -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 +300 -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/web-tools.ts +54 -2
- package/src/weights.ts +82 -19
- 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,392 @@
|
|
|
1
|
+
// Native GLM-5.2 MTP draft source.
|
|
2
|
+
//
|
|
3
|
+
// The target and MTP row share embeddings, the output head, and the resident
|
|
4
|
+
// dense target weights. Only the MTP layer's own dense tensors and int8 routed
|
|
5
|
+
// experts add model weight residency. The generic speculative serve loop owns
|
|
6
|
+
// verification, grammar composition, target-cache rollback, sampling, and
|
|
7
|
+
// telemetry. Prompt lookup remains the model-free alternative DraftProvider;
|
|
8
|
+
// selecting one provider per request prevents two independent draft histories
|
|
9
|
+
// from advancing the target or grammar state twice.
|
|
10
|
+
|
|
11
|
+
import { MlxArray } from "../mlx/array";
|
|
12
|
+
import * as ops from "../mlx/ops";
|
|
13
|
+
import { toLogprobs } from "../sampler";
|
|
14
|
+
import { Glm52DecoderLayer, Glm52Model } from "../model/glm52";
|
|
15
|
+
import { MLACache } from "../model/glm52-cache";
|
|
16
|
+
import { rmsNormF32Mlx } from "../model/glm52-mla";
|
|
17
|
+
import type { DraftProvider, DraftSource } from "./source";
|
|
18
|
+
|
|
19
|
+
type Sampler = (logprobs: MlxArray, step: number) => MlxArray;
|
|
20
|
+
|
|
21
|
+
function lastToken(feed: readonly number[]): number {
|
|
22
|
+
const token = feed.at(-1);
|
|
23
|
+
if (!Number.isSafeInteger(token) || token! < 0)
|
|
24
|
+
throw new Error("native MTP requires a non-empty non-negative token feed");
|
|
25
|
+
return token!;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export class Glm52NativeMtpProvider implements DraftProvider {
|
|
29
|
+
readonly id = "glm52-native-mtp";
|
|
30
|
+
readonly weightsBytes = 0;
|
|
31
|
+
readonly #layer: Glm52DecoderLayer;
|
|
32
|
+
|
|
33
|
+
constructor(readonly model: Glm52Model) {
|
|
34
|
+
if (!model.capabilities.mtpMetadata)
|
|
35
|
+
throw new Error("GLM-5.2 artifact does not contain a complete MTP row");
|
|
36
|
+
if (model.capabilities.mtpEnabled === false)
|
|
37
|
+
throw new Error("native GLM-5.2 MTP is disabled for this model instance");
|
|
38
|
+
if (model.glmConfig.numNextnPredictLayers !== 1) {
|
|
39
|
+
throw new Error(
|
|
40
|
+
`native GLM-5.2 MTP requires exactly one next-token layer; got ` +
|
|
41
|
+
`${model.glmConfig.numNextnPredictLayers}`,
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
const mtpBackend = model.expertRuntime?.mtpExecutor ?? null;
|
|
45
|
+
if (model.expertBackend && !mtpBackend) {
|
|
46
|
+
throw new Error(
|
|
47
|
+
"streamed GLM-5.2 MTP requires the bounded int8 MTP expert tier",
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
this.#layer = new Glm52DecoderLayer(
|
|
51
|
+
model.glmConfig,
|
|
52
|
+
model.weights,
|
|
53
|
+
model.glmConfig.numHiddenLayers,
|
|
54
|
+
false,
|
|
55
|
+
mtpBackend,
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
open(opts: Parameters<DraftProvider["open"]>[0]): DraftSource {
|
|
60
|
+
if (opts.target.model !== this.model)
|
|
61
|
+
throw new Error("native MTP provider was opened for a different target");
|
|
62
|
+
return new Glm52NativeMtpSource(this.model, this.#layer, opts.sampler);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
dispose(): void {
|
|
66
|
+
// The target model owns every shared/MTP weight and the expert tier.
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export class Glm52NativeMtpSource implements DraftSource {
|
|
71
|
+
readonly prefillMode = "full" as const;
|
|
72
|
+
readonly pinTargetKernelFamily = true;
|
|
73
|
+
readonly weightsBytes = 0;
|
|
74
|
+
#cache: MLACache;
|
|
75
|
+
#roundStart = 0;
|
|
76
|
+
#lastDraftCount = 0;
|
|
77
|
+
#closed = false;
|
|
78
|
+
|
|
79
|
+
constructor(
|
|
80
|
+
readonly model: Glm52Model,
|
|
81
|
+
readonly layer: Glm52DecoderLayer,
|
|
82
|
+
readonly sampler: Sampler,
|
|
83
|
+
) {
|
|
84
|
+
this.#cache = new MLACache({
|
|
85
|
+
kvLoraRank: model.glmConfig.kvLoraRank,
|
|
86
|
+
ropeHeadDim: model.glmConfig.qkRopeHeadDim,
|
|
87
|
+
maxTokens: model.glmConfig.maxPositionEmbeddings,
|
|
88
|
+
role: "mtp",
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
get cacheOffset(): number {
|
|
93
|
+
return this.#cache.offset;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
get cacheBytes(): number {
|
|
97
|
+
return this.#cache.byteLength;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Caller-owned zero-copy snapshot for the v3 KV persistence writer. */
|
|
101
|
+
clonePersistentCache(): MLACache {
|
|
102
|
+
this.#checkOpen();
|
|
103
|
+
if (this.#lastDraftCount !== 0)
|
|
104
|
+
throw new Error("native MTP cache cannot snapshot an uncommitted round");
|
|
105
|
+
if (this.#cache.offset === 0)
|
|
106
|
+
throw new Error("native MTP cache cannot snapshot empty state");
|
|
107
|
+
const state = this.#cache.fetch();
|
|
108
|
+
const clone = new MLACache({
|
|
109
|
+
kvLoraRank: this.#cache.kvLoraRank,
|
|
110
|
+
ropeHeadDim: this.#cache.ropeHeadDim,
|
|
111
|
+
maxTokens: this.#cache.maxTokens,
|
|
112
|
+
role: "mtp",
|
|
113
|
+
});
|
|
114
|
+
try {
|
|
115
|
+
clone.restoreCompressedState(state.latent, state.rope, null, this.#cache.offset);
|
|
116
|
+
return clone;
|
|
117
|
+
} catch (error) {
|
|
118
|
+
state.latent.dispose();
|
|
119
|
+
state.rope.dispose();
|
|
120
|
+
clone.dispose();
|
|
121
|
+
throw error;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Adopt one restored `mtp-mla` cache. Ownership transfers only after every
|
|
127
|
+
* role/geometry/offset check passes; the source disposes it thereafter.
|
|
128
|
+
*/
|
|
129
|
+
restorePersistentCache(cache: MLACache): void {
|
|
130
|
+
this.#checkOpen();
|
|
131
|
+
if (this.#lastDraftCount !== 0)
|
|
132
|
+
throw new Error("native MTP cache cannot restore during a draft round");
|
|
133
|
+
if (this.#cache.offset !== 0)
|
|
134
|
+
throw new Error("native MTP cache restore requires an empty source");
|
|
135
|
+
if (cache.role !== "mtp" || cache.dsa)
|
|
136
|
+
throw new Error("native MTP restore requires one mtp-mla cache");
|
|
137
|
+
if (
|
|
138
|
+
cache.kvLoraRank !== this.#cache.kvLoraRank ||
|
|
139
|
+
cache.ropeHeadDim !== this.#cache.ropeHeadDim ||
|
|
140
|
+
cache.maxTokens !== this.#cache.maxTokens
|
|
141
|
+
) {
|
|
142
|
+
throw new Error("native MTP restored cache geometry does not match model");
|
|
143
|
+
}
|
|
144
|
+
if (cache.offset <= 0 || cache.batchSize !== 1)
|
|
145
|
+
throw new Error("native MTP restored cache must contain one non-empty row");
|
|
146
|
+
this.#cache.dispose();
|
|
147
|
+
this.#cache = cache;
|
|
148
|
+
this.#roundStart = cache.offset;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
prefill(_promptIds: number[]): void {
|
|
152
|
+
this.#checkOpen();
|
|
153
|
+
if (this.#cache.offset !== 0)
|
|
154
|
+
throw new Error("native MTP source cannot be prefilled twice");
|
|
155
|
+
// Direct Colibri intentionally leaves MTP KV empty here ("niente
|
|
156
|
+
// prefill"): its first draft opens a decode-only window from the target
|
|
157
|
+
// anchor hidden and pending token. Seeding this cache with the prompt
|
|
158
|
+
// would change both the oracle state and the first speculative round.
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async draft(
|
|
162
|
+
feed: number[],
|
|
163
|
+
n: number,
|
|
164
|
+
stepBase: number,
|
|
165
|
+
anchorHidden?: MlxArray,
|
|
166
|
+
): Promise<number[]> {
|
|
167
|
+
this.#checkOpen();
|
|
168
|
+
if (!anchorHidden)
|
|
169
|
+
throw new Error("native MTP drafting requires the target anchor hidden");
|
|
170
|
+
if (!Number.isSafeInteger(n) || n < 0)
|
|
171
|
+
throw new Error(`native MTP draft length must be non-negative; got ${n}`);
|
|
172
|
+
if (this.#lastDraftCount !== 0)
|
|
173
|
+
throw new Error("native MTP draft called before the prior round committed");
|
|
174
|
+
const [batch, tokens, hidden] = anchorHidden.shape;
|
|
175
|
+
if (batch !== 1 || tokens !== 1 || hidden !== this.model.glmConfig.hiddenSize) {
|
|
176
|
+
throw new Error(
|
|
177
|
+
`native MTP anchor must be [1,1,${this.model.glmConfig.hiddenSize}], ` +
|
|
178
|
+
`got [${anchorHidden.shape.join(",")}]`,
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
this.#roundStart = this.#cache.offset;
|
|
183
|
+
let token = lastToken(feed);
|
|
184
|
+
let state: MlxArray | null = ops.contiguous(anchorHidden);
|
|
185
|
+
const drafts: number[] = [];
|
|
186
|
+
try {
|
|
187
|
+
for (let position = 0; position < n; position++) {
|
|
188
|
+
const nextState = await this.#step(token, state, stepBase + position);
|
|
189
|
+
state.dispose();
|
|
190
|
+
state = nextState.hidden;
|
|
191
|
+
token = nextState.token;
|
|
192
|
+
drafts.push(token);
|
|
193
|
+
}
|
|
194
|
+
this.#lastDraftCount = drafts.length;
|
|
195
|
+
return drafts;
|
|
196
|
+
} catch (error) {
|
|
197
|
+
const appended = this.#cache.offset - this.#roundStart;
|
|
198
|
+
if (appended > 0) this.#cache.trim(appended);
|
|
199
|
+
throw error;
|
|
200
|
+
} finally {
|
|
201
|
+
state?.dispose();
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async commit(
|
|
206
|
+
d: number,
|
|
207
|
+
kAccept: number,
|
|
208
|
+
vCtxML?: MlxArray,
|
|
209
|
+
verifiedHidden?: MlxArray,
|
|
210
|
+
acceptedTokens: readonly number[] = [],
|
|
211
|
+
): Promise<void> {
|
|
212
|
+
this.#checkOpen();
|
|
213
|
+
vCtxML?.dispose();
|
|
214
|
+
if (d !== this.#lastDraftCount)
|
|
215
|
+
throw new Error(`native MTP commit d=${d}, expected ${this.#lastDraftCount}`);
|
|
216
|
+
if (!Number.isSafeInteger(kAccept) || kAccept < 0 || kAccept > d)
|
|
217
|
+
throw new Error(`native MTP accepted ${kAccept} outside [0,${d}]`);
|
|
218
|
+
if (acceptedTokens.length !== kAccept)
|
|
219
|
+
throw new Error(
|
|
220
|
+
`native MTP received ${acceptedTokens.length} accepted tokens for k=${kAccept}`,
|
|
221
|
+
);
|
|
222
|
+
if (kAccept > 0 && !verifiedHidden)
|
|
223
|
+
throw new Error("native MTP absorption requires verified target hidden rows");
|
|
224
|
+
|
|
225
|
+
try {
|
|
226
|
+
// Only the first speculative MTP row is conditioned on a true target
|
|
227
|
+
// hidden. Later rows use earlier MTP hiddens, so accepted positions are
|
|
228
|
+
// rebuilt below from the target's verified hidden window.
|
|
229
|
+
const speculativeTail = Math.max(d - 1, 0);
|
|
230
|
+
if (speculativeTail > 0) this.#cache.trim(speculativeTail);
|
|
231
|
+
if (kAccept > 0)
|
|
232
|
+
await this.#absorb(acceptedTokens, verifiedHidden!);
|
|
233
|
+
const expected = this.#roundStart + (d > 0 ? 1 : 0) + kAccept;
|
|
234
|
+
if (this.#cache.offset !== expected) {
|
|
235
|
+
throw new Error(
|
|
236
|
+
`native MTP cache committed to ${this.#cache.offset}, expected ${expected}`,
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
} catch (error) {
|
|
240
|
+
const appended = this.#cache.offset - this.#roundStart;
|
|
241
|
+
if (appended > 0) this.#cache.trim(appended);
|
|
242
|
+
throw error;
|
|
243
|
+
} finally {
|
|
244
|
+
this.#lastDraftCount = 0;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
dispose(): void {
|
|
249
|
+
if (this.#closed) return;
|
|
250
|
+
this.#closed = true;
|
|
251
|
+
this.#cache.dispose();
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
async #step(
|
|
255
|
+
token: number,
|
|
256
|
+
hidden: MlxArray,
|
|
257
|
+
sampleStep: number,
|
|
258
|
+
): Promise<{ hidden: MlxArray; token: number }> {
|
|
259
|
+
const config = this.model.glmConfig;
|
|
260
|
+
const ids = ops.fromInt32([token], [1, 1]);
|
|
261
|
+
let embedded: MlxArray | null = null;
|
|
262
|
+
let embeddedNorm: MlxArray | null = null;
|
|
263
|
+
let hiddenNorm: MlxArray | null = null;
|
|
264
|
+
let joined: MlxArray | null = null;
|
|
265
|
+
let projected: MlxArray | null = null;
|
|
266
|
+
let output: MlxArray | null = null;
|
|
267
|
+
let headInput: MlxArray | null = null;
|
|
268
|
+
let logits: MlxArray | null = null;
|
|
269
|
+
let logprobs: MlxArray | null = null;
|
|
270
|
+
let sampled: MlxArray | null = null;
|
|
271
|
+
try {
|
|
272
|
+
embedded = this.model.weights.embedding(
|
|
273
|
+
ids,
|
|
274
|
+
"model.embed_tokens.weight",
|
|
275
|
+
config.vocabSize,
|
|
276
|
+
config.hiddenSize,
|
|
277
|
+
);
|
|
278
|
+
embeddedNorm = rmsNormF32Mlx(
|
|
279
|
+
embedded,
|
|
280
|
+
this.model.weights.tensor(
|
|
281
|
+
`model.layers.${config.numHiddenLayers}.enorm.weight`,
|
|
282
|
+
),
|
|
283
|
+
config.rmsNormEps,
|
|
284
|
+
);
|
|
285
|
+
hiddenNorm = rmsNormF32Mlx(
|
|
286
|
+
hidden,
|
|
287
|
+
this.model.weights.tensor(
|
|
288
|
+
`model.layers.${config.numHiddenLayers}.hnorm.weight`,
|
|
289
|
+
),
|
|
290
|
+
config.rmsNormEps,
|
|
291
|
+
);
|
|
292
|
+
joined = ops.concatAxis([embeddedNorm, hiddenNorm], 2);
|
|
293
|
+
projected = this.model.weights.linear(
|
|
294
|
+
joined,
|
|
295
|
+
`model.layers.${config.numHiddenLayers}.eh_proj.weight`,
|
|
296
|
+
config.hiddenSize,
|
|
297
|
+
2 * config.hiddenSize,
|
|
298
|
+
);
|
|
299
|
+
output = await this.layer.forwardAsync(projected, this.#cache, null);
|
|
300
|
+
headInput = rmsNormF32Mlx(
|
|
301
|
+
output,
|
|
302
|
+
this.model.weights.tensor(
|
|
303
|
+
`model.layers.${config.numHiddenLayers}.shared_head.norm.weight`,
|
|
304
|
+
),
|
|
305
|
+
config.rmsNormEps,
|
|
306
|
+
);
|
|
307
|
+
logits = this.model.logitsFromHidden(headInput);
|
|
308
|
+
logprobs = toLogprobs(logits);
|
|
309
|
+
sampled = this.sampler(logprobs, sampleStep);
|
|
310
|
+
return {
|
|
311
|
+
hidden: ops.contiguous(output),
|
|
312
|
+
token: ops.itemUint32(sampled),
|
|
313
|
+
};
|
|
314
|
+
} finally {
|
|
315
|
+
ids.dispose();
|
|
316
|
+
embedded?.dispose();
|
|
317
|
+
embeddedNorm?.dispose();
|
|
318
|
+
hiddenNorm?.dispose();
|
|
319
|
+
joined?.dispose();
|
|
320
|
+
projected?.dispose();
|
|
321
|
+
output?.dispose();
|
|
322
|
+
headInput?.dispose();
|
|
323
|
+
logits?.dispose();
|
|
324
|
+
logprobs?.dispose();
|
|
325
|
+
sampled?.dispose();
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
async #absorb(
|
|
330
|
+
acceptedTokens: readonly number[],
|
|
331
|
+
verifiedHidden: MlxArray,
|
|
332
|
+
): Promise<void> {
|
|
333
|
+
const config = this.model.glmConfig;
|
|
334
|
+
const k = acceptedTokens.length;
|
|
335
|
+
const ids = ops.fromInt32([...acceptedTokens], [1, k]);
|
|
336
|
+
let embedded: MlxArray | null = null;
|
|
337
|
+
let embeddedNorm: MlxArray | null = null;
|
|
338
|
+
let trueRows: MlxArray | null = null;
|
|
339
|
+
let hiddenNorm: MlxArray | null = null;
|
|
340
|
+
let joined: MlxArray | null = null;
|
|
341
|
+
let projected: MlxArray | null = null;
|
|
342
|
+
let output: MlxArray | null = null;
|
|
343
|
+
try {
|
|
344
|
+
embedded = this.model.weights.embedding(
|
|
345
|
+
ids,
|
|
346
|
+
"model.embed_tokens.weight",
|
|
347
|
+
config.vocabSize,
|
|
348
|
+
config.hiddenSize,
|
|
349
|
+
);
|
|
350
|
+
embeddedNorm = rmsNormF32Mlx(
|
|
351
|
+
embedded,
|
|
352
|
+
this.model.weights.tensor(
|
|
353
|
+
`model.layers.${config.numHiddenLayers}.enorm.weight`,
|
|
354
|
+
),
|
|
355
|
+
config.rmsNormEps,
|
|
356
|
+
);
|
|
357
|
+
trueRows = verifiedHidden.slice(
|
|
358
|
+
[0, 0, 0],
|
|
359
|
+
[1, k, config.hiddenSize],
|
|
360
|
+
);
|
|
361
|
+
hiddenNorm = rmsNormF32Mlx(
|
|
362
|
+
trueRows,
|
|
363
|
+
this.model.weights.tensor(
|
|
364
|
+
`model.layers.${config.numHiddenLayers}.hnorm.weight`,
|
|
365
|
+
),
|
|
366
|
+
config.rmsNormEps,
|
|
367
|
+
);
|
|
368
|
+
joined = ops.concatAxis([embeddedNorm, hiddenNorm], 2);
|
|
369
|
+
projected = this.model.weights.linear(
|
|
370
|
+
joined,
|
|
371
|
+
`model.layers.${config.numHiddenLayers}.eh_proj.weight`,
|
|
372
|
+
config.hiddenSize,
|
|
373
|
+
2 * config.hiddenSize,
|
|
374
|
+
);
|
|
375
|
+
output = await this.layer.forwardAsync(projected, this.#cache, null);
|
|
376
|
+
output.eval();
|
|
377
|
+
} finally {
|
|
378
|
+
ids.dispose();
|
|
379
|
+
embedded?.dispose();
|
|
380
|
+
embeddedNorm?.dispose();
|
|
381
|
+
trueRows?.dispose();
|
|
382
|
+
hiddenNorm?.dispose();
|
|
383
|
+
joined?.dispose();
|
|
384
|
+
projected?.dispose();
|
|
385
|
+
output?.dispose();
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
#checkOpen(): void {
|
|
390
|
+
if (this.#closed) throw new Error("native MTP source is closed");
|
|
391
|
+
}
|
|
392
|
+
}
|