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,617 @@
|
|
|
1
|
+
// Serve-time speculative decoding — the ONE verify/accept executor behind
|
|
2
|
+
// `serve --draft-model` (docs/design/mlx-lm-tool-parity-plan.md §7; sequenced
|
|
3
|
+
// by docs/design/grammar-spec-batching-integration.md Phase B).
|
|
4
|
+
//
|
|
5
|
+
// Faithful to mlx-lm's speculative_generate_step (generate.py:473-654, read
|
|
6
|
+
// from the oracle venv): per round, draft n tokens from the DraftSource, run
|
|
7
|
+
// ONE target forward over [pending, ...drafts] (n+1 positions), sample the
|
|
8
|
+
// target per position over the BATCHED lm-head logits (one matmul — matches
|
|
9
|
+
// both oracles; see src/spec/generate.ts header on why this legitimately
|
|
10
|
+
// diverges from stock decode at bf16 knife-edges), accept the longest prefix
|
|
11
|
+
// where target token == draft token (exact token-match acceptance, NOT
|
|
12
|
+
// distribution-level rejection sampling), emit the correction (or bonus)
|
|
13
|
+
// token, and trim the target caches by the rejected count.
|
|
14
|
+
//
|
|
15
|
+
// Deviations from upstream, deliberate and documented:
|
|
16
|
+
// - Rotating-cache ring wrap: upstream RAISES when a cache stops being
|
|
17
|
+
// trimmable mid-generation; a serve endpoint must not 500 mid-stream, so
|
|
18
|
+
// we STOP SPECULATING and finish the generation with plain single-token
|
|
19
|
+
// decode (bit-equivalent continuation — the target's own samples).
|
|
20
|
+
// - Prompt-cache reuse is BYPASSED in v1 (fresh caches per spec request,
|
|
21
|
+
// cachedTokens=0). mlx-lm composes spec with its LRU prompt cache
|
|
22
|
+
// (target+draft caches per entry); wiring that through our PromptCache +
|
|
23
|
+
// SSD tier is a tracked follow-up in the integration plan, not silently
|
|
24
|
+
// absent.
|
|
25
|
+
//
|
|
26
|
+
// Grammar × spec (Phase C, the constrained verify walk — novel: NO runtime
|
|
27
|
+
// serves both; mlx-lm has no grammar, oMLX no spec): the drafter runs FREE
|
|
28
|
+
// (drafts are proposals; a grammar-invalid draft is simply rejected at
|
|
29
|
+
// verify), and the grammar mask rides the per-position accept walk in
|
|
30
|
+
// samplePos — mask before sample, matcher advances on emitted tokens only,
|
|
31
|
+
// so rejected drafts never touch grammar state and no matcher rollback is
|
|
32
|
+
// needed. Grammar termination mid-burst truncates the round (the all--inf
|
|
33
|
+
// guarantee). Gates (no oracle): greedy grammar+spec ≡ greedy grammar-only
|
|
34
|
+
// long-prefix + 100% schema validity (tests/spec-serve.test.ts).
|
|
35
|
+
//
|
|
36
|
+
// Emission discipline: tokens flow through the caller's onToken ONE AT A
|
|
37
|
+
// TIME, in order (bursts of ≤ n+1 per round) — the server's stop-sequence
|
|
38
|
+
// matcher and detokenizer see exactly the stream they'd see from generate().
|
|
39
|
+
// EOS is never emitted as content (generate() parity). onToken returning
|
|
40
|
+
// false halts the generation mid-burst.
|
|
41
|
+
|
|
42
|
+
import { MlxArray } from "../mlx/array";
|
|
43
|
+
import * as ops from "../mlx/ops";
|
|
44
|
+
import { clearCache } from "../mlx/ffi";
|
|
45
|
+
import { flagOn } from "../flags";
|
|
46
|
+
import type { RuntimeModel } from "../model/factory";
|
|
47
|
+
import type { Cache } from "../model/gemma4";
|
|
48
|
+
import type { GenerateOptions, GenerateStats } from "../generate";
|
|
49
|
+
import { makeSampler, makeLogitsProcessors, toLogprobs } from "../sampler";
|
|
50
|
+
import type { OnToken } from "../serve/generation-gateway";
|
|
51
|
+
import type { DraftProvider } from "./source";
|
|
52
|
+
|
|
53
|
+
const PREFILL_CHUNK = 2048;
|
|
54
|
+
|
|
55
|
+
/** Run one target forward, and when `tapLayers` is set (⟹ a Gemma4 target — a
|
|
56
|
+
* DSpark-style source), also return the captured multi-layer context
|
|
57
|
+
* [1,L,m*H] (tapLayers concatenated on the feature axis; index nLayers is the
|
|
58
|
+
* post-finalNorm sentinel). Non-tapping sources get ctxML=null and never
|
|
59
|
+
* touch model.hiddenTap. Mirrors generate-dflash.ts forwardTapped. */
|
|
60
|
+
async function forwardMaybeTap(
|
|
61
|
+
model: RuntimeModel,
|
|
62
|
+
ids: MlxArray,
|
|
63
|
+
caches: Cache[],
|
|
64
|
+
tapLayers: number[] | undefined,
|
|
65
|
+
): Promise<{ hidden: MlxArray; ctxML: MlxArray | null }> {
|
|
66
|
+
if (!tapLayers) {
|
|
67
|
+
const asyncModel = model as RuntimeModel & {
|
|
68
|
+
forwardHiddenAsync?: (
|
|
69
|
+
ids: MlxArray,
|
|
70
|
+
caches: Cache[],
|
|
71
|
+
) => Promise<MlxArray>;
|
|
72
|
+
};
|
|
73
|
+
const hidden = typeof asyncModel.forwardHiddenAsync === "function"
|
|
74
|
+
? await asyncModel.forwardHiddenAsync(ids, caches)
|
|
75
|
+
: model.forwardHidden(ids, caches);
|
|
76
|
+
return { hidden, ctxML: null };
|
|
77
|
+
}
|
|
78
|
+
const m = model as unknown as {
|
|
79
|
+
hiddenTap: { layers: Set<number>; captured: Map<number, MlxArray> } | null;
|
|
80
|
+
};
|
|
81
|
+
const cap = new Map<number, MlxArray>();
|
|
82
|
+
m.hiddenTap = { layers: new Set(tapLayers), captured: cap };
|
|
83
|
+
let hidden: MlxArray | null = null;
|
|
84
|
+
try {
|
|
85
|
+
hidden = model.forwardHidden(ids, caches);
|
|
86
|
+
const perLayer = tapLayers.map((li) => {
|
|
87
|
+
const a = cap.get(li);
|
|
88
|
+
if (!a) throw new Error(`spec tap: layer ${li} not captured`);
|
|
89
|
+
return a;
|
|
90
|
+
});
|
|
91
|
+
const ctxML = ops.concatAxis(perLayer, 2); // [1,L,m*H]
|
|
92
|
+
for (const [, a] of cap) a.dispose();
|
|
93
|
+
cap.clear(); // consumed — the finally must not double-dispose
|
|
94
|
+
const out = { hidden, ctxML };
|
|
95
|
+
hidden = null; // ownership returned to the caller
|
|
96
|
+
return out;
|
|
97
|
+
} finally {
|
|
98
|
+
// On any throw (forward mid-capture, missing layer, concat), free the
|
|
99
|
+
// partially-captured tap tensors and the orphaned hidden. On success both
|
|
100
|
+
// are already gone (cap cleared, hidden nulled).
|
|
101
|
+
hidden?.dispose();
|
|
102
|
+
for (const [, a] of cap) a.dispose();
|
|
103
|
+
m.hiddenTap = null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export interface SpecServeExtras {
|
|
108
|
+
drafted: number;
|
|
109
|
+
accepted: number;
|
|
110
|
+
rejected: number;
|
|
111
|
+
targetCalls: number;
|
|
112
|
+
rounds: number;
|
|
113
|
+
acceptanceLengths: number[];
|
|
114
|
+
tokensPerForward: number;
|
|
115
|
+
forwardsSaved: number;
|
|
116
|
+
/** Per-draft-position counters (index = position within a round's block,
|
|
117
|
+
* 0..γ-1): how many rounds drafted/accepted at that position. Drives the
|
|
118
|
+
* Phase-1c per-position acceptance report (scripts/dspark-drafter-ab.ts)
|
|
119
|
+
* — near-zero cost, always populated. */
|
|
120
|
+
draftedByPos: number[];
|
|
121
|
+
acceptedByPos: number[];
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export async function specServeRun(
|
|
125
|
+
model: RuntimeModel,
|
|
126
|
+
provider: DraftProvider,
|
|
127
|
+
numDraftTokens: number,
|
|
128
|
+
promptIds: number[],
|
|
129
|
+
options: GenerateOptions & { stopSequences?: string[] },
|
|
130
|
+
onToken: OnToken,
|
|
131
|
+
): Promise<GenerateStats> {
|
|
132
|
+
const maxTokens = options.maxTokens ?? 512;
|
|
133
|
+
const eos = options.eosTokenIds ?? model.config.eosTokenIds;
|
|
134
|
+
const sampler = makeSampler(options);
|
|
135
|
+
const processors = makeLogitsProcessors(options);
|
|
136
|
+
const gamma = Math.max(1, numDraftTokens);
|
|
137
|
+
|
|
138
|
+
// Allocated INSIDE the try below (2026-07-07 review): provider.open()
|
|
139
|
+
// throws on a mismatched (target, drafter) pairing — as pre-try consts a
|
|
140
|
+
// throw leaked options.grammar (a live WASM matcher) and the fresh caches,
|
|
141
|
+
// and turned a config error into a per-request 500. loadContext now
|
|
142
|
+
// probe-opens the pairing at startup, so a throw here is belt+suspenders.
|
|
143
|
+
let caches: Cache[] = [];
|
|
144
|
+
let source: import("./source").DraftSource | null = null;
|
|
145
|
+
let tapLayers: number[] | undefined;
|
|
146
|
+
// Target final hidden [1,1,H] at the anchor position — the assistant source
|
|
147
|
+
// borrows it for its first draft step of each round; two-model/dflash ignore
|
|
148
|
+
// it. Retained across rounds, disposed in finally.
|
|
149
|
+
let anchorHidden: MlxArray | null = null;
|
|
150
|
+
// Round/prefill scratch tensors, hoisted so the finally disposes them on ANY
|
|
151
|
+
// throw mid-round/prefill (grammar WASM reject, awaited onToken reject, a GPU
|
|
152
|
+
// error). Each is nulled the instant its in-body disposal or ownership
|
|
153
|
+
// transfer runs, so the finally never double-frees. (A serve endpoint hits
|
|
154
|
+
// these throw seams per request; a try-body local would leak GPU memory.)
|
|
155
|
+
let lastLogits: MlxArray | null = null;
|
|
156
|
+
let prefillCtx: MlxArray | null = null;
|
|
157
|
+
const ctxParts: MlxArray[] = []; // per-chunk tapped context (DSpark only)
|
|
158
|
+
let vHidden: MlxArray | null = null;
|
|
159
|
+
let vCtxML: MlxArray | null = null;
|
|
160
|
+
let vLogits: MlxArray | null = null;
|
|
161
|
+
let roundRow: MlxArray | null = null; // the in-flight verify/continuation row
|
|
162
|
+
// Device-side token history for the logits processors (repetition/presence/
|
|
163
|
+
// frequency penalties, logit_bias) — generate()'s pushHistory discipline.
|
|
164
|
+
let history: MlxArray | null =
|
|
165
|
+
processors.length > 0 ? ops.fromInt32(promptIds, [promptIds.length]) : null;
|
|
166
|
+
|
|
167
|
+
const extras: SpecServeExtras = {
|
|
168
|
+
drafted: 0,
|
|
169
|
+
accepted: 0,
|
|
170
|
+
rejected: 0,
|
|
171
|
+
targetCalls: 0,
|
|
172
|
+
rounds: 0,
|
|
173
|
+
acceptanceLengths: [],
|
|
174
|
+
tokensPerForward: 0,
|
|
175
|
+
forwardsSaved: 0,
|
|
176
|
+
draftedByPos: [],
|
|
177
|
+
acceptedByPos: [],
|
|
178
|
+
};
|
|
179
|
+
const stats: GenerateStats = {
|
|
180
|
+
promptTokens: promptIds.length,
|
|
181
|
+
cachedTokens: 0,
|
|
182
|
+
generatedTokens: 0,
|
|
183
|
+
prefillMs: 0, decodeMs: 0, prefillTps: 0, decodeTps: 0,
|
|
184
|
+
cacheTokens: [],
|
|
185
|
+
spec: extras,
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
/** Sample ONE verify position from a [1,V] logits row: processors →
|
|
189
|
+
* grammar mask (Phase C: the constrained verify walk — the mask has the
|
|
190
|
+
* final say, same ordering as serial sampleStep and the batch closure) →
|
|
191
|
+
* log-softmax → sampler. The GRAMMAR walk is inherently sequential: the
|
|
192
|
+
* mask at position i reflects every token emitted before it, so this
|
|
193
|
+
* awaits the matcher's async fill (fired by the previous accept) before
|
|
194
|
+
* masking, samples, then advances the matcher — the matcher only ever
|
|
195
|
+
* moves on tokens that are (about to be) EMITTED, which is what makes
|
|
196
|
+
* rollback-free v1 correct: rejected DRAFTS never touch grammar state.
|
|
197
|
+
* Appends the sampled token to the processor history. */
|
|
198
|
+
const grammar = options.grammar;
|
|
199
|
+
const samplePos = async (logits1V: MlxArray, step: number): Promise<number> => {
|
|
200
|
+
// Owned intermediates drained in the finally on any throw — grammar
|
|
201
|
+
// fill reject, applyMask/toLogprobs/sampler error (2026-07-07 review:
|
|
202
|
+
// these were the one spot the header's hoist discipline missed; a
|
|
203
|
+
// grammar+penalties request whose matcher fill rejects orphaned one
|
|
204
|
+
// [1,V] row per occurrence). `cur` is owned only once it diverges from
|
|
205
|
+
// the caller's row.
|
|
206
|
+
let cur: MlxArray = logits1V;
|
|
207
|
+
let lp: MlxArray | null = null;
|
|
208
|
+
let tokArr: MlxArray | null = null;
|
|
209
|
+
try {
|
|
210
|
+
for (const p of processors) {
|
|
211
|
+
const next = p(history, cur);
|
|
212
|
+
if (cur !== logits1V) cur.dispose();
|
|
213
|
+
cur = next;
|
|
214
|
+
}
|
|
215
|
+
if (grammar && !grammar.isTerminated) {
|
|
216
|
+
await grammar.ready();
|
|
217
|
+
const masked = grammar.applyMask(cur);
|
|
218
|
+
if (cur !== logits1V) cur.dispose();
|
|
219
|
+
cur = masked;
|
|
220
|
+
}
|
|
221
|
+
lp = toLogprobs(cur);
|
|
222
|
+
if (cur !== logits1V) cur.dispose();
|
|
223
|
+
cur = logits1V; // consumed — nothing owned under this name now
|
|
224
|
+
tokArr = sampler(lp, step);
|
|
225
|
+
lp.dispose();
|
|
226
|
+
lp = null;
|
|
227
|
+
const tok = ops.itemUint32(tokArr);
|
|
228
|
+
tokArr.dispose();
|
|
229
|
+
tokArr = null;
|
|
230
|
+
// Advance the matcher on the emitted token (fires the next async fill).
|
|
231
|
+
// EOS is never content and never grammar-valid — don't feed it.
|
|
232
|
+
if (grammar && !eos.includes(tok)) grammar.accept(tok);
|
|
233
|
+
if (history) {
|
|
234
|
+
const t1 = ops.fromInt32([tok], [1]);
|
|
235
|
+
const prev = history;
|
|
236
|
+
history = ops.concatAxis([prev, t1], 0);
|
|
237
|
+
prev.dispose();
|
|
238
|
+
t1.dispose();
|
|
239
|
+
}
|
|
240
|
+
return tok;
|
|
241
|
+
} finally {
|
|
242
|
+
if (cur !== logits1V) cur.dispose();
|
|
243
|
+
lp?.dispose();
|
|
244
|
+
tokArr?.dispose();
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
/** The [1,V] logits row at position `pos` of a hidden window — batched
|
|
249
|
+
* lm-head is applied by the caller ONCE; this slices its output. */
|
|
250
|
+
const logitsRow = (logitsWindow: MlxArray, pos: number): MlxArray => {
|
|
251
|
+
const V = logitsWindow.shape[logitsWindow.shape.length - 1]!;
|
|
252
|
+
const sl = logitsWindow.slice([0, pos, 0], [1, pos + 1, V]);
|
|
253
|
+
const flat = ops.reshape(sl, [1, V]);
|
|
254
|
+
sl.dispose();
|
|
255
|
+
return flat;
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
try {
|
|
259
|
+
caches = model.makeCache();
|
|
260
|
+
const src = provider.open({ sampler, target: { model, caches } });
|
|
261
|
+
source = src;
|
|
262
|
+
tapLayers = src.tapLayers;
|
|
263
|
+
// ---- prefill (target, chunked; optionally tapped for DSpark), then seed
|
|
264
|
+
// the source. Order: the source's prefill needs the tapped context that the
|
|
265
|
+
// target prefill produces, so target-first (two-model's own draft prefill
|
|
266
|
+
// and the assistant no-op are order-independent). ----
|
|
267
|
+
//
|
|
268
|
+
// Oracle prefill convention (re-anchored 2026-07-07 after PR #18; then
|
|
269
|
+
// corrected to the SPEC oracle's true shape after a live gate vs
|
|
270
|
+
// speculative_generate_step still flipped a knife-edge): mlx-lm's
|
|
271
|
+
// speculative path drains BOTH models to len-1 (`_prefill: while y.size >
|
|
272
|
+
// 1`) and has NO separate step-0 at all — the un-drained last prompt
|
|
273
|
+
// token HEADS THE FIRST VERIFY WINDOW ([lastPromptToken, ...drafts]),
|
|
274
|
+
// and the first emitted token is that window's position-0 sample
|
|
275
|
+
// (generate.py:578-618). A separate L=1 step-0 (the serial-lane
|
|
276
|
+
// convention) is ulp-different from the (1+γ)-window GEMM head and still
|
|
277
|
+
// flips near-ties. So under the flag: drain to len-1, pending = the last
|
|
278
|
+
// prompt token, straight into the rounds. Gated live: 4/4 token-for-token
|
|
279
|
+
// vs the oracle venv (γ∈{2,3} × 2 prompts, incl. the knife-edge cell).
|
|
280
|
+
// MLX_BUN_PREFILL_TAIL_SPLIT=0 reverts to the legacy full-chunk + sampled
|
|
281
|
+
// token0 shape (kill switch; also the 1-token-prompt degenerate path).
|
|
282
|
+
//
|
|
283
|
+
// Seam interaction: the tapped seed covers rows 0..len-2 only — the last
|
|
284
|
+
// prompt token's row arrives via the FIRST verify round's vCtxML (it is
|
|
285
|
+
// that round's anchor row, which commit() appends) — so context sources
|
|
286
|
+
// stay in exact lockstep, and DSpark's round-1 anchor IS the last prompt
|
|
287
|
+
// token at position len-1, matching DeepSpec's reference loop. The
|
|
288
|
+
// assistant source's first-round anchor hidden = "the hidden that
|
|
289
|
+
// produced pending" = position len-2, the last drain chunk's final row.
|
|
290
|
+
const t0 = performance.now();
|
|
291
|
+
const tailSplit =
|
|
292
|
+
src.prefillMode !== "full" &&
|
|
293
|
+
flagOn("MLX_BUN_PREFILL_TAIL_SPLIT", true);
|
|
294
|
+
const oracleShape = tailSplit && promptIds.length > 1;
|
|
295
|
+
if (oracleShape) {
|
|
296
|
+
let pos = 0;
|
|
297
|
+
const end = promptIds.length - 1; // last prompt token NEVER prefilled
|
|
298
|
+
while (pos < end) {
|
|
299
|
+
const n = Math.min(PREFILL_CHUNK, end - pos);
|
|
300
|
+
const chunk = promptIds.slice(pos, pos + n);
|
|
301
|
+
const ids = ops.fromInt32(chunk, [1, chunk.length]);
|
|
302
|
+
const { hidden: h, ctxML } = await forwardMaybeTap(
|
|
303
|
+
model,
|
|
304
|
+
ids,
|
|
305
|
+
caches,
|
|
306
|
+
tapLayers,
|
|
307
|
+
);
|
|
308
|
+
ids.dispose();
|
|
309
|
+
if (ctxML) ctxParts.push(ctxML);
|
|
310
|
+
if (pos + n >= end) {
|
|
311
|
+
const L = h.shape[1]!;
|
|
312
|
+
const H = h.shape[2]!;
|
|
313
|
+
anchorHidden = h.slice([0, L - 1, 0], [1, L, H]); // position len-2
|
|
314
|
+
}
|
|
315
|
+
h.dispose(); // logits never computed during the drain
|
|
316
|
+
clearCache();
|
|
317
|
+
pos += n;
|
|
318
|
+
}
|
|
319
|
+
} else {
|
|
320
|
+
// Legacy shape: full chunked prefill (final chunk included), token0
|
|
321
|
+
// sampled from the last position below.
|
|
322
|
+
for (let off = 0; off < promptIds.length; off += PREFILL_CHUNK) {
|
|
323
|
+
const chunk = promptIds.slice(off, off + PREFILL_CHUNK);
|
|
324
|
+
const ids = ops.fromInt32(chunk, [1, chunk.length]);
|
|
325
|
+
const { hidden: h, ctxML } = await forwardMaybeTap(
|
|
326
|
+
model,
|
|
327
|
+
ids,
|
|
328
|
+
caches,
|
|
329
|
+
tapLayers,
|
|
330
|
+
);
|
|
331
|
+
ids.dispose();
|
|
332
|
+
if (ctxML) ctxParts.push(ctxML);
|
|
333
|
+
if (off + PREFILL_CHUNK >= promptIds.length) {
|
|
334
|
+
const L = h.shape[1]!;
|
|
335
|
+
const H = h.shape[2]!;
|
|
336
|
+
anchorHidden = h.slice([0, L - 1, 0], [1, L, H]); // [1,1,H], retained
|
|
337
|
+
const lg = model.logitsFromHidden(anchorHidden);
|
|
338
|
+
const V = lg.shape[lg.shape.length - 1]!;
|
|
339
|
+
lastLogits = ops.reshape(lg, [1, V]);
|
|
340
|
+
lg.dispose();
|
|
341
|
+
}
|
|
342
|
+
h.dispose();
|
|
343
|
+
clearCache();
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
extras.targetCalls++;
|
|
347
|
+
// Seed the source: two-model prefills its own draft cache; DSpark seeds
|
|
348
|
+
// H_ctx from the tapped prompt context (ownership transfers to prefill);
|
|
349
|
+
// the assistant is a no-op.
|
|
350
|
+
if (tapLayers) {
|
|
351
|
+
if (ctxParts.length === 1) prefillCtx = ctxParts[0]!;
|
|
352
|
+
else if (ctxParts.length > 1) {
|
|
353
|
+
prefillCtx = ops.concatAxis(ctxParts, 1); // [1,Lp,m*H]
|
|
354
|
+
for (const p of ctxParts) p.dispose();
|
|
355
|
+
}
|
|
356
|
+
ctxParts.length = 0; // parts consumed (transferred as prefillCtx or disposed)
|
|
357
|
+
await src.prefill(promptIds, prefillCtx ?? undefined); // takes ownership of prefillCtx
|
|
358
|
+
prefillCtx = null; // ownership transferred
|
|
359
|
+
} else {
|
|
360
|
+
await src.prefill(promptIds);
|
|
361
|
+
}
|
|
362
|
+
stats.prefillMs = performance.now() - t0;
|
|
363
|
+
stats.prefillTps = (promptIds.length / Math.max(stats.prefillMs, 1e-6)) * 1000;
|
|
364
|
+
|
|
365
|
+
// ---- first pending token ----
|
|
366
|
+
// Oracle shape: pending = the UNPROCESSED last prompt token — never
|
|
367
|
+
// emitted (it's prompt), it heads the first verify window; the first
|
|
368
|
+
// generated token is that window's position-0 sample, inside the walk.
|
|
369
|
+
// Legacy shape: sample + emit token0 from the prefill logits (old
|
|
370
|
+
// convention, kill-switch / 1-token prompts).
|
|
371
|
+
const tDecode = performance.now();
|
|
372
|
+
let pending: number;
|
|
373
|
+
if (oracleShape) {
|
|
374
|
+
pending = promptIds[promptIds.length - 1]!;
|
|
375
|
+
} else {
|
|
376
|
+
pending = await samplePos(lastLogits!, 0);
|
|
377
|
+
lastLogits!.dispose();
|
|
378
|
+
lastLogits = null;
|
|
379
|
+
if (eos.includes(pending)) {
|
|
380
|
+
stats.decodeMs = performance.now() - tDecode;
|
|
381
|
+
return stats;
|
|
382
|
+
}
|
|
383
|
+
stats.generatedTokens++;
|
|
384
|
+
if (
|
|
385
|
+
(await onToken(pending)) === false ||
|
|
386
|
+
stats.generatedTokens >= maxTokens ||
|
|
387
|
+
// grammar satisfied at token 0 (a 1-token grammar) — finish "stop"
|
|
388
|
+
grammar?.isTerminated
|
|
389
|
+
) {
|
|
390
|
+
stats.decodeMs = performance.now() - tDecode;
|
|
391
|
+
return stats;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
let feed: number[] = [pending];
|
|
396
|
+
let speculating = true;
|
|
397
|
+
|
|
398
|
+
// ---- rounds ----
|
|
399
|
+
decode: while (stats.generatedTokens < maxTokens) {
|
|
400
|
+
if (!speculating) {
|
|
401
|
+
// plain single-token continuation (post-ring-wrap fallback)
|
|
402
|
+
const ids = ops.fromInt32([pending], [1, 1]);
|
|
403
|
+
const { hidden: h } = await forwardMaybeTap(
|
|
404
|
+
model,
|
|
405
|
+
ids,
|
|
406
|
+
caches,
|
|
407
|
+
undefined,
|
|
408
|
+
);
|
|
409
|
+
ids.dispose();
|
|
410
|
+
const lg = model.logitsFromHidden(h);
|
|
411
|
+
h.dispose();
|
|
412
|
+
const V = lg.shape[lg.shape.length - 1]!;
|
|
413
|
+
roundRow = ops.reshape(lg, [1, V]);
|
|
414
|
+
lg.dispose();
|
|
415
|
+
extras.targetCalls++;
|
|
416
|
+
const tok = await samplePos(roundRow, stats.generatedTokens);
|
|
417
|
+
roundRow.dispose();
|
|
418
|
+
roundRow = null;
|
|
419
|
+
clearCache();
|
|
420
|
+
if (eos.includes(tok)) break;
|
|
421
|
+
stats.generatedTokens++;
|
|
422
|
+
pending = tok;
|
|
423
|
+
if ((await onToken(tok)) === false) break;
|
|
424
|
+
if (grammar?.isTerminated) break; // grammar closed — finish "stop"
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const n = Math.min(gamma, Math.max(1, maxTokens - stats.generatedTokens));
|
|
429
|
+
|
|
430
|
+
// Ring-wrap gate, BEFORE the round writes anything: a rejected-draft
|
|
431
|
+
// rollback needs trim(), and a RotatingKVCache stops being trimmable
|
|
432
|
+
// once its ring wraps (offset ≥ maxSize) — at which point the rejected
|
|
433
|
+
// KV would be woven into the live window (unrecoverable, upstream
|
|
434
|
+
// RAISES here, generate.py:529-533). Degrade to plain decode while the
|
|
435
|
+
// caches are still clean instead of 500ing a stream.
|
|
436
|
+
const roundFits = caches.every((c) =>
|
|
437
|
+
"maxSize" in c && typeof (c as { maxSize?: number }).maxSize === "number"
|
|
438
|
+
? c.offset + n + 1 < (c as { maxSize: number }).maxSize
|
|
439
|
+
: c.isTrimmable(),
|
|
440
|
+
);
|
|
441
|
+
if (!roundFits) {
|
|
442
|
+
console.warn(
|
|
443
|
+
"spec: sliding window nearly wrapped — finishing without speculation",
|
|
444
|
+
);
|
|
445
|
+
speculating = false;
|
|
446
|
+
continue;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// (a) draft UP TO n tokens (request sampler, mlx-lm parity). The return
|
|
450
|
+
// length d is authoritative — DSpark's confidence scheduler may prune the
|
|
451
|
+
// block short or skip it entirely. The assistant source
|
|
452
|
+
// borrows the target's anchor hidden; two-model/dflash ignore it.
|
|
453
|
+
const drafts = await src.draft(
|
|
454
|
+
feed,
|
|
455
|
+
n,
|
|
456
|
+
stats.generatedTokens,
|
|
457
|
+
anchorHidden ?? undefined,
|
|
458
|
+
);
|
|
459
|
+
const d = drafts.length;
|
|
460
|
+
if (d < 0 || d > n) throw new Error(`DraftSource returned ${d} drafts (contract: 0..${n})`);
|
|
461
|
+
if (Bun.env.MLX_BUN_SPEC_TRACE === "1") {
|
|
462
|
+
console.error(
|
|
463
|
+
`[SPEC_TRACE] round=${extras.rounds + 1} ` +
|
|
464
|
+
`feed=${feed.join(",")} drafts=${drafts.join(",")}`,
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
// d === 0 (a confidence scheduler skipping the round, DeepSpec ℓ=0
|
|
468
|
+
// semantics) needs NO special case: the verify window degenerates to
|
|
469
|
+
// [pending] alone, the accept walk to the single bonus position, commit
|
|
470
|
+
// to a 0-accept round (tapped sources still grow context by the anchor
|
|
471
|
+
// row — lockstep with the target cache).
|
|
472
|
+
extras.drafted += d;
|
|
473
|
+
for (let i = 0; i < d; i++)
|
|
474
|
+
extras.draftedByPos[i] = (extras.draftedByPos[i] ?? 0) + 1;
|
|
475
|
+
|
|
476
|
+
// (b) ONE target forward over [pending, ...drafts] (optionally tapped for
|
|
477
|
+
// DSpark's H_ctx). vHidden is retained past the accept walk: its slice at
|
|
478
|
+
// the emitted position is the next round's anchor hidden.
|
|
479
|
+
const vIds = ops.fromInt32([pending, ...drafts], [1, d + 1]);
|
|
480
|
+
const pinTarget = src.pinTargetKernelFamily === true &&
|
|
481
|
+
"setSpecKernelPinned" in model;
|
|
482
|
+
if (pinTarget) model.setSpecKernelPinned(true);
|
|
483
|
+
try {
|
|
484
|
+
const ft = await forwardMaybeTap(model, vIds, caches, tapLayers);
|
|
485
|
+
// Assign these before the lm-head call so the outer cleanup owns them
|
|
486
|
+
// even if logits construction throws.
|
|
487
|
+
vHidden = ft.hidden;
|
|
488
|
+
vCtxML = ft.ctxML;
|
|
489
|
+
vLogits = model.logitsFromHidden(vHidden);
|
|
490
|
+
} finally {
|
|
491
|
+
vIds.dispose();
|
|
492
|
+
if (pinTarget) model.setSpecKernelPinned(false);
|
|
493
|
+
}
|
|
494
|
+
extras.targetCalls++;
|
|
495
|
+
// The lm-head remains one logical batched operation. Native GLM MTP's
|
|
496
|
+
// SPEC_PIN implementation evaluates its rows through the same M=1
|
|
497
|
+
// quantized family before concatenating them.
|
|
498
|
+
|
|
499
|
+
// (c) per-position accept walk: sample the target at each position,
|
|
500
|
+
// accept while it reproduces the draft. Sampling is sequential because
|
|
501
|
+
// the processor history (and, in Phase C, the grammar mask) at position
|
|
502
|
+
// i depends on the tokens accepted at positions < i.
|
|
503
|
+
let kAccept = 0;
|
|
504
|
+
let correction: number | null = null; // target's token at first mismatch (or bonus)
|
|
505
|
+
let sawEos = false;
|
|
506
|
+
let halted = false;
|
|
507
|
+
const emitted: number[] = [];
|
|
508
|
+
let grammarDone = false;
|
|
509
|
+
for (let i = 0; i <= d; i++) {
|
|
510
|
+
roundRow = logitsRow(vLogits!, i);
|
|
511
|
+
const tok = await samplePos(roundRow, stats.generatedTokens + emitted.length);
|
|
512
|
+
roundRow.dispose();
|
|
513
|
+
roundRow = null;
|
|
514
|
+
if (i < d && tok === drafts[i]) {
|
|
515
|
+
kAccept++;
|
|
516
|
+
// EOS is never content — even when it arrives as an ACCEPTED DRAFT
|
|
517
|
+
// (the correction/bonus branch below always excluded it; this
|
|
518
|
+
// branch pushed-then-broke, leaking the EOS through onToken —
|
|
519
|
+
// caught by the 2026-07-07 live oracle gate: streams were
|
|
520
|
+
// bit-identical, ours emitted one extra "content" token: <|eot_id|>).
|
|
521
|
+
if (eos.includes(tok)) { sawEos = true; break; }
|
|
522
|
+
emitted.push(tok);
|
|
523
|
+
// grammar termination mid-burst truncates the round — nothing may
|
|
524
|
+
// be sampled past a satisfied grammar (the all--inf guarantee).
|
|
525
|
+
if (grammar?.isTerminated) { grammarDone = true; break; }
|
|
526
|
+
if (stats.generatedTokens + emitted.length >= maxTokens) break;
|
|
527
|
+
continue;
|
|
528
|
+
}
|
|
529
|
+
// mismatch (target replaces the draft) or bonus (i === n)
|
|
530
|
+
correction = tok;
|
|
531
|
+
if (!eos.includes(tok)) emitted.push(tok);
|
|
532
|
+
else sawEos = true;
|
|
533
|
+
if (grammar?.isTerminated) grammarDone = true;
|
|
534
|
+
break;
|
|
535
|
+
}
|
|
536
|
+
vLogits!.dispose();
|
|
537
|
+
vLogits = null;
|
|
538
|
+
extras.accepted += kAccept;
|
|
539
|
+
extras.rejected = extras.drafted - extras.accepted;
|
|
540
|
+
extras.rounds++;
|
|
541
|
+
extras.acceptanceLengths.push(kAccept);
|
|
542
|
+
for (let i = 0; i < kAccept; i++)
|
|
543
|
+
extras.acceptedByPos[i] = (extras.acceptedByPos[i] ?? 0) + 1;
|
|
544
|
+
clearCache();
|
|
545
|
+
|
|
546
|
+
// (d) emit the round's tokens through onToken, one at a time
|
|
547
|
+
for (const tok of emitted) {
|
|
548
|
+
stats.generatedTokens++;
|
|
549
|
+
if ((await onToken(tok)) === false) { halted = true; break; }
|
|
550
|
+
if (stats.generatedTokens >= maxTokens) break;
|
|
551
|
+
}
|
|
552
|
+
extras.tokensPerForward = extras.rounds > 0
|
|
553
|
+
? stats.generatedTokens / extras.rounds
|
|
554
|
+
: 0;
|
|
555
|
+
extras.forwardsSaved = Math.max(
|
|
556
|
+
0,
|
|
557
|
+
Math.max(0, stats.generatedTokens - 1) - extras.rounds,
|
|
558
|
+
);
|
|
559
|
+
const stop = sawEos || halted || grammarDone || stats.generatedTokens >= maxTokens;
|
|
560
|
+
|
|
561
|
+
// (e) roll back the rejected suffix + commit to the source (skipped on
|
|
562
|
+
// stop — the target caches are about to be disposed). commit takes
|
|
563
|
+
// ownership of vCtxML (DSpark grows H_ctx by the accepted window; the
|
|
564
|
+
// others ignore it). The next anchor = the target hidden at the emitted
|
|
565
|
+
// position (the assistant borrows it; generate.ts:230 pattern — the slice
|
|
566
|
+
// outlives its parent's dispose).
|
|
567
|
+
if (!stop) {
|
|
568
|
+
if (kAccept < d) for (const c of caches) c.trim(d - kAccept);
|
|
569
|
+
await src.commit(
|
|
570
|
+
d,
|
|
571
|
+
kAccept,
|
|
572
|
+
vCtxML ?? undefined,
|
|
573
|
+
vHidden,
|
|
574
|
+
drafts.slice(0, kAccept),
|
|
575
|
+
); // takes ownership of vCtxML; verifiedHidden stays caller-owned
|
|
576
|
+
vCtxML = null;
|
|
577
|
+
const H = vHidden!.shape[2]!;
|
|
578
|
+
anchorHidden?.dispose();
|
|
579
|
+
anchorHidden = vHidden!.slice([0, kAccept, 0], [1, kAccept + 1, H]); // [1,1,H]
|
|
580
|
+
} else {
|
|
581
|
+
vCtxML?.dispose();
|
|
582
|
+
vCtxML = null;
|
|
583
|
+
}
|
|
584
|
+
vHidden!.dispose();
|
|
585
|
+
vHidden = null;
|
|
586
|
+
if (stop) break decode;
|
|
587
|
+
|
|
588
|
+
// (f) chain: mlx-lm's re-feed rule (generate.py:645-648)
|
|
589
|
+
const emit = correction!; // non-null: the walk always sets it unless it broke on EOS/max inside accepts
|
|
590
|
+
if (d > 0 && kAccept === d) feed = [drafts[d - 1]!, emit]; // d=0 has no last draft to re-feed
|
|
591
|
+
else feed = [emit];
|
|
592
|
+
pending = emit;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
stats.decodeMs = performance.now() - tDecode;
|
|
596
|
+
stats.decodeTps = stats.decodeMs
|
|
597
|
+
? (stats.generatedTokens / stats.decodeMs) * 1000
|
|
598
|
+
: 0;
|
|
599
|
+
return stats;
|
|
600
|
+
} finally {
|
|
601
|
+
// Dispose whatever a mid-round/prefill throw left live (each is nulled the
|
|
602
|
+
// instant its normal disposal or ownership transfer ran, so no double-free).
|
|
603
|
+
anchorHidden?.dispose();
|
|
604
|
+
lastLogits?.dispose();
|
|
605
|
+
prefillCtx?.dispose();
|
|
606
|
+
for (const p of ctxParts) p.dispose();
|
|
607
|
+
vLogits?.dispose();
|
|
608
|
+
roundRow?.dispose();
|
|
609
|
+
vHidden?.dispose();
|
|
610
|
+
vCtxML?.dispose();
|
|
611
|
+
for (const c of caches) c.dispose();
|
|
612
|
+
source?.dispose();
|
|
613
|
+
history?.dispose();
|
|
614
|
+
options.grammar?.dispose(); // Phase C will consume it; never leak either way
|
|
615
|
+
clearCache();
|
|
616
|
+
}
|
|
617
|
+
}
|