mlx-bun 0.0.4

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.
Files changed (210) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +362 -0
  3. package/THIRD_PARTY_LICENSES.md +41 -0
  4. package/bin/mlx-bun.mjs +32 -0
  5. package/docs/curve-designer.html +303 -0
  6. package/docs/design/adapters-end-to-end.md +201 -0
  7. package/docs/design/compat-cli-surface-design.md +227 -0
  8. package/docs/design/docs-reorg-plan.md +152 -0
  9. package/docs/design/hlg-sampling.md +340 -0
  10. package/docs/design/memory-system.md +339 -0
  11. package/docs/design/optimization_plan.md +344 -0
  12. package/docs/design/parallel-slots.md +515 -0
  13. package/docs/design/runtime-isolation.md +247 -0
  14. package/docs/design/segmented-backward-training.md +508 -0
  15. package/docs/investigations/chunk-finetune-experiment.md +84 -0
  16. package/docs/investigations/curve-bisector-routing.md +123 -0
  17. package/docs/investigations/curve-runs/bisector-route.json +98 -0
  18. package/docs/investigations/curve-runs/bisector-trace.json +1393 -0
  19. package/docs/investigations/curve-runs/curve-terrain.json +1179 -0
  20. package/docs/investigations/curve-runs/curve-terrain.svg +42 -0
  21. package/docs/investigations/curve-terrain.html +105 -0
  22. package/docs/investigations/expert-offload-single-user-moe.md +292 -0
  23. package/docs/investigations/hlg-ab.html +34 -0
  24. package/docs/investigations/hlg-explorer.html +238 -0
  25. package/docs/investigations/hlg-figs/cliff.svg +1 -0
  26. package/docs/investigations/hlg-figs/cross.svg +1 -0
  27. package/docs/investigations/hlg-figs/edges.svg +1 -0
  28. package/docs/investigations/hlg-figs/k20-grids.svg +1 -0
  29. package/docs/investigations/hlg-figs/k20-peaks.svg +1 -0
  30. package/docs/investigations/hlg-figs/lw-curve.svg +1 -0
  31. package/docs/investigations/hlg-figs/terrain.svg +1 -0
  32. package/docs/investigations/hlg-report.html +262 -0
  33. package/docs/investigations/hlg-runs/hlg-ab.json +149 -0
  34. package/docs/investigations/hlg-runs/hlg-blind-prompts.md +87 -0
  35. package/docs/investigations/hlg-runs/hlg-blind.json +107 -0
  36. package/docs/investigations/hlg-runs/hlg-cliff.json +197 -0
  37. package/docs/investigations/hlg-runs/hlg-cross.json +505 -0
  38. package/docs/investigations/hlg-runs/hlg-edge.json +388 -0
  39. package/docs/investigations/hlg-runs/hlg-eval1.json +25 -0
  40. package/docs/investigations/hlg-runs/hlg-frontier.json +86 -0
  41. package/docs/investigations/hlg-runs/hlg-grid.json +242 -0
  42. package/docs/investigations/hlg-runs/hlg-grid2.json +343 -0
  43. package/docs/investigations/hlg-runs/hlg-lw-coarse.json +110 -0
  44. package/docs/investigations/hlg-runs/hlg-map.json +383 -0
  45. package/docs/investigations/hlg-runs/run0-original.log +159 -0
  46. package/docs/investigations/hlg-runs/runA-recommended.log +131 -0
  47. package/docs/investigations/hlg-runs/runAA-cliff-cpm5.log +26 -0
  48. package/docs/investigations/hlg-runs/runAB-grid2-e4b.log +18 -0
  49. package/docs/investigations/hlg-runs/runAC-peak-e4b-partial.log +25 -0
  50. package/docs/investigations/hlg-runs/runAD-ab-samples.log +8 -0
  51. package/docs/investigations/hlg-runs/runAE-edge-e4b.log +51 -0
  52. package/docs/investigations/hlg-runs/runAF-logit-calibration.log +10 -0
  53. package/docs/investigations/hlg-runs/runAG-eval-config2.log +16 -0
  54. package/docs/investigations/hlg-runs/runB-gain-variants.log +192 -0
  55. package/docs/investigations/hlg-runs/runC-pivots.log +162 -0
  56. package/docs/investigations/hlg-runs/runD-oetf.log +132 -0
  57. package/docs/investigations/hlg-runs/runE-eotf.log +162 -0
  58. package/docs/investigations/hlg-runs/runF-shaper-working.log +86 -0
  59. package/docs/investigations/hlg-runs/runG-shaper-sweep.log +101 -0
  60. package/docs/investigations/hlg-runs/runH-wsweep.log +176 -0
  61. package/docs/investigations/hlg-runs/runI-asweep.log +176 -0
  62. package/docs/investigations/hlg-runs/runJ-knobmap.log +49 -0
  63. package/docs/investigations/hlg-runs/runK-frontier.log +19 -0
  64. package/docs/investigations/hlg-runs/runL-widemap.log +65 -0
  65. package/docs/investigations/hlg-runs/runM-frontier-emb.log +15 -0
  66. package/docs/investigations/hlg-runs/runN-verify.log +116 -0
  67. package/docs/investigations/hlg-runs/runO-frontier-seed5000.log +15 -0
  68. package/docs/investigations/hlg-runs/runP-grid-AxSm.log +15 -0
  69. package/docs/investigations/hlg-runs/runQ-grid-tgW.log +15 -0
  70. package/docs/investigations/hlg-runs/runR-blind-transcript.log +79 -0
  71. package/docs/investigations/hlg-runs/runS-cross-coherence.log +33 -0
  72. package/docs/investigations/hlg-runs/runT-cross-cpm5.log +33 -0
  73. package/docs/investigations/hlg-runs/runU-cross-12b.log +33 -0
  74. package/docs/investigations/hlg-runs/runV-lw-e4b.log +22 -0
  75. package/docs/investigations/hlg-runs/runW-lw-12b.log +22 -0
  76. package/docs/investigations/hlg-runs/runX-lw-cpm5.log +22 -0
  77. package/docs/investigations/hlg-runs/runY-cliff-e4b.log +26 -0
  78. package/docs/investigations/hlg-runs/runZ-cliff-12b.log +26 -0
  79. package/docs/investigations/hlg-sampling-investigation.md +970 -0
  80. package/docs/investigations/lab-build-journal.md +157 -0
  81. package/docs/investigations/pi-builtin-investigation.md +308 -0
  82. package/docs/investigations/starter-model-port-handoff.md +323 -0
  83. package/docs/planning/IDEAS.md +26 -0
  84. package/docs/planning/PRODUCT_ROADMAP.md +426 -0
  85. package/docs/planning/ResearchTopics.md +119 -0
  86. package/docs/planning/chunk-task-roadmap.md +77 -0
  87. package/docs/planning/journal.md +155 -0
  88. package/docs/reference/distribution.md +121 -0
  89. package/docs/reference/embedding.md +168 -0
  90. package/docs/reference/library-api.md +153 -0
  91. package/docs/reference/server-api.md +224 -0
  92. package/docs/reference/server-config.md +243 -0
  93. package/docs/segmented-backward-handoff.md +148 -0
  94. package/package.json +68 -0
  95. package/src/anthropic.ts +491 -0
  96. package/src/chat-template.ts +186 -0
  97. package/src/cli.ts +1075 -0
  98. package/src/config.ts +229 -0
  99. package/src/curve-sampler.ts +122 -0
  100. package/src/dataset/generators.ts +1087 -0
  101. package/src/dataset/index.ts +12 -0
  102. package/src/dataset/job.ts +81 -0
  103. package/src/dataset/llm.ts +71 -0
  104. package/src/dataset/registry.ts +528 -0
  105. package/src/download.ts +283 -0
  106. package/src/eval/README.md +86 -0
  107. package/src/eval/kl-prompts.ts +29 -0
  108. package/src/eval/kl.ts +294 -0
  109. package/src/eval/quality-db.ts +92 -0
  110. package/src/eval/runner.ts +102 -0
  111. package/src/eval/score.ts +49 -0
  112. package/src/eval/tasks/bfcl.ts +500 -0
  113. package/src/eval/tasks/gsm8k.ts +85 -0
  114. package/src/eval/tasks/hashhop.ts +109 -0
  115. package/src/eval/tasks/humaneval.ts +252 -0
  116. package/src/eval/tasks/ifeval.ts +354 -0
  117. package/src/eval/tasks/mmlu.ts +145 -0
  118. package/src/evaldb.ts +199 -0
  119. package/src/expert-offload-build.ts +77 -0
  120. package/src/expert-offload.ts +61 -0
  121. package/src/expert-trace.ts +67 -0
  122. package/src/fit.ts +254 -0
  123. package/src/generate.ts +451 -0
  124. package/src/harness-pi.ts +177 -0
  125. package/src/hf-push.ts +628 -0
  126. package/src/index.ts +22 -0
  127. package/src/jobs/db.ts +139 -0
  128. package/src/jobs/index.ts +29 -0
  129. package/src/jobs/job-entry.ts +106 -0
  130. package/src/jobs/runner.ts +304 -0
  131. package/src/jobs/sse.ts +114 -0
  132. package/src/jobs/types.ts +58 -0
  133. package/src/kv-store.ts +185 -0
  134. package/src/lora.ts +314 -0
  135. package/src/md-modules.d.ts +7 -0
  136. package/src/mlx/array.ts +237 -0
  137. package/src/mlx/autograd.ts +267 -0
  138. package/src/mlx/checkpoint.ts +116 -0
  139. package/src/mlx/compile.ts +158 -0
  140. package/src/mlx/custom-vjp.ts +140 -0
  141. package/src/mlx/ffi.ts +433 -0
  142. package/src/mlx/metal-kernel.ts +130 -0
  143. package/src/mlx/ops.ts +707 -0
  144. package/src/mmap.ts +90 -0
  145. package/src/model/batched-mask.ts +204 -0
  146. package/src/model/batched-rotating.ts +300 -0
  147. package/src/model/compiled-decode.ts +681 -0
  148. package/src/model/factory.ts +27 -0
  149. package/src/model/fingerprint.ts +33 -0
  150. package/src/model/flash-attention.ts +674 -0
  151. package/src/model/fused-decode-kernel.ts +305 -0
  152. package/src/model/fused-geglu-kernel.ts +87 -0
  153. package/src/model/gemma4-base.ts +1616 -0
  154. package/src/model/gemma4.ts +1012 -0
  155. package/src/model/generated/gemma4-12b.ts +526 -0
  156. package/src/model/generated/gemma4-26b.ts +500 -0
  157. package/src/model/generated/gemma4-e4b.ts +757 -0
  158. package/src/model/generated/index.ts +18 -0
  159. package/src/model/minicpm5.ts +278 -0
  160. package/src/model/qwen3-delta.ts +224 -0
  161. package/src/model/qwen3_5.ts +429 -0
  162. package/src/model/support.ts +46 -0
  163. package/src/native-pack.ts +152 -0
  164. package/src/pi-provider.ts +95 -0
  165. package/src/pi-terminal.ts +264 -0
  166. package/src/pi-web.ts +924 -0
  167. package/src/preflight.ts +123 -0
  168. package/src/prompt-cache.ts +113 -0
  169. package/src/quantize/allocator.ts +627 -0
  170. package/src/quantize/calibration.ts +180 -0
  171. package/src/quantize/config-writer.ts +132 -0
  172. package/src/quantize/index.ts +57 -0
  173. package/src/quantize/job.ts +136 -0
  174. package/src/quantize/quantizer.ts +462 -0
  175. package/src/quantize/safetensors-writer.ts +171 -0
  176. package/src/quantize/sensitivity.ts +234 -0
  177. package/src/registry.ts +336 -0
  178. package/src/responses.ts +774 -0
  179. package/src/safetensors.ts +158 -0
  180. package/src/sampler.ts +517 -0
  181. package/src/serve/batch-scheduler.ts +362 -0
  182. package/src/serve/generation-gateway.ts +160 -0
  183. package/src/server.ts +1891 -0
  184. package/src/spec/drafter.ts +261 -0
  185. package/src/spec/generate.ts +221 -0
  186. package/src/status-page.html +497 -0
  187. package/src/tokenizer.ts +38 -0
  188. package/src/tool-call.ts +242 -0
  189. package/src/train/dataset.ts +461 -0
  190. package/src/train/export.ts +32 -0
  191. package/src/train/forward.ts +229 -0
  192. package/src/train/index.ts +43 -0
  193. package/src/train/job.ts +148 -0
  194. package/src/train/lora-params.ts +194 -0
  195. package/src/train/loss.ts +342 -0
  196. package/src/train/merge.ts +195 -0
  197. package/src/train/optimizer.ts +176 -0
  198. package/src/train/rank.ts +101 -0
  199. package/src/train/segmented.ts +520 -0
  200. package/src/train/trainer.ts +600 -0
  201. package/src/tui.ts +161 -0
  202. package/src/vision/embedder.ts +168 -0
  203. package/src/vision/preprocess.ts +247 -0
  204. package/src/vision/prompt.ts +158 -0
  205. package/src/vision/siglip.ts +599 -0
  206. package/src/web/app.html +2471 -0
  207. package/src/web/skills/web-research/SKILL.md +44 -0
  208. package/src/web/skills.ts +59 -0
  209. package/src/web-tools.ts +426 -0
  210. package/src/weights.ts +91 -0
