Download webgpu-memory-opt.html from ameraino11/baberu-ocr-webgpu: direct link, hf CLI and curl.
- Browser
- Download file 18.6 kB
-
https://huggingface.co/ameraino11/baberu-ocr-webgpu/resolve/main/webgpu-memory-opt.html
- Command line
-
hf download hf://ameraino11/baberu-ocr-webgpu/webgpu-memory-opt.html
-
curl -L -o webgpu-memory-opt.html https://huggingface.co/ameraino11/baberu-ocr-webgpu/resolve/main/webgpu-memory-opt.html
18.6 kB
| <meta charset="utf-8" /> | |
| <title>Baberu capability-preserving WebGPU memory experiment</title> | |
| <style> | |
| body { background: #111; color: #eee; font: 14px/1.45 ui-monospace, monospace; margin: 24px; } | |
| pre { white-space: pre-wrap; } | |
| </style> | |
| <h1>Baberu capability-preserving WebGPU memory experiment</h1> | |
| <pre id="log">Starting…</pre> | |
| <canvas id="canvas" width="224" height="224" hidden></canvas> | |
| <script type="module"> | |
| const parameters = new URLSearchParams(location.search); | |
| const tier = parameters.get("tier") ?? "121"; | |
| const memoryMode = parameters.get("memory") ?? "lean"; | |
| const loaderMode = parameters.get("loader") ?? "url"; | |
| const scheduleMode = parameters.get("schedule") ?? "staged"; | |
| const decoderMode = parameters.get("decoder") ?? "baseline"; | |
| const benchmarkScope = parameters.get("scope") ?? "full"; | |
| const decoderOnly = benchmarkScope === "decoder"; | |
| const maxTokens = 128; | |
| if (!new Set(["121", "242"]).has(tier)) throw new Error(`Unknown tier ${tier}`); | |
| if (!new Set(["default", "lean"]).has(memoryMode)) throw new Error(`Unknown memory mode ${memoryMode}`); | |
| if (!new Set(["url", "bytes"]).has(loaderMode)) throw new Error(`Unknown loader ${loaderMode}`); | |
| if (!new Set(["resident", "staged"]).has(scheduleMode)) throw new Error(`Unknown schedule ${scheduleMode}`); | |
| if (!new Set(["baseline", "gather-opt", "fp16-matmul", "fixed-kv"]).has(decoderMode)) throw new Error(`Unknown decoder ${decoderMode}`); | |
| if (!new Set(["full", "decoder"]).has(benchmarkScope)) throw new Error(`Unknown scope ${benchmarkScope}`); | |
| const ort = await import("./.work/ort/ort-webgpu.mjs"); | |
| const output = document.querySelector("#log"); | |
| const canvas = document.querySelector("#canvas"); | |
| const context = canvas.getContext("2d", { willReadFrequently: true }); | |
| const logLines = []; | |
| const memorySamples = []; | |
| const log = (message) => { | |
| logLines.push(`${new Date().toISOString()} ${message}`); | |
| output.textContent = logLines.join("\n"); | |
| console.log(message); | |
| }; | |
| window.addEventListener("error", (event) => log(`FAIL ${event.error?.stack ?? event.message}`)); | |
| window.addEventListener("unhandledrejection", (event) => log(`FAIL ${event.reason?.stack ?? event.reason}`)); | |
| const sampleMemory = async (label) => { | |
| const sample = { label, atMs: performance.now() }; | |
| if (performance.memory) { | |
| sample.jsHeapSizeLimit = performance.memory.jsHeapSizeLimit; | |
| sample.totalJSHeapSize = performance.memory.totalJSHeapSize; | |
| sample.usedJSHeapSize = performance.memory.usedJSHeapSize; | |
| } | |
| if (typeof performance.measureUserAgentSpecificMemory === "function") { | |
| try { | |
| sample.userAgentBytes = (await performance.measureUserAgentSpecificMemory()).bytes; | |
| } catch (error) { | |
| sample.userAgentMemoryError = String(error); | |
| } | |
| } | |
| memorySamples.push(sample); | |
| log(`MEMORY ${JSON.stringify(sample)}`); | |
| return sample; | |
| }; | |
| const runId = `tier-${tier}-${memoryMode}-${loaderMode}-${scheduleMode}-${decoderMode}-${benchmarkScope}-${Date.now()}`; | |
| await fetch("/benchmark-start", { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ runId }), | |
| }); | |
| const markPhase = async (phase) => { | |
| const response = await fetch("/benchmark-mark", { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ runId, phase }), | |
| }); | |
| if (!response.ok) throw new Error(`Failed marking ${phase}: HTTP ${response.status}`); | |
| }; | |
| await markPhase("baseline"); | |
| await sampleMemory("baseline"); | |
| ort.env.wasm.numThreads = 1; | |
| ort.env.wasm.wasmPaths = "/.work/ort/"; | |
| if (!navigator.gpu) throw new Error("WebGPU is unavailable"); | |
| let gpuDevice = null; | |
| const ensureMainDevice = async () => { | |
| if (gpuDevice) return; | |
| const adapter = await navigator.gpu.requestAdapter({ powerPreference: "high-performance" }); | |
| if (!adapter) throw new Error("No WebGPU adapter is available"); | |
| gpuDevice = await adapter.requestDevice(); | |
| ort.env.webgpu.device = gpuDevice; | |
| }; | |
| const cacheNames = [ | |
| ...Array.from({ length: 6 }, (_, index) => `present_k${index}`), | |
| ...Array.from({ length: 6 }, (_, index) => `present_v${index}`), | |
| ]; | |
| const createSession = async (path, preferredOutputLocation) => { | |
| await ensureMainDevice(); | |
| const options = { | |
| executionProviders: ["webgpu"], | |
| preferredOutputLocation, | |
| logSeverityLevel: 3, | |
| ...(memoryMode === "lean" ? { enableCpuMemArena: false, enableMemPattern: false } : {}), | |
| }; | |
| const started = performance.now(); | |
| const session = loaderMode === "url" | |
| ? await ort.InferenceSession.create(path, options) | |
| : await fetch(path).then(async (response) => { | |
| if (!response.ok) throw new Error(`${path}: HTTP ${response.status}`); | |
| return ort.InferenceSession.create(new Uint8Array(await response.arrayBuffer()), options); | |
| }); | |
| log(`SESSION ${path} ${(performance.now() - started).toFixed(1)} ms`); | |
| return session; | |
| }; | |
| const visionPath = tier === "121" | |
| ? "./.work/models/webgpu-121/vision_int4.onnx" | |
| : "./.work/models/webgpu-242/vision_fp16.onnx"; | |
| const fixedKv = decoderMode === "fixed-kv"; | |
| const decoderPath = ({ | |
| baseline: "./.work/models/shared/decoder_unified_gather_qdq_int8.onnx", | |
| "gather-opt": "./.work/models/model-opt/decoder_gather_before_dq_int8.onnx", | |
| "fp16-matmul": "./.work/models/model-opt/decoder_static_fp16_matmul.onnx", | |
| "fixed-kv": "./.work/models/model-opt/decoder_gather_dq_fixed_kv_int8.onnx", | |
| })[decoderMode]; | |
| let vision = null; | |
| let decoder = null; | |
| const createVision = async () => { | |
| vision = await createSession(visionPath, scheduleMode === "staged" ? { vision_embeds: "cpu" } : { vision_embeds: "gpu-buffer" }); | |
| if (JSON.stringify(vision.inputNames) !== JSON.stringify(["pixel_values"])) { | |
| throw new Error(`Unexpected vision inputs ${vision.inputNames}`); | |
| } | |
| await sampleMemory("vision-session-ready"); | |
| }; | |
| const createDecoder = async () => { | |
| decoder = await createSession(decoderPath, Object.fromEntries([ | |
| ["logits", "cpu"], | |
| ...cacheNames.map((name) => [name, "gpu-buffer"]), | |
| ])); | |
| for (const required of ["vision_embeds", "token_ids", "position_ids", "past_k0", "past_v5"]) { | |
| if (!decoder.inputNames.includes(required)) throw new Error(`Decoder input missing ${required}`); | |
| } | |
| if (fixedKv !== decoder.inputNames.includes("past_length")) { | |
| throw new Error(`Decoder past_length contract mismatch for ${decoderMode}`); | |
| } | |
| if (!decoder.outputNames.includes("logits") || !decoder.outputNames.includes("present_v5")) { | |
| throw new Error("Decoder capability outputs are incomplete"); | |
| } | |
| await sampleMemory("decoder-session-ready"); | |
| }; | |
| if (scheduleMode === "resident" && !decoderOnly) { | |
| await markPhase("vision-load"); | |
| await createVision(); | |
| await markPhase("decoder-load"); | |
| await createDecoder(); | |
| } | |
| if (decoderOnly) { | |
| await markPhase("decoder-load"); | |
| await createDecoder(); | |
| } | |
| const charset = await (await fetch("./tokenizer/vocab.json")).json(); | |
| const idToCharacter = ["", "", "", "", ...charset]; | |
| if (idToCharacter.length !== 14630) throw new Error(`Unexpected vocabulary ${idToCharacter.length}`); | |
| const contentIds = new Set(); | |
| for (let index = 0; index < charset.length; index += 1) { | |
| const character = charset[index]; | |
| if (character.length === 1 && !"ーー〜~".includes(character) && /[\p{Letter}\p{Number}]/u.test(character)) { | |
| contentIds.add(index + 4); | |
| } | |
| } | |
| const loadImage = async (path) => createImageBitmap(await (await fetch(path)).blob()); | |
| const preprocess = (image) => { | |
| context.imageSmoothingEnabled = true; | |
| context.imageSmoothingQuality = "high"; | |
| context.clearRect(0, 0, 224, 224); | |
| context.drawImage(image, 0, 0, image.width, image.height, 0, 0, 224, 224); | |
| const rgba = context.getImageData(0, 0, 224, 224).data; | |
| const values = new Float32Array(3 * 224 * 224); | |
| const mean = [0.485, 0.456, 0.406]; | |
| const std = [0.229, 0.224, 0.225]; | |
| for (let pixel = 0; pixel < 224 * 224; pixel += 1) { | |
| for (let channel = 0; channel < 3; channel += 1) { | |
| values[channel * 224 * 224 + pixel] = (rgba[pixel * 4 + channel] / 255 - mean[channel]) / std[channel]; | |
| } | |
| } | |
| return values; | |
| }; | |
| const tokenId = (token) => new ort.Tensor("int32", Int32Array.of(token), [1, 1]); | |
| const emptyVision = () => new ort.Tensor("float32", new Float32Array(0), [1, 0, 512]); | |
| const emptyCache = () => fixedKv | |
| ? new ort.Tensor("float32", new Float32Array(1 * 2 * 384 * 64), [1, 2, 384, 64]) | |
| : new ort.Tensor("float32", new Float32Array(0), [1, 2, 0, 64]); | |
| const pastLength = (length) => new ort.Tensor("int64", BigInt64Array.of(BigInt(length)), [1]); | |
| const positions = () => new ort.Tensor("int32", Int32Array.from({ length: 257 }, (_, index) => index), [1, 257]); | |
| const disposeResult = (result) => { | |
| result.logits?.dispose(); | |
| for (const name of cacheNames) result[name]?.dispose(); | |
| }; | |
| const chooseToken = (source, sequence, tokens) => { | |
| const seen = new Set(sequence); | |
| let blocked = -1; | |
| const last = tokens.at(-1); | |
| if (last !== undefined && contentIds.has(last)) { | |
| let run = 0; | |
| for (let index = tokens.length - 1; index >= 0 && tokens[index] === last; index -= 1) run += 1; | |
| if (run >= 12) blocked = last; | |
| } | |
| const adjusted = (index) => { | |
| if (index === blocked) return Number.NEGATIVE_INFINITY; | |
| const value = source[index]; | |
| if (!seen.has(index)) return value; | |
| return value < 0 ? value * 1.2 : value / 1.2; | |
| }; | |
| let bestToken = 0; | |
| let bestValue = adjusted(0); | |
| for (let token = 1; token < 14630; token += 1) { | |
| const value = adjusted(token); | |
| if (value > bestValue) { | |
| bestToken = token; | |
| bestValue = value; | |
| } | |
| } | |
| return bestToken; | |
| }; | |
| const encode = async (image) => { | |
| const pixels = new ort.Tensor("float32", preprocess(image), [1, 3, 224, 224]); | |
| const visionStarted = performance.now(); | |
| const visionResult = await vision.run({ pixel_values: pixels }); | |
| const visionMs = performance.now() - visionStarted; | |
| pixels.dispose(); | |
| return { visionEmbeds: visionResult.vision_embeds, visionMs }; | |
| }; | |
| const decode = async (visionEmbeds, visionMs) => { | |
| const started = performance.now(); | |
| const prefillFeeds = { | |
| vision_embeds: visionEmbeds, | |
| token_ids: tokenId(1), | |
| position_ids: positions(), | |
| ...Object.fromEntries(cacheNames.map((name) => [name.replace("present_", "past_"), emptyCache()])), | |
| }; | |
| if (fixedKv) prefillFeeds.past_length = pastLength(0); | |
| const prefillStarted = performance.now(); | |
| let cacheResult = await decoder.run(prefillFeeds); | |
| const prefillMs = performance.now() - prefillStarted; | |
| visionEmbeds.dispose(); | |
| for (const [name, tensor] of Object.entries(prefillFeeds)) if (name !== "vision_embeds") tensor.dispose(); | |
| const sequence = [1]; | |
| const tokens = []; | |
| const decodeStarted = performance.now(); | |
| for (let iteration = 0; iteration < maxTokens; iteration += 1) { | |
| // Decoder-only memory runs force a stable non-EOS token so every graph | |
| // executes the complete 128-token cache path. | |
| const next = decoderOnly ? 4 : chooseToken(cacheResult.logits.data, sequence, tokens); | |
| if (next === 2) break; | |
| tokens.push(next); | |
| sequence.push(next); | |
| if (tokens.length >= maxTokens) break; | |
| const feeds = { | |
| vision_embeds: emptyVision(), | |
| token_ids: tokenId(next), | |
| position_ids: new ort.Tensor("int32", Int32Array.of(257 + iteration), [1, 1]), | |
| }; | |
| if (fixedKv) feeds.past_length = pastLength(257 + iteration); | |
| for (let layer = 0; layer < 6; layer += 1) { | |
| feeds[`past_k${layer}`] = cacheResult[`present_k${layer}`]; | |
| feeds[`past_v${layer}`] = cacheResult[`present_v${layer}`]; | |
| } | |
| const nextResult = await decoder.run(feeds); | |
| disposeResult(cacheResult); | |
| feeds.vision_embeds.dispose(); | |
| feeds.token_ids.dispose(); | |
| feeds.position_ids.dispose(); | |
| feeds.past_length?.dispose(); | |
| cacheResult = nextResult; | |
| } | |
| const decodeMs = performance.now() - decodeStarted; | |
| disposeResult(cacheResult); | |
| return { | |
| text: tokens.map((token) => idToCharacter[token] ?? "").join(""), | |
| tokens: tokens.length, | |
| visionMs, | |
| prefillMs, | |
| decodeMs, | |
| totalMs: visionMs + performance.now() - started, | |
| }; | |
| }; | |
| const recognize = async (image) => { | |
| const encoded = await encode(image); | |
| return decode(encoded.visionEmbeds, encoded.visionMs); | |
| }; | |
| const expected = [ | |
| ["01", "知らない世界で見つけたイメージを"], | |
| ["02", "カナデトモスソラ(Kanadetomosusora)"], | |
| ["03", "建設会社社員行方"], | |
| ["04", "だとしてもこのレベルがウロつくなんて...おそらく2級の呪い"], | |
| ["05", "パチパチパチパチ"], | |
| ["06", "バビュン"], | |
| ["07", "僕の過去とか未来とか"], | |
| ["08", "くらべられっ子"], | |
| ["09", "そうだクラス分けがあるんだった!!"], | |
| ["10", "脇役よ、主役を超えよ!"], | |
| ["11", "Eh~Idon'treallywantto~"], | |
| ["12", "「Sorryforthewait~!Didyouwaitlong?」"], | |
| ["13", "YamateAreaNewresidentialdistrictforforeigners"], | |
| ]; | |
| const normalize = (text) => text.normalize("NFKC").replace(/\s+/g, ""); | |
| const editDistance = (left, right) => { | |
| let previous = Array.from({ length: right.length + 1 }, (_, index) => index); | |
| for (let leftIndex = 1; leftIndex <= left.length; leftIndex += 1) { | |
| const current = [leftIndex]; | |
| for (let rightIndex = 1; rightIndex <= right.length; rightIndex += 1) { | |
| current.push(Math.min( | |
| current[rightIndex - 1] + 1, | |
| previous[rightIndex] + 1, | |
| previous[rightIndex - 1] + (left[leftIndex - 1] === right[rightIndex - 1] ? 0 : 1), | |
| )); | |
| } | |
| previous = current; | |
| } | |
| return previous.at(-1); | |
| }; | |
| const stagedInputs = []; | |
| if (scheduleMode === "staged" && !decoderOnly) { | |
| await markPhase("vision-load"); | |
| const visionWorker = new Worker("./vision-worker.mjs", { type: "module" }); | |
| let workerRequestId = 0; | |
| const workerCalls = new Map(); | |
| visionWorker.onmessage = ({ data }) => { | |
| const call = workerCalls.get(data.requestId); | |
| if (!call) return; | |
| workerCalls.delete(data.requestId); | |
| if (data.error) call.reject(new Error(data.error)); | |
| else call.resolve(data.payload); | |
| }; | |
| visionWorker.onerror = (event) => { | |
| for (const call of workerCalls.values()) call.reject(event.error ?? new Error(event.message)); | |
| workerCalls.clear(); | |
| }; | |
| const callWorker = (type, payload, transfer = []) => new Promise((resolve, reject) => { | |
| const requestId = ++workerRequestId; | |
| workerCalls.set(requestId, { resolve, reject }); | |
| visionWorker.postMessage({ requestId, type, payload }, transfer); | |
| }); | |
| await callWorker("init", { | |
| modelPath: new URL(visionPath, location.href).href, | |
| wasmPaths: new URL("./.work/ort/", location.href).href, | |
| lean: memoryMode === "lean", | |
| }); | |
| await sampleMemory("vision-worker-session-ready"); | |
| await markPhase("vision-encode"); | |
| for (const [id, expectedText] of expected) { | |
| log(`ENCODE ${id}/13`); | |
| const image = await loadImage(`./.work/hayai13/${id}.png`); | |
| const pixels = preprocess(image); | |
| image.close(); | |
| const encoded = await callWorker("encode", { pixels }, [pixels.buffer]); | |
| stagedInputs.push({ id, expectedText, ...encoded }); | |
| } | |
| await sampleMemory("all-vision-embeddings-ready"); | |
| await markPhase("vision-release"); | |
| await callWorker("close", {}); | |
| visionWorker.terminate(); | |
| await new Promise((resolve) => setTimeout(resolve, 100)); | |
| await sampleMemory("vision-worker-terminated"); | |
| await markPhase("decoder-load"); | |
| await createDecoder(); | |
| } | |
| const benchmarkCases = decoderOnly ? [["synthetic", ""]] : expected; | |
| const results = []; | |
| await markPhase("decoder-run"); | |
| for (let caseIndex = 0; caseIndex < benchmarkCases.length; caseIndex += 1) { | |
| const [id, expectedText] = benchmarkCases[caseIndex]; | |
| log(`CASE ${id}/${benchmarkCases.length}`); | |
| let result; | |
| if (decoderOnly) { | |
| const visionEmbeds = new ort.Tensor("float32", new Float32Array(256 * 512), [1, 256, 512]); | |
| result = await decode(visionEmbeds, 0); | |
| } else if (scheduleMode === "staged") { | |
| const staged = stagedInputs[caseIndex]; | |
| const visionEmbeds = new ort.Tensor("float32", staged.values, staged.dims); | |
| result = await decode(visionEmbeds, staged.visionMs); | |
| } else { | |
| const image = await loadImage(`./.work/hayai13/${id}.png`); | |
| result = await recognize(image); | |
| image.close(); | |
| } | |
| const normalizedExpected = normalize(expectedText); | |
| const normalizedActual = normalize(result.text); | |
| results.push({ | |
| id, | |
| expected: expectedText, | |
| actual: result.text, | |
| distance: editDistance(normalizedExpected, normalizedActual), | |
| characters: normalizedExpected.length, | |
| exact: normalizedExpected === normalizedActual, | |
| ...result, | |
| }); | |
| await sampleMemory(`after-case-${id}`); | |
| } | |
| const distance = results.reduce((sum, result) => sum + result.distance, 0); | |
| const characters = results.reduce((sum, result) => sum + result.characters, 0); | |
| const sortedTimes = results.map((result) => result.totalMs).sort((left, right) => left - right); | |
| await markPhase("release"); | |
| if (vision) await vision.release(); | |
| if (decoder) await decoder.release(); | |
| if (gpuDevice) gpuDevice.destroy(); | |
| await new Promise((resolve) => setTimeout(resolve, 250)); | |
| await sampleMemory("released"); | |
| const summary = { | |
| runId, | |
| tier, | |
| memoryMode, | |
| loaderMode, | |
| scheduleMode, | |
| decoderMode, | |
| benchmarkScope, | |
| capability: { | |
| layers: 6, | |
| hiddenSize: 512, | |
| intermediateSize: 1536, | |
| attentionHeads: 8, | |
| kvHeads: 2, | |
| vocabulary: 14630, | |
| maxTokens, | |
| vision: tier === "121" ? "upstream INT4" : "upstream FP16", | |
| decoder: ({ | |
| baseline: "complete INT8-QDQ unified Gather", | |
| "gather-opt": "complete INT8-QDQ Gather-before-DQ", | |
| "fp16-matmul": "layers 1-3 static FP16 MatMul with Gather-before-DQ embedding", | |
| "fixed-kv": "complete INT8-QDQ Gather-before-DQ with fixed 384-slot KV I/O", | |
| })[decoderMode], | |
| }, | |
| cases: results.length, | |
| nCER: characters ? distance / characters : null, | |
| distance, | |
| characters, | |
| exact: results.filter((result) => result.exact).length, | |
| medianMs: sortedTimes[Math.floor(sortedTimes.length / 2)], | |
| peakCaseMs: sortedTimes.at(-1), | |
| memorySamples, | |
| results, | |
| }; | |
| const response = await fetch("/benchmark-result", { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify(summary), | |
| }); | |
| if (!response.ok) throw new Error(`Failed persisting result: HTTP ${response.status}`); | |
| summary.processMemory = await response.json(); | |
| window.__benchmarkResult = summary; | |
| window.__benchmarkDone = true; | |
| log(`SUITE_RESULT ${JSON.stringify(summary)}`); | |
| </script> | |