// MiniCPM-V-4.7 (WebBrain fine-tune) in-browser runtime: ONNX Runtime Web (WebGPU) + a Transformers.js tokenizer. // // The ONNX bundle has three graphs: // onnx/embed_tokens_q4f16.onnx input_ids int64[1,S] -> inputs_embeds float32[1,S,1024] // onnx/vision_encoder_fp16.onnx pixel_values float32[n,3,H,W] -> image_features float32[n,T,1024] // onnx/decoder_model_merged_q4f16.onnx Qwen3.5-0.8B topology (GatedDeltaNet + gated attention, M-RoPE), // chunked GDN prefill, last-position logits + decision_hidden // This module reproduces MiniCPM-V-4.7's processor (slicing, PIL-compatible bicubic resize, normalization, image // placeholders) and its canvas M-RoPE position ids exactly, so the browser builds the same inputs as Transformers. // // Everything here is plain JS; `ort` (onnxruntime-web with WebGPU) and a tokenizer with `encode(text)` are injected. export const MINICPMV = { imageTokenId: 248056, imageStartId: 248078, // imageEndId: 248079, // sliceStartId: 248088, // sliceEndId: 248089, // newlineId: 198, // "\n" imagePad: '<|image_pad|>', hidden: 1024, layers: 24, fullAttentionInterval: 4, gdnConv: [6144, 4], gdnState: [16, 128, 128], kvHeads: 2, headDim: 256, eosTokenIds: [248044, 248046], scaleResolution: 448, patchSize: 14, maxSliceNums: 9, mergeFactor: 4, // "16x": 4x4 patches per LLM token }; // ------------------------------------------------------------------------------------------ python numerics /** Python 3 round(): ties to even. */ export function pyRound(x) { const r = Math.round(x); return (Math.abs(x % 1) === 0.5) ? 2 * Math.round(x / 2) : r; } const roundHalfEven = pyRound; /** torch.linspace(start, end, steps) in float32 (same symmetric formula on CPU and CUDA). */ function linspaceF32(start, end, steps) { if (steps === 1) return [Math.fround(start)]; const f = Math.fround; const s = f(start), e = f(end); const step = f(f(e - s) / (steps - 1)); const half = Math.floor(steps / 2); const out = new Array(steps); for (let i = 0; i < steps; i++) { out[i] = i < half ? f(s + f(step * i)) : f(e - f(step * (steps - i - 1))); } return out; } // ------------------------------------------------------------------------------------------ slicing (processor) function ensureDivide(length, divisor) { return Math.max(pyRound(length / divisor) * divisor, divisor); } function findBestResize(height, width, scaleResolution, patchSize, allowUpscale = false) { if (height * width > scaleResolution * scaleResolution || allowUpscale) { const aspect = width / height; height = Math.trunc(scaleResolution / Math.sqrt(aspect)); width = Math.trunc(height * aspect); } return [ensureDivide(height, patchSize * 4), ensureDivide(width, patchSize * 4)]; } function getRefineSize(height, width, grid, scaleResolution, patchSize, allowUpscale = false) { const [gridY, gridX] = grid; const refineWidth = ensureDivide(width, gridX); const refineHeight = ensureDivide(height, gridY); const [bh, bw] = findBestResize(refineHeight / gridY, refineWidth / gridX, scaleResolution, patchSize, allowUpscale); return [bh * gridY, bw * gridX]; } export function getSlicedGrid(height, width, maxSliceNums, scaleResolution) { const logRatio = Math.log(width / height); const ratio = (width * height) / (scaleResolution * scaleResolution); const multiple = Math.min(Math.ceil(ratio), maxSliceNums); if (multiple <= 1) return null; let best = [1, 1], minError = Infinity; for (const numSlices of [multiple - 1, multiple, multiple + 1]) { if (numSlices === 1 || numSlices > maxSliceNums) continue; for (let rows = 1; rows <= numSlices; rows++) { if (numSlices % rows !== 0) continue; const cols = numSlices / rows; const error = Math.abs(logRatio - Math.log(cols / rows)); if (error < minError) { best = [rows, cols]; minError = error; } else if (error === minError && rows > best[0]) best = [rows, cols]; } } return best; } // ------------------------------------------------------------------------------------------ PIL bicubic resample // Port of Pillow's ImagingResample (BICUBIC, a = -0.5, support 2, antialiased when downscaling, 22-bit fixed-point // coefficients, horizontal pass then vertical pass with uint8 rounding in between). Torch's uint8 antialiased bicubic // (what the Transformers torchvision backend calls) reproduces Pillow's integer pipeline. const PRECISION_BITS = 32 - 8 - 2; function bicubic(x) { const a = -0.5; x = Math.abs(x); if (x < 1.0) return ((a + 2.0) * x - (a + 3.0)) * x * x + 1; if (x < 2.0) return (((x - 5) * x + 8) * x - 4) * a; return 0.0; } function precomputeCoeffs(inSize, outSize) { const scale = inSize / outSize; const filterscale = Math.max(scale, 1.0); const support = 2.0 * filterscale; const ksize = Math.ceil(support) * 2 + 1; const bounds = new Int32Array(outSize * 2); const kk = new Float64Array(outSize * ksize); const ss = 1.0 / filterscale; for (let xx = 0; xx < outSize; xx++) { const center = (xx + 0.5) * scale; let ww = 0.0; let xmin = Math.trunc(center - support + 0.5); if (xmin < 0) xmin = 0; let xmax = Math.trunc(center + support + 0.5); if (xmax > inSize) xmax = inSize; xmax -= xmin; const base = xx * ksize; let x = 0; for (; x < xmax; x++) { const w = bicubic((x + xmin - center + 0.5) * ss); kk[base + x] = w; ww += w; } for (x = 0; x < xmax; x++) if (ww !== 0.0) kk[base + x] /= ww; for (; x < ksize; x++) kk[base + x] = 0; bounds[xx * 2] = xmin; bounds[xx * 2 + 1] = xmax; } // fixed point (Pillow normalize_coeffs_8bpc) const kint = new Int32Array(outSize * ksize); for (let i = 0; i < kk.length; i++) { const v = kk[i] * (1 << PRECISION_BITS); kint[i] = v < 0 ? Math.trunc(-0.5 + v) : Math.trunc(0.5 + v); } return { ksize, bounds, kint }; } function clip8(v) { // Pillow: clip8(in) = in >> PRECISION_BITS clamped to [0, 255] after adding the rounding half const r = Math.floor(v / (1 << PRECISION_BITS)); return r < 0 ? 0 : (r > 255 ? 255 : r); } /** Resize an RGB uint8 image (Uint8Array, 3 channels interleaved) with Pillow's BICUBIC. */ export function resizeBicubicRGB(src, width, height, outW, outH) { const half = 1 << (PRECISION_BITS - 1); let cur = src, cw = width, ch = height; if (outW !== cw) { const { ksize, bounds, kint } = precomputeCoeffs(cw, outW); const dst = new Uint8Array(outW * ch * 3); for (let y = 0; y < ch; y++) { for (let xx = 0; xx < outW; xx++) { const xmin = bounds[xx * 2], xmax = bounds[xx * 2 + 1], k = xx * ksize; let s0 = half, s1 = half, s2 = half; for (let x = 0; x < xmax; x++) { const p = (y * cw + x + xmin) * 3, w = kint[k + x]; s0 += cur[p] * w; s1 += cur[p + 1] * w; s2 += cur[p + 2] * w; } const o = (y * outW + xx) * 3; dst[o] = clip8(s0); dst[o + 1] = clip8(s1); dst[o + 2] = clip8(s2); } } cur = dst; cw = outW; } if (outH !== ch) { const { ksize, bounds, kint } = precomputeCoeffs(ch, outH); const dst = new Uint8Array(cw * outH * 3); for (let yy = 0; yy < outH; yy++) { const ymin = bounds[yy * 2], ymax = bounds[yy * 2 + 1], k = yy * ksize; for (let x = 0; x < cw; x++) { let s0 = half, s1 = half, s2 = half; for (let y = 0; y < ymax; y++) { const p = ((y + ymin) * cw + x) * 3, w = kint[k + y]; s0 += cur[p] * w; s1 += cur[p + 1] * w; s2 += cur[p + 2] * w; } const o = (yy * cw + x) * 3; dst[o] = clip8(s0); dst[o + 1] = clip8(s1); dst[o + 2] = clip8(s2); } } cur = dst; ch = outH; } return cur; } function cropRGB(src, width, x0, y0, w, h) { const out = new Uint8Array(w * h * 3); for (let y = 0; y < h; y++) out.set(src.subarray(((y0 + y) * width + x0) * 3, ((y0 + y) * width + x0 + w) * 3), y * w * 3); return out; } // The processor fuses rescale (1/255) and normalize (mean = std = 0.5) into (x - 127.5) / 127.5 in float32. export const NORM_LUT = (() => { const t = new Float32Array(256); for (let x = 0; x < 256; x++) t[x] = Math.fround((x - 127.5) / 127.5); return t; })(); /** RGB uint8 HWC -> normalized float32 CHW. */ function normalizeCHW(rgb, w, h) { const out = new Float32Array(3 * h * w); const plane = h * w; for (let i = 0; i < plane; i++) { for (let c = 0; c < 3; c++) out[c * plane + i] = NORM_LUT[rgb[i * 3 + c]]; } return out; } /** * MiniCPMV4_6ImageProcessor for one image. `rgb` is uint8 RGB (HWC). Returns the crops in processor order * (thumbnail, then slices row-major) with their patch grids, and the slice grid ([rows, cols] or null). */ export function preprocessImage(rgb, width, height, { maxSliceNums = MINICPMV.maxSliceNums, scaleResolution = MINICPMV.scaleResolution, patchSize = MINICPMV.patchSize, sliceMode = true } = {}) { const grid = sliceMode ? getSlicedGrid(height, width, maxSliceNums, scaleResolution) : null; const [sh, sw] = findBestResize(height, width, scaleResolution, patchSize, grid === null); const crops = []; const source = resizeBicubicRGB(rgb, width, height, sw, sh); crops.push({ h: sh / patchSize, w: sw / patchSize, width: sw, height: sh, pixels: normalizeCHW(source, sw, sh) }); if (grid) { const [rh, rw] = getRefineSize(height, width, grid, scaleResolution, patchSize, true); const refine = resizeBicubicRGB(rgb, width, height, rw, rh); const ph = Math.floor(rh / grid[0]), pw = Math.floor(rw / grid[1]); for (let i = 0; i < rh; i += ph) { for (let j = 0; j < rw; j += pw) { const piece = cropRGB(refine, rw, j, i, pw, ph); crops.push({ h: ph / patchSize, w: pw / patchSize, width: pw, height: ph, pixels: normalizeCHW(piece, pw, ph) }); } } } return { crops, grid }; } /** The processor's text replacement for the image placeholder (use_image_id, 16x downsampling). */ export function imagePlaceholder(image, localIndex = 0) { const div = MINICPMV.mergeFactor * MINICPMV.mergeFactor; const [thumb, ...slices] = image.crops; let text = `${localIndex}${MINICPMV.imagePad.repeat((thumb.h * thumb.w) / div)}`; if (image.grid && slices.length) { const [rows, cols] = image.grid; const per = (slices[0].h * slices[0].w) / div; const slice = `${MINICPMV.imagePad.repeat(per)}`; text += Array.from({ length: rows }, () => slice.repeat(cols)).join('\n'); } return text; } /** Replace each `<|image_pad|>` produced by the chat template with its image's full placeholder. */ export function expandImagePlaceholders(prompt, images) { const parts = prompt.split(MINICPMV.imagePad); if (parts.length - 1 !== images.length) throw new Error(`prompt has ${parts.length - 1} image slots for ${images.length} images`); let out = parts[0]; for (let i = 0; i < images.length; i++) out += imagePlaceholder(images[i], i) + parts[i + 1]; return out; } // ------------------------------------------------------------------------------------------ canvas M-RoPE /** * Port of MiniCPMV4_7Model.get_rope_index for one unpadded sequence. `ids` are token ids, `targetSizes` the * patch grids [h, w] of every crop in order. Returns {t, h, w} Int32Arrays and the next free position. */ export function canvasPositions(ids, targetSizes, cfg = MINICPMV) { const S = ids.length; const T = new Int32Array(S), H = new Int32Array(S), W = new Int32Array(S); for (let i = 0; i < S; i++) { T[i] = i; H[i] = i; W[i] = i; } const markers = new Set([cfg.sliceStartId, cfg.sliceEndId, cfg.imageStartId, cfg.imageEndId, cfg.newlineId]); const isVisual = id => id === cfg.imageTokenId; // runs of visual tokens = crops const groups = []; let openSlices = null, previousEnd = 0; for (let i = 0; i < S;) { if (!isVisual(ids[i])) { i++; continue; } let j = i; while (j < S && isVisual(ids[j])) j++; const crop = [i, j]; if (openSlices !== null && ids[crop[0] - 1] === cfg.sliceStartId) { openSlices.push(crop); } else { openSlices = []; const frame = { thumbStart: crop[0], thumbEnd: crop[1], slices: openSlices }; let adjacent = groups.length > 0; for (let k = previousEnd; adjacent && k < crop[0] - 1; k++) if (!markers.has(ids[k])) adjacent = false; if (adjacent) groups[groups.length - 1].frames.push(frame); else groups.push({ start: crop[0] - 1, frames: [frame] }); } previousEnd = crop[1]; i = j; } const frameEnd = last => { let e = last; while (e < S && markers.has(ids[e])) e++; return e; }; for (const g of groups) { for (const f of g.frames) f.end = frameEnd(f.slices.length ? f.slices[f.slices.length - 1][1] : f.thumbEnd); g.end = g.frames[g.frames.length - 1].end; } const set3 = (idx, t, h, w) => { T[idx] = t; H[idx] = h; W[idx] = w; }; const mf = cfg.mergeFactor; let sizeIter = 0; const nextSize = () => targetSizes[sizeIter++]; let currentPos = 0, currentIdx = 0; for (const g of groups) { if (g.start > currentIdx) { for (let k = currentIdx; k < g.start; k++) { const p = currentPos + (k - currentIdx); set3(k, p, p, p); } currentPos += g.start - currentIdx; } let cursor = g.start; for (const f of g.frames) { const thumbSize = nextSize(); const frameStart = f.thumbStart - 1; if (frameStart > cursor) { for (let k = cursor; k < frameStart; k++) { const p = currentPos + (k - cursor); set3(k, p, p, p); } currentPos += frameStart - cursor + 1; } const fEnd = Math.min(f.end, g.end); const origin = currentPos; const halo = Math.max(origin - 1, 0); let sliceH = 0, sliceW = 0, numCols = 0; let canvasH = Math.floor(thumbSize[0] / mf), canvasW = Math.floor(thumbSize[1] / mf); let firstSlice = null; if (f.slices.length) { numCols = f.slices.length; for (let k = 0; k < f.slices.length - 1; k++) { if (f.slices[k + 1][0] - f.slices[k][1] > 2) { numCols = k + 1; break; } } let numRows = numCols > 0 ? Math.floor(f.slices.length / numCols) : 1; if (numRows * numCols !== f.slices.length) { numRows = 1; numCols = f.slices.length; } firstSlice = nextSize(); sliceH = Math.floor(firstSlice[0] / mf); sliceW = Math.floor(firstSlice[1] / mf); canvasH = numRows * sliceH; canvasW = numCols * sliceW; } for (let k = frameStart; k < fEnd; k++) set3(k, origin, origin, origin); H[frameStart] = halo; W[frameStart] = halo; if (f.thumbEnd < fEnd) { H[f.thumbEnd] = origin + canvasH; W[f.thumbEnd] = origin + canvasW; } // thumbnail stretched over the canvas { const gh = Math.floor(thumbSize[0] / mf), gw = Math.floor(thumbSize[1] / mf); const hc = linspaceF32(0, canvasH - 1, gh).map(roundHalfEven), wc = linspaceF32(0, canvasW - 1, gw).map(roundHalfEven); let k = f.thumbStart; for (let a = 0; a < gh; a++) for (let b = 0; b < gw; b++, k++) set3(k, origin, origin + hc[a], origin + wc[b]); } f.slices.forEach(([s0, s1], k) => { const hOff = Math.floor(k / numCols) * sliceH, wOff = (k % numCols) * sliceW; const size = k === 0 ? firstSlice : nextSize(); const sh = Math.floor(size[0] / mf), sw = Math.floor(size[1] / mf); if (s0 - 1 >= frameStart) { H[s0 - 1] = origin + hOff; W[s0 - 1] = origin + wOff; } if (s1 < fEnd) { H[s1] = origin + hOff + sh - 1; W[s1] = origin + wOff + sw - 1; } let idx = s0; for (let a = 0; a < sh; a++) for (let b = 0; b < sw; b++, idx++) set3(idx, origin, origin + hOff + a, origin + wOff + b); }); for (let k = 0; k < f.slices.length - 1; k++) { const gs = f.slices[k][1], ge = f.slices[k + 1][0]; if (ge - gs <= 2) continue; const boundaryH = (Math.floor(k / numCols) + 1) * sliceH - 1; const rightEdge = numCols * sliceW; for (let n = gs + 1; n < ge - 1; n++) { H[n] = origin + boundaryH; W[n] = origin + rightEdge; } } currentPos = origin + Math.max(canvasH, canvasW) + 1; cursor = fEnd; } if (cursor < g.end) { for (let k = cursor; k < g.end; k++) { const p = currentPos + (k - cursor); set3(k, p, p, p); } currentPos += g.end - cursor; } currentIdx = g.end; } for (let k = currentIdx; k < S; k++) { const p = currentPos + (k - currentIdx); set3(k, p, p, p); } let max = 0; for (let i = 0; i < S; i++) max = Math.max(max, T[i], H[i], W[i]); return { t: T, h: H, w: W, nextPosition: max + 1 }; } // ------------------------------------------------------------------------------------------ ORT session runtime const f16Zeros = n => new Uint16Array(n); export class MiniCPMVRuntime { /** * @param ort onnxruntime-web module (WebGPU build) * @param tokenizer object with encode(text) -> number[] (special tokens recognised, no BOS) * @param urls { embed, vision, decoder } graph URLs (external data `_data` sits next to each) */ constructor(ort, tokenizer, sessions, cfg = MINICPMV) { this.ort = ort; this.tokenizer = tokenizer; this.embed = sessions.embed; this.vision = sessions.vision; this.decoder = sessions.decoder; this.cfg = cfg; this.layerKinds = Array.from({ length: cfg.layers }, (_, i) => ((i + 1) % cfg.fullAttentionInterval === 0 ? 'attn' : 'gdn')); } static async create(ort, tokenizer, urls, { executionProviders = ['webgpu'], fetchData = null } = {}) { const make = async url => { const name = url.split('/').pop(); const options = { executionProviders, graphOptimizationLevel: 'all' }; const data = fetchData ? await fetchData(`${url}_data`) : null; options.externalData = [{ path: `${name}_data`, data: data || `${url}_data` }]; if (executionProviders.includes('webgpu')) options.preferredOutputLocation = 'gpu-buffer'; return ort.InferenceSession.create(url, options); }; // ORT Web's WebGPU EP creates one session at a time const embed = await make(urls.embed); const vision = await make(urls.vision); const decoder = await make(urls.decoder); return new MiniCPMVRuntime(ort, tokenizer, { embed, vision, decoder }); } tensor(type, data, dims) { return new this.ort.Tensor(type, data, dims); } async embedIds(ids) { const out = await this.embed.run({ input_ids: this.tensor('int64', BigInt64Array.from(ids.map(BigInt)), [1, ids.length]) }); const t = out.inputs_embeds; const data = t.location === 'gpu-buffer' ? await t.getData(true) : t.data; return new Float32Array(data.buffer ? data : Float32Array.from(data)); } /** Vision features for one preprocessed image, in placeholder order (thumbnail, slices). */ async encodeImage(image) { const groups = new Map(); image.crops.forEach((c, i) => { const key = `${c.height}x${c.width}`; if (!groups.has(key)) groups.set(key, []); groups.get(key).push(i); }); const perCrop = new Array(image.crops.length); for (const idx of groups.values()) { const c0 = image.crops[idx[0]]; const plane = 3 * c0.height * c0.width; const batch = new Float32Array(plane * idx.length); idx.forEach((ci, b) => batch.set(image.crops[ci].pixels, b * plane)); const out = await this.vision.run({ pixel_values: this.tensor('float32', batch, [idx.length, 3, c0.height, c0.width]) }); const t = out.image_features; const data = t.location === 'gpu-buffer' ? await t.getData(true) : t.data; const per = (c0.h / this.cfg.mergeFactor) * (c0.w / this.cfg.mergeFactor) * this.cfg.hidden; idx.forEach((ci, b) => { perCrop[ci] = data.subarray(b * per, (b + 1) * per); }); } return perCrop; } /** Build prompt inputs: ids, embeddings (image rows filled), canvas positions. */ async buildInputs(promptText, images = []) { const text = images.length ? expandImagePlaceholders(promptText, images) : promptText; const ids = this.tokenizer.encode(text); const embeds = await this.embedIds(ids); if (images.length) { const feats = []; for (const image of images) feats.push(...await this.encodeImage(image)); let row = 0, fi = 0, off = 0; const H = this.cfg.hidden; for (let i = 0; i < ids.length; i++) { if (ids[i] !== this.cfg.imageTokenId) continue; while (fi < feats.length && off >= feats[fi].length) { fi++; off = 0; } if (fi >= feats.length) throw new Error('more image placeholders than vision features'); embeds.set(feats[fi].subarray(off, off + H), i * H); off += H; row++; } const total = feats.reduce((s, f) => s + f.length / H, 0); if (row !== total) throw new Error(`image placeholders ${row} != vision features ${total}`); } const sizes = images.flatMap(im => im.crops.map(c => [c.h, c.w])); const pos = canvasPositions(ids, sizes, this.cfg); return { ids, embeds, pos, sizes }; } emptyCache() { const feeds = {}; const [convC, convK] = this.cfg.gdnConv, [hs, dk, dv] = this.cfg.gdnState; this.layerKinds.forEach((kind, i) => { if (kind === 'gdn') { feeds[`past_conv.${i}`] = this.tensor('float16', f16Zeros(convC * convK), [1, convC, convK]); feeds[`past_recurrent.${i}`] = this.tensor('float16', f16Zeros(hs * dk * dv), [1, hs, dk, dv]); } else { feeds[`past_key_values.${i}.key`] = this.tensor('float16', f16Zeros(0), [1, this.cfg.kvHeads, 0, this.cfg.headDim]); feeds[`past_key_values.${i}.value`] = this.tensor('float16', f16Zeros(0), [1, this.cfg.kvHeads, 0, this.cfg.headDim]); } }); return { feeds, length: 0 }; } /** * Run the decoder over `embeds` rows [start, end) with positions from `pos`, continuing `cache`. * Returns last-position logits (Float32Array), decision_hidden (Float32Array) and the new cache. */ async forward(embeds, pos, start, end, cache, { keepCache = true } = {}) { const n = end - start, H = this.cfg.hidden; const posData = new BigInt64Array(3 * n); for (let i = 0; i < n; i++) { posData[i] = BigInt(pos.t[start + i]); posData[n + i] = BigInt(pos.h[start + i]); posData[2 * n + i] = BigInt(pos.w[start + i]); } const total = cache.length + n; const feeds = { ...cache.feeds, inputs_embeds: this.tensor('float32', embeds.slice(start * H, end * H), [1, n, H]), attention_mask: this.tensor('int64', new BigInt64Array(total).fill(1n), [1, total]), position_ids: this.tensor('int64', posData, [3, 1, n]), }; const out = await this.decoder.run(feeds); const read = async t => { if (!t) return null; const d = t.location === 'gpu-buffer' ? await t.getData() : t.data; return d instanceof Uint16Array ? f16ToF32(d) : Float32Array.from(d); }; const logits = await read(out.logits); const hidden = await read(out.decision_hidden); const next = { feeds: {}, length: total }; for (const [name, t] of Object.entries(out)) { if (name.startsWith('present')) { const past = name.replace('present_conv', 'past_conv').replace('present_recurrent', 'past_recurrent').replace('present.', 'past_key_values.'); if (keepCache) next.feeds[past] = t; else t.dispose?.(); } else if (name !== 'logits' && name !== 'decision_hidden') t.dispose?.(); } out.logits?.dispose?.(); out.decision_hidden?.dispose?.(); return { logits, hidden, cache: next }; } /** * Greedy generation. `promptText` is the rendered chat prompt (each image as one `<|image_pad|>`); returns token ids. * New tokens continue the canvas positions: position = max(prompt positions) + 1 + step on all three axes. */ async generate(promptText, images = [], { maxNewTokens = 128, stopIds = this.cfg.eosTokenIds, onToken = null } = {}) { const inputs = await this.buildInputs(promptText, images); let { logits, cache } = await this.forward(inputs.embeds, inputs.pos, 0, inputs.ids.length, this.emptyCache()); let next = inputs.pos.nextPosition; const out = []; try { for (let step = 0; step < maxNewTokens; step++) { let best = 0; for (let i = 1; i < logits.length; i++) if (logits[i] > logits[best]) best = i; if (stopIds.includes(best)) break; out.push(best); onToken?.(best); const emb = await this.embedIds([best]); const p = { t: Int32Array.of(next), h: Int32Array.of(next), w: Int32Array.of(next) }; next += 1; const r = await this.forward(emb, p, 0, 1, cache); MiniCPMVRuntime.disposeCache(cache); ({ logits, cache } = r); } } finally { MiniCPMVRuntime.disposeCache(cache); } return { ids: out, promptTokens: inputs.ids.length }; } static disposeCache(cache) { for (const t of Object.values(cache?.feeds || {})) t.dispose?.(); } } /** IEEE half -> float. */ export function f16ToF32(u16) { const out = new Float32Array(u16.length); for (let i = 0; i < u16.length; i++) { const h = u16[i], s = (h & 0x8000) ? -1 : 1, e = (h >> 10) & 0x1f, m = h & 0x3ff; out[i] = e === 0 ? s * m * 2 ** -24 : (e === 31 ? (m ? NaN : s * Infinity) : s * (1 + m / 1024) * 2 ** (e - 15)); } return out; }