Spaces:
Running
Running
Download app.js from Renesas/AI-Dashboard: direct link, hf CLI and curl.
- Browser
- Download file 179 kB
-
https://huggingface.co/spaces/Renesas/AI-Dashboard/resolve/main/app.js
- Command line
-
hf download hf://spaces/Renesas/AI-Dashboard/app.js
-
curl -L -o app.js https://huggingface.co/spaces/Renesas/AI-Dashboard/resolve/main/app.js
179 kB
| /* app.js — Static dashboard logic (artifact-layer aware) */ | |
| const HF_BASE = "https://huggingface.co"; | |
| const HF_SPACES = "https://huggingface.co/spaces"; | |
| const MODELS_FILE = "models.json"; | |
| const LINKS_FILE = "links.json"; | |
| const $ = (id) => document.getElementById(id); | |
| const $$ = (sel) => Array.from(document.querySelectorAll(sel)); | |
| /** Proper HTML escaping. */ | |
| function esc(str) { | |
| return String(str ?? "") | |
| .replaceAll("&", "&") | |
| .replaceAll("<", "<") | |
| .replaceAll(">", ">") | |
| .replaceAll('"', """) | |
| .replaceAll("'", "'"); | |
| } | |
| let CATALOG_ROOT = { models: [] }; | |
| let CATALOG = []; | |
| let CATALOG_NOTE = "Auto-generated from Hugging Face repositories."; | |
| /* Tier-1 KPI metadata from models.json: key -> {label, unit, direction, modalities}. | |
| Lets renderers show a metric's name/unit without hardcoding a second copy of the table | |
| generate_models_json.py already built (see KPI_REGISTRY there). */ | |
| let KPI_REGISTRY = {}; | |
| let LINKS = { spaces: {}, collections: {} }; | |
| let KEY_TO_MODEL = {}; | |
| let ARCHS = ["All"]; | |
| let MODALITIES = ["All"]; | |
| let VARIANTS = ["All"]; | |
| const ALL_FORMATS = ["ONNX", "GGUF"]; | |
| const ALL_HW = ["CPU", "NPU", "DSP"]; | |
| function uniqueSorted(values) { | |
| const out = []; | |
| const seen = new Set(); | |
| for (const v of values) { | |
| const x = String(v ?? "Unknown"); | |
| if (!seen.has(x)) { seen.add(x); out.push(x); } | |
| } | |
| return out.sort((a, b) => a.localeCompare(b)); | |
| } | |
| async function loadJSON(path, fallback) { | |
| try { | |
| const res = await fetch(path, { cache: "no-store" }); | |
| if (!res.ok) throw new Error(`HTTP ${res.status} ${res.statusText}`); | |
| return await res.json(); | |
| } catch { | |
| return fallback; | |
| } | |
| } | |
| function hfModelUrl(repoId) { return `${HF_BASE}/${repoId}`; } | |
| function hfSpaceUrl(spaceId) { return `${HF_SPACES}/${spaceId}`; } | |
| function safeList(x, fallback = []) { return Array.isArray(x) ? x : fallback; } | |
| function defaultPurposeForFormat(fmt) { | |
| return fmt === "ONNX" ? "PerformanceEstimate" : "llama.cpp Benchmarking"; | |
| } | |
| function validatePurpose(fmt, purpose) { | |
| if ((purpose === "PerformanceEstimate" || purpose === "ORT Benchmarking") && fmt !== "ONNX") { | |
| return { ok: false, msg: "⚠️ <b>PerformanceEstimate / ORT benchmarking is ONNX-only.</b> Switch <b>Format → ONNX</b>." }; | |
| } | |
| if (purpose === "llama.cpp Benchmarking" && fmt !== "GGUF") { | |
| return { ok: false, msg: "⚠️ <b>llama.cpp benchmarking is GGUF-only.</b> Switch <b>Format → GGUF</b>." }; | |
| } | |
| return { ok: true, msg: "" }; | |
| } | |
| /* ---------- Badge builders (HTML) ---------- */ | |
| function formatBadgesHTML(model) { | |
| const parts = []; | |
| if (model.onnx_repo) parts.push(`<span class="badge onnx">ONNX</span>`); | |
| if (model.gguf_repo) parts.push(`<span class="badge gguf">GGUF</span>`); | |
| return `<div class="badges">${parts.join("") || `<span class="subtle">—</span>`}</div>`; | |
| } | |
| function hwBadgesHTML(targets) { | |
| const t = safeList(targets, []); | |
| const parts = []; | |
| if (t.includes("CPU")) parts.push(`<span class="badge cpu">CPU</span>`); | |
| if (t.includes("NPU")) parts.push(`<span class="badge npu">NPU</span>`); | |
| if (t.includes("DSP")) parts.push(`<span class="badge dsp">DSP</span>`); | |
| return `<div class="badges">${parts.join("") || `<span class="subtle">—</span>`}</div>`; | |
| } | |
| function repoButtonsSmallHTML(model) { | |
| const parts = []; | |
| if (model.onnx_repo) { | |
| parts.push(`<a class="link small" href="${esc(hfModelUrl(model.onnx_repo))}" target="_blank" rel="noopener noreferrer">ONNX</a>`); | |
| } | |
| if (model.gguf_repo) { | |
| parts.push(`<a class="link small" href="${esc(hfModelUrl(model.gguf_repo))}" target="_blank" rel="noopener noreferrer">GGUF</a>`); | |
| } | |
| return `<div class="badges" style="gap:8px;">${parts.join("") || `<span class="subtle">—</span>`}</div>`; | |
| } | |
| /* Precision/Quant badges (horizontal wrap) */ | |
| function artifactsBadgesHTML(model) { | |
| const a1 = safeList(model.onnx_artifacts, []); | |
| const a2 = safeList(model.gguf_artifacts, []); | |
| const arts = uniqueSorted([...a1, ...a2].map(a => artifactPrecision(model, a))); | |
| if (!arts.length) return `<div class="badges badges-row"><span class="subtle">—</span></div>`; | |
| return `<div class="badges badges-row">${ | |
| arts.map(a => `<span class="badge art" title="${esc(a)}">${esc(a)}</span>`).join("") | |
| }</div>`; | |
| } | |
| /* ---------- Metrics formatting ---------- */ | |
| // peak_mem_mb actually holds memory bandwidth (MB/s); the UI always shows it as GB/s. | |
| function memBwGBs(mb) { | |
| return mb == null ? null : Math.round((mb / 1024) * 10) / 10; | |
| } | |
| /* KPI_REGISTRY-driven label/unit lookup for the Tier-1 fields that aren't already | |
| hand-labeled below (tok_s/latency_ms_p50/peak_mem_mb keep their existing wording). */ | |
| function kpiLabel(key, fallback) { | |
| return (KPI_REGISTRY[key] && KPI_REGISTRY[key].label) || fallback; | |
| } | |
| function kpiUnit(key, fallback) { | |
| return (KPI_REGISTRY[key] && KPI_REGISTRY[key].unit) || fallback; | |
| } | |
| function summarizeUnit(name, unitObj) { | |
| if (!unitObj) return []; | |
| const lines = []; | |
| if (unitObj.tok_s != null) lines.push(`${name} tok/s: ${unitObj.tok_s}`); | |
| if (unitObj.prefill_tok_s != null) lines.push(`${name} ${kpiLabel("prefill_tok_s", "prefill")} ${kpiUnit("prefill_tok_s", "tok/s")}: ${unitObj.prefill_tok_s}`); | |
| if (unitObj.ttft_ms != null) lines.push(`${name} ${kpiLabel("ttft_ms", "TTFT")} ${kpiUnit("ttft_ms", "ms")}: ${unitObj.ttft_ms}`); | |
| if (unitObj.latency_ms_p50 != null) lines.push(`${name} p50 ms: ${unitObj.latency_ms_p50}`); | |
| if (unitObj.peak_mem_mb != null) lines.push(`${name} mem BW GB/s: ${memBwGBs(unitObj.peak_mem_mb)}`); | |
| return lines; | |
| } | |
| function metricSummaryV2(obj) { | |
| if (!obj) return "—"; | |
| const cpu = obj.cpu; | |
| const npu = obj.npu; | |
| const dsp = obj.dsp; | |
| const total = obj.total || {}; | |
| const setup = obj.setup; | |
| const updated = obj.last_updated; | |
| let lines = []; | |
| lines = lines.concat(summarizeUnit("CPU", cpu)); | |
| lines = lines.concat(summarizeUnit("NPU", npu)); | |
| lines = lines.concat(summarizeUnit("DSP", dsp)); | |
| if (total && typeof total === "object") { | |
| if (total.tok_s != null) lines.push(`TOTAL tok/s: ${total.tok_s}`); | |
| if (total.prefill_tok_s != null) lines.push(`TOTAL ${kpiLabel("prefill_tok_s", "prefill")} ${kpiUnit("prefill_tok_s", "tok/s")}: ${total.prefill_tok_s}`); | |
| if (total.ttft_ms != null) lines.push(`TOTAL ${kpiLabel("ttft_ms", "TTFT")} ${kpiUnit("ttft_ms", "ms")}: ${total.ttft_ms}`); | |
| if (total.peak_mem_mb != null) lines.push(`TOTAL mem BW GB/s: ${memBwGBs(total.peak_mem_mb)}`); | |
| if (total.latency_ms_p50 != null) lines.push(`TOTAL p50 ms: ${total.latency_ms_p50}`); | |
| } | |
| // Tier-2 generic stage timings (e.g. SigLIP's Vision Encoder / Text Encoder split). | |
| for (const s of safeList(obj.stages, [])) { | |
| if (s && s.ms != null) lines.push(`${s.label}: ${s.ms} ms`); | |
| } | |
| if (setup) lines.push(`Setup: ${setup}`); | |
| if (updated) lines.push(`Updated: ${updated}`); | |
| if (obj.accuracy != null) lines.push(`Accuracy: ${obj.accuracy}`); | |
| if (obj.llm_metrics?.overall != null) lines.push(`LLM overall: ${obj.llm_metrics.overall}`); | |
| if (obj.vlm_metrics?.overall != null) lines.push(`VLM overall: ${obj.vlm_metrics.overall}`); | |
| return lines.length ? lines.join("\n") : "✓"; | |
| } | |
| /* Shows every KPI the block actually reports (subject to the "KPIs to display" filter): | |
| throughput/prefill/TTFT, any Tier-2 stage timing (e.g. SigLIP's Vision/Text Encoder | |
| split), latency/total, and accuracy — one row each, in that order, plus a fixed mem-BW | |
| row. A block that reports nothing meaningful just renders the mem-BW row (still "—" if | |
| that's absent too), rather than three placeholder rows for metrics it never had. */ | |
| function metricBriefHTML(block) { | |
| if (!block || typeof block !== "object") return `<span class="subtle">—</span>`; | |
| const total = (block.total && typeof block.total === "object") ? block.total : {}; | |
| const mem = memBwGBs(total.peak_mem_mb); | |
| const rows = visibleKpiChips(block) | |
| .map(c => `<div class="row"><span class="k">${esc(c.label)}</span><span class="v">${esc(fmtChipFull(c))}</span></div>`); | |
| rows.push(`<div class="row"><span class="k">mem BW</span><span class="v">${mem != null ? esc(mem) + " GB/s" : "—"}</span></div>`); | |
| return `<div class="cell-metrics">${rows.join("")}</div>`; | |
| } | |
| /* ---------- Catalog helpers ---------- */ | |
| function getCfg(model, cfgId) { | |
| for (const c of safeList(model.hardware_configs, [])) { | |
| if (c.id === cfgId) return c; | |
| } | |
| return null; | |
| } | |
| function safeFirstCfg(model) { | |
| const cfgs = safeList(model.hardware_configs, []); | |
| return cfgs.length ? cfgs[0] : null; | |
| } | |
| function getArtifactsFor(modelKey, fmt) { | |
| const m = KEY_TO_MODEL[modelKey]; | |
| if (!m) return []; | |
| return (fmt === "ONNX") ? safeList(m.onnx_artifacts, []) : safeList(m.gguf_artifacts, []); | |
| } | |
| /* True only if the artifact actually carries benchmark metadata (from its | |
| benchmarks/ subfolder → an estimate or hil block). Checked against the | |
| per-artifact map directly, NOT metricsForArtifact (whose legacy fallback | |
| would make every artifact look benchmarked). A legacy config with no | |
| artifacts map at all is treated as benchmarked so it isn't hidden. */ | |
| function artifactHasBenchmark(m, art) { | |
| for (const cfg of safeList(m?.hardware_configs, [])) { | |
| const metrics = cfg.metrics || {}; | |
| const artMap = metrics.artifacts; | |
| if (artMap && typeof artMap === "object") { | |
| const a = artMap[art]; | |
| if (a && (a.estimate || a.hil)) return true; | |
| } else if (metrics.estimate || metrics.hil) { | |
| return true; | |
| } | |
| } | |
| return false; | |
| } | |
| /* Artifacts to actually surface in the results UI: only those with benchmark | |
| data (e.g. 8B's fp16 folder has no benchmark, so it must not appear). */ | |
| function benchmarkedArtifactsFor(modelKey, fmt) { | |
| const m = KEY_TO_MODEL[modelKey]; | |
| if (!m) return []; | |
| return getArtifactsFor(modelKey, fmt).filter(a => artifactHasBenchmark(m, a)); | |
| } | |
| /** | |
| * Artifact-layer aware: | |
| * - Preferred: cfg.metrics.artifacts[artifact].estimate/hil | |
| * - Fallback: cfg.metrics.estimate/hil (legacy) | |
| */ | |
| function metricsForArtifact(cfgMetrics, artifact) { | |
| const m = cfgMetrics || {}; | |
| const artMap = m.artifacts; | |
| if (artifact && artMap && typeof artMap === "object" && artMap[artifact]) { | |
| const a = artMap[artifact] || {}; | |
| return { estimate: a.estimate || null, hil: a.hil || null }; | |
| } | |
| return { estimate: m.estimate || null, hil: m.hil || null }; | |
| } | |
| function bestAvailableBlockForArtifact(cfgMetrics, artifact) { | |
| const { estimate, hil } = metricsForArtifact(cfgMetrics, artifact); | |
| return hil || estimate || null; | |
| } | |
| /* Every benchmark run recorded for an artifact (one per runtime/engine, tagged | |
| with its kind: "hil" | "estimate"). The estimate/hil blocks above are the | |
| representative single run per kind; runs[] keeps the full per-runtime detail | |
| (e.g. ResNet50 int8 measured on both onnxruntime and mwmx). */ | |
| function artifactRuns(cfgMetrics, artifact) { | |
| const artMap = (cfgMetrics || {}).artifacts; | |
| if (artifact && artMap && typeof artMap === "object" && artMap[artifact] | |
| && Array.isArray(artMap[artifact].runs)) { | |
| return artMap[artifact].runs; | |
| } | |
| return []; | |
| } | |
| /* Human-friendly task label: "image-classification" -> "Image classification". */ | |
| function prettyTask(t) { | |
| const s = String(t ?? "").trim(); | |
| if (!s) return ""; | |
| return s.replace(/[-_]+/g, " ").replace(/^\w/, c => c.toUpperCase()); | |
| } | |
| /* Accuracy for one artifact under a config, scanning all runs (HIL-first, then | |
| estimate), falling back to the representative blocks. Returns a number or null. */ | |
| function accuracyForArtifact(cfg, art) { | |
| const runs = artifactRuns(cfg?.metrics || {}, art); | |
| let a = runs.filter(r => r.kind === "hil").map(accuracyFromBlock).find(v => v != null); | |
| if (a == null) a = runs.filter(r => r.kind === "estimate").map(accuracyFromBlock).find(v => v != null); | |
| if (a == null) { | |
| const { estimate, hil } = metricsForArtifact(cfg?.metrics || {}, art); | |
| a = accuracyFromBlock(hil); | |
| if (a == null) a = accuracyFromBlock(estimate); | |
| } | |
| return a; | |
| } | |
| /* The full-precision reference accuracy (fp32, else fp16) for a model+format, | |
| used to express how far a quantized precision drops from the float baseline. */ | |
| function referenceAccuracy(m, cfg, fmt) { | |
| const arts = getArtifactsFor(m.key, fmt); | |
| for (const ref of ["fp32", "fp16"]) { | |
| if (arts.includes(ref)) { | |
| const a = accuracyForArtifact(cfg, ref); | |
| if (a != null) return { art: ref, acc: a }; | |
| } | |
| } | |
| return null; | |
| } | |
| /* ---------- Accuracy + runtime extraction ---------- */ | |
| function accuracyFromBlock(block) { | |
| if (!block || typeof block !== "object") return null; | |
| for (const k of ["accuracy", "accuracy_pct", "acc", "quality"]) { | |
| if (block[k] != null && isFinite(Number(block[k]))) return Number(block[k]); | |
| } | |
| const tryList = (obj, keys) => { | |
| if (!obj || typeof obj !== "object") return null; | |
| for (const k of keys) { | |
| if (obj[k] != null && isFinite(Number(obj[k]))) return Number(obj[k]); | |
| } | |
| return null; | |
| }; | |
| let v = tryList(block.llm_metrics, ["overall", "mmlu", "gsm8k", "hellaswag", "truthfulqa", "mt_bench"]); | |
| if (v != null) return v; | |
| v = tryList(block.vlm_metrics, ["overall", "mmbench", "vqav2", "seedbench", "pope"]); | |
| if (v != null) return v; | |
| return null; | |
| } | |
| function runtimeTotalMsFromBlock(block) { | |
| if (!block || typeof block !== "object") return null; | |
| if (block.total && block.total.latency_ms_p50 != null && isFinite(Number(block.total.latency_ms_p50))) { | |
| return Number(block.total.latency_ms_p50); | |
| } | |
| const sumUnits = ["cpu", "npu", "dsp"] | |
| .map(u => block[u]?.latency_ms_p50) | |
| .filter(v => v != null && isFinite(Number(v))) | |
| .map(Number); | |
| if (sumUnits.length) return sumUnits.reduce((a, b) => a + b, 0); | |
| const tok = (block.total && block.total.tok_s != null) ? block.total.tok_s : block.tok_s; | |
| if (tok != null && isFinite(Number(tok)) && Number(tok) > 0) { | |
| return 1000 / Number(tok); | |
| } | |
| const fps = (block.total && block.total.fps != null) ? block.total.fps : block.fps; | |
| if (fps != null && isFinite(Number(fps)) && Number(fps) > 0) { | |
| return 1000 / Number(fps); | |
| } | |
| return null; | |
| } | |
| /* ---------- Throughput / latency (LLM report tok/s, vision models img/s) ---------- | |
| These read either the block's `total` roll-up or, failing that, the per-unit | |
| (npu/cpu/dsp) values, so the UI can render a single figure with the right unit | |
| regardless of whether a model is measured in tokens or frames per second. */ | |
| function _firstUnitVal(block, key) { | |
| if (!block || typeof block !== "object") return null; | |
| const tot = block.total || {}; | |
| if (tot[key] != null && isFinite(Number(tot[key]))) return Number(tot[key]); | |
| for (const u of ["npu", "cpu", "dsp"]) { | |
| const v = block[u]?.[key]; | |
| if (v != null && isFinite(Number(v))) return Number(v); | |
| } | |
| return null; | |
| } | |
| /* Throughput as {value, unit}: tokens/sec for language models, frames/sec | |
| (img/s) for vision models. Prefers tok/s when both are somehow present. */ | |
| function throughputOf(block) { | |
| const tok = _firstUnitVal(block, "tok_s"); | |
| if (tok != null) return { value: tok, unit: "tok/s" }; | |
| const fps = _firstUnitVal(block, "fps"); | |
| if (fps != null) return { value: fps, unit: "img/s" }; | |
| return { value: null, unit: "tok/s" }; | |
| } | |
| function throughputValueOf(block) { return throughputOf(block).value; } | |
| /* Median inference latency (ms). LLM benchmarks and vision benchmarks are both | |
| normalized to latency_ms_p50 by the generator. */ | |
| function latencyMsOf(block) { return _firstUnitVal(block, "latency_ms_p50"); } | |
| /* ---------- Generic KPI chips (every metric a block actually reports) ---------- | |
| Every model family reports a different mix of Tier-1 fields (tok_s/prefill/ttft/fps), | |
| Tier-2 generic stage timings (block.stages — e.g. SigLIP's Vision/Text Encoder split), | |
| and accuracy. This turns whatever a block actually has into a flat, orderable list so | |
| cardHTML/metricBriefHTML can show "whatever is in the performance section" instead of | |
| hardcoding one metric per model family — and so the KPI display filter (KPI_KINDS / | |
| CATALOG_STATE.kpi) can hide/show each *kind* uniformly across every render surface. */ | |
| const KPI_KINDS = [ | |
| { id: "throughput", label: "Throughput (tok/s, fps)" }, | |
| { id: "prefill", label: "Prefill rate" }, | |
| { id: "ttft", label: "TTFT" }, | |
| { id: "stage", label: "Stage timings (e.g. encoder split)" }, | |
| { id: "latency", label: "Latency / Total" }, | |
| { id: "accuracy", label: "Accuracy" }, | |
| ]; | |
| function kpiChipsOf(block) { | |
| if (!block || typeof block !== "object") return []; | |
| const total = block.total || {}; | |
| const chips = []; | |
| const thr = throughputOf(block); | |
| if (thr.value != null) chips.push({ kind: "throughput", label: "Throughput", value: thr.value, unit: thr.unit }); | |
| if (total.prefill_tok_s != null) { | |
| chips.push({ kind: "prefill", label: kpiLabel("prefill_tok_s", "Prefill"), value: total.prefill_tok_s, unit: kpiUnit("prefill_tok_s", "tok/s") }); | |
| } | |
| if (total.ttft_ms != null) { | |
| chips.push({ kind: "ttft", label: kpiLabel("ttft_ms", "TTFT"), value: total.ttft_ms, unit: kpiUnit("ttft_ms", "ms") }); | |
| } | |
| for (const s of safeList(block.stages, [])) { | |
| if (s && s.ms != null) chips.push({ kind: "stage", label: s.label, value: s.ms, unit: "ms" }); | |
| } | |
| const lat = latencyMsOf(block); | |
| if (lat != null) { | |
| chips.push({ kind: "latency", label: safeList(block.stages, []).length ? "Total" : "Latency", value: lat, unit: "ms" }); | |
| } | |
| if (block.accuracy != null) chips.push({ kind: "accuracy", label: "Accuracy", value: block.accuracy * 100, unit: "%" }); | |
| if (block.top5_accuracy != null) chips.push({ kind: "accuracy", label: "Top-5 Accuracy", value: block.top5_accuracy * 100, unit: "%" }); | |
| return chips; | |
| } | |
| /* KPI chips filtered by the user's "KPIs to display" selection (CATALOG_STATE.kpi). */ | |
| function visibleKpiChips(block) { | |
| const enabled = CATALOG_STATE.kpi; | |
| return kpiChipsOf(block).filter(c => !enabled || enabled.has(c.kind)); | |
| } | |
| /* The one chip to headline (matches the old tok/fps -> latency -> accuracy fallback | |
| order), so existing LLM/CV cards keep showing throughput as the hero number. */ | |
| function primaryKpiChip(chips) { | |
| return chips.find(c => c.kind === "throughput") | |
| || chips.find(c => c.kind === "latency") | |
| || chips.find(c => c.kind === "stage") | |
| || chips.find(c => c.kind === "prefill") | |
| || chips.find(c => c.kind === "ttft") | |
| || chips.find(c => c.kind === "accuracy") | |
| || null; | |
| } | |
| /* The single best *remaining* KPI after the headline — one fixed extra line, never | |
| a variable-length list, so the card's stat box is the same size for every model | |
| regardless of how many KPIs it reports. Prefers a stage breakdown (e.g. SigLIP's | |
| Vision Encoder) over prefill/TTFT/accuracy/a second latency figure. */ | |
| function secondaryKpiChip(chips, primary) { | |
| const rest = chips.filter(c => c !== primary); | |
| return rest.find(c => c.kind === "stage") | |
| || rest.find(c => c.kind === "prefill") | |
| || rest.find(c => c.kind === "ttft") | |
| || rest.find(c => c.kind === "accuracy") | |
| || rest.find(c => c.kind === "latency") | |
| || null; | |
| } | |
| function fmtChipValue(c) { | |
| if (!c || c.value == null || !isFinite(Number(c.value))) return "—"; | |
| return String(Math.round(Number(c.value) * 10) / 10); | |
| } | |
| /* "76.3%" for percentages (no space, matching the rest of the UI), "396.8 ms" otherwise. */ | |
| function fmtChipFull(c) { | |
| const v = fmtChipValue(c); | |
| if (v === "—") return v; | |
| return c.unit === "%" ? `${v}%` : `${v} ${c.unit}`; | |
| } | |
| /* ---------- UI builders ---------- */ | |
| function buildRadioGroup(container, name, choices, value) { | |
| if (!container) return; | |
| container.innerHTML = choices.map(c => { | |
| const id = `${name}_${c.replace(/\W+/g, "_")}`; | |
| const checked = (c === value) ? "checked" : ""; | |
| return ` | |
| <label class="pill ${c === value ? "active" : ""}" for="${esc(id)}"> | |
| <input type="radio" name="${esc(name)}" id="${esc(id)}" value="${esc(c)}" ${checked} /> | |
| <span>${esc(c)}</span> | |
| </label> | |
| `; | |
| }).join(""); | |
| } | |
| function buildCheckGroup(container, name, choices, values) { | |
| if (!container) return; | |
| const set = new Set(values || []); | |
| container.innerHTML = choices.map(c => { | |
| const id = `${name}_${c.replace(/\W+/g, "_")}`; | |
| const checked = set.has(c) ? "checked" : ""; | |
| return ` | |
| <label class="pill ${set.has(c) ? "active" : ""}" for="${esc(id)}"> | |
| <input type="checkbox" name="${esc(name)}" id="${esc(id)}" value="${esc(c)}" ${checked} /> | |
| <span>${esc(c)}</span> | |
| </label> | |
| `; | |
| }).join(""); | |
| } | |
| function readRadio(name, fallback) { | |
| const el = document.querySelector(`input[name="${CSS.escape(name)}"]:checked`); | |
| return el ? el.value : fallback; | |
| } | |
| function readChecks(name) { | |
| return Array.from(document.querySelectorAll(`input[name="${CSS.escape(name)}"]:checked`)).map(x => x.value); | |
| } | |
| /* A minimal detail panel for not-yet-published models: no benchmarks, no | |
| artifacts, no download command to fabricate, and no repo link — the repo | |
| for these is either absent or just a placeholder, so linking to it would | |
| send someone to an empty page. Just what's known about the model. | |
| Bypasses the full benchmark rendering below entirely, since building a | |
| download command against zero artifacts would otherwise produce a | |
| nonsensical path. Shared by the modal and the full page, each passing the | |
| same `dense` flag their real-model rendering uses (see renderModalBodyHTML | |
| / renderFullModelPageHTML) so a coming-soon card sits in the same | |
| rail-plus-content skeleton as a published one instead of falling back to | |
| a one-off layout. */ | |
| function comingSoonDetailsHTML(m, dense) { | |
| const specs = modelSpecPairs(m, null, "", "", null).filter(s => s.k !== "Input resolution" && s.k !== "Format / compute"); | |
| const specsKvHTML = specs.map(s => `<div class="mdl-kv-row"><span class="k">${esc(s.k)}</span><span class="v" title="${esc(s.v)}">${esc(s.v)}</span></div>`).join(""); | |
| const specsGridHTML = specs.map(s => `<div class="mdl-spec-item"><div class="mdl-spec-label">${esc(s.k)}</div><div class="mdl-spec-value">${esc(s.v)}</div></div>`).join(""); | |
| const notice = `<div class="note note-warn">🚧 <b>Coming soon.</b> This model is in progress — benchmarks and download artifacts have not been published yet.</div>`; | |
| if (dense) { | |
| return ` | |
| <div class="mdl-modal-wrap"> | |
| <div class="mdl-modal-grid"> | |
| <div class="mdl-modal-rail"> | |
| <div class="mdl-example">${exampleImageHTML(m)}</div> | |
| <div class="mdl-kv-list mdl-modal-specs">${specsKvHTML}</div> | |
| </div> | |
| <div class="mdl-modal-main"> | |
| ${notice} | |
| </div> | |
| </div> | |
| </div>`; | |
| } | |
| return ` | |
| <div class="mdl-summary-row"> | |
| <div class="mdl-card mdl-card--rule"> | |
| <div class="metric-card-title">Model summary</div> | |
| <div class="mdl-spec-grid">${specsGridHTML}</div> | |
| </div> | |
| <div class="mdl-example">${exampleImageHTML(m)}</div> | |
| </div> | |
| ${notice}`; | |
| } | |
| /* Fallback illustration (animated SVG) chosen by modality. */ | |
| const MODALITY_FALLBACK = { | |
| LLM: "assets/fallback-llm.svg", | |
| VLM: "assets/fallback-vlm.svg", | |
| VLA: "assets/fallback-vla.svg", | |
| CV: "assets/fallback-cv.svg", | |
| ALM: "assets/fallback-alm.svg", | |
| // No dedicated dual-encoder illustration yet — reuse the VLM one (closest: image + text I/O). | |
| EMBED: "assets/fallback-vlm.svg", | |
| }; | |
| /* CV has multiple distinct tasks with their own illustration — a per-object | |
| bounding-box scene reads as object detection, not classification. */ | |
| const CV_TASK_FALLBACK = { | |
| "object-detection": "assets/fallback-cv.svg", | |
| "image-classification": "assets/fallback-cv-classification.svg", | |
| "3d-object-detection": "assets/fallback-cv-3d.svg", | |
| "object-detection-3d": "assets/fallback-cv-3d.svg", | |
| "lane-detection": "assets/fallback-cv-lane.svg", | |
| "image-segmentation": "assets/fallback-cv-segmentation.svg", | |
| "semantic-segmentation": "assets/fallback-cv-segmentation.svg", | |
| "video-classification": "assets/fallback-cv-video.svg", | |
| "image-feature-extraction": "assets/fallback-cv-feature.svg", | |
| }; | |
| /* VLA covers very different domains — a humanoid-robot arm reads wrong for a | |
| driving model. Same idea as CV_TASK_FALLBACK: pick by the model's own task. */ | |
| const VLA_TASK_FALLBACK = { | |
| "autonomous-driving": "assets/fallback-vla-driving.svg", | |
| }; | |
| /* Detail-page example figure: the single modality/task-appropriate animated | |
| SVG illustration for a model (unchanged — the richer "what it's doing" | |
| scene shown in the modal/full page, distinct from the compact card set | |
| below). */ | |
| function fallbackIllustrationSrc(m) { | |
| const modality = m.modality || "LLM"; | |
| const task = safeList(m.tasks, [])[0] || ""; | |
| const bev = modality === "CV" && bevKey(m); | |
| return (bev && `assets/fallback-${bev}.svg`) | |
| || (modality === "CV" && CV_TASK_FALLBACK[task]) | |
| || (modality === "VLA" && VLA_TASK_FALLBACK[task]) | |
| || MODALITY_FALLBACK[modality] | |
| || MODALITY_FALLBACK.LLM; | |
| } | |
| /* Same modality/task routing as fallbackIllustrationSrc, but returning a key | |
| into CARD_ILLUSTRATION_VARIANTS rather than a single file. */ | |
| const CV_TASK_KEY = { | |
| "image-classification": "cv-classification", | |
| "3d-object-detection": "cv-3d", | |
| "object-detection-3d": "cv-3d", | |
| "lane-detection": "cv-lane", | |
| "image-segmentation": "cv-segmentation", | |
| "semantic-segmentation": "cv-segmentation", | |
| "video-classification": "cv-video", | |
| "image-feature-extraction": "cv-feature", | |
| }; | |
| /* Bird's-eye-view models (BEVFormer, BEVFusion, BEVLaneDet) share a top-down | |
| look regardless of their nominal task, so they're detected by family/key. */ | |
| function bevKey(m) { | |
| const id = `${m.family || ""} ${m.key || ""}`.toLowerCase(); | |
| if (!/(^|[\s_-])bev(former|fusion|lane|[\s_-]|$)/.test(id)) return null; | |
| if (id.includes("fusion")) return "cv-bev-fusion"; | |
| if (safeList(m.tasks, []).includes("lane-detection")) return "cv-bev-lane"; | |
| return "cv-bev"; | |
| } | |
| function illustrationTypeKey(m) { | |
| const modality = m.modality || "LLM"; | |
| const task = safeList(m.tasks, [])[0] || ""; | |
| if (modality === "CV" && bevKey(m)) return bevKey(m); | |
| if (modality === "CV" && CV_TASK_KEY[task]) return CV_TASK_KEY[task]; | |
| if (modality === "CV") return "cv"; | |
| if (modality === "VLA" && task === "autonomous-driving") return "vla-driving"; | |
| if (modality === "VLA") return "vla"; | |
| if (modality === "VLM" || modality === "EMBED") return "vlm"; | |
| if (modality === "ALM") return "alm"; | |
| return "llm"; | |
| } | |
| /* Catalog-card illustration set — several hand-designed variants per | |
| modality/task, sized and cropped for the short banner strip (as opposed | |
| to the single wide scene each modality gets in the detail-page figure). | |
| A model always lands on the same variant (stable hash of its key) so the | |
| grid doesn't shuffle on re-render, but sibling models of the same | |
| modality don't all show the identical picture. */ | |
| const CARD_ILLUSTRATION_VARIANTS = { | |
| llm: ["assets/cards/llm-1.svg", "assets/cards/llm-2.svg", "assets/cards/llm-3.svg"], | |
| vlm: ["assets/cards/vlm-1.svg", "assets/cards/vlm-2.svg", "assets/cards/vlm-3.svg"], | |
| vla: ["assets/cards/vla-1.svg", "assets/cards/vla-2.svg", "assets/cards/vla-3.svg"], | |
| "vla-driving": ["assets/cards/vla-driving-1.svg", "assets/cards/vla-driving-2.svg", "assets/cards/vla-driving-3.svg"], | |
| cv: ["assets/cards/cv-1.svg", "assets/cards/cv-2.svg", "assets/cards/cv-3.svg"], | |
| "cv-classification": ["assets/cards/cv-classification-1.svg", "assets/cards/cv-classification-2.svg", "assets/cards/cv-classification-3.svg"], | |
| "cv-3d": ["assets/cards/cv-3d-1.svg", "assets/cards/cv-3d-2.svg", "assets/cards/cv-3d-3.svg"], | |
| "cv-lane": ["assets/cards/cv-lane-1.svg", "assets/cards/cv-lane-2.svg", "assets/cards/cv-lane-3.svg"], | |
| "cv-segmentation": ["assets/cards/cv-segmentation-1.svg", "assets/cards/cv-segmentation-2.svg", "assets/cards/cv-segmentation-3.svg"], | |
| "cv-video": ["assets/cards/cv-video-1.svg", "assets/cards/cv-video-2.svg", "assets/cards/cv-video-3.svg"], | |
| "cv-feature": ["assets/cards/cv-feature-1.svg", "assets/cards/cv-feature-2.svg", "assets/cards/cv-feature-3.svg"], | |
| "cv-bev": ["assets/cards/cv-bev-1.svg"], | |
| "cv-bev-fusion": ["assets/cards/cv-bev-fusion-1.svg"], | |
| "cv-bev-lane": ["assets/cards/cv-bev-lane-1.svg"], | |
| alm: ["assets/cards/alm-1.svg", "assets/cards/alm-2.svg", "assets/cards/alm-3.svg"], | |
| }; | |
| function stableHash(str) { | |
| let h = 0; | |
| for (let i = 0; i < str.length; i++) h = (Math.imul(h, 31) + str.charCodeAt(i)) | 0; | |
| return Math.abs(h); | |
| } | |
| /* compact = true swaps in the assets/cards/compact/ set: the same scenes | |
| redrawn slightly zoomed-out so their focal details (a chat bubble's tail, | |
| a robot arm's gripper, a detection badge) survive the shorter Overview | |
| carousel banner's object-fit:cover crop instead of bleeding off the edge. */ | |
| function cardIllustrationSrc(m, { compact = false } = {}) { | |
| const variants = CARD_ILLUSTRATION_VARIANTS[illustrationTypeKey(m)] || CARD_ILLUSTRATION_VARIANTS.llm; | |
| const src = variants[stableHash(String(m.key || m.display_name || "")) % variants.length]; | |
| return compact ? src.replace("assets/cards/", "assets/cards/compact/") : src; | |
| } | |
| /* Same hash-picked variant per model as cardIllustrationSrc, but walked in | |
| list order so that whenever two neighbors would land on the identical | |
| illustration (same family + same variant index — most likely when a | |
| burst of same-modality models land back to back, e.g. several YOLO | |
| releases), the second one is nudged to the next variant instead. Used by | |
| both the Last Update carousel (true left-right adjacency) and the | |
| Catalog grid (render order = reading order, so this at least guarantees | |
| no repeat within a row; the grid's column count isn't known at render | |
| time to also de-dupe vertically). */ | |
| function sequentialIllustrationSrcs(list, { compact = false } = {}) { | |
| let prevFamily = null, prevIdx = null; | |
| return list.map(m => { | |
| const familyKey = illustrationTypeKey(m); | |
| const variants = CARD_ILLUSTRATION_VARIANTS[familyKey] || CARD_ILLUSTRATION_VARIANTS.llm; | |
| let idx = stableHash(String(m.key || m.display_name || "")) % variants.length; | |
| if (familyKey === prevFamily && idx === prevIdx && variants.length > 1) { | |
| idx = (idx + 1) % variants.length; | |
| } | |
| prevFamily = familyKey; prevIdx = idx; | |
| const src = variants[idx]; | |
| return compact ? src.replace("assets/cards/", "assets/cards/compact/") : src; | |
| }); | |
| } | |
| /* "What it's doing" — a sample/preview image pulled from the model repo; falls | |
| back to a modality-appropriate animated SVG illustration when the repo ships | |
| none or it fails to load. Shown for all modalities. */ | |
| function exampleImageHTML(m) { | |
| const url = m && m.sample_image ? String(m.sample_image) : ""; | |
| const modality = m.modality || "LLM"; | |
| const task = safeList(m.tasks, [])[0] || ""; | |
| const fallbackSrc = fallbackIllustrationSrc(m); | |
| const taskLabel = task ? prettyTask(task) : modality; | |
| /* If the repo supplies an image, show it over the animated fallback. | |
| onerror removes the <img>, revealing the animated SVG beneath. */ | |
| const fallbackImg = `<img class="ex-img ex-img-fallback" src="${esc(fallbackSrc)}" alt="${esc(modality)} illustration" loading="lazy">`; | |
| const repoImg = url | |
| ? `<img class="ex-img" src="${esc(url)}" alt="${esc(taskLabel)} example" loading="lazy" referrerpolicy="no-referrer" onerror="this.remove()">` | |
| : ""; | |
| return ` | |
| <figure class="ex-fig" aria-label="Example of what the model does"> | |
| <div class="ex-media">${fallbackImg}${repoImg}</div> | |
| <figcaption class="ex-cap">Example${task ? " · " + esc(taskLabel) : " · " + esc(modality)}</figcaption> | |
| </figure>`; | |
| } | |
| /* Runs for an artifact: real per-runtime runs when present, else a synthetic | |
| one-row-per-kind fallback built from the aggregate hil/estimate blocks | |
| (mirrors accuracyForArtifact's HIL-first fallback). */ | |
| function runsForArtifact(cfgMetrics, art) { | |
| const direct = artifactRuns(cfgMetrics, art).filter(Boolean); | |
| if (direct.length) return direct; | |
| const { estimate, hil } = metricsForArtifact(cfgMetrics, art); | |
| const synth = []; | |
| if (hil) synth.push({ ...hil, kind: "hil" }); | |
| if (estimate) synth.push({ ...estimate, kind: "estimate" }); | |
| return synth; | |
| } | |
| /* Device utilization: how much of the chip a run actually drove (NPU instances, | |
| AI cores, clock frequency, TOPS) vs. what the hardware config has available — | |
| sourced straight from the benchmark YAML's hardware/configuration blocks. */ | |
| function utilizationOf(block) { | |
| return (block && block.utilization) ? block.utilization : null; | |
| } | |
| // function downloadIncludePattern(fmt, artifact) { | |
| // const a = String(artifact || "").trim(); | |
| // if (!a) return "*"; | |
| // if (fmt === "GGUF") return `*${a}*.gguf`; | |
| // return `*${a}*`; | |
| // } | |
| /* ---------- Download / Deploy helpers ---------- */ | |
| /* Mode A — “Download repo (filtered)”. | |
| Pulls only the artifact’s folder from the repo (host-side benchmarking | |
| or further conversion). Keeps the existing behaviour: one hf-download line. */ | |
| function downloadIncludePattern(fmt, artifact) { | |
| const a = String(artifact || "").trim(); | |
| if (!a) return "*"; | |
| // Same pattern for both formats: the artifact name is a top-level folder | |
| // in every Renesas repo (fp16/, w4a16/, w4a8/, fp32/). | |
| return `${a}/*`; | |
| } | |
| /* Mode B — “Deploy with prebuilt binaries”. | |
| Resolves installer / model / runner filenames from naming conventions in | |
| the per-repo README, with optional per-model overrides via | |
| `model.deploy[artifact]` in models.json. Returns null when no prebuilt | |
| binaries are published for the (artifact, hardware) pair, in which case | |
| the UI will gracefully degrade. Currently GGUF + RCAR-X5H only. */ | |
| function deployInfoFor(model, fmt, artifact, cfg) { | |
| if (!model || !artifact || fmt !== "GGUF") return null; | |
| // No published binaries for ONNX or non-X5H targets yet. | |
| const cfgId = (cfg?.id || "").toUpperCase(); | |
| if (cfgId && cfgId !== "RCAR-X5H") return null; | |
| const ov = model.deploy?.[artifact] || {}; | |
| const isQuant = artifact.toLowerCase() !== "fp16"; | |
| const runnerKind = isQuant ? "llama-quant-runner" : "llama-runner"; | |
| const installerVer = ov.installer_version || "0.1.0"; | |
| const installer = ov.installer || `${runnerKind}-${installerVer}-Linux.sh`; | |
| const installDir = ov.install_dir || runnerKind; | |
| const runnerBin = ov.runner_bin || runnerKind; | |
| const xosTag = ov.xos_tag || "xOS_v4.32"; | |
| const boardTag = ov.board_tag || "rcar-x5hv1"; | |
| const modelTag = isQuant ? artifact : "f16"; // README uses *-f16.gguf for FP16 | |
| const modelFile = ov.model_file || `${model.display_name || model.key}-${modelTag}.gguf`; | |
| return { | |
| installer, installDir, runnerBin, modelFile, | |
| installerPath: `${artifact}/binaries/${boardTag}/${xosTag}/${installer}`, | |
| modelPath: `${artifact}/${modelFile}`, | |
| }; | |
| } | |
| /* Render the Download section as two tabs: “Download repo” (filtered hf | |
| download) and “Deploy with binaries” (4-step install + run on X5H board). */ | |
| function renderActions(modelKey, fmt, compute, purpose, artifact, cfgId, mode = "explore") { | |
| const m = KEY_TO_MODEL[modelKey]; | |
| if (!m) return "<em>No actions.</em>"; | |
| const v = validatePurpose(fmt, purpose); | |
| if (!v.ok) { | |
| return `<div class="note note-warn">${v.msg}</div> | |
| <div class="subtle tight"><em>Actions are disabled until the selection is valid.</em></div>`; | |
| } | |
| const repoId = (fmt === "ONNX") ? (m.onnx_repo || "") : (m.gguf_repo || ""); | |
| const include = downloadIncludePattern(fmt, artifact); | |
| const cfg = cfgId ? getCfg(m, cfgId) : safeFirstCfg(m); | |
| const deploy = deployInfoFor(m, fmt, artifact, cfg); | |
| const repoUrl = repoId ? hfModelUrl(repoId) : ""; | |
| const headCls = mode === "modal" ? "no-border" : ""; | |
| const showH3 = mode !== "tab"; | |
| if (!repoId) { | |
| return `${showH3 ? `<h3 class="${headCls}">Download</h3>` : ""} | |
| <div class="note"><em>Repo not set for the selected format (${esc(fmt)}).</em></div>`; | |
| } | |
| /* Mode A — single filtered hf download */ | |
| const cmdRepo = `hf download ${repoId} --repo-type=model --include "${include}"`; | |
| /* Mode B — installer + model + on-board install + run */ | |
| const deployBlock = deploy ? ` | |
| <ol class="dl-steps"> | |
| <li> | |
| <div class="dl-step-h"><span class="dl-step-n">1</span> Download installer (host)</div> | |
| <div class="cmd-row"> | |
| <pre><code>hf download ${esc(repoId)} --repo-type=model --include "${esc(deploy.installerPath)}"</code></pre> | |
| <button class="copy-btn" data-copy="1" data-copy-text='hf download ${esc(repoId)} --repo-type=model --include "${esc(deploy.installerPath)}"'> | |
| <span class="icon">⧉</span><span class="txt">Copy</span> | |
| </button> | |
| </div> | |
| </li> | |
| <li> | |
| <div class="dl-step-h"><span class="dl-step-n">2</span> Download GGUF model (host)</div> | |
| <div class="cmd-row"> | |
| <pre><code>hf download ${esc(repoId)} --repo-type=model --include "${esc(deploy.modelPath)}"</code></pre> | |
| <button class="copy-btn" data-copy="1" data-copy-text='hf download ${esc(repoId)} --repo-type=model --include "${esc(deploy.modelPath)}"'> | |
| <span class="icon">⧉</span><span class="txt">Copy</span> | |
| </button> | |
| </div> | |
| </li> | |
| <li> | |
| <div class="dl-step-h"><span class="dl-step-n">3</span> Copy to X5H board, install & stage model</div> | |
| <div class="cmd-row"> | |
| <pre><code>bash ./${esc(deploy.installer)} --prefix=./ --exclude-subdir --skip-license | |
| mv ${esc(deploy.modelFile)} ${esc(deploy.installDir)}/</code></pre> | |
| <button class="copy-btn" data-copy="1" data-copy-text='bash ./${esc(deploy.installer)} --prefix=./ --exclude-subdir --skip-license mv ${esc(deploy.modelFile)} ${esc(deploy.installDir)}/'> | |
| <span class="icon">⧉</span><span class="txt">Copy</span> | |
| </button> | |
| </div> | |
| </li> | |
| <li> | |
| <div class="dl-step-h"><span class="dl-step-n">4</span> Run (on board)</div> | |
| <div class="cmd-row"> | |
| <pre><code>cd ${esc(deploy.installDir)} | |
| bash ./setup_npu.sh | |
| ./${esc(deploy.runnerBin)} "<PROMPT>"</code></pre> | |
| <button class="copy-btn" data-copy="1" data-copy-text='cd ${esc(deploy.installDir)} bash ./setup_npu.sh ./${esc(deploy.runnerBin)} "<PROMPT>"'> | |
| <span class="icon">⧉</span><span class="txt">Copy</span> | |
| </button> | |
| </div> | |
| </li> | |
| </ol> | |
| <p class="subtle tight" style="margin-bottom:4px">Expected on-board layout:</p> | |
| <pre aria-label="On-board folder layout"><code>${esc(deploy.installDir)}/ | |
| ├── ${esc(deploy.modelFile)} | |
| ├── ${esc(deploy.runnerBin)} | |
| ├── setup_npu.sh | |
| ├── firmwares/ | |
| ├── kernel_modules/ | |
| └── scripts/</code></pre> | |
| ` : `<div class="note note-warn">No prebuilt binaries published for <code>${esc(artifact || "—")}</code> on this hardware config yet. Use <strong>Download repo</strong> instead.</div>`; | |
| /* Sibling-selector mode cards (no JS) */ | |
| const uid = `dl_${(modelKey || "x").replace(/\W+/g, "_")}_${(artifact || "x").replace(/\W+/g, "_")}_${fmt}_${mode}`; | |
| return ` | |
| ${showH3 ? `<h3 class="${headCls}">Download</h3>` : ""} | |
| <div class="dl-tabs-wrap"> | |
| <input type="radio" name="${uid}" id="${uid}_repo" class="dl-tab-radio dl-tab-radio--repo" checked> | |
| <input type="radio" name="${uid}" id="${uid}_deploy" class="dl-tab-radio dl-tab-radio--deploy"> | |
| <div class="dl-mode-sel" role="tablist"> | |
| <label for="${uid}_repo" class="dl-mode-card dl-mode-card--repo" role="tab"> | |
| <span class="dl-mode-icon" aria-hidden="true">↓</span> | |
| <span class="dl-mode-body"> | |
| <span class="dl-mode-title">Download repo</span> | |
| <span class="dl-mode-desc">Pull the <code>${esc(artifact || "—")}</code> folder for host-side benchmarking or conversion</span> | |
| </span> | |
| </label> | |
| <label for="${uid}_deploy" class="dl-mode-card dl-mode-card--deploy${deploy ? "" : " dl-mode-card--na"}" role="tab"> | |
| <span class="dl-mode-icon" aria-hidden="true">▶</span> | |
| <span class="dl-mode-body"> | |
| <span class="dl-mode-title">Deploy with binaries${deploy ? "" : " <span class='dl-na-tag'>n/a</span>"}</span> | |
| <span class="dl-mode-desc">${deploy ? "Prebuilt runner on X5H board — 4-step install & run" : "No prebuilt binaries for this artifact / hardware config"}</span> | |
| </span> | |
| </label> | |
| </div> | |
| <div class="dl-panel dl-panel-repo"> | |
| <div class="cmd-row"> | |
| <pre><code>${esc(cmdRepo)}</code></pre> | |
| <button class="copy-btn" data-copy="1" data-copy-text="${esc(cmdRepo)}"> | |
| <span class="icon">⧉</span><span class="txt">Copy</span> | |
| </button> | |
| </div> | |
| </div> | |
| <div class="dl-panel dl-panel-deploy"> | |
| ${deployBlock} | |
| </div> | |
| </div>`; | |
| } | |
| /* ====================================================================== */ | |
| /* MODEL DETAIL — full page (#sec-model) + modal density (openModelModal) */ | |
| /* Config bar = three axes: hardware config × precision (artifact) × NPU */ | |
| /* allocation (derived below, not modeled in the data). Qualification */ | |
| /* (HIL/SIL) is never a selector — both are always computed and shown */ | |
| /* side by side. Page and modal keep independent state and share every */ | |
| /* builder below (PAGE_STATE / MODAL_STATE). */ | |
| /* ====================================================================== */ | |
| let PAGE_STATE = null; // { key, cfgId, fmt, precision, allocationId, unit, tab } | |
| let MODAL_STATE = null; // { key, cfgId, fmt, precision, allocationId, unit } | |
| function fmtNumStr(v, d) { return v == null ? null : Number(v).toFixed(d); } | |
| function fmtAccStr(a) { | |
| if (a == null) return "—"; | |
| return a <= 1.5 ? `${Math.round(a * 1000) / 10}%` : `${Math.round(a * 10) / 10}`; | |
| } | |
| function pctOf(used, total) { return (used != null && total != null && total > 0) ? Math.max(2, Math.round((used / total) * 100)) : 0; } | |
| function allocKeyOf(u) { | |
| if (!u) return "default"; | |
| return `${u.cores_used ?? "x"}|${u.freq_used_mhz ?? "x"}|${u.tops_used ?? "x"}`; | |
| } | |
| /* Groups a precision's runs[] into NPU-allocation buckets — the config | |
| bar's third axis — by utilization.cores_used + freq_used_mhz + | |
| tops_used. E.g. MobileNetV2 int8's mwmx runs land at 1 core / 7 TOPS | |
| (1.05 ms, 0.704 ms) and at 12 cores / 84 TOPS (0.751 ms): two | |
| allocations, not two duplicate "mwmx" bars. Runs with no utilization | |
| block collapse into one "default" bucket instead of one per run. */ | |
| function allocationsFor(runs) { | |
| const groups = new Map(); | |
| for (const r of runs) { | |
| const u = r.utilization || null; | |
| const key = allocKeyOf(u); | |
| if (!groups.has(key)) { | |
| groups.set(key, { | |
| id: key, | |
| cores: u?.cores_used ?? null, freq: u?.freq_used_mhz ?? null, tops: u?.tops_used ?? null, | |
| insts: u?.instances_used ?? null, | |
| coresTotal: u?.cores_total ?? null, freqTotal: u?.freq_total_mhz ?? null, | |
| topsTotal: u?.tops_total ?? null, instsTotal: u?.instances_total ?? null, | |
| npuPct: u?.npu_offload_pct ?? null, cpuPct: u?.cpu_offload_pct ?? null, | |
| }); | |
| } | |
| } | |
| const list = Array.from(groups.values()); | |
| list.forEach(g => { | |
| // Only spell out the clock speed when two allocations otherwise share the | |
| // same core count + TOPS and would read as duplicates without it. | |
| const siblings = list.filter(o => o.cores === g.cores && o.tops === g.tops); | |
| const needFreq = siblings.length > 1 && new Set(siblings.map(o => o.freq)).size > 1; | |
| const parts = []; | |
| if (g.cores != null) parts.push(`${g.cores} AI core${g.cores === 1 ? "" : "s"}`); | |
| if (needFreq && g.freq != null) parts.push(`${g.freq} MHz`); | |
| if (g.tops != null) parts.push(`${g.tops} TOPS`); | |
| g.label = parts.join(" · ") || "Unspecified allocation"; | |
| }); | |
| list.sort((a, b) => (a.cores ?? 0) - (b.cores ?? 0) || (a.tops ?? 0) - (b.tops ?? 0) || (a.freq ?? 0) - (b.freq ?? 0)); | |
| return list; | |
| } | |
| /* Default allocation = the one the best HIL run (highest throughput, else | |
| lowest latency) was measured at — matches the existing DEFAULT_FMT / | |
| DEFAULT_COMPUTE convention of opening on data that actually exists. */ | |
| function pickDefaultAllocationId(allocations, hilRuns) { | |
| if (!allocations.length) return null; | |
| let best = null, bestScore = -Infinity; | |
| for (const r of hilRuns) { | |
| const thr = throughputValueOf(r); | |
| const lat = latencyMsOf(r); | |
| const score = thr != null ? thr : (lat != null ? -lat : null); | |
| if (score != null && score > bestScore) { bestScore = score; best = r; } | |
| } | |
| const ref = best || hilRuns[0]; | |
| const key = ref ? allocKeyOf(ref.utilization) : null; | |
| return (key && allocations.some(a => a.id === key)) ? key : allocations[0].id; | |
| } | |
| /* Best value per metric across a run list — one number per KPI card. */ | |
| function bestOf(runs, metricFn, higherBetter) { | |
| let best = null; | |
| for (const r of runs) { | |
| const v = metricFn(r); | |
| if (v == null) continue; | |
| if (!best || (higherBetter ? v > best.value : v < best.value)) best = { value: v, run: r }; | |
| } | |
| return best; | |
| } | |
| /* The bar-chart fix: one best run per engine+kind, from the HIL runs of the | |
| selected allocation plus *all* SIL runs of this (config, precision) — SIL | |
| keeps its own allocation, since the estimator may have run at a | |
| different one than the HIL measurement being compared against. */ | |
| function bestPerEngineKind(hilRuns, silRuns, metricFn, higherBetter) { | |
| const pool = hilRuns.map(r => ({ r, kind: "hil" })).concat(silRuns.map(r => ({ r, kind: "estimate" }))); | |
| const byKey = new Map(); | |
| for (const { r, kind } of pool) { | |
| const v = metricFn(r); | |
| if (v == null) continue; | |
| const key = `${r.engine || "—"}|${kind}`; | |
| const cur = byKey.get(key); | |
| if (!cur || (higherBetter ? v > cur.value : v < cur.value)) { | |
| byKey.set(key, { engine: r.engine || "—", kind, value: v, run: r }); | |
| } | |
| } | |
| const list = Array.from(byKey.values()); | |
| list.sort((a, b) => higherBetter ? b.value - a.value : a.value - b.value); | |
| return list; | |
| } | |
| /* Every benchmarked (precision, run) row for the runs table, each carrying | |
| the allocation label derived from *its own* precision's grouping (an | |
| allocation id is only meaningful within the precision it was grouped | |
| from). */ | |
| function allRunsRowsFor(m, cfg, fmt) { | |
| const rows = []; | |
| for (const art of benchmarkedArtifactsFor(m.key, fmt)) { | |
| const runs = cfg ? runsForArtifact(cfg.metrics || {}, art) : []; | |
| const allocs = allocationsFor(runs); | |
| for (const r of runs) { | |
| const allocId = allocKeyOf(r.utilization); | |
| const allocLabel = (allocs.find(a => a.id === allocId) || {}).label || "—"; | |
| rows.push({ art, r, allocId, allocLabel }); | |
| } | |
| } | |
| return rows; | |
| } | |
| /* Everything below the config bar recomputes from (cfgId, precision, | |
| allocationId, unit) — the single source of truth both the full page and | |
| the modal read from. Returns null only when the model itself is missing. */ | |
| function buildDetailView(key, cfgId, fmt, precision, allocationId, unit) { | |
| const m = KEY_TO_MODEL[key]; | |
| if (!m) return null; | |
| const hwConfigs = safeList(m.hardware_configs, []); | |
| const cfg = (cfgId && getCfg(m, cfgId)) || safeFirstCfg(m) || null; | |
| const arts = benchmarkedArtifactsFor(m.key, fmt); | |
| const art = (precision && arts.includes(precision)) ? precision : (arts[0] || null); | |
| const runs = (art && cfg) ? runsForArtifact(cfg.metrics || {}, art) : []; | |
| const allocations = allocationsFor(runs); | |
| const allHilRuns = runs.filter(r => r.kind === "hil"); | |
| const allocId = (allocationId && allocations.some(a => a.id === allocationId)) | |
| ? allocationId | |
| : pickDefaultAllocationId(allocations, allHilRuns); | |
| const alloc = allocations.find(a => a.id === allocId) || null; | |
| const hilRuns = alloc ? allHilRuns.filter(r => allocKeyOf(r.utilization) === alloc.id) : []; | |
| const silRuns = runs.filter(r => r.kind === "estimate"); // allocation-independent — always all of them | |
| const unitSafe = (unit === "lat") ? "lat" : "thr"; | |
| /* Dual-encoder models (SigLIP, CLIP-style) report per-tower timings | |
| (run.stages — e.g. Vision Encoder / Text Encoder) instead of a single | |
| throughput/latency figure, so the fixed throughput/latency/accuracy KPI | |
| triple below is always empty for them. Surface each stage as its own | |
| extra KPI card and, when no real throughput/latency bars exist, use the | |
| stages as the bar chart too — otherwise this view renders nothing at | |
| all for them even though the data is right there in every run. */ | |
| const stageNames = []; | |
| for (const r of runs) { | |
| for (const s of safeList(r.stages, [])) { | |
| if (s && s.label && !stageNames.includes(s.label)) stageNames.push(s.label); | |
| } | |
| } | |
| const stageMetricFn = (name) => (r) => { | |
| const s = safeList(r.stages, []).find(x => x && x.label === name); | |
| return (s && s.ms != null && isFinite(Number(s.ms))) ? Number(s.ms) : null; | |
| }; | |
| /* Single-pass, batch-1, fixed-shape CV graphs make throughput the | |
| reciprocal of latency — the same convention the constellation chart | |
| (CSTL_REGIMES.cnn.deriveFps) already applies, so a CV run that only | |
| published latency (e.g. the ppa-estimator SIL run) still gets a | |
| throughput figure instead of silently dropping off this view. Never | |
| applied to the decode regime, where latency and tok/s measure | |
| different things. */ | |
| const canDeriveFps = ["CV", "DIFFUSION"].includes(String(m.modality || "").toUpperCase()); | |
| const derivedFps = (r) => { const lat = latencyMsOf(r); return (lat != null && lat > 0) ? 1000 / lat : null; }; | |
| // Real reported throughput always outranks a derived one — derivation only | |
| // fills a gap when *no* run in the set reports throughput directly, never | |
| // overrides a run that did. | |
| function bestThrOf(runs) { | |
| const real = bestOf(runs, throughputValueOf, true); | |
| if (real || !canDeriveFps) return real; | |
| return bestOf(runs, derivedFps, true); | |
| } | |
| /* ---- KPI strip: throughput · latency/TTFT · accuracy · NPU compute used ---- */ | |
| const thrHil = bestThrOf(hilRuns); | |
| const thrSil = bestThrOf(silRuns); | |
| const thrUnit = canDeriveFps ? "img/s" : ((thrHil && throughputOf(thrHil.run).unit) || (thrSil && throughputOf(thrSil.run).unit) || (stageNames.length ? "ms" : "tok/s")); | |
| const thrLabel = kpiLabel(thrUnit === "img/s" ? "fps" : "tok_s", "Throughput"); | |
| const thrHilDerived = !!(thrHil && throughputValueOf(thrHil.run) == null); | |
| const thrSilDerived = !!(thrSil && throughputValueOf(thrSil.run) == null); | |
| // TTFT stands in for latency only when this model's own runs actually | |
| // publish it (LLM/VLM/VLA/ALM) — never hardcoded per modality. | |
| const hasTtft = runs.some(r => _firstUnitVal(r, "ttft_ms") != null); | |
| const secKey = hasTtft ? "ttft_ms" : "latency_ms_p50"; | |
| const secMetric = hasTtft ? (r => _firstUnitVal(r, "ttft_ms")) : latencyMsOf; | |
| const secHil = bestOf(hilRuns, secMetric, false); | |
| const secSil = bestOf(silRuns, secMetric, false); | |
| const secLabel = kpiLabel(secKey, hasTtft ? "TTFT" : "Latency p50"); | |
| const secUnit = kpiUnit(secKey, "ms"); | |
| const accHil = bestOf(hilRuns, accuracyFromBlock, true); | |
| const accSil = bestOf(silRuns, accuracyFromBlock, true); | |
| const accRef = cfg ? referenceAccuracy(m, cfg, fmt) : null; | |
| const isRefArt = !!(accRef && accRef.art === art); | |
| let accDeltaPts = null; | |
| if (!isRefArt && accHil && accRef && accHil.value <= 1.5 && accRef.acc <= 1.5) { | |
| accDeltaPts = (accHil.value - accRef.acc) * 100; | |
| } | |
| const stageKpis = stageNames.map(name => { | |
| const metricFn = stageMetricFn(name); | |
| const hilS = bestOf(hilRuns, metricFn, false); | |
| const silS = bestOf(silRuns, metricFn, false); | |
| return { | |
| key: `stage:${name}`, label: name, unit: "ms", showSil: true, | |
| hil: hilS != null ? fmtNumStr(hilS.value, 1) : null, | |
| sil: silS != null ? fmtNumStr(silS.value, 1) : null, | |
| empty: hilS == null, | |
| note: hilS | |
| ? `Best run · ${hilS.run.engine || "—"} · batch ${hilS.run.batch_size ?? 1} · lower is better` | |
| : `No ${name.toLowerCase()} timing published at this allocation`, | |
| }; | |
| }); | |
| const kpis = [ | |
| { | |
| key: "throughput", label: thrLabel, unit: thrUnit, showSil: true, | |
| hil: thrHil != null ? fmtNumStr(thrHil.value, 1) : null, | |
| sil: thrSil != null ? `${fmtNumStr(thrSil.value, 1)} ${thrUnit}${thrSilDerived ? "*" : ""}` : null, | |
| empty: thrHil == null, | |
| note: thrHil | |
| ? `Best run · ${thrHil.run.engine || "—"} · batch ${thrHil.run.batch_size ?? 1}${thrHilDerived ? " · derived from latency (1000 / p50)" : ""}` | |
| : `No ${thrLabel.toLowerCase()} published at this allocation`, | |
| }, | |
| { | |
| key: "second", label: secLabel, unit: secUnit, showSil: true, | |
| hil: secHil != null ? fmtNumStr(secHil.value, 2) : null, | |
| sil: secSil != null ? `${fmtNumStr(secSil.value, 2)} ${secUnit}` : null, | |
| empty: secHil == null, | |
| note: secHil ? `Best run · ${secHil.run.engine || "—"} · lower is better` : `No ${secLabel.toLowerCase()} published at this allocation`, | |
| }, | |
| { | |
| key: "accuracy", label: "Accuracy", unit: "", showSil: true, | |
| hil: accHil != null ? fmtAccStr(accHil.value) : null, | |
| sil: accSil != null ? fmtAccStr(accSil.value) : null, | |
| empty: accHil == null, | |
| warn: accDeltaPts != null && accDeltaPts < -2, | |
| note: isRefArt | |
| ? `${(accRef && accRef.art || "FP32").toUpperCase()} reference` | |
| : accDeltaPts == null | |
| ? (accHil == null ? "No accuracy run at this allocation" : "No FP32/FP16 reference published to compare against") | |
| : `${accDeltaPts < -0.05 ? "▼" : accDeltaPts > 0.05 ? "▲" : "±"} ${fmtNumStr(Math.abs(accDeltaPts), 1)} pts vs ${accRef.art.toUpperCase()} (${fmtAccStr(accRef.acc)}) · ${accHil.run.engine || "—"}`, | |
| }, | |
| // "NPU compute used" used to live here as its own KPI card, but it was | |
| // just the resourcesCardHTML "Compute" row (alloc.tops/topsTotal) plus | |
| // its cores/freq/instances restated in the note — the same allocation | |
| // data shown twice in one glance. Dropped; resourcesCardHTML is now the | |
| // single place hardware/compute utilization is shown. | |
| ].concat(stageKpis); | |
| /* ---- Bar chart: one best bar per engine + kind ---- | |
| Real throughput always wins within an engine+kind; a derived value only | |
| fills in for an engine+kind that reported no real throughput at all | |
| (never overrides one that did — see bestThrOf above). */ | |
| function bestThrPerEngineKind() { | |
| const real = bestPerEngineKind(hilRuns, silRuns, throughputValueOf, true); | |
| if (!canDeriveFps) return real; | |
| const covered = new Set(real.map(b => `${b.engine}|${b.kind}`)); | |
| const derivedOnly = bestPerEngineKind( | |
| hilRuns.filter(r => !covered.has(`${r.engine || "—"}|hil`)), | |
| silRuns.filter(r => !covered.has(`${r.engine || "—"}|estimate`)), | |
| derivedFps, true | |
| ).map(b => ({ ...b, derived: true })); | |
| return real.concat(derivedOnly).sort((a, b) => b.value - a.value); | |
| } | |
| let barList = unitSafe === "lat" ? bestPerEngineKind(hilRuns, silRuns, latencyMsOf, false) : bestThrPerEngineKind(); | |
| let chartUnit = unitSafe === "lat" ? "ms" : thrUnit; | |
| /* Kept to a single short clause — best-run/allocation/HIL·SIL/derived-value | |
| context is already visible per bar (kind label, "*" marker + its own | |
| hover tooltip below), so repeating it here was pure duplication. */ | |
| let chartNote = unitSafe === "lat" | |
| ? `${secLabel === "TTFT" ? "Latency" : secLabel} (ms) — lower is better.` | |
| : `${thrLabel} (${thrUnit}) — higher is better.`; | |
| // Neither throughput nor latency exists for a dual-encoder model (its bars | |
| // would otherwise always read "No throughput published") — chart one bar | |
| // per encoder tower instead, same per-stage data as the stageKpis cards. | |
| let usedStageBars = false; | |
| if (!barList.length && stageNames.length) { | |
| barList = stageNames.map(name => { | |
| const metricFn = stageMetricFn(name); | |
| const best = bestOf(hilRuns, metricFn, false) || bestOf(silRuns, metricFn, false); | |
| return best ? { engine: name, kind: best.run.kind === "hil" ? "hil" : "estimate", value: best.value, run: best.run } : null; | |
| }).filter(Boolean); | |
| chartUnit = "ms"; | |
| chartNote = "Per-stage NPU time (ms) — lower is better."; | |
| usedStageBars = true; | |
| } | |
| const maxV = barList.length ? Math.max(...barList.map(b => b.value)) : 1; | |
| const bars = barList.map(b => ({ | |
| ...b, | |
| valStr: (unitSafe === "lat" && !usedStageBars) ? fmtNumStr(b.value, 2) : fmtNumStr(b.value, 1), | |
| pct: Math.max(8, Math.round((b.value / (maxV || 1)) * 100)), | |
| // Present on a hybrid run's own utilization block — lets the bar itself | |
| // render as an NPU/CPU stack instead of one flat HIL/SIL color. | |
| npuPct: b.run?.utilization?.npu_offload_pct ?? null, | |
| cpuPct: b.run?.utilization?.cpu_offload_pct ?? null, | |
| })); | |
| /* ---- Hardware resources used, straight from the selected allocation ---- | |
| Compute (TOPS) is promoted to the card's headline number (see | |
| resourcesCardHTML) instead of sitting in this list as just another row — | |
| it's the one figure "NPU compute used" used to headline before that KPI | |
| card was folded into this one. */ | |
| const resources = alloc ? [ | |
| { l: "NPU instances", v: `${alloc.insts ?? "—"}${alloc.instsTotal != null ? ` / ${alloc.instsTotal}` : ""}`, pct: pctOf(alloc.insts, alloc.instsTotal) }, | |
| { l: "AI cores", v: `${alloc.cores ?? "—"}${alloc.coresTotal != null ? ` / ${alloc.coresTotal}` : ""}`, pct: pctOf(alloc.cores, alloc.coresTotal) }, | |
| { l: "Frequency", v: `${alloc.freq ?? "—"}${alloc.freqTotal != null ? ` / ${alloc.freqTotal} MHz` : ""}`, pct: pctOf(alloc.freq, alloc.freqTotal) }, | |
| ] : []; | |
| /* ---- Accuracy vs reference card ---- */ | |
| const accIsPct = accHil != null && accHil.value <= 1.5; | |
| const accCard = { valueStr: accHil != null ? fmtAccStr(accHil.value) : "—", pct: 0, deltaHTML: "", note: "" }; | |
| if (accHil != null) accCard.pct = Math.max(0, Math.min(100, accIsPct ? Math.round(accHil.value * 100) : Math.round(accHil.value))); | |
| if (accHil == null) { | |
| accCard.note = "No accuracy run at this allocation."; | |
| } else if (isRefArt) { | |
| accCard.note = `Reference precision${accRef ? ` (${accRef.art.toUpperCase()})` : ""} — quantized precisions are scored against this.`; | |
| } else if (accRef && accDeltaPts != null) { | |
| const cls = accDeltaPts < -0.05 ? "acc-down" : accDeltaPts > 0.05 ? "acc-up" : ""; | |
| const sign = accDeltaPts < -0.05 ? "▼" : accDeltaPts > 0.05 ? "▲" : "±"; | |
| accCard.deltaHTML = `<div class="pm-acc-delta ${cls}">${sign} ${esc(fmtNumStr(Math.abs(accDeltaPts), 1))} pts vs ${esc(accRef.art.toUpperCase())}</div>`; | |
| accCard.note = `Best run · ${accHil.run.engine || "—"} · ${accRef.art.toUpperCase()} reference ${fmtAccStr(accRef.acc)}`; | |
| } else { | |
| accCard.note = `Best run · ${accHil.run.engine || "—"} · no FP32/FP16 reference published to compare against.`; | |
| } | |
| /* ---- Selector chips ---- */ | |
| const precisionChips = arts.map(a => { | |
| const aRuns = cfg ? runsForArtifact(cfg.metrics || {}, a) : []; | |
| const isRef = accRef && accRef.art === a; | |
| return { id: a, label: a.toUpperCase(), sub: isRef ? "reference" : `${aRuns.length || 1} run${(aRuns.length || 1) === 1 ? "" : "s"}`, selected: a === art }; | |
| }); | |
| const allocationChips = allocations.map(a => ({ | |
| id: a.id, label: a.label, selected: a.id === allocId, | |
| title: `${a.cores ?? "—"}${a.coresTotal != null ? `/${a.coresTotal}` : ""} cores · ${a.freq ?? "—"} MHz · ${a.tops ?? "—"}${a.topsTotal != null ? `/${a.topsTotal}` : ""} TOPS`, | |
| })); | |
| const hwChips = hwConfigs.map(c => ({ id: c.id, label: c.label || c.id, selected: !!(cfg && c.id === cfg.id) })); | |
| return { | |
| m, cfg, hwConfigs, fmt, art, arts, precisionChips, allocationChips, hwChips, | |
| allocations, alloc, allocId, unit: unitSafe, runs, hilRuns, silRuns, | |
| kpis, bars, chartUnit, chartNote, resources, accCard, | |
| }; | |
| } | |
| /* ---------- Shared markup builders (page + modal) ---------- */ | |
| function chipRowHTML(items, field, scope, extraCls = "") { | |
| if (!items.length) return ""; | |
| return items.map(it => ` | |
| <button type="button" class="chip-btn ${extraCls} ${it.selected ? "active" : ""}" | |
| data-mdl-set="${esc(field)}" data-mdl-scope="${esc(scope)}" data-mdl-value="${esc(it.id)}" | |
| aria-pressed="${it.selected ? "true" : "false"}"${it.title ? ` title="${esc(it.title)}"` : ""}>${esc(it.label)}${it.sub ? `<span class="mdl-chip-sub"> · ${esc(it.sub)}</span>` : ""}</button>`).join(""); | |
| } | |
| /* "NPU + CPU" (see defaultRowCompute) is a hybrid run, not the CPU fallback | |
| this ternary would otherwise mis-color it as — give it its own badge class. */ | |
| function computeBadgeCls(compute) { | |
| return compute === "NPU" ? "npu" : compute === "DSP" ? "dsp" : compute === "NPU + CPU" ? "hybrid" : "cpu"; | |
| } | |
| function heroChipsHTML(m, fmt, compute) { | |
| const chips = [`<span class="badge modality">${esc(m.modality || "LLM")}</span>`, `<span class="badge art">${esc(fmt)}</span>`]; | |
| if (compute) chips.push(`<span class="badge ${computeBadgeCls(compute)}">${esc(compute)}</span>`); | |
| if (m.license) chips.push(`<span class="badge license">${esc(String(m.license).toUpperCase())}</span>`); | |
| return chips.join(""); | |
| } | |
| /* Modal header line under the model name: architecture text + the same | |
| modality/format/compute badges as the full page's hero (license | |
| omitted — the header is a title bar, not the summary). */ | |
| function modalHeaderMetaHTML(m, fmt, compute) { | |
| const parts = []; | |
| if (m.architecture) parts.push(`<span class="modal-head-arch">${esc(m.architecture)}</span>`); | |
| parts.push(`<span class="badge modality">${esc(m.modality || "LLM")}</span>`); | |
| parts.push(`<span class="badge art">${esc(fmt)}</span>`); | |
| if (compute) parts.push(`<span class="badge ${computeBadgeCls(compute)}">${esc(compute)}</span>`); | |
| return parts.join(""); | |
| } | |
| function modelSpecPairs(m, cfg, fmt, compute, art) { | |
| const runs = (art && cfg) ? runsForArtifact(cfg.metrics || {}, art) : []; | |
| const inputRes = runs.map(r => r.input_resolution).find(Boolean) || null; | |
| return [ | |
| { k: "Architecture", v: m.architecture || "Unknown" }, | |
| { k: "Base model", v: m.base_model || "—" }, | |
| { k: "Task", v: safeList(m.tasks, []).map(prettyTask).join(", ") || "—" }, | |
| { k: "Modality", v: m.modality || "LLM" }, | |
| { k: "Model size", v: m.model_size_m != null ? `${m.model_size_m}M params` : "—" }, | |
| { k: "Input resolution", v: inputRes || "—" }, | |
| { k: "Variant", v: m.variant || "Default" }, | |
| { k: "Format / compute", v: `${fmt}${compute ? " · " + compute : ""}` }, | |
| { k: "License", v: m.license || "—" }, | |
| ]; | |
| } | |
| /* Each axis is its own full-width row (label + wrapping chip strip) rather | |
| than one shared flex line — a model with many precisions/allocations | |
| (e.g. EfficientNet's 9 size variants) wraps within its own row instead of | |
| shoving the next group onto a stray line or stranding Qualification in | |
| dead space. */ | |
| function cfgBarHTML(V, scope) { | |
| const cfgCount = allRunsRowsFor(V.m, V.cfg, V.fmt).length; | |
| return ` | |
| <div class="mdl-cfgbar" role="group" aria-label="Configuration"> | |
| <div class="mdl-cfgbar-head"> | |
| <div class="mdl-cfg-label">Configuration</div> | |
| <div class="mdl-cfg-count">${cfgCount} configuration${cfgCount === 1 ? "" : "s"} available</div> | |
| </div> | |
| <div class="mdl-cfg-row"> | |
| <div class="mdl-cfg-label">Hardware config</div> | |
| <div class="mdl-cfg-chips">${V.hwChips.length ? chipRowHTML(V.hwChips, "cfgId", scope) : `<span class="subtle">No hardware config published</span>`}</div> | |
| </div> | |
| <div class="mdl-cfg-row"> | |
| <div class="mdl-cfg-label">Precision</div> | |
| <div class="mdl-cfg-chips">${V.precisionChips.length ? chipRowHTML(V.precisionChips, "precision", scope) : `<span class="subtle">No benchmarked precision</span>`}</div> | |
| </div> | |
| <div class="mdl-cfg-row"> | |
| <div class="mdl-cfg-label">NPU allocation</div> | |
| <div class="mdl-cfg-chips">${V.allocationChips.length ? chipRowHTML(V.allocationChips, "allocationId", scope) : `<span class="subtle">No allocation data</span>`}</div> | |
| </div> | |
| </div>`; | |
| } | |
| /* Modal density's precision + NPU selectors — same row-per-axis pattern | |
| as cfgBarHTML, minus hardware config/qualification (not shown in the | |
| compact modal). */ | |
| function modalSelectorsHTML(V) { | |
| return ` | |
| <div class="mdl-modal-selectors"> | |
| <div class="mdl-cfg-row"> | |
| <div class="mdl-cfg-label">Precision</div> | |
| <div class="mdl-cfg-chips">${V.precisionChips.length ? chipRowHTML(V.precisionChips, "precision", "modal") : `<span class="subtle">No benchmarked precision</span>`}</div> | |
| </div> | |
| <div class="mdl-cfg-row"> | |
| <div class="mdl-cfg-label">NPU</div> | |
| <div class="mdl-cfg-chips">${V.allocationChips.length ? chipRowHTML(V.allocationChips, "allocationId", "modal") : `<span class="subtle">No allocation data</span>`}</div> | |
| </div> | |
| </div>`; | |
| } | |
| function kpiCardHTML(k, compact) { | |
| return ` | |
| <div class="${compact ? "mdl-modal-kpi" : "mdl-card"} mdl-kpi-card"> | |
| <div class="metric-card-title">${esc(k.label)}</div> | |
| <div class="mdl-kpi-row"> | |
| <div class="mdl-kpi-hil"> | |
| ${!compact ? `<div class="mdl-kpi-src mdl-kpi-src--hil">HIL · MEASURED</div>` : ""} | |
| <div class="mdl-kpi-num">${k.hil != null ? esc(k.hil) : "—"}${k.unit ? `<span class="mdl-kpi-unit"> ${esc(k.unit)}</span>` : ""}</div> | |
| </div> | |
| ${k.showSil ? ` | |
| <div class="mdl-kpi-sil"> | |
| ${!compact ? `<div class="mdl-kpi-src mdl-kpi-src--sil">SIL · EST.</div>` : ""} | |
| <div class="mdl-kpi-sil-num">${k.sil != null ? esc(k.sil) : (compact ? "" : "—")}</div> | |
| </div>` : ""} | |
| </div> | |
| <div class="mdl-kpi-note ${k.empty ? "mdl-kpi-note--empty" : ""} ${k.warn ? "mdl-kpi-note--warn" : ""}" title="${esc(k.note)}">${esc(k.note)}</div> | |
| </div>`; | |
| } | |
| function kpiStripHTML(V, compact) { | |
| return `<div class="${compact ? "mdl-modal-kpis" : "mdl-kpi-strip"}">${V.kpis.map(k => kpiCardHTML(k, compact)).join("")}</div>`; | |
| } | |
| /* A hybrid run's bar (npuPct + cpuPct both published) is a cumulative stack | |
| — the NPU share at the base and the CPU fallback share on top, together | |
| filling the same height a flat HIL/SIL bar would — instead of one flat | |
| color that hides how much of the graph actually fell back to CPU. */ | |
| function barFillHTML(b) { | |
| if (b.npuPct != null && b.cpuPct != null) { | |
| return ` | |
| <div class="mdl-bar-fill mdl-bar-fill--split" style="height:${b.pct}%"> | |
| <div class="mdl-bar-seg mdl-bar-seg--cpu" style="height:${b.cpuPct}%"></div> | |
| <div class="mdl-bar-seg mdl-bar-seg--npu" style="height:${b.npuPct}%"></div> | |
| </div>`; | |
| } | |
| return `<div class="mdl-bar-fill mdl-bar-fill--${b.kind === "hil" ? "hil" : "sil"}" style="height:${b.pct}%"></div>`; | |
| } | |
| function barsHTML(V) { | |
| if (!V.bars.length) { | |
| return `<div class="mdl-bars-empty">No ${V.unit === "lat" ? "latency" : "throughput"} published at this allocation.</div>`; | |
| } | |
| const cols = V.bars.map(b => ` | |
| <div class="mdl-bar-col" title="${b.derived ? "Derived as 1000 / p50 latency (batch-1) — this run reported no direct throughput" : ""}"> | |
| <div class="mdl-bar-val">${esc(b.valStr)}${b.derived ? "*" : ""}</div> | |
| <div class="mdl-bar-track">${barFillHTML(b)}</div> | |
| </div>`).join(""); | |
| const labels = V.bars.map(b => ` | |
| <div class="mdl-bar-lbl"> | |
| <div class="mdl-bar-name">${esc(b.engine)}</div> | |
| <div class="mdl-bar-kind mdl-bar-kind--${b.kind === "hil" ? "hil" : "sil"}">${b.kind === "hil" ? "HIL · measured" : "SIL · estimated"}${b.derived ? " *" : ""}</div> | |
| </div>`).join(""); | |
| const anySplit = V.bars.some(b => b.npuPct != null && b.cpuPct != null); | |
| const legend = anySplit ? ` | |
| <div class="mdl-bar-split-legend"> | |
| <span class="mdl-bar-split-item"><span class="mdl-bar-split-swatch mdl-bar-split-swatch--npu"></span>NPU</span> | |
| <span class="mdl-bar-split-item"><span class="mdl-bar-split-swatch mdl-bar-split-swatch--cpu"></span>CPU</span> | |
| </div>` : ""; | |
| return `<div class="mdl-bars">${cols}</div><div class="mdl-bar-baseline"></div><div class="mdl-bar-labels">${labels}</div>${legend}`; | |
| } | |
| function chartCardHTML(V, scope) { | |
| const units = [{ id: "thr", label: V.chartUnit }, { id: "lat", label: "ms" }]; | |
| const toggle = units.map(u => `<button type="button" class="pm-seg-btn ${V.unit === u.id ? "active" : ""}" data-mdl-set="unit" data-mdl-scope="${esc(scope)}" data-mdl-value="${esc(u.id)}">${esc(u.label)}</button>`).join(""); | |
| return ` | |
| <div class="mdl-card mdl-chart-card"> | |
| <div class="mdl-chart-head"> | |
| <div class="metric-card-title" style="margin-bottom:0">By runtime</div> | |
| <div class="pm-metric-seg" role="group" aria-label="Chart unit">${toggle}</div> | |
| </div> | |
| <div class="mdl-chart-note">${esc(V.chartNote)}</div> | |
| ${barsHTML(V)} | |
| </div>`; | |
| } | |
| /* Headline number + note, styled like the old standalone "NPU compute used" | |
| KPI card (same .mdl-kpi-num treatment) — kept so folding that card into | |
| this one didn't also flatten its visual weight into just another list row. */ | |
| function resourcesHighlightHTML(alloc) { | |
| if (!alloc || alloc.tops == null) return ""; | |
| const note = [ | |
| alloc.cores != null ? `${esc(String(alloc.cores))}${alloc.coresTotal != null ? ` of ${esc(String(alloc.coresTotal))}` : ""} AI cores` : null, | |
| alloc.freq != null ? `${esc(String(alloc.freq))} MHz` : null, | |
| alloc.insts != null ? `${esc(String(alloc.insts))}${alloc.instsTotal != null ? `/${esc(String(alloc.instsTotal))}` : ""} NPU instances` : null, | |
| ].filter(Boolean).join(" · "); | |
| return ` | |
| <div class="mdl-res-highlight"> | |
| <div class="mdl-res-highlight-num">${esc(String(alloc.tops))}<span class="mdl-res-highlight-unit"> ${alloc.topsTotal != null ? `of ${esc(String(alloc.topsTotal))} TOPS` : "TOPS"}</span></div> | |
| ${note ? `<div class="mdl-res-highlight-note">${note}</div>` : ""} | |
| </div>`; | |
| } | |
| /* NPU/CPU offload split — only rendered when the benchmark YAML actually | |
| published it (configuration.npu_offload_pct / cpu_offload_pct on a hybrid | |
| run); a pure-NPU run carries neither field. Given its own two-tone bar | |
| (NPU/CPU badge colors) rather than the generic single-value track used | |
| below, since "what fraction ran where" is a different kind of fact than | |
| "how much of the available resource did this run use". */ | |
| function resourcesSplitHTML(alloc) { | |
| if (!alloc || (alloc.npuPct == null && alloc.cpuPct == null)) return ""; | |
| return ` | |
| <div class="mdl-res-split"> | |
| <div class="mdl-res-split-head"> | |
| <span class="k">NPU / CPU split</span> | |
| <span class="v"> | |
| <span class="mdl-res-split-tag mdl-res-split-tag--npu">${alloc.npuPct != null ? `${esc(String(alloc.npuPct))}%` : "—"} NPU</span> | |
| <span class="mdl-res-split-tag mdl-res-split-tag--cpu">${alloc.cpuPct != null ? `${esc(String(alloc.cpuPct))}%` : "—"} CPU</span> | |
| </span> | |
| </div> | |
| <div class="mdl-res-track mdl-res-track--split"> | |
| <div class="mdl-res-fill mdl-res-fill--npu" style="width:${alloc.npuPct ?? 0}%"></div><div class="mdl-res-fill mdl-res-fill--cpu" style="width:${alloc.cpuPct ?? 0}%"></div> | |
| </div> | |
| </div>`; | |
| } | |
| function resourcesCardHTML(V) { | |
| const alloc = V.alloc; | |
| if (!alloc) return `<div class="mdl-card"><div class="metric-card-title">Hardware resources used</div><div class="subtle">No allocation selected.</div></div>`; | |
| return ` | |
| <div class="mdl-card"> | |
| <div class="metric-card-title">Hardware resources used</div> | |
| ${resourcesHighlightHTML(alloc)} | |
| ${resourcesSplitHTML(alloc)} | |
| <div class="mdl-res-list"> | |
| ${V.resources.map(r => ` | |
| <div class="mdl-res-row"> | |
| <div class="mdl-res-head"><span class="k">${esc(r.l)}</span><span class="v" title="${esc(r.v)}">${esc(r.v)}</span></div> | |
| <div class="mdl-res-track"><div class="mdl-res-fill" style="width:${r.pct}%"></div></div> | |
| </div>`).join("")} | |
| </div> | |
| </div>`; | |
| } | |
| function accuracyCardHTML(V) { | |
| const c = V.accCard; | |
| return ` | |
| <div class="mdl-card mdl-card--accent"> | |
| <div class="metric-card-title">Accuracy vs reference</div> | |
| <div class="pm-acc-body" style="align-items:flex-start;text-align:left"> | |
| <div class="pm-acc-num ${c.valueStr === "—" ? "empty" : ""}">${esc(c.valueStr)}</div> | |
| ${c.deltaHTML} | |
| <div class="pm-acc-bar"><div class="pm-acc-bar-fill" style="width:${c.pct}%"></div></div> | |
| <div class="mdl-acc-note">${esc(c.note)}</div> | |
| </div> | |
| </div>`; | |
| } | |
| function perfRowHTML(V, scope, compact, extraSideHTML = "") { | |
| if (compact) { | |
| return ` | |
| <div class="mdl-modal-perf"> | |
| ${chartCardHTML(V, scope)} | |
| <div class="mdl-modal-side">${resourcesCardHTML(V)}${extraSideHTML}</div> | |
| </div>`; | |
| } | |
| return `<div class="mdl-perf-row">${chartCardHTML(V, scope)}${resourcesCardHTML(V)}${accuracyCardHTML(V)}</div>`; | |
| } | |
| /* Compact "route to the full page" links — sits beside the resources card | |
| in the modal's right column (not a separate full-width row: at 248px | |
| density there's no room to spare below a two-column area). No direct | |
| "Open repo" link here — the repo is always reachable via the Download tab | |
| (repo link + hf download command), so a second shortcut was redundant. */ | |
| function modalQuickLinksHTML(state, V, runsCount) { | |
| const rows = [ | |
| { tab: "runs", label: `All ${runsCount} benchmark runs →` }, | |
| // No model file for the selected precision -> nothing to download yet, so | |
| // the link to the full page's Download & run tab is dropped rather than | |
| // sending the user to a tab whose command can't actually be run. | |
| ...(artifactFileMissing(V.m, V.fmt, V.art) ? [] : [{ tab: "dl", label: "Download & run instructions →" }]), | |
| ].map(l => `<a href="#" class="mdl-ql-link" data-mdl-goto-page="1" data-key="${esc(state.key)}" data-cfg="${esc(V.cfg?.id || "")}" data-fmt="${esc(state.fmt)}" data-tab="${l.tab}">${esc(l.label)}</a>`).join(""); | |
| return `<div class="mdl-card mdl-modal-quicklinks">${rows}</div>`; | |
| } | |
| function runsTableHTML(V) { | |
| const rows = allRunsRowsFor(V.m, V.cfg, V.fmt); | |
| if (!rows.length) return `<div class="subtle" style="padding:12px 0">No benchmark runs published for this hardware config yet.</div>`; | |
| const body = rows.map(({ art, r, allocId, allocLabel }) => { | |
| const sel = art === V.art && allocId === V.allocId; | |
| const thr = throughputOf(r); | |
| const lat = latencyMsOf(r); | |
| const acc = accuracyFromBlock(r); | |
| const qual = r.kind === "hil" ? `<span class="rt-kind rt-hil">HIL</span>` : `<span class="rt-kind rt-est">SIL</span>`; | |
| return `<tr class="${sel ? "rt-row--sel" : ""}"> | |
| <td class="rt-eng">${esc(art)}</td> | |
| <td>${esc(allocLabel)}</td> | |
| <td class="rt-eng">${esc(r.engine || "—")}</td> | |
| <td>${qual}</td> | |
| <td class="rt-num">${thr.value != null ? esc(fmtNumStr(thr.value, 1)) + " " + esc(thr.unit) : "—"}</td> | |
| <td class="rt-num">${lat != null ? esc(fmtNumStr(lat, 2)) + " ms" : "—"}</td> | |
| <td class="rt-num">${esc(fmtAccStr(acc))}</td> | |
| <td class="rt-num">${esc(r.last_updated || "—")}</td> | |
| </tr>`; | |
| }).join(""); | |
| return ` | |
| <div class="mdl-runs-head"> | |
| <div class="metric-card-title" style="margin-bottom:0">All benchmark runs · ${rows.length} published</div> | |
| <div class="subtle tight">Rows matching the current selection are highlighted</div> | |
| </div> | |
| <div class="rt-table-wrap"> | |
| <table class="rt-table"> | |
| <thead><tr><th>Precision</th><th>NPU allocation</th><th>Runtime</th><th>Qual.</th><th>Throughput</th><th>Latency</th><th>Accuracy</th><th>Updated</th></tr></thead> | |
| <tbody>${body}</tbody> | |
| </table> | |
| </div> | |
| <div class="subtle tight" style="margin-top:10px;line-height:1.5">HIL = measured on hardware. SIL = estimated by the PPA estimator. Blank cells mean the metric was not reported by that run — never zero.</div>`; | |
| } | |
| /* MWMX AI Compiler installer/package live behind Renesas's customer portal, | |
| not on a public URL the doc reveals — link straight to the gated portal | |
| page rather than the generic myRenesas landing page. Only relevant for the | |
| MWMX/NNAC (ONNX) toolchain; GGUF's llama.cpp-style runner needs no | |
| separate compiler download, so callers gate this on the run's engine. */ | |
| const MYRENESAS_AI_COMPILER_URL = "https://www.renesas.com/en/myrenesas/secure-portals/gen5-r-car-x5x-sw-ai"; | |
| function compilerLinkHTML(run) { | |
| if (String(run?.engine || "").toLowerCase() !== "mwmx") return ""; | |
| return `<div class="note">Get the <b>MWMX AI Compiler</b> (installer + NNAC portable package, myRenesas account required) from <a href="${esc(MYRENESAS_AI_COMPILER_URL)}" target="_blank" rel="noopener noreferrer">myRenesas › Secure Software Portal</a>.</div>`; | |
| } | |
| /* A benchmark YAML can carry an explicit `reproduce:` block — exact commands | |
| its author verified on real hardware (see generate_models_json.py | |
| parse_reproduce()) — because the real deployment flow (single vs. multi NPU | |
| cluster, an ORT-split subgraph, a GGUF runner binary, ...) varies per model | |
| and isn't safe to guess. When the selected run has one, it replaces the | |
| generic per-format placeholder flow below entirely; it keeps the same | |
| two-column layout (toolchain meta + commands) so the two paths look like | |
| one feature, not two different UIs. */ | |
| function reproduceStepsHTML(run, cfg, toolchain) { | |
| const rp = run.reproduce; | |
| const steps = safeList(rp.steps, []); | |
| const body = steps.map((s, i) => { | |
| const isNote = s.kind === "note"; | |
| return ` | |
| <div class="mdl-dl-cmd-block"> | |
| <div class="metric-card-title">${i + 1} · ${esc(s.title || "Run")}</div> | |
| ${isNote | |
| ? `<div class="cmd-note-text">${esc(s.command)}</div>` | |
| : `<div class="cmd-row"><pre><code>${esc(s.command)}</code></pre> | |
| <button class="copy-btn" data-copy="1" data-copy-text="${esc(s.command)}"><span class="icon">⧉</span><span class="txt">Copy</span></button> | |
| </div>`} | |
| ${s.expected ? `<div class="subtle tight">Expected: ${esc(s.expected)}</div>` : ""} | |
| </div>`; | |
| }).join(""); | |
| return ` | |
| <div class="mdl-dl-grid dl-tabs-wrap"> | |
| <div class="mdl-dl-col mdl-dl-col--meta"> | |
| <div class="mdl-card"> | |
| <div class="metric-card-title">Toolchain requirements</div> | |
| <div class="mdl-kv-list">${toolchain.map(t => `<div class="mdl-kv-row"><span class="k">${esc(t.k)}</span><span class="v" title="${esc(t.v)}">${esc(t.v)}</span></div>`).join("")}</div> | |
| </div> | |
| ${compilerLinkHTML(run)} | |
| ${rp.reference ? `<div class="note">Verified against <code>${esc(rp.reference)}</code>.</div>` : ""} | |
| </div> | |
| <div class="mdl-dl-col mdl-dl-col--cmds"> | |
| <div class="mdl-card mdl-dl-cmds-card"> | |
| <div class="metric-card-title">Reproduce · ${esc(cfg?.label || cfg?.id || "")}</div> | |
| ${body} | |
| </div> | |
| ${rp.notes ? `<div class="subtle tight" style="margin-top:8px">${esc(rp.notes)}</div>` : ""} | |
| </div> | |
| </div>`; | |
| } | |
| /* Driven by the selected precision. ONNX gets the README's own-repo | |
| layout (download cmd + onnxruntime/RcarNpuExecutionProvider snippet + | |
| board setup + toolchain + a note on what the catalog can't show yet); | |
| GGUF reuses the existing deploy-with-binaries renderActions() flow. Either | |
| is overridden by an explicit reproduce: block when the selected run has one. */ | |
| function downloadRunHTML(V) { | |
| const { m, cfg, fmt, art } = V; | |
| const bestRun = V.hilRuns[0] || V.silRuns[0] || V.runs[0] || null; | |
| const inputRes = V.runs.map(r => r.input_resolution).find(Boolean); | |
| const toolchain = [ | |
| { k: "Hardware config", v: cfg?.label || cfg?.id || "—" }, | |
| { k: "Runtime engine", v: bestRun?.engine || "—" }, | |
| { k: "Toolchain version", v: bestRun?.toolchain_version || "—" }, | |
| { k: "Execution provider", v: bestRun?.execution_provider ? String(bestRun.execution_provider).toUpperCase() : "NPU" }, | |
| { k: "Batch size", v: bestRun?.batch_size != null ? String(bestRun.batch_size) : "—" }, | |
| ]; | |
| if (inputRes) toolchain.push({ k: "Input resolution", v: inputRes }); | |
| if (bestRun?.reproduce?.steps?.length) { | |
| return reproduceStepsHTML(bestRun, cfg, toolchain); | |
| } | |
| if (fmt !== "ONNX") { | |
| return renderActions(m.key, fmt, defaultRowCompute(m, cfg), defaultPurposeForFormat(fmt), art, cfg?.id, "tab"); | |
| } | |
| const repoId = m.onnx_repo || ""; | |
| if (!repoId) return `<div class="note note-warn">No ONNX repo published for this model yet.</div>`; | |
| if (!art) return `<div class="note note-warn">No benchmarked precision to build a download command from yet.</div>`; | |
| const include = downloadIncludePattern(fmt, art); | |
| const cmdRepo = `hf download ${repoId} --repo-type=model --include "${include}"`; | |
| const isCV = ["CV", "DIFFUSION"].includes(String(m.modality || "").toUpperCase()); | |
| const dims = (inputRes && /^\d+x\d+$/i.test(inputRes)) ? inputRes.split(/x/i).join(", ") : "224, 224"; | |
| const py = isCV | |
| ? `import onnxruntime as ort, numpy as np\n` | |
| + `sess = ort.InferenceSession("${art}/<artifact_file>.onnx", providers=["RcarNpuExecutionProvider"])\n` | |
| + `x = np.zeros((1, 3, ${dims}), dtype=np.float32)\n` | |
| + `print(sess.run(None, {sess.get_inputs()[0].name: x})[0].argmax())` | |
| : `import onnxruntime as ort, numpy as np\n` | |
| + `sess = ort.InferenceSession("${art}/<artifact_file>.onnx", providers=["RcarNpuExecutionProvider"])\n` | |
| + `input_ids = np.array([[1]], dtype=np.int64) # replace with a real tokenized prompt\n` | |
| + `print(sess.run(None, {sess.get_inputs()[0].name: input_ids}))`; | |
| const boardSteps = [ | |
| { n: "1", t: "Copy the artifact to the board", c: `scp -r ${art}/ root@x5h:/opt/models/${m.key}/` }, | |
| { n: "2", t: "Bring up the NPU", c: `cd /opt/models/${m.key} && bash ./setup_npu.sh` }, | |
| { n: "3", t: "Benchmark it yourself", c: `python3 run_infer.py --model ${art}/<artifact_file>.onnx --provider RcarNpuExecutionProvider` }, | |
| ]; | |
| return ` | |
| <div class="mdl-dl-grid dl-tabs-wrap"> | |
| <div class="mdl-dl-col mdl-dl-col--meta"> | |
| <div class="mdl-card"> | |
| <div class="metric-card-title">Toolchain requirements</div> | |
| <div class="mdl-kv-list">${toolchain.map(t => `<div class="mdl-kv-row"><span class="k">${esc(t.k)}</span><span class="v" title="${esc(t.v)}">${esc(t.v)}</span></div>`).join("")}</div> | |
| </div> | |
| ${compilerLinkHTML(bestRun)} | |
| <div class="note">Artifact file sizes and checksums aren't captured by <code>generate_models_json.py</code> yet — it only reads filenames from the repo's file tree. Browse the exact files in the <a href="${esc(hfModelUrl(repoId))}" target="_blank" rel="noopener noreferrer">ONNX repo</a>.</div> | |
| </div> | |
| <div class="mdl-dl-col mdl-dl-col--cmds"> | |
| <div class="mdl-card mdl-dl-cmds-card"> | |
| <div class="mdl-dl-cmd-block"> | |
| <div class="metric-card-title">1 · Download the ${esc(art.toUpperCase())} artifact</div> | |
| <div class="cmd-row"><pre><code>${esc(cmdRepo)}</code></pre> | |
| <button class="copy-btn" data-copy="1" data-copy-text="${esc(cmdRepo)}"><span class="icon">⧉</span><span class="txt">Copy</span></button> | |
| </div> | |
| </div> | |
| <div class="mdl-dl-cmd-block"> | |
| <div class="metric-card-title">2 · Run inference on the board</div> | |
| <div class="cmd-row"><pre><code>${esc(py)}</code></pre> | |
| <button class="copy-btn" data-copy="1" data-copy-text="${esc(py)}"><span class="icon">⧉</span><span class="txt">Copy</span></button> | |
| </div> | |
| <div class="subtle tight" style="margin-top:8px">Replace <code><artifact_file>.onnx</code> with the exact filename from the repo — the catalog generator doesn't capture individual filenames yet.</div> | |
| </div> | |
| <div class="mdl-dl-cmd-block"> | |
| <div class="metric-card-title">Board setup · ${esc(cfg?.label || cfg?.id || "R-Car X5H")}</div> | |
| <ol class="dl-steps"> | |
| ${boardSteps.map(s => ` | |
| <li> | |
| <div class="dl-step-h"><span class="dl-step-n">${s.n}</span> ${esc(s.t)}</div> | |
| <div class="cmd-row"><pre><code>${esc(s.c)}</code></pre> | |
| <button class="copy-btn" data-copy="1" data-copy-text="${esc(s.c)}"><span class="icon">⧉</span><span class="txt">Copy</span></button> | |
| </div> | |
| </li>`).join("")} | |
| </ol> | |
| </div> | |
| </div> | |
| </div> | |
| </div>`; | |
| } | |
| /* The page's own topbar already carries the Renesas wordmark + primary | |
| nav (Overview/Catalog) — repeating it here would just be a second | |
| header. The name and modality are both restated a breath away (hero | |
| title, then chips), so a "Catalog / CV / <name>" trail here would be | |
| the third repetition on screen; a bare back arrow says "where am I" | |
| just as well and keeps this row tight. */ | |
| /* Breadcrumb row (navigation) + title row (name, meta, chips) merged into | |
| one compact head block — kept as two separate bands before, which | |
| duplicated padding/borders and pushed the actual performance content | |
| far down the page for no reason (breadcrumb and title never need to | |
| scroll independently of each other). */ | |
| function pageHeadHTML(m, fmt, compute) { | |
| const repoId = fmt === "GGUF" ? (m.gguf_repo || "") : (m.onnx_repo || ""); | |
| const metaBits = [m.architecture, safeList(m.tasks, [])[0] ? prettyTask(m.tasks[0]) : null, m.model_size_m != null ? `${m.model_size_m}M params` : null].filter(Boolean); | |
| return ` | |
| <div class="mdl-pagehead"> | |
| <div class="mdl-pagehead-top"> | |
| <div class="mdl-hero-title-row"> | |
| <button type="button" class="icon-btn mdl-back-btn" data-mdl-nav="sec-catalog" aria-label="Back to catalog" title="Back to catalog">←</button> | |
| <div class="mdl-hero-name">${esc(m.display_name || m.key)}</div> | |
| </div> | |
| <div class="mdl-pagehead-actions"> | |
| <div class="mdl-updated">Benchmarks updated <b>${esc(m.last_modified ? String(m.last_modified).slice(0, 10) : "—")}</b></div> | |
| ${repoId ? `<a class="btn-primary" href="${esc(hfModelUrl(repoId))}" target="_blank" rel="noopener noreferrer">Open ${esc(fmt)} repo ↗</a>` : ""} | |
| </div> | |
| </div> | |
| <div class="mdl-hero-meta"> | |
| <span class="mdl-hero-meta-txt">${esc(metaBits.join(" · "))}</span> | |
| <span class="mdl-hero-sep"></span> | |
| ${heroChipsHTML(m, fmt, compute)} | |
| </div> | |
| </div>`; | |
| } | |
| function summaryRowHTML(m, cfg, fmt, compute, art) { | |
| const specs = modelSpecPairs(m, cfg, fmt, compute, art); | |
| return ` | |
| <div class="mdl-summary-row"> | |
| <div class="mdl-card mdl-card--rule"> | |
| <div class="metric-card-title">Model summary</div> | |
| <div class="mdl-spec-grid">${specs.map(s => `<div class="mdl-spec-item"><div class="mdl-spec-label">${esc(s.k)}</div><div class="mdl-spec-value">${esc(s.v)}</div></div>`).join("")}</div> | |
| </div> | |
| <div class="mdl-example">${exampleImageHTML(m)}</div> | |
| </div>`; | |
| } | |
| function footerHTML() { | |
| return ` | |
| <div class="mdl-footer"> | |
| <img src="assets/logo_renesas_blue.svg" alt="Renesas" onerror="this.style.display='none'"> | |
| </div>`; | |
| } | |
| /* ---------- Full page (#sec-model) ---------- */ | |
| function renderFullModelPageHTML(state) { | |
| const m = KEY_TO_MODEL[state.key]; | |
| if (!m) return `<div class="note note-warn">Model not found.</div>`; | |
| if (isComingSoon(m)) { | |
| return `${pageHeadHTML(m, state.fmt, defaultRowCompute(m, null))}${comingSoonDetailsHTML(m)}${footerHTML()}`; | |
| } | |
| const V = buildDetailView(state.key, state.cfgId, state.fmt, state.precision, state.allocationId, state.unit); | |
| state.cfgId = V.cfg?.id || state.cfgId || ""; | |
| state.precision = V.art; | |
| state.allocationId = V.allocId; | |
| state.unit = V.unit; | |
| const compute = defaultRowCompute(V.m, V.cfg); | |
| const uid = `mdlp_${String(state.key).replace(/\W+/g, "_")}`; | |
| // No model file for the selected precision -> the Download & run tab has | |
| // nothing runnable to show, so it's dropped rather than left open on a | |
| // command that would fail. Falls back to the runs tab if that's where the | |
| // (now-hidden) dl tab was left selected. | |
| const fileMissing = artifactFileMissing(m, state.fmt, V.art); | |
| const tab = (!fileMissing && state.tab === "dl") ? "dl" : "runs"; | |
| return ` | |
| ${pageHeadHTML(m, state.fmt, compute)} | |
| ${missingFileBannerHTML(m, state.fmt, V.art)} | |
| ${summaryRowHTML(m, V.cfg, state.fmt, compute, V.art)} | |
| ${cfgBarHTML(V, "page")} | |
| <div class="mdl-perf-head"> | |
| <div class="mdl-perf-title">Performance · ${esc(state.fmt)} · ${esc(compute)} · ${esc((V.art || "—").toUpperCase())}${V.alloc ? " · " + esc(V.alloc.label) : ""}</div> | |
| <div class="mdl-perf-hint">Every figure below reflects the selection above</div> | |
| </div> | |
| ${kpiStripHTML(V, false)} | |
| ${perfRowHTML(V, "page", false)} | |
| <input type="radio" name="${uid}" id="${uid}_runs" class="det-radio det-radio--runs" ${tab === "runs" ? "checked" : ""}> | |
| ${fileMissing ? "" : `<input type="radio" name="${uid}" id="${uid}_dl" class="det-radio det-radio--dl" ${tab === "dl" ? "checked" : ""}>`} | |
| ${fileMissing ? "" : ` | |
| <div class="det-tab-bar" role="tablist"> | |
| <label for="${uid}_runs" class="det-tab det-tab--runs" role="tab">All benchmark runs</label> | |
| <label for="${uid}_dl" class="det-tab det-tab--dl" role="tab">Download & run</label> | |
| </div>`} | |
| <div class="det-panel det-panel-runs mdl-card">${runsTableHTML(V)}</div> | |
| ${fileMissing ? "" : `<div class="det-panel det-panel-dl">${downloadRunHTML(V)}</div>`} | |
| ${footerHTML()}`; | |
| } | |
| function renderPageNow() { | |
| if (!PAGE_STATE) return; | |
| const container = $("modelPage"); | |
| if (!container) return; | |
| if (container.querySelector(".det-radio--dl:checked")) PAGE_STATE.tab = "dl"; | |
| else if (container.querySelector(".det-radio--runs:checked")) PAGE_STATE.tab = "runs"; | |
| container.innerHTML = renderFullModelPageHTML(PAGE_STATE); | |
| } | |
| function openModelPage(modelKey, cfgId, fmt, opts = {}) { | |
| const m = KEY_TO_MODEL[modelKey]; | |
| if (!m) return; | |
| const cfg = cfgId ? getCfg(m, cfgId) : safeFirstCfg(m); | |
| PAGE_STATE = { | |
| key: modelKey, cfgId: cfg?.id || cfgId || "", fmt: fmt || defaultRowFmt(m), | |
| precision: null, allocationId: null, unit: null, tab: opts.tab === "dl" ? "dl" : "runs", | |
| }; | |
| const container = $("modelPage"); | |
| if (container) container.innerHTML = renderFullModelPageHTML(PAGE_STATE); | |
| setActiveSection("sec-model"); | |
| const hash = `#/model/${encodeURIComponent(modelKey)}`; | |
| if (location.hash !== hash) history.replaceState ? history.replaceState(null, "", hash) : (location.hash = hash); | |
| window.scrollTo(0, 0); | |
| } | |
| function routeFromHash() { | |
| const match = /^#\/model\/([^/?#]+)/.exec(location.hash || ""); | |
| if (!match) return; | |
| const key = decodeURIComponent(match[1]); | |
| if (KEY_TO_MODEL[key]) openModelPage(key, "", defaultRowFmt(KEY_TO_MODEL[key])); | |
| } | |
| /* ---------- Modal density ---------- */ | |
| function renderModalBodyHTML(state) { | |
| const m = KEY_TO_MODEL[state.key]; | |
| if (!m) return `<div class="note note-warn">Model not found.</div>`; | |
| if (isComingSoon(m)) return comingSoonDetailsHTML(m, true); | |
| const V = buildDetailView(state.key, state.cfgId, state.fmt, state.precision, state.allocationId, state.unit); | |
| state.cfgId = V.cfg?.id || state.cfgId || ""; | |
| state.precision = V.art; | |
| state.allocationId = V.allocId; | |
| state.unit = V.unit; | |
| const compute = defaultRowCompute(V.m, V.cfg); | |
| const specs = modelSpecPairs(m, V.cfg, state.fmt, compute, V.art); | |
| const runsCount = allRunsRowsFor(m, V.cfg, state.fmt).length; | |
| return ` | |
| <div class="mdl-modal-wrap"> | |
| <div class="mdl-modal-grid"> | |
| <div class="mdl-modal-rail"> | |
| <div class="mdl-example">${exampleImageHTML(m)}</div> | |
| <div class="mdl-kv-list mdl-modal-specs">${specs.map(s => `<div class="mdl-kv-row"><span class="k">${esc(s.k)}</span><span class="v" title="${esc(s.v)}">${esc(s.v)}</span></div>`).join("")}</div> | |
| </div> | |
| <div class="mdl-modal-main"> | |
| ${modalSelectorsHTML(V)} | |
| ${missingFileBannerHTML(m, state.fmt, V.art)} | |
| ${kpiStripHTML(V, true)} | |
| ${perfRowHTML(V, "modal", true, modalQuickLinksHTML(state, V, runsCount))} | |
| </div> | |
| </div> | |
| </div>`; | |
| } | |
| function renderModalNow() { | |
| if (!MODAL_STATE) return; | |
| const el = $("modalDetails"); | |
| if (el) el.innerHTML = renderModalBodyHTML(MODAL_STATE); | |
| } | |
| /* ---------- Navigation ---------- */ | |
| function setActiveSection(targetId) { | |
| // Leaving the model page: drop its #/model/<key> hash so the address bar | |
| // matches what's on screen instead of staying pinned to the last model | |
| // (routeFromHash would otherwise reopen it on the next reload/back nav). | |
| if (targetId !== "sec-model" && /^#\/model\//.test(location.hash || "")) { | |
| const url = location.pathname + location.search; | |
| history.replaceState ? history.replaceState(null, "", url) : (location.hash = ""); | |
| } | |
| $$(".section").forEach(sec => sec.classList.remove("visible")); | |
| document.getElementById(targetId)?.classList.add("visible"); | |
| $$(".nav-item").forEach(btn => btn.classList.remove("active")); | |
| document.querySelector(`.nav-item[data-target="${CSS.escape(targetId)}"]`)?.classList.add("active"); | |
| if (targetId === "sec-overview") requestAnimationFrame(redrawConstellation); | |
| if (targetId === "sec-catalog" && CATALOG_STATE.view === "table") { | |
| requestAnimationFrame(() => setupPinnedColumns($("modelsTable"), 1)); | |
| } | |
| } | |
| /* ---------- All Models (table) ---------- */ | |
| function defaultRowFmt(model) { | |
| return model.onnx_repo ? "ONNX" : (model.gguf_repo ? "GGUF" : "ONNX"); | |
| } | |
| function defaultRowCompute(model, cfg) { | |
| const targets = safeList(cfg?.targets, safeList(model.targets, ["CPU"])); | |
| // A hybrid run genuinely executes on both — label it as such rather than | |
| // collapsing to whichever of NPU/CPU happens to win the single-badge priority | |
| // below, which would hide that the other unit is doing real work too. | |
| if (targets.includes("NPU") && targets.includes("CPU")) return "NPU + CPU"; | |
| return targets.includes("CPU") ? "CPU" : (targets.includes("NPU") ? "NPU" : "DSP"); | |
| } | |
| function defaultRowArtifact(model, fmt) { | |
| const arts = (fmt === "ONNX") ? safeList(model.onnx_artifacts, []) : safeList(model.gguf_artifacts, []); | |
| return arts[0] || null; | |
| } | |
| function buildTableRows(models) { | |
| const rows = []; | |
| for (const m of models) { | |
| const cfg = safeFirstCfg(m) || {}; | |
| const fmt = defaultRowFmt(m); | |
| const defArt = defaultRowArtifact(m, fmt); | |
| const { hil } = metricsForArtifact(cfg.metrics || {}, defArt); | |
| const detailsBtn = `<button class="mini-btn" type="button" | |
| data-open-model="1" | |
| data-key="${esc(m.key)}" | |
| data-cfg="${esc(cfg.id || "")}" | |
| data-fmt="${esc(fmt)}" | |
| >Details</button>`; | |
| rows.push({ | |
| _key: m.key || "", | |
| _cfg: cfg.id || "", | |
| _fmt: fmt, | |
| "Name": `<span class="tc-name"><span class="mc-logo">${designerOf(m).svg}</span><span class="tc-name-txt">${esc(m.display_name || m.key || "")}</span>${isComingSoon(m) ? `<span class="badge status soon">Coming Soon</span>` : modelHasMissingFile(m) ? `<span class="badge status pending" title="At least one benchmarked precision has no model file uploaded yet">⏳ File pending</span>` : ""}</span>`, | |
| "Modality": esc(m.modality || "LLM"), | |
| "Task": esc(safeList(m.tasks, [])[0] || "—"), | |
| "Repo": repoButtonsSmallHTML(m), | |
| "Precision/Quant": artifactsBadgesHTML(m), | |
| "HIL": metricBriefHTML(hil), | |
| "": detailsBtn | |
| }); | |
| } | |
| return rows; | |
| } | |
| function renderTable(rows) { | |
| const table = $("modelsTable"); | |
| if (!table) return; | |
| const thead = table.querySelector("thead"); | |
| const tbody = table.querySelector("tbody"); | |
| if (!rows.length) { | |
| thead.innerHTML = ""; | |
| tbody.innerHTML = `<tr><td style="padding:14px;" colspan="1">No rows.</td></tr>`; | |
| return; | |
| } | |
| const cols = Object.keys(rows[0]).filter(k => !k.startsWith("_")); | |
| const pinCount = 1; // pin only the identifier (Name) column | |
| thead.innerHTML = `<tr>${cols.map((c, i) => { | |
| const isPinned = i < pinCount; | |
| const isDetails = (c === ""); | |
| const isName = (c === "Name"); | |
| const cls = `${isPinned ? "pin" : ""}${isDetails ? " details-col" : ""}${isName ? " cell-name" : ""}`.trim(); | |
| const pinAttr = isPinned ? ` data-pin="${i}"` : ""; | |
| const header = isDetails ? "" : esc(c); | |
| return `<th class="${cls}"${pinAttr}>${header}</th>`; | |
| }).join("")}</tr>`; | |
| const htmlCols = new Set(["Name", "Repo", "Precision/Quant", "HIL", ""]); | |
| tbody.innerHTML = rows.map((r, ri) => { | |
| return `<tr data-row="${ri}" data-key="${esc(r._key)}" data-cfg="${esc(r._cfg)}" data-fmt="${esc(r._fmt)}">${ | |
| cols.map((c, i) => { | |
| const isPinned = i < pinCount; | |
| const isDetails = (c === ""); | |
| const isName = (c === "Name"); | |
| const cls = `${isPinned ? "pin" : ""}${isDetails ? " details-col" : ""}${isName ? " cell-name" : ""}`.trim(); | |
| const pinAttr = isPinned ? ` data-pin="${i}"` : ""; | |
| // data-label drives the stacked-card layout's field labels on narrow screens | |
| const labelAttr = ` data-label="${esc(c)}"`; | |
| const val = r[c]; | |
| if (htmlCols.has(c)) return `<td class="${cls}"${pinAttr}${labelAttr}>${val ?? ""}</td>`; | |
| return `<td class="${cls}"${pinAttr}${labelAttr}>${esc(val)}</td>`; | |
| }).join("") | |
| }</tr>`; | |
| }).join("\n"); | |
| setupPinnedColumns(table, pinCount); | |
| } | |
| function setupPinnedColumns(table, count) { | |
| if (!table) return; | |
| const headCells = table.querySelectorAll(`thead th.pin`); | |
| if (!headCells.length) return; | |
| const lefts = []; | |
| let acc = 0; | |
| for (let i = 0; i < count; i++) { | |
| const cell = table.querySelector(`thead th.pin[data-pin="${i}"]`); | |
| if (!cell) break; | |
| lefts[i] = acc; | |
| acc += cell.getBoundingClientRect().width; | |
| } | |
| for (let i = 0; i < lefts.length; i++) { | |
| table.querySelectorAll(`.pin[data-pin="${i}"]`).forEach(el => { el.style.left = `${lefts[i]}px`; }); | |
| } | |
| } | |
| /* ---------- Model modal ---------- */ | |
| function openModelModal(modelKey, cfgId, fmt) { | |
| const m = KEY_TO_MODEL[modelKey]; | |
| if (!m) return; | |
| const modal = $("modelModal"); | |
| modal.classList.add("open"); | |
| modal.setAttribute("aria-hidden", "false"); | |
| const cfg = cfgId ? getCfg(m, cfgId) : safeFirstCfg(m); | |
| MODAL_STATE = { key: modelKey, cfgId: cfg?.id || cfgId || "", fmt, precision: null, allocationId: null, unit: null }; | |
| $("modalTitle").textContent = m.display_name || m.key; | |
| const sub = $("modalSubtitle"); | |
| if (sub) sub.innerHTML = modalHeaderMetaHTML(m, fmt, defaultRowCompute(m, cfg)); | |
| renderModalNow(); | |
| } | |
| function closeModelModal() { | |
| const modal = $("modelModal"); | |
| modal.classList.remove("open"); | |
| modal.setAttribute("aria-hidden", "true"); | |
| const el = $("modalDetails"); | |
| if (el) el.innerHTML = ""; | |
| MODAL_STATE = null; | |
| } | |
| /* ====================================================================== */ | |
| /* OVERVIEW — constellation (size vs throughput) */ | |
| /* ====================================================================== */ | |
| const FAMILY_PALETTE = ["#C96442", "#6E8B6A", "#C9A24B", "#5B7A99", "#A6573F", "#8E6E9E", "#4C8C7D", "#B07A3C", "#9A6B6B", "#7C8B5A"]; | |
| const COLOR_CACHE = {}; | |
| function colorFor(label) { | |
| if (COLOR_CACHE[label] != null) return COLOR_CACHE[label]; | |
| const c = FAMILY_PALETTE[Object.keys(COLOR_CACHE).length % FAMILY_PALETTE.length]; | |
| COLOR_CACHE[label] = c; | |
| return c; | |
| } | |
| function allArtifactsOf(m) { | |
| return uniqueSorted([...safeList(m.onnx_artifacts, []), ...safeList(m.gguf_artifacts, [])]); | |
| } | |
| /* A repo with no artifact folders at all has nothing published yet — treated | |
| as a "coming soon" teaser rather than a model with missing benchmarks. This | |
| flips automatically the day real artifacts are pushed, with no status flag | |
| to remember to clear. */ | |
| function isComingSoon(m) { | |
| return allArtifactsOf(m).length === 0; | |
| } | |
| /* File-vs-benchmark decorrelation — a benchmark YAML lands the moment a run | |
| completes, which is routinely before the (much larger) weight file itself | |
| is uploaded. `<fmt>_file_status[artifact]` (generate_models_json.py, | |
| artifact_has_payload()) is `false` only when that specific artifact has no | |
| real payload; absent/undefined means the model predates this field, so it | |
| is treated as available rather than flagged. Kept per-format because the | |
| same precision name can be a real file in one repo and not the other | |
| (Llama-3.1-8B-Instruct: GGUF w4a16 ships coefficients, ONNX w4a16 doesn't). */ | |
| function fileStatusFor(m, fmt) { | |
| return (fmt === "ONNX" ? m?.onnx_file_status : m?.gguf_file_status) || {}; | |
| } | |
| function artifactFileMissing(m, fmt, art) { | |
| return !!art && fileStatusFor(m, fmt)[art] === false; | |
| } | |
| function modelHasMissingFile(m) { | |
| const vals = [...Object.values(m?.onnx_file_status || {}), ...Object.values(m?.gguf_file_status || {})]; | |
| return vals.some(v => v === false); | |
| } | |
| /* True if at least one artifact (either format) actually has a downloadable | |
| file — i.e. the model isn't "coming soon" (no artifacts at all) and isn't | |
| stuck with every artifact's file still unpublished (see fileStatusFor). */ | |
| function modelHasAvailableFile(m) { | |
| if (isComingSoon(m)) return false; | |
| const onnxOk = safeList(m.onnx_artifacts, []).some(a => !artifactFileMissing(m, "ONNX", a)); | |
| const ggufOk = safeList(m.gguf_artifacts, []).some(a => !artifactFileMissing(m, "GGUF", a)); | |
| return onnxOk || ggufOk; | |
| } | |
| /* Shared banner for the modal and the full model page — same warning style as | |
| the "Coming soon" notice, scoped to just the currently-selected precision | |
| instead of the whole model. */ | |
| function missingFileBannerHTML(m, fmt, art) { | |
| if (!artifactFileMissing(m, fmt, art)) return ""; | |
| return `<div class="note note-warn">⏳ <b>${esc(String(art).toUpperCase())} model file not yet uploaded.</b> The benchmark numbers below for this precision were published ahead of the model weights — download will not work until the file is added to the repo.</div>`; | |
| } | |
| function fmtOfArtifact(m, art) { | |
| if (safeList(m.gguf_artifacts, []).includes(art)) return "GGUF"; | |
| if (safeList(m.onnx_artifacts, []).includes(art)) return "ONNX"; | |
| return "—"; | |
| } | |
| function pickBlock(cfgMetrics, art, source) { | |
| const { estimate, hil } = metricsForArtifact(cfgMetrics || {}, art); | |
| if (source === "HIL") return hil ? { block: hil, source: "HIL" } : null; | |
| if (source === "Estimate") return estimate ? { block: estimate, source: "Estimate" } : null; | |
| if (hil) return { block: hil, source: "HIL" }; | |
| if (estimate) return { block: estimate, source: "Estimate" }; | |
| return null; | |
| } | |
| function tokSOf(block) { | |
| if (!block) return null; | |
| let t = (block.total && block.total.tok_s != null) ? block.total.tok_s : null; | |
| if (t == null) t = block.npu?.tok_s ?? block.cpu?.tok_s ?? block.dsp?.tok_s ?? null; | |
| return (t != null && isFinite(Number(t))) ? Number(t) : null; | |
| } | |
| /* ---------------------------------------------------------------------- */ | |
| /* Measurement regimes = the constellation tabs */ | |
| /* ---------------------------------------------------------------------- | |
| The catalog deliberately mixes model *types* (CNN, dual encoder, | |
| transformer decoder, LLM/VLM/ALM/VLA), and each type answers a different | |
| question with a different unit. Plotting them on one pair of axes is not | |
| just crowded, it is wrong: 952 img/s (MobileNetV2, one full image per | |
| inference) and 41 tok/s (Llama-3.2-1B, one *token* per inference step) | |
| are not the same quantity, and putting them on a shared linear axis both | |
| implies a comparison that doesn't exist and squeezes every decoder into | |
| the left margin. | |
| So the chart is split by what "one inference" means, which is what fixes | |
| the unit — and therefore the axis and even the chart form: | |
| gen one generated token -> tok/s -> scatter | |
| cnn one full image, fixed graph -> img/s, ms -> scatter | |
| enc one pass per encoder tower -> ms per stage -> bar chart | |
| soon undefined (no KPI/artifacts) -> none -> cards | |
| A model lands in a regime by modality, with a data-shape fallback for | |
| modalities this table doesn't know yet, so a new modality shows up in a | |
| sensible tab instead of vanishing. */ | |
| const CSTL_REGIMES = [ | |
| { | |
| id: "gen", | |
| label: "Token generators", | |
| unit: "tok/s", | |
| chart: "scatter", | |
| modalities: ["LLM", "VLM", "ALM"], | |
| why: `<b>One inference = one generated token.</b> Decoders share the same memory-bound decode loop, | |
| so tok/s and size are directly comparable here.`, | |
| x: ["tok", "size", "prefill", "ttft"], | |
| y: ["size", "tok", "ttft", "mem"], | |
| }, | |
| { | |
| id: "cnn", | |
| label: "Vision CNNs", | |
| unit: "img/s · ms", | |
| chart: "scatter", | |
| modalities: ["CV"], | |
| why: `<b>One inference = one fixed-shape image.</b> Compute-bound and quantized, so img/s and | |
| latency sit orders of magnitude above any token rate — hence the separate scale.`, | |
| x: ["fps", "lat", "size"], | |
| y: ["size", "lat", "fps"], | |
| /* Single-pass, batch-1, fixed-shape graph → throughput is the reciprocal of | |
| latency, and the CV benchmark files state that convention themselves | |
| ("fps: null # throughput: 1000 / latency" in RetinaNet's int8 run). It | |
| also holds in the data: MobileNetV2 952.4 vs 1000/1.05, EfficientNetV2-B0 | |
| 480 vs 1000/2.08, ResNet50 303 vs 1000/3.23 — within ~2%. So a CV variant | |
| that timed a run but left fps null still gets a point, drawn dashed and | |
| labelled as derived. Never enabled for the decode regime, where latency | |
| and tok/s measure different things. */ | |
| deriveFps: true, | |
| }, | |
| { | |
| id: "enc", | |
| label: "Encoders & embeddings", | |
| unit: "ms / stage", | |
| chart: "stages", | |
| modalities: ["EMBED"], | |
| why: `<b>No inference loop — two towers, different rates.</b> A blended throughput would be fiction, | |
| so each tower's stage latency is charted separately.`, | |
| }, | |
| { | |
| id: "soon", | |
| label: "Roadmap", | |
| unit: "no KPI yet", | |
| chart: "cards", | |
| modalities: ["VLA"], | |
| why: `<b>Nothing measurable published yet.</b> No KPI pipeline or artifacts yet — listed as cards | |
| so they stay visible.`, | |
| }, | |
| ]; | |
| const CSTL_REGIME_BY_ID = Object.fromEntries(CSTL_REGIMES.map(r => [r.id, r])); | |
| /* Every benchmark block a model publishes, across configs/artifacts/kinds. */ | |
| function allBlocksOf(m) { | |
| const out = []; | |
| for (const cfg of safeList(m.hardware_configs, [])) { | |
| for (const art of allArtifactsOf(m)) { | |
| const { estimate, hil } = metricsForArtifact(cfg.metrics || {}, art); | |
| if (estimate) out.push(estimate); | |
| if (hil) out.push(hil); | |
| } | |
| } | |
| return out; | |
| } | |
| function publishesMetric(m, key) { | |
| return allBlocksOf(m).some(b => _firstUnitVal(b, key) != null); | |
| } | |
| /* Which tab a model belongs to. Modality first (it is the declared model type), | |
| then the shape of the published numbers — so a modality not listed above, or a | |
| VLA that one day reports a token rate, still lands somewhere sensible. */ | |
| function regimeOf(m) { | |
| if (isComingSoon(m)) return "soon"; | |
| const mod = String(m.modality || "").trim().toUpperCase(); | |
| if (mod === "VLA") return publishesMetric(m, "tok_s") ? "gen" : "soon"; | |
| const byMod = CSTL_REGIMES.find(r => safeList(r.modalities, []).includes(mod)); | |
| if (byMod) return byMod.id; | |
| if (publishesMetric(m, "tok_s")) return "gen"; | |
| if (publishesMetric(m, "fps")) return "cnn"; | |
| if (allBlocksOf(m).some(b => safeList(b.stages, []).length)) return "enc"; | |
| return "soon"; | |
| } | |
| function fmtAxisNum(v) { | |
| return Math.abs(v) >= 100 ? String(Math.round(v)) : String(Math.round(v * 10) / 10); | |
| } | |
| /* Measured values keep their tenth of a millisecond (396.8 ms, not 397 ms) — axis | |
| ticks are the only place rounding to whole units is fine. */ | |
| function fmtMs(v) { return v == null ? "—" : String(Math.round(Number(v) * 10) / 10); } | |
| /* Axis registry. `get` returns null when a variant never published that metric — | |
| the caller reports those as excluded rather than dropping them silently. */ | |
| const AXES = { | |
| tok: { get: p => p.tok, label: "Decode throughput (tok/s)", tick: fmtAxisNum }, | |
| prefill: { get: p => p.prefill, label: "Prefill rate (tok/s)", tick: fmtAxisNum }, | |
| ttft: { get: p => p.ttft, label: "Time to first token (ms)", tick: fmtAxisNum, lowerBetter: true }, | |
| fps: { get: p => p.fps, label: "Throughput (img/s)", tick: fmtAxisNum }, | |
| lat: { get: p => p.lat, label: "Latency p50 (ms)", tick: fmtAxisNum, lowerBetter: true }, | |
| mem: { get: p => p.memBw, label: "Memory bandwidth (GB/s)", tick: fmtAxisNum }, | |
| size: { get: p => p.sizeM, label: "Model size (params)", tick: v => (v >= 1000 ? `${Math.round(v / 100) / 10}B` : `${Math.round(v)}M`) }, | |
| }; | |
| /* One point per (model × hardware config × artifact) inside one regime, carrying | |
| every metric the block reports so each regime can pick its own axes. */ | |
| function regimePoints(regimeId, source) { | |
| const pts = []; | |
| const regime = CSTL_REGIME_BY_ID[regimeId]; | |
| for (const m of CATALOG) { | |
| if (regimeOf(m) !== regimeId) continue; | |
| for (const cfg of safeList(m.hardware_configs, [])) { | |
| for (const art of allArtifactsOf(m)) { | |
| const got = pickBlock(cfg.metrics || {}, art, source); | |
| if (!got) continue; | |
| const b = got.block; | |
| /* Size is per point, not per model: an artifact that declares its own | |
| parameter count in <artifact>/.metadata.yaml wins over the repo-level | |
| figure. Falls back to the model value, which is the common case. */ | |
| const sizeM = artifactSizeM(m, art); | |
| const lat = latencyMsOf(b); | |
| let fps = _firstUnitVal(b, "fps"); | |
| let fpsDerived = false; | |
| if (fps == null && regime?.deriveFps && lat != null && lat > 0) { | |
| fps = 1000 / lat; | |
| fpsDerived = true; | |
| } | |
| pts.push({ | |
| key: m.key, cfgId: cfg.id, fmt: fmtOfArtifact(m, art), | |
| name: m.display_name || m.key, | |
| arch: m.architecture || m.display_name || m.key, | |
| family: familyOf(m), | |
| quant: art, | |
| precision: artifactPrecision(m, art), | |
| target: safeList(cfg.targets, safeList(m.targets, []))[0] || "—", | |
| hwLabel: cfg.label || cfg.id || "—", | |
| sizeM, | |
| tok: _firstUnitVal(b, "tok_s"), | |
| fps, fpsDerived, lat, | |
| prefill: _firstUnitVal(b, "prefill_tok_s"), | |
| ttft: _firstUnitVal(b, "ttft_ms"), | |
| memBw: memBwGBs(_firstUnitVal(b, "peak_mem_mb")), | |
| acc: b.accuracy != null ? Number(b.accuracy) : null, | |
| stages: safeList(b.stages, []).filter(s => s && s.ms != null), | |
| source: got.source, block: b, | |
| }); | |
| } | |
| } | |
| } | |
| return pts; | |
| } | |
| function colorKey(p, colorBy) { | |
| return colorBy === "quant" ? p.precision | |
| : colorBy === "format" ? p.fmt | |
| : colorBy === "target" ? p.target | |
| : colorBy === "family" ? p.family | |
| : p.arch; | |
| } | |
| /* Parameter count for one artifact of one model, in millions. | |
| `artifact_info[<art>].size_m` comes from that artifact's own .metadata.yaml and | |
| overrides the repo-level `model_size_m` — the generator collected those files | |
| but never read them, so per-artifact sizes used to be invisible here. */ | |
| function artifactSizeM(m, art) { | |
| const info = (m.artifact_info || {})[art]; | |
| if (info && info.size_m != null) return Number(info.size_m); | |
| return (m.model_size_m != null) ? Number(m.model_size_m) : null; | |
| } | |
| /* Some .metadata.yaml files record precision as a {weights, activations} | |
| object (e.g. "w4a16" written out longhand as {weights: "int4", activations: | |
| "int16"}) instead of the short code — String()'ing that object is where the | |
| catalog's "[object Object]" quant badge came from. Collapses it back to the | |
| standard wNaM shorthand, and folds the "f16"/"f32" spelling some repos use | |
| into the "fp16"/"fp32" names used everywhere else. */ | |
| function normalizeQuantName(raw) { | |
| if (raw && typeof raw === "object") { | |
| const bits = v => { const m = String(v ?? "").match(/\d+/); return m ? m[0] : null; }; | |
| const w = bits(raw.weights), a = bits(raw.activations); | |
| return (w && a) ? `w${w}a${a}` : "mixed"; | |
| } | |
| const s = String(raw ?? "").trim().toLowerCase(); | |
| return (s === "f16") ? "fp16" : (s === "f32") ? "fp32" : s; | |
| } | |
| /* The actual quantization/precision (fp32, int8, w4a16, …) of an artifact — | |
| NOT the artifact folder name, which is usually the same string but isn't | |
| always: e.g. ResNet18-OpticalFlow-ONNX's artifacts are named "xavier-export" | |
| / "a100-export" (two reference-target exports, both int8 per artifact_info), | |
| so listing the raw folder name as a "quantization" is simply wrong. Falls | |
| back to the artifact name for repos with no artifact_info override, where | |
| the folder name already *is* the precision. */ | |
| function artifactPrecision(m, art) { | |
| const info = (m.artifact_info || {})[art]; | |
| return normalizeQuantName((info && info.precision) ? info.precision : art); | |
| } | |
| function sizeLabel(sizeM) { | |
| if (sizeM == null) return "size n/a"; | |
| return sizeM >= 1000 ? `${Math.round(sizeM / 100) / 10}B` : `${Math.round(sizeM)}M`; | |
| } | |
| /* Tooltip / excluded-list body: whatever the block actually reports, unfiltered | |
| by the Catalog's KPI display toggles (those belong to the Catalog surface). */ | |
| function pointKpiText(p) { | |
| const chips = kpiChipsOf(p.block).map(c => `${c.label} ${fmtChipFull(c)}`); | |
| if (p.fpsDerived) chips.unshift(`Throughput ${fmtAxisNum(p.fps)} img/s (derived: 1000 / ${fmtMs(p.lat)} ms)`); | |
| return chips.length ? chips : ["no metrics recorded"]; | |
| } | |
| /* A block with accuracy but no timing at all is not a failed performance run — | |
| it is the FP32 reference the quantized variants are scored against (the CV | |
| repos' fp32 runs say so: "reference accuracy measured on physical X5H | |
| silicon", with fps and latency explicitly null). Charting it as a missing | |
| point would report a deliberate baseline as a gap, so these are pulled out of | |
| the point set and listed as references instead. */ | |
| function isAccuracyReference(p) { | |
| return p.tok == null && p.fps == null && p.lat == null && p.acc != null; | |
| } | |
| /* The reference accuracy for a model, so a quantized point can show what its | |
| accuracy is measured against. */ | |
| function accuracyReferenceOf(key, refs) { | |
| return safeList(refs, []).find(r => r.key === key) || null; | |
| } | |
| let CSTL_HIT = []; | |
| /* Accuracy-reference runs for the open regime, kept so a point's tooltip can | |
| name the baseline its quantization is scored against. */ | |
| let CSTL_REFS = []; | |
| const CSTL_FONT = "system-ui, -apple-system, Segoe UI, Roboto, Arial"; | |
| const CSTL_INK = "rgba(31,30,28,0.72)"; | |
| const CSTL_INK_SOFT = "rgba(31,30,28,0.50)"; | |
| const CSTL_GRID = "rgba(31,30,28,0.08)"; | |
| /* Which regime tab is open, plus the axis pair chosen per regime (kept per tab so | |
| switching back restores what you were looking at). */ | |
| const CSTL_STATE = { regime: CSTL_REGIMES[0].id, axes: {} }; | |
| function cstlAxesFor(r) { | |
| if (!CSTL_STATE.axes[r.id]) { | |
| CSTL_STATE.axes[r.id] = { x: safeList(r.x, ["tok"])[0], y: safeList(r.y, ["size"])[0] }; | |
| } | |
| return CSTL_STATE.axes[r.id]; | |
| } | |
| /* Circles (scatter) hit by distance, bars (stage chart) by rectangle. */ | |
| function hitTest(list, evt, canvas) { | |
| const rect = canvas.getBoundingClientRect(); | |
| const x = evt.clientX - rect.left, y = evt.clientY - rect.top; | |
| let best = null, bestD = Infinity; | |
| for (const p of list) { | |
| if (p.rect) { | |
| if (x >= p.rect[0] && x <= p.rect[2] && y >= p.rect[1] && y <= p.rect[3]) return p; | |
| continue; | |
| } | |
| const dx = x - p.x, dy = y - p.y, d = dx * dx + dy * dy; | |
| if (d <= p.r * p.r && d < bestD) { best = p; bestD = d; } | |
| } | |
| return best; | |
| } | |
| function prepCanvas(canvas) { | |
| const ctx = canvas.getContext("2d"); | |
| const w = canvas.clientWidth || canvas.parentElement?.clientWidth || 600; | |
| const h = canvas.clientHeight || 460; | |
| const dpr = window.devicePixelRatio || 1; | |
| canvas.width = Math.max(1, Math.floor(w * dpr)); | |
| canvas.height = Math.max(1, Math.floor(h * dpr)); | |
| ctx.setTransform(dpr, 0, 0, dpr, 0, 0); | |
| ctx.clearRect(0, 0, w, h); | |
| ctx.font = `12px ${CSTL_FONT}`; | |
| ctx.textAlign = "left"; | |
| CSTL_HIT = []; | |
| return { ctx, w, h }; | |
| } | |
| function emptyCanvasMsg(ctx, w, h, msg) { | |
| ctx.fillStyle = CSTL_INK; | |
| ctx.textAlign = "center"; | |
| ctx.fillText(msg, w / 2, h / 2); | |
| ctx.textAlign = "left"; | |
| } | |
| /* Ellipsis-truncate to fit maxWidth under the ctx's current font — used for the | |
| stage chart's model-name column, since architecture names vary wildly in | |
| length (e.g. "SigLIP-SO400M-patch14-384") and must never overlap the bars. */ | |
| function truncateToWidth(ctx, text, maxWidth) { | |
| if (ctx.measureText(text).width <= maxWidth) return text; | |
| let lo = 0, hi = text.length; | |
| while (lo < hi) { | |
| const mid = (lo + hi + 1) >> 1; | |
| if (ctx.measureText(text.slice(0, mid) + "…").width <= maxWidth) lo = mid; else hi = mid - 1; | |
| } | |
| return lo > 0 ? text.slice(0, lo) + "…" : "…"; | |
| } | |
| function barPath(ctx, x, y, w, h, r) { | |
| const rr = Math.min(r, h / 2, w / 2); | |
| ctx.beginPath(); | |
| ctx.moveTo(x + rr, y); | |
| ctx.lineTo(x + w - rr, y); | |
| ctx.quadraticCurveTo(x + w, y, x + w, y + rr); | |
| ctx.lineTo(x + w, y + h - rr); | |
| ctx.quadraticCurveTo(x + w, y + h, x + w - rr, y + h); | |
| ctx.lineTo(x + rr, y + h); | |
| ctx.quadraticCurveTo(x, y + h, x, y + h - rr); | |
| ctx.lineTo(x, y + rr); | |
| ctx.quadraticCurveTo(x, y, x + rr, y); | |
| ctx.closePath(); | |
| } | |
| /* Point labels so the chart reads without hovering. For each point: try four | |
| offsets, prefer the first that clears both the other labels and every bubble, | |
| fall back to the first that at least clears the other labels, and skip the | |
| point if even that fails. Labels are drawn with a white halo so the fallback | |
| case stays legible where the catalog clusters (small quantized models all | |
| land in the same corner). */ | |
| function drawPointLabels(ctx, items, box) { | |
| const overlaps = (a, b) => !(a[2] < b[0] || a[0] > b[2] || a[3] < b[1] || a[1] > b[3]); | |
| const bubbles = items.map(it => [it.x - it.r, it.y - it.r, it.x + it.r, it.y + it.r]); | |
| const placed = []; | |
| ctx.font = `600 11px ${CSTL_FONT}`; | |
| ctx.lineJoin = "round"; | |
| for (const it of items) { | |
| const tw = ctx.measureText(it.text).width; | |
| const cands = [ | |
| [it.x + it.r + 5, it.y + 4], | |
| [it.x - it.r - 5 - tw, it.y + 4], | |
| [it.x - tw / 2, it.y - it.r - 6], | |
| [it.x - tw / 2, it.y + it.r + 14], | |
| ].map(([lx, ly]) => ({ lx, ly, rect: [lx - 2, ly - 11, lx + tw + 2, ly + 3] })) | |
| .filter(c => c.rect[0] >= box.l && c.rect[2] <= box.r && c.rect[1] >= box.t && c.rect[3] <= box.b) | |
| .filter(c => !placed.some(q => overlaps(c.rect, q))); | |
| const pick = cands.find(c => !bubbles.some(b => overlaps(c.rect, b))) || cands[0]; | |
| if (!pick) continue; | |
| placed.push(pick.rect); | |
| ctx.strokeStyle = "rgba(255,255,255,0.92)"; | |
| ctx.lineWidth = 3; | |
| ctx.strokeText(it.text, pick.lx, pick.ly); | |
| ctx.fillStyle = "rgba(31,30,28,0.72)"; | |
| ctx.fillText(it.text, pick.lx, pick.ly); | |
| } | |
| ctx.lineWidth = 1; | |
| ctx.font = `12px ${CSTL_FONT}`; | |
| } | |
| /* Scatter for the regimes whose models publish two commensurable numbers. | |
| Returns the split between what could be placed and what could not (and why), | |
| so the caller can show the misses instead of dropping them. */ | |
| function drawScatter(canvas, points, opts) { | |
| const { ctx, w, h } = prepCanvas(canvas); | |
| const ax = AXES[opts.xKey] || AXES.tok; | |
| const ay = AXES[opts.yKey] || AXES.size; | |
| const plotted = [], excluded = []; | |
| for (const p of points) { | |
| const miss = []; | |
| if (ax.get(p) == null) miss.push(ax.label); | |
| if (ay.get(p) == null) miss.push(ay.label); | |
| if (miss.length) excluded.push({ p, miss }); else plotted.push(p); | |
| } | |
| if (!plotted.length) { | |
| emptyCanvasMsg(ctx, w, h, points.length | |
| ? "No variant reports both of these axes — see the list below." | |
| : "No benchmark data for this selection."); | |
| return { plotted, excluded }; | |
| } | |
| const pad = { l: 70, r: 22, t: 24, b: 52 }; | |
| const innerW = Math.max(1, w - pad.l - pad.r); | |
| const innerH = Math.max(1, h - pad.t - pad.b); | |
| const xs = plotted.map(ax.get), ys = plotted.map(ay.get); | |
| let xMin = Math.min(0, ...xs), xMax = Math.max(...xs); | |
| let yMin = Math.min(0, ...ys), yMax = Math.max(...ys); | |
| if (xMax - xMin < 1e-9) xMax = xMin + 1; | |
| if (yMax - yMin < 1e-9) yMax = yMin + 1; | |
| xMax += (xMax - xMin) * 0.10; | |
| yMax += (yMax - yMin) * 0.14; | |
| const xOf = v => pad.l + ((v - xMin) / (xMax - xMin)) * innerW; | |
| const yOf = v => pad.t + innerH - ((v - yMin) / (yMax - yMin)) * innerH; | |
| const ticks = 4; | |
| ctx.lineWidth = 1; | |
| ctx.strokeStyle = CSTL_GRID; | |
| for (let i = 0; i <= ticks; i++) { | |
| const ty = pad.t + innerH - (innerH * i / ticks); | |
| ctx.beginPath(); ctx.moveTo(pad.l, ty); ctx.lineTo(pad.l + innerW, ty); ctx.stroke(); | |
| ctx.fillStyle = CSTL_INK; | |
| ctx.fillText(ay.tick(yMin + (yMax - yMin) * (i / ticks)), 8, ty + 4); | |
| } | |
| for (let i = 0; i <= ticks; i++) { | |
| const tx = pad.l + (innerW * i / ticks); | |
| ctx.beginPath(); ctx.moveTo(tx, pad.t); ctx.lineTo(tx, pad.t + innerH); ctx.stroke(); | |
| ctx.fillStyle = CSTL_INK; | |
| ctx.textAlign = i === ticks ? "right" : "center"; | |
| ctx.fillText(ax.tick(xMin + (xMax - xMin) * (i / ticks)), tx, h - 30); | |
| ctx.textAlign = "left"; | |
| } | |
| ctx.fillStyle = CSTL_INK_SOFT; | |
| ctx.fillText(`${ax.label} ${ax.lowerBetter ? "← lower is better" : "→ higher is better"}`, pad.l, h - 10); | |
| ctx.fillText(`↑ ${ay.label}${ay.lowerBetter ? " (lower is better)" : ""}`, 8, 14); | |
| const maxSz = Math.max(1, ...plotted.map(p => p.sizeM || 0)); | |
| const rOf = s => (s == null ? 8 : 6 + 16 * Math.sqrt(s / maxSz)); | |
| // Big bubbles first so a small fast variant is never buried under a large one. | |
| const order = plotted.slice().sort((a, b) => (b.sizeM || 0) - (a.sizeM || 0)); | |
| const labels = []; | |
| order.forEach(p => { | |
| const x = xOf(ax.get(p)), y = yOf(ay.get(p)), r = rOf(p.sizeM); | |
| const col = colorFor(colorKey(p, opts.colorBy)); | |
| // A dashed ring marks a point whose plotted throughput was derived from its | |
| // own latency rather than reported directly. | |
| const derivedOnAxis = p.fpsDerived && (opts.xKey === "fps" || opts.yKey === "fps"); | |
| ctx.setLineDash(derivedOnAxis ? [4, 3] : []); | |
| ctx.beginPath(); ctx.arc(x, y, r, 0, Math.PI * 2); | |
| if (p.source === "HIL") { ctx.fillStyle = col + "C0"; ctx.fill(); ctx.strokeStyle = col; ctx.lineWidth = 1.5; ctx.stroke(); } | |
| else { ctx.fillStyle = col + "33"; ctx.fill(); ctx.strokeStyle = col; ctx.lineWidth = 1.8; ctx.stroke(); } | |
| ctx.setLineDash([]); | |
| CSTL_HIT.push({ x, y, r: Math.max(r, 10), data: p }); | |
| labels.push({ x, y, r, text: `${p.arch} ${p.quant}` }); | |
| }); | |
| if (labels.length <= 18) { | |
| drawPointLabels(ctx, labels, { l: 4, r: w - 4, t: pad.t - 12, b: pad.t + innerH + 10 }); | |
| } | |
| return { plotted, excluded }; | |
| } | |
| /* Stage chart for the encoder regime: one row per artifact/source, one bar per | |
| encoder tower. Bars are scaled to the largest *stage*, and the run total is | |
| printed in the row label rather than drawn as a bar — it is an order of | |
| magnitude larger than the towers and is not their sum. */ | |
| function drawStageChart(canvas, points) { | |
| const { ctx, w, h } = prepCanvas(canvas); | |
| // Grouped by model first, so several dual-encoder models sort into visibly | |
| // separate blocks instead of interleaving by quant/source. | |
| const rows = points.filter(p => p.stages.length) | |
| .sort((a, b) => a.arch.localeCompare(b.arch) || a.quant.localeCompare(b.quant) || a.source.localeCompare(b.source)); | |
| const excluded = points.filter(p => !p.stages.length).map(p => ({ p, miss: ["per-stage timings"] })); | |
| const stageNames = []; | |
| rows.forEach(p => p.stages.forEach(s => { if (!stageNames.includes(s.label)) stageNames.push(s.label); })); | |
| if (!rows.length) { | |
| emptyCanvasMsg(ctx, w, h, "No per-stage timings for this selection."); | |
| return { plotted: rows, excluded, stageNames }; | |
| } | |
| const maxMs = Math.max(1, ...rows.flatMap(p => p.stages.map(s => Number(s.ms) || 0))); | |
| const pad = { l: 186, r: 96, t: 24, b: 46 }; | |
| const innerW = Math.max(1, w - pad.l - pad.r); | |
| const innerH = Math.max(1, h - pad.t - pad.b); | |
| const rowH = innerH / rows.length; | |
| const xOf = ms => pad.l + (ms / maxMs) * innerW; | |
| const ticks = 4; | |
| ctx.lineWidth = 1; | |
| ctx.strokeStyle = CSTL_GRID; | |
| for (let i = 0; i <= ticks; i++) { | |
| const tx = pad.l + (innerW * i / ticks); | |
| ctx.beginPath(); ctx.moveTo(tx, pad.t); ctx.lineTo(tx, pad.t + innerH); ctx.stroke(); | |
| ctx.fillStyle = CSTL_INK; | |
| ctx.textAlign = i === ticks ? "right" : "center"; | |
| ctx.fillText(fmtAxisNum(maxMs * i / ticks), tx, h - 26); | |
| ctx.textAlign = "left"; | |
| } | |
| ctx.fillStyle = CSTL_INK_SOFT; | |
| ctx.fillText("Per-stage NPU time (ms) ← lower is better", pad.l, h - 8); | |
| rows.forEach((p, i) => { | |
| const top = pad.t + i * rowH; | |
| const newModel = i === 0 || rows[i - 1].arch !== p.arch; | |
| if (i) { | |
| // A heavier line at a model boundary reads as a group break; the default | |
| // hairline just separates two variants of the same model. | |
| ctx.strokeStyle = newModel ? "rgba(31,30,28,0.16)" : CSTL_GRID; | |
| ctx.lineWidth = newModel ? 1.4 : 1; | |
| ctx.beginPath(); ctx.moveTo(8, top); ctx.lineTo(w - 8, top); ctx.stroke(); | |
| ctx.lineWidth = 1; | |
| } | |
| const labelMaxW = pad.l - 22; | |
| // Model name — the identity that used to be missing — with a color dot | |
| // matching the same model's swatch elsewhere (cards/scatter), so several | |
| // models scan at a glance even before reading the text. | |
| ctx.font = `700 12px ${CSTL_FONT}`; | |
| const nameText = truncateToWidth(ctx, p.arch, labelMaxW - 12); | |
| ctx.fillStyle = colorFor(p.arch); | |
| ctx.beginPath(); ctx.arc(11, top + rowH / 2 - 15, 3.5, 0, Math.PI * 2); ctx.fill(); | |
| ctx.fillStyle = CSTL_INK; | |
| ctx.fillText(nameText, 20, top + rowH / 2 - 11); | |
| ctx.font = `600 11.5px ${CSTL_FONT}`; | |
| ctx.fillStyle = CSTL_INK_SOFT; | |
| ctx.fillText(truncateToWidth(ctx, `${p.quant} · ${p.source}`, labelMaxW), 8, top + rowH / 2 + 3); | |
| ctx.font = `11px ${CSTL_FONT}`; | |
| ctx.fillStyle = CSTL_INK_SOFT; | |
| ctx.fillText(p.lat != null ? `Σ run ${fmtMs(p.lat)} ms` : `${p.fmt} · ${p.target}`, 8, top + rowH / 2 + 17); | |
| ctx.font = `12px ${CSTL_FONT}`; | |
| const bars = p.stages; | |
| const barH = Math.max(10, Math.min(24, (rowH - 18) / bars.length - 6)); | |
| const stackH = bars.length * (barH + 6) - 6; | |
| bars.forEach((s, j) => { | |
| const y = top + (rowH - stackH) / 2 + j * (barH + 6); | |
| const bw = Math.max(3, xOf(Number(s.ms)) - pad.l); | |
| const col = colorFor(s.label); | |
| barPath(ctx, pad.l, y, bw, barH, 3); | |
| ctx.fillStyle = col + "D9"; ctx.fill(); | |
| ctx.strokeStyle = col; ctx.lineWidth = 1; ctx.stroke(); | |
| const nameW = ctx.measureText(s.label).width; | |
| const inside = bw > nameW + 18 && barH >= 14; | |
| if (inside) { | |
| ctx.fillStyle = "#FFFFFF"; | |
| ctx.fillText(s.label, pad.l + 9, y + barH / 2 + 4); | |
| } | |
| ctx.fillStyle = CSTL_INK; | |
| ctx.fillText(`${fmtMs(s.ms)} ms${inside ? "" : " · " + s.label}`, pad.l + bw + 7, y + barH / 2 + 4); | |
| CSTL_HIT.push({ rect: [pad.l, y, pad.l + bw, y + barH], data: p }); | |
| }); | |
| }); | |
| return { plotted: rows, excluded, stageNames }; | |
| } | |
| function setLegend(items, extraHTML) { | |
| const el = $("cstlLegend"); | |
| if (!el) return; | |
| el.innerHTML = items.map(it => | |
| `<span class="legend-item"><span class="legend-dot" style="background:${it.color}"></span>${esc(it.label)}</span>` | |
| ).join("") + (extraHTML || ""); | |
| } | |
| /* Roadmap regime: no metric exists, so there is nothing to plot — the models are | |
| listed with the reason, which is the point of the tab. */ | |
| function roadmapReasons(m) { | |
| const mod = String(m.modality || "").trim().toUpperCase(); | |
| const out = []; | |
| if (mod === "VLA") { | |
| out.push("Emits an action chunk per step — the comparable KPI is closed-loop control rate (Hz) and task success, which the benchmark pipeline does not produce yet."); | |
| } | |
| if (isComingSoon(m)) out.push("No artifact folder published yet, so there is nothing to measure or download."); | |
| if (!out.length) out.push("No plottable metric published yet."); | |
| return out; | |
| } | |
| function roadmapCardHTML(m) { | |
| const col = colorFor(m.architecture || "Unknown"); | |
| const d = designerOf(m); | |
| const cfg = safeFirstCfg(m); | |
| const defFmt = m.gguf_repo ? "GGUF" : (m.onnx_repo ? "ONNX" : "GGUF"); | |
| return ` | |
| <div class="rm-card" data-rm-card="1" role="button" tabindex="0" | |
| data-key="${esc(m.key)}" data-cfg="${esc(cfg?.id || "")}" data-fmt="${esc(defFmt)}" | |
| aria-label="Details for ${esc(m.display_name || m.key)}"> | |
| <div class="rm-head"> | |
| <span class="mc-monogram" style="background:linear-gradient(135deg, ${col}, ${col}99)">${esc(monogramText(m))}</span> | |
| <span class="rm-id"> | |
| <span class="rm-title">${esc(m.display_name || m.key)}</span> | |
| <span class="rm-sub">${esc(m.architecture || "")}</span> | |
| </span> | |
| </div> | |
| <div class="rm-chips"> | |
| <span class="badge modality">${esc(m.modality || "—")}</span> | |
| ${isComingSoon(m) ? `<span class="badge status soon">Coming soon</span>` : ""} | |
| </div> | |
| <div class="rm-why">${roadmapReasons(m).map(r => `<span class="rm-why-row">${esc(r)}</span>`).join("")}</div> | |
| <div class="rm-foot"><span class="mc-logo">${d.svg}</span><span>${esc(d.name)}</span><span class="rm-size">${esc(sizeLabel(m.model_size_m))}</span></div> | |
| </div>`; | |
| } | |
| function renderRoadmapCards(models) { | |
| const el = $("cstlCards"); | |
| if (!el) return; | |
| el.innerHTML = models.length | |
| ? models.map(roadmapCardHTML).join("") | |
| : `<div class="subtle" style="padding:18px;">Nothing pending — every model in the catalog reports a plottable metric.</div>`; | |
| } | |
| /* One row per variant that isn't a point on this chart, in two groups with very | |
| different meanings: a genuine metadata gap (the metric is missing) versus an | |
| accuracy reference run (deliberately has no timing). */ | |
| function cstlRowHTML(p, missHTML) { | |
| return ` | |
| <div class="cx-item" data-rm-card="1" role="button" tabindex="0" | |
| data-key="${esc(p.key)}" data-cfg="${esc(p.cfgId || "")}" data-fmt="${esc(p.fmt)}" | |
| aria-label="Details for ${esc(p.name)}"> | |
| <span class="cx-name">${esc(p.arch)}</span> | |
| <span class="badge art">${esc(p.quant)}</span> | |
| <span class="badge ${p.source === "HIL" ? "npu" : "art"}">${esc(p.source)}</span> | |
| ${missHTML} | |
| <span class="cx-has">${esc(pointKpiText(p).join(" · "))}</span> | |
| </div>`; | |
| } | |
| function renderExcluded(list, refs) { | |
| const el = $("cstlExcluded"); | |
| if (!el) return; | |
| const refList = safeList(refs, []); | |
| if (!list.length && !refList.length) { el.hidden = true; el.innerHTML = ""; return; } | |
| el.hidden = false; | |
| const groups = []; | |
| if (list.length) { | |
| groups.push(` | |
| <div class="cx-head">Not plottable on these axes (${list.length}) — metric missing from the repo metadata</div> | |
| <div class="cx-list">${list.map(({ p, miss }) => | |
| cstlRowHTML(p, `<span class="cx-miss">no ${esc(miss.join(" · no "))}</span>`)).join("")}</div>`); | |
| } | |
| if (refList.length) { | |
| groups.push(` | |
| <div class="cx-head">Accuracy reference runs (${refList.length}) — no timing by design</div> | |
| <div class="cx-list">${refList.map(p => | |
| cstlRowHTML(p, `<span class="cx-ref">baseline for the quantized variants</span>`)).join("")}</div>`); | |
| } | |
| el.innerHTML = groups.join(""); | |
| } | |
| function syncRegimeUI(r) { | |
| $$(".cstl-tab").forEach(btn => { | |
| const on = btn.getAttribute("data-regime") === r.id; | |
| btn.classList.toggle("active", on); | |
| btn.setAttribute("aria-selected", on ? "true" : "false"); | |
| btn.tabIndex = on ? 0 : -1; | |
| }); | |
| $("cstlPanel")?.setAttribute("aria-labelledby", `cstlTab_${r.id}`); | |
| const why = $("cstlWhy"); | |
| if (why) why.innerHTML = r.why || ""; | |
| const showXY = r.chart === "scatter"; | |
| const showSource = r.chart !== "cards"; | |
| const fields = { cstlFieldX: showXY, cstlFieldY: showXY, cstlFieldColor: showXY, cstlFieldSource: showSource }; | |
| for (const [id, on] of Object.entries(fields)) { const el = $(id); if (el) el.hidden = !on; } | |
| const anyControl = showXY || showSource; | |
| const ctl = $("cstlControls"); if (ctl) ctl.hidden = !anyControl; | |
| const tgl = $("cstlFilterToggle"); if (tgl) tgl.hidden = !anyControl; | |
| if (showXY) { | |
| const sel = cstlAxesFor(r); | |
| fillAxisSelect($("cstlX"), safeList(r.x, []), sel.x); | |
| fillAxisSelect($("cstlY"), safeList(r.y, []), sel.y); | |
| } | |
| } | |
| /* Rebuilds the <option>s only when the regime actually changed the list, so the | |
| redraws triggered by the source radio don't churn (and refocus) the selects. */ | |
| function fillAxisSelect(sel, keys, value) { | |
| if (!sel) return; | |
| const current = Array.from(sel.options).map(o => o.value).join(","); | |
| if (current !== keys.join(",")) { | |
| sel.innerHTML = keys.map(k => `<option value="${esc(k)}">${esc(AXES[k]?.label || k)}</option>`).join(""); | |
| } | |
| sel.value = value; | |
| } | |
| function buildRegimeTabs() { | |
| const el = $("cstlTabs"); | |
| if (!el) return; | |
| const counts = {}; | |
| CSTL_REGIMES.forEach(r => { counts[r.id] = 0; }); | |
| CATALOG.forEach(m => { const id = regimeOf(m); if (counts[id] != null) counts[id]++; }); | |
| el.innerHTML = CSTL_REGIMES.map(r => { | |
| const on = r.id === CSTL_STATE.regime; | |
| return ` | |
| <button class="cstl-tab${on ? " active" : ""}" type="button" role="tab" id="cstlTab_${esc(r.id)}" | |
| data-regime="${esc(r.id)}" aria-selected="${on ? "true" : "false"}" | |
| aria-controls="cstlPanel" tabindex="${on ? 0 : -1}"> | |
| <span class="ct-label">${esc(r.label)}</span> | |
| <span class="ct-unit">${esc(r.unit)}</span> | |
| <span class="ct-count">${esc(counts[r.id])}</span> | |
| </button>`; | |
| }).join(""); | |
| } | |
| function setRegime(id) { | |
| if (!CSTL_REGIME_BY_ID[id] || id === CSTL_STATE.regime) return; | |
| CSTL_STATE.regime = id; | |
| redrawConstellation(); | |
| } | |
| function redrawConstellation() { | |
| const canvas = $("cstlCanvas"); | |
| if (!canvas) return; | |
| const r = CSTL_REGIME_BY_ID[CSTL_STATE.regime] || CSTL_REGIMES[0]; | |
| /* Capture the axis pickers before syncRegimeUI rebuilds their <option>s. | |
| The guard drops a value carried over from another regime's option list | |
| (this runs on tab switches too, when the selects still show the old tab). */ | |
| if (r.chart === "scatter") { | |
| const sel = cstlAxesFor(r); | |
| const xv = $("cstlX")?.value, yv = $("cstlY")?.value; | |
| if (xv && safeList(r.x, []).includes(xv)) sel.x = xv; | |
| if (yv && safeList(r.y, []).includes(yv)) sel.y = yv; | |
| } | |
| syncRegimeUI(r); | |
| const plot = $("cstlPlot"), cards = $("cstlCards"), status = $("cstlStatus"), note = $("cstlNote"); | |
| const srcRaw = readRadio("cstlSource", "Best available"); | |
| const source = (srcRaw === "HIL" || srcRaw === "Estimate") ? srcRaw : "Best"; | |
| if (r.chart === "cards") { | |
| const models = CATALOG.filter(m => regimeOf(m) === r.id); | |
| if (plot) plot.hidden = true; | |
| if (cards) cards.hidden = false; | |
| renderRoadmapCards(models); | |
| setLegend([], ""); | |
| if (status) status.textContent = `${models.length} model${models.length === 1 ? "" : "s"} · no benchmark KPI defined yet`; | |
| if (note) { note.hidden = true; note.textContent = ""; } | |
| CSTL_REFS = []; | |
| renderExcluded([], []); | |
| return; | |
| } | |
| if (plot) plot.hidden = false; | |
| if (cards) { cards.hidden = true; cards.innerHTML = ""; } | |
| const all = regimePoints(r.id, source); | |
| CSTL_REFS = all.filter(isAccuracyReference); | |
| const points = all.filter(p => !isAccuracyReference(p)); | |
| const hw = uniqueSorted(all.map(p => p.hwLabel)).join(", "); | |
| let res; | |
| if (r.chart === "stages") { | |
| /* A bar row needs far less height than a scatter, and only as many rows as | |
| there are artifacts — so this chart sizes itself instead of leaving half | |
| the 460px canvas empty. */ | |
| const rowCount = Math.max(1, points.filter(p => p.stages.length).length); | |
| canvas.style.height = `${Math.max(210, rowCount * 86 + 74)}px`; | |
| res = drawStageChart(canvas, points); | |
| setLegend(safeList(res.stageNames, []).map(s => ({ label: s, color: colorFor(s) })), ""); | |
| if (note) { | |
| note.hidden = false; | |
| note.innerHTML = `<b>Σ run</b> is the total latency reported for the benchmark run — it is not the sum of the towers, ` | |
| + `so the two are charted separately rather than stacked.`; | |
| } | |
| } else { | |
| canvas.style.height = ""; | |
| const sel = cstlAxesFor(r); | |
| const colorBy = $("cstlColor")?.value || "arch"; | |
| res = drawScatter(canvas, points, { xKey: sel.x, yKey: sel.y, colorBy }); | |
| // When coloring by model, each point is already text-labelled on the chart | |
| // itself (drawPointLabels) — repeating every model name again in the legend | |
| // below is pure duplication, so that legend row is skipped in that mode only. | |
| const seen = []; | |
| if (colorBy !== "arch") { | |
| for (const p of res.plotted) { const k = colorKey(p, colorBy); if (!seen.includes(k)) seen.push(k); } | |
| } | |
| const derived = res.plotted.filter(p => p.fpsDerived && (sel.x === "fps" || sel.y === "fps")).length; | |
| setLegend(seen.map(k => ({ label: k, color: colorFor(k) })), | |
| `<span class="legend-item">● measured (HIL) ◌ estimate</span>` | |
| + (derived ? `<span class="legend-item">⌁ dashed = img/s derived from p50 latency</span>` : "")); | |
| if (note) { | |
| note.hidden = !derived; | |
| note.innerHTML = derived | |
| ? `${derived} point(s) report a measured latency but no <b>fps</b>, so their img/s is the batch-1 ` | |
| + `reciprocal <b>1000 / p50 latency</b> — the convention the CV benchmark files state themselves.` | |
| : ""; | |
| } | |
| } | |
| if (status) { | |
| status.textContent = `${res.plotted.length} of ${points.length} variant point(s) · ${srcRaw}${hw ? " · " + hw : ""}`; | |
| } | |
| renderExcluded(res.excluded, CSTL_REFS); | |
| } | |
| function fillStatTiles() { | |
| const el = $("statTiles"); | |
| if (!el) return; | |
| const variants = new Set(); | |
| for (const m of CATALOG) { | |
| for (const cfg of safeList(m.hardware_configs, [])) | |
| for (const art of allArtifactsOf(m)) | |
| if (pickBlock(cfg.metrics || {}, art, "Best")) variants.add(`${m.key}|${art}`); | |
| } | |
| const soonCount = CATALOG.filter(isComingSoon).length; | |
| const tiles = [ | |
| [CATALOG.length, "Models"], | |
| [variants.size, "Benchmarked variants"], | |
| ]; | |
| if (soonCount) tiles.push([soonCount, "Coming soon"]); | |
| el.innerHTML = tiles.map(([n, l]) => | |
| `<div class="stat-tile"><div class="stat-num">${esc(n)}</div><div class="stat-label">${esc(l)}</div></div>` | |
| ).join(""); | |
| } | |
| /* ---------- Recently-added carousel ---------- */ | |
| const RECENT_WINDOW_DAYS = 31; | |
| function modelRecency(m) { | |
| const c = m.created_at ? Date.parse(m.created_at) : NaN; | |
| const u = m.last_modified ? Date.parse(m.last_modified) : NaN; | |
| const v = [c, u].filter(x => !isNaN(x)); | |
| return v.length ? Math.max(...v) : NaN; | |
| } | |
| function timeAgo(ts) { | |
| if (isNaN(ts)) return ""; | |
| const d = Math.floor((Date.now() - ts) / 86400000); | |
| if (d <= 0) return "today"; | |
| if (d === 1) return "yesterday"; | |
| if (d < 30) return `${d} days ago`; | |
| const mo = Math.round(d / 30); | |
| return `${mo} month${mo > 1 ? "s" : ""} ago`; | |
| } | |
| function recentLabel(m) { | |
| const c = m.created_at ? Date.parse(m.created_at) : NaN; | |
| const u = m.last_modified ? Date.parse(m.last_modified) : NaN; | |
| const cut = Date.now() - RECENT_WINDOW_DAYS * 86400000; | |
| if (!isNaN(c) && c >= cut) return { isNew: true, text: `added ${timeAgo(c)}` }; | |
| if (!isNaN(u)) return { isNew: false, text: `updated ${timeAgo(u)}` }; | |
| return { isNew: false, text: "" }; | |
| } | |
| function recentModels() { | |
| return CATALOG | |
| .filter(m => !isNaN(modelRecency(m))) | |
| .sort((a, b) => modelRecency(b) - modelRecency(a)); | |
| } | |
| function recentCardHTML(m, illustrationSrc) { | |
| const best = bestHilFirstVariantOf(m); | |
| const firstCfg = safeFirstCfg(m); | |
| const defFmt = m.gguf_repo ? "GGUF" : (m.onnx_repo ? "ONNX" : "GGUF"); | |
| const lbl = recentLabel(m); | |
| const topsBit = best && best.util && best.util.tops_used != null | |
| ? ` · ${esc(Math.round(best.util.tops_used * 10) / 10)} TOPS` | |
| : ""; | |
| const stat = isComingSoon(m) | |
| ? `<div class="rc-stat empty">🚧 Coming soon</div>` | |
| : best | |
| ? (best.tok != null | |
| ? `<div class="rc-stat"><b>${esc(Math.round(best.tok * 10) / 10)}</b> ${esc(best.unit)} · ${esc(best.quant)}${topsBit}</div>` | |
| : best.latencyMs != null | |
| ? `<div class="rc-stat"><b>${esc(Math.round(best.latencyMs * 100) / 100)}</b> ms · ${esc(best.quant)}${topsBit}</div>` | |
| : best.stageMax != null | |
| ? `<div class="rc-stat"><b>${esc(Math.round(best.stageMax * 100) / 100)}</b> ms · ${esc(best.quant)}${topsBit}</div>` | |
| : `<div class="rc-stat empty">No throughput yet</div>`) | |
| : `<div class="rc-stat empty">No throughput yet</div>`; | |
| const metaBadge = `<div class="rc-meta ${lbl.isNew ? "is-new" : ""}">${lbl.isNew ? "✦" : "⟳"} ${esc(lbl.text)}</div>`; | |
| return ` | |
| <div class="model-card rc-card" data-card="1" data-key="${esc(m.key)}" data-cfg="${esc(firstCfg?.id || "")}" data-fmt="${esc(defFmt)}"> | |
| ${cardBannerHTML(m, { compact: true, overlay: metaBadge, illustrationSrc })} | |
| ${designerRowHTML(m)} | |
| <div class="mc-head"> | |
| <div style="min-width:0"> | |
| <div class="mc-title">${esc(m.display_name || m.key)}</div> | |
| <div class="mc-sub">${esc(m.architecture || "")}${m.variant ? " · " + esc(m.variant) : ""}</div> | |
| </div> | |
| </div> | |
| ${stat} | |
| </div>`; | |
| } | |
| function renderRecentCarousel() { | |
| const sec = $("recentSection"), track = $("recentTrack"); | |
| if (!sec || !track) return; | |
| const list = recentModels(); | |
| if (!list.length) { sec.hidden = true; return; } | |
| sec.hidden = false; | |
| const illustrations = sequentialIllustrationSrcs(list, { compact: true }); | |
| track.innerHTML = list.map((m, i) => recentCardHTML(m, illustrations[i])).join(""); | |
| const sub = $("recentSub"); | |
| if (sub) sub.textContent = `${list.length} model${list.length > 1 ? "s" : ""} · newest first`; | |
| } | |
| function initOverview() { | |
| fillStatTiles(); | |
| renderRecentCarousel(); | |
| const track = $("recentTrack"); | |
| $("recentPrev")?.addEventListener("click", () => track?.scrollBy({ left: -276, behavior: "smooth" })); | |
| $("recentNext")?.addEventListener("click", () => track?.scrollBy({ left: 276, behavior: "smooth" })); | |
| /* Click-and-drag ("grab") scrolling with the mouse, in addition to the buttons/scrollbar. */ | |
| let dragging = false, dragMoved = false, dragStartX = 0, dragStartScroll = 0; | |
| track?.addEventListener("mousedown", (e) => { | |
| if (e.button !== 0) return; | |
| dragging = true; dragMoved = false; | |
| dragStartX = e.pageX; | |
| dragStartScroll = track.scrollLeft; | |
| }); | |
| window.addEventListener("mousemove", (e) => { | |
| if (!dragging) return; | |
| const dx = e.pageX - dragStartX; | |
| if (Math.abs(dx) > 4) { | |
| if (!dragMoved) track.classList.add("dragging"); | |
| dragMoved = true; | |
| } | |
| track.scrollLeft = dragStartScroll - dx; | |
| }); | |
| window.addEventListener("mouseup", () => { | |
| if (!dragging) return; | |
| dragging = false; | |
| track.classList.remove("dragging"); | |
| }); | |
| track?.addEventListener("click", (e) => { | |
| if (dragMoved) { dragMoved = false; return; } | |
| if (e.target.closest("a")) return; | |
| const card = e.target.closest('.model-card[data-card="1"]'); | |
| if (card) openModelModal(card.getAttribute("data-key"), card.getAttribute("data-cfg") || "", card.getAttribute("data-fmt") || "GGUF"); | |
| }); | |
| buildRadioGroup($("cstlSource"), "cstlSource", ["Best available", "HIL", "Estimate"], "Best available"); | |
| ["cstlX", "cstlY", "cstlColor"].forEach(id => $(id)?.addEventListener("change", redrawConstellation)); | |
| document.addEventListener("change", (e) => { if (e.target && e.target.name === "cstlSource") redrawConstellation(); }); | |
| /* Regime tabs: click, plus up/down (and left/right, kept for the ≤1040px | |
| layout where the rail still reads left-to-right) roving focus per the | |
| tablist pattern. */ | |
| buildRegimeTabs(); | |
| const tabs = $("cstlTabs"); | |
| tabs?.addEventListener("click", (e) => { | |
| const btn = e.target.closest(".cstl-tab[data-regime]"); | |
| if (btn) setRegime(btn.getAttribute("data-regime")); | |
| }); | |
| tabs?.addEventListener("keydown", (e) => { | |
| const forward = e.key === "ArrowRight" || e.key === "ArrowDown"; | |
| const backward = e.key === "ArrowLeft" || e.key === "ArrowUp"; | |
| if (!forward && !backward) return; | |
| const list = $$(".cstl-tab"); | |
| const i = list.findIndex(b => b.getAttribute("data-regime") === CSTL_STATE.regime); | |
| const next = list[(i + (forward ? 1 : list.length - 1)) % list.length]; | |
| if (!next) return; | |
| e.preventDefault(); | |
| setRegime(next.getAttribute("data-regime")); | |
| $(`cstlTab_${CSTL_STATE.regime}`)?.focus(); | |
| }); | |
| /* Chart-filters collapse: same show/hide idea as the catalog's facets rail, | |
| but only toggling a small inline toolbar rather than a whole column. */ | |
| const cstlFilterBtn = $("cstlFilterToggle"); | |
| cstlFilterBtn?.addEventListener("click", () => { | |
| const open = $("cstlControls")?.classList.toggle("open"); | |
| cstlFilterBtn.classList.toggle("active", !!open); | |
| cstlFilterBtn.setAttribute("aria-expanded", open ? "true" : "false"); | |
| cstlFilterBtn.setAttribute("aria-label", open ? "Hide chart filters" : "Show chart filters"); | |
| }); | |
| /* Roadmap cards + "not plottable" rows open the same model-details modal. */ | |
| const openFromEl = (el) => openModelModal(el.getAttribute("data-key"), el.getAttribute("data-cfg") || "", el.getAttribute("data-fmt") || "GGUF"); | |
| document.addEventListener("click", (e) => { | |
| const el = e.target.closest('[data-rm-card="1"]'); | |
| if (el) openFromEl(el); | |
| }); | |
| document.addEventListener("keydown", (e) => { | |
| if (e.key !== "Enter" && e.key !== " ") return; | |
| const el = e.target.closest?.('[data-rm-card="1"]'); | |
| if (!el) return; | |
| e.preventDefault(); | |
| openFromEl(el); | |
| }); | |
| const canvas = $("cstlCanvas"); | |
| if (canvas && canvas.dataset.bound !== "1") { | |
| canvas.dataset.bound = "1"; | |
| const tip = $("cstlTooltip"); | |
| canvas.addEventListener("mousemove", (evt) => { | |
| const hit = hitTest(CSTL_HIT, evt, canvas); | |
| if (!hit || !tip) { if (tip) tip.style.display = "none"; return; } | |
| const p = hit.data; | |
| const rect = canvas.parentElement.getBoundingClientRect(); | |
| const x = evt.clientX - rect.left, y = evt.clientY - rect.top; | |
| const ref = accuracyReferenceOf(p.key, CSTL_REFS); | |
| tip.innerHTML = `<b>${esc(p.name)}</b><br/>` | |
| + `${esc(p.quant)} · ${esc(p.fmt)} · ${esc(p.target)} · ${esc(p.source)} · ${esc(sizeLabel(p.sizeM))}<br/>` | |
| + pointKpiText(p).map(t => esc(t)).join("<br/>") | |
| + (ref && p.acc == null | |
| ? `<br/><span class="mt-ref">${esc(ref.quant)} reference · Accuracy ${esc(Math.round(ref.acc * 1000) / 10)}%</span>` | |
| : ""); | |
| tip.style.display = "block"; | |
| tip.style.left = `${Math.max(8, Math.min(x + 14, rect.width - 260))}px`; | |
| tip.style.top = `${Math.max(8, y - 10)}px`; | |
| }); | |
| canvas.addEventListener("mouseleave", () => { if (tip) tip.style.display = "none"; }); | |
| canvas.addEventListener("click", (evt) => { | |
| const hit = hitTest(CSTL_HIT, evt, canvas); | |
| if (hit) openModelModal(hit.data.key, hit.data.cfgId, hit.data.fmt); | |
| }); | |
| } | |
| redrawConstellation(); | |
| } | |
| /* ====================================================================== */ | |
| /* CATALOG — card gallery + facet rail + table view */ | |
| /* ====================================================================== */ | |
| const SIZE_BUCKETS = [ | |
| { id: "≤1B", test: s => s != null && s <= 1000 }, | |
| { id: "1–4B", test: s => s != null && s > 1000 && s <= 4000 }, | |
| { id: "4–10B", test: s => s != null && s > 4000 && s <= 10000 }, | |
| { id: ">10B", test: s => s != null && s > 10000 }, | |
| ]; | |
| const CATALOG_STATE = { | |
| search: "", sort: "name_asc", view: "grid", | |
| facets: { family: new Set(), size: new Set(), format: new Set(), target: new Set(), quant: new Set(), task: new Set(), availability: new Set(), modality: new Set() }, | |
| // Which KPI *kinds* to render on cards/table — a display filter, not a model filter | |
| // (unchecking one hides that metric everywhere but never hides a model). All on by | |
| // default so a model shows every metric it actually reports out of the box. | |
| kpi: new Set(KPI_KINDS.map(k => k.id)), | |
| // Desktop-only: rail starts expanded. Cleared automatically below 1041px | |
| // (mobile has its own separate collapse-behind-toggle mechanism). | |
| filtersCollapsed: false, | |
| }; | |
| function bestVariantBySource(m, source) { | |
| let best = null, bestLat = null, bestStage = null; | |
| for (const cfg of safeList(m.hardware_configs, [])) { | |
| for (const art of allArtifactsOf(m)) { | |
| const got = pickBlock(cfg.metrics || {}, art, source); | |
| if (!got) continue; | |
| const thr = throughputOf(got.block); | |
| const lat = latencyMsOf(got.block); | |
| const target = safeList(cfg.targets, safeList(m.targets, []))[0] || "—"; | |
| const util = utilizationOf(got.block); | |
| const lastUpdated = got.block ? got.block.last_updated : null; | |
| if (thr.value != null) { | |
| if (!best || thr.value > best.tok) { | |
| best = { tok: thr.value, unit: thr.unit, latencyMs: lat, quant: art, source: got.source, target, util, lastUpdated, block: got.block }; | |
| } | |
| } else if (lat != null) { | |
| // No throughput published — keep the lowest-latency variant as a fallback | |
| // (e.g. a vision model with only a latency estimate). | |
| if (!bestLat || lat < bestLat.latencyMs) { | |
| bestLat = { tok: null, unit: thr.unit, latencyMs: lat, quant: art, source: got.source, target, util, lastUpdated, block: got.block }; | |
| } | |
| } else { | |
| // No throughput and no total latency either — a dual-encoder model | |
| // (SigLIP, CLIP-style) reports only per-tower stage timings | |
| // (block.stages). The two towers run independently, so the model's | |
| // effective latency is bounded by its slower tower, not their sum — | |
| // take the slowest stage (e.g. max(Vision Encoder, Text Encoder)) as | |
| // this variant's number, then across variants keep the best-performing | |
| // (lowest) one as a last resort so it still gets a card instead of "No | |
| // published throughput yet"; kpiChipsOf/cardHTML already know how to | |
| // render a stage chip once `best` isn't null. | |
| const stages = safeList(got.block && got.block.stages, []); | |
| if (stages.length) { | |
| const stageMax = Math.max(...stages.map(s => Number(s && s.ms) || 0)); | |
| if (!bestStage || stageMax < bestStage.stageMax) { | |
| bestStage = { tok: null, unit: thr.unit, latencyMs: null, stageMax, quant: art, source: got.source, target, util, lastUpdated, block: got.block }; | |
| } | |
| } | |
| } | |
| } | |
| } | |
| return best || bestLat || bestStage; | |
| } | |
| function bestVariantOf(m) { return bestVariantBySource(m, "Best"); } | |
| /* Recency cards prioritise measured HIL numbers: show the best HIL variant | |
| when any exists, only falling back to the best Estimate when it has none. */ | |
| function bestHilFirstVariantOf(m) { | |
| return bestVariantBySource(m, "HIL") || bestVariantBySource(m, "Estimate"); | |
| } | |
| function sizeBucketOf(s) { const b = SIZE_BUCKETS.find(x => x.test(s)); return b ? b.id : "—"; } | |
| function modelFormats(m) { const f = []; if (m.gguf_repo) f.push("GGUF"); if (m.onnx_repo) f.push("ONNX"); return f; } | |
| /* Friendly display name for a known architecture.family slug; anything not listed | |
| falls back to a generic title-case of the slug (see familyOf()). Vendor | |
| capitalisation matters here — a title-cased slug reads as "Yolo"/"Vgg"/"Detr", | |
| so every family actually present in the catalog should have an entry. */ | |
| const FAMILY_LABELS = { | |
| // language / multimodal | |
| llama: "Llama", qwen: "Qwen", gemma: "Gemma", gr00t: "GR00T", alpamayo: "Alpamayo", | |
| siglip: "SigLIP", clip: "CLIP", phi: "Phi", mistral: "Mistral", | |
| // classification backbones | |
| resnet: "ResNet", resnext: "ResNeXt", vgg: "VGG", vit: "ViT", swin: "Swin", | |
| convnext: "ConvNeXt", densenet: "DenseNet", inception: "Inception", | |
| mobilenet: "MobileNet", mobileone: "MobileOne", efficientnet: "EfficientNet", | |
| hrnet: "HRNet", dino: "DINO", metaformer: "MetaFormer", | |
| // detection | |
| yolo: "YOLO", detr: "DETR", ssd: "SSD", retinanet: "RetinaNet", | |
| centernet: "CenterNet", pointpillars: "PointPillars", tao: "NVIDIA TAO", | |
| // segmentation / driving | |
| fcn: "FCN", unet: "UNet", deeplab: "DeepLab", segformer: "SegFormer", | |
| sparsedrive: "SparseDrive", | |
| }; | |
| /* Broad model family for the Catalog's "Family" filter (Llama, Qwen, YOLO, ...), | |
| coarser than `architecture` (which carries the size, e.g. "Llama-3.1-8B" — every | |
| size of the same family would otherwise be its own facet option). Derived from | |
| the raw `family` slug generate_models_json.py reads out of .metadata.yaml. | |
| `family` is the architecture LINEAGE only: the generation lives in | |
| architecture.version, so llama 3.1/3.2, yolo v3..v10 and mobilenet v2/v3 all | |
| bucket together without any string surgery here. This used to strip a trailing | |
| digit run to fold llama2/llama3, which mangled every slug that ended in a | |
| version — "dinov2" rendered as "Dinov", "yolov5" as "Yolov", "mobilenetv3" as | |
| "Mobilenetv". lint_metadata.py now rejects a versioned family slug at the | |
| source, so the slug is used as-is. */ | |
| function familyOf(m) { | |
| const slug = (m.family || "").toLowerCase().trim(); | |
| if (!slug) return m.architecture || "Unknown"; | |
| return FAMILY_LABELS[slug] || (slug.charAt(0).toUpperCase() + slug.slice(1)); | |
| } | |
| function facetDefs() { | |
| return [ | |
| { group: "availability", title: "Availability", opts: ["Model file available", "Coming soon"] }, | |
| { group: "modality", title: "Model type", opts: uniqueSorted(CATALOG.map(m => m.modality || "LLM")) }, | |
| { group: "task", title: "Task", opts: uniqueSorted(CATALOG.flatMap(m => safeList(m.tasks, []))) }, | |
| { group: "family", title: "Family", opts: uniqueSorted(CATALOG.map(familyOf)) }, | |
| { group: "size", title: "Size", opts: SIZE_BUCKETS.map(b => b.id).filter(id => CATALOG.some(m => sizeBucketOf(m.model_size_m) === id)) }, | |
| // Only precisions that actually have a benchmark run attached — an | |
| // unbenchmarked artifact (e.g. a shipped fp16 folder with no runs yet) | |
| // isn't a quantization a user can compare results for. | |
| { group: "quant", title: "Quantization", opts: uniqueSorted(CATALOG.flatMap(m => allArtifactsOf(m).filter(a => artifactHasBenchmark(m, a)).map(a => artifactPrecision(m, a)))) }, | |
| ]; | |
| } | |
| function buildFacetRail() { | |
| const el = $("cgFacets"); | |
| if (!el) return; | |
| let html = facetDefs().map(d => ` | |
| <div class="facet-group"> | |
| <div class="facet-group-title">${esc(d.title)}</div> | |
| ${d.opts.map(o => { | |
| const checked = CATALOG_STATE.facets[d.group].has(o) ? "checked" : ""; | |
| return `<label class="facet-opt"><input type="checkbox" data-facet-group="${esc(d.group)}" value="${esc(o)}" ${checked}/> <span>${esc(d.group === "task" ? prettyTask(o) : o)}</span></label>`; | |
| }).join("")} | |
| </div>`).join(""); | |
| // Display filter, not a model filter: unchecking a KPI kind hides that metric on | |
| // every card/table row but never removes a model from the results. | |
| html += ` | |
| <div class="facet-group"> | |
| <div class="facet-group-title">KPIs to display</div> | |
| ${KPI_KINDS.map(k => { | |
| const checked = CATALOG_STATE.kpi.has(k.id) ? "checked" : ""; | |
| return `<label class="facet-opt"><input type="checkbox" data-kpi-kind="${esc(k.id)}" ${checked}/> <span>${esc(k.label)}</span></label>`; | |
| }).join("")} | |
| </div>`; | |
| el.innerHTML = html; | |
| } | |
| function modelMatches(m) { | |
| const f = CATALOG_STATE.facets; | |
| if (f.availability.size) { | |
| const availOk = (f.availability.has("Model file available") && modelHasAvailableFile(m)) || | |
| (f.availability.has("Coming soon") && isComingSoon(m)); | |
| if (!availOk) return false; | |
| } | |
| if (f.modality.size && !f.modality.has(m.modality || "LLM")) return false; | |
| if (f.family.size && !f.family.has(familyOf(m))) return false; | |
| if (f.size.size && !f.size.has(sizeBucketOf(m.model_size_m))) return false; | |
| if (f.format.size && !modelFormats(m).some(x => f.format.has(x))) return false; | |
| if (f.target.size && !safeList(m.targets, []).some(x => f.target.has(x))) return false; | |
| if (f.quant.size && !allArtifactsOf(m).some(x => artifactHasBenchmark(m, x) && f.quant.has(artifactPrecision(m, x)))) return false; | |
| if (f.task.size && !safeList(m.tasks, []).some(x => f.task.has(x))) return false; | |
| const q = CATALOG_STATE.search.trim().toLowerCase(); | |
| if (q) { | |
| const hay = [m.display_name, m.key, m.architecture, m.base_model, m.license, safeList(m.tasks, []).join(" "), allArtifactsOf(m).join(" ")].join(" ").toLowerCase(); | |
| if (!hay.includes(q)) return false; | |
| } | |
| return true; | |
| } | |
| function sortModels(list) { | |
| const s = CATALOG_STATE.sort; | |
| const tok = m => { const b = bestVariantOf(m); return b && b.tok != null ? b.tok : -1; }; | |
| const arr = list.slice(); | |
| if (s === "size_desc") arr.sort((a, b) => (b.model_size_m || 0) - (a.model_size_m || 0)); | |
| else if (s === "size_asc") arr.sort((a, b) => (a.model_size_m || 0) - (b.model_size_m || 0)); | |
| else if (s === "tok_desc") arr.sort((a, b) => tok(b) - tok(a)); | |
| else if (s === "name_asc") arr.sort((a, b) => String(a.display_name || a.key).localeCompare(String(b.display_name || b.key))); | |
| return arr; | |
| } | |
| function monogramText(m) { | |
| const s = m.model_size_m; | |
| if (s != null) return s >= 1000 ? `${Math.round(s / 100) / 10}B` : `${s}M`; | |
| return (m.display_name || m.key || "?").slice(0, 2).toUpperCase(); | |
| } | |
| /* ---- Model designer / publisher logos (derived from base_model org or architecture) ---- */ | |
| const DESIGNERS = { | |
| meta: { name: "Meta", svg: `<img src="assets/meta_icon.png" width="15" height="15" alt="" aria-hidden="true" style="object-fit:contain" />` }, | |
| nvidia: { name: "NVIDIA", svg: `<img src="assets/nvidia_icon.png" width="15" height="15" alt="" aria-hidden="true" style="object-fit:contain" />` }, | |
| google: { name: "Google", svg: `<svg viewBox="0 0 24 24" width="15" height="15" aria-hidden="true"><path d="M23 12.2c0-.8-.1-1.6-.2-2.3H12v4.5h6.2a5.3 5.3 0 0 1-2.3 3.5v2.9h3.7C21.7 18.7 23 15.8 23 12.2Z" fill="#4285F4"/><path d="M12 24c3.1 0 5.7-1 7.6-2.8l-3.7-2.9c-1 .7-2.3 1.1-3.9 1.1-3 0-5.5-2-6.4-4.7H1.8v3C3.7 21.4 7.6 24 12 24Z" fill="#34A853"/><path d="M5.6 14.7a7.2 7.2 0 0 1 0-4.6V7.1H1.8a12 12 0 0 0 0 10.6l3.8-3Z" fill="#FBBC05"/><path d="M12 4.8c1.7 0 3.2.6 4.4 1.7l3.3-3.3C17.7 1.2 15.1 0 12 0 7.6 0 3.7 2.6 1.8 6.4l3.8 3C6.5 6.7 9 4.8 12 4.8Z" fill="#EA4335"/></svg>` }, | |
| microsoft: { name: "Microsoft", svg: `<svg viewBox="0 0 23 23" width="14" height="14" aria-hidden="true"><rect x="1" y="1" width="10" height="10" fill="#F25022"/><rect x="12" y="1" width="10" height="10" fill="#7FBA00"/><rect x="1" y="12" width="10" height="10" fill="#00A4EF"/><rect x="12" y="12" width="10" height="10" fill="#FFB900"/></svg>` }, | |
| mistral: { name: "Mistral AI", svg: `<svg viewBox="0 0 24 24" width="15" height="15" aria-hidden="true"><g><rect x="2" y="3" width="5" height="4.5" fill="#F7D046"/><rect x="17" y="3" width="5" height="4.5" fill="#F7D046"/><rect x="2" y="8" width="5" height="4.5" fill="#F2A73B"/><rect x="9.5" y="8" width="5" height="4.5" fill="#F2A73B"/><rect x="17" y="8" width="5" height="4.5" fill="#F2A73B"/><rect x="2" y="13" width="5" height="4.5" fill="#EE792F"/><rect x="17" y="13" width="5" height="4.5" fill="#EE792F"/><rect x="2" y="18" width="5" height="4.5" fill="#EA3326"/><rect x="17" y="18" width="5" height="4.5" fill="#EA3326"/></g></svg>` }, | |
| qwen: { name: "Qwen", svg: `<svg viewBox="0 0 200 200" width="15" height="15" aria-hidden="true"><path d="M174.82 108.75L155.38 75L165.64 57.75C166.46 56.31 166.46 54.53 165.64 53.09L155.38 35.84C154.86 34.91 153.87 34.33 152.78 34.33H114.88L106.14 19.03C105.62 18.1 104.63 17.52 103.54 17.52H83.3C82.21 17.52 81.22 18.1 80.7 19.03L61.26 52.77H41.02C39.93 52.77 38.94 53.35 38.42 54.28L28.16 71.53C27.34 72.97 27.34 74.75 28.16 76.19L45.52 107.5L36.78 122.8C35.96 124.24 35.96 126.02 36.78 127.46L47.04 144.71C47.56 145.64 48.55 146.22 49.64 146.22H87.54L96.28 161.52C96.8 162.45 97.79 163.03 98.88 163.03H119.12C120.21 163.03 121.2 162.45 121.72 161.52L141.16 127.78H158.52C159.61 127.78 160.6 127.2 161.12 126.27L171.38 109.02C172.2 107.58 172.2 105.8 171.38 104.36L174.82 108.75Z" fill="#5B57D1"/><path d="M119.12 163.03H98.88L87.54 144.71H49.64L61.26 126.39H80.7L38.42 55.29H61.26L83.3 19.03L93.56 37.35L83.3 55.29H161.58L151.32 72.54L170.76 106.28H151.32L141.16 88.34L101.18 163.03H119.12Z" fill="#fff"/><path d="M127.86 79.83H76.14L101.18 122.11L127.86 79.83Z" fill="#5B57D1"/></svg>` }, | |
| deepseek: { name: "DeepSeek", svg: `<svg viewBox="0 0 24 24" width="15" height="15" aria-hidden="true"><circle cx="12" cy="12" r="11" fill="#4D6BFE"/><path d="M6 9.5c3.2.2 4 2.2 6 2.2s2.2-2 .2-3.2" stroke="#fff" stroke-width="1.6" fill="none" stroke-linecap="round"/><circle cx="9.2" cy="14.2" r="1.3" fill="#fff"/></svg>` }, | |
| renesas: { name: "Renesas", svg: `<svg viewBox="0 0 24 24" width="15" height="15" aria-hidden="true"><rect width="24" height="24" rx="4" fill="#2A289D"/><text x="12" y="16.5" font-family="Arial, sans-serif" font-size="13" font-weight="700" fill="#fff" text-anchor="middle">R</text></svg>` }, | |
| onnx: { name: "ONNX", svg: `<img src="assets/onnx_icon.png" width="15" height="15" alt="" aria-hidden="true" style="object-fit:contain" />` }, | |
| generic: { name: "AI Model", svg: `<svg viewBox="0 0 24 24" width="15" height="15" aria-hidden="true"><circle cx="12" cy="12" r="11" fill="#8E8DA3"/><circle cx="12" cy="12" r="4" fill="#fff"/></svg>` }, | |
| }; | |
| function designerOf(m) { | |
| const base = (m.base_model || "").toLowerCase(); | |
| const org = base.includes("/") ? base.split("/")[0] : ""; | |
| const hay = `${org} ${(m.architecture || "").toLowerCase()} ${(m.key || "").toLowerCase()}`; | |
| if (org.includes("nvidia") || hay.includes("nemotron") || hay.includes("nemo") | |
| || hay.includes("gr00t") || hay.includes("groot") || hay.includes("alpamayo")) return DESIGNERS.nvidia; | |
| if (org.includes("meta") || org.includes("facebook") || hay.includes("llama")) return DESIGNERS.meta; | |
| if (org.includes("google") || hay.includes("gemma") || hay.includes("siglip")) return DESIGNERS.google; | |
| if (org.includes("microsoft") || hay.includes("phi")) return DESIGNERS.microsoft; | |
| if (org.includes("mistral") || hay.includes("mistral") || hay.includes("mixtral")) return DESIGNERS.mistral; | |
| if (org.includes("qwen") || org.includes("alibaba") || hay.includes("qwen")) return DESIGNERS.qwen; | |
| if (org.includes("deepseek") || hay.includes("deepseek")) return DESIGNERS.deepseek; | |
| if (org.includes("renesas")) return DESIGNERS.renesas; | |
| // "onnx" and "onnxmodelzoo" orgs, plus models with no known base model that only | |
| // ship an ONNX repo, are all the same publisher in practice — one badge, not three. | |
| if (org.includes("onnx")) return DESIGNERS.onnx; | |
| if (org) return { name: org.split(/[-_]/)[0].replace(/^\w/, c => c.toUpperCase()), svg: DESIGNERS.generic.svg }; | |
| if (!org && m.onnx_repo) return DESIGNERS.onnx; | |
| return DESIGNERS.generic; | |
| } | |
| function designerRowHTML(m) { | |
| const d = designerOf(m); | |
| return `<div class="mc-designer"><span class="mc-logo">${d.svg}</span><span class="mc-designer-name">${esc(d.name)}</span></div>`; | |
| } | |
| /* Card hero — a real sample image from the repo when the model ships one, | |
| else one of the compact card-format illustrations, cropped to a | |
| fixed-height strip so every card gets the same visual weight regardless | |
| of source. `compact` shrinks the strip for the Overview carousel, whose | |
| cards need to stay short; `overlay` drops extra markup (e.g. the | |
| "updated" badge) over the image instead of a separate row, to keep that | |
| compact form tight. `illustrationSrc` lets a caller (the Last Update | |
| carousel) override the hash-picked illustration, e.g. to avoid two | |
| neighbors landing on the same one. */ | |
| function cardBannerHTML(m, { compact = false, overlay = "", illustrationSrc = null } = {}) { | |
| const url = m && m.sample_image ? String(m.sample_image) : ""; | |
| const fallbackSrc = illustrationSrc || cardIllustrationSrc(m, { compact }); | |
| const fallbackImg = `<img class="mc-banner-img mc-banner-fallback" src="${esc(fallbackSrc)}" alt="" loading="lazy">`; | |
| const repoImg = url | |
| ? `<img class="mc-banner-img" src="${esc(url)}" alt="" loading="lazy" referrerpolicy="no-referrer" onerror="this.remove()">` | |
| : ""; | |
| const cls = "mc-banner" + (compact ? " mc-banner-sm" : ""); | |
| return `<div class="${cls}">${fallbackImg}${repoImg}${overlay}</div>`; | |
| } | |
| function cardHTML(m, illustrationSrc) { | |
| const soon = isComingSoon(m); | |
| const best = bestHilFirstVariantOf(m); | |
| const modalityChip = `<span class="badge modality">${esc(m.modality || "LLM")}</span>`; | |
| const _task = safeList(m.tasks, [])[0] || ""; | |
| const taskChip = _task ? `<span class="badge task">${esc(prettyTask(_task))}</span>` : ""; | |
| const pendingChip = (!soon && modelHasMissingFile(m)) | |
| ? `<span class="badge status pending" title="At least one benchmarked precision has no model file uploaded yet">⏳ File pending</span>` | |
| : ""; | |
| const estTag = best && best.source === "Estimate" ? " (est)" : ""; | |
| const topsBit = best && best.util && best.util.tops_used != null | |
| ? ` · ${esc(Math.round(best.util.tops_used * 10) / 10)} TOPS` | |
| : ""; | |
| // One headline number (throughput, else total latency, else a stage or accuracy — | |
| // subject to the "KPIs to display" filter) plus, always, exactly one more line: | |
| // the single best remaining KPI (e.g. SigLIP's Total latency headline with a | |
| // "Vision Encoder: 396.8 ms" third line; an LLM's tok/s headline with "Prefill: | |
| // 20.8 tok/s"). A fixed 3-line stat box, never a variable-length list, so every | |
| // card is the exact same size regardless of how many KPIs a model reports. | |
| const chips = (!soon && best) ? visibleKpiChips(best.block) : []; | |
| const primary = primaryKpiChip(chips); | |
| const secondary = secondaryKpiChip(chips, primary); | |
| const subLine = best ? `${esc(best.target)} · ${esc(best.quant)}${estTag}${topsBit}` : ""; | |
| const extraLine = `<span class="mc-stat-extra">${secondary ? `${esc(secondary.label)}: <b>${esc(fmtChipFull(secondary))}</b>` : ""}</span>`; | |
| const stat = soon | |
| ? `<div class="mc-stat soon"><span class="mc-stat-soon-text">🚧 Coming Soon</span></div>` | |
| : primary | |
| ? `<div class="mc-stat"><span class="mc-stat-num">${esc(fmtChipValue(primary))}<span class="mc-stat-unit">${esc(primary.unit)}</span></span><span class="mc-stat-sub">${subLine}</span>${extraLine}</div>` | |
| : `<div class="mc-stat empty"><span class="mc-stat-sub">No published throughput yet</span></div>`; | |
| const repo = m.gguf_repo || m.onnx_repo || ""; | |
| const repoLink = (!soon && repo) ? `<a class="link small" href="${esc(hfModelUrl(repo))}" target="_blank" rel="noopener noreferrer" onclick="event.stopPropagation()">View repo</a>` : ""; | |
| const firstCfg = safeFirstCfg(m); | |
| const defFmt = m.gguf_repo ? "GGUF" : (m.onnx_repo ? "ONNX" : "GGUF"); | |
| const updatedLine = best && best.lastUpdated ? `<span class="mc-meta-updated">Updated ${esc(best.lastUpdated)}</span>` : ""; | |
| const hasFooter = !!(m.license || repoLink || updatedLine); | |
| return ` | |
| <div class="model-card" data-card="1" data-key="${esc(m.key)}" data-cfg="${esc(firstCfg?.id || "")}" data-fmt="${esc(defFmt)}"> | |
| ${cardBannerHTML(m, { illustrationSrc })} | |
| ${designerRowHTML(m)} | |
| <div class="mc-head"> | |
| <div style="min-width:0"> | |
| <div class="mc-title">${esc(m.display_name || m.key)}</div> | |
| <div class="mc-sub">${esc(m.architecture || "")}${m.variant ? " · " + esc(m.variant) : ""}</div> | |
| </div> | |
| </div> | |
| <div class="mc-chips">${modalityChip}${taskChip}${pendingChip}</div> | |
| ${stat} | |
| <div class="mc-foot${hasFooter ? "" : " mc-foot--empty"}"> | |
| ${hasFooter ? `<div class="mc-meta-col"><span class="mc-meta">${esc(m.license || "")}</span>${updatedLine}</div>${repoLink}` : ""} | |
| </div> | |
| </div>`; | |
| } | |
| function renderCards(models) { | |
| const el = $("cardGrid"); | |
| if (!el) return; | |
| if (!models.length) { | |
| el.innerHTML = `<div class="subtle" style="padding:24px;">No models match the current filters.</div>`; | |
| return; | |
| } | |
| const illustrations = sequentialIllustrationSrcs(models); | |
| el.innerHTML = models.map((m, i) => cardHTML(m, illustrations[i])).join(""); | |
| } | |
| /* Flags the filter icon/button whenever a facet or search narrows the | |
| catalog — the desktop rail collapses to icon-only width and the mobile | |
| rail hides its body behind this same button, so without a visible mark | |
| it's easy to forget a filter is still applied. KPI checkboxes are a | |
| display option, not a model filter (see facetDefs), so they're excluded. */ | |
| function updateFilterIndicator() { | |
| const active = !!(CATALOG_STATE.search && CATALOG_STATE.search.trim()) | |
| || Object.values(CATALOG_STATE.facets).some(s => s.size > 0); | |
| $("facetsCollapseToggle")?.classList.toggle("has-filters", active); | |
| document.querySelector('.filter-toggle[data-target="facetsBody"]')?.classList.toggle("has-filters", active); | |
| } | |
| function renderCatalog() { | |
| updateFilterIndicator(); | |
| const filtered = sortModels(CATALOG.filter(modelMatches)); | |
| const count = $("cgCount"); | |
| if (count) count.innerHTML = `<b>${filtered.length}</b> of ${CATALOG.length} models`; | |
| const grid = $("cardGrid"), table = $("tableView"); | |
| if (CATALOG_STATE.view === "grid") { | |
| if (grid) grid.hidden = false; | |
| if (table) table.hidden = true; | |
| renderCards(filtered); | |
| } else { | |
| if (grid) grid.hidden = true; | |
| if (table) table.hidden = false; | |
| renderTable(buildTableRows(filtered)); | |
| requestAnimationFrame(() => setupPinnedColumns($("modelsTable"), 1)); | |
| } | |
| } | |
| function setView(view) { | |
| CATALOG_STATE.view = view; | |
| $("cgViewGrid")?.classList.toggle("active", view === "grid"); | |
| $("cgViewTable")?.classList.toggle("active", view === "table"); | |
| $("cgViewGrid")?.setAttribute("aria-pressed", String(view === "grid")); | |
| $("cgViewTable")?.setAttribute("aria-pressed", String(view === "table")); | |
| renderCatalog(); | |
| } | |
| function toggleFacet(group, value) { | |
| const set = CATALOG_STATE.facets[group]; | |
| if (!set) return; | |
| if (set.has(value)) set.delete(value); else set.add(value); | |
| buildFacetRail(); | |
| renderCatalog(); | |
| } | |
| function initCatalog() { | |
| buildFacetRail(); | |
| $("cgSearch")?.addEventListener("input", () => { | |
| clearTimeout(window.__cgT); | |
| window.__cgT = setTimeout(() => { CATALOG_STATE.search = $("cgSearch").value; renderCatalog(); }, 90); | |
| }); | |
| $("cgSort")?.addEventListener("change", () => { CATALOG_STATE.sort = $("cgSort").value; renderCatalog(); }); | |
| $("cgReset")?.addEventListener("click", () => { | |
| CATALOG_STATE.search = ""; CATALOG_STATE.sort = "name_asc"; | |
| Object.values(CATALOG_STATE.facets).forEach(s => s.clear()); | |
| CATALOG_STATE.kpi = new Set(KPI_KINDS.map(k => k.id)); | |
| if ($("cgSearch")) $("cgSearch").value = ""; | |
| if ($("cgSort")) $("cgSort").value = "name_asc"; | |
| buildFacetRail(); | |
| renderCatalog(); | |
| }); | |
| $("cgViewGrid")?.addEventListener("click", () => setView("grid")); | |
| $("cgViewTable")?.addEventListener("click", () => setView("table")); | |
| // Desktop-only collapse: rail shrinks to icon width, everything in it hidden | |
| // until re-expanded. Starts expanded (see CATALOG_STATE.filtersCollapsed). | |
| const setFiltersCollapsed = (collapsed) => { | |
| CATALOG_STATE.filtersCollapsed = collapsed; | |
| $$(".facets").forEach(el => el.classList.toggle("collapsed", collapsed)); | |
| $$(".catalog-layout").forEach(el => el.classList.toggle("filters-collapsed", collapsed)); | |
| const btn = $("facetsCollapseToggle"); | |
| btn?.setAttribute("aria-expanded", String(!collapsed)); | |
| btn?.setAttribute("aria-label", collapsed ? "Expand filters" : "Collapse filters"); | |
| }; | |
| $("facetsCollapseToggle")?.addEventListener("click", () => setFiltersCollapsed(!CATALOG_STATE.filtersCollapsed)); | |
| // Below the desktop breakpoint the collapse button is hidden and mobile has its | |
| // own collapse-behind-toggle instead — never leave the rail stuck collapsed | |
| // (or the layout stuck at icon-width) if the window is resized narrower. | |
| const desktopFilterMQ = window.matchMedia("(min-width: 1041px)"); | |
| desktopFilterMQ.addEventListener?.("change", (e) => { if (!e.matches) setFiltersCollapsed(false); }); | |
| $("cgFacets")?.addEventListener("change", (e) => { | |
| const t = e.target; | |
| const kpiKind = t.getAttribute("data-kpi-kind"); | |
| if (kpiKind) { | |
| if (t.checked) CATALOG_STATE.kpi.add(kpiKind); else CATALOG_STATE.kpi.delete(kpiKind); | |
| renderCatalog(); | |
| return; | |
| } | |
| const group = t.getAttribute("data-facet-group"); | |
| if (!group) return; | |
| const set = CATALOG_STATE.facets[group]; | |
| if (t.checked) set.add(t.value); else set.delete(t.value); | |
| renderCatalog(); | |
| }); | |
| $("cardGrid")?.addEventListener("click", (e) => { | |
| const chip = e.target.closest("[data-facet]"); | |
| if (chip) { e.stopPropagation(); toggleFacet(chip.getAttribute("data-facet"), chip.getAttribute("data-value")); return; } | |
| if (e.target.closest("a")) return; | |
| const card = e.target.closest('.model-card[data-card="1"]'); | |
| if (card) openModelModal(card.getAttribute("data-key"), card.getAttribute("data-cfg") || "", card.getAttribute("data-fmt") || "GGUF"); | |
| }); | |
| // Clicking anywhere on a table row opens the model modal, same as its Details | |
| // button — except links (repo buttons) and the Details button itself, which | |
| // already have their own click behavior. | |
| $("modelsTable")?.addEventListener("click", (e) => { | |
| if (e.target.closest("a, button")) return; | |
| const row = e.target.closest("tbody tr[data-key]"); | |
| if (!row) return; | |
| const key = row.getAttribute("data-key"); | |
| if (key) openModelModal(key, row.getAttribute("data-cfg") || "", row.getAttribute("data-fmt") || "ONNX"); | |
| }); | |
| renderCatalog(); | |
| } | |
| /* ---------- Boot ---------- */ | |
| async function init() { | |
| // Tabs | |
| document.addEventListener("click", (e) => { | |
| const navBtn = e.target.closest(".nav-item[data-target], .brand[data-target]"); | |
| if (navBtn) setActiveSection(navBtn.getAttribute("data-target")); | |
| }); | |
| // Copy-to-clipboard (Download command) | |
| document.addEventListener("click", async (e) => { | |
| const btn = e.target.closest('button[data-copy="1"]'); | |
| if (!btn) return; | |
| const text = btn.getAttribute("data-copy-text") || ""; | |
| if (!text) return; | |
| const setCopiedUI = () => { | |
| btn.classList.add("copied"); | |
| const label = btn.querySelector(".txt"); | |
| if (label) label.textContent = "Copied"; | |
| setTimeout(() => { | |
| btn.classList.remove("copied"); | |
| if (label) label.textContent = "Copy"; | |
| }, 900); | |
| }; | |
| try { | |
| if (navigator.clipboard && navigator.clipboard.writeText) { | |
| await navigator.clipboard.writeText(text); | |
| setCopiedUI(); | |
| return; | |
| } | |
| } catch { | |
| // fallback below | |
| } | |
| try { | |
| const ta = document.createElement("textarea"); | |
| ta.value = text; | |
| ta.setAttribute("readonly", "true"); | |
| ta.style.position = "fixed"; | |
| ta.style.left = "-9999px"; | |
| document.body.appendChild(ta); | |
| ta.select(); | |
| document.execCommand("copy"); | |
| document.body.removeChild(ta); | |
| setCopiedUI(); | |
| } catch { | |
| // ignore | |
| } | |
| }); | |
| // Model-detail selector chips: hardware config / precision / NPU | |
| // allocation / chart unit, shared by the full page and the modal — one | |
| // state update + re-render, no per-control handler needed. | |
| document.addEventListener("click", (e) => { | |
| const btn = e.target.closest("[data-mdl-set]"); | |
| if (!btn) return; | |
| e.preventDefault(); | |
| const scope = btn.getAttribute("data-mdl-scope"); | |
| const field = btn.getAttribute("data-mdl-set"); | |
| const value = btn.getAttribute("data-mdl-value"); | |
| const state = scope === "modal" ? MODAL_STATE : PAGE_STATE; | |
| if (!state) return; | |
| state[field] = value; | |
| if (field === "cfgId") { state.precision = null; state.allocationId = null; } | |
| if (field === "precision") state.allocationId = null; | |
| if (scope === "modal") renderModalNow(); else renderPageNow(); | |
| }); | |
| // In-page navigation from the model page's breadcrumb. | |
| document.addEventListener("click", (e) => { | |
| const el = e.target.closest("[data-mdl-nav]"); | |
| if (el) { e.preventDefault(); setActiveSection(el.getAttribute("data-mdl-nav")); } | |
| }); | |
| // Modal links ("All N benchmark runs →", "Download & run instructions →") | |
| // route to the full model page. | |
| document.addEventListener("click", (e) => { | |
| const a = e.target.closest("[data-mdl-goto-page]"); | |
| if (!a) return; | |
| e.preventDefault(); | |
| closeModelModal(); | |
| openModelPage(a.getAttribute("data-key"), a.getAttribute("data-cfg") || "", a.getAttribute("data-fmt") || "ONNX", { tab: a.getAttribute("data-tab") || "runs" }); | |
| }); | |
| // Modal open from Details buttons (table) | |
| document.addEventListener("click", (e) => { | |
| const btn = e.target.closest('button[data-open-model="1"]'); | |
| if (!btn) return; | |
| const key = btn.getAttribute("data-key"); | |
| const cfg = btn.getAttribute("data-cfg") || ""; | |
| const fmt = btn.getAttribute("data-fmt") || "ONNX"; | |
| if (key) openModelModal(key, cfg, fmt); | |
| }); | |
| // Modal close | |
| document.addEventListener("click", (e) => { | |
| if (e.target && e.target.getAttribute("data-modal-close") === "1") closeModelModal(); | |
| }); | |
| document.addEventListener("keydown", (e) => { | |
| if (e.key === "Escape") { | |
| const modal = $("modelModal"); | |
| if (modal && modal.classList.contains("open")) closeModelModal(); | |
| } | |
| }); | |
| // Load data | |
| CATALOG_ROOT = await loadJSON(MODELS_FILE, { models: [] }); | |
| CATALOG = safeList(CATALOG_ROOT.models, []); | |
| CATALOG_NOTE = CATALOG_ROOT.catalog_note || CATALOG_NOTE; | |
| KPI_REGISTRY = CATALOG_ROOT.kpi_registry || {}; | |
| LINKS = await loadJSON(LINKS_FILE, { spaces: {}, collections: {} }); | |
| KEY_TO_MODEL = Object.fromEntries(CATALOG.map(m => [m.key, m])); | |
| ARCHS = ["All"].concat(uniqueSorted(CATALOG.map(m => m.architecture || "Unknown"))); | |
| MODALITIES = ["All"].concat(uniqueSorted(CATALOG.map(m => m.modality || "LLM"))); | |
| VARIANTS = ["All"].concat(uniqueSorted(CATALOG.map(m => m.variant || "Default"))); | |
| /* Catalog (cards + table + facet rail) */ | |
| initCatalog(); | |
| /* Overview (constellation) */ | |
| initOverview(); | |
| /* Deep link to a model's full page: #/model/<key> */ | |
| window.addEventListener("hashchange", routeFromHash); | |
| routeFromHash(); | |
| window.addEventListener("resize", () => { | |
| if ($("sec-overview")?.classList.contains("visible")) redrawConstellation(); | |
| if (CATALOG_STATE.view === "table") setupPinnedColumns($("modelsTable"), 1); | |
| }); | |
| } | |
| document.addEventListener("DOMContentLoaded", init); |