mlx-bun 0.0.10 → 0.0.11
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 +66 -21
- package/THIRD_PARTY_LICENSES.md +1 -0
- package/docs/archive/mlx-bun-lab-report.html +275 -0
- package/docs/archive/wwdc-mlx-bun.html +848 -0
- package/docs/design/audio-input-plan.md +356 -0
- package/docs/design/batching-perf-path.md +58 -35
- package/docs/design/batching-v2-plan.md +23 -9
- package/docs/design/bucketing-stage.md +2 -1
- package/docs/design/compat-cli-surface-design.md +4 -3
- package/docs/design/decode-speed-program.md +172 -0
- package/docs/design/docs-reorg-plan.md +1 -1
- package/docs/design/dspark-serving-program.md +371 -0
- package/docs/design/dspark-speculative-decoding.md +167 -5
- package/docs/design/faithful-l1-consolidation.md +310 -0
- package/docs/design/generic-model-support.md +8 -5
- package/docs/design/grammar-spec-batching-integration.md +301 -0
- package/docs/design/minicpm5-decode-megakernel.md +16 -11
- package/docs/design/mlx-lm-tool-parity-plan.md +11 -1
- package/docs/design/omlx-adoption-map.md +14 -6
- package/docs/design/orpo-training.md +34 -30
- package/docs/design/paged-kv-cache.md +214 -0
- package/docs/design/parallel-slots.md +82 -49
- package/docs/design/parity-tier-dag.md +43 -33
- package/docs/design/repo-cleanup-plan.md +189 -0
- package/docs/design/runtime-isolation.md +19 -3
- package/docs/design/segmented-backward-training.md +3 -2
- package/docs/design/ssd-kv-cold-tier.md +130 -0
- package/docs/design/structured-output.md +484 -0
- package/docs/design/the-dreaming-master-plan.md +2 -3
- package/docs/design/turboquant-kv.md +235 -0
- package/docs/design/unified-engine-frontier-plan.md +774 -0
- package/docs/design/web-chat-beat-matrix.md +315 -0
- package/docs/design/web-chat-redesign.md +1112 -0
- package/docs/investigations/dspark-handoff.md +182 -16
- package/docs/investigations/lab-build-journal.md +2 -2
- package/docs/investigations/openwiki-evaluation.md +249 -0
- package/docs/planning/IDEAS.md +14 -0
- package/docs/planning/PRODUCT_ROADMAP.md +2 -3
- package/docs/planning/chat-ui-vision-memory-first.md +199 -0
- package/docs/planning/chat-ui-vision.md +50 -0
- package/docs/planning/memory-docs-and-dag-plan.md +15 -6
- package/docs/planning/release-notes-v0.0.11.md +239 -0
- package/docs/reference/cli.md +35 -16
- package/docs/reference/distribution.md +13 -6
- package/docs/reference/embedding.md +35 -14
- package/docs/reference/features-matrix.md +97 -0
- package/docs/reference/library-api.md +30 -9
- package/docs/reference/memory.md +26 -6
- package/docs/reference/models.md +11 -1
- package/docs/reference/orpo-quickstart.md +3 -4
- package/docs/reference/server-api.md +634 -22
- package/docs/reference/server-config.md +250 -121
- package/docs/reference/training.md +14 -15
- package/package.json +5 -3
- package/src/anthropic.ts +13 -0
- package/src/audio/conformer.ts +647 -0
- package/src/audio/decode.ts +133 -0
- package/src/audio/features.ts +202 -0
- package/src/audio/transcode.ts +67 -0
- package/src/cli.ts +356 -105
- package/src/config.ts +49 -2
- package/src/download.ts +40 -0
- package/src/eval/README.md +7 -6
- package/src/eval/kl.ts +59 -3
- package/src/eval/quality-db.ts +2 -2
- package/src/eval/runner.ts +3 -3
- package/src/expert-offload.ts +15 -2
- package/src/flags.ts +13 -0
- package/src/generate.ts +330 -24
- package/src/grammar.ts +585 -0
- package/src/hub-rest.ts +242 -0
- package/src/kv-store.ts +442 -88
- package/src/lora.ts +8 -0
- package/src/md-modules.d.ts +42 -0
- package/src/media-fetch.ts +284 -0
- package/src/memory/rest.ts +245 -0
- package/src/memory/vault.ts +74 -0
- package/src/mlx/array.ts +106 -19
- package/src/mlx/autograd.ts +2 -2
- package/src/mlx/ffi.ts +27 -3
- package/src/mlx/metal-kernel.ts +7 -4
- package/src/mlx/ops.ts +153 -23
- package/src/mlx/turboquant-ops.ts +831 -0
- package/src/mlx/turboquant-tables.ts +237 -0
- package/src/model/batched-mask.ts +62 -1
- package/src/model/batched-quant.ts +206 -0
- package/src/model/batched-rotating-quant.ts +346 -0
- package/src/model/batched-rotating.ts +46 -1
- package/src/model/compiled-decode.ts +4 -15
- package/src/model/factory.ts +10 -4
- package/src/model/gemma4-base.ts +363 -39
- package/src/model/gemma4.ts +122 -33
- package/src/model/generated/gemma4-12b.ts +9 -13
- package/src/model/generated/gemma4-26b.ts +10 -5
- package/src/model/generated/gemma4-e4b.ts +14 -9
- package/src/model/minicpm5.ts +58 -102
- package/src/model/paged-kv.ts +253 -0
- package/src/model/qwen3-delta.ts +76 -15
- package/src/model/qwen3-moe.ts +404 -0
- package/src/model/qwen3.ts +9 -8
- package/src/model/qwen3_5.ts +88 -23
- package/src/model/support.ts +12 -1
- package/src/model/universal/dense.ts +40 -5
- package/src/model/universal/rope.ts +36 -21
- package/src/pi-web.ts +1299 -82
- package/src/preflight.ts +27 -8
- package/src/prompt-cache.ts +293 -33
- package/src/quantize/quantizer.ts +16 -1
- package/src/registry.ts +57 -2
- package/src/serve/batch-scheduler.ts +965 -72
- package/src/serve/generation-gateway.ts +214 -11
- package/src/serve/isolate.ts +417 -0
- package/src/serve/lane-registry.ts +44 -0
- package/src/serve/session-search.ts +326 -0
- package/src/server.ts +1380 -135
- package/src/spec/assistant-source.ts +150 -0
- package/src/spec/deepspec-source.ts +143 -0
- package/src/spec/dflash-source.ts +124 -0
- package/src/spec/dspark/ab-stats.ts +136 -0
- package/src/spec/dspark/calibration.ts +112 -0
- package/src/spec/dspark/deepspec-module.ts +801 -0
- package/src/spec/dspark/generate-dflash.ts +72 -20
- package/src/spec/dspark/loader.ts +23 -0
- package/src/spec/dspark/module-dflash.ts +278 -36
- package/src/spec/dspark/quantize-drafter.ts +91 -0
- package/src/spec/generate.ts +59 -25
- package/src/spec/ngram-source.ts +132 -0
- package/src/spec/serve-loop.ts +536 -0
- package/src/spec/source.ts +93 -0
- package/src/spec/two-model.ts +171 -0
- package/src/ssd-cache.ts +107 -35
- package/src/tokenizer.ts +288 -2
- package/src/tool-approvals.ts +131 -0
- package/src/tool-call.ts +408 -20
- package/src/train/flash-cce.ts +2 -2
- package/src/train/job.ts +0 -2
- package/src/train/trainer.ts +1 -4
- package/src/vision/prompt.ts +217 -48
- package/src/web/app.html +1184 -1789
- package/src/web/app.js +5667 -0
- package/src/web/icon.svg +13 -0
- package/src/web/manifest.webmanifest +24 -0
- package/src/web/src/adapters-panel.ts +288 -0
- package/src/web/src/api.ts +57 -0
- package/src/web/src/assistant.ts +485 -0
- package/src/web/src/chat.ts +1050 -0
- package/src/web/src/composer.ts +959 -0
- package/src/web/src/dataset.ts +132 -0
- package/src/web/src/finetune.ts +267 -0
- package/src/web/src/hub.ts +428 -0
- package/src/web/src/main.ts +98 -0
- package/src/web/src/markdown.ts +0 -0
- package/src/web/src/memory-panel.ts +572 -0
- package/src/web/src/model-picker.ts +159 -0
- package/src/web/src/palette.ts +346 -0
- package/src/web/src/protocol.ts +62 -0
- package/src/web/src/quantize.ts +167 -0
- package/src/web/src/rag.ts +274 -0
- package/src/web/src/sessions.ts +469 -0
- package/src/web/src/shell.ts +804 -0
- package/src/web/src/status.ts +252 -0
- package/src/web/src/ui-catalog.ts +92 -0
- package/src/web/sw.js +71 -0
- package/src/web/vendor/README.md +61 -0
- package/src/web/vendor/hljs-LICENSE +29 -0
- package/src/web/vendor/hljs-theme.css +59 -0
- package/src/web/vendor/hljs.js +26 -0
- package/src/model/fused-decode-kernel.ts +0 -312
- package/src/model/fused-geglu-kernel.ts +0 -198
- package/src/model/fused-mlp-kernel.ts +0 -169
- package/src/model/fused-swiglu-kernel.ts +0 -240
- package/src/model/steel-linear-kernel.ts +0 -96
- package/src/status-page.html +0 -497
package/src/vision/prompt.ts
CHANGED
|
@@ -1,10 +1,21 @@
|
|
|
1
|
-
// Multimodal prompt assembly: OpenAI
|
|
2
|
-
// token ids + merged input embeddings +
|
|
1
|
+
// Multimodal prompt assembly: OpenAI image/audio content parts → spliced
|
|
2
|
+
// token ids + merged input embeddings + attention/zeroing masks.
|
|
3
3
|
//
|
|
4
|
-
// The chat template renders one <|image|> token (258880) per image part
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
4
|
+
// The chat template renders one <|image|> token (258880) per image part and
|
|
5
|
+
// one <|audio|> token (258881) per audio part; we expand each in document
|
|
6
|
+
// order — <boi> + image_token × softTokens + <eoi> (matching optiq's vision
|
|
7
|
+
// frontend) and <boa> + audio_token × n + <eoa> with
|
|
8
|
+
// n = min(ceil(duration_ms/40), 750) (processing_gemma4.py) — embed the full
|
|
9
|
+
// sequence, then overwrite the soft-token rows with the towers' features.
|
|
10
|
+
//
|
|
11
|
+
// Attention semantics (audio-input-plan.md §3.3, resolved vs the oracle):
|
|
12
|
+
// - image runs attend bidirectionally among themselves (bidirMask), BUT
|
|
13
|
+
// any audio token in the prompt disables the vision overlay entirely —
|
|
14
|
+
// mixed image+audio prompts run fully causal (bidirMask = null);
|
|
15
|
+
// - audio runs are strictly causal, always;
|
|
16
|
+
// - per-layer-input id zeroing (e2b/e4b) applies to the UNION of
|
|
17
|
+
// multimodal soft tokens (image | audio) — multimodalMask, decoupled
|
|
18
|
+
// from the bidirectional mask.
|
|
8
19
|
|
|
9
20
|
import { MlxArray } from "../mlx/array";
|
|
10
21
|
import { Dtype } from "../mlx/ffi";
|
|
@@ -12,6 +23,10 @@ import * as ops from "../mlx/ops";
|
|
|
12
23
|
import type { Gemma4Model } from "../model/gemma4";
|
|
13
24
|
import type { ChatTemplate, ChatMessage, ToolDefinition } from "../chat-template";
|
|
14
25
|
import type { LoadedTokenizer } from "../tokenizer";
|
|
26
|
+
import type { AudioTower } from "../audio/conformer";
|
|
27
|
+
import { audioSoftTokenCount, decodeAudio } from "../audio/decode";
|
|
28
|
+
import { extractMelFeatures } from "../audio/features";
|
|
29
|
+
import { fetchMediaBytes } from "../media-fetch";
|
|
15
30
|
|
|
16
31
|
/** Common contract for both vision towers (encoder-free gemma4_unified in
|
|
17
32
|
* ./embedder.ts and the SigLIP encoder in ./siglip.ts): preprocess image
|
|
@@ -33,12 +48,38 @@ export interface VisionPrompt {
|
|
|
33
48
|
imageMask: MlxArray;
|
|
34
49
|
}
|
|
35
50
|
|
|
51
|
+
export interface MultimodalPrompt {
|
|
52
|
+
ids: number[];
|
|
53
|
+
/** Unscaled merged embeddings [1, L, hidden] — caller disposes. */
|
|
54
|
+
embeddings: MlxArray;
|
|
55
|
+
/** bool [L] image-token mask for the bidirectional attention overlay.
|
|
56
|
+
* null when the prompt contains ANY audio (§3.3 Q1: mixed image+audio
|
|
57
|
+
* runs fully causal) or no images. Caller disposes when non-null. */
|
|
58
|
+
bidirMask: MlxArray | null;
|
|
59
|
+
/** bool [L] union multimodal soft-token mask (image | audio) for
|
|
60
|
+
* per-layer-input id zeroing (§3.3 Q2). Caller disposes. */
|
|
61
|
+
multimodalMask: MlxArray;
|
|
62
|
+
}
|
|
63
|
+
|
|
36
64
|
export interface VisionTokenIds {
|
|
37
65
|
imageTokenId: number;
|
|
38
66
|
boiTokenId: number;
|
|
39
67
|
eoiTokenId: number;
|
|
40
68
|
}
|
|
41
69
|
|
|
70
|
+
export interface AudioTokenIds {
|
|
71
|
+
audioTokenId: number;
|
|
72
|
+
boaTokenId: number;
|
|
73
|
+
eoaTokenId: number;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** The towers a multimodal prompt may need; pass only what the request uses
|
|
77
|
+
* (vision-only and audio-only fall out as special cases). */
|
|
78
|
+
export interface MultimodalTowers<P extends { softTokens: number }> {
|
|
79
|
+
vision?: { tower: VisionEncoder<P>; tokenIds: VisionTokenIds };
|
|
80
|
+
audio?: { tower: AudioTower; tokenIds: AudioTokenIds };
|
|
81
|
+
}
|
|
82
|
+
|
|
42
83
|
/** Extract image bytes from OpenAI-style content parts, rewriting the
|
|
43
84
|
* parts to the template's {type:"image"} form. */
|
|
44
85
|
export async function extractImages(
|
|
@@ -57,7 +98,7 @@ export async function extractImages(
|
|
|
57
98
|
const url = (part.image_url as { url: string } | undefined)?.url
|
|
58
99
|
?? (part.image_url as unknown as string);
|
|
59
100
|
if (typeof url !== "string") throw new Error("image_url part missing url");
|
|
60
|
-
images.push(await
|
|
101
|
+
images.push(await fetchMediaBytes(url, "image"));
|
|
61
102
|
parts.push({ type: "image" });
|
|
62
103
|
} else if (part.type === "image") {
|
|
63
104
|
// already template-form: expect base64 `data` or pass-through marker
|
|
@@ -76,83 +117,211 @@ export async function extractImages(
|
|
|
76
117
|
return { messages: out, images };
|
|
77
118
|
}
|
|
78
119
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
120
|
+
/** Extract audio bytes from OpenAI-style content parts, rewriting the parts
|
|
121
|
+
* to the template's {type:"audio"} form. Accepts the OpenAI-canonical
|
|
122
|
+
* {type:"input_audio", input_audio:{data:<b64>, format:...}} plus optiq's
|
|
123
|
+
* {type:"audio"} (base64 `data`) and {type:"audio_url"} aliases. */
|
|
124
|
+
export async function extractAudio(
|
|
125
|
+
messages: ChatMessage[],
|
|
126
|
+
): Promise<{ messages: ChatMessage[]; audio: Uint8Array[] }> {
|
|
127
|
+
const audio: Uint8Array[] = [];
|
|
128
|
+
const out: ChatMessage[] = [];
|
|
129
|
+
for (const m of messages) {
|
|
130
|
+
if (!Array.isArray(m.content)) {
|
|
131
|
+
out.push(m);
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
const parts: Array<Record<string, unknown>> = [];
|
|
135
|
+
for (const part of m.content) {
|
|
136
|
+
if (part.type === "input_audio") {
|
|
137
|
+
const data = (part.input_audio as { data?: string } | undefined)?.data;
|
|
138
|
+
if (typeof data !== "string")
|
|
139
|
+
throw new Error("input_audio part requires base64 `input_audio.data`");
|
|
140
|
+
audio.push(Uint8Array.from(Buffer.from(data, "base64")));
|
|
141
|
+
parts.push({ type: "audio" });
|
|
142
|
+
} else if (part.type === "audio") {
|
|
143
|
+
if (typeof part.data === "string") {
|
|
144
|
+
audio.push(Uint8Array.from(Buffer.from(part.data, "base64")));
|
|
145
|
+
parts.push({ type: "audio" });
|
|
146
|
+
} else {
|
|
147
|
+
throw new Error("audio part requires base64 `data`");
|
|
148
|
+
}
|
|
149
|
+
} else if (part.type === "audio_url") {
|
|
150
|
+
const url = (part.audio_url as { url: string } | undefined)?.url
|
|
151
|
+
?? (part.audio_url as unknown as string);
|
|
152
|
+
if (typeof url !== "string") throw new Error("audio_url part missing url");
|
|
153
|
+
audio.push(await fetchMediaBytes(url, "audio"));
|
|
154
|
+
parts.push({ type: "audio" });
|
|
155
|
+
} else {
|
|
156
|
+
parts.push(part);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
out.push({ ...m, content: parts });
|
|
92
160
|
}
|
|
93
|
-
|
|
161
|
+
return { messages: out, audio };
|
|
94
162
|
}
|
|
95
163
|
|
|
96
|
-
|
|
164
|
+
/** Decoded + featurized audio clip, ready to splice. */
|
|
165
|
+
interface PreparedAudio {
|
|
166
|
+
mel: Float32Array;
|
|
167
|
+
frames: number;
|
|
168
|
+
softTokens: number;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export async function buildMultimodalPrompt<P extends { softTokens: number }>(
|
|
97
172
|
model: Gemma4Model,
|
|
98
|
-
|
|
173
|
+
towers: MultimodalTowers<P>,
|
|
99
174
|
tokenizer: LoadedTokenizer,
|
|
100
175
|
template: ChatTemplate,
|
|
101
176
|
messages: ChatMessage[],
|
|
102
177
|
images: Uint8Array[],
|
|
103
|
-
|
|
178
|
+
audio: Uint8Array[],
|
|
104
179
|
tools: ToolDefinition[] | null = null,
|
|
105
|
-
): Promise<
|
|
180
|
+
): Promise<MultimodalPrompt> {
|
|
181
|
+
if (images.length > 0 && !towers.vision)
|
|
182
|
+
throw new Error("images present but no vision tower configured");
|
|
183
|
+
if (audio.length > 0 && !towers.audio)
|
|
184
|
+
throw new Error("audio present but no audio tower configured");
|
|
185
|
+
|
|
106
186
|
const rendered = template.render(messages, { tools });
|
|
107
187
|
let ids = tokenizer.encode(rendered);
|
|
108
188
|
if (ids[0] === ids[1] && ids[0] === tokenizer.bosTokenId) ids = ids.slice(1);
|
|
109
189
|
|
|
110
|
-
// preprocess
|
|
111
|
-
//
|
|
112
|
-
const pre = await Promise.all(
|
|
190
|
+
// preprocess every clip/image up front (splice lengths depend on them);
|
|
191
|
+
// audio soft-token counts come from the DECODED samples, never metadata.
|
|
192
|
+
const pre = await Promise.all(
|
|
193
|
+
images.map((bytes) => towers.vision!.tower.preprocess(bytes)),
|
|
194
|
+
);
|
|
195
|
+
const clips: PreparedAudio[] = audio.map((bytes) => {
|
|
196
|
+
const samples = decodeAudio(bytes);
|
|
197
|
+
const softTokens = audioSoftTokenCount(samples.length);
|
|
198
|
+
const m = extractMelFeatures(samples);
|
|
199
|
+
return { mel: m.features, frames: m.frames, softTokens };
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
const vIds = towers.vision?.tokenIds;
|
|
203
|
+
const aIds = towers.audio?.tokenIds;
|
|
113
204
|
|
|
114
|
-
// expand each <|image|> into <boi> + image_token×soft + <eoi>
|
|
205
|
+
// expand each <|image|> into <boi> + image_token×soft + <eoi> and each
|
|
206
|
+
// <|audio|> into <boa> + audio_token×n + <eoa>, in document order
|
|
115
207
|
const spliced: number[] = [];
|
|
116
|
-
const runs: { start: number; length: number }[] = [];
|
|
208
|
+
const runs: { start: number; length: number; kind: "image" | "audio"; index: number }[] = [];
|
|
117
209
|
let imgIdx = 0;
|
|
210
|
+
let audIdx = 0;
|
|
118
211
|
for (const id of ids) {
|
|
119
|
-
if (id ===
|
|
212
|
+
if (vIds && id === vIds.imageTokenId) {
|
|
120
213
|
const p = pre[imgIdx];
|
|
121
214
|
if (!p) throw new Error("more <|image|> markers than images");
|
|
122
|
-
spliced.push(
|
|
123
|
-
runs.push({ start: spliced.length, length: p.softTokens });
|
|
124
|
-
for (let k = 0; k < p.softTokens; k++) spliced.push(
|
|
125
|
-
spliced.push(
|
|
215
|
+
spliced.push(vIds.boiTokenId);
|
|
216
|
+
runs.push({ start: spliced.length, length: p.softTokens, kind: "image", index: imgIdx });
|
|
217
|
+
for (let k = 0; k < p.softTokens; k++) spliced.push(vIds.imageTokenId);
|
|
218
|
+
spliced.push(vIds.eoiTokenId);
|
|
126
219
|
imgIdx++;
|
|
220
|
+
} else if (aIds && id === aIds.audioTokenId) {
|
|
221
|
+
const clip = clips[audIdx];
|
|
222
|
+
if (!clip) throw new Error("more <|audio|> markers than audio clips");
|
|
223
|
+
spliced.push(aIds.boaTokenId);
|
|
224
|
+
runs.push({ start: spliced.length, length: clip.softTokens, kind: "audio", index: audIdx });
|
|
225
|
+
for (let k = 0; k < clip.softTokens; k++) spliced.push(aIds.audioTokenId);
|
|
226
|
+
spliced.push(aIds.eoaTokenId);
|
|
227
|
+
audIdx++;
|
|
127
228
|
} else {
|
|
128
229
|
spliced.push(id);
|
|
129
230
|
}
|
|
130
231
|
}
|
|
131
232
|
if (imgIdx !== images.length)
|
|
132
233
|
throw new Error(`${images.length} images but ${imgIdx} <|image|> markers`);
|
|
234
|
+
if (audIdx !== audio.length)
|
|
235
|
+
throw new Error(`${audio.length} audio clips but ${audIdx} <|audio|> markers`);
|
|
133
236
|
|
|
134
|
-
// embed text tokens, then overwrite
|
|
237
|
+
// embed text tokens, then overwrite soft-token rows with tower features
|
|
135
238
|
const idsArr = ops.fromInt32(spliced, [1, spliced.length]);
|
|
136
239
|
let embeds = model.embed.encode(idsArr);
|
|
137
240
|
idsArr.dispose();
|
|
138
241
|
const hidden = embeds.shape[2]!;
|
|
139
|
-
for (
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
242
|
+
for (const run of runs) {
|
|
243
|
+
let cast: MlxArray;
|
|
244
|
+
if (run.kind === "image") {
|
|
245
|
+
// vision towers return pre-divided by embed_scale (their oracle's
|
|
246
|
+
// frontend convention) — cast to the embedding dtype and splice.
|
|
247
|
+
const feats = towers.vision!.tower.features(pre[run.index]!); // [1, soft, hidden]
|
|
248
|
+
cast = feats.astype(embeds.dtype);
|
|
249
|
+
feats.dispose();
|
|
250
|
+
} else {
|
|
251
|
+
// audio mirrors gen-e4b-audio-golden.py EXACTLY:
|
|
252
|
+
// features.astype(embeds.dtype) / embed_scale
|
|
253
|
+
// — raw f32 embed_audio output, cast to bf16 FIRST, then divided by a
|
|
254
|
+
// weak (dtype-following, i.e. bf16) embed_scale scalar. The order is
|
|
255
|
+
// load-bearing for the bit-exact greedy gate (see AudioTower.features).
|
|
256
|
+
const clip = clips[run.index]!;
|
|
257
|
+
const raw = towers.audio!.tower.features(clip.mel, clip.frames, false); // [1, n, hidden] f32
|
|
258
|
+
if (raw.shape[1] !== clip.softTokens) {
|
|
259
|
+
const n = raw.shape[1];
|
|
260
|
+
raw.dispose();
|
|
261
|
+
embeds.dispose();
|
|
262
|
+
throw new Error(`audio tower produced ${n} frames but splice expects ${clip.softTokens}`);
|
|
263
|
+
}
|
|
264
|
+
const bf = raw.astype(embeds.dtype);
|
|
265
|
+
raw.dispose();
|
|
266
|
+
const scale = ops.scalarLike(towers.audio!.tower.embedScale, bf);
|
|
267
|
+
cast = ops.div(bf, scale);
|
|
268
|
+
bf.dispose();
|
|
269
|
+
scale.dispose();
|
|
270
|
+
}
|
|
271
|
+
const { start, length } = run;
|
|
144
272
|
const updated = ops.sliceUpdate(embeds, cast, [0, start, 0], [1, start + length, hidden]);
|
|
145
273
|
cast.dispose();
|
|
146
274
|
embeds.dispose();
|
|
147
275
|
embeds = updated;
|
|
148
276
|
}
|
|
149
277
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
|
|
278
|
+
// masks over the spliced sequence: union multimodal soft tokens (per-layer
|
|
279
|
+
// id zeroing) always; image-only bidirectional overlay only when NO audio
|
|
280
|
+
// is present (§3.3 Q1).
|
|
281
|
+
const mmInts = new Int32Array(spliced.length);
|
|
282
|
+
for (let i = 0; i < spliced.length; i++) {
|
|
283
|
+
const id = spliced[i]!;
|
|
284
|
+
mmInts[i] = (vIds && id === vIds.imageTokenId) || (aIds && id === aIds.audioTokenId) ? 1 : 0;
|
|
285
|
+
}
|
|
286
|
+
const mmI32 = MlxArray.fromInt32(mmInts, [spliced.length]);
|
|
287
|
+
const multimodalMask = mmI32.astype(Dtype.bool);
|
|
288
|
+
mmI32.dispose();
|
|
156
289
|
|
|
157
|
-
|
|
290
|
+
let bidirMask: MlxArray | null = null;
|
|
291
|
+
if (images.length > 0 && audio.length === 0) {
|
|
292
|
+
const imgInts = new Int32Array(spliced.length);
|
|
293
|
+
for (let i = 0; i < spliced.length; i++)
|
|
294
|
+
imgInts[i] = spliced[i] === vIds!.imageTokenId ? 1 : 0;
|
|
295
|
+
const imgI32 = MlxArray.fromInt32(imgInts, [spliced.length]);
|
|
296
|
+
bidirMask = imgI32.astype(Dtype.bool);
|
|
297
|
+
imgI32.dispose();
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return { ids: spliced, embeddings: embeds, bidirMask, multimodalMask };
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/** Vision-only prompt (back-compat wrapper over buildMultimodalPrompt —
|
|
304
|
+
* behavior byte-identical to the pre-audio builder: the returned imageMask
|
|
305
|
+
* serves as BOTH the bidirectional-attention mask and the per-layer-input
|
|
306
|
+
* zeroing mask, which forwardEmbeddings derives from `bidir` when no
|
|
307
|
+
* separate multimodal mask is passed). */
|
|
308
|
+
export async function buildVisionPrompt<P extends { softTokens: number }>(
|
|
309
|
+
model: Gemma4Model,
|
|
310
|
+
tower: VisionEncoder<P>,
|
|
311
|
+
tokenizer: LoadedTokenizer,
|
|
312
|
+
template: ChatTemplate,
|
|
313
|
+
messages: ChatMessage[],
|
|
314
|
+
images: Uint8Array[],
|
|
315
|
+
tokenIds: VisionTokenIds,
|
|
316
|
+
tools: ToolDefinition[] | null = null,
|
|
317
|
+
): Promise<VisionPrompt> {
|
|
318
|
+
const mp = await buildMultimodalPrompt(
|
|
319
|
+
model, { vision: { tower, tokenIds } }, tokenizer, template,
|
|
320
|
+
messages, images, [], tools,
|
|
321
|
+
);
|
|
322
|
+
// no audio was passed, so the union mask IS the image mask — reuse it for
|
|
323
|
+
// the zero-image edge (the original builder returned an all-false mask).
|
|
324
|
+
const imageMask = mp.bidirMask ?? mp.multimodalMask;
|
|
325
|
+
if (mp.bidirMask) mp.multimodalMask.dispose();
|
|
326
|
+
return { ids: mp.ids, embeddings: mp.embeddings, imageMask };
|
|
158
327
|
}
|