package/src/tui.ts ADDED
@@ -0,0 +1,161 @@
1
+ // Startup TUI: gradient banner, spinner steps, boxed ready-card.
2
+ // Zero dependencies — truecolor ANSI by hand, matching the status
3
+ // page's keynote gradient (orange → pink → purple → blue). Everything
4
+ // degrades to plain text when stdout isn't a TTY or NO_COLOR is set.
5
+
6
+ const tty = (): boolean => Boolean(process.stdout.isTTY) && !process.env.NO_COLOR;
7
+
8
+ const RESET = "\x1b[0m";
9
+ const DIM = "\x1b[2m";
10
+ const BOLD = "\x1b[1m";
11
+
12
+ type Rgb = [number, number, number];
13
+ // the keynote ramp
14
+ const RAMP: Rgb[] = [[255, 159, 10], [255, 55, 95], [191, 90, 242], [10, 132, 255]];
15
+
16
+ function lerp(a: Rgb, b: Rgb, t: number): Rgb {
17
+ return [0, 1, 2].map((i) => Math.round(a[i]! + (b[i]! - a[i]!) * t)) as Rgb;
18
+ }
19
+ function rampAt(t: number): Rgb {
20
+ const x = Math.min(0.999, Math.max(0, t)) * (RAMP.length - 1);
21
+ return lerp(RAMP[Math.floor(x)]!, RAMP[Math.floor(x) + 1]!, x - Math.floor(x));
22
+ }
23
+ const fg = ([r, g, b]: Rgb) => `\x1b[38;2;${r};${g};${b}m`;
24
+
25
+ /** Horizontal gradient across each line (visible chars only). */
26
+ export function gradient(text: string): string {
27
+ if (!tty()) return text;
28
+ return text.split("\n").map((line) => {
29
+ const chars = [...line];
30
+ const n = Math.max(1, chars.length - 1);
31
+ return chars.map((ch, i) => (ch === " " ? ch : fg(rampAt(i / n)) + ch)).join("") + RESET;
32
+ }).join("\n");
33
+ }
34
+
35
+ const WORDMARK = `
36
+ ███╗ ███╗██╗ ██╗ ██╗ ██████╗ ██╗ ██╗███╗ ██╗
37
+ ████╗ ████║██║ ╚██╗██╔╝ ██╔══██╗██║ ██║████╗ ██║
38
+ ██╔████╔██║██║ ╚███╔╝ █████╗██████╔╝██║ ██║██╔██╗ ██║
39
+ ██║╚██╔╝██║██║ ██╔██╗ ╚════╝██╔══██╗██║ ██║██║╚██╗██║
40
+ ██║ ╚═╝ ██║███████╗██╔╝ ██╗ ██████╔╝╚██████╔╝██║ ╚████║
41
+ ╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═══╝`.slice(1);
42
+
43
+ export function banner(version: string, tagline = "local AI · apple silicon · one binary"): void {
44
+ if (!tty()) {
45
+ console.log(`mlx-bun ${version} — ${tagline}`);
46
+ return;
47
+ }
48
+ console.log();
49
+ console.log(gradient(WORDMARK));
50
+ console.log(`${DIM} v${version} · ${tagline}${RESET}\n`);
51
+ }
52
+
53
+ const FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
54
+ const OK = tty() ? "\x1b[38;2;48;209;88m✓\x1b[0m" : "✓";
55
+ const FAIL = tty() ? "\x1b[38;2;255;69;58m✗\x1b[0m" : "✗";
56
+
57
+ export interface Step {
58
+ update(text: string): void;
59
+ done(text?: string): void;
60
+ fail(text?: string): void;
61
+ }
62
+
63
+ /** Spinner line; on non-TTY prints start/end lines instead. */
64
+ export function step(text: string): Step {
65
+ let current = text;
66
+ if (!tty()) {
67
+ console.log(` - ${text}`);
68
+ return {
69
+ update(t) { current = t; },
70
+ done(t) { console.log(` ${OK} ${t ?? current}`); },
71
+ fail(t) { console.log(` ${FAIL} ${t ?? current}`); },
72
+ };
73
+ }
74
+ let i = 0;
75
+ const draw = () => process.stdout.write(`\r\x1b[2K ${fg(rampAt((i % 20) / 20))}${FRAMES[i % FRAMES.length]}${RESET} ${current}`);
76
+ draw();
77
+ const timer = setInterval(() => { i++; draw(); }, 80);
78
+ const finish = (mark: string, t?: string) => {
79
+ clearInterval(timer);
80
+ process.stdout.write(`\r\x1b[2K ${mark} ${t ?? current}\n`);
81
+ };
82
+ return {
83
+ update(t) { current = t; draw(); },
84
+ done(t) { finish(OK, t); },
85
+ fail(t) { finish(FAIL, t); },
86
+ };
87
+ }
88
+
89
+ /** Rounded box with a gradient border. Lines may contain ANSI codes. */
90
+ export function box(lines: string[], { pad = 1 }: { pad?: number } = {}): void {
91
+ const visible = (s: string) => [...s.replace(/\x1b\[[0-9;]*m/g, "")].length;
92
+ const width = Math.max(...lines.map(visible)) + pad * 2;
93
+ const hue = (s: string) => (tty() ? fg(rampAt(0.15)) + s + RESET : s);
94
+ console.log(hue(` ╭${"─".repeat(width)}╮`));
95
+ for (const l of lines) {
96
+ const fill = " ".repeat(width - visible(l) - pad * 2);
97
+ console.log(`${hue(" │")}${" ".repeat(pad)}${l}${fill}${" ".repeat(pad)}${hue("│")}`);
98
+ }
99
+ console.log(hue(` ╰${"─".repeat(width)}╯`));
100
+ }
101
+
102
+ export interface Column {
103
+ header: string;
104
+ align?: "left" | "right";
105
+ /** Optional per-cell styling (receives the padded cell). */
106
+ paint?: (cell: string, row: number) => string;
107
+ }
108
+
109
+ /** Aligned table with dim uppercase headers; rows are plain strings. */
110
+ export function table(cols: Column[], rows: string[][], indent = " "): void {
111
+ const widths = cols.map((c, i) =>
112
+ Math.max([...c.header].length, ...rows.map((r) => [...(r[i] ?? "")].length)));
113
+ console.log(indent + cols.map((c, i) =>
114
+ style.dim((c.align === "right" ? c.header.padStart(widths[i]!) : c.header.padEnd(widths[i]!)).toUpperCase())).join(" "));
115
+ rows.forEach((r, ri) => {
116
+ console.log(indent + cols.map((c, i) => {
117
+ const cell = c.align === "right" ? (r[i] ?? "").padStart(widths[i]!) : (r[i] ?? "").padEnd(widths[i]!);
118
+ return c.paint ? c.paint(cell, ri) : cell;
119
+ }).join(" "));
120
+ });
121
+ }
122
+
123
+ /** Gradient section header, e.g. `h1("library")`. */
124
+ export function h1(text: string): void {
125
+ console.log();
126
+ console.log(" " + (tty() ? gradient(text.toUpperCase()) : text.toUpperCase()));
127
+ }
128
+
129
+ /** Style plain help text structurally: title line, Usage, section
130
+ * headers, flags, defaults. Content is untouched on non-TTY. */
131
+ export function renderHelp(text: string): string {
132
+ if (!tty()) return text;
133
+ const accent = fg(rampAt(0.35));
134
+ const blue = fg(rampAt(0.9));
135
+ return text.split("\n").map((line) => {
136
+ // "mlx-bun <cmd> — description" title line
137
+ const title = /^(mlx-bun(?: \S+)?)( — )(.*)$/.exec(line);
138
+ if (title) return gradient(title[1]!) + DIM + title[2]! + title[3]! + RESET;
139
+ // "Usage: ..." line
140
+ const usage = /^(Usage:)( .*)$/.exec(line);
141
+ if (usage) return DIM + usage[1]! + RESET + BOLD + usage[2]! + RESET;
142
+ // Section headers ("Server options:", "Commands:", "Examples:")
143
+ if (/^[A-Z][\w &/()'|-]*( \(.*\))?:$/.test(line)) return accent + BOLD + line + RESET;
144
+ // Flag/command lines: color the flag tokens, dim [default: ...]
145
+ let out = line.replace(/(^|\s)(--[\w-]+|-[a-zA-Z](?=[,\s]))/g, (_, pre, f) => pre + blue + f + RESET);
146
+ out = out.replace(/\[default:[^\]]*\]/g, (m) => DIM + m + RESET);
147
+ // Leading command word in overview command lists (" pi Launch...")
148
+ out = out.replace(/^( {2})([a-z][\w-]*)( {2,})/g, (_, pre, cmd2, sp) => pre + BOLD + cmd2 + RESET + sp);
149
+ // Example lines: dim trailing comments
150
+ out = out.replace(/(#.*)$/g, (m) => DIM + m + RESET);
151
+ return out;
152
+ }).join("\n");
153
+ }
154
+
155
+ export const style = {
156
+ dim: (s: string) => (tty() ? DIM + s + RESET : s),
157
+ bold: (s: string) => (tty() ? BOLD + s + RESET : s),
158
+ green: (s: string) => (tty() ? "\x1b[38;2;48;209;88m" + s + RESET : s),
159
+ accent: (s: string) => (tty() ? fg(rampAt(0.35)) + s + RESET : s),
160
+ url: (s: string) => (tty() ? "\x1b[4m" + fg(rampAt(0.9)) + s + RESET : s),
161
+ };
@@ -0,0 +1,168 @@
1
+ // Vision feature pipeline — port of optiq's gemma4_unified VisionEmbedder
2
+ // + MultimodalEmbedder (vendored from mlx-vlm, BSD-3).
3
+ //
4
+ // Encoder-free: patches [n, 6912] → LayerNorm → Linear(+bias) → LayerNorm
5
+ // → + 2D pos embedding → LayerNorm → RMSNormNoScale → Linear(no bias)
6
+ // → divide by embed_scale (the language model re-multiplies it).
7
+ //
8
+ // Sidecar weights stay bf16; features come out bf16 like text embeddings.
9
+
10
+ import { ptr, read } from "bun:ffi";
11
+ import { MlxArray, cpuStream } from "../mlx/array";
12
+ import { C } from "../mlx/ffi";
13
+ import * as ops from "../mlx/ops";
14
+ import type { PreprocessedImage } from "./preprocess";
15
+ import { NUM_SOFT_TOKENS, preprocessImage } from "./preprocess";
16
+
17
+ const cstr = (s: string) => Buffer.from(s + "\0", "utf8");
18
+
19
+ export class VisionTower {
20
+ #weights = new Map<string, MlxArray>();
21
+ readonly embedScale: number;
22
+ readonly rmsNormEps: number;
23
+
24
+ private constructor(embedScale: number, rmsNormEps: number) {
25
+ this.embedScale = embedScale;
26
+ this.rmsNormEps = rmsNormEps;
27
+ }
28
+
29
+ static load(modelDir: string, embedScale: number, rmsNormEps = 1e-6): VisionTower {
30
+ const self = new VisionTower(embedScale, rmsNormEps);
31
+ // out-param slots read back via read.u64, not [0] (DFG stale-read bug
32
+ // — see outArray in mlx/ffi.ts).
33
+ const arrMap = new BigUint64Array([C.mlx_map_string_to_array_new()]);
34
+ const metaMap = new BigUint64Array([C.mlx_map_string_to_string_new()]);
35
+ const arrMapPtr = ptr(arrMap);
36
+ const metaMapPtr = ptr(metaMap);
37
+ const status = C.mlx_load_safetensors(
38
+ arrMapPtr, metaMapPtr, ptr(cstr(`${modelDir}/optiq_vision.safetensors`)), cpuStream,
39
+ );
40
+ C.mlx_map_string_to_string_free(read.u64(metaMapPtr, 0));
41
+ if (status !== 0) throw new Error(`failed to load vision sidecar from ${modelDir}`);
42
+ const arrMapHandle = read.u64(arrMapPtr, 0);
43
+ const names = [
44
+ "vision_embedder.patch_ln1.weight", "vision_embedder.patch_ln1.bias",
45
+ "vision_embedder.patch_dense.weight", "vision_embedder.patch_dense.bias",
46
+ "vision_embedder.patch_ln2.weight", "vision_embedder.patch_ln2.bias",
47
+ "vision_embedder.pos_embedding",
48
+ "vision_embedder.pos_norm.weight", "vision_embedder.pos_norm.bias",
49
+ "embed_vision.embedding_projection.weight",
50
+ ];
51
+ for (const name of names) {
52
+ const slot = new BigUint64Array([C.mlx_array_new()]);
53
+ const slotPtr = ptr(slot);
54
+ if (C.mlx_map_string_to_array_get(slotPtr, arrMapHandle, ptr(cstr(name))) !== 0)
55
+ throw new Error(`vision sidecar missing tensor ${name}`);
56
+ self.#weights.set(name, new MlxArray(read.u64(slotPtr, 0)));
57
+ }
58
+ C.mlx_map_string_to_array_free(arrMapHandle);
59
+ return self;
60
+ }
61
+
62
+ async preprocess(bytes: Uint8Array): Promise<PreprocessedImage> {
63
+ return preprocessImage(bytes);
64
+ }
65
+
66
+ #w(name: string): MlxArray {
67
+ return this.#weights.get(name)!;
68
+ }
69
+
70
+ /** nn.Linear: x @ W.T (+ bias). */
71
+ #linear(x: MlxArray, weight: MlxArray, bias: MlxArray | null): MlxArray {
72
+ const wT = ops.transposeAxes(weight, [1, 0]);
73
+ let out = ops.matmul(x, wT);
74
+ wT.dispose();
75
+ if (bias) {
76
+ const withBias = ops.add(out, bias);
77
+ out.dispose();
78
+ out = withBias;
79
+ }
80
+ return out;
81
+ }
82
+
83
+ /** One image's preprocessed patches → language-space features
84
+ * [softTokens, hidden] (pre-divided by embed_scale). */
85
+ features(img: PreprocessedImage): MlxArray {
86
+ const eps = this.rmsNormEps;
87
+ const patchDim = img.patches.length / NUM_SOFT_TOKENS;
88
+ let h: MlxArray = MlxArray.fromFloat32(img.patches, [1, NUM_SOFT_TOKENS, patchDim]);
89
+
90
+ h = disposing(h, ops.layerNorm(h,
91
+ this.#w("vision_embedder.patch_ln1.weight"),
92
+ this.#w("vision_embedder.patch_ln1.bias"), eps));
93
+ h = disposing(h, this.#linear(h,
94
+ this.#w("vision_embedder.patch_dense.weight"),
95
+ this.#w("vision_embedder.patch_dense.bias")));
96
+ h = disposing(h, ops.layerNorm(h,
97
+ this.#w("vision_embedder.patch_ln2.weight"),
98
+ this.#w("vision_embedder.patch_ln2.bias"), eps));
99
+
100
+ // + 2D position embedding: posemb[clamped[:,0], 0] + posemb[clamped[:,1], 1]
101
+ // (clamp + validity mask precomputed host-side; padding rows get -1)
102
+ const n = NUM_SOFT_TOKENS;
103
+ const xIdx = new Int32Array(n);
104
+ const yIdx = new Int32Array(n);
105
+ const valid = new Float32Array(2 * n);
106
+ for (let i = 0; i < n; i++) {
107
+ const px = img.positions[i * 2]!;
108
+ const py = img.positions[i * 2 + 1]!;
109
+ xIdx[i] = Math.max(px, 0);
110
+ yIdx[i] = Math.max(py, 0);
111
+ valid[i] = px === -1 ? 0 : 1;
112
+ valid[n + i] = py === -1 ? 0 : 1;
113
+ }
114
+ const posemb = this.#w("vision_embedder.pos_embedding"); // [1120, 2, 3840]
115
+ const addPos = (idx: Int32Array, dim: number, validOff: number): MlxArray => {
116
+ const idxArr = MlxArray.fromInt32(idx, [n]);
117
+ const rows = ops.takeAxis(posemb, idxArr, 0); // [n, 2, 3840]
118
+ idxArr.dispose();
119
+ const sel = rows.slice([0, dim, 0], [n, dim + 1, 3840]);
120
+ rows.dispose();
121
+ const flat = ops.reshape(sel, [1, n, 3840]);
122
+ sel.dispose();
123
+ const v = MlxArray.fromFloat32(valid.subarray(validOff, validOff + n).slice(), [1, n, 1]);
124
+ const vCast = v.astype(flat.dtype);
125
+ v.dispose();
126
+ const masked = ops.mul(flat, vCast);
127
+ flat.dispose();
128
+ vCast.dispose();
129
+ return masked;
130
+ };
131
+ const xPos = addPos(xIdx, 0, 0);
132
+ const yPos = addPos(yIdx, 1, n);
133
+ const posSum = ops.add(xPos, yPos);
134
+ xPos.dispose();
135
+ yPos.dispose();
136
+ h = disposing(h, ops.add(h, posSum));
137
+ posSum.dispose();
138
+
139
+ h = disposing(h, ops.layerNorm(h,
140
+ this.#w("vision_embedder.pos_norm.weight"),
141
+ this.#w("vision_embedder.pos_norm.bias"), eps));
142
+
143
+ // MultimodalEmbedder: RMSNormNoScale → Linear(no bias)
144
+ h = disposing(h, ops.rmsNorm(h, null, eps));
145
+ h = disposing(h, this.#linear(h,
146
+ this.#w("embed_vision.embedding_projection.weight"), null));
147
+
148
+ // keep only real (unpadded) soft tokens; pre-divide by embed_scale
149
+ const hidden = h.shape[2]!;
150
+ const real = h.slice([0, 0, 0], [1, img.softTokens, hidden]);
151
+ h.dispose();
152
+ const scale = ops.scalarLike(this.embedScale, real);
153
+ const out = ops.div(real, scale);
154
+ real.dispose();
155
+ scale.dispose();
156
+ return out; // [1, softTokens, hidden]
157
+ }
158
+
159
+ dispose(): void {
160
+ for (const w of this.#weights.values()) w.dispose();
161
+ this.#weights.clear();
162
+ }
163
+ }
164
+
165
+ function disposing(old: MlxArray, next: MlxArray): MlxArray {
166
+ old.dispose();
167
+ return next;
168
+ }
@@ -0,0 +1,247 @@
1
+ // Gemma-4 unified image preprocessing — port of optiq's
2
+ // vlm/gemma4_unified/image_processing.py (vendored from mlx-vlm, BSD-3).
3
+ //
4
+ // decode → aspect-ratio-preserving resize (PIL-style convolution
5
+ // resampling, bicubic kernel with antialias-scaled support) → rescale
6
+ // 1/255 → patchify into 48×48 patch vectors with 2D grid positions →
7
+ // pad to the 280 soft-token budget.
8
+ //
9
+ // Decode strategy (Bun ≥1.3.14): PNG goes straight through fast-png
10
+ // (exact, lossless — the parity-golden path). Everything else (JPEG,
11
+ // HEIC, AVIF, WebP, TIFF, GIF, BMP) is transcoded to lossless PNG by
12
+ // Bun.Image — native OS codecs (ImageIO on macOS), EXIF auto-orient,
13
+ // off-thread — then decoded by fast-png. Bun.Image has no raw-pixel
14
+ // terminal, so the PNG bridge is the supported path to pixels.
15
+ //
16
+ // Resize note: Bun.Image.resize is NOT used — its kernels (lanczos3,
17
+ // cubic≠PIL-bicubic) don't match PIL's antialiased bicubic, which the
18
+ // vision tower was trained behind. PIL's exact float pipeline isn't
19
+ // bit-reproducible here either, so vision logits can differ slightly
20
+ // from the python stack on images that need resizing. Images already
21
+ // sized to multiples of 48 (≤2520 patches) skip the resize entirely and
22
+ // are bit-identical through preprocessing.
23
+
24
+ import { decode as decodePng } from "fast-png";
25
+
26
+ export const PATCH_SIZE = 16;
27
+ export const POOLING_KERNEL_SIZE = 3;
28
+ export const MODEL_PATCH_SIZE = 48; // patch_size * pooling_kernel_size
29
+ export const NUM_SOFT_TOKENS = 280;
30
+
31
+ export interface RGBImage {
32
+ width: number;
33
+ height: number;
34
+ /** Row-major RGB, 3 bytes/px. */
35
+ data: Uint8Array;
36
+ }
37
+
38
+ export interface PreprocessedImage {
39
+ /** [numSoftTokens, 6912] f32 patch vectors (padded). */
40
+ patches: Float32Array;
41
+ /** [numSoftTokens, 2] (x, y) grid positions; -1 padding. */
42
+ positions: Int32Array;
43
+ /** Real (unpadded) soft-token count. */
44
+ softTokens: number;
45
+ }
46
+
47
+ function pngToRgb(bytes: Uint8Array): RGBImage {
48
+ const png = decodePng(bytes);
49
+ const ch = png.channels;
50
+ const out = new Uint8Array(png.width * png.height * 3);
51
+ const src = png.data as Uint8Array;
52
+ const n = png.width * png.height;
53
+ if (png.depth === 16) {
54
+ const src16 = png.data as Uint16Array;
55
+ for (let i = 0; i < n; i++)
56
+ for (let c = 0; c < 3; c++)
57
+ out[i * 3 + c] = src16[i * ch + Math.min(c, ch - 1)]! >> 8;
58
+ } else {
59
+ for (let i = 0; i < n; i++)
60
+ for (let c = 0; c < 3; c++)
61
+ out[i * 3 + c] = src[i * ch + Math.min(c, ch - 1)]!;
62
+ }
63
+ return { width: png.width, height: png.height, data: out };
64
+ }
65
+
66
+ export async function decodeImage(bytes: Uint8Array): Promise<RGBImage> {
67
+ // PNG: direct, exact (the parity-golden path)
68
+ if (bytes.length > 8 && bytes[0] === 0x89 && bytes[1] === 0x50)
69
+ return pngToRgb(bytes);
70
+ // everything else: native transcode to lossless PNG (HEIC/AVIF/WebP/
71
+ // JPEG/TIFF/GIF/BMP — whatever the OS codecs decode), EXIF auto-orient
72
+ try {
73
+ const png = await new Bun.Image(bytes).png({ compressionLevel: 1 }).bytes();
74
+ return pngToRgb(png);
75
+ } catch (e) {
76
+ const code = (e as { code?: string }).code;
77
+ throw new Error(
78
+ code === "ERR_IMAGE_UNKNOWN_FORMAT"
79
+ ? "unsupported image format"
80
+ : `image decode failed: ${(e as Error).message}`,
81
+ );
82
+ }
83
+ }
84
+
85
+ // --- PIL-style convolution resize (bicubic, a = -0.5) ---------------------
86
+
87
+ function bicubic(x: number): number {
88
+ const a = -0.5;
89
+ x = Math.abs(x);
90
+ if (x < 1) return ((a + 2) * x - (a + 3)) * x * x + 1;
91
+ if (x < 2) return (((x - 5) * x + 8) * x - 4) * a;
92
+ return 0;
93
+ }
94
+
95
+ interface ResampleCoeffs {
96
+ bounds: Int32Array; // [outSize × 2]: (start, count)
97
+ weights: Float64Array; // [outSize × kmax]
98
+ kmax: number;
99
+ }
100
+
101
+ /** Port of PIL's precompute_coeffs (antialias: support scales with ratio). */
102
+ function precomputeCoeffs(inSize: number, outSize: number): ResampleCoeffs {
103
+ const SUPPORT = 2.0;
104
+ const scale = inSize / outSize;
105
+ const filterscale = Math.max(scale, 1.0);
106
+ const support = SUPPORT * filterscale;
107
+ const kmax = Math.ceil(support) * 2 + 1;
108
+ const bounds = new Int32Array(outSize * 2);
109
+ const weights = new Float64Array(outSize * kmax);
110
+
111
+ for (let xx = 0; xx < outSize; xx++) {
112
+ const center = (xx + 0.5) * scale;
113
+ let xmin = Math.floor(center - support);
114
+ if (xmin < 0) xmin = 0;
115
+ let xmax = Math.ceil(center + support);
116
+ if (xmax > inSize) xmax = inSize;
117
+ const count = xmax - xmin;
118
+ let sum = 0;
119
+ for (let k = 0; k < count; k++) {
120
+ const w = bicubic((xmin + k - center + 0.5) / filterscale);
121
+ weights[xx * kmax + k] = w;
122
+ sum += w;
123
+ }
124
+ if (sum !== 0)
125
+ for (let k = 0; k < count; k++) weights[xx * kmax + k]! /= sum;
126
+ bounds[xx * 2] = xmin;
127
+ bounds[xx * 2 + 1] = count;
128
+ }
129
+ return { bounds, weights, kmax };
130
+ }
131
+
132
+ /** Separable resize on float RGB planes (clamped back to [0,255] like PIL). */
133
+ export function resizeBicubic(img: RGBImage, outW: number, outH: number): RGBImage {
134
+ const { width: inW, height: inH, data } = img;
135
+ // horizontal pass → [inH, outW]
136
+ const hCoef = precomputeCoeffs(inW, outW);
137
+ const tmp = new Float64Array(inH * outW * 3);
138
+ for (let y = 0; y < inH; y++) {
139
+ for (let x = 0; x < outW; x++) {
140
+ const xmin = hCoef.bounds[x * 2]!;
141
+ const count = hCoef.bounds[x * 2 + 1]!;
142
+ let r = 0, g = 0, b = 0;
143
+ for (let k = 0; k < count; k++) {
144
+ const w = hCoef.weights[x * hCoef.kmax + k]!;
145
+ const src = (y * inW + xmin + k) * 3;
146
+ r += data[src]! * w;
147
+ g += data[src + 1]! * w;
148
+ b += data[src + 2]! * w;
149
+ }
150
+ const dst = (y * outW + x) * 3;
151
+ tmp[dst] = r;
152
+ tmp[dst + 1] = g;
153
+ tmp[dst + 2] = b;
154
+ }
155
+ }
156
+ // vertical pass → [outH, outW]
157
+ const vCoef = precomputeCoeffs(inH, outH);
158
+ const out = new Uint8Array(outH * outW * 3);
159
+ for (let y = 0; y < outH; y++) {
160
+ const ymin = vCoef.bounds[y * 2]!;
161
+ const count = vCoef.bounds[y * 2 + 1]!;
162
+ for (let x = 0; x < outW; x++) {
163
+ let r = 0, g = 0, b = 0;
164
+ for (let k = 0; k < count; k++) {
165
+ const w = vCoef.weights[y * vCoef.kmax + k]!;
166
+ const src = ((ymin + k) * outW + x) * 3;
167
+ r += tmp[src]! * w;
168
+ g += tmp[src + 1]! * w;
169
+ b += tmp[src + 2]! * w;
170
+ }
171
+ const dst = (y * outW + x) * 3;
172
+ out[dst] = Math.min(255, Math.max(0, Math.round(r)));
173
+ out[dst + 1] = Math.min(255, Math.max(0, Math.round(g)));
174
+ out[dst + 2] = Math.min(255, Math.max(0, Math.round(b)));
175
+ }
176
+ }
177
+ return { width: outW, height: outH, data: out };
178
+ }
179
+
180
+ /** Port of _aspect_ratio_preserving_resize. */
181
+ export function targetSize(
182
+ width: number, height: number,
183
+ patchSize = PATCH_SIZE, poolingKernel = POOLING_KERNEL_SIZE,
184
+ ): { width: number; height: number } {
185
+ const maxPatches = NUM_SOFT_TOKENS * poolingKernel ** 2;
186
+ const targetPx = maxPatches * patchSize ** 2;
187
+ const factor = Math.sqrt(targetPx / (height * width));
188
+ const sideMult = poolingKernel * patchSize; // 48
189
+
190
+ let tH = Math.floor((factor * height) / sideMult) * sideMult;
191
+ let tW = Math.floor((factor * width) / sideMult) * sideMult;
192
+ if (tH === 0 && tW === 0) throw new Error("image resizes to 0×0");
193
+
194
+ const maxSide = Math.floor(maxPatches / poolingKernel ** 2) * sideMult;
195
+ if (tH === 0) {
196
+ tH = sideMult;
197
+ tW = Math.min(Math.floor(width / height) * sideMult, maxSide);
198
+ } else if (tW === 0) {
199
+ tW = sideMult;
200
+ tH = Math.min(Math.floor(height / width) * sideMult, maxSide);
201
+ }
202
+ return { width: tW, height: tH };
203
+ }
204
+
205
+ /** Full pipeline: bytes → padded patch vectors + grid positions. */
206
+ export async function preprocessImage(bytes: Uint8Array): Promise<PreprocessedImage> {
207
+ let img = await decodeImage(bytes);
208
+ const t = targetSize(img.width, img.height);
209
+ if (t.width !== img.width || t.height !== img.height)
210
+ img = resizeBicubic(img, t.width, t.height);
211
+
212
+ const { width, height, data } = img;
213
+ const p = MODEL_PATCH_SIZE;
214
+ const pH = Math.floor(height / p);
215
+ const pW = Math.floor(width / p);
216
+ const n = pH * pW;
217
+ if (n === 0) throw new Error("image smaller than one 48×48 patch");
218
+ if (n > NUM_SOFT_TOKENS)
219
+ throw new Error(`image yields ${n} patches > budget ${NUM_SOFT_TOKENS}`);
220
+
221
+ const patchDim = p * p * 3;
222
+ const patches = new Float32Array(NUM_SOFT_TOKENS * patchDim);
223
+ const positions = new Int32Array(NUM_SOFT_TOKENS * 2).fill(-1);
224
+
225
+ // patch vector layout matches the python reshape/transpose:
226
+ // [pH, pW, p, p, C] flattened — i.e. for patch (py, px):
227
+ // index = ((dy * p) + dx) * 3 + c over [C=channel-last]... python does
228
+ // transpose(1,3,2,4,0) from [C, pH, p, pW, p] → [pH, pW, p, p, C]
229
+ for (let py = 0; py < pH; py++) {
230
+ for (let px = 0; px < pW; px++) {
231
+ const base = (py * pW + px) * patchDim;
232
+ for (let dy = 0; dy < p; dy++) {
233
+ for (let dx = 0; dx < p; dx++) {
234
+ const src = ((py * p + dy) * width + px * p + dx) * 3;
235
+ const dst = base + (dy * p + dx) * 3;
236
+ // python rescales then patchifies in CHW; layout [p, p, C]
237
+ patches[dst] = data[src]! / 255;
238
+ patches[dst + 1] = data[src + 1]! / 255;
239
+ patches[dst + 2] = data[src + 2]! / 255;
240
+ }
241
+ }
242
+ positions[(py * pW + px) * 2] = px; // x
243
+ positions[(py * pW + px) * 2 + 1] = py; // y
244
+ }
245
+ }
246
+ return { patches, positions, softTokens: n };
247
+ }