Spaces:
Running
Running
File size: 11,456 Bytes
0dd5f5c | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 | // Thinletter demo, shared loader: the DOM-free pieces used by app.js (corpus search in the tab) and legal.js (Czech
// legal corpus over the full Elasticsearch index). Nothing here touches the document; the pages own their elements.
//
// fetchJSON / fetchBuf same-origin fetch helpers
// l2n, percentile, fmt* maths and formatting (same code as client/browser/bench.js)
// opfsRoot, opfsCachedFile the browser's private origin file system (OPFS) where the assembled GGUF is kept
// assembleFromManifest byte-chunk manifest (model/<file>.chunks.json) -> one File in OPFS (or a Blob in memory)
// probeWebGPU, parseGpuLog the page's view of the WebGPU adapter; GPU evidence from llama.cpp's own log lines
// newWllama, loadModelSource wllama instance with the native log kept; download (or cache) + loadModel with timings
// measureMemory performance.measureUserAgentSpecificMemory when cross-origin isolated
import { Wllama } from './vendor/wllama/index.js';
export { Wllama };
export const WLLAMA_VERSION = '3.6.1';
export const WASM_URL = new URL('./vendor/wllama/wllama.wasm', import.meta.url).href;
export const fmtMiB = (b) => `${(b / 2 ** 20).toFixed(b < 10 * 2 ** 20 ? 2 : 0)} MiB`;
export const fmtMs = (ms) => (ms >= 1000 ? `${(ms / 1000).toFixed(2)} s` : `${ms.toFixed(0)} ms`);
export const fmtS = (s) => `${s.toFixed(1)} s`;
export const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
// ---- maths ---------------------------------------------------------------------------------------------------
export function l2n(v) {
let s = 0;
for (let i = 0; i < v.length; i++) s += v[i] * v[i];
const inv = s > 0 ? 1 / Math.sqrt(s) : 0;
const out = new Float32Array(v.length);
for (let i = 0; i < v.length; i++) out[i] = v[i] * inv;
return out;
}
export function percentile(a, p) { // numpy default (linear interpolation)
const s = [...a].sort((x, y) => x - y);
if (!s.length) return NaN;
const h = (s.length - 1) * p, lo = Math.floor(h), hi = Math.ceil(h);
return s[lo] + (s[hi] - s[lo]) * (h - lo);
}
// ---- fetch -----------------------------------------------------------------------------------------------------
export async function fetchJSON(url) { const r = await fetch(url); if (!r.ok) throw new Error(`fetch ${url}: ${r.status}`); return r.json(); }
export async function fetchBuf(url) { const r = await fetch(url); if (!r.ok) throw new Error(`${url}: ${r.status}`); return r.arrayBuffer(); }
export async function measureMemory() {
const out = { api: 'n/a', bytes: null };
if (self.crossOriginIsolated && performance.measureUserAgentSpecificMemory) {
try {
const m = await performance.measureUserAgentSpecificMemory();
return { api: 'measureUserAgentSpecificMemory', bytes: m.bytes };
} catch (e) { console.warn('measureUserAgentSpecificMemory failed', e); }
}
try { if (performance.memory) return { api: 'performance.memory.usedJSHeapSize (main thread only)', bytes: performance.memory.usedJSHeapSize }; } catch (e) { /* ignore */ }
return out;
}
// ---- model download: byte chunks -> one File in OPFS (or a Blob in memory) ------------------------------------
export async function opfsRoot() { try { return await navigator.storage.getDirectory(); } catch (e) { return null; } }
export async function opfsCachedFile(name, sizeBytes) { // the assembled file from an earlier visit, or null
try { const root = await opfsRoot(); if (!root) return null; const f = await (await root.getFileHandle(name)).getFile(); return f.size === sizeBytes ? f : null; } catch (e) { return null; }
}
export async function assembleFromManifest(manifestUrl, onProgress) {
const manifest = await fetchJSON(manifestUrl);
const total = manifest.size_bytes, name = manifest.file;
const root = await opfsRoot();
if (root) { // cached copy from an earlier visit?
try { const f = await (await root.getFileHandle(name)).getFile(); if (f.size === total) return { file: f, cached: true, persisted: true, manifest }; } catch (e) { /* not cached */ }
}
let fh = null, writable = null;
if (root) { try { fh = await root.getFileHandle(name, { create: true }); writable = await fh.createWritable(); } catch (e) { console.warn('OPFS write unavailable, keeping the model in memory', e); writable = null; } }
let loaded = 0;
const fetchChunk = async (c) => {
const r = await fetch(new URL(c.url, manifestUrl).href);
if (!r.ok) throw new Error(`chunk ${c.url}: HTTP ${r.status}`);
const reader = r.body.getReader(); const bufs = [];
for (;;) { const { done, value } = await reader.read(); if (done) break; bufs.push(value); loaded += value.length; onProgress(loaded, total); }
const blob = new Blob(bufs);
if (blob.size !== c.bytes) throw new Error(`chunk ${c.url}: got ${blob.size} bytes, expected ${c.bytes}`);
return blob;
};
const parts = [], pending = []; let next = 0;
for (let i = 0; i < manifest.chunks.length; i++) { // up to 3 chunks in flight, written in order
while (pending.length < 3 && next < manifest.chunks.length) pending.push(fetchChunk(manifest.chunks[next++]));
const blob = await pending.shift();
if (writable) await writable.write(blob); else parts.push(blob);
}
if (writable) {
await writable.close();
const f = await fh.getFile();
if (f.size !== total) throw new Error(`assembled ${f.size} bytes, expected ${total}`);
return { file: f, cached: false, persisted: true, manifest };
}
const blob = new Blob(parts);
if (blob.size !== total) throw new Error(`assembled ${blob.size} bytes, expected ${total}`);
return { file: blob, cached: false, persisted: false, manifest };
}
export async function clearModelCache(fileName, wllama) { // the OPFS copy and wllama's own cache (plain .gguf URLs)
const root = await opfsRoot();
if (root && fileName) { try { await root.removeEntry(fileName); } catch (e) { /* none */ } }
try { await (wllama || new Wllama({ default: WASM_URL })).cacheManager.clear(); } catch (e) { /* none */ }
}
// ---- backend: WebGPU (llama.cpp ggml-webgpu inside the same wasm) or CPU WebAssembly ----------------------------
export async function probeWebGPU() { // the page's view of the adapter; the worker requests it again with default options
const out = { available: false, reason: null, vendor: null, architecture: null, device: null, description: null, shader_f16: null };
if (!navigator.gpu) { out.reason = 'navigator.gpu is not available in this browser'; return out; }
try {
const ad = await navigator.gpu.requestAdapter();
if (!ad) { out.reason = 'requestAdapter() returned no adapter'; return out; }
let info = null;
try { info = ad.info || (ad.requestAdapterInfo ? await ad.requestAdapterInfo() : null); } catch (e) { /* no info */ }
Object.assign(out, { vendor: info?.vendor || null, architecture: info?.architecture || null, device: info?.device || null, description: info?.description || null,
shader_f16: ad.features.has('shader-f16') });
if (!out.shader_f16) { out.reason = 'adapter has no shader-f16 (ggml-webgpu registers no device)'; return out; }
out.available = true;
} catch (e) { out.reason = `requestAdapter failed: ${e && e.message ? e.message : e}`; }
return out;
}
export function adapterLabel(g) { return [g?.vendor, g?.architecture].filter(Boolean).join(' / ') || g?.description || 'unknown adapter'; }
export function pickBackend(requested, gpuProbe) { return requested === 'wasm' ? 'wasm' : requested === 'webgpu' ? 'webgpu' : (gpuProbe?.available ? 'webgpu' : 'wasm'); }
export function parseGpuLog(log, requested) { // GPU evidence from the llama.cpp INFO lines (client/webgpu/README.md section 4)
const pick = (re) => log.filter((s) => re.test(s)).map((s) => s.trim());
const g = { requested, adapter_info: pick(/ggml_webgpu: adapter_info/)[0] || null, offloaded: pick(/offloaded \d+\/\d+ layers to GPU/)[0] || null,
graph_splits: pick(/graph splits/)[0] || null, model_buffers: pick(/model buffer size/), flash_attn: pick(/Flash Attention/)[0] || null,
ggml_errors: pick(/ggml_webgpu: (Failed|Device|Error)/) };
const m = /offloaded (\d+)\/(\d+) layers/.exec(g.offloaded || '');
g.layers = m ? `${m[1]}/${m[2]}` : null;
g.active = !!g.adapter_info && !!m && Number(m[1]) > 0;
g.fallback = requested === 'webgpu' && !g.active;
return g;
}
// ---- wllama instance + model load ------------------------------------------------------------------------------
export function newWllama(nativeLog) { // llama.cpp's own log lines are kept in nativeLog (first 600) for parseGpuLog
const keep = (a) => { const t = a.map((x) => (typeof x === 'string' ? x : JSON.stringify(x))).join(' '); if (nativeLog.length < 600) nativeLog.push(t); };
return new Wllama({ default: WASM_URL }, { suppressNativeLog: false, logger: { debug: (...a) => keep(a), log: (...a) => { keep(a); console.log(...a); }, warn: (...a) => console.warn(...a), error: (...a) => console.error(...a) } });
}
export const LOAD_OPTS = { n_ctx: 512, n_batch: 512, n_ubatch: 512, embeddings: true, pooling_type: 'last', n_parallel: 1 }; // plus n_threads, n_gpu_layers
/** Download (or take from the cache) and load one model into `wllama`.
* source: a plain (or llama.cpp-split) .gguf URL, which wllama downloads and caches itself, or a byte-chunk manifest
* (.chunks.json), assembled into OPFS and handed to wllama as one File. onProgress(loaded, total) during the download,
* onStatus(text) for the phases, onCached(size) when no download was needed. Returns the timings. */
export async function loadModelSource(wllama, source, loadOpts, { onProgress = () => {}, onStatus = () => {}, onCached = () => {}, backend = 'wasm' } = {}) {
const t0 = performance.now(); let tDown = null, cached = false, persisted = null;
const prog = (loaded, total) => { onProgress(loaded, total); if (loaded >= total && tDown == null) tDown = performance.now(); };
onStatus(`fetching ${source.split('/').pop()} …`);
if (/\.gguf(\?.*)?$/i.test(source)) { // a plain (or llama.cpp-split) GGUF URL: wllama downloads and caches it itself
try { cached = (await wllama.modelManager.getModels()).some((m) => m.url === source); } catch (e) { /* ignore */ }
if (cached) onStatus('model found in the browser cache, no download');
await wllama.loadModelFromUrl(source, { ...loadOpts, useCache: true, progressCallback: ({ loaded, total }) => prog(loaded, total || loaded) });
if (tDown == null) tDown = performance.now();
persisted = true;
} else { // byte-chunk manifest (default): assemble, then hand one File to wllama
const a = await assembleFromManifest(source, prog);
cached = a.cached; persisted = a.persisted;
if (cached) { onStatus('model found in this browser\'s storage (OPFS), no download'); onCached(a.file.size); }
tDown = performance.now();
onStatus(backend === 'webgpu' ? 'starting the WebAssembly runtime and loading the weights onto the GPU (WebGPU) …' : 'starting the WebAssembly runtime and loading the weights …');
await wllama.loadModel([a.file], loadOpts);
}
const t1 = performance.now();
return { cached, persisted, download_s: cached ? 0 : (tDown - t0) / 1000, load_s: (t1 - tDown) / 1000 };
}
|