mlx-bun 0.4.0 → 0.5.0
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 +57 -32
- package/docs/README.md +55 -42
- package/docs/archive/investigations/benchmark-ledger-through-v0.4.0.md +3617 -0
- package/docs/archive/planning/release-notes-v0.5.0.md +37 -0
- package/docs/design/batching.md +220 -7
- package/docs/design/decode-speed-program.md +121 -66
- package/docs/design/generic-model-support.md +116 -0
- package/docs/design/kv-cache.md +380 -211
- package/docs/design/speculative-decoding.md +165 -26
- package/docs/design/turboquant.md +20 -7
- package/docs/design/unified-engine-frontier-plan.md +120 -13
- package/docs/planning/PRODUCT_ROADMAP.md +5 -6
- package/docs/planning/public-readiness-handoff.md +244 -0
- package/docs/planning/public-readiness.md +149 -0
- package/docs/reference/benchmarks.md +3463 -3489
- package/docs/reference/cli.md +62 -1
- package/docs/reference/distribution.md +29 -4
- package/docs/reference/environment.md +104 -8
- package/docs/reference/library-api.md +18 -4
- package/docs/reference/memory.md +1 -1
- package/docs/reference/models.md +17 -5
- package/docs/reference/server-api.md +181 -11
- package/docs/reference/server-config.md +232 -105
- package/package.json +2 -2
- package/src/audio/audiotoolbox.ts +112 -0
- package/src/audio/conformer.ts +8 -0
- package/src/audio/mic-capture.ts +98 -0
- package/src/audio/silero-vad.ts +379 -0
- package/src/audio/whisper-decode.ts +774 -0
- package/src/audio/whisper-fast.ts +732 -0
- package/src/audio/whisper-format.ts +73 -0
- package/src/audio/whisper-mel.ts +185 -0
- package/src/audio/whisper-timing.ts +274 -0
- package/src/audio/whisper-tokenizer.ts +239 -0
- package/src/audio/whisper-transcribe.ts +410 -0
- package/src/backends/mlx/autoregressive.ts +22 -1
- package/src/backends/mlx/batch-group.ts +111 -27
- package/src/backends/mlx/cache-layout.ts +3 -3
- package/src/backends/mlx/continuation-persistence.ts +5 -4
- package/src/backends/mlx/encoder-cache.ts +25 -0
- package/src/backends/mlx/fill-append.ts +21 -8
- package/src/backends/mlx/fill-group.ts +497 -0
- package/src/backends/mlx/gateway-binding.ts +56 -12
- package/src/backends/mlx/kv-maintenance.ts +30 -2
- package/src/backends/mlx/mixed-iteration.ts +62 -0
- package/src/backends/mlx/model-prompt.ts +46 -32
- package/src/backends/mlx/model-serving.ts +4 -3
- package/src/backends/mlx/paged-cache-codec.ts +27 -0
- package/src/backends/mlx/prefill-cohort.ts +15 -4
- package/src/backends/mlx/prefill-policy.ts +26 -0
- package/src/backends/mlx/prefill-rows.ts +57 -13
- package/src/backends/mlx/prompt-input.ts +29 -0
- package/src/backends/mlx/qwen-prompt-input.ts +40 -0
- package/src/backends/mlx/request-state-policy.ts +18 -5
- package/src/backends/mlx/rollback.ts +6 -3
- package/src/backends/mlx/serial-executor.ts +16 -7
- package/src/backends/mlx/speculative-group.ts +105 -26
- package/src/backends/mlx/speculative.ts +24 -7
- package/src/backends/mlx/target-layout-capability.ts +1 -1
- package/src/backends/mlx/transitioning-kv-rows.ts +8 -0
- package/src/cli.ts +322 -0
- package/src/contracts/execution.ts +10 -0
- package/src/contracts/object-cache.ts +8 -0
- package/src/contracts/pi-provider.ts +1 -1
- package/src/contracts/pi.ts +3 -1
- package/src/contracts/prefix-cache.ts +13 -3
- package/src/contracts/scheduling.ts +9 -0
- package/src/engine/execution-plan.ts +23 -12
- package/src/engine/execution-tasks.ts +44 -0
- package/src/engine/namespaced-cache.ts +13 -0
- package/src/engine/scheduler.ts +22 -2
- package/src/fill/fill-session.ts +14 -3
- package/src/generate.ts +28 -22
- package/src/grammar.ts +13 -0
- package/src/inference/prefill.ts +6 -0
- package/src/kv-scheme.ts +22 -0
- package/src/kv-store.ts +115 -79
- package/src/lab/README.md +5 -1
- package/src/lab/paged-kv/paged-attention.ts +117 -0
- package/src/lab/paged-kv/paged-kv-rows.ts +33 -5
- package/src/lab/paged-kv/paged-kv.ts +108 -42
- package/src/md-modules.d.ts +5 -0
- package/src/mlx/array.ts +37 -0
- package/src/mlx/ffi.ts +19 -0
- package/src/mlx/normalized-argmax.ts +116 -0
- package/src/mlx/ops.ts +97 -10
- package/src/model/batched-turboquant-kv.ts +6 -5
- package/src/model/delayed-quantized-kv.ts +3 -3
- package/src/model/delayed-rotating-quantized-kv.ts +1 -1
- package/src/model/delayed-turboquant-kv.ts +6 -4
- package/src/model/factory.ts +26 -0
- package/src/model/fingerprint.ts +12 -0
- package/src/model/full-prefill-row.ts +1 -1
- package/src/model/gemma4-base.ts +25 -6
- package/src/model/gemma4.ts +82 -2
- package/src/model/kv-attention-view.ts +4 -1
- package/src/model/profile.ts +29 -4
- package/src/model/quantized-append-attention.ts +31 -0
- package/src/model/qwen3-mrope.ts +7 -9
- package/src/model/qwen3_5.ts +99 -37
- package/src/model/support.ts +8 -0
- package/src/model/token-groups.ts +66 -0
- package/src/model/trellis-linear.ts +2 -2
- package/src/model/turboquant-codec.ts +6 -0
- package/src/model/whisper.ts +435 -0
- package/src/native/mic_capture.swift +92 -0
- package/src/native-pack.ts +6 -5
- package/src/pi-web.ts +9 -1
- package/src/prompt-cache.ts +265 -43
- package/src/runtime-config.ts +1 -5
- package/src/safetensors.ts +9 -2
- package/src/sampler.ts +5 -4
- package/src/serve/audio-routes.ts +323 -0
- package/src/serve/chat-request.ts +2 -0
- package/src/serve/chat-stage.ts +17 -12
- package/src/serve/completion-executor.ts +9 -1
- package/src/serve/discovery-routes.ts +17 -1
- package/src/serve/generated-token-history.ts +57 -0
- package/src/serve/generation-gateway.ts +52 -11
- package/src/serve/model-binding.ts +7 -1
- package/src/serve/preparation.ts +3 -3
- package/src/serve/prompt-response-trace.ts +14 -0
- package/src/serve/request-plan.ts +2 -0
- package/src/serve/request-prep.ts +38 -21
- package/src/serve/transcription-server.ts +64 -0
- package/src/serve/transcription-service.ts +502 -0
- package/src/server.ts +132 -164
- package/src/spec/assistant-rows.ts +1 -0
- package/src/spec/deepspec-rows.ts +1 -1
- package/src/spec/dflash-source.ts +1 -1
- package/src/spec/glm52-mtp-source.ts +1 -0
- package/src/spec/ngram-source.ts +27 -5
- package/src/spec/projected-draft-rows.ts +2 -2
- package/src/spec/qwen-mtp-rows.ts +11 -0
- package/src/spec/qwen-mtp-source.ts +3 -0
- package/src/spec/serve-loop.ts +5 -3
- package/src/spec/source.ts +14 -1
- package/src/spec/two-model-rows.ts +1 -0
- package/src/spec/two-model.ts +2 -2
- package/src/ssd-cache.ts +76 -17
- package/src/ssd-durability.ts +20 -22
- package/src/storage/host-buffer.ts +24 -0
- package/src/storage/kv-writer.ts +83 -0
- package/src/storage/kv-writer.worker.js +264 -0
- package/src/storage/retention-policy.ts +37 -0
- package/src/tiered-prompt-cache.ts +45 -0
- package/src/vision/embedder.ts +8 -0
- package/src/vision/prompt.ts +101 -65
- package/src/vision/qwen3vl-prompt.ts +101 -64
- package/src/vision/qwen3vl-tower.ts +11 -0
- package/src/vision/siglip.ts +8 -0
- package/src/web/app.html +12 -0
- package/src/web/app.js +160 -0
- package/src/web/src/chat.ts +4 -1
- package/src/web/src/composer.ts +3 -0
- package/src/web/src/voice.ts +141 -0
- package/src/web/sw.js +1 -1
- /package/docs/{planning → archive/planning}/release-notes-v0.4.0.md +0 -0
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { namespacedCache } from "../../engine/namespaced-cache";
|
|
2
|
+
import { runtimeConfig, runtimeFlag, withRuntimeConfig, type RuntimeConfig } from "../../runtime-config";
|
|
1
3
|
import type { GenerateOptions } from "../../generate";
|
|
2
4
|
import type { RuntimeModel } from "../../model/factory";
|
|
3
5
|
import { KVCache, type Cache } from "../../model/gemma4-base";
|
|
@@ -24,18 +26,29 @@ export function maybePageKv(
|
|
|
24
26
|
for (let i = 0; i < cache.length; i++) {
|
|
25
27
|
if (cache[i] instanceof KVCache) {
|
|
26
28
|
cache[i]!.dispose(); // fresh (offset 0) — nothing stored yet
|
|
27
|
-
cache[i] = new PagedKVCache(capacityTokens, blockSize
|
|
29
|
+
cache[i] = new PagedKVCache(capacityTokens, blockSize, undefined,
|
|
30
|
+
options.kvBits === 4 || options.kvBits === 8 ? { bits: options.kvBits, groupSize: options.kvGroupSize ?? 64 } : undefined);
|
|
28
31
|
}
|
|
29
32
|
}
|
|
30
33
|
}
|
|
31
34
|
|
|
35
|
+
export function pagedPrefixNamespace(options: GenerateOptions, base: string,
|
|
36
|
+
direct = runtimeFlag("MLX_BUN_PAGED_ATTN", false)): string {
|
|
37
|
+
return JSON.stringify(["paged-v1", options.pagedKv?.blockSize ?? PagedKVCache.DEFAULT_BLOCK_SIZE,
|
|
38
|
+
options.kvBits ?? 0, options.kvGroupSize ?? 64, direct, base]);
|
|
39
|
+
}
|
|
40
|
+
|
|
32
41
|
export function bindPagedRequestState(model: RuntimeModel, options: GenerateOptions,
|
|
33
|
-
capacityTokens: number): MlxRequestStatePolicy | undefined {
|
|
42
|
+
capacityTokens: number, cache?: RowPromptCache, runtime: RuntimeConfig = runtimeConfig()): MlxRequestStatePolicy | undefined {
|
|
34
43
|
if (!options.pagedKv) return undefined;
|
|
35
44
|
const blockSize = options.pagedKv.blockSize ?? PagedKVCache.DEFAULT_BLOCK_SIZE;
|
|
36
|
-
|
|
45
|
+
const stateOptions = { pagedKv: { blockSize }, kvBits: options.kvBits, kvGroupSize: options.kvGroupSize };
|
|
46
|
+
const direct = runtime.flag("MLX_BUN_PAGED_ATTN", false);
|
|
47
|
+
const namespace = (base: string) => pagedPrefixNamespace(stateOptions, base, direct);
|
|
48
|
+
return { key: namespace(""),
|
|
49
|
+
promptCache: cache ? namespacedCache(cache, namespace) : undefined, create: () => withRuntimeConfig(runtime, () => {
|
|
37
50
|
const caches = model.makeCache();
|
|
38
|
-
try { maybePageKv(caches,
|
|
51
|
+
try { maybePageKv(caches, stateOptions, capacityTokens); return caches; }
|
|
39
52
|
catch (error) { disposeResources(caches); throw error; }
|
|
40
|
-
} };
|
|
53
|
+
}) };
|
|
41
54
|
}
|
|
@@ -3,7 +3,7 @@ import type { Cache } from "../../model/gemma4-base";
|
|
|
3
3
|
|
|
4
4
|
interface TransactionLayer<Acceptance> {
|
|
5
5
|
fits(drafts: number): boolean;
|
|
6
|
-
begin(): void;
|
|
6
|
+
begin(drafts: number): void;
|
|
7
7
|
resolve(accepted: Acceptance, drafts: number): void;
|
|
8
8
|
}
|
|
9
9
|
|
|
@@ -20,8 +20,11 @@ export function bindRowCacheRollback(
|
|
|
20
20
|
): SpeculativeTransaction<readonly number[]> {
|
|
21
21
|
return bindTransaction(caches.map(cache => ({
|
|
22
22
|
fits: () => true,
|
|
23
|
-
|
|
23
|
+
// A zero-candidate round consumes only the committed pending token.
|
|
24
|
+
// It cannot reject any input, so retaining rollback state is unnecessary.
|
|
25
|
+
begin: drafts => { if (drafts > 0) cache.specRoundBegin(); },
|
|
24
26
|
resolve(accepted: readonly number[], drafts: number) {
|
|
27
|
+
if (drafts === 0) return;
|
|
25
28
|
if (accepted.every(count => count === drafts)) cache.specRoundCommit();
|
|
26
29
|
else cache.specRoundRollback(accepted.every(count => count === accepted[0])
|
|
27
30
|
? accepted[0]! + 1 : accepted.map(count => count + 1));
|
|
@@ -75,7 +78,7 @@ function bindTransaction<Acceptance>(
|
|
|
75
78
|
if (active !== undefined || !layers.every((layer) => layer.fits(drafts)))
|
|
76
79
|
throw new Error("speculative state cannot begin this round");
|
|
77
80
|
active = drafts;
|
|
78
|
-
try { for (const layer of layers) layer.begin(); }
|
|
81
|
+
try { for (const layer of layers) layer.begin(drafts); }
|
|
79
82
|
catch (error) { invalid = true; throw error; }
|
|
80
83
|
},
|
|
81
84
|
resolve(accepted) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { pagedPrefixNamespace } from "./request-state-policy";
|
|
1
2
|
import type { SpecRunServices } from "../../spec/serve-loop";
|
|
2
3
|
import { cleanupFailure, disposeResources, ownResource } from "../../engine/resources";
|
|
3
4
|
import { bindGeneration } from "../../generate";
|
|
@@ -66,6 +67,14 @@ export interface MlxSerialServices {
|
|
|
66
67
|
export function createMlxSerialExecutor(binding: MlxSerialBinding, services: MlxSerialServices): SerialRun {
|
|
67
68
|
const { promptCache, checkpoints: ssdStore } = services;
|
|
68
69
|
return (promptIds, options, onToken, vision, trace, execution) => withRuntimeConfig(binding.runtime, async () => {
|
|
70
|
+
if (promptCache.reclaim) {
|
|
71
|
+
const publish = onToken;
|
|
72
|
+
let steps = 0;
|
|
73
|
+
onToken = (token, logprobs) => {
|
|
74
|
+
if (steps++ % 256 === 0) promptCache.reclaim!();
|
|
75
|
+
return publish(token, logprobs);
|
|
76
|
+
};
|
|
77
|
+
}
|
|
69
78
|
let releaseContinuation: (() => void) | undefined;
|
|
70
79
|
let caches: Cache[] = [];
|
|
71
80
|
let retain: (() => void) | undefined;
|
|
@@ -94,16 +103,16 @@ export function createMlxSerialExecutor(binding: MlxSerialBinding, services: Mlx
|
|
|
94
103
|
}
|
|
95
104
|
// Cache entries are adapter-specific: KV computed under one adapter
|
|
96
105
|
// must never seed another's (or the base's) prefill.
|
|
97
|
-
const
|
|
106
|
+
const baseNs = options.adapters?.length ? services.adapterNamespace(options.adapters) : "";
|
|
107
|
+
const cacheNs = execution.pagedKv ? pagedPrefixNamespace(options, baseNs) : baseNs;
|
|
98
108
|
// Paged-KV request scope (docs/design/kv-cache.md): media
|
|
99
109
|
// prompts (bidir overlay) and LoRA-adapter requests are v1 non-goals —
|
|
100
110
|
// they run the PLAIN cache path even under --paged-kv (scope the flag
|
|
101
111
|
// per request, never 400). Effective value computed ONCE so the
|
|
102
112
|
// prompt-cache bypass below and the generate() options can't disagree.
|
|
103
113
|
const pagedKv = execution.pagedKv ? options.pagedKv : undefined;
|
|
104
|
-
//
|
|
105
|
-
//
|
|
106
|
-
// precedent). Fresh caches per request, disposed on completion.
|
|
114
|
+
// The resolved policy controls reuse; paged layouts have their own
|
|
115
|
+
// numerical namespace and use the same RAM/SSD cache interface.
|
|
107
116
|
const skipPromptCache = !execution.promptCache;
|
|
108
117
|
const checkpointEvery = services.checkpointEveryTokens;
|
|
109
118
|
const checkpointEligible = execution.checkpoint;
|
|
@@ -124,7 +133,7 @@ export function createMlxSerialExecutor(binding: MlxSerialBinding, services: Mlx
|
|
|
124
133
|
const generationPromptIds = checkpoint?.cacheTokens ?? promptIds;
|
|
125
134
|
const entry = skipPromptCache || resuming
|
|
126
135
|
? null
|
|
127
|
-
: promptCache.take(promptIds, cacheNs);
|
|
136
|
+
: promptCache.take(promptIds, cacheNs, options.cacheSessionId);
|
|
128
137
|
if (entry) { caches = entry.caches; retain = entry.retain; }
|
|
129
138
|
if (!checkpoint && !entry) caches = binding.makeCache();
|
|
130
139
|
closeCacheLookup?.();
|
|
@@ -204,7 +213,7 @@ export function createMlxSerialExecutor(binding: MlxSerialBinding, services: Mlx
|
|
|
204
213
|
try {
|
|
205
214
|
const snapshot = ownResource(services.cloneState(caches), disposeResources);
|
|
206
215
|
try {
|
|
207
|
-
promptCache.put(promptIds.slice(0, boundary), snapshot.borrow(), cacheNs);
|
|
216
|
+
promptCache.put(promptIds.slice(0, boundary), snapshot.borrow(), cacheNs, undefined, undefined, options.cacheSessionId);
|
|
208
217
|
snapshot.transfer();
|
|
209
218
|
} finally { snapshot.close(); }
|
|
210
219
|
} catch (e) {
|
|
@@ -229,7 +238,7 @@ export function createMlxSerialExecutor(binding: MlxSerialBinding, services: Mlx
|
|
|
229
238
|
if (!skipPromptCache) {
|
|
230
239
|
// put() fires onPut → the debounced write-behind SSD snapshot
|
|
231
240
|
// (wired below), covering the batch lane's puts too.
|
|
232
|
-
promptCache.put(s.cacheTokens, caches, cacheNs, retain);
|
|
241
|
+
promptCache.put(s.cacheTokens, caches, cacheNs, retain, undefined, options.cacheSessionId);
|
|
233
242
|
caches = []; retain = undefined; // ownership returned to the prefix store
|
|
234
243
|
}
|
|
235
244
|
return s;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {captureSpeculativeOptions, speculativePrefixNamespace} from "../../spec/cache-identity";
|
|
2
2
|
import type { RuntimeModel } from "../../model/factory";
|
|
3
3
|
import type { GenerateOptions } from "../../generate";
|
|
4
|
+
import type { FillSession, Proposal } from "../../fill/fill-session";
|
|
4
5
|
import type { DraftProvider, DraftRowGroup, DraftPrefillGroup, DraftRowCheckpoint } from "../../spec/source";
|
|
5
6
|
import { makeSampler, makeStepSampler, readStepExtras, type NumberStepSampler, type Sampler } from "../../sampler";
|
|
6
7
|
import { MlxArray } from "../../mlx/array";
|
|
@@ -11,16 +12,19 @@ import { cloneKvCaches } from "../../kv-store";
|
|
|
11
12
|
import type { Cache } from "../../model/gemma4-base";
|
|
12
13
|
import { targetCacheLayout } from "./cache-layout";
|
|
13
14
|
import { MlxStateRows } from "./state-rows";
|
|
14
|
-
import {
|
|
15
|
+
import { bindSpeculativeTargetModel, type MlxSpeculativeTargetBinding } from "./speculative";
|
|
15
16
|
import { bindLegacyDraftTarget } from "./draft-target";
|
|
16
17
|
import { bindRowCacheRollback } from "./rollback";
|
|
17
18
|
import { createKvMaintenance } from "./kv-maintenance";
|
|
18
19
|
import { disposeAttachments, type CheckpointAttachment } from "./checkpoint-state";
|
|
19
20
|
import { advanceSpeculativeOutputs } from "./speculative-round";
|
|
20
21
|
import { MlxPrefillRows, type MlxPrefillState } from "./prefill-rows";
|
|
22
|
+
import type { MlxForwardWork } from "./mixed-iteration";
|
|
21
23
|
import type { MlxGroupMethodHost, MlxGroupMethodRequest, MlxGroupPreparation, MlxGroupedMethod, Row } from "./batch-group";
|
|
22
24
|
|
|
23
25
|
interface RequestState {
|
|
26
|
+
fill?: FillSession;
|
|
27
|
+
proposal?: { value: Proposal; emitted: number };
|
|
24
28
|
sampling: NumberStepSampler;
|
|
25
29
|
draftSampling: Sampler;
|
|
26
30
|
retain?: () => void;
|
|
@@ -33,8 +37,8 @@ interface RequestState {
|
|
|
33
37
|
|
|
34
38
|
/** Binding owns graph/layout selection. The executor receives only the method
|
|
35
39
|
* key and lifecycle; sampling, checkpoints and numerical state remain ports. */
|
|
36
|
-
export function bindSpeculativeGroupRequests(model: RuntimeModel, provider: DraftProvider, depth: number) {
|
|
37
|
-
const binding =
|
|
40
|
+
export function bindSpeculativeGroupRequests(model: RuntimeModel, provider: Pick<DraftProvider, "id" | "grouped">, depth: number) {
|
|
41
|
+
const binding = bindSpeculativeTargetModel(model);
|
|
38
42
|
return (input: GenerateOptions): MlxGroupMethodRequest => {
|
|
39
43
|
const options = captureSpeculativeOptions(input);
|
|
40
44
|
return {
|
|
@@ -54,7 +58,18 @@ class SpeculativeGroup implements MlxGroupedMethod {
|
|
|
54
58
|
#steps = 0;
|
|
55
59
|
|
|
56
60
|
constructor(readonly host: MlxGroupMethodHost, readonly model: RuntimeModel,
|
|
57
|
-
readonly provider: DraftProvider, readonly binding:
|
|
61
|
+
readonly provider: Pick<DraftProvider, "id" | "grouped">, readonly binding: MlxSpeculativeTargetBinding, readonly depth: number) {}
|
|
62
|
+
|
|
63
|
+
get runningTokens(): number {
|
|
64
|
+
const rows = this.host.rows;
|
|
65
|
+
if (!rows.length) return 0;
|
|
66
|
+
// Target verification consumes the pending token and the draft candidates.
|
|
67
|
+
const echo = Math.max(...rows.map(row => {
|
|
68
|
+
const proposal = this.#requests.get(row)?.proposal;
|
|
69
|
+
return proposal?.emitted ? proposal.value.ids.length - proposal.emitted : 0;
|
|
70
|
+
}));
|
|
71
|
+
return rows.length * (1 + (echo || Math.min(this.depth, Math.max(...rows.map(row => row.req.maxTokens - row.generated)))));
|
|
72
|
+
}
|
|
58
73
|
|
|
59
74
|
prepare(first: Row): MlxGroupPreparation {
|
|
60
75
|
const method = this;
|
|
@@ -91,7 +106,7 @@ class SpeculativeGroup implements MlxGroupedMethod {
|
|
|
91
106
|
try {
|
|
92
107
|
caches = snapshot(); attachments.push(prefix!.capture(index).attachment);
|
|
93
108
|
method.host.promptCache!.put(state.row.req.promptIds.slice(0, state.pos), caches,
|
|
94
|
-
state.request.namespace, undefined, attachments);
|
|
109
|
+
state.request.namespace, undefined, attachments, state.row.req.cacheSessionId);
|
|
95
110
|
caches = []; attachments = [];
|
|
96
111
|
} finally { disposeResources([...caches, { dispose: () => disposeAttachments(attachments) }]); }
|
|
97
112
|
};
|
|
@@ -103,11 +118,13 @@ class SpeculativeGroup implements MlxGroupedMethod {
|
|
|
103
118
|
acceptanceLengths: [], tokensPerForward: 0, forwardsSaved: 0, draftedByPos: [], acceptedByPos: [] };
|
|
104
119
|
const sampler = makeSampler(options);
|
|
105
120
|
const request: RequestState = { draftSampling: sampler, namespace: "", prefixLength: 0,
|
|
121
|
+
fill: method.provider.grouped?.supportsExternalTokens ? options.fill : undefined,
|
|
106
122
|
processed: method.host.promptCache ? [] : undefined,
|
|
107
123
|
sampling: makeStepSampler(options, { tokenRepresentation: "number", grammarWait: "before-sample",
|
|
108
124
|
historyUpdate: "after-sample", initialHistory: row.req.promptIds, acceptGrammar: true,
|
|
109
125
|
eosTokenIds: row.req.eosTokenIds, captureSelectedLogprob: options.logprobs === true,
|
|
110
126
|
captureTopLogprobs: options.topLogprobs }) };
|
|
127
|
+
if (request.fill) row.fill = request.fill.stats;
|
|
111
128
|
let caches: Cache[] = [], retained: (() => void) | undefined;
|
|
112
129
|
let appended = false;
|
|
113
130
|
const previous = prefix?.rowCount ?? 0;
|
|
@@ -118,7 +135,7 @@ class SpeculativeGroup implements MlxGroupedMethod {
|
|
|
118
135
|
const pendingPrompt = prefix.prefillMode !== "full" && method.binding.prefillTailSplit && prompt.length > 1;
|
|
119
136
|
request.prefixLength = prompt.length - Number(pendingPrompt);
|
|
120
137
|
request.namespace = speculativePrefixNamespace(prefix.namespace, row.cacheNamespace ?? "", options);
|
|
121
|
-
const hit = method.host.promptCache?.take(prompt, request.namespace);
|
|
138
|
+
const hit = method.host.promptCache?.take(prompt, request.namespace, row.req.cacheSessionId);
|
|
122
139
|
if (hit) {
|
|
123
140
|
try {
|
|
124
141
|
disposeResources(caches); caches = hit.caches.splice(0);
|
|
@@ -134,7 +151,7 @@ class SpeculativeGroup implements MlxGroupedMethod {
|
|
|
134
151
|
} else { prefix.append([null]); appended = true; }
|
|
135
152
|
return { row, request, solo: caches, retain: retained, pos: row.cachedTokens,
|
|
136
153
|
end: request.prefixLength, pendingPrompt, transferred: false, closed: false,
|
|
137
|
-
chunkSize: options.prefillChunkSize ?? method.host.prefillChunkSize,
|
|
154
|
+
chunkSize: options.prefillChunkSize ?? row.req.prefillChunkSize ?? method.host.prefillChunkSize,
|
|
138
155
|
boundary: method.host.promptCache ? Math.min(row.req.snapshotAt ?? prompt.length, prompt.length - 1) : -1 };
|
|
139
156
|
} catch (error) {
|
|
140
157
|
return cleanupFailure(error, () => disposeResources([
|
|
@@ -148,9 +165,9 @@ class SpeculativeGroup implements MlxGroupedMethod {
|
|
|
148
165
|
return { start: state.pos, end, kind: end === state.end ? "final" : "drain",
|
|
149
166
|
snapshot: end === state.boundary && end > state.row.cachedTokens, batchYield: true, maintain: true };
|
|
150
167
|
},
|
|
151
|
-
async forward(ids, caches) {
|
|
168
|
+
async forward(ids, caches, _states, work) {
|
|
152
169
|
const taps = prefix!.tapLayers;
|
|
153
|
-
const output = await method.binding.forward(ids, caches, taps.length ? [...taps] : undefined);
|
|
170
|
+
const output = await method.binding.forward(ids, caches, taps.length ? [...taps] : undefined, work);
|
|
154
171
|
context = output.ctxML;
|
|
155
172
|
return output.hidden;
|
|
156
173
|
},
|
|
@@ -184,12 +201,14 @@ class SpeculativeGroup implements MlxGroupedMethod {
|
|
|
184
201
|
method.host.finish(row, "stop"); return;
|
|
185
202
|
}
|
|
186
203
|
const more = await method.host.publish(row, row.current, readStepExtras(sampled.extras));
|
|
204
|
+
state.request.fill?.observe(row.current);
|
|
187
205
|
if (more === false || row.req.grammar?.isTerminated || row.generated >= row.req.maxTokens) {
|
|
188
206
|
if (method.host.promptCache && !row.req.signal?.aborted) capture(state, () => cloneKvCaches(state.solo), index);
|
|
189
207
|
disposeResources(state.solo.splice(0)); state.retain?.(); state.retain = undefined;
|
|
190
208
|
method.host.finish(row, more === false || row.req.grammar?.isTerminated ? "stop" : "length"); return;
|
|
191
209
|
}
|
|
192
210
|
}
|
|
211
|
+
method.#proposeEcho(row, state.request);
|
|
193
212
|
ready.push({ row, request: state.request, caches: state.solo, retain: state.retain,
|
|
194
213
|
draft: prefix!.capture(index), transferred: false });
|
|
195
214
|
state.transferred = true;
|
|
@@ -211,7 +230,7 @@ class SpeculativeGroup implements MlxGroupedMethod {
|
|
|
211
230
|
get canAdmit() { return target.canAdmit; },
|
|
212
231
|
get tokenWeight() { return target.tokenWeight + ready.reduce((sum, state) => sum + state.row.promptTokens - state.row.cachedTokens, 0); },
|
|
213
232
|
admit: row => target.admit(row),
|
|
214
|
-
advance: async
|
|
233
|
+
advance: async work => {
|
|
215
234
|
// Publication returns to scheduling before prepared state joins active
|
|
216
235
|
// decode. This preserves the first-output flush boundary at every B.
|
|
217
236
|
if (ready.length) {
|
|
@@ -225,7 +244,8 @@ class SpeculativeGroup implements MlxGroupedMethod {
|
|
|
225
244
|
maintain.prepareBatch?.(state.caches);
|
|
226
245
|
this.#target ??= new MlxStateRows(state.caches.map(targetCacheLayout));
|
|
227
246
|
this.#draft ??= this.provider.grouped!.open({ target: bindLegacyDraftTarget(this.model, this.#target.caches),
|
|
228
|
-
checkpoints: [], sampling: { sample: (lp, steps) => this.#sampleDraftRows(lp, steps) }
|
|
247
|
+
checkpoints: [], sampling: { sample: (lp, steps) => this.#sampleDraftRows(lp, steps) },
|
|
248
|
+
constraints: { propose: async (row, maxTokens) => this.host.rows[row]!.req.grammar?.proposeTokens(maxTokens) ?? [] } });
|
|
229
249
|
applyStateChanges([() => this.#target!.prepareAppend(state.caches),
|
|
230
250
|
() => this.#draft!.prepareAppend([state.draft]), () => ({ commit: () => {
|
|
231
251
|
state.request.retain = state.retain; state.retain = undefined;
|
|
@@ -236,7 +256,7 @@ class SpeculativeGroup implements MlxGroupedMethod {
|
|
|
236
256
|
}
|
|
237
257
|
return target.rows.length === 0;
|
|
238
258
|
}
|
|
239
|
-
const done = await target.advance();
|
|
259
|
+
const done = await target.advance(work);
|
|
240
260
|
return done && ready.length === 0;
|
|
241
261
|
},
|
|
242
262
|
dispose() {
|
|
@@ -259,7 +279,7 @@ class SpeculativeGroup implements MlxGroupedMethod {
|
|
|
259
279
|
} finally { disposeResources(tokens); }
|
|
260
280
|
}
|
|
261
281
|
|
|
262
|
-
async advance(): Promise<void> {
|
|
282
|
+
async advance(work?: MlxForwardWork): Promise<void> {
|
|
263
283
|
const active = [...this.host.rows];
|
|
264
284
|
const live = active.flatMap((row, index) => row.req.signal?.aborted ? [] : [index]);
|
|
265
285
|
if (live.length !== active.length) {
|
|
@@ -269,6 +289,19 @@ class SpeculativeGroup implements MlxGroupedMethod {
|
|
|
269
289
|
const rows = [...this.host.rows];
|
|
270
290
|
if (!rows.length) return;
|
|
271
291
|
const depth = Math.min(this.depth, Math.max(...rows.map(row => row.req.maxTokens - row.generated)));
|
|
292
|
+
// The ordinary MTP round already supplies samples that can establish a
|
|
293
|
+
// copied prefix. Do not verify a wide span before any sample matches it.
|
|
294
|
+
const proposals = rows.map(row => {
|
|
295
|
+
const proposal = this.#requests.get(row)!.proposal;
|
|
296
|
+
return proposal?.emitted ? proposal.value.ids.slice(proposal.emitted) : [];
|
|
297
|
+
});
|
|
298
|
+
const echoDepth = Math.max(...proposals.map(ids => ids.length));
|
|
299
|
+
let externalTokens: MlxArray | undefined;
|
|
300
|
+
const rollback = bindRowCacheRollback(this.#target!.caches, rows.length);
|
|
301
|
+
const fills = echoDepth ? rows.flatMap((row, index) => {
|
|
302
|
+
const request = this.#requests.get(row)!;
|
|
303
|
+
return proposals[index]!.length ? [request.fill!] : [];
|
|
304
|
+
}) : [];
|
|
272
305
|
const halted = new Set<Row>();
|
|
273
306
|
const pin = this.binding.pinVerify?.();
|
|
274
307
|
try {
|
|
@@ -280,11 +313,27 @@ class SpeculativeGroup implements MlxGroupedMethod {
|
|
|
280
313
|
const more = await this.host.publish(row, ids[0]!, metadata?.[0]);
|
|
281
314
|
if (more === false) { halted.add(row); return false; }
|
|
282
315
|
} },
|
|
283
|
-
})), depth,
|
|
284
|
-
|
|
316
|
+
})), echoDepth || depth, echoDepth ? {
|
|
317
|
+
draft: () => proposals,
|
|
318
|
+
commit: (accepted, context) => this.#draft!.consume!(externalTokens!, context, accepted.map(n => n + 1)),
|
|
319
|
+
} : this.#draft!, {
|
|
320
|
+
transaction: !echoDepth ? rollback : { ...rollback,
|
|
321
|
+
begin(depth) {
|
|
322
|
+
const start = performance.now(); rollback.begin(depth);
|
|
323
|
+
const elapsed = performance.now() - start;
|
|
324
|
+
for (const fill of fills) fill.noteVerifyEvent(elapsed);
|
|
325
|
+
},
|
|
326
|
+
resolve(accepted) {
|
|
327
|
+
const start = performance.now(); rollback.resolve(accepted);
|
|
328
|
+
const elapsed = performance.now() - start;
|
|
329
|
+
for (const fill of fills) fill.stats.checkpointMs += elapsed;
|
|
330
|
+
},
|
|
331
|
+
},
|
|
285
332
|
forward: async ids => {
|
|
333
|
+
if (echoDepth) externalTokens = ops.copyOf(ids);
|
|
286
334
|
const taps = this.#draft!.tapLayers;
|
|
287
|
-
const result = await this.binding.forward(ids, this.#target!.caches, taps.length ? [...taps] : undefined
|
|
335
|
+
const result = await this.binding.forward(ids, this.#target!.caches, taps.length ? [...taps] : undefined,
|
|
336
|
+
work ? (tokens, caches, options) => work(tokens, caches, { ...options, preserveTokenGeometry: true }) : undefined);
|
|
288
337
|
const context = result.ctxML ?? result.hidden;
|
|
289
338
|
try { return { logits: this.binding.projectLogits(result.hidden), context }; }
|
|
290
339
|
catch (error) { context.dispose(); throw error; }
|
|
@@ -295,23 +344,39 @@ class SpeculativeGroup implements MlxGroupedMethod {
|
|
|
295
344
|
for (let index = 0; index < rows.length; index++) {
|
|
296
345
|
const row = rows[index]!, output = completed.outputs[index]!, round = completed.rounds[index]!;
|
|
297
346
|
const request = this.#requests.get(row)!;
|
|
347
|
+
if (request.fill) {
|
|
348
|
+
for (const token of round.acceptance.emitted.slice(0, output.generated)) {
|
|
349
|
+
const proposal = request.proposal;
|
|
350
|
+
if (proposal && token === proposal.value.ids[proposal.emitted]) {
|
|
351
|
+
if (++proposal.emitted === proposal.value.ids.length) this.#settleEcho(request);
|
|
352
|
+
} else {
|
|
353
|
+
this.#settleEcho(request); request.fill.observe(token);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
if (output.kind === "continue") this.#proposeEcho(row, request);
|
|
357
|
+
else this.#settleEcho(request);
|
|
358
|
+
}
|
|
298
359
|
if (output.kind !== "failed" && output.kind !== "cancelled")
|
|
299
360
|
request.processed?.push(row.current, ...round.drafts.slice(0, output.accepted));
|
|
300
361
|
const stats = row.spec!;
|
|
301
|
-
stats.drafted += round.drafts.length;
|
|
302
|
-
stats.accepted += round.acceptance.accepted;
|
|
362
|
+
stats.drafted += echoDepth ? 0 : round.drafts.length;
|
|
363
|
+
stats.accepted += echoDepth ? 0 : round.acceptance.accepted;
|
|
303
364
|
stats.rejected = stats.drafted - stats.accepted;
|
|
304
|
-
stats.
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
365
|
+
stats.targetCalls++;
|
|
366
|
+
if (!echoDepth) {
|
|
367
|
+
stats.rounds!++;
|
|
368
|
+
stats.acceptanceLengths!.push(round.acceptance.accepted);
|
|
369
|
+
for (let p = 0; p < round.drafts.length; p++) stats.draftedByPos![p] = (stats.draftedByPos![p] ?? 0) + 1;
|
|
370
|
+
for (let p = 0; p < round.acceptance.accepted; p++) stats.acceptedByPos![p] = (stats.acceptedByPos![p] ?? 0) + 1;
|
|
371
|
+
}
|
|
308
372
|
row.sampled += round.acceptance.emitted.length + Number(round.acceptance.sawEos);
|
|
309
373
|
// A consumer may stop inside a verified burst before its terminal EOS.
|
|
310
374
|
if (output.kind === "stop" && !halted.has(row) && round.acceptance.sawEos &&
|
|
311
375
|
output.generated === round.acceptance.emitted.length && row.generated < row.req.maxTokens)
|
|
312
376
|
row.generated++;
|
|
313
|
-
|
|
314
|
-
stats.
|
|
377
|
+
const forwards = stats.targetCalls - 1;
|
|
378
|
+
stats.tokensPerForward = row.generated / forwards;
|
|
379
|
+
stats.forwardsSaved = Math.max(0, row.generated - 1 - forwards);
|
|
315
380
|
if (output.kind === "continue") { row.current = output.pending; keep.push(index); }
|
|
316
381
|
else if (output.kind === "failed") row.reject(output.error);
|
|
317
382
|
else if (output.kind === "cancelled") row.reject(row.req.signal?.reason ?? new Error(output.reason));
|
|
@@ -322,7 +387,19 @@ class SpeculativeGroup implements MlxGroupedMethod {
|
|
|
322
387
|
}
|
|
323
388
|
if (keep.length !== rows.length) this.host.filterRows(keep);
|
|
324
389
|
if (++this.#steps % 256 === 0) clearCache();
|
|
325
|
-
} finally { pin?.close(); }
|
|
390
|
+
} finally { externalTokens?.dispose(); pin?.close(); }
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
#proposeEcho(row: Row, request: RequestState): void {
|
|
394
|
+
if (request.proposal || !request.fill) return;
|
|
395
|
+
const value = request.fill.propose(row.req.maxTokens - row.generated, "verify");
|
|
396
|
+
if (value) request.proposal = { value, emitted: 0 };
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
#settleEcho(request: RequestState): void {
|
|
400
|
+
if (!request.proposal) return;
|
|
401
|
+
request.fill!.commit(request.proposal.value, request.proposal.emitted);
|
|
402
|
+
request.proposal = undefined;
|
|
326
403
|
}
|
|
327
404
|
|
|
328
405
|
/** The method aligns token coverage and captures its state. The cache owns
|
|
@@ -334,7 +411,7 @@ class SpeculativeGroup implements MlxGroupedMethod {
|
|
|
334
411
|
target = this.#target!.extractRow(index);
|
|
335
412
|
attachments.push(this.#draft!.capture(index).attachment);
|
|
336
413
|
this.host.promptCache.put([...row.req.promptIds.slice(0, request.prefixLength), ...request.processed], target,
|
|
337
|
-
request.namespace, undefined, attachments);
|
|
414
|
+
request.namespace, undefined, attachments, row.req.cacheSessionId);
|
|
338
415
|
target = []; attachments = [];
|
|
339
416
|
} finally { disposeResources([...target, { dispose: () => disposeAttachments(attachments) }]); }
|
|
340
417
|
}
|
|
@@ -344,6 +421,7 @@ class SpeculativeGroup implements MlxGroupedMethod {
|
|
|
344
421
|
const retained = new Set(keep.map(index => this.host.rows[index]!));
|
|
345
422
|
const releases: { dispose(): void }[] = [];
|
|
346
423
|
for (const [row, request] of this.#requests) if (!retained.has(row)) {
|
|
424
|
+
this.#settleEcho(request);
|
|
347
425
|
this.#requests.delete(row);
|
|
348
426
|
releases.push(request.sampling, { dispose: () => request.retain?.() });
|
|
349
427
|
}
|
|
@@ -351,6 +429,7 @@ class SpeculativeGroup implements MlxGroupedMethod {
|
|
|
351
429
|
}
|
|
352
430
|
|
|
353
431
|
dispose(): void {
|
|
432
|
+
for (const request of this.#requests.values()) this.#settleEcho(request);
|
|
354
433
|
const resources = [...(this.#target ? [this.#target] : []), ...(this.#draft ? [this.#draft] : []),
|
|
355
434
|
...[...this.#requests.values()].flatMap(request => [request.sampling, { dispose: () => request.retain?.() }])];
|
|
356
435
|
this.#target = null; this.#draft = null; this.#requests.clear();
|
|
@@ -10,12 +10,16 @@ import type { SpeculativeTransaction } from "../../inference/rollback";
|
|
|
10
10
|
import { bindCacheRollback } from "./rollback";
|
|
11
11
|
import type { GraphDescriptor } from "../../inference/graph";
|
|
12
12
|
import { bindLegacyDraftTarget } from "./draft-target";
|
|
13
|
+
import type { MlxForwardWork } from "./mixed-iteration";
|
|
14
|
+
import type { PrefillPolicy } from "../../inference/prefill";
|
|
15
|
+
import { resolveMlxPrefillPolicy } from "./prefill-policy";
|
|
13
16
|
|
|
14
17
|
/** Bound target operations and draft construction for one speculative run.
|
|
15
18
|
* A replacement graph supplies this entire port, including any hidden taps.
|
|
16
19
|
* The verifier never needs a RuntimeModel or an artifact-family check. */
|
|
17
20
|
export interface MlxSpeculativeBinding {
|
|
18
21
|
readonly runtime?: RuntimeConfig;
|
|
22
|
+
readonly prefillPolicy?: PrefillPolicy;
|
|
19
23
|
readonly memory?: MlxModelMemory;
|
|
20
24
|
readonly descriptor: GraphDescriptor;
|
|
21
25
|
readonly eosTokenIds: readonly number[];
|
|
@@ -23,27 +27,29 @@ export interface MlxSpeculativeBinding {
|
|
|
23
27
|
makeCache(): Cache[];
|
|
24
28
|
openDraft(sampler: Sampler, caches: Cache[]): DraftSource;
|
|
25
29
|
bindRollback(caches: Cache[]): SpeculativeTransaction;
|
|
26
|
-
forward(ids: MlxArray, caches: Cache[], tapLayers?: number[]):
|
|
30
|
+
forward(ids: MlxArray, caches: Cache[], tapLayers?: number[], work?: MlxForwardWork):
|
|
27
31
|
Promise<{ hidden: MlxArray; ctxML: MlxArray | null }>;
|
|
28
32
|
projectLogits(hidden: MlxArray): MlxArray;
|
|
29
33
|
/** Establish the implementation's verify kernel context; restore on close. */
|
|
30
34
|
pinVerify?(): { close(): void };
|
|
31
35
|
}
|
|
32
36
|
|
|
37
|
+
export type MlxSpeculativeTargetBinding = Omit<MlxSpeculativeBinding, "openDraft">;
|
|
38
|
+
|
|
33
39
|
/** Legacy mutable tap/kernel fields require the gateway's exclusive lease. */
|
|
34
|
-
export function
|
|
40
|
+
export function bindSpeculativeTargetModel(model: RuntimeModel): MlxSpeculativeTargetBinding {
|
|
35
41
|
const runtime = runtimeConfig();
|
|
36
42
|
return {
|
|
37
43
|
runtime,
|
|
44
|
+
prefillPolicy: resolveMlxPrefillPolicy(model.config, runtime),
|
|
38
45
|
memory: model,
|
|
39
46
|
descriptor: Object.freeze({ id: `legacy-spec:${model.config.modelType}`, backend: "mlx",
|
|
40
47
|
graphAbi: "mlx-hidden-bsh-v1", stateAbi: "legacy-cache-array-v1", artifact: "legacy-resident-model" }),
|
|
41
48
|
eosTokenIds: model.config.eosTokenIds,
|
|
42
49
|
prefillTailSplit: runtime.flag("MLX_BUN_PREFILL_TAIL_SPLIT", true),
|
|
43
50
|
makeCache: model.makeCache.bind(model),
|
|
44
|
-
openDraft: (sampler, caches) => provider.open({ sampler, target: bindLegacyDraftTarget(model, caches) }),
|
|
45
51
|
bindRollback: bindCacheRollback,
|
|
46
|
-
forward: (ids, caches, tapLayers) => legacyForwardWithTaps(model, ids, caches, tapLayers),
|
|
52
|
+
forward: (ids, caches, tapLayers, work) => legacyForwardWithTaps(model, ids, caches, tapLayers, work),
|
|
47
53
|
projectLogits: model.logitsFromHidden.bind(model),
|
|
48
54
|
...("setSpecKernelPinned" in model ? {
|
|
49
55
|
pinVerify() {
|
|
@@ -54,6 +60,12 @@ export function bindLegacySpeculativeModel(model: RuntimeModel, provider: DraftP
|
|
|
54
60
|
};
|
|
55
61
|
}
|
|
56
62
|
|
|
63
|
+
/** Serial compatibility binds draft construction separately from target work. */
|
|
64
|
+
export function bindLegacySpeculativeModel(model: RuntimeModel, provider: DraftProvider): MlxSpeculativeBinding {
|
|
65
|
+
return { ...bindSpeculativeTargetModel(model),
|
|
66
|
+
openDraft: (sampler, caches) => provider.open({ sampler, target: bindLegacyDraftTarget(model, caches) }) };
|
|
67
|
+
}
|
|
68
|
+
|
|
57
69
|
export function assertMlxSpeculativeBinding(binding: MlxSpeculativeBinding): void {
|
|
58
70
|
const descriptor = binding.descriptor;
|
|
59
71
|
if (descriptor.backend !== "mlx" || descriptor.graphAbi !== "mlx-hidden-bsh-v1" ||
|
|
@@ -71,8 +83,10 @@ async function legacyForwardWithTaps(
|
|
|
71
83
|
ids: MlxArray,
|
|
72
84
|
caches: Cache[],
|
|
73
85
|
tapLayers: number[] | undefined,
|
|
86
|
+
work?: MlxForwardWork,
|
|
74
87
|
): Promise<{ hidden: MlxArray; ctxML: MlxArray | null }> {
|
|
75
88
|
if (!tapLayers) {
|
|
89
|
+
if (work) return { hidden: await work(ids, caches), ctxML: null };
|
|
76
90
|
const asyncModel = model as RuntimeModel & {
|
|
77
91
|
forwardHiddenAsync?: (
|
|
78
92
|
ids: MlxArray,
|
|
@@ -88,10 +102,13 @@ async function legacyForwardWithTaps(
|
|
|
88
102
|
const m = model;
|
|
89
103
|
const previousTap = m.hiddenTap;
|
|
90
104
|
const cap = new Map<number, MlxArray>();
|
|
91
|
-
|
|
105
|
+
const layers = new Set(tapLayers);
|
|
106
|
+
if (!work) m.hiddenTap = { layers, captured: cap };
|
|
92
107
|
let hidden: MlxArray | null = null;
|
|
93
108
|
try {
|
|
94
|
-
hidden =
|
|
109
|
+
hidden = work ? await work(ids, caches, { captureLayer(layer, h) {
|
|
110
|
+
if (layers.has(layer)) cap.set(layer, ops.contiguous(h));
|
|
111
|
+
} }) : model.forwardHidden(ids, caches);
|
|
95
112
|
const perLayer = tapLayers.map((li) => {
|
|
96
113
|
const a = cap.get(li);
|
|
97
114
|
if (!a) throw new Error(`spec tap: layer ${li} not captured`);
|
|
@@ -109,6 +126,6 @@ async function legacyForwardWithTaps(
|
|
|
109
126
|
// are already gone (cap cleared, hidden nulled).
|
|
110
127
|
hidden?.dispose();
|
|
111
128
|
for (const [, a] of cap) a.dispose();
|
|
112
|
-
m.hiddenTap = previousTap;
|
|
129
|
+
if (!work) m.hiddenTap = previousTap;
|
|
113
130
|
}
|
|
114
131
|
}
|
|
@@ -27,7 +27,7 @@ export function targetRowLayoutFactory(cache: Cache): (() => TargetRowLayout) |
|
|
|
27
27
|
if (cache instanceof DelayedTurboQuantKVCache || cache instanceof DelayedQuantizedKVCache ||
|
|
28
28
|
cache instanceof BatchedTurboQuantKVCache || cache instanceof BatchedKVCache ||
|
|
29
29
|
cache instanceof BatchedQuantizedKVCache || cache instanceof BatchedSSMCache) return () => cache.makeEmptyBatch();
|
|
30
|
-
if (cache instanceof TurboQuantKVCache) return () => new BatchedTurboQuantKVCache(cache.kBits, cache.vBits);
|
|
30
|
+
if (cache instanceof TurboQuantKVCache) return () => new BatchedTurboQuantKVCache(cache.kBits, cache.vBits, cache.fusedDecode);
|
|
31
31
|
if (cache instanceof SSMCache) return () => new BatchedSSMCache();
|
|
32
32
|
if (cache instanceof QuantizedKVCache) return () => new BatchedQuantizedKVCache(cache.groupSize, cache.bits);
|
|
33
33
|
if (cache instanceof KVCache) return () => new BatchedKVCache();
|
|
@@ -17,6 +17,7 @@ export interface SpeculativeTransitionedKvLayout extends TransitionedKvLayout {
|
|
|
17
17
|
}
|
|
18
18
|
export interface KvRowTransition<Layout extends TransitionedKvLayout> {
|
|
19
19
|
readonly signature: string;
|
|
20
|
+
readonly conversionOffset?: number;
|
|
20
21
|
maintain(rows: Cache[]): void;
|
|
21
22
|
converted(row: Cache): boolean;
|
|
22
23
|
makeLayout(): Layout;
|
|
@@ -77,6 +78,13 @@ export abstract class TransitioningKvRows<Layout extends TransitionedKvLayout> i
|
|
|
77
78
|
get rowOffsets(): readonly number[] { return this.packed?.rowOffsets ?? this.positions.rowOffsets; }
|
|
78
79
|
get leftPad(): readonly number[] { return this.packed?.leftPad ?? this.positions.leftPad; }
|
|
79
80
|
get offset(): number { return this.packed?.offset ?? this.positions.offset; }
|
|
81
|
+
maxAppendTokens(): number {
|
|
82
|
+
if (this.packed) return this.packed.maxAppendTokens?.() ?? Infinity;
|
|
83
|
+
const end = this.transition.conversionOffset;
|
|
84
|
+
if (end === undefined) return Infinity;
|
|
85
|
+
return Math.min(Infinity, ...this.rowOffsets.map((offset, row) =>
|
|
86
|
+
offset < end && !this.transition.converted(this.rows[row]!) ? end - offset : Infinity));
|
|
87
|
+
}
|
|
80
88
|
get batchSize(): number | null { return this.packed?.batchSize ?? this.positions.batchSize; }
|
|
81
89
|
get ropeOffsetArr(): MlxArray | undefined { return this.packed ? this.packed.ropeOffsetArr : this.positions.ropeOffsetArr; }
|
|
82
90
|
bytesPerToken(): number { return this.packed?.bytesPerToken?.() ?? Math.max(0, ...this.rows.map(row => row.bytesPerToken?.() ?? 0)); }
|