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 { turboQuantFusedDecode } from "../../model/turboquant-codec";
|
|
2
|
+
import { runtimeConfig, withRuntimeConfig } from "../../runtime-config";
|
|
1
3
|
import type { KvSchemeOptions } from "../../kv-scheme";
|
|
2
4
|
import { KVCache, QuantizedKVCache, RotatingKVCache, RotatingQuantizedKVCache, TurboQuantKVCache, type Cache } from "../../model/gemma4-base";
|
|
3
5
|
import type { KvQuantSpec, TurboQuantScheme } from "../../config";
|
|
@@ -10,6 +12,8 @@ import { DelayedTurboQuantKVCache } from "../../model/delayed-turboquant-kv";
|
|
|
10
12
|
|
|
11
13
|
export interface KvMaintenance {
|
|
12
14
|
(cache: Cache[]): void;
|
|
15
|
+
/** Limit committed work at a pending precision transition. */
|
|
16
|
+
maxAppendTokens?(cache: readonly Cache[]): number;
|
|
13
17
|
/** Bind state requiring row-local maintenance before shared decode. */
|
|
14
18
|
prepareBatch?(cache: Cache[]): void;
|
|
15
19
|
/** Bind all precision policies before a prefill cohort owns row boundaries. */
|
|
@@ -43,12 +47,24 @@ export function createKvMaintenance(options: Readonly<Omit<KvSchemeOptions, "kvC
|
|
|
43
47
|
if (turboQuant) {
|
|
44
48
|
const start = options.quantizedKvStart ?? 0;
|
|
45
49
|
const scheme = { ...turboQuant };
|
|
46
|
-
const
|
|
50
|
+
const runtime = runtimeConfig();
|
|
51
|
+
const fusedDecode = turboQuantFusedDecode(runtime);
|
|
52
|
+
const maintain: KvMaintenance = (cache) => withRuntimeConfig(runtime, () => maybeTurboQuantizeKv(cache, scheme, start));
|
|
53
|
+
if (start > 0) maintain.maxAppendTokens = (cache) => {
|
|
54
|
+
let remaining = Number.POSITIVE_INFINITY;
|
|
55
|
+
for (const c of cache) {
|
|
56
|
+
remaining = Math.min(remaining, c.maxAppendTokens?.() ?? Infinity);
|
|
57
|
+
const conversion = c.turboConversion ?? (c instanceof KVCache ? c : undefined);
|
|
58
|
+
if (conversion && conversion.offset < start)
|
|
59
|
+
remaining = Math.min(remaining, start - conversion.offset);
|
|
60
|
+
}
|
|
61
|
+
return remaining;
|
|
62
|
+
};
|
|
47
63
|
maintain.preparePrefill = (cache) => {
|
|
48
64
|
for (let layer = 0; layer < cache.length; layer++) {
|
|
49
65
|
const row = cache[layer]!;
|
|
50
66
|
if (row instanceof KVCache || row instanceof TurboQuantKVCache)
|
|
51
|
-
cache[layer] = new DelayedTurboQuantKVCache(scheme.kBits, scheme.vBits, start, maintain, row);
|
|
67
|
+
cache[layer] = new DelayedTurboQuantKVCache(scheme.kBits, scheme.vBits, start, maintain, row, fusedDecode);
|
|
52
68
|
}
|
|
53
69
|
};
|
|
54
70
|
if (start > 0) maintain.prepareBatch = maintain.preparePrefill;
|
|
@@ -83,6 +99,18 @@ export function createKvMaintenance(options: Readonly<Omit<KvSchemeOptions, "kvC
|
|
|
83
99
|
clearCache();
|
|
84
100
|
}
|
|
85
101
|
};
|
|
102
|
+
if (start > 0) maintain.maxAppendTokens = (cache) => {
|
|
103
|
+
let remaining = Number.POSITIVE_INFINITY;
|
|
104
|
+
for (let layer = 0; layer < cache.length; layer++) {
|
|
105
|
+
if (byLayer && !byLayer.has(layer)) continue;
|
|
106
|
+
const c = cache[layer]!;
|
|
107
|
+
remaining = Math.min(remaining, c.maxAppendTokens?.() ?? Infinity);
|
|
108
|
+
const conversion = c.affineConversion ?? (c instanceof KVCache || c instanceof RotatingKVCache ? c : undefined);
|
|
109
|
+
if (conversion && conversion.offset < start)
|
|
110
|
+
remaining = Math.min(remaining, start - conversion.offset);
|
|
111
|
+
}
|
|
112
|
+
return remaining;
|
|
113
|
+
};
|
|
86
114
|
maintain.preparePrefill = (cache) => {
|
|
87
115
|
for (let layer = 0; layer < cache.length; layer++) {
|
|
88
116
|
const row = cache[layer]!;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { MlxArray } from "../../mlx/array";
|
|
2
|
+
import type { Cache } from "../../model/gemma4-base";
|
|
3
|
+
import type { TokenGroup, TokenWorkOptions } from "../../model/token-groups";
|
|
4
|
+
|
|
5
|
+
export type MlxForwardWork = (ids: MlxArray, cache: Cache[], options?: TokenWorkOptions) => Promise<MlxArray>;
|
|
6
|
+
export interface MlxPreparationWork {
|
|
7
|
+
/** Real prompt tokens across all rows, not a padded sequence width. */
|
|
8
|
+
readonly maxTokens?: number;
|
|
9
|
+
readonly forward?: MlxForwardWork;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Join two method-owned work producers at their forward boundary. Decode
|
|
13
|
+
* finishes sampling/publication/retirement before preparation can join rows.
|
|
14
|
+
* The model owns packing; this coordinator never merges caches or samples.
|
|
15
|
+
* A restored/cancelled preparation need not produce a forward at all. */
|
|
16
|
+
export async function runMixedTokenIteration(options: {
|
|
17
|
+
prepare(forward: MlxForwardWork): Promise<void>;
|
|
18
|
+
decode(forward: MlxForwardWork): Promise<void>;
|
|
19
|
+
forward: MlxForwardWork;
|
|
20
|
+
mixed(groups: readonly TokenGroup[]): MlxArray[];
|
|
21
|
+
}): Promise<boolean> {
|
|
22
|
+
let notify!: () => void;
|
|
23
|
+
const offered = new Promise<void>(resolve => { notify = resolve; });
|
|
24
|
+
let pending: { group: TokenGroup; resolve(hidden: MlxArray): void; reject(error: unknown): void } | undefined;
|
|
25
|
+
let captured = false;
|
|
26
|
+
let preparationError: unknown;
|
|
27
|
+
let preparationFailed = false;
|
|
28
|
+
const preparing = options.prepare(async (ids, cache, policy) => {
|
|
29
|
+
if (captured) return options.forward(ids, cache, policy);
|
|
30
|
+
captured = true;
|
|
31
|
+
return new Promise<MlxArray>((resolve, reject) => {
|
|
32
|
+
pending = { group: { ids, cache, ...policy }, resolve, reject }; notify();
|
|
33
|
+
});
|
|
34
|
+
}).catch(error => { preparationFailed = true; preparationError = error; }).finally(notify);
|
|
35
|
+
await offered;
|
|
36
|
+
if (!pending) {
|
|
37
|
+
await preparing;
|
|
38
|
+
if (preparationFailed) throw preparationError;
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
const held = pending;
|
|
42
|
+
let preparedHidden: MlxArray | undefined;
|
|
43
|
+
let mixed = false;
|
|
44
|
+
try {
|
|
45
|
+
await options.decode(async (ids, cache, policy) => {
|
|
46
|
+
if (mixed) return options.forward(ids, cache, policy);
|
|
47
|
+
const outputs = options.mixed([{ ids, cache, ...policy }, held.group]);
|
|
48
|
+
mixed = true; preparedHidden = outputs[1]!;
|
|
49
|
+
return outputs[0]!;
|
|
50
|
+
});
|
|
51
|
+
// A final unread token can retire decode without another model call.
|
|
52
|
+
preparedHidden ??= await options.forward(held.group.ids, held.group.cache, held.group);
|
|
53
|
+
held.resolve(preparedHidden); preparedHidden = undefined;
|
|
54
|
+
await preparing;
|
|
55
|
+
if (preparationFailed) throw preparationError;
|
|
56
|
+
return true;
|
|
57
|
+
} catch (error) {
|
|
58
|
+
preparedHidden?.dispose(); held.reject(error);
|
|
59
|
+
await preparing;
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -4,7 +4,7 @@ import { Gemma4Model } from "../../model/gemma4";
|
|
|
4
4
|
import { Qwen35Model } from "../../model/qwen3_5";
|
|
5
5
|
import { spliceImageTokens } from "../../vision/diffusion-vision";
|
|
6
6
|
import { buildMultimodalPrompt, buildVisionPrompt, extractAudio, extractImages, extractVideos,
|
|
7
|
-
type
|
|
7
|
+
type MultimodalTowers } from "../../vision/prompt";
|
|
8
8
|
import { buildQwen3VLVisionPrompt } from "../../vision/qwen3vl-prompt";
|
|
9
9
|
import type { Qwen3VLVisionTower } from "../../vision/qwen3vl-tower";
|
|
10
10
|
import { normalizeMessages, type ChatRequestParams } from "../../serve/chat-request";
|
|
@@ -12,7 +12,8 @@ import { getAudioTower, getVisionTower, type ServerContext } from "../../serve/m
|
|
|
12
12
|
import { RequestError } from "../../serve/pipeline";
|
|
13
13
|
import type { RequestOwnership } from "../../serve/request-plan";
|
|
14
14
|
import type { RequestPrep } from "../../serve/request-prep";
|
|
15
|
-
import type { BuiltPrompt } from "../../serve/model-binding";
|
|
15
|
+
import type { BuiltPrompt, PromptNativeWork } from "../../serve/model-binding";
|
|
16
|
+
import { EncoderCache } from "./encoder-cache";
|
|
16
17
|
|
|
17
18
|
export async function buildModelPrompt(
|
|
18
19
|
ctx: ServerContext,
|
|
@@ -20,6 +21,8 @@ export async function buildModelPrompt(
|
|
|
20
21
|
body: ChatRequestParams,
|
|
21
22
|
tools: ChatRequestParams["tools"] | null,
|
|
22
23
|
ownership: RequestOwnership,
|
|
24
|
+
nativeWork: PromptNativeWork = (work) => work(),
|
|
25
|
+
objects?: import("../../contracts/object-cache").ObjectCache<import("./checkpoint-state").CheckpointAttachment[]>,
|
|
23
26
|
): Promise<BuiltPrompt> {
|
|
24
27
|
const toolList = tools ?? null;
|
|
25
28
|
const partOf = (types: string[]) => body.messages.some(
|
|
@@ -53,19 +56,6 @@ export async function buildModelPrompt(
|
|
|
53
56
|
// audio on a model whose tower is unavailable is an explicit 400
|
|
54
57
|
// — never a silent text-only degrade (that leniency is only for
|
|
55
58
|
// requests without the media, getVisionTower's contract).
|
|
56
|
-
const audioTower = getAudioTower(ctx);
|
|
57
|
-
if (!audioTower || !ctx.audioTokenIds) {
|
|
58
|
-
throw new RequestError(400,
|
|
59
|
-
`model ${ctx.modelId} has no audio tower — audio input needs ` +
|
|
60
|
-
`a model whose config.json carries audio_config and whose ` +
|
|
61
|
-
`sidecar ships the audio tensors (e.g. gemma-4 e4b OptiQ)`);
|
|
62
|
-
}
|
|
63
|
-
let visionSide: { tower: VisionEncoder; tokenIds: VisionTokenIds } | undefined;
|
|
64
|
-
if (hasImages) {
|
|
65
|
-
const tower = getVisionTower(ctx);
|
|
66
|
-
if (!tower) throw new RequestError(400, "model has no vision sidecar");
|
|
67
|
-
visionSide = { tower, tokenIds: ctx.visionTokenIds };
|
|
68
|
-
}
|
|
69
59
|
const { messages: withAudioParts, images } =
|
|
70
60
|
await extractImages(normalizeMessages(body.messages));
|
|
71
61
|
const { messages, audio } = await extractAudio(withAudioParts);
|
|
@@ -74,13 +64,30 @@ export async function buildModelPrompt(
|
|
|
74
64
|
// Failures throw into the prompt-build 400.
|
|
75
65
|
const wavs = await Promise.all(audio.map(ensureWav));
|
|
76
66
|
// The towers are only ever non-null for Gemma4 (loader gates).
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
{
|
|
67
|
+
const towers = await nativeWork(async () => {
|
|
68
|
+
const audioTower = getAudioTower(ctx);
|
|
69
|
+
if (!audioTower || !ctx.audioTokenIds) {
|
|
70
|
+
throw new RequestError(400,
|
|
71
|
+
`model ${ctx.modelId} has no audio tower — audio input needs ` +
|
|
72
|
+
`a model whose config.json carries audio_config and whose ` +
|
|
73
|
+
`sidecar ships the audio tensors (e.g. gemma-4 e4b OptiQ)`);
|
|
74
|
+
}
|
|
75
|
+
let visionSide: MultimodalTowers<{ softTokens: number }>["vision"];
|
|
76
|
+
if (hasImages) {
|
|
77
|
+
const tower = getVisionTower(ctx);
|
|
78
|
+
if (!tower) throw new RequestError(400, "model has no vision sidecar");
|
|
79
|
+
visionSide = { tower, tokenIds: ctx.visionTokenIds,
|
|
80
|
+
cache: objects && tower.cacheIdentity ? new EncoderCache(objects, tower.cacheIdentity) : undefined };
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
80
83
|
...(visionSide ? { vision: visionSide } : {}),
|
|
81
|
-
audio: { tower: audioTower, tokenIds: ctx.audioTokenIds
|
|
82
|
-
|
|
83
|
-
|
|
84
|
+
audio: { tower: audioTower, tokenIds: ctx.audioTokenIds,
|
|
85
|
+
cache: objects ? new EncoderCache(objects, audioTower.cacheIdentity) : undefined },
|
|
86
|
+
};
|
|
87
|
+
});
|
|
88
|
+
const mp = await buildMultimodalPrompt(
|
|
89
|
+
ctx.model as Gemma4Model, towers, ctx.tokenizer, ctx.template,
|
|
90
|
+
messages, images, wavs, toolList, nativeWork,
|
|
84
91
|
);
|
|
85
92
|
// bidirMask is null whenever audio is present (§3.3 Q1: mixed
|
|
86
93
|
// prompts run fully causal); the union mask does the per-layer
|
|
@@ -89,7 +96,7 @@ export async function buildModelPrompt(
|
|
|
89
96
|
...noMedia,
|
|
90
97
|
promptIds: mp.ids,
|
|
91
98
|
vision: {
|
|
92
|
-
embeddings: mp.embeddings,
|
|
99
|
+
embeddings: mp.embeddings, prefixIdentity: mp.prefixIdentity,
|
|
93
100
|
...(mp.bidirMask ? { imageMask: mp.bidirMask } : {}),
|
|
94
101
|
multimodalMask: mp.multimodalMask,
|
|
95
102
|
},
|
|
@@ -106,7 +113,7 @@ export async function buildModelPrompt(
|
|
|
106
113
|
throw new RequestError(400, "DiffusionGemma image input supports exactly one image");
|
|
107
114
|
const rendered = ctx.template.render(messages, { tools: toolList, addGenerationPrompt: true });
|
|
108
115
|
const rawIds = ctx.tokenizer.encode(rendered, /* addSpecialTokens */ false);
|
|
109
|
-
const { pixels, softTokens } = await dm.visionTower
|
|
116
|
+
const { pixels, softTokens } = await nativeWork(() => dm.visionTower!.preprocess(images[0]!));
|
|
110
117
|
const diffusionPixels = ownership.own(pixels);
|
|
111
118
|
const promptIds = spliceImageTokens(rawIds, [softTokens], {
|
|
112
119
|
image: ctx.visionTokenIds.imageTokenId,
|
|
@@ -121,13 +128,16 @@ export async function buildModelPrompt(
|
|
|
121
128
|
// image/video spans splice into input embeddings and the request
|
|
122
129
|
// carries mRoPE positions + delta (PLAN 14v/14w). Videos decode
|
|
123
130
|
// to sampled frames via the AVFoundation sidecar.
|
|
124
|
-
const tower = getVisionTower(ctx) as unknown as Qwen3VLVisionTower | null;
|
|
125
|
-
if (!tower) throw new RequestError(400, "model has no vision sidecar");
|
|
126
131
|
const { messages: withVideos, images } =
|
|
127
132
|
await extractImages(normalizeMessages(body.messages));
|
|
128
133
|
const { messages, videos } = await extractVideos(withVideos);
|
|
134
|
+
const { tower, encoderCache } = await nativeWork(async () => {
|
|
135
|
+
const tower = getVisionTower(ctx) as unknown as Qwen3VLVisionTower | null;
|
|
136
|
+
if (!tower) throw new RequestError(400, "model has no vision sidecar");
|
|
137
|
+
return { tower, encoderCache: objects ? new EncoderCache(objects, tower.cacheIdentity) : undefined };
|
|
138
|
+
});
|
|
129
139
|
const vp = await buildQwen3VLVisionPrompt(
|
|
130
|
-
ctx.model, tower, ctx.tokenizer, ctx.template, messages, images,
|
|
140
|
+
ctx.model as Qwen35Model, tower, ctx.tokenizer, ctx.template, messages, images,
|
|
131
141
|
{
|
|
132
142
|
imageTokenId: (ctx.model.config.raw.image_token_id as number) ?? 248056,
|
|
133
143
|
videoTokenId: (ctx.model.config.raw.video_token_id as number) ?? 248057,
|
|
@@ -135,23 +145,27 @@ export async function buildModelPrompt(
|
|
|
135
145
|
visionEndId: (ctx.model.config.raw.vision_end_token_id as number) ?? 248054,
|
|
136
146
|
},
|
|
137
147
|
prep.templateOptionsFor(body, toolList),
|
|
138
|
-
videos,
|
|
148
|
+
videos, nativeWork, encoderCache,
|
|
139
149
|
);
|
|
140
|
-
return { ...noMedia, promptIds: vp.ids, vision: { embeddings: vp.embeddings, mrope: vp.mrope } };
|
|
150
|
+
return { ...noMedia, promptIds: vp.ids, vision: { embeddings: vp.embeddings, mrope: vp.mrope, prefixIdentity: vp.prefixIdentity } };
|
|
141
151
|
}
|
|
142
152
|
if (hasImages) {
|
|
143
153
|
// Loads (and caches) the tower on first image request — text-only
|
|
144
154
|
// sessions never pay for it.
|
|
145
|
-
const tower = getVisionTower(ctx);
|
|
146
|
-
if (!tower) throw new RequestError(400, "model has no vision sidecar");
|
|
147
155
|
const { messages, images } = await extractImages(normalizeMessages(body.messages));
|
|
148
156
|
// The tower is only ever non-null for Gemma4 (sidecar gate in
|
|
149
157
|
// makeVisionLoader), so the model narrow is safe here.
|
|
158
|
+
const { tower, encoderCache } = await nativeWork(async () => {
|
|
159
|
+
const tower = getVisionTower(ctx);
|
|
160
|
+
if (!tower) throw new RequestError(400, "model has no vision sidecar");
|
|
161
|
+
return { tower, encoderCache: objects && tower.cacheIdentity
|
|
162
|
+
? new EncoderCache(objects, tower.cacheIdentity) : undefined };
|
|
163
|
+
});
|
|
150
164
|
const vp = await buildVisionPrompt(
|
|
151
165
|
ctx.model as Gemma4Model, tower, ctx.tokenizer, ctx.template,
|
|
152
|
-
messages, images, ctx.visionTokenIds, toolList,
|
|
166
|
+
messages, images, ctx.visionTokenIds, toolList, nativeWork, encoderCache,
|
|
153
167
|
);
|
|
154
|
-
return { ...noMedia, promptIds: vp.ids, vision: { embeddings: vp.embeddings, imageMask: vp.imageMask } };
|
|
168
|
+
return { ...noMedia, promptIds: vp.ids, vision: { embeddings: vp.embeddings, imageMask: vp.imageMask, prefixIdentity: vp.prefixIdentity } };
|
|
155
169
|
}
|
|
156
170
|
const text = prep.promptIdsFor(body, toolList);
|
|
157
171
|
return {
|
|
@@ -8,8 +8,8 @@ import { buildModelPrompt } from "./model-prompt";
|
|
|
8
8
|
import { MLX_VERSION, deviceArchitecture } from "../../mlx/ffi";
|
|
9
9
|
|
|
10
10
|
export function modelPromptBuilder(context: ServingContext): ModelServingBinding["buildPrompt"] {
|
|
11
|
-
return context.serving?.buildPrompt ?? ((body, tools, ownership, prep) =>
|
|
12
|
-
buildModelPrompt(context as ServerContext, prep, body, tools, ownership));
|
|
11
|
+
return context.serving?.buildPrompt ?? ((body, tools, ownership, prep, nativeWork, objects) =>
|
|
12
|
+
buildModelPrompt(context as ServerContext, prep, body, tools, ownership, nativeWork, objects));
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
/** Existing model classes enter the interface once here. New implementations
|
|
@@ -27,8 +27,9 @@ export function modelServingBinding(context: ServingContext): ModelServingBindin
|
|
|
27
27
|
stateCompatibility: `mlx-${MLX_VERSION}-${deviceArchitecture()}`,
|
|
28
28
|
gateway: bindMlxGateway(model, ctx.draft ?? undefined),
|
|
29
29
|
createSerial: (services) => createMlxSerialExecutor(serial, services),
|
|
30
|
-
buildPrompt: (body, tools, ownership, prep) => buildModelPrompt(ctx, prep, body, tools, ownership),
|
|
30
|
+
buildPrompt: (body, tools, ownership, prep, nativeWork, objects) => buildModelPrompt(ctx, prep, body, tools, ownership, nativeWork, objects),
|
|
31
31
|
restore: (store, entry) => store.restore(entry, model),
|
|
32
|
+
restoreAsync: (store, entry) => store.restoreAsync(entry, model),
|
|
32
33
|
async signal(ids, count, minimum) {
|
|
33
34
|
const caches = model.makeCache();
|
|
34
35
|
try {
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { CacheCodec } from "../../kv-store";
|
|
2
|
+
import { BlockPool, PagedKVCache } from "../../lab/paged-kv/paged-kv";
|
|
3
|
+
|
|
4
|
+
/** Page layout owns its persistence codec, independently of SSD transport. */
|
|
5
|
+
export const pagedCacheCodec: CacheCodec = {
|
|
6
|
+
matches: cache => cache instanceof PagedKVCache,
|
|
7
|
+
snapshot(cache, context) {
|
|
8
|
+
const c = cache as PagedKVCache, p = c.pool!;
|
|
9
|
+
for (const array of p.arrays()) context.push(array, false);
|
|
10
|
+
return { kind: "paged", offset: c.offset, tensors: context.slots,
|
|
11
|
+
...(p.quantization ? p.quantization : {}),
|
|
12
|
+
paged: { capacityTokens: c.capacityTokens, blockSize: c.blockSize, blockTable: [...c.blockTable],
|
|
13
|
+
numBlocks: p.numBlocks, headDim: p.headDim, vHeadDim: p.vHeadDim, dtype: p.dtype, direct: c.direct } };
|
|
14
|
+
},
|
|
15
|
+
clone: cache => (cache as PagedKVCache).clone(),
|
|
16
|
+
load(entry, context) {
|
|
17
|
+
const layout = entry.paged!;
|
|
18
|
+
const quantization = entry.bits ? { bits: entry.bits as 4 | 8, groupSize: entry.groupSize! } : undefined;
|
|
19
|
+
const cache = new PagedKVCache(layout.capacityTokens, layout.blockSize, layout.direct, quantization);
|
|
20
|
+
const pool = new BlockPool({ ...layout, numKvHeads: entry.tensors[0]!.shape[1]!, quantization });
|
|
21
|
+
try { pool.restoreArrays(entry.tensors.map(context.arr), layout.blockTable); }
|
|
22
|
+
catch (error) { pool.dispose(); throw error; }
|
|
23
|
+
cache.pool = pool; cache.offset = entry.offset; cache.blockTable = [...layout.blockTable];
|
|
24
|
+
return cache;
|
|
25
|
+
},
|
|
26
|
+
headerTrimmable: () => true,
|
|
27
|
+
};
|
|
@@ -32,6 +32,10 @@ interface PrefillHost {
|
|
|
32
32
|
|
|
33
33
|
/** Ordinary sampling/cache policy over the shared target preparation driver. */
|
|
34
34
|
export class MlxPrefillCohort extends MlxPrefillRows<PrefillState> {
|
|
35
|
+
override get canAdmit(): boolean {
|
|
36
|
+
return super.canAdmit && this.supportsMixedWork;
|
|
37
|
+
}
|
|
38
|
+
get supportsMixedWork(): boolean { return !this.rows.some(row => row.req.promptInput); }
|
|
35
39
|
constructor(readonly host: PrefillHost) {
|
|
36
40
|
super({
|
|
37
41
|
stateCodecs: host.stateCodecs, maintain: host.maintain,
|
|
@@ -47,7 +51,7 @@ export class MlxPrefillCohort extends MlxPrefillRows<PrefillState> {
|
|
|
47
51
|
return { row, solo: continuation.caches, pos: row.req.promptIds.length,
|
|
48
52
|
snapAt: null, continuation };
|
|
49
53
|
}
|
|
50
|
-
const hit = host.promptCache?.take(row.req.promptIds, row.cacheNamespace) ?? null;
|
|
54
|
+
const hit = host.promptCache?.take(row.req.promptIds, row.cacheNamespace, row.req.cacheSessionId) ?? null;
|
|
51
55
|
if (hit) owned = hit;
|
|
52
56
|
closeCache?.(); closeCache = undefined;
|
|
53
57
|
if (hit) row.cachedTokens = hit.tokens.length;
|
|
@@ -59,6 +63,7 @@ export class MlxPrefillCohort extends MlxPrefillRows<PrefillState> {
|
|
|
59
63
|
});
|
|
60
64
|
const closeSetup = row.req.trace?.begin("prefill.batch_setup", { mechanism: "continuous" });
|
|
61
65
|
owned ??= { caches: row.req.statePolicy?.create() ?? host.model.makeCache() }; closeSetup?.();
|
|
66
|
+
if (row.req.promptInput) host.maintain?.(owned.caches);
|
|
62
67
|
return { row, solo: owned.caches, pos: hit?.tokens.length ?? 0, retain: owned.retain, snapAt, closePrefill };
|
|
63
68
|
} catch (error) {
|
|
64
69
|
return cleanupFailure(error, () => disposeResources([...(owned?.caches ?? []),
|
|
@@ -66,16 +71,22 @@ export class MlxPrefillCohort extends MlxPrefillRows<PrefillState> {
|
|
|
66
71
|
} finally { closeCache?.(); }
|
|
67
72
|
},
|
|
68
73
|
ready: state => !!state.continuation,
|
|
69
|
-
plan(state) {
|
|
74
|
+
plan(state) {
|
|
75
|
+
if (state.row.req.promptInput) return { kind: "final", start: state.pos,
|
|
76
|
+
end: state.row.promptTokens, snapshot: false, batchYield: false, atomic: true };
|
|
77
|
+
return nextPrefillStep({ length: state.row.promptTokens, position: state.pos,
|
|
70
78
|
chunkSize: state.row.req.prefillChunkSize ?? host.chunkSize,
|
|
71
79
|
tailSplit: host.tailSplit, snapshotAt: state.snapAt }); },
|
|
72
|
-
forward:
|
|
80
|
+
forward: (ids, caches, states, work) => states[0]?.row.req.promptInput
|
|
81
|
+
? Promise.resolve(states[0].row.req.promptInput.forward(ids, caches, states[0].pos))
|
|
82
|
+
: work ? work(ids, caches) : host.forward(ids, caches),
|
|
83
|
+
project: host.project.bind(host),
|
|
73
84
|
complete: (state, logits) => state.continuation ? host.resume!(state) : host.complete(state, logits!), reject: host.reject.bind(host),
|
|
74
85
|
checkpoint(state, capture) {
|
|
75
86
|
let snapshot: Cache[] | undefined;
|
|
76
87
|
try {
|
|
77
88
|
snapshot = capture();
|
|
78
|
-
host.promptCache!.put(state.row.req.promptIds.slice(0, state.pos), snapshot, state.row.cacheNamespace);
|
|
89
|
+
host.promptCache!.put(state.row.req.promptIds.slice(0, state.pos), snapshot, state.row.cacheNamespace, undefined, undefined, state.row.req.cacheSessionId);
|
|
79
90
|
snapshot = undefined;
|
|
80
91
|
} catch (error) {
|
|
81
92
|
if (snapshot) disposeResources(snapshot);
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { ModelConfig } from "../../config";
|
|
2
|
+
import type { PrefillPolicy } from "../../inference/prefill";
|
|
3
|
+
import { sdpaFallbackBytes } from "../../kv-scheme";
|
|
4
|
+
import type { RuntimeConfig } from "../../runtime-config";
|
|
5
|
+
|
|
6
|
+
/** Retain large chunks for short prompts, but bound the materialized attention
|
|
7
|
+
* score workspace of recurrent-attention models at long context. This changes
|
|
8
|
+
* work size, never request admission. Explicit chunk settings remain exact. */
|
|
9
|
+
export function resolveMlxPrefillPolicy(config: ModelConfig, runtime: RuntimeConfig,
|
|
10
|
+
override?: number): PrefillPolicy {
|
|
11
|
+
const explicit = override ?? (runtime.value("MLX_BUN_RD_PREFILL_CHUNK") === undefined
|
|
12
|
+
? undefined : runtime.number("MLX_BUN_RD_PREFILL_CHUNK", 2048));
|
|
13
|
+
if (explicit !== undefined) {
|
|
14
|
+
const chunk = Math.max(1, Math.floor(explicit));
|
|
15
|
+
return { chunkSize: () => chunk };
|
|
16
|
+
}
|
|
17
|
+
if (!config.text?.layerTypes?.includes("linear_attention")) return { chunkSize: () => 2048 };
|
|
18
|
+
// Capture geometry now, independently of later configuration mutation.
|
|
19
|
+
const geometry = { ...config, text: { ...config.text, layerTypes: [...config.text.layerTypes] } };
|
|
20
|
+
const workspaceBytes = 1024 ** 3;
|
|
21
|
+
return { chunkSize(promptTokens) {
|
|
22
|
+
let chunk = 2048;
|
|
23
|
+
while (chunk > 8 && sdpaFallbackBytes(geometry, chunk, promptTokens) > workspaceBytes) chunk /= 2;
|
|
24
|
+
return chunk;
|
|
25
|
+
} };
|
|
26
|
+
}
|
|
@@ -9,9 +9,23 @@ import type { MlxGroupPreparation, Row } from "./batch-group";
|
|
|
9
9
|
import { evalCacheState } from "./prefill";
|
|
10
10
|
import { prefillCacheLayout } from "./cache-layout";
|
|
11
11
|
import { MlxStateRows } from "./state-rows";
|
|
12
|
+
import type { P2RTracePhase, P2RTraceAttributes } from "../../serve/prompt-response-trace";
|
|
13
|
+
import type { MlxForwardWork, MlxPreparationWork } from "./mixed-iteration";
|
|
12
14
|
import type { KvMaintenance } from "./kv-maintenance";
|
|
13
15
|
|
|
16
|
+
// Shared work is recorded on every participating request. workId identifies
|
|
17
|
+
// duplicates when constructing a process timeline; row spans are not additive.
|
|
18
|
+
let nextWorkId = 0;
|
|
19
|
+
function traceRows(states: readonly MlxPrefillState[], phase: P2RTracePhase, attributes: P2RTraceAttributes) {
|
|
20
|
+
if (!states.some(state => state.row.req.trace)) return null;
|
|
21
|
+
const closes = states.map(state => state.row.req.trace?.begin(phase, attributes));
|
|
22
|
+
return { [Symbol.dispose]() { for (const close of closes) close?.(); } };
|
|
23
|
+
}
|
|
24
|
+
|
|
14
25
|
export interface MlxPrefillStep extends PrefillStep {
|
|
26
|
+
/** A non-causal input cannot be split at a scheduler token budget.
|
|
27
|
+
* Its preparation must decline joins until this step completes. */
|
|
28
|
+
readonly atomic?: boolean;
|
|
15
29
|
/** Some methods maintain their final target chunk before sampling. */
|
|
16
30
|
readonly maintain?: boolean;
|
|
17
31
|
}
|
|
@@ -34,7 +48,7 @@ export interface MlxPrefillRowsHost<State extends MlxPrefillState> {
|
|
|
34
48
|
plan(state: State): MlxPrefillStep;
|
|
35
49
|
/** Already prepared state can complete without any target forward. */
|
|
36
50
|
ready?(state: State): boolean;
|
|
37
|
-
forward(ids: MlxArray, caches: Cache[], states: readonly State[]): Promise<MlxArray>;
|
|
51
|
+
forward(ids: MlxArray, caches: Cache[], states: readonly State[], work?: MlxForwardWork): Promise<MlxArray>;
|
|
38
52
|
project(hidden: MlxArray, caches: Cache[], completed: readonly State[]): MlxArray | null;
|
|
39
53
|
/** Consume method context after target maintenance, before snapshots. */
|
|
40
54
|
afterForward?(ids: MlxArray, caches: Cache[], states: readonly State[], hidden: MlxArray): void | Promise<void>;
|
|
@@ -105,7 +119,8 @@ export class MlxPrefillRows<State extends MlxPrefillState> implements MlxGroupPr
|
|
|
105
119
|
for (const cache of this.#stateRows.caches) cache.prefillMaintenance?.beginPrefill();
|
|
106
120
|
}
|
|
107
121
|
|
|
108
|
-
async advance(): Promise<boolean> {
|
|
122
|
+
async advance(workLimit?: MlxPreparationWork): Promise<boolean> {
|
|
123
|
+
let remaining = workLimit?.maxTokens ?? Infinity;
|
|
109
124
|
// Complete restored inputs before merging: their cache coverage must not
|
|
110
125
|
// introduce padding or a zero-width forward into unrelated cold rows.
|
|
111
126
|
if (this.operations.ready && this.#states.some(state => this.operations.ready!(state))) {
|
|
@@ -122,7 +137,10 @@ export class MlxPrefillRows<State extends MlxPrefillState> implements MlxGroupPr
|
|
|
122
137
|
this.#states = remaining; this.#rows = remaining.map(state => state.row);
|
|
123
138
|
if (!remaining.length) { this.dispose(); return true; }
|
|
124
139
|
}
|
|
125
|
-
|
|
140
|
+
{
|
|
141
|
+
using span = traceRows(this.#states, "prefill.row_sync", { workId: ++nextWorkId, batchSize: this.#states.length });
|
|
142
|
+
this.#synchronizeRows();
|
|
143
|
+
}
|
|
126
144
|
const keep: number[] = [];
|
|
127
145
|
for (const [index, state] of this.#states.entries()) {
|
|
128
146
|
if (!state.row.req.signal?.aborted) { keep.push(index); continue; }
|
|
@@ -132,15 +150,23 @@ export class MlxPrefillRows<State extends MlxPrefillState> implements MlxGroupPr
|
|
|
132
150
|
if (!this.#states.length) { this.dispose(); return true; }
|
|
133
151
|
while (this.#states.length) {
|
|
134
152
|
for (const state of this.#states) state.planned ??= this.operations.plan(state);
|
|
135
|
-
const count = Math.min(
|
|
153
|
+
const count = Math.min(this.#states.some(state => state.planned!.atomic) ? Infinity
|
|
154
|
+
: Math.max(1, Math.floor(remaining / this.#states.length)),
|
|
155
|
+
...this.#states.map(state => state.planned!.end - state.pos));
|
|
156
|
+
const work = { workId: ++nextWorkId, batchSize: this.#states.length, tokensPerRow: count };
|
|
136
157
|
const closes = this.#states.map(state => state.row.req.trace?.begin("prefill.chunk", {
|
|
137
|
-
mechanism: "continuous", startToken: state.pos,
|
|
158
|
+
mechanism: "continuous", startToken: state.pos, ...work,
|
|
138
159
|
}));
|
|
139
160
|
const caches = this.#stateRows?.caches ?? this.#states[0]!.solo;
|
|
140
161
|
try {
|
|
141
162
|
using ids = count ? ops.fromInt32(this.#states.flatMap(state => state.row.req.promptIds.slice(state.pos, state.pos + count)),
|
|
142
163
|
[this.#states.length, count]) : null;
|
|
143
|
-
|
|
164
|
+
let forwarded: MlxArray | null;
|
|
165
|
+
{
|
|
166
|
+
using span = traceRows(this.#states, "prefill.forward", work);
|
|
167
|
+
forwarded = ids ? await this.operations.forward(ids, caches, this.#states, workLimit?.forward) : null;
|
|
168
|
+
}
|
|
169
|
+
using hidden = forwarded;
|
|
144
170
|
const drains: number[] = [], finals: number[] = [];
|
|
145
171
|
let batchYield = false;
|
|
146
172
|
for (const [row, state] of this.#states.entries()) {
|
|
@@ -152,25 +178,42 @@ export class MlxPrefillRows<State extends MlxPrefillState> implements MlxGroupPr
|
|
|
152
178
|
}
|
|
153
179
|
const maintained = [...drains, ...finals.filter(row => this.#states[row]!.planned!.maintain)];
|
|
154
180
|
if (count && (maintained.length || !finals.length)) {
|
|
155
|
-
|
|
181
|
+
{
|
|
182
|
+
using span = traceRows(this.#states, "prefill.evaluate", work);
|
|
183
|
+
evalCacheState(caches);
|
|
184
|
+
}
|
|
185
|
+
using span = traceRows(this.#states, "prefill.kv_maintenance", work);
|
|
156
186
|
if (this.#stateRows) {
|
|
157
187
|
for (const cache of caches) cache.prefillMaintenance?.commitPrefill(maintained);
|
|
158
188
|
} else if (maintained.length) this.operations.maintain?.(this.#states[0]!.solo);
|
|
159
|
-
|
|
189
|
+
// A scheduler budget can split one planned chunk into many pieces.
|
|
190
|
+
// Resolve state at each yield, but retain allocator reuse until the
|
|
191
|
+
// method's maintenance boundary instead of purging it every piece.
|
|
192
|
+
if (maintained.length) clearCache();
|
|
160
193
|
}
|
|
161
|
-
if (ids) {
|
|
194
|
+
if (ids && this.operations.afterForward) {
|
|
195
|
+
using span = traceRows(this.#states, "prefill.companion", work);
|
|
162
196
|
const companion = this.operations.afterForward?.(ids, caches, this.#states, hidden!);
|
|
163
197
|
if (companion) await companion;
|
|
164
198
|
}
|
|
165
199
|
for (const row of [...drains, ...finals]) {
|
|
166
200
|
const state = this.#states[row]!;
|
|
167
|
-
if (state.planned!.snapshot)
|
|
168
|
-
|
|
201
|
+
if (state.planned!.snapshot) {
|
|
202
|
+
using span = traceRows([state], "prefill.checkpoint", { ...work, row });
|
|
203
|
+
this.operations.checkpoint(state, () =>
|
|
204
|
+
this.#stateRows ? this.#stateRows.extractRow(row) : cloneKvCaches(state.solo, this.operations.stateCodecs), row);
|
|
205
|
+
}
|
|
169
206
|
if (!finals.includes(row)) state.planned = undefined;
|
|
170
207
|
}
|
|
171
|
-
|
|
208
|
+
let projected: MlxArray | null = null;
|
|
209
|
+
if (hidden && finals.length) {
|
|
210
|
+
using span = traceRows(this.#states, "prefill.project", work);
|
|
211
|
+
projected = this.operations.project(hidden, caches, finals.map(row => this.#states[row]!));
|
|
212
|
+
}
|
|
213
|
+
using logits = projected;
|
|
172
214
|
for (const row of finals) {
|
|
173
215
|
const state = this.#states[row]!;
|
|
216
|
+
using span = traceRows([state], "prefill.complete", { ...work, row });
|
|
174
217
|
if (this.#stateRows) state.solo = this.#stateRows.extractRow(row);
|
|
175
218
|
using part = logits ? logits.slice([row, logits.shape[1]! - 1, 0], [row + 1, logits.shape[1]!, logits.shape[2]!]) : null;
|
|
176
219
|
try {
|
|
@@ -185,7 +228,8 @@ export class MlxPrefillRows<State extends MlxPrefillState> implements MlxGroupPr
|
|
|
185
228
|
for (const cache of caches) (cache as { releaseRopeArr?: () => void }).releaseRopeArr?.();
|
|
186
229
|
if (finals.length) this.#filter(this.#states.flatMap((_, row) => finals.includes(row) ? [] : [row]));
|
|
187
230
|
if (!this.#states.length) { this.dispose(); return true; }
|
|
188
|
-
|
|
231
|
+
remaining -= count * work.batchSize;
|
|
232
|
+
if (batchYield || finals.length || remaining < this.#states.length) return false;
|
|
189
233
|
} finally { for (const close of closes) close?.(); }
|
|
190
234
|
}
|
|
191
235
|
return true;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { MlxArray } from "../../mlx/array";
|
|
2
|
+
import type { Cache } from "../../model/gemma4-base";
|
|
3
|
+
|
|
4
|
+
/** Prepared native input with an indivisible prefill. The preparation owner
|
|
5
|
+
* retains its tensors; the model binding returns an owned final hidden row.
|
|
6
|
+
* Decode, sampling and row retirement use the ordinary execution group. */
|
|
7
|
+
export interface MlxPromptInput {
|
|
8
|
+
forward(ids: MlxArray, caches: Cache[], start?: number): MlxArray;
|
|
9
|
+
readonly decodeState?: MlxDecodeState;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Additional model-owned forward state. Equal keys declare compatible row
|
|
13
|
+
* state; the backend supplies the current row order after joins/retirement. */
|
|
14
|
+
export interface MlxDecodeState {
|
|
15
|
+
readonly key: string;
|
|
16
|
+
forward(ids: MlxArray, caches: Cache[], rows: readonly MlxDecodeState[]): MlxArray;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Preserve the ordinary generator's projection geometry for a prepared
|
|
20
|
+
* embedding sequence: process the whole prompt, then project only its tip. */
|
|
21
|
+
export function bindEmbeddingsInput(
|
|
22
|
+
forward: (ids: MlxArray, caches: Cache[], start: number) => MlxArray,
|
|
23
|
+
): MlxPromptInput {
|
|
24
|
+
return { forward(ids, caches, start = 0) {
|
|
25
|
+
using hidden = forward(ids, caches, start);
|
|
26
|
+
const [batch, length, width] = hidden.shape;
|
|
27
|
+
return hidden.slice([0, length! - 1, 0], [batch!, length!, width!]);
|
|
28
|
+
} };
|
|
29
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { Qwen35Model } from "../../model/qwen3_5";
|
|
2
|
+
import { mropePositionIds, type MropeRequestState } from "../../model/qwen3-mrope";
|
|
3
|
+
import type { MlxArray } from "../../mlx/array";
|
|
4
|
+
import type { Cache } from "../../model/gemma4-base";
|
|
5
|
+
import * as ops from "../../mlx/ops";
|
|
6
|
+
import { Dtype } from "../../mlx/ffi";
|
|
7
|
+
import { bindEmbeddingsInput, type MlxDecodeState, type MlxPromptInput } from "./prompt-input";
|
|
8
|
+
|
|
9
|
+
class QwenMediaDecodeState implements MlxDecodeState {
|
|
10
|
+
readonly key = "qwen-interleaved-mrope";
|
|
11
|
+
constructor(readonly model: Qwen35Model, readonly delta: number) {}
|
|
12
|
+
|
|
13
|
+
forward(ids: MlxArray, caches: Cache[], rows: readonly MlxDecodeState[]): MlxArray {
|
|
14
|
+
const B = rows.length, L = ids.shape[1]!;
|
|
15
|
+
const cache = caches[this.model.faIdx]!;
|
|
16
|
+
// The cache supplies logical positions, including left padding, on device.
|
|
17
|
+
// No offset readback or request-global position mutation is needed.
|
|
18
|
+
const offsets = (cache as Cache & { ropeOffsetArr?: MlxArray }).ropeOffsetArr;
|
|
19
|
+
using deltas = ops.fromInt32(rows.map(row => (row as QwenMediaDecodeState).delta), [B]);
|
|
20
|
+
using scalar = offsets ? null : ops.fromInt32([cache.offset], []);
|
|
21
|
+
using starts = ops.add(offsets ?? scalar!, deltas);
|
|
22
|
+
using startRows = ops.reshape(starts, [B, 1]);
|
|
23
|
+
using indices = ops.arange(0, L, 1, Dtype.int32);
|
|
24
|
+
using positions = ops.add(startRows, indices);
|
|
25
|
+
using axis = ops.reshape(positions, [1, B, L]);
|
|
26
|
+
using grid = ops.concatAxis([axis, axis, axis], 0);
|
|
27
|
+
return this.model.forwardHiddenAtPositions(ids, caches, grid);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** The prompt owns its grid; only its continuation delta survives into decode. */
|
|
32
|
+
export function bindQwenMediaInput(model: Qwen35Model, embeddings: MlxArray,
|
|
33
|
+
state: MropeRequestState): MlxPromptInput {
|
|
34
|
+
const input = bindEmbeddingsInput((ids, caches, start) => {
|
|
35
|
+
using positions = mropePositionIds(state, start, ids.shape[1]!);
|
|
36
|
+
return start > 0 ? model.forwardHiddenAtPositions(ids, caches, positions)
|
|
37
|
+
: model.forwardEmbeddingsAtPositions(embeddings, caches, positions);
|
|
38
|
+
});
|
|
39
|
+
return { ...input, decodeState: new QwenMediaDecodeState(model, state.delta) };
|
|
40
|
+
}
|