Updated benchmarks
This commit is contained in:
@@ -0,0 +1,503 @@
|
||||
:root {
|
||||
--bg: #f5f6fa;
|
||||
--ink: #101828;
|
||||
--muted: #6b7080;
|
||||
--accent: #155eef;
|
||||
--border: #d8dce6;
|
||||
--card: #ffffff;
|
||||
--chip-bg: #e6ecff;
|
||||
--chip-active-bg: #155eef;
|
||||
--chip-active-ink: #fff;
|
||||
--winner-bg: #d7f5e3;
|
||||
--winner-ink: #025333;
|
||||
--warn: #c2410c;
|
||||
--model-col: 180px;
|
||||
--winner-col: 120px;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font: 13px/1.35 "Inter", "Segoe UI", system-ui, -apple-system, sans-serif;
|
||||
background: var(--bg);
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
header {
|
||||
padding: 14px 20px 4px;
|
||||
background: var(--card);
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
header h1 {
|
||||
margin: 0 0 4px;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
header p {
|
||||
margin: 2px 0;
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.controls,
|
||||
.panel {
|
||||
background: var(--card);
|
||||
border-bottom: 1px solid var(--border);
|
||||
padding: 10px 20px;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.control {
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.control.grow {
|
||||
flex: 1 1 320px;
|
||||
}
|
||||
|
||||
.slider-block {
|
||||
min-width: 260px;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
font-size: 10px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
color: var(--muted);
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
input[type="text"],
|
||||
select {
|
||||
width: 100%;
|
||||
padding: 6px 9px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--border);
|
||||
font-size: 13px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.chip-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.chip {
|
||||
border: none;
|
||||
border-radius: 999px;
|
||||
padding: 3px 10px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
background: var(--chip-bg);
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.chip.active {
|
||||
background: var(--chip-active-bg);
|
||||
color: var(--chip-active-ink);
|
||||
}
|
||||
|
||||
.chip.small {
|
||||
font-size: 11px;
|
||||
padding: 3px 8px;
|
||||
}
|
||||
|
||||
.panel.compact {
|
||||
padding: 8px 20px;
|
||||
}
|
||||
|
||||
.panel-split {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.backend-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px 14px;
|
||||
}
|
||||
|
||||
.backend-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.backend-actions {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.backend-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 12px;
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.backend-item input {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
.backend-item .tag {
|
||||
font-size: 10px;
|
||||
padding: 0 6px;
|
||||
border-radius: 999px;
|
||||
background: #eef2ff;
|
||||
color: #1d3ea5;
|
||||
text-transform: uppercase;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.stats-box {
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
#tables {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.test-block h2 {
|
||||
margin: 0 0 4px;
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
|
||||
.table-wrap {
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--card);
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.table-scroll {
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
scrollbar-gutter: stable both-edges;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.table-scroll table {
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
font-size: 11.5px;
|
||||
width: max-content;
|
||||
min-width: 100%;
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
thead {
|
||||
background: #f4f6fb;
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
padding: 4px 6px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
white-space: normal;
|
||||
border-right: 1px solid var(--border);
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
th {
|
||||
position: relative;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
th.sticky,
|
||||
td.sticky {
|
||||
position: sticky;
|
||||
left: 0;
|
||||
background: inherit;
|
||||
z-index: 3;
|
||||
box-shadow: 1px 0 0 var(--border);
|
||||
}
|
||||
|
||||
th.model,
|
||||
td.model {
|
||||
width: var(--model-col);
|
||||
position: sticky;
|
||||
left: 0;
|
||||
z-index: 3;
|
||||
background: #f8f9ff;
|
||||
}
|
||||
|
||||
th.winner,
|
||||
td.winner {
|
||||
width: var(--winner-col);
|
||||
position: sticky;
|
||||
left: var(--model-col);
|
||||
z-index: 3;
|
||||
background: #f1f5ff;
|
||||
}
|
||||
|
||||
td.model {
|
||||
min-width: 170px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.data-cell {
|
||||
white-space: normal;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.data-cell[data-env]:hover::after {
|
||||
content: attr(data-env);
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
left: 50%;
|
||||
transform: translate(-50%, -120%);
|
||||
background: rgba(16, 24, 40, 0.92);
|
||||
color: #fff;
|
||||
padding: 4px 8px;
|
||||
border-radius: 6px;
|
||||
font-size: 11px;
|
||||
white-space: nowrap;
|
||||
pointer-events: none;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.data-cell[data-env]:hover::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -30%);
|
||||
border: 6px solid transparent;
|
||||
border-top-color: rgba(16, 24, 40, 0.92);
|
||||
pointer-events: none;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.data-cell .measure,
|
||||
.data-cell .std {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.row-actions {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
margin-top: 4px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.row-action-btn {
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--accent);
|
||||
font-size: 11px;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 2px;
|
||||
}
|
||||
|
||||
.row-action-btn:hover {
|
||||
color: #0d3fb8;
|
||||
}
|
||||
|
||||
td.model .meta {
|
||||
font-size: 10px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
tbody tr:nth-child(even) td {
|
||||
background: #fafbff;
|
||||
}
|
||||
|
||||
.measure {
|
||||
font-feature-settings: "tnum";
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.std {
|
||||
color: var(--muted);
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.winner-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.winner-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 2px 6px;
|
||||
border-radius: 999px;
|
||||
font-size: 10px;
|
||||
background: #dbeafe;
|
||||
color: #1e3a8a;
|
||||
margin: 1px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.cell-error {
|
||||
color: var(--warn);
|
||||
}
|
||||
|
||||
.cell-empty {
|
||||
color: #c3c7d1;
|
||||
}
|
||||
|
||||
.best {
|
||||
background: var(--winner-bg) !important;
|
||||
color: var(--winner-ink);
|
||||
}
|
||||
|
||||
td.best .measure,
|
||||
td.best .std {
|
||||
color: var(--winner-ink);
|
||||
}
|
||||
|
||||
.resize-handle {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 6px;
|
||||
height: 100%;
|
||||
cursor: col-resize;
|
||||
}
|
||||
|
||||
.resize-handle::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
th.backend-header {
|
||||
cursor: grab;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
th.backend-header.dragging {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
th.backend-header.drop-target {
|
||||
outline: 2px dashed var(--accent);
|
||||
}
|
||||
|
||||
.resize-line {
|
||||
width: 2px;
|
||||
background: var(--accent);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.resize-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.resize-bar {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 6px;
|
||||
cursor: col-resize;
|
||||
pointer-events: auto;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 0 6px;
|
||||
border-radius: 999px;
|
||||
background: #f1f5ff;
|
||||
color: #1d4ed8;
|
||||
font-size: 11px;
|
||||
}
|
||||
.range-wrap {
|
||||
position: relative;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.range-wrap input[type="range"] {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
background: transparent;
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.range-wrap input[type="range"]::-webkit-slider-thumb {
|
||||
pointer-events: auto;
|
||||
-webkit-appearance: none;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 50%;
|
||||
background: var(--accent);
|
||||
border: 2px solid #fff;
|
||||
box-shadow: 0 0 3px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.range-wrap input[type="range"]::-moz-range-thumb {
|
||||
pointer-events: auto;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 50%;
|
||||
background: var(--accent);
|
||||
border: 2px solid #fff;
|
||||
}
|
||||
|
||||
.range-track {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 6px;
|
||||
border-radius: 999px;
|
||||
background: #e3e7f1;
|
||||
transform: translateY(-50%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.range-values {
|
||||
font-size: 11px;
|
||||
color: var(--muted);
|
||||
margin-top: 4px;
|
||||
}
|
||||
@@ -0,0 +1,687 @@
|
||||
const DEFAULT_CTX = "default";
|
||||
const K_SIGMA = 1.0;
|
||||
const MIN_TOL = 0.25;
|
||||
const MODEL_COL_WIDTH = 180;
|
||||
const WINNER_COL_WIDTH = 120;
|
||||
|
||||
const state = {
|
||||
contexts: [],
|
||||
contextMap: new Map(),
|
||||
envs: [],
|
||||
backendOrder: [],
|
||||
columnWidths: {},
|
||||
filters: {
|
||||
search: "",
|
||||
quant: "",
|
||||
context: DEFAULT_CTX,
|
||||
backends: new Set(),
|
||||
sizeLo: null,
|
||||
sizeHi: null,
|
||||
},
|
||||
ui: {},
|
||||
sizeStats: { min: Infinity, max: -Infinity },
|
||||
draggingEnv: null,
|
||||
};
|
||||
|
||||
document.addEventListener("DOMContentLoaded", async () => {
|
||||
cacheUI();
|
||||
try {
|
||||
const res = await fetch("results.json");
|
||||
const data = await res.json();
|
||||
prepareData(data?.runs || []);
|
||||
initializeControls();
|
||||
renderTables();
|
||||
} catch (err) {
|
||||
console.error("Failed to load results.json", err);
|
||||
state.ui.stats.textContent = "Failed to load results.json";
|
||||
}
|
||||
});
|
||||
|
||||
function cacheUI() {
|
||||
state.ui = {
|
||||
search: document.getElementById("filter-search"),
|
||||
quant: document.getElementById("filter-quant"),
|
||||
contextChips: document.getElementById("context-chips"),
|
||||
backendList: document.getElementById("backend-list"),
|
||||
backendAll: document.getElementById("backend-all"),
|
||||
backendNone: document.getElementById("backend-none"),
|
||||
sizeLo: document.getElementById("sizeLo"),
|
||||
sizeHi: document.getElementById("sizeHi"),
|
||||
sizeTrack: document.getElementById("sizeTrack"),
|
||||
sizeLoVal: document.getElementById("sizeLoVal"),
|
||||
sizeHiVal: document.getElementById("sizeHiVal"),
|
||||
stats: document.getElementById("stats-line"),
|
||||
resetBtn: document.getElementById("reset-layout"),
|
||||
tables: document.getElementById("tables"),
|
||||
};
|
||||
}
|
||||
|
||||
function prepareData(runs) {
|
||||
const contextMap = new Map();
|
||||
const envSet = new Set();
|
||||
const quantSet = new Set();
|
||||
|
||||
for (const run of runs) {
|
||||
const test = normalizeTest(run.test);
|
||||
if (!test || !run.env) continue;
|
||||
const contextKey = run.context || DEFAULT_CTX;
|
||||
const env = run.env;
|
||||
envSet.add(env);
|
||||
if (run.quant) quantSet.add(run.quant.toUpperCase());
|
||||
|
||||
const ctx = ensureContext(contextMap, contextKey, run.context_tokens);
|
||||
const testEntry = ensureTest(ctx, test.original);
|
||||
|
||||
const modelName = run.model_clean || run.model;
|
||||
const row = ensureModel(testEntry, modelName, run);
|
||||
row.backends[env] = {
|
||||
mean: typeof run.tps_mean === "number" ? run.tps_mean : null,
|
||||
std: typeof run.tps_std === "number" ? run.tps_std : null,
|
||||
error: Boolean(run.error),
|
||||
error_type: run.error_type || null,
|
||||
};
|
||||
}
|
||||
|
||||
state.contextMap = contextMap;
|
||||
state.contexts = [...contextMap.values()].sort((a, b) => {
|
||||
if (a.key === DEFAULT_CTX) return -1;
|
||||
if (b.key === DEFAULT_CTX) return 1;
|
||||
if (a.tokens && b.tokens) return a.tokens - b.tokens;
|
||||
if (a.tokens) return -1;
|
||||
if (b.tokens) return 1;
|
||||
return a.key.localeCompare(b.key);
|
||||
});
|
||||
state.envs = [...envSet].sort();
|
||||
state.backendOrder = [...state.envs];
|
||||
state.columnWidths = Object.fromEntries(state.envs.map((env) => [env, 120]));
|
||||
state.quantOptions = [...quantSet].sort();
|
||||
state.filters.context = state.contexts[0]?.key || DEFAULT_CTX;
|
||||
state.filters.backends = new Set(state.envs);
|
||||
}
|
||||
|
||||
function ensureContext(map, key, tokens) {
|
||||
if (!map.has(key)) {
|
||||
map.set(key, {
|
||||
key,
|
||||
label: formatContextLabel(key, tokens),
|
||||
tokens: tokens ?? null,
|
||||
tests: new Map(),
|
||||
});
|
||||
} else if (tokens && !map.get(key).tokens) {
|
||||
const ctx = map.get(key);
|
||||
ctx.tokens = tokens;
|
||||
ctx.label = formatContextLabel(key, tokens);
|
||||
}
|
||||
return map.get(key);
|
||||
}
|
||||
|
||||
function ensureTest(ctx, testName) {
|
||||
if (!ctx.tests.has(testName)) {
|
||||
ctx.tests.set(testName, {
|
||||
name: testName,
|
||||
models: new Map(),
|
||||
});
|
||||
}
|
||||
return ctx.tests.get(testName);
|
||||
}
|
||||
|
||||
function ensureModel(testEntry, modelName, run) {
|
||||
if (!testEntry.models.has(modelName)) {
|
||||
testEntry.models.set(modelName, {
|
||||
model: modelName,
|
||||
quant: (run.quant || "Unknown").toUpperCase(),
|
||||
sizeB: run.name_params_b ?? run.params_b ?? null,
|
||||
backends: {},
|
||||
search_blob: [modelName, run.quant, run.env, run.test]
|
||||
.filter(Boolean)
|
||||
.map((s) => s.toString().toLowerCase())
|
||||
.join(" "),
|
||||
});
|
||||
}
|
||||
const row = testEntry.models.get(modelName);
|
||||
const sizeCandidate = run.name_params_b ?? run.params_b;
|
||||
if (row.sizeB == null && typeof sizeCandidate === "number") {
|
||||
row.sizeB = sizeCandidate;
|
||||
}
|
||||
if (typeof row.sizeB === "number") {
|
||||
state.sizeStats.min = Math.min(state.sizeStats.min, row.sizeB);
|
||||
state.sizeStats.max = Math.max(state.sizeStats.max, row.sizeB);
|
||||
}
|
||||
return row;
|
||||
}
|
||||
|
||||
function initializeControls() {
|
||||
const { quant, contextChips, backendList, search, resetBtn, sizeLo, sizeHi } = state.ui;
|
||||
|
||||
quant.innerHTML = "";
|
||||
const anyOpt = document.createElement("option");
|
||||
anyOpt.value = "";
|
||||
anyOpt.textContent = "Any";
|
||||
quant.appendChild(anyOpt);
|
||||
state.quantOptions.forEach((q) => {
|
||||
const opt = document.createElement("option");
|
||||
opt.value = q;
|
||||
opt.textContent = q;
|
||||
quant.appendChild(opt);
|
||||
});
|
||||
|
||||
contextChips.innerHTML = "";
|
||||
state.contexts.forEach((ctx) => {
|
||||
const btn = document.createElement("button");
|
||||
btn.type = "button";
|
||||
btn.className = "chip" + (ctx.key === state.filters.context ? " active" : "");
|
||||
btn.dataset.context = ctx.key;
|
||||
btn.textContent = ctx.label;
|
||||
contextChips.appendChild(btn);
|
||||
});
|
||||
|
||||
renderBackendList();
|
||||
setupSizeSlider();
|
||||
|
||||
search.addEventListener("input", (e) => {
|
||||
state.filters.search = (e.target.value || "").trim().toLowerCase();
|
||||
renderTables();
|
||||
});
|
||||
|
||||
quant.addEventListener("change", (e) => {
|
||||
state.filters.quant = e.target.value;
|
||||
renderTables();
|
||||
});
|
||||
|
||||
contextChips.addEventListener("click", (e) => {
|
||||
const btn = e.target.closest("button[data-context]");
|
||||
if (!btn) return;
|
||||
state.filters.context = btn.dataset.context;
|
||||
[...contextChips.querySelectorAll("button")].forEach((b) => b.classList.toggle("active", b === btn));
|
||||
renderTables();
|
||||
});
|
||||
|
||||
backendList.addEventListener("change", (e) => {
|
||||
const checkbox = e.target.closest("input[data-env]");
|
||||
if (!checkbox) return;
|
||||
const env = checkbox.dataset.env;
|
||||
if (checkbox.checked) {
|
||||
state.filters.backends.add(env);
|
||||
} else {
|
||||
state.filters.backends.delete(env);
|
||||
}
|
||||
renderTables();
|
||||
});
|
||||
|
||||
state.ui.backendAll.addEventListener("click", () => {
|
||||
state.filters.backends = new Set(state.envs);
|
||||
renderBackendList();
|
||||
renderTables();
|
||||
});
|
||||
|
||||
state.ui.backendNone.addEventListener("click", () => {
|
||||
state.filters.backends = new Set();
|
||||
renderBackendList();
|
||||
renderTables();
|
||||
});
|
||||
|
||||
sizeLo.addEventListener("input", () => updateSizeUI(true));
|
||||
sizeHi.addEventListener("input", () => updateSizeUI(true));
|
||||
|
||||
resetBtn.addEventListener("click", () => {
|
||||
state.filters.search = "";
|
||||
state.filters.quant = "";
|
||||
state.filters.context = state.contexts[0]?.key || DEFAULT_CTX;
|
||||
state.filters.backends = new Set(state.envs);
|
||||
search.value = "";
|
||||
quant.value = "";
|
||||
[...contextChips.querySelectorAll("button")].forEach((btn) =>
|
||||
btn.classList.toggle("active", btn.dataset.context === state.filters.context)
|
||||
);
|
||||
renderBackendList();
|
||||
setupSizeSlider();
|
||||
renderTables();
|
||||
});
|
||||
}
|
||||
|
||||
function renderBackendList() {
|
||||
const container = state.ui.backendList;
|
||||
container.innerHTML = "";
|
||||
state.backendOrder.forEach((env) => {
|
||||
const label = document.createElement("label");
|
||||
label.className = "backend-item";
|
||||
const checkbox = document.createElement("input");
|
||||
checkbox.type = "checkbox";
|
||||
checkbox.dataset.env = env;
|
||||
checkbox.checked = state.filters.backends.has(env);
|
||||
label.appendChild(checkbox);
|
||||
|
||||
const baseSpan = document.createElement("span");
|
||||
const { base, tags } = splitEnvName(env);
|
||||
baseSpan.textContent = base;
|
||||
label.appendChild(baseSpan);
|
||||
tags.forEach((tag) => {
|
||||
const pill = document.createElement("span");
|
||||
pill.className = "tag";
|
||||
pill.textContent = tag;
|
||||
label.appendChild(pill);
|
||||
});
|
||||
|
||||
container.appendChild(label);
|
||||
});
|
||||
}
|
||||
|
||||
function setupSizeSlider() {
|
||||
const { sizeLo, sizeHi } = state.ui;
|
||||
const minRaw = state.sizeStats.min === Infinity ? 0 : Math.floor(state.sizeStats.min || 0);
|
||||
const maxRaw = state.sizeStats.max === -Infinity ? 0 : Math.ceil(state.sizeStats.max || 0);
|
||||
const minB = Math.max(0, minRaw);
|
||||
const maxB = Math.max(minB, maxRaw);
|
||||
|
||||
[sizeLo, sizeHi].forEach((inp) => {
|
||||
inp.min = minB;
|
||||
inp.max = maxB;
|
||||
inp.step = 1;
|
||||
});
|
||||
|
||||
sizeLo.value = minB;
|
||||
sizeHi.value = maxB;
|
||||
sizeLo.style.zIndex = 2;
|
||||
sizeHi.style.zIndex = 1;
|
||||
updateSizeUI(false);
|
||||
}
|
||||
|
||||
function updateSizeUI(triggerRender) {
|
||||
const { sizeLo, sizeHi, sizeLoVal, sizeHiVal, sizeTrack } = state.ui;
|
||||
if (+sizeLo.value > +sizeHi.value) {
|
||||
if (document.activeElement === sizeLo) {
|
||||
sizeHi.value = sizeLo.value;
|
||||
} else {
|
||||
sizeLo.value = sizeHi.value;
|
||||
}
|
||||
}
|
||||
sizeLo.style.zIndex = +sizeLo.value >= +sizeHi.max - 1 ? 4 : 2;
|
||||
sizeHi.style.zIndex = +sizeHi.value <= +sizeLo.min + 1 ? 3 : 1;
|
||||
state.filters.sizeLo = +sizeLo.value;
|
||||
state.filters.sizeHi = +sizeHi.value;
|
||||
sizeLoVal.textContent = formatSizeLabel(state.filters.sizeLo);
|
||||
sizeHiVal.textContent = formatSizeLabel(state.filters.sizeHi);
|
||||
const range = (sizeHi.max - sizeLo.min) || 1;
|
||||
const minB = +sizeLo.min;
|
||||
const start = ((state.filters.sizeLo - minB) / range) * 100;
|
||||
const end = ((state.filters.sizeHi - minB) / range) * 100;
|
||||
sizeTrack.style.background = `linear-gradient(to right, #e3e7f1 ${start}%, var(--accent) ${start}%, var(--accent) ${end}%, #e3e7f1 ${end}%)`;
|
||||
if (triggerRender) renderTables();
|
||||
}
|
||||
|
||||
function renderTables() {
|
||||
const ctx = state.contextMap.get(state.filters.context);
|
||||
if (!ctx) {
|
||||
state.ui.tables.innerHTML = "<p>No data for this context.</p>";
|
||||
state.ui.stats.textContent = "0 rows";
|
||||
return;
|
||||
}
|
||||
|
||||
const backendList = state.backendOrder.filter((env) => state.filters.backends.has(env));
|
||||
const tests = [...ctx.tests.values()].sort((a, b) => a.name.localeCompare(b.name));
|
||||
const frag = document.createDocumentFragment();
|
||||
let totalRows = 0;
|
||||
|
||||
for (const test of tests) {
|
||||
const models = filterModels(test.models);
|
||||
if (!models.length) continue;
|
||||
totalRows += models.length;
|
||||
const block = document.createElement("div");
|
||||
block.className = "test-block";
|
||||
const heading = document.createElement("h2");
|
||||
heading.textContent = `${test.name.toUpperCase()} — tokens/second`;
|
||||
block.appendChild(heading);
|
||||
|
||||
const tableWrap = document.createElement("div");
|
||||
tableWrap.className = "table-wrap";
|
||||
const scroller = document.createElement("div");
|
||||
scroller.className = "table-scroll";
|
||||
|
||||
const modelsWithWinners = models.map((model) => {
|
||||
const winners = computeWinners(model, backendList);
|
||||
return { ...model, _cachedWinners: winners };
|
||||
});
|
||||
|
||||
const table = buildSingleTable(modelsWithWinners, backendList);
|
||||
scroller.appendChild(table);
|
||||
tableWrap.appendChild(scroller);
|
||||
block.appendChild(tableWrap);
|
||||
setupResizeOverlay(scroller, backendList, table);
|
||||
frag.appendChild(block);
|
||||
}
|
||||
|
||||
state.ui.tables.innerHTML = "";
|
||||
if (frag.childNodes.length) {
|
||||
state.ui.tables.appendChild(frag);
|
||||
} else {
|
||||
state.ui.tables.innerHTML = "<p>No models match the current filters.</p>";
|
||||
}
|
||||
state.ui.stats.textContent = `Showing ${totalRows.toLocaleString()} model rows across ${backendList.length} backends`;
|
||||
}
|
||||
|
||||
function buildSingleTable(models, backendList) {
|
||||
const table = document.createElement("table");
|
||||
const colgroup = document.createElement("colgroup");
|
||||
const colModel = document.createElement("col");
|
||||
colModel.style.width = `${MODEL_COL_WIDTH}px`;
|
||||
colgroup.appendChild(colModel);
|
||||
const colWinner = document.createElement("col");
|
||||
colWinner.style.width = `${WINNER_COL_WIDTH}px`;
|
||||
colgroup.appendChild(colWinner);
|
||||
backendList.forEach((env) => {
|
||||
const col = document.createElement("col");
|
||||
col.style.width = `${state.columnWidths[env] || 120}px`;
|
||||
col.dataset.env = env;
|
||||
colgroup.appendChild(col);
|
||||
});
|
||||
table.appendChild(colgroup);
|
||||
|
||||
const thead = document.createElement("thead");
|
||||
const headRow = document.createElement("tr");
|
||||
headRow.appendChild(makeHeaderCell("Model", "model"));
|
||||
headRow.appendChild(makeHeaderCell("Winner", "winner"));
|
||||
backendList.forEach((env) => {
|
||||
const th = makeHeaderCell(env, "backend-header");
|
||||
attachHeaderInteractions(th, env);
|
||||
headRow.appendChild(th);
|
||||
});
|
||||
thead.appendChild(headRow);
|
||||
table.appendChild(thead);
|
||||
|
||||
const tbody = document.createElement("tbody");
|
||||
models.forEach((model) => {
|
||||
const tr = document.createElement("tr");
|
||||
const tdModel = document.createElement("td");
|
||||
tdModel.className = "model";
|
||||
tdModel.innerHTML = `<div>${model.model}</div><div class="meta">${model.quant} · ${formatSize(model.sizeB)}</div>`;
|
||||
|
||||
const actionWrap = document.createElement("div");
|
||||
actionWrap.className = "row-actions";
|
||||
const btnDesc = document.createElement("button");
|
||||
btnDesc.type = "button";
|
||||
btnDesc.className = "row-action-btn";
|
||||
btnDesc.textContent = "Sort ↓";
|
||||
btnDesc.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
sortBackendsByModel(model, "desc");
|
||||
});
|
||||
const btnAsc = document.createElement("button");
|
||||
btnAsc.type = "button";
|
||||
btnAsc.className = "row-action-btn";
|
||||
btnAsc.textContent = "Sort ↑";
|
||||
btnAsc.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
sortBackendsByModel(model, "asc");
|
||||
});
|
||||
actionWrap.appendChild(btnDesc);
|
||||
actionWrap.appendChild(btnAsc);
|
||||
tdModel.appendChild(actionWrap);
|
||||
tr.appendChild(tdModel);
|
||||
|
||||
const tdWinner = document.createElement("td");
|
||||
tdWinner.className = "winner";
|
||||
if (model._cachedWinners.length) {
|
||||
const wrap = document.createElement("div");
|
||||
wrap.className = "winner-list";
|
||||
wrap.innerHTML = model._cachedWinners.map((w) => `<span class="winner-pill">${w}</span>`).join("");
|
||||
tdWinner.appendChild(wrap);
|
||||
} else {
|
||||
tdWinner.innerHTML = `<span class="cell-empty">—</span>`;
|
||||
}
|
||||
|
||||
tr.appendChild(tdWinner);
|
||||
|
||||
backendList.forEach((env) => {
|
||||
const td = document.createElement("td");
|
||||
td.className = "data-cell";
|
||||
td.dataset.env = env;
|
||||
const cell = model.backends[env];
|
||||
if (!cell) {
|
||||
td.innerHTML = `<span class="cell-empty">—</span>`;
|
||||
} else if (cell.error || cell.mean == null) {
|
||||
td.innerHTML = `<span class="cell-error">⚠ ${cell.error_type || "error"}</span>`;
|
||||
} else {
|
||||
const isBest = model._cachedWinners.includes(env);
|
||||
if (isBest) td.classList.add("best");
|
||||
td.innerHTML = `<div class="measure">${cell.mean.toFixed(2)}</div><div class="std">± ${cell.std?.toFixed(2) ?? "—"}</div>`;
|
||||
}
|
||||
tr.appendChild(td);
|
||||
});
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
table.appendChild(tbody);
|
||||
return table;
|
||||
}
|
||||
|
||||
function makeHeaderCell(label, extra = "") {
|
||||
const th = document.createElement("th");
|
||||
th.textContent = label;
|
||||
if (extra) th.className = extra;
|
||||
return th;
|
||||
}
|
||||
|
||||
function attachHeaderInteractions(th, env) {
|
||||
const width = state.columnWidths[env] || 120;
|
||||
th.style.width = `${width}px`;
|
||||
th.style.minWidth = `${width}px`;
|
||||
th.draggable = true;
|
||||
th.addEventListener("dragstart", (e) => {
|
||||
state.draggingEnv = env;
|
||||
th.classList.add("dragging");
|
||||
e.dataTransfer.effectAllowed = "move";
|
||||
});
|
||||
th.addEventListener("dragend", () => {
|
||||
state.draggingEnv = null;
|
||||
th.classList.remove("dragging");
|
||||
document.querySelectorAll("th.backend-header.drop-target").forEach((el) => el.classList.remove("drop-target"));
|
||||
});
|
||||
th.addEventListener("dragover", (e) => {
|
||||
if (!state.draggingEnv || state.draggingEnv === env) return;
|
||||
e.preventDefault();
|
||||
th.classList.add("drop-target");
|
||||
});
|
||||
th.addEventListener("dragleave", () => th.classList.remove("drop-target"));
|
||||
th.addEventListener("drop", (e) => {
|
||||
if (!state.draggingEnv || state.draggingEnv === env) return;
|
||||
e.preventDefault();
|
||||
moveBackend(state.draggingEnv, env);
|
||||
th.classList.remove("drop-target");
|
||||
});
|
||||
|
||||
const handle = document.createElement("span");
|
||||
handle.className = "resize-handle";
|
||||
handle.addEventListener("mousedown", (e) => startResize(e, env));
|
||||
th.appendChild(handle);
|
||||
}
|
||||
|
||||
function moveBackend(from, to) {
|
||||
const order = state.backendOrder;
|
||||
const fromIdx = order.indexOf(from);
|
||||
const toIdx = order.indexOf(to);
|
||||
if (fromIdx === -1 || toIdx === -1) return;
|
||||
const [col] = order.splice(fromIdx, 1);
|
||||
order.splice(toIdx, 0, col);
|
||||
renderBackendList();
|
||||
renderTables();
|
||||
}
|
||||
|
||||
function filterModels(modelsMap) {
|
||||
const models = [];
|
||||
for (const model of modelsMap.values()) {
|
||||
if (state.filters.search && !model.search_blob.includes(state.filters.search)) continue;
|
||||
if (state.filters.quant && model.quant !== state.filters.quant) continue;
|
||||
if (model.sizeB != null) {
|
||||
if (state.filters.sizeLo != null && model.sizeB < state.filters.sizeLo - 1e-6) continue;
|
||||
if (state.filters.sizeHi != null && model.sizeB > state.filters.sizeHi + 1e-6) continue;
|
||||
}
|
||||
models.push(model);
|
||||
}
|
||||
models.sort((a, b) => a.model.localeCompare(b.model));
|
||||
return models;
|
||||
}
|
||||
|
||||
function computeWinners(model, backends) {
|
||||
const values = [];
|
||||
backends.forEach((env) => {
|
||||
const entry = model.backends[env];
|
||||
if (entry && !entry.error && typeof entry.mean === "number") {
|
||||
values.push({
|
||||
env,
|
||||
mean: entry.mean,
|
||||
std: typeof entry.std === "number" ? entry.std : 0,
|
||||
});
|
||||
}
|
||||
});
|
||||
if (!values.length) return [];
|
||||
let best = values[0];
|
||||
for (const v of values) if (v.mean > best.mean) best = v;
|
||||
const winners = [];
|
||||
for (const v of values) {
|
||||
const pooled = Math.sqrt((best.std || 0) ** 2 + (v.std || 0) ** 2);
|
||||
const tol = Math.max(MIN_TOL, K_SIGMA * pooled);
|
||||
if ((best.mean - v.mean) <= tol) winners.push(v.env);
|
||||
}
|
||||
return winners;
|
||||
}
|
||||
|
||||
function normalizeTest(name) {
|
||||
if (!name) return null;
|
||||
return { key: name.toLowerCase(), original: name };
|
||||
}
|
||||
|
||||
function formatContextLabel(key, tokens) {
|
||||
if (key === DEFAULT_CTX) return "Default window";
|
||||
if (tokens) return `ctx ${tokens.toLocaleString()}`;
|
||||
return key;
|
||||
}
|
||||
|
||||
function formatSize(size) {
|
||||
if (size == null) return "—";
|
||||
return `${Number(size).toFixed(1)}B`;
|
||||
}
|
||||
|
||||
function formatSizeLabel(size) {
|
||||
if (size >= 1000) return `${(size / 1000).toFixed(1)}kB`;
|
||||
return `${Math.round(size)}B`;
|
||||
}
|
||||
|
||||
function sortBackendsByModel(model, direction) {
|
||||
const dir = direction === "asc" ? 1 : -1;
|
||||
const order = [...state.backendOrder].sort((a, b) => {
|
||||
const va = backendValue(model.backends[a], direction);
|
||||
const vb = backendValue(model.backends[b], direction);
|
||||
if (va === vb) return a.localeCompare(b);
|
||||
return (va - vb) * dir;
|
||||
});
|
||||
state.backendOrder = order;
|
||||
renderBackendList();
|
||||
renderTables();
|
||||
}
|
||||
|
||||
function backendValue(entry, direction) {
|
||||
if (!entry || entry.error || typeof entry.mean !== "number") {
|
||||
return direction === "asc" ? Number.POSITIVE_INFINITY : Number.NEGATIVE_INFINITY;
|
||||
}
|
||||
return entry.mean;
|
||||
}
|
||||
|
||||
function splitEnvName(env) {
|
||||
const parts = env.split(/-(?=rocwmma|improved|hblt0)/g);
|
||||
if (parts.length === 1) return { base: env, tags: [] };
|
||||
const base = parts[0];
|
||||
const tags = env
|
||||
.slice(base.length)
|
||||
.split("-")
|
||||
.filter(Boolean)
|
||||
.map((t) => t.toUpperCase());
|
||||
return { base, tags };
|
||||
}
|
||||
|
||||
function startResize(event, env) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
const column = state.columnWidths[env] || 120;
|
||||
const startX = event.clientX;
|
||||
const shellRect = state.ui.tables.getBoundingClientRect();
|
||||
const guide = document.createElement("div");
|
||||
guide.className = "resize-line";
|
||||
guide.style.position = "fixed";
|
||||
guide.style.top = `${shellRect.top}px`;
|
||||
guide.style.bottom = `${window.innerHeight - shellRect.bottom}px`;
|
||||
guide.style.left = `${startX}px`;
|
||||
guide.style.width = "2px";
|
||||
guide.style.background = "var(--accent)";
|
||||
guide.style.zIndex = "10";
|
||||
document.body.appendChild(guide);
|
||||
let nextWidth = column;
|
||||
|
||||
const onMove = (e) => {
|
||||
const delta = e.clientX - startX;
|
||||
nextWidth = Math.max(80, column + delta);
|
||||
guide.style.left = `${e.clientX}px`;
|
||||
};
|
||||
|
||||
const onUp = () => {
|
||||
document.removeEventListener("mousemove", onMove);
|
||||
document.removeEventListener("mouseup", onUp);
|
||||
guide.remove();
|
||||
state.columnWidths[env] = nextWidth;
|
||||
renderTables();
|
||||
};
|
||||
|
||||
document.addEventListener("mousemove", onMove);
|
||||
document.addEventListener("mouseup", onUp);
|
||||
}
|
||||
|
||||
function setupResizeOverlay(tableWrap, backendList, table) {
|
||||
let overlay = tableWrap.querySelector(".resize-overlay");
|
||||
if (!overlay) {
|
||||
overlay = document.createElement("div");
|
||||
overlay.className = "resize-overlay";
|
||||
tableWrap.appendChild(overlay);
|
||||
} else {
|
||||
overlay.innerHTML = "";
|
||||
}
|
||||
|
||||
overlay.style.width = `${tableWrap.clientWidth}px`;
|
||||
overlay.style.height = `${table.offsetHeight}px`;
|
||||
|
||||
const bars = [];
|
||||
let offset = MODEL_COL_WIDTH + WINNER_COL_WIDTH;
|
||||
backendList.forEach((env) => {
|
||||
const width = state.columnWidths[env] || 120;
|
||||
const bar = document.createElement("div");
|
||||
bar.className = "resize-bar";
|
||||
bar.dataset.env = env;
|
||||
bar.addEventListener("mousedown", (e) => startResize(e, env));
|
||||
overlay.appendChild(bar);
|
||||
bars.push({ bar, offset, width, env });
|
||||
offset += width;
|
||||
});
|
||||
|
||||
const positionBars = () => {
|
||||
bars.forEach(({ bar, offset, width }) => {
|
||||
const left = offset + width - 3 - tableWrap.scrollLeft;
|
||||
bar.style.left = `${left}px`;
|
||||
});
|
||||
};
|
||||
positionBars();
|
||||
|
||||
if (tableWrap._overlayScroll) {
|
||||
tableWrap.removeEventListener("scroll", tableWrap._overlayScroll);
|
||||
}
|
||||
const onScroll = () => positionBars();
|
||||
tableWrap.addEventListener("scroll", onScroll);
|
||||
tableWrap._overlayScroll = onScroll;
|
||||
|
||||
if (tableWrap._overlayResize) {
|
||||
tableWrap._overlayResize.disconnect();
|
||||
}
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
overlay.style.width = `${tableWrap.clientWidth}px`;
|
||||
overlay.style.height = `${table.offsetHeight}px`;
|
||||
positionBars();
|
||||
});
|
||||
resizeObserver.observe(tableWrap);
|
||||
tableWrap._overlayResize = resizeObserver;
|
||||
}
|
||||
+45
-752
@@ -2,780 +2,73 @@
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<title>AMD Ryzen AI MAX+ 395 "Strix Halo" — Llama.cpp Backend Performance Comparison</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #ffffff;
|
||||
--ink: #000000;
|
||||
--muted: #555555;
|
||||
--accent: #0645ad;
|
||||
--card: #f9f9f9;
|
||||
--border: #cccccc;
|
||||
--sticky: #f4f4f4;
|
||||
--pill: #e0e0e0;
|
||||
--roc: #d3caff;
|
||||
--roc-text: #000000;
|
||||
--warn: #d88f00;
|
||||
--bad: #d32f2f;
|
||||
--model-w: 300px;
|
||||
--winner-w: 220px;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
background: var(--bg);
|
||||
color: var(--ink);
|
||||
font: 15px/1.5 system-ui, -apple-system, Segoe UI, Roboto, Inter;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--accent);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
header {
|
||||
padding: 14px 20px 0;
|
||||
}
|
||||
|
||||
header h1 {
|
||||
margin: 0 0 6px;
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
header p {
|
||||
margin: 4px 0 12px;
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 160px 260px 180px;
|
||||
gap: 10px;
|
||||
align-items: end;
|
||||
padding: 10px 20px;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
margin: 0 0 6px;
|
||||
}
|
||||
|
||||
input[type="text"],
|
||||
select,
|
||||
input[type="number"] {
|
||||
width: 100%;
|
||||
background: #fff;
|
||||
color: var(--ink);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
padding: 8px 10px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.slider {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 90px;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
|
||||
.range-wrap {
|
||||
position: relative;
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.range-wrap input[type="range"] {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
background: transparent;
|
||||
pointer-events: none;
|
||||
appearance: none;
|
||||
}
|
||||
|
||||
.range-wrap input[type="range"]::-webkit-slider-thumb {
|
||||
pointer-events: auto;
|
||||
appearance: none;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
border-radius: 50%;
|
||||
background: var(--accent);
|
||||
border: 2px solid #fff;
|
||||
box-shadow: 0 0 2px rgba(0, 0, 0, 0.3);
|
||||
cursor: pointer;
|
||||
transition: background 0.2s, transform 0.2s;
|
||||
}
|
||||
|
||||
.range-wrap input[type="range"]::-webkit-slider-thumb:hover {
|
||||
background: #0451cc;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.range-wrap input[type="range"]::-moz-range-thumb {
|
||||
pointer-events: auto;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
border-radius: 50%;
|
||||
background: var(--accent);
|
||||
border: 2px solid #fff;
|
||||
box-shadow: 0 0 2px rgba(0, 0, 0, 0.3);
|
||||
cursor: pointer;
|
||||
transition: background 0.2s, transform 0.2s;
|
||||
}
|
||||
|
||||
.range-wrap input[type="range"]::-moz-range-thumb:hover {
|
||||
background: #0451cc;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.range-track {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
height: 6px;
|
||||
width: 100%;
|
||||
transform: translateY(-50%);
|
||||
border-radius: 999px;
|
||||
background: #e5e5e5;
|
||||
}
|
||||
|
||||
.range-values {
|
||||
margin-top: 4px;
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
||||
input[type="range"] {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.ticks {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
color: var(--muted);
|
||||
font-size: 11px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.cols {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
padding: 0 20px 8px;
|
||||
}
|
||||
|
||||
.colbox {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
padding: 8px 10px;
|
||||
min-width: 220px;
|
||||
background: var(--card);
|
||||
}
|
||||
|
||||
.colbox label {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.colbox input {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
.badge {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
border-radius: 999px;
|
||||
background: var(--pill);
|
||||
font-size: 11px;
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
.roc {
|
||||
background: var(--roc);
|
||||
color: var(--roc-text);
|
||||
border: 1px solid #b19cff55;
|
||||
}
|
||||
|
||||
.faall {
|
||||
background: #cfe9ff;
|
||||
/* light blue chip */
|
||||
color: #000000;
|
||||
border: 1px solid #9bc9ff55;
|
||||
}
|
||||
|
||||
.meta {
|
||||
padding: 0 20px 14px;
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.section {
|
||||
padding: 6px 20px 0;
|
||||
}
|
||||
|
||||
.section h2 {
|
||||
margin: 6px 0 8px;
|
||||
font-size: 16px;
|
||||
color: var(--ink);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.tablewrap {
|
||||
margin: 0 0 16px;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.scroller {
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
.scroller::-webkit-scrollbar {
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
.scroller::-webkit-scrollbar-thumb {
|
||||
background: var(--border);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.scroller-top {
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
height: 12px;
|
||||
/* slim */
|
||||
margin: 0 0 6px;
|
||||
/* a little gap above the table */
|
||||
}
|
||||
|
||||
.scroller-top .scroller-spacer {
|
||||
height: 1px;
|
||||
/* tiny content so the bar renders */
|
||||
}
|
||||
|
||||
|
||||
table {
|
||||
width: max-content;
|
||||
min-width: 100vw;
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
}
|
||||
|
||||
thead th {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background: var(--card);
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
padding: 10px 12px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
th {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
td {
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.col-model {
|
||||
position: sticky;
|
||||
left: 0;
|
||||
background: var(--sticky);
|
||||
z-index: 3;
|
||||
max-width: var(--model-w);
|
||||
width: var(--model-w);
|
||||
}
|
||||
|
||||
.col-winner {
|
||||
position: sticky;
|
||||
left: calc(var(--model-w));
|
||||
background: var(--sticky);
|
||||
z-index: 3;
|
||||
max-width: var(--winner-w);
|
||||
width: var(--winner-w);
|
||||
}
|
||||
|
||||
.model-title {
|
||||
font-weight: 600;
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.model-meta {
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.cell {
|
||||
min-width: 120px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.tps {
|
||||
display: block;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.std {
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.warn {
|
||||
color: var(--warn);
|
||||
}
|
||||
|
||||
.err {
|
||||
color: var(--bad);
|
||||
}
|
||||
|
||||
.winner-pill {
|
||||
display: inline-block;
|
||||
background: var(--pill);
|
||||
border-radius: 999px;
|
||||
padding: 2px 8px;
|
||||
margin: 4px 4px 4px 0;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.winner-wrap {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.env-sub {
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>AMD Strix Halo — Backend Benchmarks (Grid View)</title>
|
||||
<link rel="stylesheet" href="assets/index2.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<header>
|
||||
<h1>AMD Ryzen AI MAX+ 395 "Strix Halo" — Llama.cpp Backend Performance Comparison</h1>
|
||||
<p class="muted">
|
||||
Compare model throughput across backends (pp512 & tg128).
|
||||
Repo: <a href="https://github.com/kyuz0/amd-strix-halo-toolboxes" target="_blank"
|
||||
rel="noreferrer">kyuz0/amd-strix-halo-toolboxes</a>
|
||||
</p>
|
||||
<p class="muted">Platform: Framework Desktop, 128GB Unified RAM (accelerator-performance tuned profile)</p>
|
||||
<p id="meta-line">Loading meta…</p>
|
||||
<h1>AMD Ryzen AI MAX+ 395 “Strix Halo” — Benchmark Grid</h1>
|
||||
<p>Framework Desktop · AMD Ryzen AI MAX 395+ · 128GB unified RAM</p>
|
||||
<p>Fedora 42 · Linux 6.18.0-0.rc5.243.vanilla.fc42.x86_64 · llama.cpp build 1c398dc9e (7034)</p>
|
||||
<p>Benchmarks captured 14 Nov 2025 · Repo: <a href="https://github.com/kyuz0/amd-strix-halo-toolboxes"
|
||||
target="_blank" rel="noreferrer">kyuz0/amd-strix-halo-toolboxes</a></p>
|
||||
</header>
|
||||
|
||||
|
||||
<div class="controls">
|
||||
<div>
|
||||
<label for="search">Search model</label>
|
||||
<input id="search" type="text" placeholder="e.g. llama, qwen, 30B, Q8_K…">
|
||||
<section class="controls">
|
||||
<div class="control">
|
||||
<label for="filter-search">Search models</label>
|
||||
<input id="filter-search" type="text" placeholder="e.g. llama, qwen, 30B…">
|
||||
</div>
|
||||
<div>
|
||||
<label for="quant">Quant</label>
|
||||
<select id="quant">
|
||||
<div class="control">
|
||||
<label for="filter-quant">Quant</label>
|
||||
<select id="filter-quant">
|
||||
<option value="">Any</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label>Model params</label>
|
||||
|
||||
<div class="range-wrap" id="sizeRange">
|
||||
<div class="control grow slider-block">
|
||||
<label>Context windows</label>
|
||||
<div id="context-chips" class="chip-row tight"></div>
|
||||
</div>
|
||||
<div class="control grow slider-block">
|
||||
<label>Model params (B)</label>
|
||||
<div class="range-wrap">
|
||||
<input type="range" id="sizeLo" step="1">
|
||||
<input type="range" id="sizeHi" step="1">
|
||||
<div class="range-track" id="sizeTrack"></div>
|
||||
</div>
|
||||
|
||||
<div class="ticks">
|
||||
<span>4B</span><span>50B</span><span>96B</span><span>143B</span><span>189B</span><span>235B</span>
|
||||
</div>
|
||||
|
||||
<div class="range-values">
|
||||
<span id="sizeLoVal">4B</span> – <span id="sizeHiVal">235B</span>
|
||||
<span id="sizeLoVal">0B</span> – <span id="sizeHiVal">0B</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="faMode">Flash Attention</label>
|
||||
<select id="faMode">
|
||||
<option value="off">FA off</option>
|
||||
<option value="on" selected>FA on</option> <!-- default ON -->
|
||||
<option value="both">Both</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="cols" id="columns"></div>
|
||||
<div class="meta">Winner = every selected backend within the best’s uncertainty range, combining ± errors from both
|
||||
results.</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>Prompt Processing (pp512) — tokens/second</h2>
|
||||
</div>
|
||||
<section class="tablewrap">
|
||||
<div class="scroller scroller-top" data-for="pp">
|
||||
<div class="scroller-spacer"></div>
|
||||
</div>
|
||||
<div class="scroller" id="scroller-pp">
|
||||
<table id="tbl-pp">
|
||||
<thead>
|
||||
<tr></tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
|
||||
<div class="section">
|
||||
<h2>Text Generation (tg128) — tokens/second</h2>
|
||||
</div>
|
||||
<section class="tablewrap">
|
||||
<div class="scroller scroller-top" data-for="tg">
|
||||
<div class="scroller-spacer"></div>
|
||||
<section class="panel compact">
|
||||
<div class="panel-split">
|
||||
<div class="backend-header">
|
||||
<div class="backend-label">
|
||||
<label>Backends</label>
|
||||
<div class="backend-actions">
|
||||
<button type="button" id="backend-all" class="chip small">All</button>
|
||||
<button type="button" id="backend-none" class="chip small">None</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="backend-list" class="backend-list"></div>
|
||||
</div>
|
||||
<div class="stats-box">
|
||||
<div class="stat-line" id="stats-line">Loading…</div>
|
||||
<button id="reset-layout" type="button" class="chip small">Reset filters</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="scroller" id="scroller-tg">
|
||||
<table id="tbl-tg">
|
||||
<thead>
|
||||
<tr></tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
|
||||
<script>
|
||||
(async function () {
|
||||
const K_SIGMA = 1.0;
|
||||
const MIN_TOL = 0.25;
|
||||
|
||||
const res = await fetch('results.json');
|
||||
const data = await res.json();
|
||||
const allRuns = data.runs || [];
|
||||
const perfRuns = allRuns.filter(r => r.test === 'pp512' || r.test === 'tg128');
|
||||
|
||||
const metaLine = document.getElementById('meta-line');
|
||||
const search = document.getElementById('search');
|
||||
const quantSel = document.getElementById('quant');
|
||||
const colPicker = document.getElementById('columns');
|
||||
const faMode = document.getElementById('faMode');
|
||||
|
||||
// NEW dual slider elements
|
||||
const sizeLo = document.getElementById('sizeLo');
|
||||
const sizeHi = document.getElementById('sizeHi');
|
||||
const sizeTrack = document.getElementById('sizeTrack');
|
||||
const sizeLoVal = document.getElementById('sizeLoVal');
|
||||
const sizeHiVal = document.getElementById('sizeHiVal');
|
||||
|
||||
metaLine.textContent = `${data.meta?.os_kernel || ''} — llama.cpp ${(data.meta?.llamacpp_builds || []).map(b => b.hash).join(', ') || 'unknown'} — generated ${data.meta?.generated_at || ''}`;
|
||||
|
||||
const models = [...new Set(perfRuns.map(r => r.model_clean || r.model))].sort((a, b) => a.localeCompare(b));
|
||||
const byModel = {};
|
||||
for (const name of models) { byModel[name] = { pp512: {}, tg128: {}, quant: null, sizeB: null }; }
|
||||
for (const r of perfRuns) {
|
||||
const name = r.model_clean || r.model;
|
||||
const cfg = `${r.env}|fa=${r.fa ? 1 : 0}`;
|
||||
byModel[name][r.test][cfg] = { mean: r.tps_mean, std: r.tps_std, err: r.error, et: r.error_type };
|
||||
if (!byModel[name].quant && r.quant) byModel[name].quant = r.quant;
|
||||
if (byModel[name].sizeB == null) byModel[name].sizeB = (r.name_params_b ?? r.params_b);
|
||||
}
|
||||
for (const r of allRuns) {
|
||||
if (r.test || !r.error) continue;
|
||||
const name = r.model_clean || r.model; if (!byModel[name]) continue;
|
||||
const cfg = `${r.env}|fa=${r.fa ? 1 : 0}`;
|
||||
for (const t of ['pp512', 'tg128']) {
|
||||
if (!byModel[name][t][cfg]) byModel[name][t][cfg] = { mean: null, std: null, err: true, et: r.error_type || 'error' };
|
||||
}
|
||||
}
|
||||
|
||||
const envs = [...new Set(allRuns.map(r => r.env))].sort();
|
||||
function envBox(env) {
|
||||
const roc = env.includes('rocwmma');
|
||||
const hipBLASTt_off = env.includes('hblt0');
|
||||
const fa_all_quants = env.includes('fa_all_quants');
|
||||
const id = `env_${env.replace(/[^a-z0-9_-]/gi, '_')}`;
|
||||
return `
|
||||
<div class="colbox">
|
||||
<label for="${id}">
|
||||
<input id="${id}" type="checkbox" data-env="${env}"
|
||||
${/(vulkan_amdvlk|vulkan_radv|rocm6_4_4-rocwmma|rocm7_rc-rocwmma)(?![-\w])/.test(env.trim()) ? 'checked' : ''}>
|
||||
<span>
|
||||
<strong>${env}</strong>
|
||||
${roc ? '<span class="badge roc">rocWMMA</span>' : ''}
|
||||
${fa_all_quants ? '<span class="badge faall">FA all quants</span>' : ''}
|
||||
${hipBLASTt_off ? '<span class="badge roc">hipBLASTt OFF</span>' : ''}
|
||||
</span>
|
||||
</label>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
colPicker.innerHTML = envs.map(envBox).join('');
|
||||
const selectedEnvs = () => [...colPicker.querySelectorAll('input[type="checkbox"]')].filter(i => i.checked).map(i => i.dataset.env);
|
||||
|
||||
const quants = [...new Set(Object.values(byModel).map(m => (m.quant || 'Unknown').toUpperCase()))].sort();
|
||||
quants.forEach(q => { const o = document.createElement('option'); o.value = (q === 'UNKNOWN' ? '' : q); o.textContent = q; quantSel.appendChild(o); });
|
||||
|
||||
// --- Dual range slider setup ---
|
||||
const sizes = Object.values(byModel)
|
||||
.map(m => m.sizeB)
|
||||
.filter(v => typeof v === 'number')
|
||||
.sort((a, b) => a - b);
|
||||
|
||||
// force clean integer min/max
|
||||
const MIN_B = Math.floor(sizes[0] ?? 0);
|
||||
const MAX_B = Math.ceil(sizes[sizes.length - 1] ?? 100);
|
||||
|
||||
[sizeLo, sizeHi].forEach(inp => {
|
||||
inp.min = MIN_B;
|
||||
inp.max = MAX_B;
|
||||
inp.step = 1; // integers only
|
||||
});
|
||||
|
||||
sizeLo.value = MIN_B;
|
||||
sizeHi.value = MAX_B;
|
||||
|
||||
const filters = { sizeLo: MIN_B, sizeHi: MAX_B };
|
||||
|
||||
function fmtB(n) {
|
||||
return `${Number(n).toFixed(0)}B`;
|
||||
}
|
||||
|
||||
function clampRange() {
|
||||
if (+sizeLo.value > +sizeHi.value) {
|
||||
if (document.activeElement === sizeLo) {
|
||||
sizeHi.value = sizeLo.value;
|
||||
} else {
|
||||
sizeLo.value = sizeHi.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function paintTrack() {
|
||||
const a = (+sizeLo.value - MIN_B) / (MAX_B - MIN_B) * 100;
|
||||
const b = (+sizeHi.value - MIN_B) / (MAX_B - MIN_B) * 100;
|
||||
sizeTrack.style.background = `
|
||||
linear-gradient(to right,
|
||||
#e5e5e5 ${a}%,
|
||||
var(--accent) ${a}%,
|
||||
var(--accent) ${b}%,
|
||||
#e5e5e5 ${b}%)`;
|
||||
}
|
||||
|
||||
function updateSizeUI(pushRender = true) {
|
||||
clampRange();
|
||||
|
||||
// Fix rounding: snap to min/max if slider is at extremes
|
||||
if (Math.abs(sizeLo.value - MIN_B) < 0.0001) sizeLo.value = MIN_B;
|
||||
if (Math.abs(sizeHi.value - MAX_B) < 0.0001) sizeHi.value = MAX_B;
|
||||
|
||||
sizeLoVal.textContent = fmtB(sizeLo.value);
|
||||
sizeHiVal.textContent = fmtB(sizeHi.value);
|
||||
filters.sizeLo = +sizeLo.value;
|
||||
filters.sizeHi = +sizeHi.value;
|
||||
|
||||
paintTrack();
|
||||
if (pushRender) render();
|
||||
}
|
||||
|
||||
sizeLo.addEventListener('input', () => updateSizeUI(true));
|
||||
sizeHi.addEventListener('input', () => updateSizeUI(true));
|
||||
updateSizeUI(false);
|
||||
|
||||
function setupScrollSync(prefix) {
|
||||
const top = document.querySelector(`.scroller-top[data-for="${prefix}"]`);
|
||||
const bottom = document.getElementById(`scroller-${prefix}`);
|
||||
const spacer = top.querySelector('.scroller-spacer');
|
||||
if (!top || !bottom || !spacer) return () => { };
|
||||
|
||||
function syncWidth() {
|
||||
|
||||
// Match the table’s full scrollable width
|
||||
spacer.style.width = bottom.scrollWidth + 'px';
|
||||
// Keep positions aligned after rerender
|
||||
top.scrollLeft = bottom.scrollLeft;
|
||||
}
|
||||
|
||||
// Two-way sync
|
||||
let lock = false;
|
||||
top.addEventListener('scroll', () => {
|
||||
if (lock) return; lock = true;
|
||||
bottom.scrollLeft = top.scrollLeft;
|
||||
lock = false;
|
||||
});
|
||||
bottom.addEventListener('scroll', () => {
|
||||
if (lock) return; lock = true;
|
||||
top.scrollLeft = bottom.scrollLeft;
|
||||
lock = false;
|
||||
});
|
||||
|
||||
// Resize/refresh on content changes
|
||||
const ro = new ResizeObserver(syncWidth);
|
||||
ro.observe(bottom);
|
||||
syncWidth();
|
||||
|
||||
// Return a refresher for manual calls after rerender
|
||||
return syncWidth;
|
||||
}
|
||||
|
||||
// --- Filters ---
|
||||
function matchesFilters(name) {
|
||||
const q = (search.value || '').toLowerCase();
|
||||
if (q && !name.toLowerCase().includes(q)) return false;
|
||||
|
||||
const info = byModel[name];
|
||||
if (quantSel.value && (info.quant || '').toUpperCase() !== quantSel.value.toUpperCase()) return false;
|
||||
|
||||
if (info.sizeB != null && (info.sizeB < filters.sizeLo - 1e-9 || info.sizeB > filters.sizeHi + 1e-9)) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
function cleanTitle(name, quant) {
|
||||
const qTokens = ['BF16', 'F16', 'F32', 'FP16', 'MXFP2', 'MXFP4', 'MXFP8'];
|
||||
if (quant) qTokens.push(quant.toUpperCase());
|
||||
let t = name;
|
||||
t = t.replace(/[-_](Q\d+(?:_[A-Z0-9]+)*)/gi, '');
|
||||
t = t.replace(new RegExp(`[-_]?(${qTokens.join('|')})\\b`, 'gi'), '');
|
||||
t = t.replace(/-UD\b/gi, '');
|
||||
t = t.replace(/[-_]{2,}/g, '-')
|
||||
.replace(/\s{2,}/g, ' ')
|
||||
.replace(/[-_]+$/, '')
|
||||
.trim();
|
||||
return t;
|
||||
}
|
||||
|
||||
function buildColDefs(envSel, mode) {
|
||||
const cols = [];
|
||||
for (const e of envSel) {
|
||||
if (mode === 'both') {
|
||||
cols.push({ env: e, key: `${e}|fa=0`, label: `${e}<div class="env-sub">FA off</div>` });
|
||||
cols.push({ env: e, key: `${e}|fa=1`, label: `${e}<div class="env-sub">FA on</div>` });
|
||||
} else {
|
||||
const k = `${e}|fa=${mode === 'on' ? 1 : 0}`;
|
||||
cols.push({ env: e, key: k, label: `${e}<div class="env-sub">FA ${mode === 'on' ? 'on' : 'off'}</div>` });
|
||||
}
|
||||
}
|
||||
return cols;
|
||||
}
|
||||
|
||||
function headerHTML(test, colDefs) {
|
||||
return `<th class="col-model">Model</th><th class="col-winner">Winner (${test})</th>` +
|
||||
colDefs.map(c => `<th>${c.label}</th>`).join('');
|
||||
}
|
||||
|
||||
function cellHTML(name, test, key) {
|
||||
const v = byModel[name][test][key];
|
||||
if (!v) return '<div class="cell"><span class="muted">—</span></div>';
|
||||
if (v.err || v.mean == null) return `<div class="cell"><span class="warn">⚠ ${v.et || 'error'}</span></div>`;
|
||||
return `<div class="cell"><span class="tps">${v.mean.toFixed(2)}</span><span class="std">± ${v.std?.toFixed(2) ?? '—'}</span></div>`;
|
||||
}
|
||||
|
||||
function winnersHTML(name, test, colDefs) {
|
||||
const vals = [];
|
||||
for (const c of colDefs) {
|
||||
const v = byModel[name][test][c.key];
|
||||
if (v && !v.err && typeof v.mean === 'number') {
|
||||
vals.push({
|
||||
env: c.env,
|
||||
fa: c.key.endsWith('|fa=1') ? 'on' : 'off',
|
||||
mean: v.mean,
|
||||
std: (typeof v.std === 'number' ? v.std : 0)
|
||||
});
|
||||
}
|
||||
}
|
||||
if (!vals.length) return '<span class="muted">—</span>';
|
||||
let best = vals[0];
|
||||
for (const v of vals) if (v.mean > best.mean) best = v;
|
||||
const winners = [];
|
||||
for (const v of vals) {
|
||||
const pooled = Math.sqrt((best.std || 0) ** 2 + (v.std || 0) ** 2);
|
||||
const tol = Math.max(MIN_TOL, K_SIGMA * pooled);
|
||||
if ((best.mean - v.mean) <= tol) winners.push(v);
|
||||
}
|
||||
if (faMode.value !== 'both') {
|
||||
const envs = [...new Set(winners.map(w => w.env))];
|
||||
return `<div class="winner-wrap">${envs.map(env =>
|
||||
`<span class="winner-pill">🏆 ${env}</span>`).join('')}</div>`;
|
||||
} else {
|
||||
const byEnv = {};
|
||||
for (const w of winners) (byEnv[w.env] ||= new Set()).add(w.fa);
|
||||
return `<div class="winner-wrap">${Object.entries(byEnv).map(([env, fas]) => {
|
||||
const list = [...fas].sort().map(x => `FA ${x}`).join(' & ');
|
||||
return `<span class="winner-pill">🏆 ${env} (${list})</span>`;
|
||||
}).join('')
|
||||
}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
function renderTable(prefix, test) {
|
||||
const envSel = selectedEnvs(); const mode = faMode.value;
|
||||
const colDefs = buildColDefs(envSel, mode);
|
||||
const h = document.querySelector(`#tbl-${prefix} thead tr`) || document.getElementById(`${prefix}-h`);
|
||||
if (!h) return; // nothing to render into; bail safely
|
||||
h.innerHTML = headerHTML(test, colDefs);
|
||||
const tbody = document.querySelector(`#tbl-${prefix} tbody`);
|
||||
tbody.innerHTML = '';
|
||||
for (const name of models.filter(matchesFilters)) {
|
||||
const tr = document.createElement('tr');
|
||||
const tdM = document.createElement('td');
|
||||
tdM.className = 'col-model';
|
||||
const info = byModel[name];
|
||||
tdM.innerHTML = `<div class="model-title">${cleanTitle(name, info.quant)}</div><div class="model-meta">${info.quant || '—'} · ${(info.sizeB ?? '—')}B</div>`;
|
||||
tr.appendChild(tdM);
|
||||
const tdW = document.createElement('td');
|
||||
tdW.className = 'col-winner';
|
||||
tdW.innerHTML = winnersHTML(name, test, colDefs);
|
||||
tr.appendChild(tdW);
|
||||
for (const c of colDefs) {
|
||||
const td = document.createElement('td');
|
||||
td.innerHTML = cellHTML(name, test, c.key);
|
||||
tr.appendChild(td);
|
||||
}
|
||||
tbody.appendChild(tr);
|
||||
}
|
||||
}
|
||||
let refreshScrollPP = null, refreshScrollTG = null;
|
||||
function render() {
|
||||
renderTable('pp', 'pp512');
|
||||
renderTable('tg', 'tg128');
|
||||
// Update top scrollbar widths after content changes
|
||||
if (typeof refreshScrollPP === 'function') refreshScrollPP();
|
||||
if (typeof refreshScrollTG === 'function') refreshScrollTG();
|
||||
}
|
||||
|
||||
[search, quantSel, faMode].forEach(el => el.addEventListener('input', render));
|
||||
colPicker.addEventListener('change', render);
|
||||
render();
|
||||
|
||||
refreshScrollPP = setupScrollSync('pp');
|
||||
refreshScrollTG = setupScrollSync('tg');
|
||||
})();
|
||||
</script>
|
||||
<section class="panel compact" id="tables-panel">
|
||||
<div id="tables"></div>
|
||||
</section>
|
||||
|
||||
<script src="assets/index2.js" type="module"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
|
||||
+26751
-12940
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user