diskformer 0.5.2 → 0.5.3
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/CHANGELOG.md +8 -0
- package/engines/gemma4_moe_ssd.js +3 -286
- package/engines/minicpmv_vision.js +506 -0
- package/engines/qwen35_moe_ssd.js +478 -66
- package/engines/qwen3_moe_ssd.js +307 -9
- package/package.json +1 -1
|
@@ -0,0 +1,506 @@
|
|
|
1
|
+
/* minicpmv_vision.js — MiniCPM-V 4.7's vision encoder (mmproj GGUF, projector `minicpmv4_7`) on WebGPU.
|
|
2
|
+
*
|
|
3
|
+
* One image tile in, its token embeddings out (64 × 2048 for a 448 × 448 tile), ready to splice into the text model.
|
|
4
|
+
* The math follows llama.cpp a0e6dc738 tools/mtmd/models/minicpmv.cpp (clip_graph_minicpmv4_6::build) and the index
|
|
5
|
+
* tables in clip.cpp, and is gated against scripts/minicpmv-vision-ref.py (float64 numpy). Activations are f32; the
|
|
6
|
+
* BF16 weights are read as stored (a BF16 value is the top half of an f32, so the shader widens it exactly).
|
|
7
|
+
*
|
|
8
|
+
* const v = await MiniCpmVVision.load(device, { read: (off, len) => Promise<Uint8Array>, size });
|
|
9
|
+
* const { tokens, dim, buffer } = await v.encode(pixels, width, height); // pixels: Float32Array, H × W × 3
|
|
10
|
+
* const f32 = await v.readback(buffer, tokens * dim);
|
|
11
|
+
*
|
|
12
|
+
* Graph per tile (patch grid ph × pw, both even and their halves even):
|
|
13
|
+
* patch conv 14×14 + bias + position embedding (SigLIP 70×70 buckets)
|
|
14
|
+
* ViT layers 0..insert (LayerNorm → attention 16×72 → residual; LayerNorm → FFN tanh-GELU → residual)
|
|
15
|
+
* merger: LayerNorm → attention inside each 2×2 window → residual; 2×2 downsample: LayerNorm(concat) → MLP
|
|
16
|
+
* (tanh-GELU) + mean of the four
|
|
17
|
+
* ViT layers insert+1..26 on the quarter grid; post LayerNorm
|
|
18
|
+
* final merger: 2×2 concat → LayerNorm → MLP (erf-GELU) → 2048
|
|
19
|
+
*/
|
|
20
|
+
import { parseGguf } from '../src/gguf.js';
|
|
21
|
+
|
|
22
|
+
const STORAGE = 0x80, COPY_SRC = 0x04, COPY_DST = 0x08, UNIFORM = 0x40, MAP_READ = 0x01;
|
|
23
|
+
const F32 = 0, BF16 = 30;
|
|
24
|
+
|
|
25
|
+
const BF = 'fn bf(w: u32, hi: bool) -> f32 { return bitcast<f32>(select(w << 16u, w & 0xffff0000u, hi)); }';
|
|
26
|
+
|
|
27
|
+
const K = {
|
|
28
|
+
// y[p][o] = bias[o] + Σ_{c,ky,kx} W[o][c][ky][kx] · img[(py·14+ky)][(px·14+kx)][c] + posE[bucket(p)][o]
|
|
29
|
+
patch: `
|
|
30
|
+
struct P { ph: u32, pw: u32, C: u32, W: u32 }
|
|
31
|
+
@group(0) @binding(0) var<storage, read> img: array<f32>;
|
|
32
|
+
@group(0) @binding(1) var<storage, read> w: array<f32>;
|
|
33
|
+
@group(0) @binding(2) var<storage, read> bias: array<f32>;
|
|
34
|
+
@group(0) @binding(3) var<storage, read> posE: array<f32>;
|
|
35
|
+
@group(0) @binding(4) var<storage, read_write> y: array<f32>;
|
|
36
|
+
@group(0) @binding(5) var<uniform> p: P;
|
|
37
|
+
@compute @workgroup_size(64) fn main(@builtin(global_invocation_id) g: vec3<u32>) {
|
|
38
|
+
let o = g.x; let pi = g.y; if (o >= p.C) { return; }
|
|
39
|
+
let py = pi / p.pw; let px = pi % p.pw;
|
|
40
|
+
var acc = bias[o];
|
|
41
|
+
for (var c = 0u; c < 3u; c++) {
|
|
42
|
+
for (var ky = 0u; ky < 14u; ky++) {
|
|
43
|
+
let row = (py * 14u + ky) * p.W + px * 14u;
|
|
44
|
+
let wb = ((o * 3u + c) * 14u + ky) * 14u;
|
|
45
|
+
for (var kx = 0u; kx < 14u; kx++) { acc += w[wb + kx] * img[(row + kx) * 3u + c]; }
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
let bucket = ((70u * py) / p.ph) * 70u + (70u * px) / p.pw;
|
|
49
|
+
y[pi * p.C + o] = acc + posE[bucket * p.C + o];
|
|
50
|
+
}`,
|
|
51
|
+
// LayerNorm with weight and bias, one workgroup per row.
|
|
52
|
+
layerNorm: `
|
|
53
|
+
struct P { n: u32, eps: f32 }
|
|
54
|
+
@group(0) @binding(0) var<storage, read> x: array<f32>;
|
|
55
|
+
@group(0) @binding(1) var<storage, read> w: array<f32>;
|
|
56
|
+
@group(0) @binding(2) var<storage, read> b: array<f32>;
|
|
57
|
+
@group(0) @binding(3) var<storage, read_write> y: array<f32>;
|
|
58
|
+
@group(0) @binding(4) var<uniform> p: P;
|
|
59
|
+
var<workgroup> red: array<f32, 256>;
|
|
60
|
+
@compute @workgroup_size(256) fn main(@builtin(workgroup_id) wg: vec3<u32>, @builtin(local_invocation_id) l: vec3<u32>) {
|
|
61
|
+
let base = wg.x * p.n; let t = l.x;
|
|
62
|
+
var s = 0.0;
|
|
63
|
+
for (var i = t; i < p.n; i += 256u) { s += x[base + i]; }
|
|
64
|
+
red[t] = s; workgroupBarrier();
|
|
65
|
+
for (var k = 128u; k > 0u; k >>= 1u) { if (t < k) { red[t] += red[t + k]; } workgroupBarrier(); }
|
|
66
|
+
let mean = red[0] / f32(p.n); workgroupBarrier();
|
|
67
|
+
var v = 0.0;
|
|
68
|
+
for (var i = t; i < p.n; i += 256u) { let d = x[base + i] - mean; v += d * d; }
|
|
69
|
+
red[t] = v; workgroupBarrier();
|
|
70
|
+
for (var k = 128u; k > 0u; k >>= 1u) { if (t < k) { red[t] += red[t + k]; } workgroupBarrier(); }
|
|
71
|
+
let inv = 1.0 / sqrt(red[0] / f32(p.n) + p.eps);
|
|
72
|
+
for (var i = t; i < p.n; i += 256u) { y[base + i] = (x[base + i] - mean) * inv * w[i] + b[i]; }
|
|
73
|
+
}`,
|
|
74
|
+
// Y[t·M + m] = bias[m] + Σ_k X[t·N + k] · W[m·N + k], W in BF16 (two per u32, element 2i in the low half).
|
|
75
|
+
// 16 rows × 16 tokens per workgroup through shared memory, 32-wide k blocks; the last block is guarded (the FFN's
|
|
76
|
+
// K = 4304 is not a multiple of 32). Dispatch (ceil(M/16), ceil(T/16)).
|
|
77
|
+
gemm: `
|
|
78
|
+
struct P { M: u32, N: u32, T: u32, pad: u32 }
|
|
79
|
+
@group(0) @binding(0) var<storage, read> x: array<f32>;
|
|
80
|
+
@group(0) @binding(1) var<storage, read> w: array<u32>;
|
|
81
|
+
@group(0) @binding(2) var<storage, read> bias: array<f32>;
|
|
82
|
+
@group(0) @binding(3) var<storage, read_write> y: array<f32>;
|
|
83
|
+
@group(0) @binding(4) var<uniform> p: P;
|
|
84
|
+
${BF}
|
|
85
|
+
var<workgroup> ws: array<f32, 512>;
|
|
86
|
+
var<workgroup> xs: array<f32, 512>;
|
|
87
|
+
@compute @workgroup_size(256) fn main(@builtin(workgroup_id) wg: vec3<u32>, @builtin(local_invocation_id) l: vec3<u32>) {
|
|
88
|
+
let m0 = wg.x * 16u; let t0 = wg.y * 16u; let r = l.x / 16u; let c = l.x % 16u;
|
|
89
|
+
var acc = 0.0;
|
|
90
|
+
for (var k0 = 0u; k0 < p.N; k0 += 32u) {
|
|
91
|
+
for (var i = l.x; i < 512u; i += 256u) {
|
|
92
|
+
let rr = i / 32u; let kk = i % 32u;
|
|
93
|
+
let m = m0 + rr; var wv = 0.0;
|
|
94
|
+
let kin = k0 + kk < p.N;
|
|
95
|
+
if (m < p.M && kin) { let e = m * p.N + k0 + kk; wv = bf(w[e >> 1u], (e & 1u) == 1u); }
|
|
96
|
+
ws[i] = wv;
|
|
97
|
+
let t = t0 + rr; var xv = 0.0;
|
|
98
|
+
if (t < p.T && kin) { xv = x[t * p.N + k0 + kk]; }
|
|
99
|
+
xs[i] = xv;
|
|
100
|
+
}
|
|
101
|
+
workgroupBarrier();
|
|
102
|
+
for (var kk = 0u; kk < 32u; kk++) { acc += ws[r * 32u + kk] * xs[c * 32u + kk]; }
|
|
103
|
+
workgroupBarrier();
|
|
104
|
+
}
|
|
105
|
+
let m = m0 + r; let t = t0 + c;
|
|
106
|
+
if (m < p.M && t < p.T) { y[t * p.M + m] = acc + bias[m]; }
|
|
107
|
+
}`,
|
|
108
|
+
// S[h][i][j] = scale · q_i·k_j for head h (d = 72); with win = 1, keys outside query i's 2×2 window get -inf.
|
|
109
|
+
scores: `
|
|
110
|
+
struct P { n: u32, C: u32, heads: u32, dh: u32, scale: f32, win: u32, pw: u32, pad: u32 }
|
|
111
|
+
@group(0) @binding(0) var<storage, read> q: array<f32>;
|
|
112
|
+
@group(0) @binding(1) var<storage, read> k: array<f32>;
|
|
113
|
+
@group(0) @binding(2) var<storage, read_write> s: array<f32>;
|
|
114
|
+
@group(0) @binding(3) var<uniform> p: P;
|
|
115
|
+
fn winOf(i: u32) -> u32 { return ((i / p.pw) / 2u) * (p.pw / 2u) + (i % p.pw) / 2u; }
|
|
116
|
+
@compute @workgroup_size(64) fn main(@builtin(global_invocation_id) g: vec3<u32>) {
|
|
117
|
+
let j = g.x; let i = g.y; let h = g.z;
|
|
118
|
+
if (j >= p.n) { return; }
|
|
119
|
+
let o = (h * p.n + i) * p.n + j;
|
|
120
|
+
if (p.win == 1u && winOf(i) != winOf(j)) { s[o] = -3.4e38; return; }
|
|
121
|
+
var acc = 0.0;
|
|
122
|
+
let qb = i * p.C + h * p.dh; let kb = j * p.C + h * p.dh;
|
|
123
|
+
for (var d = 0u; d < p.dh; d++) { acc += q[qb + d] * k[kb + d]; }
|
|
124
|
+
s[o] = acc * p.scale;
|
|
125
|
+
}`,
|
|
126
|
+
softmax: `
|
|
127
|
+
struct P { n: u32 }
|
|
128
|
+
@group(0) @binding(0) var<storage, read_write> s: array<f32>;
|
|
129
|
+
@group(0) @binding(1) var<uniform> p: P;
|
|
130
|
+
var<workgroup> red: array<f32, 256>;
|
|
131
|
+
@compute @workgroup_size(256) fn main(@builtin(workgroup_id) wg: vec3<u32>, @builtin(local_invocation_id) l: vec3<u32>) {
|
|
132
|
+
let base = wg.x * p.n; let t = l.x;
|
|
133
|
+
var m = -3.4e38;
|
|
134
|
+
for (var i = t; i < p.n; i += 256u) { m = max(m, s[base + i]); }
|
|
135
|
+
red[t] = m; workgroupBarrier();
|
|
136
|
+
for (var k = 128u; k > 0u; k >>= 1u) { if (t < k) { red[t] = max(red[t], red[t + k]); } workgroupBarrier(); }
|
|
137
|
+
let mx = red[0]; workgroupBarrier();
|
|
138
|
+
var sum = 0.0;
|
|
139
|
+
for (var i = t; i < p.n; i += 256u) { let e = select(exp(s[base + i] - mx), 0.0, s[base + i] <= -3.0e38); s[base + i] = e; sum += e; }
|
|
140
|
+
red[t] = sum; workgroupBarrier();
|
|
141
|
+
for (var k = 128u; k > 0u; k >>= 1u) { if (t < k) { red[t] += red[t + k]; } workgroupBarrier(); }
|
|
142
|
+
let inv = 1.0 / red[0];
|
|
143
|
+
for (var i = t; i < p.n; i += 256u) { s[base + i] *= inv; }
|
|
144
|
+
}`,
|
|
145
|
+
// O[i][h·72 + d] = Σ_j P[h][i][j] · v[j][h·72 + d]
|
|
146
|
+
attnOut: `
|
|
147
|
+
struct P { n: u32, C: u32, heads: u32, dh: u32 }
|
|
148
|
+
@group(0) @binding(0) var<storage, read> s: array<f32>;
|
|
149
|
+
@group(0) @binding(1) var<storage, read> v: array<f32>;
|
|
150
|
+
@group(0) @binding(2) var<storage, read_write> o: array<f32>;
|
|
151
|
+
@group(0) @binding(3) var<uniform> p: P;
|
|
152
|
+
@compute @workgroup_size(64) fn main(@builtin(global_invocation_id) g: vec3<u32>) {
|
|
153
|
+
let c = g.x; let i = g.y; if (c >= p.C) { return; }
|
|
154
|
+
let h = c / p.dh; let sb = (h * p.n + i) * p.n;
|
|
155
|
+
var acc = 0.0;
|
|
156
|
+
for (var j = 0u; j < p.n; j++) { acc += s[sb + j] * v[j * p.C + c]; }
|
|
157
|
+
o[i * p.C + c] = acc;
|
|
158
|
+
}`,
|
|
159
|
+
geluTanh: `
|
|
160
|
+
struct P { n: u32 }
|
|
161
|
+
@group(0) @binding(0) var<storage, read_write> x: array<f32>;
|
|
162
|
+
@group(0) @binding(1) var<uniform> p: P;
|
|
163
|
+
@compute @workgroup_size(256) fn main(@builtin(global_invocation_id) g: vec3<u32>, @builtin(num_workgroups) ng: vec3<u32>) {
|
|
164
|
+
let i = g.y * ng.x * 256u + g.x; if (i >= p.n) { return; }
|
|
165
|
+
let v = x[i];
|
|
166
|
+
x[i] = 0.5 * v * (1.0 + tanh(clamp(0.7978845608028654 * (v + 0.044715 * v * v * v), -10.0, 10.0)));
|
|
167
|
+
}`,
|
|
168
|
+
// erf by Abramowitz–Stegun 7.1.26 (absolute error under 1.5e-7).
|
|
169
|
+
geluErf: `
|
|
170
|
+
struct P { n: u32 }
|
|
171
|
+
@group(0) @binding(0) var<storage, read_write> x: array<f32>;
|
|
172
|
+
@group(0) @binding(1) var<uniform> p: P;
|
|
173
|
+
fn erf_(z: f32) -> f32 {
|
|
174
|
+
let a = abs(z); let t = 1.0 / (1.0 + 0.3275911 * a);
|
|
175
|
+
let y = 1.0 - (((((1.061405429 * t - 1.453152027) * t) + 1.421413741) * t - 0.284496736) * t + 0.254829592) * t * exp(-a * a);
|
|
176
|
+
return select(-y, y, z >= 0.0);
|
|
177
|
+
}
|
|
178
|
+
@compute @workgroup_size(256) fn main(@builtin(global_invocation_id) g: vec3<u32>, @builtin(num_workgroups) ng: vec3<u32>) {
|
|
179
|
+
let i = g.y * ng.x * 256u + g.x; if (i >= p.n) { return; }
|
|
180
|
+
let v = x[i];
|
|
181
|
+
x[i] = 0.5 * v * (1.0 + erf_(v * 0.7071067811865476));
|
|
182
|
+
}`,
|
|
183
|
+
add: `
|
|
184
|
+
struct P { n: u32 }
|
|
185
|
+
@group(0) @binding(0) var<storage, read_write> y: array<f32>;
|
|
186
|
+
@group(0) @binding(1) var<storage, read> x: array<f32>;
|
|
187
|
+
@group(0) @binding(2) var<uniform> p: P;
|
|
188
|
+
@compute @workgroup_size(256) fn main(@builtin(global_invocation_id) g: vec3<u32>, @builtin(num_workgroups) ng: vec3<u32>) {
|
|
189
|
+
let i = g.y * ng.x * 256u + g.x; if (i >= p.n) { return; }
|
|
190
|
+
y[i] += x[i];
|
|
191
|
+
}`,
|
|
192
|
+
// 2×2 gather on a gh × gw grid: cat[q] = [x(2i,2j), x(2i,2j+1), x(2i+1,2j), x(2i+1,2j+1)] (4C wide),
|
|
193
|
+
// mean[q] = their average (written when withMean = 1). q = i·(gw/2) + j.
|
|
194
|
+
gather: `
|
|
195
|
+
struct P { gw: u32, C: u32, nq: u32, withMean: u32 }
|
|
196
|
+
@group(0) @binding(0) var<storage, read> x: array<f32>;
|
|
197
|
+
@group(0) @binding(1) var<storage, read_write> cat: array<f32>;
|
|
198
|
+
@group(0) @binding(2) var<storage, read_write> mean: array<f32>;
|
|
199
|
+
@group(0) @binding(3) var<uniform> p: P;
|
|
200
|
+
@compute @workgroup_size(64) fn main(@builtin(global_invocation_id) g: vec3<u32>) {
|
|
201
|
+
let c = g.x; let q = g.y; if (c >= p.C || q >= p.nq) { return; }
|
|
202
|
+
let hw = p.gw / 2u; let i = q / hw; let j = q % hw;
|
|
203
|
+
let a = x[((2u * i) * p.gw + 2u * j) * p.C + c];
|
|
204
|
+
let b = x[((2u * i) * p.gw + 2u * j + 1u) * p.C + c];
|
|
205
|
+
let d = x[((2u * i + 1u) * p.gw + 2u * j) * p.C + c];
|
|
206
|
+
let e = x[((2u * i + 1u) * p.gw + 2u * j + 1u) * p.C + c];
|
|
207
|
+
let cb = q * 4u * p.C;
|
|
208
|
+
cat[cb + c] = a; cat[cb + p.C + c] = b; cat[cb + 2u * p.C + c] = d; cat[cb + 3u * p.C + c] = e;
|
|
209
|
+
if (p.withMean == 1u) { mean[q * p.C + c] = (a + b + d + e) * 0.25; }
|
|
210
|
+
}`,
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
// ── Preprocessing: llama.cpp a0e6dc738's MiniCPM-V path (mtmd-image.cpp: mtmd_image_preprocessor_minicpmv over
|
|
214
|
+
// llava_uhd), so a PNG gives the same pixels as llama-mtmd-cli. Slice size 448, alignment 56 (patch 14 × 4), sizes
|
|
215
|
+
// rounded half to even (Python's round), at most 9 slices, Pillow's bicubic (a = -0.5, 22-bit fixed point), no padding.
|
|
216
|
+
const SLICE = 448, ALIGN = 56, MAX_SLICES = 9;
|
|
217
|
+
export const roundEven = (v) => { const f = Math.floor(v), d = v - f; return d > 0.5 ? f + 1 : d < 0.5 ? f : (f % 2 === 0 ? f : f + 1); };
|
|
218
|
+
const ensureDivide = (len, a) => Math.max(roundEven(len / a) * a, a);
|
|
219
|
+
// get_best_resize: float32 arithmetic as the C++ does it.
|
|
220
|
+
function bestResize(w, h, res, a, upscale) {
|
|
221
|
+
if (w * h > res * res || upscale) {
|
|
222
|
+
const r = Math.fround(w / h);
|
|
223
|
+
h = Math.trunc(Math.fround(res / Math.fround(Math.sqrt(r))));
|
|
224
|
+
w = Math.trunc(Math.fround(h * r));
|
|
225
|
+
}
|
|
226
|
+
return [ensureDivide(w, a), ensureDivide(h, a)];
|
|
227
|
+
}
|
|
228
|
+
function bestGrid(multiple, logRatio) {
|
|
229
|
+
const cands = [];
|
|
230
|
+
for (const n of [multiple - 1, multiple, multiple + 1]) {
|
|
231
|
+
if (n === 1 || n > MAX_SLICES) continue;
|
|
232
|
+
for (let m = 1; m <= n; m++) if (n % m === 0) cands.push([m, n / m]);
|
|
233
|
+
}
|
|
234
|
+
let best = [1, 1], minErr = Infinity;
|
|
235
|
+
for (const g of cands) { const err = Math.fround(Math.abs(logRatio - Math.log(g[0] / g[1]))); if (err < minErr) { best = g; minErr = err; } }
|
|
236
|
+
return best;
|
|
237
|
+
}
|
|
238
|
+
// The tiles of a w × h image: { overview: [w, h], grid: [cols, rows] | null, cell: [w, h], refined: [w, h] }.
|
|
239
|
+
export function planImage(w, h) {
|
|
240
|
+
const ratio = Math.fround(Math.fround(w * h) / (SLICE * SLICE));
|
|
241
|
+
if (ratio <= 1) return { overview: bestResize(w, h, SLICE, ALIGN, true), grid: null, cell: null, refined: null };
|
|
242
|
+
const overview = bestResize(w, h, SLICE, ALIGN, false);
|
|
243
|
+
const grid = bestGrid(Math.min(Math.ceil(ratio), MAX_SLICES), Math.fround(Math.log(Math.fround(w / h))));
|
|
244
|
+
const rw = ensureDivide(w, grid[0]), rh = ensureDivide(h, grid[1]);
|
|
245
|
+
const cell = bestResize(Math.trunc(rw / grid[0]), Math.trunc(rh / grid[1]), SLICE, ALIGN, true);
|
|
246
|
+
return { overview, grid, cell, refined: [cell[0] * grid[0], cell[1] * grid[1]] };
|
|
247
|
+
}
|
|
248
|
+
// Pillow-compatible separable bicubic resize of packed RGB bytes (img_tool::resize_pillow).
|
|
249
|
+
export function resizeBicubic(src, sw, sh, tw, th) {
|
|
250
|
+
if (sw === tw && sh === th) return src;
|
|
251
|
+
const PB = 22, filter = (x) => { x = Math.abs(x); const a = -0.5; return x < 1 ? ((a + 2) * x - (a + 3)) * x * x + 1 : x < 2 ? (((x - 5) * x + 8) * x - 4) * a : 0; };
|
|
252
|
+
const weights = (inSize, outSize) => {
|
|
253
|
+
const scale = inSize / outSize, fs = Math.max(1, scale), support = 2 * fs, ksize = Math.ceil(support) * 2 + 1;
|
|
254
|
+
const bounds = new Int32Array(outSize * 2), k = new Int32Array(outSize * ksize), pre = new Float64Array(ksize);
|
|
255
|
+
for (let xx = 0; xx < outSize; xx++) {
|
|
256
|
+
const center = (xx + 0.5) * scale, ss = 1 / fs;
|
|
257
|
+
let xmin = Math.trunc(center - support + 0.5); if (xmin < 0) xmin = 0;
|
|
258
|
+
let xmax = Math.trunc(center + support + 0.5); if (xmax > inSize) xmax = inSize;
|
|
259
|
+
xmax -= xmin;
|
|
260
|
+
let ww = 0;
|
|
261
|
+
for (let x = 0; x < xmax; x++) { const w = filter((x + xmin - center + 0.5) * ss); pre[x] = w; ww += w; }
|
|
262
|
+
for (let x = 0; x < ksize; x++) {
|
|
263
|
+
const w = x < xmax ? (ww !== 0 ? pre[x] / ww : pre[x]) : 0;
|
|
264
|
+
k[xx * ksize + x] = Math.trunc(w * 4194304 + (w < 0 ? -0.5 : 0.5));
|
|
265
|
+
}
|
|
266
|
+
bounds[xx * 2] = xmin; bounds[xx * 2 + 1] = xmax;
|
|
267
|
+
}
|
|
268
|
+
return { bounds, k, ksize };
|
|
269
|
+
};
|
|
270
|
+
const clip8 = (v) => (v < 0 ? 0 : v > 255 ? 255 : v);
|
|
271
|
+
let cur = src, cw = sw;
|
|
272
|
+
if (tw !== sw) {
|
|
273
|
+
const { bounds, k, ksize } = weights(sw, tw), out = new Uint8Array(tw * sh * 3);
|
|
274
|
+
for (let y = 0; y < sh; y++) for (let xx = 0; xx < tw; xx++) {
|
|
275
|
+
const xmin = bounds[xx * 2], cnt = bounds[xx * 2 + 1], kb = xx * ksize;
|
|
276
|
+
let s0 = 1 << (PB - 1), s1 = s0, s2 = s0;
|
|
277
|
+
for (let x = 0; x < cnt; x++) { const p = (y * sw + xmin + x) * 3, w = k[kb + x]; s0 += cur[p] * w; s1 += cur[p + 1] * w; s2 += cur[p + 2] * w; }
|
|
278
|
+
const o = (y * tw + xx) * 3; out[o] = clip8(s0 >> PB); out[o + 1] = clip8(s1 >> PB); out[o + 2] = clip8(s2 >> PB);
|
|
279
|
+
}
|
|
280
|
+
cur = out; cw = tw;
|
|
281
|
+
}
|
|
282
|
+
if (th !== sh) {
|
|
283
|
+
const { bounds, k, ksize } = weights(sh, th), row = cw * 3, out = new Uint8Array(row * th), acc = new Float64Array(row);
|
|
284
|
+
for (let yy = 0; yy < th; yy++) {
|
|
285
|
+
const ymin = bounds[yy * 2], cnt = bounds[yy * 2 + 1], kb = yy * ksize;
|
|
286
|
+
acc.fill(1 << (PB - 1));
|
|
287
|
+
for (let y = 0; y < cnt; y++) { const w = k[kb + y], rb = (ymin + y) * row; for (let i = 0; i < row; i++) acc[i] += cur[rb + i] * w; }
|
|
288
|
+
for (let i = 0; i < row; i++) out[yy * row + i] = clip8(acc[i] >> PB);
|
|
289
|
+
}
|
|
290
|
+
cur = out;
|
|
291
|
+
}
|
|
292
|
+
return cur;
|
|
293
|
+
}
|
|
294
|
+
// (x / 255 - 0.5) / 0.5 in float32, as clip_image_f32::normalize.
|
|
295
|
+
function normalize(rgb) {
|
|
296
|
+
const out = new Float32Array(rgb.length);
|
|
297
|
+
for (let i = 0; i < rgb.length; i++) out[i] = Math.fround(Math.fround(Math.fround(rgb[i] / 255) - 0.5) / 0.5);
|
|
298
|
+
return out;
|
|
299
|
+
}
|
|
300
|
+
function crop(src, sw, x0, y0, w, h) {
|
|
301
|
+
const out = new Uint8Array(w * h * 3);
|
|
302
|
+
for (let y = 0; y < h; y++) out.set(src.subarray(((y0 + y) * sw + x0) * 3, ((y0 + y) * sw + x0 + w) * 3), y * w * 3);
|
|
303
|
+
return out;
|
|
304
|
+
}
|
|
305
|
+
// An image (packed RGB bytes, w × h) → its tiles, overview first then slices row by row, each { pixels, width, height },
|
|
306
|
+
// and the slice grid ([cols, rows], or null for an overview-only image).
|
|
307
|
+
export function preprocessImage(rgb, w, h) {
|
|
308
|
+
const plan = planImage(w, h);
|
|
309
|
+
const tiles = [{ width: plan.overview[0], height: plan.overview[1], pixels: normalize(resizeBicubic(rgb, w, h, plan.overview[0], plan.overview[1])) }];
|
|
310
|
+
if (plan.grid) {
|
|
311
|
+
const [rw, rh] = plan.refined, refined = resizeBicubic(rgb, w, h, rw, rh), [cw, ch] = plan.cell;
|
|
312
|
+
for (let r = 0; r < plan.grid[1]; r++) for (let c = 0; c < plan.grid[0]; c++) tiles.push({ width: cw, height: ch, pixels: normalize(crop(refined, rw, c * cw, r * ch, cw, ch)) });
|
|
313
|
+
}
|
|
314
|
+
return { tiles, grid: plan.grid, plan };
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
export class MiniCpmVVision {
|
|
318
|
+
// read(off, len) → Uint8Array of the mmproj GGUF's bytes [off, off + len). size: the file's byte length.
|
|
319
|
+
static async load(device, { read, onProgress = () => {} } = {}) {
|
|
320
|
+
const head = await read(0, 8 << 20);
|
|
321
|
+
const gguf = parseGguf(head);
|
|
322
|
+
const v = new MiniCpmVVision(device, gguf);
|
|
323
|
+
await v.upload(read, onProgress);
|
|
324
|
+
return v;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// The mmproj kept in OPFS (localmind-ssd/<name>/mmproj.gguf, with a manifest naming its source so Settings → Models
|
|
328
|
+
// lists it under the model): downloaded once in 64 MB ranges with fetchFn, each range committed, so an interrupted
|
|
329
|
+
// download resumes. Then load() reads from the stored file.
|
|
330
|
+
static async loadCached(device, { url, size, source = null, name, fetch: fetchFn = (u, i) => fetch(u, i), onProgress = () => {} } = {}) {
|
|
331
|
+
const root = await navigator.storage.getDirectory();
|
|
332
|
+
const dir = await (await root.getDirectoryHandle('localmind-ssd', { create: true })).getDirectoryHandle(name, { create: true });
|
|
333
|
+
const fh = await dir.getFileHandle('mmproj.gguf', { create: true });
|
|
334
|
+
let have = (await fh.getFile()).size;
|
|
335
|
+
if (have > size) { const w = await fh.createWritable(); await w.truncate(0); await w.close(); have = 0; }
|
|
336
|
+
const CH = 64 << 20;
|
|
337
|
+
while (have < size) {
|
|
338
|
+
const end = Math.min(size, have + CH) - 1;
|
|
339
|
+
const r = await fetchFn(url, { headers: { Range: `bytes=${have}-${end}` } });
|
|
340
|
+
if (r.status !== 206 && !(r.status === 200 && have === 0 && end === size - 1)) throw new Error(`vision weights: HTTP ${r.status} for bytes ${have}-${end}`);
|
|
341
|
+
const buf = new Uint8Array(await r.arrayBuffer());
|
|
342
|
+
if (buf.byteLength !== end - have + 1) throw new Error(`vision weights: got ${buf.byteLength} of ${end - have + 1} bytes`);
|
|
343
|
+
const w = await fh.createWritable({ keepExistingData: true });
|
|
344
|
+
await w.seek(have); await w.write(buf); await w.close();
|
|
345
|
+
have += buf.byteLength;
|
|
346
|
+
onProgress({ status: 'vision-download', loaded: have, total: size });
|
|
347
|
+
}
|
|
348
|
+
if (source) {
|
|
349
|
+
const m = await dir.getFileHandle('manifest.json', { create: true }), w = await m.createWritable();
|
|
350
|
+
await w.write(JSON.stringify({ source, file: 'mmproj.gguf', size })); await w.close();
|
|
351
|
+
}
|
|
352
|
+
const file = await fh.getFile();
|
|
353
|
+
const read = async (off, len) => new Uint8Array(await file.slice(off, off + len).arrayBuffer());
|
|
354
|
+
return MiniCpmVVision.load(device, { read, onProgress });
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
constructor(device, gguf) {
|
|
358
|
+
const kv = gguf.kv || gguf.metadata;
|
|
359
|
+
if (kv['clip.projector_type'] !== 'minicpmv4_7') throw new Error(`MiniCpmVVision needs a minicpmv4_7 mmproj, got ${kv['clip.projector_type']}`);
|
|
360
|
+
this.device = device;
|
|
361
|
+
this.gguf = gguf;
|
|
362
|
+
this.cfg = {
|
|
363
|
+
C: kv['clip.vision.embedding_length'], heads: kv['clip.vision.attention.head_count'], layers: kv['clip.vision.block_count'],
|
|
364
|
+
patch: kv['clip.vision.patch_size'], eps: kv['clip.vision.attention.layer_norm_epsilon'],
|
|
365
|
+
insert: kv['clip.vision.wa_layer_indexes'][0], imageSize: kv['clip.vision.image_size'],
|
|
366
|
+
out: 0,
|
|
367
|
+
};
|
|
368
|
+
if (this.cfg.patch !== 14) throw new Error(`patch kernel assumes 14×14 patches, got ${this.cfg.patch}`);
|
|
369
|
+
this.pipelines = {};
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
pipeline(name) {
|
|
373
|
+
return (this.pipelines[name] ||= this.device.createComputePipeline({ layout: 'auto', compute: { module: this.device.createShaderModule({ code: K[name] }), entryPoint: 'main' } }));
|
|
374
|
+
}
|
|
375
|
+
bind(name, bufs) {
|
|
376
|
+
return this.device.createBindGroup({ layout: this.pipeline(name).getBindGroupLayout(0), entries: bufs.map((b, i) => ({ binding: i, resource: { buffer: b } })) });
|
|
377
|
+
}
|
|
378
|
+
uniform(vals) {
|
|
379
|
+
const ab = new ArrayBuffer(Math.max(16, Math.ceil(vals.length / 4) * 16)), u = new Uint32Array(ab), f = new Float32Array(ab);
|
|
380
|
+
vals.forEach((x, i) => { if (typeof x === 'object') f[i] = x.f; else u[i] = x; });
|
|
381
|
+
const b = this.device.createBuffer({ size: ab.byteLength, usage: UNIFORM | COPY_DST });
|
|
382
|
+
this.device.queue.writeBuffer(b, 0, ab);
|
|
383
|
+
(this.tmp ||= []).push(b);
|
|
384
|
+
return b;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
async upload(read, onProgress) {
|
|
388
|
+
const dev = this.device, g = this.gguf;
|
|
389
|
+
dev.pushErrorScope('validation');
|
|
390
|
+
for (const k of Object.keys(K)) this.pipeline(k);
|
|
391
|
+
const err = await dev.popErrorScope();
|
|
392
|
+
if (err) throw new Error(`minicpmv vision WGSL: ${err.message}`);
|
|
393
|
+
this.w = {};
|
|
394
|
+
let done = 0, bytes = 0;
|
|
395
|
+
for (const t of g.tensors) {
|
|
396
|
+
const n = t.dims.reduce((a, b) => a * b, 1);
|
|
397
|
+
if (t.type !== F32 && t.type !== BF16) throw new Error(`${t.name}: unsupported type ${t.type}`);
|
|
398
|
+
const len = n * (t.type === F32 ? 4 : 2);
|
|
399
|
+
const data = await read(g.dataStart + t.offset, len);
|
|
400
|
+
if (data.byteLength !== len || len % 4) throw new Error(`${t.name}: read ${data.byteLength} of ${len} bytes`);
|
|
401
|
+
const buf = dev.createBuffer({ size: len, usage: STORAGE | COPY_DST | COPY_SRC });
|
|
402
|
+
dev.queue.writeBuffer(buf, 0, data);
|
|
403
|
+
this.w[t.name] = buf;
|
|
404
|
+
bytes += len; done++;
|
|
405
|
+
onProgress({ status: 'vision-weights', loaded: done, total: g.tensors.length, bytes });
|
|
406
|
+
}
|
|
407
|
+
this.cfg.out = this.gguf.tensors.find((t) => t.name === 'mm.down.weight').dims[1];
|
|
408
|
+
const sep = g.tensors.find((t) => t.name === 'v.tok_embd_sep');
|
|
409
|
+
this.sepRows = sep ? sep.dims[1] : 0;
|
|
410
|
+
this.gpuBytes = bytes;
|
|
411
|
+
await dev.queue.onSubmittedWorkDone();
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// The separator embeddings (`v.tok_embd_sep`, rows 0 = </image>, 1 = <slice>, 2 = </slice>, 3 = \n) as one Float32Array.
|
|
415
|
+
async separators() {
|
|
416
|
+
if (!this.sepRows) return null;
|
|
417
|
+
return new Float32Array(await this.readback(this.w['v.tok_embd_sep'], this.sepRows * this.cfg.out));
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// pixels: Float32Array, height × width × 3, as the encoder sees them (normalised). width and height must be
|
|
421
|
+
// multiples of 56 (14-pixel patches, then two 2×2 merges). Returns a GPU buffer of tokens × out floats.
|
|
422
|
+
async encode(pixels, width, height, { capture = null } = {}) {
|
|
423
|
+
const dev = this.device, c = this.cfg, C = c.C, P = c.patch;
|
|
424
|
+
if (width % (4 * P) || height % (4 * P)) throw new Error(`image ${width}×${height}: sides must be multiples of ${4 * P}`);
|
|
425
|
+
const ph = height / P, pw = width / P, N = ph * pw, Nq = N / 4, No = N / 16, dh = C / c.heads;
|
|
426
|
+
const dimOf = (name) => this.gguf.tensors.find((t) => t.name === name).dims[1];
|
|
427
|
+
const FF = dimOf('v.blk.0.ffn_up.weight'), DF = dimOf('v.vit_merger.ds_ffn_up.weight'), UF = dimOf('mm.up.weight');
|
|
428
|
+
const A = (n) => { const b = dev.createBuffer({ size: Math.max(16, n * 4), usage: STORAGE | COPY_SRC | COPY_DST }); this.tmp.push(b); return b; };
|
|
429
|
+
this.tmp = [];
|
|
430
|
+
const img = A(pixels.length); dev.queue.writeBuffer(img, 0, pixels);
|
|
431
|
+
const x = A(N * C), h = A(N * C), q = A(N * C), k = A(N * C), v = A(N * C), att = A(N * C), o = A(N * C);
|
|
432
|
+
const ff = A(Math.max(N * FF, Nq * DF, No * UF)), sc = A(c.heads * N * N);
|
|
433
|
+
const cat = A(Nq * 4 * C), catN = A(Nq * 4 * C), mean = A(Nq * C), xq = A(Nq * C), out = dev.createBuffer({ size: No * c.out * 4, usage: STORAGE | COPY_SRC });
|
|
434
|
+
const W = this.w;
|
|
435
|
+
const enc = dev.createCommandEncoder();
|
|
436
|
+
const run = (name, bufs, x1, y1 = 1, z1 = 1) => { const p = enc.beginComputePass(); p.setPipeline(this.pipeline(name)); p.setBindGroup(0, this.bind(name, bufs)); p.dispatchWorkgroups(x1, y1, z1); p.end(); };
|
|
437
|
+
const flat = (n) => { const wg = Math.ceil(n / 256); const xw = Math.min(wg, 65535); return [xw, Math.ceil(wg / xw)]; };
|
|
438
|
+
const ln = (src, dst, rows, n, pre) => run('layerNorm', [src, W[pre + '.weight'], W[pre + '.bias'], dst, this.uniform([n, { f: c.eps }])], rows);
|
|
439
|
+
const lin = (src, dst, T, Kd, M, pre) => run('gemm', [src, W[pre + '.weight'], W[pre + '.bias'], dst, this.uniform([M, Kd, T, 0])], Math.ceil(M / 16), Math.ceil(T / 16));
|
|
440
|
+
const add = (dst, src, n) => run('add', [dst, src, this.uniform([n])], ...flat(n));
|
|
441
|
+
const gelu = (buf, n, kind) => run(kind, [buf, this.uniform([n])], ...flat(n));
|
|
442
|
+
const snap = (name, buf, n) => { if (capture) { const r = dev.createBuffer({ size: n * 4, usage: COPY_DST | MAP_READ }); enc.copyBufferToBuffer(buf, 0, r, 0, n * 4); capture[name] = r; this.tmp.push(r); } };
|
|
443
|
+
const dbg = (il, name, buf, n) => { if (il === 0 && capture && capture.layer0) snap('l0-' + name, buf, n); };
|
|
444
|
+
const attention = (T, gridW, pre, win, il = -1) => {
|
|
445
|
+
lin(h, q, T, C, C, pre + '.attn_q'); lin(h, k, T, C, C, pre + '.attn_k'); lin(h, v, T, C, C, pre + '.attn_v');
|
|
446
|
+
dbg(il, 'h', h, T * C); dbg(il, 'q', q, T * C);
|
|
447
|
+
run('scores', [q, k, sc, this.uniform([T, C, c.heads, dh, { f: 1 / Math.sqrt(dh) }, win ? 1 : 0, gridW, 0])], Math.ceil(T / 64), T, c.heads);
|
|
448
|
+
run('softmax', [sc, this.uniform([T])], c.heads * T);
|
|
449
|
+
run('attnOut', [sc, v, att, this.uniform([T, C, c.heads, dh])], Math.ceil(C / 64), T);
|
|
450
|
+
dbg(il, 'att', att, T * C);
|
|
451
|
+
lin(att, o, T, C, C, pre + '.attn_out');
|
|
452
|
+
add(x, o, T * C);
|
|
453
|
+
dbg(il, 'x1', x, T * C);
|
|
454
|
+
};
|
|
455
|
+
const layer = (il, T, gridW) => {
|
|
456
|
+
const pre = `v.blk.${il}`;
|
|
457
|
+
ln(x, h, T, C, pre + '.ln1');
|
|
458
|
+
attention(T, gridW, pre, false, il);
|
|
459
|
+
ln(x, h, T, C, pre + '.ln2');
|
|
460
|
+
lin(h, ff, T, C, FF, pre + '.ffn_up'); dbg(il, 'u', ff, T * FF);
|
|
461
|
+
gelu(ff, T * FF, 'geluTanh'); lin(ff, o, T, FF, C, pre + '.ffn_down');
|
|
462
|
+
add(x, o, T * C);
|
|
463
|
+
dbg(il, 'x2', x, T * C);
|
|
464
|
+
};
|
|
465
|
+
|
|
466
|
+
run('patch', [img, W['v.patch_embd.weight'], W['v.patch_embd.bias'], W['v.position_embd.weight'], x, this.uniform([ph, pw, C, width])], Math.ceil(C / 64), N);
|
|
467
|
+
snap('patch', x, N * C);
|
|
468
|
+
for (let il = 0; il <= c.insert; il++) layer(il, N, pw);
|
|
469
|
+
snap(`l${c.insert}`, x, N * C);
|
|
470
|
+
// merger: window attention over 2×2 windows, then the 2×2 downsample (tanh-GELU MLP + mean)
|
|
471
|
+
ln(x, h, N, C, 'v.vit_merger.ln1');
|
|
472
|
+
attention(N, pw, 'v.vit_merger', true);
|
|
473
|
+
snap('merger_attn', x, N * C);
|
|
474
|
+
run('gather', [x, cat, mean, this.uniform([pw, C, Nq, 1])], Math.ceil(C / 64), Nq);
|
|
475
|
+
ln(cat, catN, Nq, 4 * C, 'v.vit_merger.ds_ln');
|
|
476
|
+
lin(catN, ff, Nq, 4 * C, DF, 'v.vit_merger.ds_ffn_up'); gelu(ff, Nq * DF, 'geluTanh');
|
|
477
|
+
lin(ff, xq, Nq, DF, C, 'v.vit_merger.ds_ffn_down');
|
|
478
|
+
add(xq, mean, Nq * C);
|
|
479
|
+
enc.copyBufferToBuffer(xq, 0, x, 0, Nq * C * 4);
|
|
480
|
+
snap('ds', x, Nq * C);
|
|
481
|
+
for (let il = c.insert + 1; il < c.layers; il++) layer(il, Nq, pw / 2);
|
|
482
|
+
snap('last', x, Nq * C);
|
|
483
|
+
ln(x, h, Nq, C, 'v.post_ln');
|
|
484
|
+
snap('post_ln', h, Nq * C);
|
|
485
|
+
// final merger: 2×2 concat on the half grid → LayerNorm → erf-GELU MLP → out
|
|
486
|
+
run('gather', [h, cat, mean, this.uniform([pw / 2, C, No, 0])], Math.ceil(C / 64), No);
|
|
487
|
+
ln(cat, catN, No, 4 * C, 'mm.input_norm');
|
|
488
|
+
lin(catN, ff, No, 4 * C, UF, 'mm.up'); gelu(ff, No * UF, 'geluErf');
|
|
489
|
+
lin(ff, out, No, UF, c.out, 'mm.down');
|
|
490
|
+
dev.queue.submit([enc.finish()]);
|
|
491
|
+
await dev.queue.onSubmittedWorkDone();
|
|
492
|
+
if (capture) for (const [name, b] of Object.entries(capture)) { if (!(b instanceof GPUBuffer)) continue; await b.mapAsync(1); capture[name] = new Float32Array(b.getMappedRange().slice(0)); b.unmap(); }
|
|
493
|
+
for (const b of this.tmp) b.destroy();
|
|
494
|
+
this.tmp = [];
|
|
495
|
+
return { tokens: No, dim: c.out, buffer: out };
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
async readback(buffer, n) {
|
|
499
|
+
const dev = this.device, r = dev.createBuffer({ size: n * 4, usage: COPY_DST | MAP_READ });
|
|
500
|
+
const enc = dev.createCommandEncoder(); enc.copyBufferToBuffer(buffer, 0, r, 0, n * 4); dev.queue.submit([enc.finish()]);
|
|
501
|
+
await r.mapAsync(1); const a = new Float32Array(r.getMappedRange().slice(0)); r.unmap(); r.destroy();
|
|
502
|
+
return a;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
dispose() { for (const b of Object.values(this.w || {})) b.destroy(); this.w = null; }
|
|
506
|
+
}
|