mlx-bun 0.3.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 +58 -42
- package/docs/archive/investigations/benchmark-ledger-through-v0.4.0.md +3617 -0
- package/docs/archive/investigations/openjarvis-fit.md +198 -0
- package/docs/archive/investigations/qwen38-27b-campaign-log.md +1092 -0
- package/docs/archive/planning/release-notes-v0.4.0.md +45 -0
- package/docs/archive/planning/release-notes-v0.5.0.md +37 -0
- package/docs/design/batching.md +1535 -50
- package/docs/design/decode-speed-program.md +394 -1171
- package/docs/design/generic-model-support.md +195 -3
- package/docs/design/kv-cache.md +538 -225
- package/docs/design/speculative-decoding.md +552 -50
- package/docs/design/turboquant.md +140 -17
- package/docs/design/unified-engine-frontier-plan.md +259 -150
- package/docs/planning/PRODUCT_ROADMAP.md +21 -6
- package/docs/planning/public-readiness-handoff.md +244 -0
- package/docs/planning/public-readiness.md +149 -0
- package/docs/reference/benchmarks.md +3537 -1112
- package/docs/reference/cli.md +102 -2
- package/docs/reference/distribution.md +45 -13
- package/docs/reference/environment.md +159 -13
- package/docs/reference/library-api.md +18 -4
- package/docs/reference/memory.md +1 -1
- package/docs/reference/models.md +28 -7
- package/docs/reference/server-api.md +257 -33
- package/docs/reference/server-config.md +372 -151
- package/docs/reference/troubleshooting.md +13 -21
- 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/assistant-target.ts +46 -0
- package/src/backends/mlx/autoregressive-draft-rows.ts +120 -0
- package/src/backends/mlx/autoregressive.ts +22 -1
- package/src/backends/mlx/batch-group.ts +464 -236
- package/src/backends/mlx/cache-attachment.ts +67 -0
- package/src/backends/mlx/cache-layout.ts +31 -0
- package/src/backends/mlx/checkpoint-state.ts +53 -0
- package/src/backends/mlx/continuation-persistence.ts +107 -0
- package/src/backends/mlx/continuation-request.ts +43 -0
- package/src/backends/mlx/continuation.ts +77 -0
- package/src/backends/mlx/draft-target.ts +7 -22
- 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/full-kv-row-append.ts +32 -0
- package/src/backends/mlx/full-kv-row-donor.ts +54 -0
- package/src/backends/mlx/full-transitioning-kv-rows.ts +65 -0
- package/src/backends/mlx/gateway-binding.ts +122 -19
- package/src/backends/mlx/kv-codec.ts +9 -0
- package/src/backends/mlx/kv-maintenance.ts +161 -0
- package/src/backends/mlx/kv-tensor-rows.ts +212 -0
- 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 +5 -4
- package/src/backends/mlx/paged-cache-codec.ts +27 -0
- package/src/backends/mlx/prefill-cohort.ts +100 -0
- package/src/backends/mlx/prefill-policy.ts +26 -0
- package/src/backends/mlx/prefill-rows.ts +244 -0
- package/src/backends/mlx/prefill.ts +1 -1
- package/src/backends/mlx/projected-context-rows.ts +72 -0
- 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 +54 -0
- package/src/backends/mlx/rollback.ts +45 -4
- package/src/backends/mlx/row-sampling.ts +49 -0
- package/src/backends/mlx/serial-executor.ts +51 -73
- package/src/backends/mlx/speculative-group.ts +438 -0
- package/src/backends/mlx/speculative-round.ts +161 -0
- package/src/backends/mlx/speculative.ts +24 -7
- package/src/backends/mlx/state-rows.ts +104 -0
- package/src/backends/mlx/state-views.ts +21 -1
- package/src/backends/mlx/target-layout-capability.ts +35 -0
- package/src/backends/mlx/transitioning-kv-rows.ts +173 -0
- package/src/cli.ts +333 -36
- package/src/contracts/execution.ts +21 -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 +33 -0
- package/src/contracts/resources.ts +7 -0
- package/src/contracts/sampling.ts +23 -0
- package/src/contracts/scheduling.ts +27 -0
- package/src/engine/execution-plan.ts +38 -25
- package/src/engine/execution-tasks.ts +44 -0
- package/src/engine/namespaced-cache.ts +13 -0
- package/src/engine/resources.ts +12 -1
- package/src/engine/scheduler.ts +32 -7
- package/src/fill/fill-session.ts +14 -3
- package/src/fit.ts +29 -15
- package/src/generate.ts +105 -195
- package/src/grammar.ts +13 -0
- package/src/inference/draft-acceptance.ts +41 -0
- package/src/inference/draft-output.ts +49 -0
- package/src/inference/prefill.ts +6 -0
- package/src/inference/rollback.ts +2 -2
- package/src/kv-scheme.ts +92 -12
- package/src/kv-store.ts +183 -93
- 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 +111 -0
- package/src/lab/paged-kv/paged-kv.ts +126 -46
- package/src/md-modules.d.ts +5 -0
- package/src/mlx/array.ts +39 -1
- 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/artifact-identity.ts +73 -0
- package/src/model/batched-kv.ts +235 -0
- package/src/model/batched-mask.ts +12 -4
- package/src/model/batched-quant.ts +8 -2
- package/src/model/batched-quantized-kv.ts +144 -0
- package/src/model/batched-rotating-quant.ts +76 -15
- package/src/model/batched-rotating-state.ts +76 -0
- package/src/model/batched-rotating.ts +64 -22
- package/src/model/batched-row-storage.ts +52 -0
- package/src/model/batched-ssm.ts +52 -0
- package/src/model/batched-turboquant-kv.ts +94 -0
- package/src/model/compiled-decode.ts +7 -1
- package/src/model/decoded-kv-donor.ts +12 -0
- package/src/model/delayed-quantized-kv.ts +72 -0
- package/src/model/delayed-rotating-quantized-kv.ts +93 -0
- package/src/model/delayed-turboquant-kv.ts +57 -0
- package/src/model/factory.ts +26 -0
- package/src/model/fingerprint.ts +12 -0
- package/src/model/full-prefill-padding.ts +56 -0
- package/src/model/full-prefill-row.ts +105 -0
- package/src/model/gemma4-base.ts +183 -82
- package/src/model/gemma4.ts +116 -28
- package/src/model/generated/gemma4-12b.ts +4 -1
- package/src/model/generated/gemma4-26b.ts +4 -1
- package/src/model/generated/gemma4-e4b.ts +4 -1
- package/src/model/glm52-cache.ts +80 -4
- package/src/model/glm52-mla.ts +12 -6
- package/src/model/glm52.ts +19 -40
- package/src/model/kv-attention-view.ts +70 -0
- package/src/model/minicpm5.ts +11 -7
- package/src/model/profile.ts +29 -4
- package/src/model/quantized-append-attention.ts +31 -0
- package/src/model/qwen3-delta-state.ts +74 -0
- package/src/model/qwen3-delta.ts +59 -36
- package/src/model/qwen3-mrope.ts +7 -9
- package/src/model/qwen3_5.ts +177 -75
- package/src/model/rotating-kv-donor.ts +35 -0
- package/src/model/rotating-kv-layout.ts +262 -0
- package/src/model/rotating-row-transaction.ts +35 -0
- package/src/model/speculative-rotating-kv.ts +101 -0
- package/src/model/ssm-prefill-padding.ts +39 -0
- package/src/model/support.ts +8 -0
- package/src/model/token-groups.ts +66 -0
- package/src/model/trellis-linear.ts +4 -4
- package/src/model/turboquant-codec.ts +89 -0
- package/src/model/universal/dense.ts +14 -13
- 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/preflight.ts +31 -15
- package/src/prompt-cache.ts +306 -86
- package/src/runtime-config.ts +1 -5
- package/src/safetensors.ts +9 -2
- package/src/sampler.ts +37 -25
- package/src/serve/audio-routes.ts +323 -0
- package/src/serve/chat-request.ts +2 -0
- package/src/serve/chat-stage.ts +19 -14
- 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 +88 -38
- package/src/serve/model-binding.ts +7 -1
- package/src/serve/model-host.ts +3 -4
- package/src/serve/preparation.ts +3 -3
- package/src/serve/prompt-response-trace.ts +14 -0
- package/src/serve/request-plan.ts +6 -3
- package/src/serve/request-prep.ts +40 -25
- package/src/serve/text-completion-stage.ts +2 -2
- package/src/serve/transcription-server.ts +64 -0
- package/src/serve/transcription-service.ts +502 -0
- package/src/server.ts +185 -261
- package/src/spec/assistant-rows.ts +93 -0
- package/src/spec/assistant-source.ts +28 -18
- package/src/spec/cache-identity.ts +22 -0
- package/src/spec/deepspec-rows.ts +29 -0
- package/src/spec/deepspec-source.ts +12 -10
- package/src/spec/dflash-source.ts +28 -4
- package/src/spec/drafter.ts +153 -142
- package/src/spec/dspark/deepspec-module.ts +95 -120
- package/src/spec/dspark/module-dflash.ts +126 -170
- package/src/spec/glm52-mtp-graph.ts +29 -0
- package/src/spec/glm52-mtp-rows.ts +132 -0
- package/src/spec/glm52-mtp-source.ts +54 -136
- package/src/spec/glm52-mtp-state.ts +33 -0
- package/src/spec/ngram-source.ts +115 -21
- package/src/spec/projected-draft-rows.ts +73 -0
- package/src/spec/qwen-mtp-module.ts +171 -0
- package/src/spec/qwen-mtp-rows.ts +230 -0
- package/src/spec/qwen-mtp-source.ts +84 -226
- package/src/spec/qwen-mtp-state.ts +30 -0
- package/src/spec/serve-loop.ts +73 -69
- package/src/spec/source.ts +93 -15
- package/src/spec/two-model-rows.ts +62 -0
- package/src/spec/two-model.ts +45 -103
- package/src/ssd-cache.ts +107 -36
- package/src/ssd-durability.ts +29 -27
- 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 +217 -57
- 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/src/backends/mlx/memory-guard.ts +0 -40
- package/src/spec/prefix-state.ts +0 -45
- /package/docs/{planning → archive/planning}/release-notes-v0.3.0.md +0 -0
|
@@ -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
|
|
@@ -25,10 +25,11 @@ export function modelServingBinding(context: ServingContext): ModelServingBindin
|
|
|
25
25
|
const embedding = isEmbeddingModel(model);
|
|
26
26
|
return {
|
|
27
27
|
stateCompatibility: `mlx-${MLX_VERSION}-${deviceArchitecture()}`,
|
|
28
|
-
gateway: bindMlxGateway(model),
|
|
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
|
+
};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import type { Cache } from "../../model/gemma4-base";
|
|
2
|
+
import type { RuntimeModel } from "../../model/factory";
|
|
3
|
+
import type { MlxArray } from "../../mlx/array";
|
|
4
|
+
import { nextPrefillStep } from "../../inference/prefill";
|
|
5
|
+
import type { CacheCodecProvider } from "../../kv-store";
|
|
6
|
+
import { disposeResources, cleanupFailure } from "../../engine/resources";
|
|
7
|
+
import type { Row, RowPromptCache } from "./batch-group";
|
|
8
|
+
import type { KvMaintenance } from "./kv-maintenance";
|
|
9
|
+
import { MlxPrefillRows, type MlxPrefillState } from "./prefill-rows";
|
|
10
|
+
|
|
11
|
+
export interface PrefillState extends MlxPrefillState {
|
|
12
|
+
snapAt: number | null;
|
|
13
|
+
continuation?: import("./continuation").OrdinaryContinuationState;
|
|
14
|
+
closePrefill?: () => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface PrefillHost {
|
|
18
|
+
model: RuntimeModel;
|
|
19
|
+
chunkSize: number;
|
|
20
|
+
tailSplit: boolean;
|
|
21
|
+
promptCache?: RowPromptCache;
|
|
22
|
+
stateCodecs?: CacheCodecProvider;
|
|
23
|
+
maintain?: KvMaintenance;
|
|
24
|
+
forward(ids: MlxArray, caches: Cache[]): Promise<MlxArray>;
|
|
25
|
+
/** Target projection preserves the forward batch geometry before sampling. */
|
|
26
|
+
project(hidden: MlxArray, caches: Cache[], completed: readonly PrefillState[]): MlxArray;
|
|
27
|
+
/** Borrows [1,1,V] logits; takes ownership of the completed request caches. */
|
|
28
|
+
complete(state: PrefillState, logits: MlxArray): Promise<void>;
|
|
29
|
+
resume?(state: PrefillState): Promise<void>;
|
|
30
|
+
reject(row: Row, error: unknown): void;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Ordinary sampling/cache policy over the shared target preparation driver. */
|
|
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); }
|
|
39
|
+
constructor(readonly host: PrefillHost) {
|
|
40
|
+
super({
|
|
41
|
+
stateCodecs: host.stateCodecs, maintain: host.maintain,
|
|
42
|
+
open(row) {
|
|
43
|
+
let owned: { caches: Cache[]; retain?: () => void } | undefined;
|
|
44
|
+
let closeCache: (() => void) | undefined;
|
|
45
|
+
let closePrefill: (() => void) | undefined;
|
|
46
|
+
try {
|
|
47
|
+
closeCache = row.req.trace?.begin("cache.lookup_restore", { mechanism: "continuous" });
|
|
48
|
+
const continuation = row.req.continuation?.restore(row.cacheNamespace ?? "");
|
|
49
|
+
if (continuation) {
|
|
50
|
+
owned = { caches: continuation.caches };
|
|
51
|
+
return { row, solo: continuation.caches, pos: row.req.promptIds.length,
|
|
52
|
+
snapAt: null, continuation };
|
|
53
|
+
}
|
|
54
|
+
const hit = host.promptCache?.take(row.req.promptIds, row.cacheNamespace, row.req.cacheSessionId) ?? null;
|
|
55
|
+
if (hit) owned = hit;
|
|
56
|
+
closeCache?.(); closeCache = undefined;
|
|
57
|
+
if (hit) row.cachedTokens = hit.tokens.length;
|
|
58
|
+
const length = row.req.promptIds.length;
|
|
59
|
+
const boundary = Math.min(row.req.snapshotAt ?? length, length - 1);
|
|
60
|
+
const snapAt = host.promptCache && boundary >= 256 && boundary > (hit?.tokens.length ?? 0) ? boundary : null;
|
|
61
|
+
closePrefill = row.req.trace?.begin("prefill.total", {
|
|
62
|
+
mechanism: "continuous", promptTokens: row.promptTokens, cachedTokens: row.cachedTokens,
|
|
63
|
+
});
|
|
64
|
+
const closeSetup = row.req.trace?.begin("prefill.batch_setup", { mechanism: "continuous" });
|
|
65
|
+
owned ??= { caches: row.req.statePolicy?.create() ?? host.model.makeCache() }; closeSetup?.();
|
|
66
|
+
if (row.req.promptInput) host.maintain?.(owned.caches);
|
|
67
|
+
return { row, solo: owned.caches, pos: hit?.tokens.length ?? 0, retain: owned.retain, snapAt, closePrefill };
|
|
68
|
+
} catch (error) {
|
|
69
|
+
return cleanupFailure(error, () => disposeResources([...(owned?.caches ?? []),
|
|
70
|
+
{ dispose: () => owned?.retain?.() }, { dispose: () => closePrefill?.() }]));
|
|
71
|
+
} finally { closeCache?.(); }
|
|
72
|
+
},
|
|
73
|
+
ready: state => !!state.continuation,
|
|
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,
|
|
78
|
+
chunkSize: state.row.req.prefillChunkSize ?? host.chunkSize,
|
|
79
|
+
tailSplit: host.tailSplit, snapshotAt: state.snapAt }); },
|
|
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),
|
|
84
|
+
complete: (state, logits) => state.continuation ? host.resume!(state) : host.complete(state, logits!), reject: host.reject.bind(host),
|
|
85
|
+
checkpoint(state, capture) {
|
|
86
|
+
let snapshot: Cache[] | undefined;
|
|
87
|
+
try {
|
|
88
|
+
snapshot = capture();
|
|
89
|
+
host.promptCache!.put(state.row.req.promptIds.slice(0, state.pos), snapshot, state.row.cacheNamespace, undefined, undefined, state.row.req.cacheSessionId);
|
|
90
|
+
snapshot = undefined;
|
|
91
|
+
} catch (error) {
|
|
92
|
+
if (snapshot) disposeResources(snapshot);
|
|
93
|
+
console.warn(`batch-lane boundary snapshot skipped: ${(error as Error).message}`);
|
|
94
|
+
}
|
|
95
|
+
state.snapAt = null;
|
|
96
|
+
},
|
|
97
|
+
close(state) { const close = state.closePrefill; state.closePrefill = undefined; close?.(); },
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import type { BatchableCache, Cache } from "../../model/gemma4-base";
|
|
2
|
+
import type { MlxArray } from "../../mlx/array";
|
|
3
|
+
import * as ops from "../../mlx/ops";
|
|
4
|
+
import { clearCache } from "../../mlx/ffi";
|
|
5
|
+
import type { PrefillStep } from "../../inference/prefill";
|
|
6
|
+
import { cloneKvCaches, type CacheCodecProvider } from "../../kv-store";
|
|
7
|
+
import { disposeResources, cleanupFailure } from "../../engine/resources";
|
|
8
|
+
import type { MlxGroupPreparation, Row } from "./batch-group";
|
|
9
|
+
import { evalCacheState } from "./prefill";
|
|
10
|
+
import { prefillCacheLayout } from "./cache-layout";
|
|
11
|
+
import { MlxStateRows } from "./state-rows";
|
|
12
|
+
import type { P2RTracePhase, P2RTraceAttributes } from "../../serve/prompt-response-trace";
|
|
13
|
+
import type { MlxForwardWork, MlxPreparationWork } from "./mixed-iteration";
|
|
14
|
+
import type { KvMaintenance } from "./kv-maintenance";
|
|
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
|
+
|
|
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;
|
|
29
|
+
/** Some methods maintain their final target chunk before sampling. */
|
|
30
|
+
readonly maintain?: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface MlxPrefillState {
|
|
34
|
+
row: Row;
|
|
35
|
+
solo: Cache[];
|
|
36
|
+
pos: number;
|
|
37
|
+
retain?: () => void;
|
|
38
|
+
planned?: MlxPrefillStep;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** The method owns initialization, planning, companion state, snapshots and
|
|
42
|
+
* completion. This driver owns common target batching and work boundaries.
|
|
43
|
+
* Checkpoint factories return owned cache state; close/dispose are idempotent. */
|
|
44
|
+
export interface MlxPrefillRowsHost<State extends MlxPrefillState> {
|
|
45
|
+
stateCodecs?: CacheCodecProvider;
|
|
46
|
+
maintain?: KvMaintenance;
|
|
47
|
+
open(row: Row): State;
|
|
48
|
+
plan(state: State): MlxPrefillStep;
|
|
49
|
+
/** Already prepared state can complete without any target forward. */
|
|
50
|
+
ready?(state: State): boolean;
|
|
51
|
+
forward(ids: MlxArray, caches: Cache[], states: readonly State[], work?: MlxForwardWork): Promise<MlxArray>;
|
|
52
|
+
project(hidden: MlxArray, caches: Cache[], completed: readonly State[]): MlxArray | null;
|
|
53
|
+
/** Consume method context after target maintenance, before snapshots. */
|
|
54
|
+
afterForward?(ids: MlxArray, caches: Cache[], states: readonly State[], hidden: MlxArray): void | Promise<void>;
|
|
55
|
+
checkpoint(state: State, capture: () => Cache[], row: number): void;
|
|
56
|
+
complete(state: State, logits: MlxArray | null, row: number): Promise<void>;
|
|
57
|
+
reject(row: Row, error: unknown): void;
|
|
58
|
+
close(state: State): void;
|
|
59
|
+
filterRows?(keep: readonly number[]): void;
|
|
60
|
+
dispose?(): void;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** One target preparation lifecycle at B1/B>1. Late joins retain each row's
|
|
64
|
+
* planned chunk endpoint, precision transition and checkpoint boundary. */
|
|
65
|
+
export class MlxPrefillRows<State extends MlxPrefillState> implements MlxGroupPreparation {
|
|
66
|
+
#states: State[] = [];
|
|
67
|
+
#rows: Row[] = [];
|
|
68
|
+
#stateRows?: MlxStateRows;
|
|
69
|
+
constructor(readonly operations: MlxPrefillRowsHost<State>) {}
|
|
70
|
+
get rows(): readonly Row[] { return this.#rows; }
|
|
71
|
+
get canAdmit(): boolean { return this.#rows.length > 0; }
|
|
72
|
+
/** Keep a request's prefill weight until retirement so a nearly completed
|
|
73
|
+
* request does not repeatedly attract another long prefill into its tail. */
|
|
74
|
+
get tokenWeight(): number { return this.#states.reduce((sum, state) => sum + state.row.promptTokens - state.row.cachedTokens, 0); }
|
|
75
|
+
|
|
76
|
+
admit(row: Row): void {
|
|
77
|
+
this.#states.push(this.operations.open(row));
|
|
78
|
+
this.#rows.push(row);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
#release(state: State): void {
|
|
82
|
+
const caches = state.solo; state.solo = [];
|
|
83
|
+
const retain = state.retain; state.retain = undefined;
|
|
84
|
+
disposeResources([...caches, { dispose: () => retain?.() }, { dispose: () => this.operations.close(state) }]);
|
|
85
|
+
}
|
|
86
|
+
#filter(keep: number[]): void {
|
|
87
|
+
if (keep.length) this.#stateRows?.filterRows(keep);
|
|
88
|
+
else { const rows = this.#stateRows; this.#stateRows = undefined; rows?.dispose(); }
|
|
89
|
+
this.operations.filterRows?.(keep);
|
|
90
|
+
this.#states = keep.map(row => this.#states[row]!);
|
|
91
|
+
this.#rows = keep.map(row => this.#rows[row]!);
|
|
92
|
+
}
|
|
93
|
+
/** Admission changes membership only between forwards. Existing planned
|
|
94
|
+
* endpoints survive a join, including partially consumed drain chunks. */
|
|
95
|
+
#synchronizeRows(): void {
|
|
96
|
+
if (!this.#stateRows && this.#states.length === 1) return;
|
|
97
|
+
if (!this.#stateRows) {
|
|
98
|
+
for (const state of this.#states) this.operations.maintain?.preparePrefill?.(state.solo);
|
|
99
|
+
const layouts: BatchableCache[] = [];
|
|
100
|
+
try { for (const cache of this.#states[0]!.solo) layouts.push(prefillCacheLayout(cache)); }
|
|
101
|
+
catch (error) { return cleanupFailure(error, () => disposeResources(layouts)); }
|
|
102
|
+
this.#stateRows = new MlxStateRows(layouts);
|
|
103
|
+
this.#stateRows.mergeRows(this.#states.map(state => state.solo));
|
|
104
|
+
} else {
|
|
105
|
+
for (const state of this.#states.slice(this.#stateRows.rowCount)) {
|
|
106
|
+
this.operations.maintain?.preparePrefill?.(state.solo);
|
|
107
|
+
this.#stateRows.append(state.solo);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
// Resolve input-backed copies before releasing SSD restore leases.
|
|
111
|
+
if (this.#states.some(state => state.solo.length)) {
|
|
112
|
+
evalCacheState(this.#stateRows.caches);
|
|
113
|
+
for (const state of this.#states) {
|
|
114
|
+
const caches = state.solo; state.solo = [];
|
|
115
|
+
const retain = state.retain; state.retain = undefined;
|
|
116
|
+
disposeResources([...caches, { dispose: () => retain?.() }]);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
for (const cache of this.#stateRows.caches) cache.prefillMaintenance?.beginPrefill();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async advance(workLimit?: MlxPreparationWork): Promise<boolean> {
|
|
123
|
+
let remaining = workLimit?.maxTokens ?? Infinity;
|
|
124
|
+
// Complete restored inputs before merging: their cache coverage must not
|
|
125
|
+
// introduce padding or a zero-width forward into unrelated cold rows.
|
|
126
|
+
if (this.operations.ready && this.#states.some(state => this.operations.ready!(state))) {
|
|
127
|
+
const remaining: State[] = [];
|
|
128
|
+
for (const state of this.#states) {
|
|
129
|
+
if (!this.operations.ready(state)) { remaining.push(state); continue; }
|
|
130
|
+
try {
|
|
131
|
+
state.row.req.signal?.throwIfAborted();
|
|
132
|
+
await this.operations.complete(state, null, -1);
|
|
133
|
+
state.solo = []; state.retain = undefined;
|
|
134
|
+
} catch (error) { this.operations.reject(state.row, error); this.#release(state); }
|
|
135
|
+
finally { this.operations.close(state); }
|
|
136
|
+
}
|
|
137
|
+
this.#states = remaining; this.#rows = remaining.map(state => state.row);
|
|
138
|
+
if (!remaining.length) { this.dispose(); return true; }
|
|
139
|
+
}
|
|
140
|
+
{
|
|
141
|
+
using span = traceRows(this.#states, "prefill.row_sync", { workId: ++nextWorkId, batchSize: this.#states.length });
|
|
142
|
+
this.#synchronizeRows();
|
|
143
|
+
}
|
|
144
|
+
const keep: number[] = [];
|
|
145
|
+
for (const [index, state] of this.#states.entries()) {
|
|
146
|
+
if (!state.row.req.signal?.aborted) { keep.push(index); continue; }
|
|
147
|
+
this.#release(state); this.operations.reject(state.row, state.row.req.signal.reason);
|
|
148
|
+
}
|
|
149
|
+
if (keep.length !== this.#states.length) this.#filter(keep);
|
|
150
|
+
if (!this.#states.length) { this.dispose(); return true; }
|
|
151
|
+
while (this.#states.length) {
|
|
152
|
+
for (const state of this.#states) state.planned ??= this.operations.plan(state);
|
|
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 };
|
|
157
|
+
const closes = this.#states.map(state => state.row.req.trace?.begin("prefill.chunk", {
|
|
158
|
+
mechanism: "continuous", startToken: state.pos, ...work,
|
|
159
|
+
}));
|
|
160
|
+
const caches = this.#stateRows?.caches ?? this.#states[0]!.solo;
|
|
161
|
+
try {
|
|
162
|
+
using ids = count ? ops.fromInt32(this.#states.flatMap(state => state.row.req.promptIds.slice(state.pos, state.pos + count)),
|
|
163
|
+
[this.#states.length, count]) : null;
|
|
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;
|
|
170
|
+
const drains: number[] = [], finals: number[] = [];
|
|
171
|
+
let batchYield = false;
|
|
172
|
+
for (const [row, state] of this.#states.entries()) {
|
|
173
|
+
state.pos += count;
|
|
174
|
+
if (state.pos !== state.planned!.end) continue;
|
|
175
|
+
if (state.planned!.kind === "final") finals.push(row);
|
|
176
|
+
else drains.push(row);
|
|
177
|
+
batchYield ||= state.planned!.batchYield;
|
|
178
|
+
}
|
|
179
|
+
const maintained = [...drains, ...finals.filter(row => this.#states[row]!.planned!.maintain)];
|
|
180
|
+
if (count && (maintained.length || !finals.length)) {
|
|
181
|
+
{
|
|
182
|
+
using span = traceRows(this.#states, "prefill.evaluate", work);
|
|
183
|
+
evalCacheState(caches);
|
|
184
|
+
}
|
|
185
|
+
using span = traceRows(this.#states, "prefill.kv_maintenance", work);
|
|
186
|
+
if (this.#stateRows) {
|
|
187
|
+
for (const cache of caches) cache.prefillMaintenance?.commitPrefill(maintained);
|
|
188
|
+
} else if (maintained.length) this.operations.maintain?.(this.#states[0]!.solo);
|
|
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();
|
|
193
|
+
}
|
|
194
|
+
if (ids && this.operations.afterForward) {
|
|
195
|
+
using span = traceRows(this.#states, "prefill.companion", work);
|
|
196
|
+
const companion = this.operations.afterForward?.(ids, caches, this.#states, hidden!);
|
|
197
|
+
if (companion) await companion;
|
|
198
|
+
}
|
|
199
|
+
for (const row of [...drains, ...finals]) {
|
|
200
|
+
const state = this.#states[row]!;
|
|
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
|
+
}
|
|
206
|
+
if (!finals.includes(row)) state.planned = undefined;
|
|
207
|
+
}
|
|
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;
|
|
214
|
+
for (const row of finals) {
|
|
215
|
+
const state = this.#states[row]!;
|
|
216
|
+
using span = traceRows([state], "prefill.complete", { ...work, row });
|
|
217
|
+
if (this.#stateRows) state.solo = this.#stateRows.extractRow(row);
|
|
218
|
+
using part = logits ? logits.slice([row, logits.shape[1]! - 1, 0], [row + 1, logits.shape[1]!, logits.shape[2]!]) : null;
|
|
219
|
+
try {
|
|
220
|
+
state.row.req.signal?.throwIfAborted();
|
|
221
|
+
await this.operations.complete(state, part, row);
|
|
222
|
+
state.solo = []; state.retain = undefined;
|
|
223
|
+
} catch (error) {
|
|
224
|
+
this.operations.reject(state.row, error);
|
|
225
|
+
this.#release(state);
|
|
226
|
+
} finally { this.operations.close(state); }
|
|
227
|
+
}
|
|
228
|
+
for (const cache of caches) (cache as { releaseRopeArr?: () => void }).releaseRopeArr?.();
|
|
229
|
+
if (finals.length) this.#filter(this.#states.flatMap((_, row) => finals.includes(row) ? [] : [row]));
|
|
230
|
+
if (!this.#states.length) { this.dispose(); return true; }
|
|
231
|
+
remaining -= count * work.batchSize;
|
|
232
|
+
if (batchYield || finals.length || remaining < this.#states.length) return false;
|
|
233
|
+
} finally { for (const close of closes) close?.(); }
|
|
234
|
+
}
|
|
235
|
+
return true;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
dispose(): void {
|
|
239
|
+
const states = this.#states; this.#states = []; this.#rows = [];
|
|
240
|
+
const rows = this.#stateRows; this.#stateRows = undefined;
|
|
241
|
+
disposeResources([...(rows ? [rows] : []), ...states.map(state => ({ dispose: () => this.#release(state) })),
|
|
242
|
+
{ dispose: () => this.operations.dispose?.() }]);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
@@ -9,7 +9,7 @@ import { withResource } from "../../engine/resources";
|
|
|
9
9
|
/** State views with explicit ownership are released even on deferred errors.
|
|
10
10
|
* Owned views precede borrowed state, preserving the serial eval ordering. */
|
|
11
11
|
export function evalCacheState(cache: Cache[]): void {
|
|
12
|
-
withResource(leaseCacheStates(cache), (state) => ops.evalAll([...state]));
|
|
12
|
+
withResource(leaseCacheStates(cache), (state) => { if (state.length) ops.evalAll([...state]); });
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
/** Forward once. Drains evaluate state, apply KV maintenance, then clear the
|