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.
- package/LICENSE +21 -0
- package/README.md +362 -0
- package/THIRD_PARTY_LICENSES.md +41 -0
- package/bin/mlx-bun.mjs +32 -0
- package/docs/curve-designer.html +303 -0
- package/docs/design/adapters-end-to-end.md +201 -0
- package/docs/design/compat-cli-surface-design.md +227 -0
- package/docs/design/docs-reorg-plan.md +152 -0
- package/docs/design/hlg-sampling.md +340 -0
- package/docs/design/memory-system.md +339 -0
- package/docs/design/optimization_plan.md +344 -0
- package/docs/design/parallel-slots.md +515 -0
- package/docs/design/runtime-isolation.md +247 -0
- package/docs/design/segmented-backward-training.md +508 -0
- package/docs/investigations/chunk-finetune-experiment.md +84 -0
- package/docs/investigations/curve-bisector-routing.md +123 -0
- package/docs/investigations/curve-runs/bisector-route.json +98 -0
- package/docs/investigations/curve-runs/bisector-trace.json +1393 -0
- package/docs/investigations/curve-runs/curve-terrain.json +1179 -0
- package/docs/investigations/curve-runs/curve-terrain.svg +42 -0
- package/docs/investigations/curve-terrain.html +105 -0
- package/docs/investigations/expert-offload-single-user-moe.md +292 -0
- package/docs/investigations/hlg-ab.html +34 -0
- package/docs/investigations/hlg-explorer.html +238 -0
- package/docs/investigations/hlg-figs/cliff.svg +1 -0
- package/docs/investigations/hlg-figs/cross.svg +1 -0
- package/docs/investigations/hlg-figs/edges.svg +1 -0
- package/docs/investigations/hlg-figs/k20-grids.svg +1 -0
- package/docs/investigations/hlg-figs/k20-peaks.svg +1 -0
- package/docs/investigations/hlg-figs/lw-curve.svg +1 -0
- package/docs/investigations/hlg-figs/terrain.svg +1 -0
- package/docs/investigations/hlg-report.html +262 -0
- package/docs/investigations/hlg-runs/hlg-ab.json +149 -0
- package/docs/investigations/hlg-runs/hlg-blind-prompts.md +87 -0
- package/docs/investigations/hlg-runs/hlg-blind.json +107 -0
- package/docs/investigations/hlg-runs/hlg-cliff.json +197 -0
- package/docs/investigations/hlg-runs/hlg-cross.json +505 -0
- package/docs/investigations/hlg-runs/hlg-edge.json +388 -0
- package/docs/investigations/hlg-runs/hlg-eval1.json +25 -0
- package/docs/investigations/hlg-runs/hlg-frontier.json +86 -0
- package/docs/investigations/hlg-runs/hlg-grid.json +242 -0
- package/docs/investigations/hlg-runs/hlg-grid2.json +343 -0
- package/docs/investigations/hlg-runs/hlg-lw-coarse.json +110 -0
- package/docs/investigations/hlg-runs/hlg-map.json +383 -0
- package/docs/investigations/hlg-runs/run0-original.log +159 -0
- package/docs/investigations/hlg-runs/runA-recommended.log +131 -0
- package/docs/investigations/hlg-runs/runAA-cliff-cpm5.log +26 -0
- package/docs/investigations/hlg-runs/runAB-grid2-e4b.log +18 -0
- package/docs/investigations/hlg-runs/runAC-peak-e4b-partial.log +25 -0
- package/docs/investigations/hlg-runs/runAD-ab-samples.log +8 -0
- package/docs/investigations/hlg-runs/runAE-edge-e4b.log +51 -0
- package/docs/investigations/hlg-runs/runAF-logit-calibration.log +10 -0
- package/docs/investigations/hlg-runs/runAG-eval-config2.log +16 -0
- package/docs/investigations/hlg-runs/runB-gain-variants.log +192 -0
- package/docs/investigations/hlg-runs/runC-pivots.log +162 -0
- package/docs/investigations/hlg-runs/runD-oetf.log +132 -0
- package/docs/investigations/hlg-runs/runE-eotf.log +162 -0
- package/docs/investigations/hlg-runs/runF-shaper-working.log +86 -0
- package/docs/investigations/hlg-runs/runG-shaper-sweep.log +101 -0
- package/docs/investigations/hlg-runs/runH-wsweep.log +176 -0
- package/docs/investigations/hlg-runs/runI-asweep.log +176 -0
- package/docs/investigations/hlg-runs/runJ-knobmap.log +49 -0
- package/docs/investigations/hlg-runs/runK-frontier.log +19 -0
- package/docs/investigations/hlg-runs/runL-widemap.log +65 -0
- package/docs/investigations/hlg-runs/runM-frontier-emb.log +15 -0
- package/docs/investigations/hlg-runs/runN-verify.log +116 -0
- package/docs/investigations/hlg-runs/runO-frontier-seed5000.log +15 -0
- package/docs/investigations/hlg-runs/runP-grid-AxSm.log +15 -0
- package/docs/investigations/hlg-runs/runQ-grid-tgW.log +15 -0
- package/docs/investigations/hlg-runs/runR-blind-transcript.log +79 -0
- package/docs/investigations/hlg-runs/runS-cross-coherence.log +33 -0
- package/docs/investigations/hlg-runs/runT-cross-cpm5.log +33 -0
- package/docs/investigations/hlg-runs/runU-cross-12b.log +33 -0
- package/docs/investigations/hlg-runs/runV-lw-e4b.log +22 -0
- package/docs/investigations/hlg-runs/runW-lw-12b.log +22 -0
- package/docs/investigations/hlg-runs/runX-lw-cpm5.log +22 -0
- package/docs/investigations/hlg-runs/runY-cliff-e4b.log +26 -0
- package/docs/investigations/hlg-runs/runZ-cliff-12b.log +26 -0
- package/docs/investigations/hlg-sampling-investigation.md +970 -0
- package/docs/investigations/lab-build-journal.md +157 -0
- package/docs/investigations/pi-builtin-investigation.md +308 -0
- package/docs/investigations/starter-model-port-handoff.md +323 -0
- package/docs/planning/IDEAS.md +26 -0
- package/docs/planning/PRODUCT_ROADMAP.md +426 -0
- package/docs/planning/ResearchTopics.md +119 -0
- package/docs/planning/chunk-task-roadmap.md +77 -0
- package/docs/planning/journal.md +155 -0
- package/docs/reference/distribution.md +121 -0
- package/docs/reference/embedding.md +168 -0
- package/docs/reference/library-api.md +153 -0
- package/docs/reference/server-api.md +224 -0
- package/docs/reference/server-config.md +243 -0
- package/docs/segmented-backward-handoff.md +148 -0
- package/package.json +68 -0
- package/src/anthropic.ts +491 -0
- package/src/chat-template.ts +186 -0
- package/src/cli.ts +1075 -0
- package/src/config.ts +229 -0
- package/src/curve-sampler.ts +122 -0
- package/src/dataset/generators.ts +1087 -0
- package/src/dataset/index.ts +12 -0
- package/src/dataset/job.ts +81 -0
- package/src/dataset/llm.ts +71 -0
- package/src/dataset/registry.ts +528 -0
- package/src/download.ts +283 -0
- package/src/eval/README.md +86 -0
- package/src/eval/kl-prompts.ts +29 -0
- package/src/eval/kl.ts +294 -0
- package/src/eval/quality-db.ts +92 -0
- package/src/eval/runner.ts +102 -0
- package/src/eval/score.ts +49 -0
- package/src/eval/tasks/bfcl.ts +500 -0
- package/src/eval/tasks/gsm8k.ts +85 -0
- package/src/eval/tasks/hashhop.ts +109 -0
- package/src/eval/tasks/humaneval.ts +252 -0
- package/src/eval/tasks/ifeval.ts +354 -0
- package/src/eval/tasks/mmlu.ts +145 -0
- package/src/evaldb.ts +199 -0
- package/src/expert-offload-build.ts +77 -0
- package/src/expert-offload.ts +61 -0
- package/src/expert-trace.ts +67 -0
- package/src/fit.ts +254 -0
- package/src/generate.ts +451 -0
- package/src/harness-pi.ts +177 -0
- package/src/hf-push.ts +628 -0
- package/src/index.ts +22 -0
- package/src/jobs/db.ts +139 -0
- package/src/jobs/index.ts +29 -0
- package/src/jobs/job-entry.ts +106 -0
- package/src/jobs/runner.ts +304 -0
- package/src/jobs/sse.ts +114 -0
- package/src/jobs/types.ts +58 -0
- package/src/kv-store.ts +185 -0
- package/src/lora.ts +314 -0
- package/src/md-modules.d.ts +7 -0
- package/src/mlx/array.ts +237 -0
- package/src/mlx/autograd.ts +267 -0
- package/src/mlx/checkpoint.ts +116 -0
- package/src/mlx/compile.ts +158 -0
- package/src/mlx/custom-vjp.ts +140 -0
- package/src/mlx/ffi.ts +433 -0
- package/src/mlx/metal-kernel.ts +130 -0
- package/src/mlx/ops.ts +707 -0
- package/src/mmap.ts +90 -0
- package/src/model/batched-mask.ts +204 -0
- package/src/model/batched-rotating.ts +300 -0
- package/src/model/compiled-decode.ts +681 -0
- package/src/model/factory.ts +27 -0
- package/src/model/fingerprint.ts +33 -0
- package/src/model/flash-attention.ts +674 -0
- package/src/model/fused-decode-kernel.ts +305 -0
- package/src/model/fused-geglu-kernel.ts +87 -0
- package/src/model/gemma4-base.ts +1616 -0
- package/src/model/gemma4.ts +1012 -0
- package/src/model/generated/gemma4-12b.ts +526 -0
- package/src/model/generated/gemma4-26b.ts +500 -0
- package/src/model/generated/gemma4-e4b.ts +757 -0
- package/src/model/generated/index.ts +18 -0
- package/src/model/minicpm5.ts +278 -0
- package/src/model/qwen3-delta.ts +224 -0
- package/src/model/qwen3_5.ts +429 -0
- package/src/model/support.ts +46 -0
- package/src/native-pack.ts +152 -0
- package/src/pi-provider.ts +95 -0
- package/src/pi-terminal.ts +264 -0
- package/src/pi-web.ts +924 -0
- package/src/preflight.ts +123 -0
- package/src/prompt-cache.ts +113 -0
- package/src/quantize/allocator.ts +627 -0
- package/src/quantize/calibration.ts +180 -0
- package/src/quantize/config-writer.ts +132 -0
- package/src/quantize/index.ts +57 -0
- package/src/quantize/job.ts +136 -0
- package/src/quantize/quantizer.ts +462 -0
- package/src/quantize/safetensors-writer.ts +171 -0
- package/src/quantize/sensitivity.ts +234 -0
- package/src/registry.ts +336 -0
- package/src/responses.ts +774 -0
- package/src/safetensors.ts +158 -0
- package/src/sampler.ts +517 -0
- package/src/serve/batch-scheduler.ts +362 -0
- package/src/serve/generation-gateway.ts +160 -0
- package/src/server.ts +1891 -0
- package/src/spec/drafter.ts +261 -0
- package/src/spec/generate.ts +221 -0
- package/src/status-page.html +497 -0
- package/src/tokenizer.ts +38 -0
- package/src/tool-call.ts +242 -0
- package/src/train/dataset.ts +461 -0
- package/src/train/export.ts +32 -0
- package/src/train/forward.ts +229 -0
- package/src/train/index.ts +43 -0
- package/src/train/job.ts +148 -0
- package/src/train/lora-params.ts +194 -0
- package/src/train/loss.ts +342 -0
- package/src/train/merge.ts +195 -0
- package/src/train/optimizer.ts +176 -0
- package/src/train/rank.ts +101 -0
- package/src/train/segmented.ts +520 -0
- package/src/train/trainer.ts +600 -0
- package/src/tui.ts +161 -0
- package/src/vision/embedder.ts +168 -0
- package/src/vision/preprocess.ts +247 -0
- package/src/vision/prompt.ts +158 -0
- package/src/vision/siglip.ts +599 -0
- package/src/web/app.html +2471 -0
- package/src/web/skills/web-research/SKILL.md +44 -0
- package/src/web/skills.ts +59 -0
- package/src/web-tools.ts +426 -0
- 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
|
+
}
|