Spaces:
Running
Running
Download fork.js from spl16/local-model-explorer: direct link, hf CLI and curl.
- Browser
- Download file 28.5 kB
-
https://huggingface.co/spaces/spl16/local-model-explorer/resolve/main/fork.js
- Command line
-
hf download hf://spaces/spl16/local-model-explorer/fork.js
-
curl -L -o fork.js https://huggingface.co/spaces/spl16/local-model-explorer/resolve/main/fork.js
28.5 kB
| /* Quant-picking frontend. Catalog + fit data come live from the | |
| * LocalLLaMA explorer API; the best-quant choice per model comes from | |
| * rankQuants.js + quant_scores.json (measured KL benchmarks). */ | |
| ; | |
| const API = "https://codelion-local-model-explorer.hf.space"; | |
| const $ = (s, r = document) => r.querySelector(s); | |
| const $$ = (s, r = document) => [...r.querySelectorAll(s)]; | |
| let META = null, SCORES = null, runId = 0, offset = 0, total = 0, nofitDropped = 0, autoPages = 0, seenModels = []; | |
| function el(tag, cls, text) { | |
| const n = document.createElement(tag); | |
| if (cls) n.className = cls; | |
| if (text !== undefined) n.textContent = text; | |
| return n; | |
| } | |
| function opt(value, label, selected) { | |
| const o = document.createElement("option"); | |
| o.value = value; o.textContent = label; | |
| if (selected) o.selected = true; | |
| return o; | |
| } | |
| const gb = (v, d = 1) => v === null || v === undefined ? "?" : `${Number(v).toFixed(d)} GB`; | |
| const bytesGB = (b) => gb(b / 1e9); | |
| const params = (p) => p >= 1e9 ? `${+(p / 1e9).toFixed(p >= 1e10 ? 0 : 1)}B` : `${Math.round(p / 1e6)}M`; | |
| const compact = (n) => n >= 1e6 ? `${(n / 1e6).toFixed(1)}M` : n >= 1e3 ? `${Math.round(n / 1e3)}k` : String(n || 0); | |
| const ctxLabel = (c) => c >= 1024 ? `${Math.round(c / 1024)}k` : String(c); | |
| function rig() { | |
| const kind = $('input[name="kind"]:checked').value; | |
| const out = { kind, gpus: [], ram_gb: 32, | |
| vram_spare: +($("#vram-spare") || {}).value || 0, | |
| ram_spare: +($("#ram-spare") || {}).value || 0 }; | |
| if (kind === "gpu") { | |
| const one = (sel, vsel, csel, fb) => { | |
| const name = $(sel).value; | |
| const each = name === "custom" ? Math.max(1, Math.min(256, +$(vsel).value || fb)) : META.hardware.gpus[name]; | |
| return Array.from({ length: +$(csel).value || 1 }, () => each); | |
| }; | |
| out.gpus = one("#gpu", "#custom-vram", "#gpu-count", 24); | |
| if (!$("#gpu2-wrap").hidden) out.gpus = out.gpus.concat(one("#gpu2", "#custom-vram2", "#gpu2-count", 8)).slice(0, 16); | |
| out.ram_gb = +$("#ram").value; | |
| } else if (kind === "unified") { | |
| out.unified_system = $("#usys").value; | |
| out.ram_gb = +$("#umem").value; | |
| } else { | |
| out.ram_gb = +$("#ram").value; | |
| } | |
| return out; | |
| } | |
| function query(append) { | |
| const r = rig(); | |
| const pair = $("#kv").value; | |
| // upstream kv_type only accepts f16/q8_0/q4_0 with K=V. ask q8_0 and | |
| // rescale client-side (rescaleKvFits): q8_0-based scores and fits are | |
| // close to every smaller pair, so the page stays nearly consistent | |
| // (an f16 base would leave every score stale). f16/bf16 ask f16. | |
| const q8base = pair !== "f16/f16" && pair !== "bf16/bf16"; | |
| const rescaled = q8base ? kvScale(pair) * 16 / 8.5 < 1 : false; | |
| return { | |
| hardware: { kind: r.kind, gpus: r.gpus, ram_gb: r.ram_gb, | |
| ...(r.unified_system ? { unified_system: r.unified_system } : {}) }, | |
| context: +$("#ctx").value, kv_type: q8base ? "q8_0" : "f16", kv_pair: pair, | |
| min_bits: +$("#min-bits").value, | |
| sort: $("#sort").value, family: $("#family").value || null, size: $("#size").value || null, | |
| search: $("#search").value.trim() || null, | |
| moe_only: $("#moe-only").checked, vision_only: $("#vision-only").checked, | |
| fits_only: rescaled ? false : ($("#fits-only").checked || $("#sort").value === "best"), | |
| vram_only: rescaled ? false : (r.kind === "gpu" && $("#vram-only").checked), | |
| mmproj: $("#mmproj").value, | |
| offset: append ? offset : 0, limit: 24, | |
| }; | |
| } | |
| function rankWithReserve(allOptions, fam, r) { | |
| // rank headroom-kept options; when the reserve hides every fitting | |
| // quant, fall back to ranking everything so the card can name what | |
| // missed and by how much instead of showing nothing-fits. | |
| const fitOk = (o) => ({ ...o, | |
| fit_ok: o.fit && o.fit.tier !== "no" && o.fit.tier !== "no_vram" }); | |
| const { kept, hidden } = applyHeadroom(allOptions, headroomCaps(r), r.kind); | |
| let { pick, warnings } = rankOptions(kept.map(fitOk), fam, SCORES); | |
| let overReserve = false; | |
| if (!pick && hidden) { | |
| ({ pick, warnings } = rankOptions((allOptions || []).map(fitOk), fam, SCORES)); | |
| overReserve = !!pick; | |
| } | |
| return { pick, warnings, hidden, overReserve }; | |
| } | |
| function reserveNote(pick, r) { | |
| if (!pick || !pick.fit) return null; | |
| const caps = headroomCaps(r); | |
| const over = []; | |
| if (r.kind !== "cpu" && (pick.fit.gpu_gb || 0) > caps.vram) | |
| over.push(`${gb((pick.fit.gpu_gb || 0) - caps.vram)} VRAM`); | |
| if ((pick.fit.ram_gb || 0) > caps.ram) | |
| over.push(`${gb((pick.fit.ram_gb || 0) - caps.ram)} RAM`); | |
| if (!over.length) return null; | |
| return `Over your headroom reserve by ${over.join(" + ")} — lower "Keep free" to run it.`; | |
| } | |
| const SCORE_PARTS = [["fit", "Fit"], ["capacity", "Size"], ["popularity", "Downloads"], ["recency", "New"], ["community", "Reports"]]; | |
| function scoreTitle(m) { | |
| const w = META.score_weights || {}; | |
| return SCORE_PARTS.map(([k, l]) => `${l}: ${Math.round((m.components[k] || 0) * w[k] * 100)} of ${Math.round(w[k] * 100)}`).join("\n"); | |
| } | |
| function rail(fit, r) { | |
| if (!fit) return null; | |
| const box = el("div", "rail"); | |
| const pct = (v, cap) => `${Math.max(0, Math.min(100, (v / cap) * 100))}%`; | |
| const vram = r.kind === "gpu" ? r.gpus.reduce((a, v) => a + v, 0) : 0; | |
| const ram = r.ram_gb; | |
| if (r.kind === "unified") { | |
| const share = (META.hardware.unified_shares || {})[r.unified_system]; | |
| const cap = r.ram_gb * (share || (r.ram_gb <= 36 ? 0.67 : 0.75)); | |
| const pool = el("div", "pool"); | |
| pool.style.flex = "1"; | |
| const i = el("i", "gpu"); i.style.width = pct(fit.gpu_gb, cap); pool.append(i); | |
| if (fit.gpu_gb > cap) { const o = el("i", "over"); o.style.width = "30%"; pool.append(o); } | |
| box.append(pool); | |
| return box; | |
| } | |
| if (vram) { | |
| const pool = el("div", "pool"); pool.style.flex = `${vram / (vram + ram)}`; | |
| const i = el("i", "gpu"); i.style.width = pct(fit.gpu_gb, vram); pool.append(i); | |
| box.append(pool); | |
| } | |
| const ramPool = el("div", "pool"); ramPool.style.flex = `${ram / ((vram || 1) + ram)}`; | |
| const j = el("i", fit.tier === "gpu_moe" ? "exp" : "ram"); j.style.width = pct(fit.ram_gb, ram); | |
| ramPool.append(j); | |
| if (fit.tier === "no" || fit.tier === "no_vram") { const o = el("i", "over"); o.style.width = "30%"; ramPool.append(o); } | |
| box.append(ramPool); | |
| return box; | |
| } | |
| function fitText(fit) { | |
| if (!fit) return ""; | |
| const parts = [fit.label]; | |
| if (fit.tier !== "no" && fit.tier !== "no_vram") parts.push(`${gb(fit.gpu_gb)} GPU, ${gb(fit.ram_gb)} RAM`); | |
| return parts.join(" · "); | |
| } | |
| function card(m) { | |
| const c = el("div", "card"); | |
| const h = el("h3"); | |
| const btn = el("button", null, m.name); | |
| h.append(btn); | |
| c.append(h); | |
| const tags = [ | |
| m.params ? params(m.params) + (m.active_params ? ` (${params(m.active_params)} active)` : "") : null, | |
| m.moe ? "MoE" : null, m.vision ? "vision" : null, m.family, | |
| `${m.repo_count} GGUF repos`, `${compact(m.downloads)} downloads`, | |
| ].filter(Boolean).join(" · "); | |
| c.append(el("div", "meta", tags)); | |
| const side = el("div", "r-side"); | |
| const sc = el("span", "score", String(Math.round(m.score))); | |
| sc.title = scoreTitle(m) + "\nHardware ranking, not model quality."; | |
| side.append(sc, el("span", "muted", "score")); | |
| c.append(side); | |
| const status = el("p", "muted", "Ranking quants…"); | |
| c.append(status); | |
| btn.addEventListener("click", () => openDetail(m, query())); | |
| return { c, status }; | |
| } | |
| // the backend bakes the QUERIED kv type into run commands; rewrite the | |
| // flags to the picked K/V pair (asymmetric included). f16 needs no flags. | |
| function kvCmd(cmd, pair) { | |
| let [k, v] = String(pair || "f16/f16").split("/"); | |
| if (KV_BITS[k] === undefined) k = "f16"; | |
| if (KV_BITS[v] === undefined) v = "f16"; | |
| cmd = cmd.replace(/\s*(--cache-type-k|-ctk)([=\s]+)\S+/g, k === "f16" ? "" : ` -ctk ${k}`) | |
| .replace(/\s*(--cache-type-v|-ctv)([=\s]+)\S+/g, v === "f16" ? "" : ` -ctv ${v}`); | |
| if (k !== "f16" && !/(--cache-type-k|-ctk)([=\s]|$)/.test(cmd)) cmd += ` -ctk ${k}`; | |
| if (v !== "f16" && !/(--cache-type-v|-ctv)([=\s]|$)/.test(cmd)) cmd += ` -ctv ${v}`; | |
| return cmd; | |
| } | |
| function renderDetail(m, body, kvPair) { | |
| const d = m._detail, pick = m._pick; | |
| const g = d.geometry; | |
| const facts = [ | |
| d.params ? `${params(d.params)} parameters${d.active_params ? `, ${params(d.active_params)} active` : ""}` : null, | |
| g ? `${g.n_layers} layers` : null, | |
| g && g.moe ? `${g.expert_count} experts, ${g.expert_used} per token` : null, | |
| d.context_length ? `${ctxLabel(d.context_length)} native context` : null, | |
| g && g.notes && g.notes.length ? g.notes.join(", ") : null, | |
| ].filter(Boolean).join(" • "); | |
| if (facts) body.append(el("p", "muted", facts)); | |
| const opts = d.options || []; | |
| const sel = opts.find((o) => pick && o.repo === pick.repo && o.quant === pick.quant) || opts[0]; | |
| if (sel && sel.commands) { | |
| const row = el("div", "row"); | |
| const copy = el("button", "btn ghost", "Copy command"); | |
| // upstream still emits the old `llama-server` binary name; current llama.cpp renamed it. | |
| const pre = el("pre", "cmd", kvCmd((sel.commands["llama.cpp"] || "").replace(/llama-server/g, "llama serve"), kvPair)); | |
| copy.addEventListener("click", async () => { | |
| try { await navigator.clipboard.writeText(pre.textContent); copy.textContent = "Copied"; } | |
| catch { copy.textContent = "Select and copy"; } | |
| setTimeout(() => (copy.textContent = "Copy command"), 1500); | |
| }); | |
| row.append(el("span", "muted", `Run it (${sel.quant} from ${sel.repo.split("/")[0]}):`), copy); | |
| body.append(row, pre); | |
| if (sel.fit) body.append(el("div", "muted", fitText(sel.fit))); | |
| if (m._reserveNote) body.append(el("div", "warn", m._reserveNote)); | |
| } | |
| body.append(el("h3", null, `Every quant (${opts.length})`)); | |
| const hr = headroomNote(m); | |
| if (hr) body.append(el("p", "muted", hr)); | |
| if (d.hidden_below_min_bits) { | |
| body.append(el("p", "muted", | |
| `${d.hidden_below_min_bits} quant(s) hidden by the bit floor. Lower "Lowest quant to consider" to see them.`)); | |
| } | |
| const t = el("table", "quants"); | |
| const head = el("tr"); | |
| for (const [h, num] of [["Uploader", 0], ["Quant", 0], ["Size", 1], ["On your hardware", 0]]) { | |
| head.append(el("th", num ? "num" : null, h)); | |
| } | |
| const tb = el("tbody"); | |
| for (const o of [...opts].sort((a, b) => a.size - b.size)) { | |
| const tr = el("tr"); | |
| if (pick && o.quant === pick.quant && o.repo === pick.repo) tr.className = "is-pick"; | |
| const qtd = el("td"); | |
| qtd.append(el("b", null, o.quant)); | |
| if (pick && o.quant === pick.quant && o.repo === pick.repo) qtd.append(" ◀ pick"); | |
| const link = el("a", null, o.repo.split("/")[0]); | |
| link.href = o.url; link.target = "_blank"; link.rel = "noopener"; | |
| const utd = el("td"); utd.append(link); | |
| tr.append(utd, qtd, el("td", "num", bytesGB(o.size)), el("td", null, o.fit ? o.fit.label : "?")); | |
| tb.append(tr); | |
| } | |
| t.append(head, tb); | |
| body.append(t); | |
| if (d.kv_cache_gb) { | |
| body.append(el("h3", null, "KV cache")); | |
| const strip = el("div", "kv-strip"); | |
| for (const [ctx, v] of Object.entries(d.kv_cache_gb)) { | |
| const s = el("span", null, `${ctxLabel(+ctx)} `); | |
| s.append(el("b", null, gb(v, v < 10 ? 2 : 1))); | |
| strip.append(s); | |
| } | |
| body.append(strip); | |
| } | |
| const fm = d.other_formats || {}; | |
| const names = { mlx: "MLX", onnx: "ONNX", awq: "AWQ", gptq: "GPTQ", fp8: "FP8", exl3: "EXL3", exl2: "EXL2" }; | |
| const any = Object.keys(names).filter((k) => fm[k] && fm[k].length); | |
| if (any.length) { | |
| body.append(el("h3", null, "Other formats")); | |
| for (const k of any) { | |
| const p = el("p", "muted", `${names[k]} (${fm[k].length}): `); | |
| for (const id of fm[k].slice(0, 4)) { | |
| const a = el("a", null, id.split("/")[1] || id); | |
| a.href = `https://huggingface.co/${id}`; a.target = "_blank"; a.rel = "noopener"; | |
| p.append(a, " "); | |
| } | |
| body.append(p); | |
| } | |
| } | |
| const cm = d.community; | |
| if (cm && cm.bench_rows && cm.bench_rows.length) { | |
| body.append(el("h3", null, "Community benchmarks")); | |
| const bt = el("table", "quants"); | |
| const bh = el("tr"); | |
| for (const [h, num] of [["Quant", 0], ["Hardware", 0], ["Backend", 0], ["Prompt t/s", 1], ["Gen t/s", 1]]) { | |
| bh.append(el("th", num ? "num" : null, h)); | |
| } | |
| const bb = el("tbody"); | |
| for (const b of cm.bench_rows) { | |
| const tr = el("tr"); | |
| tr.append(el("td", null, b.quant || "?"), | |
| el("td", null, b.gpu_name || b.gpu_model || (b.vram_class ? `${b.vram_class} GB GPU` : "CPU")), | |
| el("td", null, b.backend || "?"), | |
| el("td", "num", b.prompt_tps ? String(Math.round(b.prompt_tps)) : "—"), | |
| el("td", "num", b.generation_tps ? b.generation_tps.toFixed(1) : "—")); | |
| bb.append(tr); | |
| } | |
| bt.append(bh, bb); | |
| body.append(bt); | |
| } | |
| } | |
| function headroomCaps(r) { | |
| // usable GB after keeping the spare free. the upstream estimator has | |
| // no headroom params, so this is a client-side filter on fit_gb. | |
| if (r.kind === "unified") { | |
| const share = (META.hardware.unified_shares || {})[r.unified_system]; | |
| const cap = r.ram_gb * (share || (r.ram_gb <= 36 ? 0.67 : 0.75)); | |
| return { vram: cap - r.ram_spare, ram: cap - r.ram_spare }; | |
| } | |
| const vram = r.kind === "gpu" ? r.gpus.reduce((a, v) => a + v, 0) : 0; | |
| return { vram: vram - r.vram_spare, ram: r.ram_gb - r.ram_spare }; | |
| } | |
| function applyHeadroom(options, caps, kind) { | |
| // drop options that fit the estimator but leave less than the spare | |
| // free, e.g. a 31.7GB-RAM fit on a 32GB box with a 2GB reserve. | |
| // already-failing tiers pass through: the ranker reports those. | |
| let hidden = 0; | |
| const kept = []; | |
| for (const o of options || []) { | |
| const fit = o.fit || {}; | |
| const okTier = fit.tier && fit.tier !== "no" && fit.tier !== "no_vram"; | |
| const over = (kind !== "cpu" && (fit.gpu_gb || 0) > caps.vram) | |
| || (fit.ram_gb || 0) > caps.ram; | |
| if (okTier && over) { hidden++; continue; } | |
| kept.push(o); | |
| } | |
| return { kept, hidden }; | |
| } | |
| // Nominal bits per cached element (quants carry block-scale overhead). | |
| // Merged bench: quant-bench/kv_scores.json (3 models, KL vs f16 @4k). | |
| const KV_BITS = { f16: 16, bf16: 16, q8_0: 8.5, q5_0: 5.5, q5_1: 6, | |
| q4_0: 4.5, q4_1: 5, iq4_nl: 4.4 }; | |
| const KV_FULL_TIERS = new Set(["gpu", "gpu_moe", "cpu", "unified", "ram"]); | |
| function kvScale(pair) { | |
| const [k, v] = String(pair || "f16/f16").split("/"); | |
| return ((KV_BITS[k] ?? 16) + (KV_BITS[v] ?? 16)) / 2 / 16; | |
| } | |
| // f16-base rescale: the upstream API only accepts f16/q8_0/q4_0 and one | |
| // type for both K and V, so always query f16 and scale the KV part. | |
| // KV bytes are exact math (2*layers*kv_heads*head_dim*ctx*bpe), verified | |
| // against the estimator's own kv_cache_gb; weights + per-model overhead | |
| // pass through untouched. Only full tiers and failing tiers are adjusted; | |
| // partial offloads keep the estimator's f16 numbers (conservative). | |
| function rescaleKvFits(d, pair, ctx, hw) { | |
| // ratio is against the queried base: q8_0 for everything below it, | |
| // f16 for f16/bf16 (ratio 1, no-op). see query(). | |
| const base = pair === "f16/f16" || pair === "bf16/bf16" ? 16 : 8.5; | |
| const ratio = kvScale(pair) * 16 / base; | |
| if (!(ratio < 1)) return; | |
| const kvf = d.kv_cache_gb && d.kv_cache_gb[String(ctx)]; | |
| if (!(kvf > 0)) return; | |
| const scaled = {}; | |
| for (const [c, g] of Object.entries(d.kv_cache_gb)) scaled[c] = g * ratio; | |
| d.kv_cache_gb = scaled; | |
| const delta = kvf * (1 - ratio); | |
| const vram = hw.kind === "gpu" ? hw.gpus.reduce((a, x) => a + x, 0) : 0; | |
| // per-model overhead + RAM constant: median over the estimator's own | |
| // full-gpu options (single-option calibration flips picks on 0.2GB | |
| // noise; the backend's implied overhead spans ~1.7-2.8 per model). | |
| let oh = 1.75, ramConst = 1.2; | |
| const gpuRefs = (d.options || []).filter((o) => o.fit && o.fit.tier === "gpu"); | |
| if (gpuRefs.length) { | |
| const med = (xs) => { const s = xs.filter((x) => x > 0).sort((a, b) => a - b); | |
| return s.length ? s[Math.floor(s.length / 2)] : null; }; | |
| oh = med(gpuRefs.map((o) => (o.fit.gpu_gb || 0) - o.size / 1e9 - kvf)) ?? oh; | |
| ramConst = med(gpuRefs.map((o) => o.fit.ram_gb || 0)) ?? ramConst; | |
| } else { | |
| const ref = (d.options || []).find((o) => o.fit && | |
| (o.fit.tier === "cpu" || o.fit.tier === "ram")); | |
| if (ref) oh = Math.max(0, (ref.fit.gpu_gb || ref.fit.ram_gb || 0) - ref.size / 1e9 - kvf); | |
| } | |
| const full = (o, tier, gpuGb, ramGb, label) => { | |
| o.fit.tier = tier; o.fit.gpu_gb = gpuGb; o.fit.ram_gb = ramGb; | |
| o.fit.label = label; o.fit.ngl = o.fit.layers; | |
| }; | |
| const promote = (o) => { | |
| // failing AND partial tiers: the estimator graded these under f16, | |
| // where nothing big fits whole. rebuild as full when the scaled | |
| // footprint fits; otherwise leave the estimator's numbers alone. | |
| const sizeGb = o.size / 1e9; | |
| const candGpu = sizeGb + kvf * ratio + oh; | |
| if (hw.kind === "cpu") { | |
| if (candGpu <= hw.ram_gb) full(o, "cpu", 0, candGpu, "Fits in RAM (CPU)"); | |
| } else if (hw.kind === "unified") { | |
| const share = (META.hardware.unified_shares || {})[hw.unified_system]; | |
| const cap = hw.ram_gb * (share || (hw.ram_gb <= 36 ? 0.67 : 0.75)); | |
| if (candGpu <= cap) full(o, "unified", candGpu, 0, "Fits in shared memory"); | |
| } else if (candGpu <= vram) { | |
| full(o, "gpu", candGpu, ramConst, "Fits on GPU"); | |
| } | |
| // deliberately no ram-tier promotion on gpu rigs: a CPU-resident pick | |
| // would outrank GPU picks on quality and recommend slower inference. | |
| // the estimator's partial/no tiers stand instead, as before. | |
| }; | |
| for (const o of d.options || []) { | |
| const fit = o.fit; | |
| if (!fit) continue; | |
| if (!KV_FULL_TIERS.has(fit.tier)) { promote(o); continue; } | |
| if (fit.tier === "gpu" || fit.tier === "gpu_moe") fit.gpu_gb -= delta; | |
| else if (fit.tier === "cpu" || fit.tier === "ram") fit.ram_gb -= delta; | |
| else if (fit.tier === "unified") fit.gpu_gb -= delta; | |
| } | |
| } | |
| function headroomNote(m) { | |
| if (!m._hiddenHeadroom) return null; | |
| const s = m._spares || {}; | |
| const bits = []; | |
| if (s.v) bits.push(`${s.v} GB VRAM`); | |
| if (s.r) bits.push(`${s.r} GB RAM`); | |
| return `${m._hiddenHeadroom} quant(s) hidden by the headroom reserve (${bits.join(" + ")} kept free). Lower it to see them.`; | |
| } | |
| function ensureDialog() { | |
| let dlg = $("#detail"); | |
| if (!dlg) { | |
| dlg = document.createElement("dialog"); | |
| dlg.id = "detail"; | |
| const x = el("button", "btn ghost small dlg-x", "Close"); | |
| x.addEventListener("click", () => dlg.close()); | |
| dlg.append(x, el("h3", "dlg-title"), el("div", "dlg-body")); | |
| dlg.addEventListener("click", (e) => { if (e.target === dlg) dlg.close(); }); | |
| document.body.append(dlg); | |
| } | |
| return dlg; | |
| } | |
| async function openDetail(m, q) { | |
| const dlg = ensureDialog(); | |
| const body = $(".dlg-body", dlg); | |
| $(".dlg-title", dlg).textContent = m.name; | |
| if (!m._detail || m._kvPair !== q.kv_pair || m._ctx !== q.context) { | |
| body.replaceChildren(el("p", "muted", "Loading quants…")); | |
| if (!dlg.open) dlg.showModal(); | |
| try { | |
| const d = await detailFor(m.key, q); | |
| const r = rig(); | |
| rescaleKvFits(d, q.kv_pair, q.context, q.hardware); | |
| const { pick, hidden, overReserve } = rankWithReserve(d.options, famGuess(m.key), r); | |
| m._detail = d; m._pick = pick; m._kvPair = q.kv_pair; m._ctx = q.context; | |
| m._hiddenHeadroom = hidden; | |
| m._overReserve = overReserve; | |
| m._reserveNote = overReserve ? reserveNote(pick, r) : null; | |
| m._spares = { v: r.vram_spare, r: r.ram_spare }; | |
| } catch (err) { | |
| body.replaceChildren(el("p", "muted", | |
| `Couldn't load quants (${String(err).slice(0, 100)}).`)); | |
| return; | |
| } | |
| } else if (!dlg.open) { | |
| dlg.showModal(); | |
| } | |
| body.replaceChildren(); | |
| renderDetail(m, body, q.kv_pair); | |
| } | |
| async function detailFor(key, q, signal) { | |
| const r = await fetch(`${API}/api/model`, { method: "POST", | |
| headers: { "content-type": "application/json" }, signal, | |
| body: JSON.stringify({ key, hardware: q.hardware, context: q.context, | |
| kv_type: q.kv_type, priority: "balanced", min_bits: q.min_bits, | |
| vram_only: q.vram_only, mmproj: q.mmproj }) }); | |
| // priority is the upstream estimator's required field; our client-side | |
| // pick is single-mode and ignores it. | |
| if (!r.ok) throw new Error(`model ${r.status}`); | |
| return r.json(); | |
| } | |
| function famGuess(key) { | |
| if (/qwen/i.test(key)) return "Qwen"; | |
| if (/gemma/i.test(key)) return "Gemma"; | |
| return null; | |
| } | |
| async function search(append) { | |
| const id = ++runId; | |
| const q = query(append); | |
| if (!append) offset = 0; | |
| const box = $("#results"); | |
| if (!append) box.replaceChildren(); | |
| $("#count").textContent = append ? $("#count").textContent : "Finding models…"; | |
| const ctrl = new AbortController(); | |
| const alive = () => id === runId; | |
| let res; | |
| try { | |
| const rr = await fetch(`${API}/api/recommend`, { method: "POST", | |
| headers: { "content-type": "application/json" }, signal: ctrl.signal, | |
| body: JSON.stringify(q) }); | |
| if (!rr.ok) throw new Error(`recommend ${rr.status}`); | |
| res = await rr.json(); | |
| } catch { | |
| if (alive()) $("#count").textContent = "Couldn't reach the catalog API. Try again."; | |
| return; | |
| } | |
| if (!alive()) return; | |
| total = res.total; | |
| offset += res.results.length; | |
| $("#count").textContent = `${total.toLocaleString()} models`; | |
| $("#more").hidden = offset >= total; | |
| const r = rig(); | |
| // best-sort promises fitting models, so non-fits are filtered server-side | |
| // (fits_only above) and dropped client-side (headroom residuals) below. | |
| const bestSort = q.sort === "best"; | |
| if (!append) { nofitDropped = 0; autoPages = 0; seenModels = []; } | |
| seenModels.push(...res.results); | |
| for (const m of res.results) { | |
| const ui = card(m); | |
| m._ui = ui; | |
| box.append(ui.c); | |
| } | |
| const queue = [...res.results]; | |
| async function worker() { | |
| while (alive() && queue.length) { | |
| const m = queue.shift(); | |
| try { | |
| const d = await detailFor(m.key, q, ctrl.signal); | |
| if (!alive()) return; | |
| rescaleKvFits(d, q.kv_pair, q.context, q.hardware); | |
| const { pick, warnings, hidden, overReserve } = rankWithReserve(d.options, famGuess(m.key), r); | |
| m._detail = d; m._pick = pick; | |
| m._hiddenHeadroom = hidden; | |
| m._overReserve = overReserve; | |
| m._reserveNote = overReserve ? reserveNote(pick, r) : null; | |
| m._spares = { v: r.vram_spare, r: r.ram_spare }; | |
| const ui = m._ui; | |
| if (!ui) continue; | |
| ui.status.replaceChildren(); | |
| if (pick && !(bestSort && overReserve)) { | |
| const line = el("div", "choice"); | |
| line.append(el("b", null, pick.quant), | |
| ` from ${pick.repo.split("/")[0]} — ${bytesGB(pick.size)}${pick.full === false ? " (partial offload)" : ""}`); | |
| ui.status.append(line); | |
| ui.status.append(el("div", "muted", fitText(pick.fit))); | |
| for (const w of warnings) ui.status.append(el("div", "warn", w)); | |
| if (m._reserveNote) ui.status.append(el("div", "warn", m._reserveNote)); | |
| const hr = headroomNote(m); | |
| if (hr && !overReserve) ui.status.append(el("div", "muted", hr)); | |
| const railEl = rail(pick.fit, r); | |
| if (railEl) ui.status.append(railEl); | |
| } else if (bestSort) { | |
| ui.c.remove(); | |
| nofitDropped++; | |
| } else { | |
| ui.status.textContent = warnings[0] || "Doesn't fit this hardware."; | |
| } | |
| } catch (err) { | |
| if (alive() && m._ui) m._ui.status.textContent = | |
| `Quant ranking failed (${String(err).slice(0, 100)}).`; | |
| } | |
| } | |
| } | |
| await Promise.all(Array.from({ length: 4 }, worker)); | |
| if (!alive()) return; | |
| // a best-sort batch can hide entirely (e.g. nothing but 35Bs on top at | |
| // tight headroom): page forward while the screen is still empty, bounded | |
| // so it can never run away. any new search or click stops the chain. | |
| if (bestSort && box.childElementCount === 0 && autoPages < 4 && offset < total) { | |
| autoPages++; | |
| search(true); | |
| return; | |
| } | |
| // the server can no longer pre-filter fits (it only speaks q8_0/f16), | |
| // so best-sort keeps server score order and the client drops whatever | |
| // has no pick. count what is shown — the server total is unfiltered. | |
| // the vram-only and fits-only checkboxes are enforced here too, since | |
| // their server-side equivalents are forced off while rescaling. | |
| if (bestSort || $("#fits-only").checked) { | |
| for (const m of seenModels) { | |
| if (!m._ui || !m._ui.c.isConnected) continue; | |
| if (!m._pick) { m._ui.c.remove(); nofitDropped++; } | |
| } | |
| } | |
| if ($("#vram-only").checked && r.kind === "gpu") { | |
| for (const m of seenModels) { | |
| if (!m._ui || !m._ui.c.isConnected) continue; | |
| if (!m._pick || !m._pick.fit || m._pick.fit.tier !== "gpu") { | |
| m._ui.c.remove(); nofitDropped++; | |
| } | |
| } | |
| } | |
| if (bestSort) { | |
| $("#count").textContent = | |
| `${box.childElementCount.toLocaleString()} models that fit your hardware`; | |
| } | |
| } | |
| let deb; | |
| function schedule(append) { | |
| clearTimeout(deb); | |
| deb = setTimeout(() => search(append || false), 250); | |
| } | |
| function syncKind() { | |
| const kind = $('input[name="kind"]:checked').value; | |
| $$("[data-show]").forEach((n) => { | |
| if (n.id === "gpu2-wrap") return; // toggle-owned, not kind-owned | |
| n.hidden = !n.dataset.show.split(" ").includes(kind); | |
| }); | |
| $("#custom-vram-field").hidden = kind !== "gpu" || $("#gpu").value !== "custom"; | |
| $("#custom-vram2-field").hidden = $("#gpu2").value !== "custom"; | |
| $("#gpu2-toggle").textContent = $("#gpu2-wrap").hidden ? "Add a different card" : "Remove the other card"; | |
| } | |
| async function boot() { | |
| try { | |
| META = await fetch(`${API}/api/meta`).then((r) => r.json()); | |
| SCORES = await fetch("quant_scores.json").then((r) => r.json()); | |
| } catch { | |
| $("#count").textContent = "Couldn't load. Check your connection and refresh."; | |
| return; | |
| } | |
| const H = META.hardware; | |
| const groups = (sel) => { | |
| for (const [label, names] of H.gpu_groups) { | |
| const g = document.createElement("optgroup"); | |
| g.label = label; | |
| for (const n of names) g.append(opt(n, `${n.replace(/^(NVIDIA|AMD|Intel) /, "")} (${H.gpus[n]} GB)`)); | |
| $(sel).append(g); | |
| } | |
| $(sel).append(opt("custom", "Other: enter VRAM")); | |
| }; | |
| groups("#gpu"); groups("#gpu2"); | |
| // Steam Hardware Survey defaults (Jul 2026): RTX 3060 most common GPU, | |
| // 16 GB most common RAM, 64k ctx per the bench follow-ups. | |
| $("#gpu").value = "NVIDIA RTX 3060 12GB"; | |
| $("#gpu2").value = "NVIDIA RTX 4060"; | |
| const vramSet = [...new Set(Object.values(H.gpus))].sort((a, b) => a - b); | |
| for (const sel of ["#custom-vram", "#custom-vram2"]) { | |
| for (const v of vramSet) $(sel).append(opt(v, `${v} GB`, v === 12)); | |
| } | |
| for (const sel of ["#gpu-count", "#gpu2-count"]) { | |
| for (const n of [1, 2, 3, 4, 6, 8]) $(sel).append(opt(n, n, n === 1)); | |
| } | |
| for (const v of H.ram) $("#ram").append(opt(v, `${v} GB`, v === 16)); | |
| for (const s of Object.keys(H.unified)) $("#usys").append(opt(s, s)); | |
| const syncMem = () => { | |
| const mems = H.unified[$("#usys").value] || []; | |
| $("#umem").replaceChildren(...mems.map((m) => opt(m, `${m} GB`))); | |
| }; | |
| $("#usys").addEventListener("change", () => { syncMem(); schedule(); }); | |
| syncMem(); | |
| for (const c of H.contexts) $("#ctx").append(opt(c, `${ctxLabel(c)} tokens`, c === 65536)); | |
| for (const f of META.families) $("#family").append(opt(f.family, `${f.family} (${f.count})`)); | |
| for (const s of META.sizes) $("#size").append(opt(s, s)); | |
| $("#kind").addEventListener("change", () => { syncKind(); schedule(); }); | |
| $("#gpu").addEventListener("change", syncKind); | |
| $("#gpu2").addEventListener("change", syncKind); | |
| $("#gpu2-toggle").addEventListener("click", () => { | |
| $("#gpu2-wrap").hidden = !$("#gpu2-wrap").hidden; | |
| syncKind(); schedule(); | |
| }); | |
| $("#more").addEventListener("click", () => search(true)); | |
| $("#search").addEventListener("input", () => schedule()); | |
| for (const sel of ["#gpu", "#custom-vram", "#gpu-count", "#gpu2", "#custom-vram2", "#gpu2-count", | |
| "#ram", "#usys", "#umem", "#ctx", "#kv", "#mmproj", "#min-bits", | |
| "#vram-spare", "#ram-spare", "#family", "#size", "#sort"]) { | |
| $(sel).addEventListener("change", () => schedule()); | |
| } | |
| for (const sel of ["#vram-only", "#fits-only", "#moe-only", "#vision-only"]) { | |
| $(sel).addEventListener("change", () => schedule()); | |
| } | |
| $$('input[name="kind"]').forEach((i) => i.addEventListener("change", () => schedule())); | |
| syncKind(); | |
| search(false); | |
| } | |
| boot(); | |