Add shareable URL state, speedup column and family-scoped metric legend
#7
by JonnaMat - opened
- app.js +52 -11
- config.json +1 -0
- embed.js +5 -2
- index.html +2 -0
- speedup.js +127 -0
- style.css +21 -0
- urlstate.js +213 -0
app.js
CHANGED
|
@@ -371,11 +371,29 @@ function createFilterGroup(label, id) {
|
|
| 371 |
return div;
|
| 372 |
}
|
| 373 |
|
| 374 |
-
// Metric legend
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 375 |
const legendGrid = document.getElementById("legend-grid");
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 379 |
|
| 380 |
// βββ State ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 381 |
|
|
@@ -431,9 +449,13 @@ function updateDependentFilters(resetDefaults) {
|
|
| 431 |
const strVals = vals.map(String);
|
| 432 |
const needsReset = resetDefaults || !strVals.includes(String(filters[f.column]));
|
| 433 |
if (needsReset) {
|
|
|
|
|
|
|
| 434 |
// Prefer family-specific default_device for the device/group_by filter
|
| 435 |
const defaultVal = f.column === GROUP_BY && familyCfg.default_device;
|
| 436 |
-
if (
|
|
|
|
|
|
|
| 437 |
filters[f.column] = defaultVal;
|
| 438 |
} else {
|
| 439 |
filters[f.column] = vals[0] ?? "";
|
|
@@ -529,9 +551,7 @@ function buildChart(filtered) {
|
|
| 529 |
headerRight.className = "chart-header-right";
|
| 530 |
|
| 531 |
// Only show metric buttons for metrics that have non-zero data
|
| 532 |
-
const chartVisibleMetrics = config.metrics.filter(
|
| 533 |
-
gRows.some(r => r[m.column] !== undefined && r[m.column] !== null && r[m.column] !== 0 && r[m.column] !== "N/A")
|
| 534 |
-
);
|
| 535 |
if (chartVisibleMetrics.length > 1) {
|
| 536 |
const metricEl = metricGroup.querySelector(".btn-group");
|
| 537 |
renderBtnGroup(metricEl,
|
|
@@ -683,9 +703,7 @@ function buildTables(filtered, chartsShown) {
|
|
| 683 |
});
|
| 684 |
|
| 685 |
// Hide metric columns where every value in the filtered data is zero, null, or N/A
|
| 686 |
-
const visibleMetrics = config.metrics.filter(
|
| 687 |
-
filtered.some(r => r[m.column] !== undefined && r[m.column] !== null && r[m.column] !== 0 && r[m.column] !== "N/A")
|
| 688 |
-
);
|
| 689 |
|
| 690 |
// Build column list: Model + visible display cols + metrics
|
| 691 |
const colDefs = [
|
|
@@ -743,6 +761,9 @@ function buildTables(filtered, chartsShown) {
|
|
| 743 |
return metricHib ? va - vb : vb - va;
|
| 744 |
});
|
| 745 |
|
|
|
|
|
|
|
|
|
|
| 746 |
// Track row group for break detection
|
| 747 |
let prevGroupVal = undefined;
|
| 748 |
|
|
@@ -759,6 +780,7 @@ function buildTables(filtered, chartsShown) {
|
|
| 759 |
const cls = i === firstMetricIdx ? ' class="first-metric metric-cell"' : (c.isMetric ? ' class="metric-cell"' : '');
|
| 760 |
return `<th${tip}${cls}>${c.label}</th>`;
|
| 761 |
}).join("");
|
|
|
|
| 762 |
html += `</tr></thead><tbody>`;
|
| 763 |
|
| 764 |
// Compute best metric value per sub-group (tableGroupBy) per column
|
|
@@ -805,6 +827,7 @@ function buildTables(filtered, chartsShown) {
|
|
| 805 |
html += `<td>${val || "β"}</td>`;
|
| 806 |
}
|
| 807 |
});
|
|
|
|
| 808 |
html += "</tr>";
|
| 809 |
});
|
| 810 |
|
|
@@ -932,6 +955,17 @@ async function buildAccuracyTable() {
|
|
| 932 |
section.appendChild(card);
|
| 933 |
}
|
| 934 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 935 |
// βββ Embed SVG module ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 936 |
|
| 937 |
const embed = initEmbed({
|
|
@@ -939,8 +973,12 @@ const embed = initEmbed({
|
|
| 939 |
getData: () => DATA, MODEL_COL, FAMILY_COL, GROUP_BY, CHART_CFG,
|
| 940 |
MODEL_COLORS, MODEL_SHORT, isOOMRow, isExternalModel,
|
| 941 |
sortModels, parseModelSize,
|
|
|
|
| 942 |
});
|
| 943 |
|
|
|
|
|
|
|
|
|
|
| 944 |
// βββ Render βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 945 |
|
| 946 |
function render() {
|
|
@@ -959,9 +997,12 @@ function render() {
|
|
| 959 |
buildChart(filtered);
|
| 960 |
const chartsShown = charts.length > 0;
|
| 961 |
buildTables(filtered, chartsShown);
|
|
|
|
|
|
|
| 962 |
buildDemo();
|
| 963 |
buildAccuracyTable();
|
| 964 |
buildExperimentSetup();
|
|
|
|
| 965 |
}
|
| 966 |
|
| 967 |
// βββ Switch Base Family (load data + re-render) βββββββββββββββββββββββββββββββ
|
|
|
|
| 371 |
return div;
|
| 372 |
}
|
| 373 |
|
| 374 |
+
// βββ Metric legend ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 375 |
+
|
| 376 |
+
// [legend] Single source of truth for "which metrics have data in these rows".
|
| 377 |
+
// Used by buildChart (metric buttons), buildTables (metric columns) and the
|
| 378 |
+
// legend below, so all three agree on what is relevant to the current family.
|
| 379 |
+
function metricsWithData(rows) {
|
| 380 |
+
return config.metrics.filter(m =>
|
| 381 |
+
rows.some(r => r[m.column] !== undefined && r[m.column] !== null && r[m.column] !== 0 && r[m.column] !== "N/A")
|
| 382 |
+
);
|
| 383 |
+
}
|
| 384 |
+
|
| 385 |
+
// [legend] The legend explains only the metrics currently on screen: a CV family
|
| 386 |
+
// (ips/lat/mem) no longer shows the LLM keys and vice versa. Re-rendered on every
|
| 387 |
+
// render(). Set config.legend_filter = false to always list every metric.
|
| 388 |
const legendGrid = document.getElementById("legend-grid");
|
| 389 |
+
|
| 390 |
+
function renderLegend(metrics) {
|
| 391 |
+
const shown = config.legend_filter === false ? config.metrics : metrics;
|
| 392 |
+
legendGrid.innerHTML = shown.map(m =>
|
| 393 |
+
`<div><strong>${m.short || m.column}</strong> ${m.description || m.label}</div>`
|
| 394 |
+
).join("");
|
| 395 |
+
legendGrid.style.display = shown.length ? "" : "none";
|
| 396 |
+
}
|
| 397 |
|
| 398 |
// βββ State ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 399 |
|
|
|
|
| 449 |
const strVals = vals.map(String);
|
| 450 |
const needsReset = resetDefaults || !strVals.includes(String(filters[f.column]));
|
| 451 |
if (needsReset) {
|
| 452 |
+
// [urlstate] a value asked for by the URL hash wins over the family default
|
| 453 |
+
const wanted = urlstate.desiredFilterValue(f.column, strVals);
|
| 454 |
// Prefer family-specific default_device for the device/group_by filter
|
| 455 |
const defaultVal = f.column === GROUP_BY && familyCfg.default_device;
|
| 456 |
+
if (wanted !== null) {
|
| 457 |
+
filters[f.column] = wanted;
|
| 458 |
+
} else if (defaultVal && strVals.includes(String(defaultVal))) {
|
| 459 |
filters[f.column] = defaultVal;
|
| 460 |
} else {
|
| 461 |
filters[f.column] = vals[0] ?? "";
|
|
|
|
| 551 |
headerRight.className = "chart-header-right";
|
| 552 |
|
| 553 |
// Only show metric buttons for metrics that have non-zero data
|
| 554 |
+
const chartVisibleMetrics = metricsWithData(gRows); // [legend] was an inline config.metrics.filter(...)
|
|
|
|
|
|
|
| 555 |
if (chartVisibleMetrics.length > 1) {
|
| 556 |
const metricEl = metricGroup.querySelector(".btn-group");
|
| 557 |
renderBtnGroup(metricEl,
|
|
|
|
| 703 |
});
|
| 704 |
|
| 705 |
// Hide metric columns where every value in the filtered data is zero, null, or N/A
|
| 706 |
+
const visibleMetrics = metricsWithData(filtered); // [legend] was an inline config.metrics.filter(...)
|
|
|
|
|
|
|
| 707 |
|
| 708 |
// Build column list: Model + visible display cols + metrics
|
| 709 |
const colDefs = [
|
|
|
|
| 761 |
return metricHib ? va - vb : vb - va;
|
| 762 |
});
|
| 763 |
|
| 764 |
+
// [speedup] pair optimized rows with their baseline for this table
|
| 765 |
+
speedup.prepare(rows, { tableGroupCols, visibleMetrics, activeMetricCol });
|
| 766 |
+
|
| 767 |
// Track row group for break detection
|
| 768 |
let prevGroupVal = undefined;
|
| 769 |
|
|
|
|
| 780 |
const cls = i === firstMetricIdx ? ' class="first-metric metric-cell"' : (c.isMetric ? ' class="metric-cell"' : '');
|
| 781 |
return `<th${tip}${cls}>${c.label}</th>`;
|
| 782 |
}).join("");
|
| 783 |
+
html += speedup.headerHtml(); // [speedup] extra right-hand column
|
| 784 |
html += `</tr></thead><tbody>`;
|
| 785 |
|
| 786 |
// Compute best metric value per sub-group (tableGroupBy) per column
|
|
|
|
| 827 |
html += `<td>${val || "β"}</td>`;
|
| 828 |
}
|
| 829 |
});
|
| 830 |
+
html += speedup.cellHtml(r); // [speedup] extra right-hand column
|
| 831 |
html += "</tr>";
|
| 832 |
});
|
| 833 |
|
|
|
|
| 955 |
section.appendChild(card);
|
| 956 |
}
|
| 957 |
|
| 958 |
+
// βββ URL state module ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 959 |
+
|
| 960 |
+
// [urlstate] Construction applies any incoming #hash to `filters` right here,
|
| 961 |
+
// before populateFilters()/switchBaseFamily() below, so the first render
|
| 962 |
+
// already shows the linked view. render() then keeps the hash in sync.
|
| 963 |
+
const urlstate = initUrlState({
|
| 964 |
+
config, filters, BASE_FAMILIES, deriveBaseFamily, GROUP_BY,
|
| 965 |
+
availableOptions, renderSidebar, updateDependentFilters,
|
| 966 |
+
switchBaseFamily, render,
|
| 967 |
+
});
|
| 968 |
+
|
| 969 |
// βββ Embed SVG module ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 970 |
|
| 971 |
const embed = initEmbed({
|
|
|
|
| 973 |
getData: () => DATA, MODEL_COL, FAMILY_COL, GROUP_BY, CHART_CFG,
|
| 974 |
MODEL_COLORS, MODEL_SHORT, isOOMRow, isExternalModel,
|
| 975 |
sortModels, parseModelSize,
|
| 976 |
+
shareHash: urlstate.currentHash, // [urlstate] embed link points at this view
|
| 977 |
});
|
| 978 |
|
| 979 |
+
// [speedup] Speedup vs baseline column module
|
| 980 |
+
const speedup = initSpeedup({ config, MODEL_COL, FAMILY_COL, isExternalModel });
|
| 981 |
+
|
| 982 |
// βββ Render βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 983 |
|
| 984 |
function render() {
|
|
|
|
| 997 |
buildChart(filtered);
|
| 998 |
const chartsShown = charts.length > 0;
|
| 999 |
buildTables(filtered, chartsShown);
|
| 1000 |
+
renderLegend(metricsWithData(filtered)); // [legend] follows the table columns
|
| 1001 |
+
|
| 1002 |
buildDemo();
|
| 1003 |
buildAccuracyTable();
|
| 1004 |
buildExperimentSetup();
|
| 1005 |
+
urlstate.sync(); // [urlstate] mirror the settled view into location.hash
|
| 1006 |
}
|
| 1007 |
|
| 1008 |
// βββ Switch Base Family (load data + re-render) βββββββββββββββββββββββββββββββ
|
config.json
CHANGED
|
@@ -5,6 +5,7 @@
|
|
| 5 |
"model_family_column": "model_family",
|
| 6 |
"model_link_prefix": "https://huggingface.co/",
|
| 7 |
"optimized_org": "embedl",
|
|
|
|
| 8 |
"base_names": {
|
| 9 |
"Llama-3.2-1B": "Llama-3.2-1B-Instruct",
|
| 10 |
"Llama-3.2-3B": "Llama-3.2-3B-Instruct",
|
|
|
|
| 5 |
"model_family_column": "model_family",
|
| 6 |
"model_link_prefix": "https://huggingface.co/",
|
| 7 |
"optimized_org": "embedl",
|
| 8 |
+
"speedup_column": true,
|
| 9 |
"base_names": {
|
| 10 |
"Llama-3.2-1B": "Llama-3.2-1B-Instruct",
|
| 11 |
"Llama-3.2-3B": "Llama-3.2-3B-Instruct",
|
embed.js
CHANGED
|
@@ -10,7 +10,7 @@ const {
|
|
| 10 |
config, filters, activeFamilyKey, getActiveModelSet,
|
| 11 |
getData, MODEL_COL, FAMILY_COL, GROUP_BY, CHART_CFG,
|
| 12 |
MODEL_COLORS, MODEL_SHORT, isOOMRow, isExternalModel,
|
| 13 |
-
sortModels, parseModelSize,
|
| 14 |
} = deps;
|
| 15 |
|
| 16 |
let embedModal = null;
|
|
@@ -310,7 +310,10 @@ async function showEmbedModal() {
|
|
| 310 |
const fileName = embedFileName();
|
| 311 |
const svgUrl = "https://huggingface.co/datasets/" + HF_DOCS_REPO + "/resolve/main/" + HF_SVG_FOLDER + "/" + fileName;
|
| 312 |
|
| 313 |
-
|
|
|
|
|
|
|
|
|
|
| 314 |
|
| 315 |
embedModal.querySelector("#embed-code").value = embedCode;
|
| 316 |
embedModal.classList.add("visible");
|
|
|
|
| 10 |
config, filters, activeFamilyKey, getActiveModelSet,
|
| 11 |
getData, MODEL_COL, FAMILY_COL, GROUP_BY, CHART_CFG,
|
| 12 |
MODEL_COLORS, MODEL_SHORT, isOOMRow, isExternalModel,
|
| 13 |
+
sortModels, parseModelSize, shareHash,
|
| 14 |
} = deps;
|
| 15 |
|
| 16 |
let embedModal = null;
|
|
|
|
| 310 |
const fileName = embedFileName();
|
| 311 |
const svgUrl = "https://huggingface.co/datasets/" + HF_DOCS_REPO + "/resolve/main/" + HF_SVG_FOLDER + "/" + fileName;
|
| 312 |
|
| 313 |
+
// Deep-link the embed back to the exact view it was generated from.
|
| 314 |
+
const spacesUrl = HF_SPACES_URL + (typeof shareHash === "function" ? shareHash() : "");
|
| 315 |
+
|
| 316 |
+
const embedCode = '<a href="' + spacesUrl + '" target="_blank" rel="noopener">\n <img\n src="' + svgUrl + '"\n alt="Edge Inference Benchmarks for ' + famKey + '"\n width="100%"\n />\n</a>';
|
| 317 |
|
| 318 |
embedModal.querySelector("#embed-code").value = embedCode;
|
| 319 |
embedModal.classList.add("visible");
|
index.html
CHANGED
|
@@ -86,6 +86,8 @@
|
|
| 86 |
|
| 87 |
<script src="demo/demo.js"></script>
|
| 88 |
<script src="embed.js"></script>
|
|
|
|
|
|
|
| 89 |
<script src="app.js"></script>
|
| 90 |
</body>
|
| 91 |
</html>
|
|
|
|
| 86 |
|
| 87 |
<script src="demo/demo.js"></script>
|
| 88 |
<script src="embed.js"></script>
|
| 89 |
+
<script src="speedup.js"></script>
|
| 90 |
+
<script src="urlstate.js"></script>
|
| 91 |
<script src="app.js"></script>
|
| 92 |
</body>
|
| 93 |
</html>
|
speedup.js
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// βββ Speedup vs Baseline Column ββββββββββββββββββββββββββββββββββββββββββββββ
|
| 2 |
+
//
|
| 3 |
+
// Extracted module. Call initSpeedup(deps) from the main app once globals are
|
| 4 |
+
// ready. Returns { prepare, headerHtml, cellHtml }.
|
| 5 |
+
//
|
| 6 |
+
// The benchmark data is inherently paired: an external baseline model (e.g.
|
| 7 |
+
// meta-llama/Llama-3.2-1B-Instruct) and the optimized embedl variants of it
|
| 8 |
+
// (embedl/Llama-3.2-1B-Instruct-FlashHead, β¦) measured under identical
|
| 9 |
+
// conditions. This module pairs them up and renders their ratio for the
|
| 10 |
+
// active metric as an extra right-hand column in the benchmark tables.
|
| 11 |
+
|
| 12 |
+
// eslint-disable-next-line no-unused-vars
|
| 13 |
+
function initSpeedup(deps) {
|
| 14 |
+
|
| 15 |
+
const {
|
| 16 |
+
config, MODEL_COL, FAMILY_COL, isExternalModel,
|
| 17 |
+
} = deps;
|
| 18 |
+
|
| 19 |
+
const ENABLED = config.speedup_column !== false;
|
| 20 |
+
const LABEL = config.speedup_label || "VS BASE";
|
| 21 |
+
|
| 22 |
+
// Two rows are comparable only when everything that defines the measurement
|
| 23 |
+
// matches: the model family, every filter column (type / batch / device) and
|
| 24 |
+
// every configured display column (res / fps / frames / ctx). Leaving the
|
| 25 |
+
// display columns out would happily pair a 1920x1080 run with a 854x480 one.
|
| 26 |
+
const KEY_COLS = [
|
| 27 |
+
FAMILY_COL,
|
| 28 |
+
...config.filters.map(f => f.column),
|
| 29 |
+
...(config.display_columns || []).map(d => d.column),
|
| 30 |
+
].filter(Boolean);
|
| 31 |
+
|
| 32 |
+
function pairKey(row, cols) {
|
| 33 |
+
return cols.map(c => String(row[c] ?? "")).join("\0");
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
/** Metric value as a usable positive number; undefined for OOM / not-measured / "N/A" / 0. */
|
| 37 |
+
function usable(val) {
|
| 38 |
+
return typeof val === "number" && isFinite(val) && val > 0 ? val : undefined;
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
function formatRatio(ratio) {
|
| 42 |
+
return ratio.toFixed(2) + "Γ";
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
// Per-table state, rebuilt by prepare() before each table is rendered.
|
| 46 |
+
let state = null;
|
| 47 |
+
|
| 48 |
+
// βββ Pairing βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 49 |
+
|
| 50 |
+
/**
|
| 51 |
+
* Compute the baseline pairing for one rendered table.
|
| 52 |
+
* @param rows the rows of this table (already filtered to one group_by value)
|
| 53 |
+
* @param opts { tableGroupCols, visibleMetrics, activeMetricCol }
|
| 54 |
+
*/
|
| 55 |
+
function prepare(rows, opts) {
|
| 56 |
+
state = null;
|
| 57 |
+
if (!ENABLED || !rows || !rows.length) return;
|
| 58 |
+
|
| 59 |
+
const visibleMetrics = opts.visibleMetrics || [];
|
| 60 |
+
const metricCfg = visibleMetrics.find(m => m.column === opts.activeMetricCol)
|
| 61 |
+
|| visibleMetrics[0];
|
| 62 |
+
if (!metricCfg) return;
|
| 63 |
+
|
| 64 |
+
const metricCol = metricCfg.column;
|
| 65 |
+
const hib = metricCfg.higher_is_better !== false;
|
| 66 |
+
const cols = [...new Set(KEY_COLS.concat(opts.tableGroupCols || []))];
|
| 67 |
+
|
| 68 |
+
// Bucket the external (baseline) rows by comparison key.
|
| 69 |
+
const baselines = {};
|
| 70 |
+
rows.forEach(r => {
|
| 71 |
+
if (!isExternalModel(r[MODEL_COL])) return;
|
| 72 |
+
const k = pairKey(r, cols);
|
| 73 |
+
(baselines[k] = baselines[k] || []).push(r);
|
| 74 |
+
});
|
| 75 |
+
|
| 76 |
+
const ratios = new Map(); // optimized row -> ratio
|
| 77 |
+
const isBase = new Set(); // baseline rows that actually anchor a pair
|
| 78 |
+
|
| 79 |
+
rows.forEach(r => {
|
| 80 |
+
if (isExternalModel(r[MODEL_COL])) return;
|
| 81 |
+
const cands = baselines[pairKey(r, cols)];
|
| 82 |
+
if (!cands || !cands.length) return;
|
| 83 |
+
// Ambiguous: several *different* baseline models match the same key.
|
| 84 |
+
// Show nothing rather than an arbitrary ratio.
|
| 85 |
+
if (new Set(cands.map(c => c[MODEL_COL])).size > 1) return;
|
| 86 |
+
const base = cands[0];
|
| 87 |
+
const b = usable(base[metricCol]);
|
| 88 |
+
const o = usable(r[metricCol]);
|
| 89 |
+
if (b === undefined || o === undefined) return;
|
| 90 |
+
const ratio = hib ? o / b : b / o;
|
| 91 |
+
if (!isFinite(ratio) || ratio <= 0) return;
|
| 92 |
+
ratios.set(r, ratio);
|
| 93 |
+
isBase.add(base);
|
| 94 |
+
});
|
| 95 |
+
|
| 96 |
+
if (!ratios.size) return; // no real pair in this table -> no column
|
| 97 |
+
state = { ratios, isBase, metricCfg };
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
// βββ Rendering βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 101 |
+
|
| 102 |
+
function headerHtml() {
|
| 103 |
+
if (!state) return "";
|
| 104 |
+
const m = state.metricCfg;
|
| 105 |
+
const tip = `Ratio of this model's ${m.label || m.column} against the original `
|
| 106 |
+
+ `(non-${config.optimized_org || "embedl"}) model measured under identical conditions. `
|
| 107 |
+
+ `Always oriented so that higher is better: 1.26Γ means 26% better than the baseline.`;
|
| 108 |
+
return `<th class="metric-cell speedup-cell" data-tip="${tip.replace(/"/g, """)}">${LABEL}</th>`;
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
function cellHtml(row) {
|
| 112 |
+
if (!state) return "";
|
| 113 |
+
if (state.isBase.has(row)) {
|
| 114 |
+
// The baseline is the reference point, so it is 1.00x by definition.
|
| 115 |
+
// Spelling it out keeps "β" unambiguously meaning "no comparison".
|
| 116 |
+
return `<td class="metric-cell speedup-cell"><span class="speedup-ref">1.00Γ</span></td>`;
|
| 117 |
+
}
|
| 118 |
+
const ratio = state.ratios.get(row);
|
| 119 |
+
if (ratio === undefined) return `<td class="metric-cell speedup-cell">β</td>`;
|
| 120 |
+
const down = ratio < 0.995 ? " is-down" : "";
|
| 121 |
+
return `<td class="metric-cell speedup-cell">`
|
| 122 |
+
+ `<span class="speedup-val${down}">${formatRatio(ratio)}</span></td>`;
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
return { prepare, headerHtml, cellHtml };
|
| 126 |
+
|
| 127 |
+
}
|
style.css
CHANGED
|
@@ -798,6 +798,27 @@ tbody tr.row-group-break td {
|
|
| 798 |
border-color: var(--btn-active-border);
|
| 799 |
}
|
| 800 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 801 |
/* ββ Responsive ββββββββββββββββββββββββββββββββββββββββ */
|
| 802 |
@media (max-width: 900px) {
|
| 803 |
.page {
|
|
|
|
| 798 |
border-color: var(--btn-active-border);
|
| 799 |
}
|
| 800 |
|
| 801 |
+
/* ββ Speedup βββββββββββββββββββββββββββββββββββββββββββ */
|
| 802 |
+
td.speedup-cell {
|
| 803 |
+
position: relative;
|
| 804 |
+
color: var(--text);
|
| 805 |
+
font-variant-numeric: tabular-nums;
|
| 806 |
+
}
|
| 807 |
+
|
| 808 |
+
.speedup-val {
|
| 809 |
+
position: relative;
|
| 810 |
+
font-weight: 600;
|
| 811 |
+
color: var(--green);
|
| 812 |
+
}
|
| 813 |
+
|
| 814 |
+
.speedup-val.is-down {
|
| 815 |
+
color: var(--red);
|
| 816 |
+
}
|
| 817 |
+
|
| 818 |
+
.speedup-ref {
|
| 819 |
+
color: var(--text-dim);
|
| 820 |
+
}
|
| 821 |
+
|
| 822 |
/* ββ Responsive ββββββββββββββββββββββββββββββββββββββββ */
|
| 823 |
@media (max-width: 900px) {
|
| 824 |
.page {
|
urlstate.js
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// βββ Shareable URL State βββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 2 |
+
//
|
| 3 |
+
// Mirrors the current view β base family, variant, metric and every
|
| 4 |
+
// config.filters column β into location.hash so that a view can be linked:
|
| 5 |
+
//
|
| 6 |
+
// #family=Llama-3.2&device=agx_orin&metric=tps
|
| 7 |
+
//
|
| 8 |
+
// Extracted module. Call initUrlState(deps) once, after `filters` exists and
|
| 9 |
+
// BEFORE the first render: construction applies an incoming hash to `filters`
|
| 10 |
+
// in place, so the very first render already shows the linked view (there is
|
| 11 |
+
// no visible second render). Returns { desiredFilterValue, sync, currentHash }.
|
| 12 |
+
//
|
| 13 |
+
// Disable with "url_state": false in config.json.
|
| 14 |
+
|
| 15 |
+
// eslint-disable-next-line no-unused-vars
|
| 16 |
+
function initUrlState(deps) {
|
| 17 |
+
|
| 18 |
+
const {
|
| 19 |
+
config, filters, BASE_FAMILIES, deriveBaseFamily, GROUP_BY,
|
| 20 |
+
availableOptions, renderSidebar, updateDependentFilters,
|
| 21 |
+
switchBaseFamily, render,
|
| 22 |
+
} = deps;
|
| 23 |
+
|
| 24 |
+
const ENABLED = config.url_state !== false;
|
| 25 |
+
const FILTER_COLS = config.filters.map(f => f.column);
|
| 26 |
+
const METRIC_COLS = config.metrics.map(m => m.column);
|
| 27 |
+
|
| 28 |
+
// Filter values requested by the hash but not applied yet. They are consumed
|
| 29 |
+
// by updateDependentFilters(), which otherwise resets every filter column to a
|
| 30 |
+
// family default whenever the newly loaded data does not contain the current
|
| 31 |
+
// value. Cleared by sync() once the view has settled.
|
| 32 |
+
let pending = {};
|
| 33 |
+
|
| 34 |
+
// The hash this module last wrote, so our own replaceState is not re-applied.
|
| 35 |
+
let lastWritten = null;
|
| 36 |
+
|
| 37 |
+
// Guard against re-entrant hashchange handling during an async family switch.
|
| 38 |
+
let applying = false;
|
| 39 |
+
|
| 40 |
+
// βββ Parse ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 41 |
+
|
| 42 |
+
function parseHash(raw) {
|
| 43 |
+
const out = {};
|
| 44 |
+
const s = String(raw || "").replace(/^#/, "");
|
| 45 |
+
if (!s) return out;
|
| 46 |
+
s.split("&").forEach(part => {
|
| 47 |
+
const i = part.indexOf("=");
|
| 48 |
+
if (i < 1) return;
|
| 49 |
+
try {
|
| 50 |
+
const k = decodeURIComponent(part.slice(0, i)).trim();
|
| 51 |
+
const v = decodeURIComponent(part.slice(i + 1)).trim();
|
| 52 |
+
if (k) out[k] = v;
|
| 53 |
+
} catch {
|
| 54 |
+
// Malformed percent-escape β drop the pair rather than throw.
|
| 55 |
+
}
|
| 56 |
+
});
|
| 57 |
+
return out;
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
// Accept either a base family ("Llama-3.2") or a config family key
|
| 61 |
+
// ("Llama-3.2-1B"), mirroring the ?family= query deep link.
|
| 62 |
+
function resolveFamily(fam) {
|
| 63 |
+
if (!fam) return null;
|
| 64 |
+
if (BASE_FAMILIES[fam]) return { baseFamily: fam, variant: null };
|
| 65 |
+
if (config.model_families?.[fam]) {
|
| 66 |
+
const base = deriveBaseFamily(fam);
|
| 67 |
+
if (BASE_FAMILIES[base]) return { baseFamily: base, variant: fam };
|
| 68 |
+
}
|
| 69 |
+
return null;
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
// Apply parsed hash params onto `filters`. Unknown families/metrics/variants
|
| 73 |
+
// are ignored, never applied. Returns true when the base family changed (the
|
| 74 |
+
// caller then has to reload that family's data).
|
| 75 |
+
function applyParams(params) {
|
| 76 |
+
let familyChanged = false;
|
| 77 |
+
|
| 78 |
+
const fam = resolveFamily(params.family);
|
| 79 |
+
if (fam) {
|
| 80 |
+
familyChanged = fam.baseFamily !== filters.baseFamily;
|
| 81 |
+
filters.baseFamily = fam.baseFamily;
|
| 82 |
+
filters.variant = fam.variant;
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
if (params.variant !== undefined) {
|
| 86 |
+
const v = params.variant;
|
| 87 |
+
// A variant may be a data-derived model_family key that is absent from
|
| 88 |
+
// config, so it can only be checked against the base-family rule here;
|
| 89 |
+
// sync() drops it later if the loaded family does not offer it.
|
| 90 |
+
filters.variant = (v && deriveBaseFamily(v) === filters.baseFamily) ? v : null;
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
if (params.metric && METRIC_COLS.includes(params.metric)) {
|
| 94 |
+
filters.metric = params.metric;
|
| 95 |
+
}
|
| 96 |
+
|
| 97 |
+
pending = {};
|
| 98 |
+
FILTER_COLS.forEach(col => {
|
| 99 |
+
if (params[col] !== undefined && params[col] !== "") pending[col] = params[col];
|
| 100 |
+
});
|
| 101 |
+
|
| 102 |
+
return familyChanged;
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
// βββ Serialize ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 106 |
+
|
| 107 |
+
// Drop a variant the currently loaded family does not actually offer β the
|
| 108 |
+
// sidebar only shows variants when there is more than one.
|
| 109 |
+
function normalizeVariant() {
|
| 110 |
+
const variants = BASE_FAMILIES[filters.baseFamily]?.variants || [];
|
| 111 |
+
if (filters.variant && (variants.length <= 1 || !variants.includes(filters.variant))) {
|
| 112 |
+
filters.variant = null;
|
| 113 |
+
}
|
| 114 |
+
}
|
| 115 |
+
|
| 116 |
+
function buildHash() {
|
| 117 |
+
const parts = [];
|
| 118 |
+
const push = (k, v) => parts.push(encodeURIComponent(k) + "=" + encodeURIComponent(v));
|
| 119 |
+
|
| 120 |
+
push("family", filters.baseFamily);
|
| 121 |
+
if (filters.variant) push("variant", filters.variant);
|
| 122 |
+
|
| 123 |
+
// Always link the group_by (device) column β it is what people share.
|
| 124 |
+
// The remaining filter columns are only linked when the loaded family
|
| 125 |
+
// actually offers a choice; otherwise their button group is hidden and the
|
| 126 |
+
// value is noise that would be re-derived identically anyway.
|
| 127 |
+
const opts = availableOptions();
|
| 128 |
+
config.filters.forEach(f => {
|
| 129 |
+
const v = filters[f.column];
|
| 130 |
+
if (v === "" || v === null || v === undefined) return;
|
| 131 |
+
if (f.column !== GROUP_BY && (opts[f.column] || []).length <= 1) return;
|
| 132 |
+
push(f.column, v);
|
| 133 |
+
});
|
| 134 |
+
|
| 135 |
+
if (filters.metric) push("metric", filters.metric);
|
| 136 |
+
return parts.join("&");
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
// βββ Public: called from app.js ββββββββββββββββββββββββββββββββββββββββββββββ
|
| 140 |
+
|
| 141 |
+
// Hook in updateDependentFilters(): the value the hash asked for, when it is
|
| 142 |
+
// available in the freshly loaded data, else null (caller falls back to its
|
| 143 |
+
// own default). "all" is only meaningful for the group_by column.
|
| 144 |
+
function desiredFilterValue(col, availableStrVals) {
|
| 145 |
+
if (!ENABLED) return null;
|
| 146 |
+
const want = pending[col];
|
| 147 |
+
if (want === undefined) return null;
|
| 148 |
+
if (col === GROUP_BY && want === "all") return "all";
|
| 149 |
+
return availableStrVals.includes(want) ? want : null;
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
// Hook at the end of render(): the view has settled, publish it. replaceState
|
| 153 |
+
// keeps every button click out of the history stack.
|
| 154 |
+
function sync() {
|
| 155 |
+
if (!ENABLED) return;
|
| 156 |
+
pending = {};
|
| 157 |
+
normalizeVariant();
|
| 158 |
+
const hash = buildHash();
|
| 159 |
+
if (hash === lastWritten && String(location.hash).replace(/^#/, "") === hash) return;
|
| 160 |
+
lastWritten = hash;
|
| 161 |
+
const url = location.pathname + location.search + "#" + hash;
|
| 162 |
+
try {
|
| 163 |
+
history.replaceState(history.state, "", url);
|
| 164 |
+
} catch {
|
| 165 |
+
// file:// and a few sandboxes reject replaceState; the assignment below
|
| 166 |
+
// fires hashchange, which the lastWritten guard then ignores.
|
| 167 |
+
location.hash = hash;
|
| 168 |
+
}
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
function currentHash() {
|
| 172 |
+
return ENABLED ? "#" + buildHash() : "";
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
// βββ Incoming navigation βββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 176 |
+
|
| 177 |
+
async function onHashChange() {
|
| 178 |
+
if (!ENABLED || applying) return;
|
| 179 |
+
const raw = String(location.hash).replace(/^#/, "");
|
| 180 |
+
if (raw === lastWritten) return;
|
| 181 |
+
const params = parseHash(raw);
|
| 182 |
+
if (!Object.keys(params).length) return;
|
| 183 |
+
applying = true;
|
| 184 |
+
try {
|
| 185 |
+
const familyChanged = applyParams(params);
|
| 186 |
+
renderSidebar();
|
| 187 |
+
if (familyChanged) {
|
| 188 |
+
await switchBaseFamily(filters.baseFamily);
|
| 189 |
+
} else {
|
| 190 |
+
// Reset first so columns the URL omits fall back to their default,
|
| 191 |
+
// then desiredFilterValue() re-applies the ones it does name.
|
| 192 |
+
updateDependentFilters(true);
|
| 193 |
+
render();
|
| 194 |
+
}
|
| 195 |
+
} finally {
|
| 196 |
+
applying = false;
|
| 197 |
+
}
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
// βββ Init βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 201 |
+
|
| 202 |
+
// Applied here, before populateFilters()/switchBaseFamily() run, so the first
|
| 203 |
+
// render already reflects the link. The hash therefore wins over the existing
|
| 204 |
+
// ?family= query deep link, which is applied earlier in app.js.
|
| 205 |
+
if (ENABLED) {
|
| 206 |
+
const initial = parseHash(typeof location !== "undefined" ? location.hash : "");
|
| 207 |
+
if (Object.keys(initial).length) applyParams(initial);
|
| 208 |
+
if (typeof window !== "undefined") window.addEventListener("hashchange", onHashChange);
|
| 209 |
+
}
|
| 210 |
+
|
| 211 |
+
return { desiredFilterValue, sync, currentHash };
|
| 212 |
+
|
| 213 |
+
} // end initUrlState
|