x
This commit is contained in:
@@ -0,0 +1,401 @@
|
||||
:root {
|
||||
--bg: #f8fafc;
|
||||
--surface: #ffffff;
|
||||
--text: #0f172a;
|
||||
--text-sub: #64748b;
|
||||
--accent: #d90007;
|
||||
/* AMD Red */
|
||||
--accent-fade: #fff0f0;
|
||||
--border: #e2e8f0;
|
||||
--font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
font-family: var(--font);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
header,
|
||||
.controls,
|
||||
.panel-split,
|
||||
#tables {
|
||||
max-width: 860px;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 4px 0 0;
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-sub);
|
||||
}
|
||||
|
||||
.legend {
|
||||
margin-top: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.legend label {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-sub);
|
||||
}
|
||||
|
||||
.legend-pills {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.legend-pill {
|
||||
cursor: default !important;
|
||||
}
|
||||
|
||||
.legend-pill-default::before {
|
||||
content: "";
|
||||
display: inline-block;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: #cbd5e1;
|
||||
border-radius: 50%;
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.legend-pill-dual::before {
|
||||
content: "";
|
||||
display: inline-block;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: #d90007;
|
||||
border-radius: 50%;
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.controls {
|
||||
background: var(--surface);
|
||||
border-bottom: 1px solid var(--border);
|
||||
padding: 12px 24px;
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.control {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.control label {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-sub);
|
||||
}
|
||||
|
||||
input[type="text"],
|
||||
select {
|
||||
padding: 6px 10px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
font-size: 0.875rem;
|
||||
background: var(--bg);
|
||||
min-width: 180px;
|
||||
}
|
||||
|
||||
.range-wrap {
|
||||
position: relative;
|
||||
width: 200px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.range-track {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 4px;
|
||||
background: #e3e7f1;
|
||||
border-radius: 2px;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
input[type=range] {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
pointer-events: none;
|
||||
appearance: none;
|
||||
background: none;
|
||||
margin: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
input[type=range]::-webkit-slider-thumb {
|
||||
pointer-events: auto;
|
||||
appearance: none;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
background: var(--surface);
|
||||
border: 2px solid var(--accent);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.range-values {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-sub);
|
||||
margin-top: 4px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.panel {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.panel.compact {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
#tables-panel {
|
||||
flex: 1;
|
||||
background: var(--bg);
|
||||
padding: 0;
|
||||
overflow-y: auto;
|
||||
margin-top: 24px;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.panel-split {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 24px;
|
||||
background: var(--surface);
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.backend-header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.backend-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.backend-label label {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-sub);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.backend-list {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.backend-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 0.875rem;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.test-block {
|
||||
margin-bottom: 32px;
|
||||
background: var(--surface);
|
||||
border-top: 1px solid var(--border);
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
h2 {
|
||||
padding: 16px 24px;
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
background: #f1f5f9;
|
||||
color: var(--text);
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.table-wrap {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.table-scroll {
|
||||
overflow-x: auto;
|
||||
padding-bottom: 12px;
|
||||
/* Scrollbar space */
|
||||
}
|
||||
|
||||
/* ... */
|
||||
|
||||
.best {
|
||||
background: #f0fdf4;
|
||||
}
|
||||
|
||||
.cell-error {
|
||||
color: #ef4444;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.cell-empty {
|
||||
color: var(--border);
|
||||
font-size: 0.75rem;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Resize Overlay */
|
||||
.resize-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
pointer-events: none;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.resize-bar {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 6px;
|
||||
height: 100%;
|
||||
cursor: col-resize;
|
||||
pointer-events: auto;
|
||||
/* invisible usually, but can hover */
|
||||
}
|
||||
|
||||
.resize-bar:hover {
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.resize-handle {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 4px;
|
||||
cursor: col-resize;
|
||||
}
|
||||
|
||||
.backend-header.dragging {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.backend-header.drop-target {
|
||||
border-left: 2px solid var(--accent);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #cbd5e1;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* Modal Styles */
|
||||
.modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 1000;
|
||||
opacity: 1;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.modal.hidden {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: var(--surface);
|
||||
padding: 24px 32px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
||||
max-width: 500px;
|
||||
width: 90%;
|
||||
position: relative;
|
||||
transform: translateY(0);
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.modal.hidden .modal-content {
|
||||
transform: translateY(20px);
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
right: 12px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
font-size: 1.5rem;
|
||||
line-height: 1;
|
||||
color: var(--text-sub);
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.modal-close:hover {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.modal-content h2 {
|
||||
margin-top: 0;
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
font-size: 1.25rem;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.modal-content p {
|
||||
margin-bottom: 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
@@ -0,0 +1,542 @@
|
||||
const K_SIGMA = 1.0;
|
||||
const MIN_TOL = 0.25;
|
||||
const MODEL_COL_WIDTH = 300;
|
||||
// Winner column removed
|
||||
|
||||
const state = {
|
||||
envs: ["TP1", "TP2"],
|
||||
backendOrder: ["TP1", "TP2"],
|
||||
columnWidths: { "TP1": 260, "TP2": 260 },
|
||||
filters: {
|
||||
search: "",
|
||||
quant: "",
|
||||
backends: new Set(["TP1", "TP2"]),
|
||||
sizeLo: null,
|
||||
sizeHi: null,
|
||||
},
|
||||
ui: {},
|
||||
sizeStats: { min: Infinity, max: -Infinity },
|
||||
draggingEnv: null,
|
||||
quantOptions: [],
|
||||
};
|
||||
|
||||
document.addEventListener("DOMContentLoaded", async () => {
|
||||
cacheUI();
|
||||
setupModals();
|
||||
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"),
|
||||
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"),
|
||||
// Modal hooks
|
||||
tp1ModalOpen: document.getElementById("tp1-modal-open"),
|
||||
tp2ModalOpen: document.getElementById("tp2-modal-open"),
|
||||
tp1Modal: document.getElementById("tp1-modal"),
|
||||
tp2Modal: document.getElementById("tp2-modal"),
|
||||
tp1ModalClose: document.getElementById("tp1-modal-close"),
|
||||
tp2ModalClose: document.getElementById("tp2-modal-close"),
|
||||
};
|
||||
}
|
||||
|
||||
function setupModals() {
|
||||
const modalConfigs = [
|
||||
{ open: state.ui.tp1ModalOpen, modal: state.ui.tp1Modal, close: state.ui.tp1ModalClose },
|
||||
{ open: state.ui.tp2ModalOpen, modal: state.ui.tp2Modal, close: state.ui.tp2ModalClose },
|
||||
];
|
||||
|
||||
modalConfigs.forEach(({ open, modal, close }) => {
|
||||
if (!open || !modal) return;
|
||||
const openModal = () => modal.classList.remove("hidden");
|
||||
const closeModal = () => modal.classList.add("hidden");
|
||||
open.addEventListener("click", openModal);
|
||||
close?.addEventListener("click", closeModal);
|
||||
modal.addEventListener("click", (e) => {
|
||||
if (e.target === modal) closeModal();
|
||||
});
|
||||
document.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Escape" && !modal.classList.contains("hidden")) {
|
||||
closeModal();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function prepareData(runs) {
|
||||
const quantSet = new Set();
|
||||
// Tests map: TestName -> { name: ..., models: Map(ModelName -> Row) }
|
||||
const testsMap = new Map();
|
||||
|
||||
for (const run of runs) {
|
||||
if (!run.test) continue;
|
||||
const testKey = run.test;
|
||||
|
||||
if (run.quant) quantSet.add(run.quant.toUpperCase());
|
||||
|
||||
if (!testsMap.has(testKey)) {
|
||||
testsMap.set(testKey, { name: testKey, models: new Map() });
|
||||
}
|
||||
const testEntry = testsMap.get(testKey);
|
||||
|
||||
const modelName = run.model_clean || run.model;
|
||||
|
||||
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);
|
||||
|
||||
// Update stats
|
||||
if (row.sizeB != null) {
|
||||
state.sizeStats.min = Math.min(state.sizeStats.min, row.sizeB);
|
||||
state.sizeStats.max = Math.max(state.sizeStats.max, row.sizeB);
|
||||
}
|
||||
|
||||
// Add backend data
|
||||
// run.env comes from python script as "TP1" or "TP2"
|
||||
const env = run.env;
|
||||
row.backends[env] = {
|
||||
mean: typeof run.tps_mean === "number" ? run.tps_mean : null,
|
||||
std: 0, // Not currently parsed
|
||||
error: Boolean(run.error),
|
||||
error_type: run.error_type || null,
|
||||
};
|
||||
}
|
||||
|
||||
state.tests = [...testsMap.values()].sort((a, b) => a.name.localeCompare(b.name));
|
||||
state.quantOptions = [...quantSet].sort();
|
||||
}
|
||||
|
||||
function initializeControls() {
|
||||
const { quant, 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);
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
|
||||
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.backends = new Set(state.envs);
|
||||
search.value = "";
|
||||
quant.value = "";
|
||||
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");
|
||||
baseSpan.textContent = env;
|
||||
label.appendChild(baseSpan);
|
||||
|
||||
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 backendList = state.backendOrder.filter((env) => state.filters.backends.has(env));
|
||||
const frag = document.createDocumentFragment();
|
||||
let totalRows = 0;
|
||||
|
||||
for (const test of state.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;
|
||||
block.appendChild(heading);
|
||||
|
||||
const tableWrap = document.createElement("div");
|
||||
tableWrap.className = "table-wrap";
|
||||
const scroller = document.createElement("div");
|
||||
scroller.className = "table-scroll";
|
||||
|
||||
const table = buildSingleTable(models, 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} configurations`;
|
||||
}
|
||||
|
||||
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);
|
||||
// Winner colGroup removed
|
||||
|
||||
backendList.forEach((env) => {
|
||||
const col = document.createElement("col");
|
||||
col.style.width = `${state.columnWidths[env] || 200}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"));
|
||||
// Winner header removed
|
||||
|
||||
backendList.forEach((env) => {
|
||||
const th = makeHeaderCell(env, ""); // REMOVED "backend-header" class
|
||||
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";
|
||||
const head = document.createElement("div");
|
||||
head.className = "model-head";
|
||||
const nameSpan = document.createElement("span");
|
||||
nameSpan.className = "model-name";
|
||||
nameSpan.textContent = model.model;
|
||||
head.appendChild(nameSpan);
|
||||
tdModel.appendChild(head);
|
||||
|
||||
const meta = document.createElement("div");
|
||||
meta.className = "meta";
|
||||
meta.textContent = `${model.quant} · ${formatSize(model.sizeB)}`;
|
||||
tdModel.appendChild(meta);
|
||||
tr.appendChild(tdModel);
|
||||
|
||||
// Winner cell removed
|
||||
|
||||
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">N/A</span>`;
|
||||
} else if (cell.error || cell.mean == null) {
|
||||
td.innerHTML = `<span class="cell-error">FAIL</span>`;
|
||||
} else {
|
||||
td.innerHTML = `<div class="measure">${cell.mean.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] || 200;
|
||||
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.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 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 startResize(event, env) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
const column = state.columnWidths[env] || 200;
|
||||
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 column width removed
|
||||
backendList.forEach((env) => {
|
||||
const width = state.columnWidths[env] || 200;
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,804 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>GPU Comparison - AMD R9700 vs NVIDIA</title>
|
||||
<style>
|
||||
/* Inherited Styles from index.html (Light Theme) */
|
||||
:root {
|
||||
--bg-body: #f9fafb;
|
||||
--bg-card: #ffffff;
|
||||
--text-main: #111827;
|
||||
--text-muted: #6b7280;
|
||||
--border: #e5e7eb;
|
||||
--primary: #ef4444;
|
||||
/* AMD Red */
|
||||
--primary-bg: #fef2f2;
|
||||
--nvidia-green: #22c55e;
|
||||
--nvidia-bg: #f0fdf4;
|
||||
--font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--bg-body);
|
||||
color: var(--text-main);
|
||||
font-family: var(--font-sans);
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 20px auto;
|
||||
}
|
||||
|
||||
/* Header */
|
||||
header {
|
||||
margin-bottom: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2.25rem;
|
||||
font-weight: 800;
|
||||
margin: 0 0 10px 0;
|
||||
letter-spacing: -0.05rem;
|
||||
color: var(--text-main);
|
||||
}
|
||||
|
||||
p.subtitle {
|
||||
color: var(--text-muted);
|
||||
font-size: 1.1rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Controls */
|
||||
.controls {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-bottom: 24px;
|
||||
background: var(--bg-card);
|
||||
padding: 16px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
|
||||
border: 1px solid var(--border);
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.btn {
|
||||
background: #fff;
|
||||
color: var(--text-muted);
|
||||
border: 1px solid var(--border);
|
||||
padding: 10px 16px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
font-weight: 500;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
color: var(--text-main);
|
||||
border-color: var(--text-muted);
|
||||
}
|
||||
|
||||
.btn.active {
|
||||
background: var(--primary);
|
||||
color: white;
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
/* Card / Table Container */
|
||||
.section-card {
|
||||
background: var(--bg-card);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
|
||||
border: 1px solid var(--border);
|
||||
margin-bottom: 32px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.table-responsive {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
th {
|
||||
background: #f9fafb;
|
||||
color: var(--text-muted);
|
||||
font-weight: 600;
|
||||
font-size: 0.8rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
/* Left-align model name */
|
||||
th:first-child,
|
||||
td:first-child {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
th:hover {
|
||||
color: var(--text-main);
|
||||
background: #f3f4f6;
|
||||
}
|
||||
|
||||
tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
tr:hover td {
|
||||
background: #f9fafb;
|
||||
}
|
||||
|
||||
/* Rank Badges */
|
||||
.rank-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
margin-right: 6px;
|
||||
color: #fff;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.rank-1 {
|
||||
background: #f59e0b;
|
||||
border: 1px solid #d97706;
|
||||
}
|
||||
|
||||
/* Gold */
|
||||
.rank-2 {
|
||||
background: #94a3b8;
|
||||
border: 1px solid #64748b;
|
||||
}
|
||||
|
||||
/* Silver */
|
||||
.rank-3 {
|
||||
background: #b45309;
|
||||
border: 1px solid #78350f;
|
||||
}
|
||||
|
||||
/* Bronze */
|
||||
|
||||
.rank-cell-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* Specific Cell Styles */
|
||||
.model-name {
|
||||
font-weight: 600;
|
||||
color: var(--text-main);
|
||||
display: block;
|
||||
}
|
||||
|
||||
.model-meta {
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-muted);
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.val-cell {
|
||||
font-family: var(--font-mono);
|
||||
font-weight: 500;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
/* Highlight Winner Cell Background */
|
||||
td.winner {
|
||||
background-color: #f0fdf4;
|
||||
/* Light Green */
|
||||
}
|
||||
|
||||
/* AMD Column Highlight (Different background for visibility) */
|
||||
.amd-col-header {
|
||||
border-bottom: 2px solid var(--primary);
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.gpu-sub {
|
||||
display: block;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 400;
|
||||
text-transform: none;
|
||||
margin-top: 4px;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
/* Metric Badge */
|
||||
.metric-badge {
|
||||
background: #f3f4f6;
|
||||
padding: 4px 10px;
|
||||
border-radius: 20px;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 500;
|
||||
color: var(--text-muted);
|
||||
margin-top: 10px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
footer {
|
||||
margin-top: 40px;
|
||||
text-align: center;
|
||||
color: var(--text-muted);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
/* Filter Styles */
|
||||
.filter-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-top: 12px;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.toggle-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 0.9rem;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
background: #fff;
|
||||
padding: 6px 12px;
|
||||
border-radius: 20px;
|
||||
border: 1px solid var(--border);
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.toggle-label:hover {
|
||||
border-color: var(--text-muted);
|
||||
}
|
||||
|
||||
.toggle-label input {
|
||||
accent-color: var(--primary);
|
||||
}
|
||||
|
||||
.toggle-label.unchecked {
|
||||
opacity: 0.6;
|
||||
background: #f3f4f6;
|
||||
}
|
||||
|
||||
.weight-input {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.weight-input input {
|
||||
width: 60px;
|
||||
padding: 4px 8px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
/* Modal Styles */
|
||||
.modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
backdrop-filter: blur(2px);
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background-color: #fefefe;
|
||||
margin: 10% auto;
|
||||
padding: 24px;
|
||||
border: 1px solid #888;
|
||||
width: 90%;
|
||||
max-width: 500px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.close {
|
||||
color: #aaa;
|
||||
float: right;
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.close:hover,
|
||||
.close:focus {
|
||||
color: black;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.price-input-group {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.price-input-group:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.price-input-group input {
|
||||
padding: 8px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
width: 100px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div class="container">
|
||||
<header>
|
||||
<h1>Single GPU Performance Comparison</h1>
|
||||
<p class="subtitle">vLLM Decoding Throughput (Tokens/s)</p>
|
||||
<div id="current-metric-display" class="metric-badge">Metric: Raw Tokens/s</div>
|
||||
<p style="margin-top: 10px;">
|
||||
<a href="index.html" style="color: var(--primary); text-decoration: none; font-weight: 500;">← Back
|
||||
to Benchmarks</a>
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<div class="controls-wrapper"
|
||||
style="background: var(--bg-card); padding: 16px; border-radius: 12px; box-shadow: 0 1px 3px rgba(0,0,0,0.05); border: 1px solid var(--border); margin-bottom: 24px;">
|
||||
<div class="controls"
|
||||
style="margin-bottom: 0; box-shadow: none; border: none; padding: 0; background: none;">
|
||||
<button class="btn active" onclick="setMetric('raw')">Raw Performance</button>
|
||||
<button class="btn" onclick="setMetric('price')">Perf / $ (Price)</button>
|
||||
<button class="btn" onclick="setMetric('power')">Perf / Watt (TDP)</button>
|
||||
<button class="btn" onclick="setMetric('vram')">vRAM Value (MB/$)</button>
|
||||
<button class="btn" onclick="setMetric('composite')">Composite Score</button>
|
||||
<button class="btn" onclick="openPriceModal()"
|
||||
style="margin-left: auto; border-color: var(--primary); color: var(--primary);">✎ Edit
|
||||
Prices</button>
|
||||
</div>
|
||||
|
||||
<!-- Price Config Modal -->
|
||||
<div id="price-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<span class="close" onclick="closePriceModal()">×</span>
|
||||
<h2>Edit GPU Prices</h2>
|
||||
<p class="subtitle" style="margin-bottom: 20px;">Adjust prices to see how value metrics change.</p>
|
||||
<div id="price-inputs-container">
|
||||
<!-- Populated by JS -->
|
||||
</div>
|
||||
<div style="margin-top: 20px; text-align: right;">
|
||||
<button class="btn" onclick="closePriceModal()">Cancel</button>
|
||||
<button class="btn active" onclick="savePrices()">Save & Recalculate</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Weighted Scoring Controls (Initially Hidden) -->
|
||||
<div id="weight-controls"
|
||||
style="display: none; margin-top: 16px; padding-top: 16px; border-top: 1px solid var(--border);">
|
||||
<div style="display: flex; gap: 20px; flex-wrap: wrap; justify-content: center; align-items: center;">
|
||||
<div class="weight-input">
|
||||
<label>Perf/$ Weight:</label>
|
||||
<input type="number" id="w-price" value="1" min="0" max="10" step="0.1"
|
||||
onchange="renderTable()">
|
||||
</div>
|
||||
<div class="weight-input">
|
||||
<label>Perf/Watt Weight:</label>
|
||||
<input type="number" id="w-power" value="1" min="0" max="10" step="0.1"
|
||||
onchange="renderTable()">
|
||||
</div>
|
||||
<div class="weight-input">
|
||||
<label>vRAM/$ Weight:</label>
|
||||
<input type="number" id="w-vram" value="1" min="0" max="10" step="0.1" onchange="renderTable()">
|
||||
</div>
|
||||
</div>
|
||||
<p style="text-align: center; font-size: 0.8rem; color: var(--text-muted); margin-top: 8px;">
|
||||
Adjust weights to customize the Composite Score. (vRAM/$ = GB per Dollar)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="filter-group" id="gpu-filters">
|
||||
<!-- Populated by JS -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-card">
|
||||
<div class="table-responsive">
|
||||
<table id="comparison-table">
|
||||
<thead>
|
||||
<tr id="table-header">
|
||||
<!-- Populated by JS -->
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="table-body">
|
||||
<!-- Populated by JS -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
* Prices are estimated market averages/MSRP. Power based on TDP.<br>
|
||||
* <b>Composite Score</b> = Weighted index of Perf/$, Perf/Watt, and vRAM/$ (GB per Dollar). Higher is
|
||||
better.<br>
|
||||
* <b>R9700 (UV+PL)</b>: Undervolted -75mV, Power Limit raised to 315W.
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Reference Specs
|
||||
const GPUS = [
|
||||
{ id: "AMD R9700", name: "AMD R9700 (32GB)", price: 1250, tdp: 300, vram: 32, vendor: "amd" },
|
||||
{ id: "AMD R9700 (UV+PL)", name: "R9700 (UV+PL)*", price: 1250, tdp: 315, vram: 32, vendor: "amd" },
|
||||
{ id: "NVIDIA RTX 3090", name: "RTX 3090 (24GB)", price: 1499, tdp: 350, vram: 24, vendor: "nvidia" },
|
||||
{ id: "NVIDIA RTX 4090", name: "RTX 4090 (24GB)", price: 2000, tdp: 450, vram: 24, vendor: "nvidia" },
|
||||
{ id: "NVIDIA RTX 5090", name: "RTX 5090 (32GB)", price: 2700, tdp: 575, vram: 32, vendor: "nvidia" },
|
||||
{ id: "NVIDIA RTX 5000 Ada", name: "RTX 5000 (32GB)", price: 4700, tdp: 250, vram: 32, vendor: "nvidia" },
|
||||
{ id: "NVIDIA A100", name: "A100 (40GB)", price: 9000, tdp: 300, vram: 40, vendor: "nvidia" }
|
||||
];
|
||||
|
||||
let rawData = [];
|
||||
let currentMetric = 'raw'; // raw, price, power, composite
|
||||
let sortCol = "AMD R9700"; // Default sort
|
||||
let sortDesc = true;
|
||||
|
||||
let visibleGPUs = new Set(GPUS.map(g => g.id));
|
||||
|
||||
async function loadData() {
|
||||
try {
|
||||
const response = await fetch('comparison_results.json');
|
||||
rawData = await response.json();
|
||||
initFilters();
|
||||
renderTable();
|
||||
} catch (e) {
|
||||
console.error("Failed to load data", e);
|
||||
document.getElementById('table-body').innerHTML = `<tr><td colspan="7" style="text-align:center; padding: 40px; color: var(--primary);">Error loading data. Ensure generate_comparison_data.py has been run.</td></tr>`;
|
||||
}
|
||||
}
|
||||
|
||||
function initFilters() {
|
||||
const container = document.getElementById('gpu-filters');
|
||||
container.innerHTML = `<span style="font-size: 0.85rem; font-weight: 600; color: var(--text-muted);">Compare:</span>`;
|
||||
|
||||
GPUS.forEach(gpu => {
|
||||
const label = document.createElement('label');
|
||||
label.className = 'toggle-label';
|
||||
label.innerHTML = `
|
||||
<input type="checkbox" checked onchange="toggleGPU('${gpu.id}')">
|
||||
${gpu.name}
|
||||
`;
|
||||
container.appendChild(label);
|
||||
});
|
||||
}
|
||||
|
||||
function toggleGPU(id) {
|
||||
if (visibleGPUs.has(id)) {
|
||||
visibleGPUs.delete(id);
|
||||
} else {
|
||||
visibleGPUs.add(id);
|
||||
}
|
||||
renderTable();
|
||||
}
|
||||
|
||||
function setMetric(metric) {
|
||||
currentMetric = metric;
|
||||
|
||||
// Update buttons
|
||||
document.querySelectorAll('.controls .btn').forEach(btn => btn.classList.remove('active'));
|
||||
event.target.classList.add('active');
|
||||
|
||||
// Show/Hide Weight Controls
|
||||
const wControls = document.getElementById('weight-controls');
|
||||
if (metric === 'composite') {
|
||||
wControls.style.display = 'block';
|
||||
} else {
|
||||
wControls.style.display = 'none';
|
||||
}
|
||||
|
||||
// Update Label
|
||||
const labels = {
|
||||
'raw': 'Metric: Raw Throughput (Tokens/s)',
|
||||
'price': 'Metric: Performance per Dollar (Tokens/s / $)',
|
||||
'power': 'Metric: Performance per Watt (Tokens/s / W)',
|
||||
'vram': 'Metric: vRAM Value (Megabytes per Dollar)',
|
||||
'composite': 'Metric: Weighted Composite Score'
|
||||
};
|
||||
document.getElementById('current-metric-display').textContent = labels[metric];
|
||||
|
||||
renderTable();
|
||||
}
|
||||
|
||||
function calculateValue(rawScore, gpuId, rowModelName) {
|
||||
const gpu = GPUS.find(g => g.id === gpuId);
|
||||
if (!gpu) return 0;
|
||||
|
||||
// For vRAM metric, we don't strictly require rawScore > 0 to have a value,
|
||||
// BUT we want to mask it if the model can't run.
|
||||
// For other metrics, we need rawScore.
|
||||
if (!rawScore && currentMetric !== 'composite' && currentMetric !== 'vram') return 0;
|
||||
|
||||
// Strict masking for vRAM: If throughput is 0, value is 0 (useless for this model)
|
||||
if (currentMetric === 'vram') {
|
||||
if (!rawScore || rawScore <= 0) return 0;
|
||||
// Return MB per Dollar
|
||||
return (gpu.vram * 1024) / gpu.price;
|
||||
}
|
||||
|
||||
if (currentMetric === 'price') {
|
||||
return rawScore / gpu.price;
|
||||
} else if (currentMetric === 'power') {
|
||||
return rawScore / gpu.tdp;
|
||||
} else if (currentMetric === 'composite') {
|
||||
// Weights
|
||||
const wPrice = parseFloat(document.getElementById('w-price').value) || 0;
|
||||
const wPower = parseFloat(document.getElementById('w-power').value) || 0;
|
||||
const wVram = parseFloat(document.getElementById('w-vram').value) || 0;
|
||||
const totalWeight = wPrice + wPower + wVram;
|
||||
|
||||
if (totalWeight === 0) return 0;
|
||||
|
||||
// We need normalization contexts (Max values for the row/category)
|
||||
const row = rawData.find(r => r.model_name === rowModelName);
|
||||
if (!row) return 0;
|
||||
|
||||
// 1. Calculate raw metrics for ALL VISIBLE GPUs to find maxima
|
||||
let maxPriceMet = 0;
|
||||
let maxPowerMet = 0;
|
||||
let maxVramMet = 0;
|
||||
|
||||
GPUS.forEach(g => {
|
||||
if (!visibleGPUs.has(g.id)) return;
|
||||
|
||||
// Perf metrics
|
||||
const rs = row.gpus[g.id] || 0;
|
||||
|
||||
if (rs > 0) {
|
||||
const pMet = rs / g.price;
|
||||
const wMet = rs / g.tdp;
|
||||
if (pMet > maxPriceMet) maxPriceMet = pMet;
|
||||
if (wMet > maxPowerMet) maxPowerMet = wMet;
|
||||
}
|
||||
|
||||
// vRAM metrics (Static per GPU) - Normalize as MB/$
|
||||
const vMet = (g.vram * 1024) / g.price;
|
||||
if (vMet > maxVramMet) maxVramMet = vMet;
|
||||
});
|
||||
|
||||
if (rawScore <= 0) return 0;
|
||||
|
||||
// 2. Normalize THIS gpu's scores
|
||||
const myPriceMet = rawScore / gpu.price;
|
||||
const myPowerMet = rawScore / gpu.tdp;
|
||||
const myVramMet = (gpu.vram * 1024) / gpu.price;
|
||||
|
||||
const normPrice = maxPriceMet > 0 ? (myPriceMet / maxPriceMet) : 0;
|
||||
const normPower = maxPowerMet > 0 ? (myPowerMet / maxPowerMet) : 0;
|
||||
const normVram = maxVramMet > 0 ? (myVramMet / maxVramMet) : 0;
|
||||
|
||||
// 3. Weighted Average
|
||||
const composite = (wPrice * normPrice + wPower * normPower + wVram * normVram) / totalWeight;
|
||||
|
||||
return composite * 100; // Scale 0-100
|
||||
}
|
||||
return rawScore;
|
||||
}
|
||||
|
||||
function formatValue(val) {
|
||||
if (val === 0 || val === undefined || isNaN(val)) return "-";
|
||||
|
||||
if (currentMetric === 'raw') return Math.round(val).toLocaleString();
|
||||
if (currentMetric === 'price') return val.toFixed(2);
|
||||
if (currentMetric === 'power') return val.toFixed(2);
|
||||
if (currentMetric === 'vram') return Math.round(val) + " MB/$"; // Integer MB/$
|
||||
if (currentMetric === 'composite') return Math.round(val);
|
||||
return val;
|
||||
}
|
||||
|
||||
function sortData() {
|
||||
return [...rawData].sort((a, b) => {
|
||||
let valA = 0;
|
||||
let valB = 0;
|
||||
|
||||
// If sorting by Model Name
|
||||
if (sortCol === 'Model') {
|
||||
return sortDesc ? b.model_name.localeCompare(a.model_name) : a.model_name.localeCompare(b.model_name);
|
||||
}
|
||||
|
||||
// If sorting by GPU Value
|
||||
const rawA = a.gpus[sortCol] || 0;
|
||||
const rawB = b.gpus[sortCol] || 0;
|
||||
|
||||
valA = calculateValue(rawA, sortCol, a.model_name);
|
||||
valB = calculateValue(rawB, sortCol, b.model_name);
|
||||
|
||||
return sortDesc ? valB - valA : valA - valB;
|
||||
});
|
||||
}
|
||||
|
||||
function handleHeaderClick(colId) {
|
||||
if (sortCol === colId) {
|
||||
sortDesc = !sortDesc;
|
||||
} else {
|
||||
sortCol = colId;
|
||||
sortDesc = true;
|
||||
}
|
||||
renderTable();
|
||||
}
|
||||
|
||||
function renderTable() {
|
||||
const thead = document.getElementById('table-header');
|
||||
const tbody = document.getElementById('table-body');
|
||||
const sortedData = sortData();
|
||||
|
||||
const activeGPUS = GPUS.filter(g => visibleGPUs.has(g.id));
|
||||
|
||||
// 1. Render Header
|
||||
let headerHTML = `<th onclick="handleHeaderClick('Model')">Model ${sortCol === 'Model' ? (sortDesc ? '↓' : '↑') : ''}</th>`;
|
||||
|
||||
activeGPUS.forEach(gpu => {
|
||||
const isSorted = sortCol === gpu.id;
|
||||
const arrow = isSorted ? (sortDesc ? '↓' : '↑') : '';
|
||||
const formatArrow = `<span style="margin-left:4px; opacity: ${isSorted ? 1 : 0.3};"> ${arrow || '↕'}</span>`;
|
||||
|
||||
const amdClass = gpu.id === 'AMD R9700' ? 'amd-col-header' : '';
|
||||
const subText = `$${gpu.price.toLocaleString()} | ${gpu.tdp}W`;
|
||||
|
||||
headerHTML += `
|
||||
<th class="${amdClass}" onclick="handleHeaderClick('${gpu.id}')">
|
||||
<div>${gpu.name} ${formatArrow}</div>
|
||||
<span class="gpu-sub">${subText}</span>
|
||||
</th>
|
||||
`;
|
||||
});
|
||||
thead.innerHTML = headerHTML;
|
||||
|
||||
// 2. Render Rows
|
||||
tbody.innerHTML = sortedData.map(row => {
|
||||
let rowHTML = `<tr>
|
||||
<td>
|
||||
<span class="model-name">${row.model_name}</span>
|
||||
<!-- <span class="model-meta">Info/Meta</span> -->
|
||||
</td>`;
|
||||
|
||||
// Calculate all values for this row to determine Rankings
|
||||
const rowValues = [];
|
||||
activeGPUS.forEach(gpu => {
|
||||
const rawVal = row.gpus[gpu.id];
|
||||
const val = calculateValue(rawVal, gpu.id, row.model_name);
|
||||
rowValues.push({ id: gpu.id, val: val });
|
||||
});
|
||||
|
||||
// Sort to find ranks (descending)
|
||||
const sortedValues = [...rowValues].sort((a, b) => b.val - a.val);
|
||||
|
||||
// Map GPU ID to Rank (1, 2, 3...)
|
||||
const ranks = {};
|
||||
sortedValues.forEach((item, index) => {
|
||||
if (item.val > 0) {
|
||||
ranks[item.id] = index + 1;
|
||||
}
|
||||
});
|
||||
|
||||
activeGPUS.forEach(gpu => {
|
||||
const rawVal = row.gpus[gpu.id];
|
||||
const val = calculateValue(rawVal, gpu.id, row.model_name);
|
||||
const rank = ranks[gpu.id];
|
||||
|
||||
let cellClass = 'val-cell';
|
||||
let badgeHTML = '';
|
||||
|
||||
// Only rank if we have enough competitors (>1)
|
||||
if (sortedValues.length > 1) {
|
||||
if (rank === 1) {
|
||||
cellClass += ' winner';
|
||||
badgeHTML = '<span class="rank-badge rank-1">1</span>';
|
||||
} else if (rank === 2) {
|
||||
badgeHTML = '<span class="rank-badge rank-2">2</span>';
|
||||
} else if (rank === 3) {
|
||||
badgeHTML = '<span class="rank-badge rank-3">3</span>';
|
||||
}
|
||||
}
|
||||
|
||||
rowHTML += `<td class="${cellClass}">
|
||||
<div class="rank-cell-content">
|
||||
${badgeHTML}
|
||||
<span>${formatValue(val)}</span>
|
||||
</div>
|
||||
</td>`;
|
||||
});
|
||||
|
||||
rowHTML += `</tr>`;
|
||||
return rowHTML;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// Initialize
|
||||
loadData();
|
||||
|
||||
// Modal Functions
|
||||
function openPriceModal() {
|
||||
const container = document.getElementById('price-inputs-container');
|
||||
container.innerHTML = '';
|
||||
|
||||
GPUS.forEach((gpu, index) => {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'price-input-group';
|
||||
div.innerHTML = `
|
||||
<label style="font-weight: 500; color: var(--text-main);">${gpu.name}</label>
|
||||
<div style="display:flex; align-items:center; gap:4px;">
|
||||
<span style="color:var(--text-muted);">$</span>
|
||||
<input type="number" id="price-input-${index}" value="${gpu.price}" min="0">
|
||||
</div>
|
||||
`;
|
||||
container.appendChild(div);
|
||||
});
|
||||
|
||||
document.getElementById('price-modal').style.display = 'block';
|
||||
}
|
||||
|
||||
function closePriceModal() {
|
||||
document.getElementById('price-modal').style.display = 'none';
|
||||
}
|
||||
|
||||
function savePrices() {
|
||||
GPUS.forEach((gpu, index) => {
|
||||
const input = document.getElementById(`price-input-${index}`);
|
||||
if (input) {
|
||||
const newPrice = parseFloat(input.value);
|
||||
if (!isNaN(newPrice) && newPrice > 0) {
|
||||
gpu.price = newPrice;
|
||||
// Update default subtext in header if we wanted to rebuild header dynamically
|
||||
// (renderTable does this already)
|
||||
}
|
||||
}
|
||||
});
|
||||
closePriceModal();
|
||||
renderTable(); // Re-render to show updated calculations
|
||||
}
|
||||
|
||||
// Close modal when clicking outside
|
||||
window.onclick = function (event) {
|
||||
const modal = document.getElementById('price-modal');
|
||||
if (event.target == modal) {
|
||||
modal.style.display = "none";
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,54 @@
|
||||
[
|
||||
{
|
||||
"model_name": "RedHatAI/Qwen3-14B-FP8-dynamic",
|
||||
"model_clean": "RedHatAI_Qwen3-14B-FP8-dynamic",
|
||||
"gpus": {
|
||||
"AMD R9700": 1360.3779166371198,
|
||||
"AMD R9700 (UV+PL)": 1484.1354103024892,
|
||||
"NVIDIA RTX 3090": 512.5391321707765,
|
||||
"NVIDIA RTX 4090": 832.9600726881711,
|
||||
"NVIDIA RTX 5090": 3251.0147985872827,
|
||||
"NVIDIA RTX 5000 Ada": 1826.0356413838635,
|
||||
"NVIDIA A100": 2984.3323175624723
|
||||
}
|
||||
},
|
||||
{
|
||||
"model_name": "cpatonn/Qwen3-Coder-30B-A3B-Instruct-GPTQ-4bit",
|
||||
"model_clean": "cpatonn_Qwen3-Coder-30B-A3B-Instruct-GPTQ-4bit",
|
||||
"gpus": {
|
||||
"AMD R9700": 1019.986034633913,
|
||||
"AMD R9700 (UV+PL)": 1089.8731231693348,
|
||||
"NVIDIA RTX 3090": 2417.170549559942,
|
||||
"NVIDIA RTX 4090": 3441.9784198061966,
|
||||
"NVIDIA RTX 5090": 5550.512371624096,
|
||||
"NVIDIA RTX 5000 Ada": 2407.9706011923054,
|
||||
"NVIDIA A100": 3719.016081057382
|
||||
}
|
||||
},
|
||||
{
|
||||
"model_name": "meta-llama/Meta-Llama-3.1-8B-Instruct",
|
||||
"model_clean": "meta-llama_Meta-Llama-3.1-8B-Instruct",
|
||||
"gpus": {
|
||||
"AMD R9700": 1795.5908237208996,
|
||||
"AMD R9700 (UV+PL)": 2077.5827013698454,
|
||||
"NVIDIA RTX 3090": 1830.1667164087519,
|
||||
"NVIDIA RTX 4090": 2917.0500237249767,
|
||||
"NVIDIA RTX 5090": 3197.1386468735236,
|
||||
"NVIDIA RTX 5000 Ada": 2149.8297795990215,
|
||||
"NVIDIA A100": 3771.8246502543934
|
||||
}
|
||||
},
|
||||
{
|
||||
"model_name": "openai/gpt-oss-20b",
|
||||
"model_clean": "openai_gpt-oss-20b",
|
||||
"gpus": {
|
||||
"AMD R9700": 1095.31715306375,
|
||||
"AMD R9700 (UV+PL)": 1225.6785068919007,
|
||||
"NVIDIA RTX 3090": 2066.190202557434,
|
||||
"NVIDIA RTX 4090": 2685.5060579349843,
|
||||
"NVIDIA RTX 5090": 6896.704477650824,
|
||||
"NVIDIA RTX 5000 Ada": 2883.1478405820326,
|
||||
"NVIDIA A100": 4726.040550232053
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,107 @@
|
||||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
# Configuration
|
||||
SCRIPT_DIR = Path(__file__).parent
|
||||
PROJECT_ROOT = SCRIPT_DIR.parent
|
||||
BENCHMARKS_ROOT = PROJECT_ROOT / "benchmarks"
|
||||
OUTPUT_FILE = SCRIPT_DIR / "comparison_results.json"
|
||||
|
||||
# GPU Mapping
|
||||
GPU_MAP = {
|
||||
"benchmark_results_amd-r9700": "AMD R9700",
|
||||
"benchmark_results_amd-r9700-uv+pl": "AMD R9700 (UV+PL)",
|
||||
"benchmark_results_nvidia-3090": "NVIDIA RTX 3090",
|
||||
"benchmark_results_nvidia-4090": "NVIDIA RTX 4090",
|
||||
"benchmark_results_nvidia-5090": "NVIDIA RTX 5090",
|
||||
"benchmark_results_nvidia-ada5000": "NVIDIA RTX 5000 Ada",
|
||||
"benchmark_results_nvidia-a100": "NVIDIA A100"
|
||||
}
|
||||
|
||||
def parse_model_name(clean_name):
|
||||
"""
|
||||
Extracts a pretty display name and basic metadata from the filename-safe model string.
|
||||
"""
|
||||
# Example: meta-llama_Meta-Llama-3.1-8B-Instruct -> meta-llama/Meta-Llama-3.1-8B-Instruct
|
||||
# Example: RedHatAI_Qwen3-14B-FP8-dynamic -> RedHatAI/Qwen3-14B-FP8-dynamic
|
||||
|
||||
# Simple heuristic: replace first underscore with slash to restore Org/Model format
|
||||
if "_" in clean_name:
|
||||
display_name = clean_name.replace("_", "/", 1)
|
||||
else:
|
||||
display_name = clean_name
|
||||
|
||||
return display_name
|
||||
|
||||
def analyze_benchmarks():
|
||||
results = {}
|
||||
|
||||
if not BENCHMARKS_ROOT.exists():
|
||||
print(f"Error: {BENCHMARKS_ROOT} not found.")
|
||||
return
|
||||
|
||||
print(f"Scanning {BENCHMARKS_ROOT}...")
|
||||
|
||||
for folder_name, gpu_display_name in GPU_MAP.items():
|
||||
folder_path = BENCHMARKS_ROOT / folder_name
|
||||
if not folder_path.exists():
|
||||
print(f"Warning: {folder_path} not found, skipping.")
|
||||
continue
|
||||
|
||||
print(f"Processing {gpu_display_name}...")
|
||||
|
||||
# We only care about throughput.json files for "Tokens/s" comparison
|
||||
# And STRICTLY tp1 (Single GPU)
|
||||
for json_file in folder_path.glob("*_tp1_throughput.json"):
|
||||
try:
|
||||
data = json.loads(json_file.read_text())
|
||||
except json.JSONDecodeError:
|
||||
print(f" Skipping invalid JSON: {json_file.name}")
|
||||
continue
|
||||
|
||||
# Extract basic info from filename
|
||||
# Format: {model_clean}_tp1_throughput.json
|
||||
filename = json_file.name
|
||||
|
||||
# Remove suffix to get model_clean
|
||||
model_clean = filename.replace("_tp1_throughput.json", "")
|
||||
|
||||
# Get metric
|
||||
tokens_per_sec = data.get("tokens_per_second", 0)
|
||||
if not tokens_per_sec:
|
||||
continue
|
||||
|
||||
# Store in results structure
|
||||
# Structure: results[model_clean] = { "display_name": "...", "gpus": { "AMD R9700": 123.4, ... } }
|
||||
|
||||
if model_clean not in results:
|
||||
results[model_clean] = {
|
||||
"model_name": parse_model_name(model_clean),
|
||||
"model_clean": model_clean,
|
||||
"gpus": {}
|
||||
}
|
||||
|
||||
results[model_clean]["gpus"][gpu_display_name] = tokens_per_sec
|
||||
|
||||
# Convert to list for easier frontend consumption
|
||||
final_output = []
|
||||
for model_key, info in results.items():
|
||||
# Only include if we have at least TWO data points for comparison
|
||||
# (User requested removal of single-GPU only models)
|
||||
if len(info["gpus"]) < 2:
|
||||
continue
|
||||
|
||||
final_output.append(info)
|
||||
|
||||
# Sort by model name for consistency
|
||||
final_output.sort(key=lambda x: x["model_name"])
|
||||
|
||||
print(f"Found data for {len(final_output)} models.")
|
||||
|
||||
with open(OUTPUT_FILE, "w") as f:
|
||||
json.dump(final_output, f, indent=2)
|
||||
print(f"Saved to {OUTPUT_FILE}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
analyze_benchmarks()
|
||||
+885
@@ -0,0 +1,885 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>AMD R9700 vLLM Benchmarks</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg-body: #f9fafb;
|
||||
--bg-card: #ffffff;
|
||||
--text-main: #111827;
|
||||
--text-muted: #6b7280;
|
||||
--border: #e5e7eb;
|
||||
--primary: #ef4444;
|
||||
/* AMD Red-ish */
|
||||
--primary-bg: #fef2f2;
|
||||
--font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--bg-body);
|
||||
color: var(--text-main);
|
||||
font-family: var(--font-sans);
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1000px;
|
||||
margin: 20px auto;
|
||||
}
|
||||
|
||||
/* Header */
|
||||
header {
|
||||
margin-bottom: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2.25rem;
|
||||
font-weight: 800;
|
||||
margin: 0 0 10px 0;
|
||||
letter-spacing: -0.05rem;
|
||||
}
|
||||
|
||||
p.subtitle {
|
||||
color: var(--text-muted);
|
||||
font-size: 1.1rem;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Controls */
|
||||
.controls {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
margin-bottom: 24px;
|
||||
background: var(--bg-card);
|
||||
padding: 16px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
|
||||
border: 1px solid var(--border);
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
input[type="text"],
|
||||
select {
|
||||
padding: 10px 14px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
font-size: 0.95rem;
|
||||
outline: none;
|
||||
transition: border-color 0.15s;
|
||||
}
|
||||
|
||||
input[type="text"]:focus,
|
||||
select:focus {
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 0 0 2px var(--primary-bg);
|
||||
}
|
||||
|
||||
.search {
|
||||
flex: 1;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
/* Section Cards */
|
||||
.section-card {
|
||||
background: var(--bg-card);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
|
||||
border: 1px solid var(--border);
|
||||
margin-bottom: 32px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
background: #fcfcfc;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.section-header h2 {
|
||||
margin: 0;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Table */
|
||||
.table-responsive {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
padding: 8px 12px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
th {
|
||||
background: #f9fafb;
|
||||
color: var(--text-muted);
|
||||
font-weight: 600;
|
||||
font-size: 0.75rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
/* Columns */
|
||||
.col-model {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.col-data {
|
||||
text-align: right;
|
||||
width: 1%;
|
||||
white-space: nowrap;
|
||||
font-family: var(--font-mono);
|
||||
font-feature-settings: "tnum";
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
/* Model Cell Styling */
|
||||
.model-cell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.model-name {
|
||||
font-weight: 600;
|
||||
color: var(--text-main);
|
||||
}
|
||||
|
||||
.model-meta {
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-muted);
|
||||
margin-top: 4px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Tags */
|
||||
.tag {
|
||||
display: inline-block;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
background: #f3f4f6;
|
||||
color: #4b5563;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Data Styling */
|
||||
.val {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.val-na {
|
||||
color: #d1d5db;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.highlight {
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
/* Modal/Overlay */
|
||||
#loading {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* Modal Styles */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 1000;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.modal-overlay.active {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.modal {
|
||||
background: var(--bg-card);
|
||||
width: 90%;
|
||||
max-width: 600px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: 85vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
padding: 20px 24px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background: #f9fafb;
|
||||
}
|
||||
|
||||
.modal-header h3 {
|
||||
margin: 0;
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.5rem;
|
||||
cursor: pointer;
|
||||
color: var(--text-muted);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
padding: 24px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.modal-section {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.modal-section h4 {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 0.9rem;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-muted);
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.modal-section p {
|
||||
margin: 0;
|
||||
font-size: 0.95rem;
|
||||
color: var(--text-main);
|
||||
}
|
||||
|
||||
.code-block {
|
||||
background: #f3f4f6;
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.85rem;
|
||||
color: #374151;
|
||||
margin-top: 8px;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
/* Help Button */
|
||||
.btn-help {
|
||||
background: none;
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text-muted);
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
margin-left: 10px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.btn-help:hover {
|
||||
border-color: var(--primary);
|
||||
color: var(--primary);
|
||||
background: var(--primary-bg);
|
||||
}
|
||||
|
||||
.section-title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.section-desc {
|
||||
color: var(--text-muted);
|
||||
font-size: 0.9rem;
|
||||
font-weight: 400;
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
footer {
|
||||
margin-top: 60px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid var(--border);
|
||||
color: var(--text-muted);
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.sys-config {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
margin-top: 12px;
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
.sys-item {
|
||||
display: grid;
|
||||
grid-template-columns: 140px 1fr;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.sys-label {
|
||||
font-weight: 600;
|
||||
font-size: 0.75rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
/* Tabs */
|
||||
.tab-nav {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 24px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
padding-bottom: 0px;
|
||||
}
|
||||
|
||||
.tab-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 12px 20px;
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
border-bottom: 2px solid transparent;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.tab-btn:hover {
|
||||
color: var(--text-main);
|
||||
}
|
||||
|
||||
.tab-btn.active {
|
||||
color: var(--primary);
|
||||
border-bottom-color: var(--primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Filter Checkboxes */
|
||||
.filter-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 0 12px;
|
||||
border-left: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.check-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 0.9rem;
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.check-item input[type="checkbox"] {
|
||||
accent-color: var(--primary);
|
||||
}
|
||||
|
||||
.check-item:hover {
|
||||
color: var(--text-main);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<div class="container">
|
||||
<header>
|
||||
<h1>AMD R9700 AI PRO vLLM Benchmarks</h1>
|
||||
<p class="subtitle">Performance comparison on vLLM</p>
|
||||
<p style="margin: 4px 0 0 0; font-size: 0.9rem;">
|
||||
<a href="https://github.com/kyuz0/amd-r9700-vllm-toolboxes/" target="_blank"
|
||||
style="color: var(--primary); text-decoration: none;">View on GitHub →</a>
|
||||
<span style="margin: 0 8px; color: var(--border);">|</span>
|
||||
<a href="compare.html"
|
||||
style="color: var(--text-muted); text-decoration: none; border-bottom: 1px dotted var(--text-muted);">
|
||||
Compare vs NVIDIA & Tuned Results
|
||||
</a>
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<div class="controls">
|
||||
<input type="text" id="searchInput" class="search" placeholder="Search models (e.g. 'llama', 'fp8')..."
|
||||
autocomplete="off">
|
||||
<select id="quantFilter">
|
||||
<option value="">All Quantizations</option>
|
||||
</select>
|
||||
<div class="filter-group">
|
||||
<span style="font-size:0.8rem; font-weight:600; color:var(--text-muted); margin-right:6px;">GPUs:</span>
|
||||
<label class="check-item"><input type="checkbox" id="chkTP1" checked> TP1</label>
|
||||
<label class="check-item"><input type="checkbox" id="chkTP2" checked> TP2</label>
|
||||
</div>
|
||||
|
||||
<div class="filter-group">
|
||||
<span
|
||||
style="font-size:0.8rem; font-weight:600; color:var(--text-muted); margin-right:6px;">Attention:</span>
|
||||
<label class="check-item"><input type="checkbox" id="chkTriton" checked> Triton</label>
|
||||
<label class="check-item"><input type="checkbox" id="chkROCm"> ROCm</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav id="tabNav" class="tab-nav">
|
||||
<!-- Dynamic Tabs -->
|
||||
</nav>
|
||||
|
||||
<div id="dashboard">
|
||||
<div id="loading">Loading benchmark results...</div>
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
<div style="font-weight: 600; margin-bottom: 8px;">System Configuration</div>
|
||||
<div class="sys-config">
|
||||
<div class="sys-item">
|
||||
<span class="sys-label">GPUs</span>
|
||||
<span>2x AMD Radeon AI PRO R9700 (64GB Total VRAM)</span>
|
||||
</div>
|
||||
<div class="sys-item">
|
||||
<span class="sys-label">CPU</span>
|
||||
<span>AMD Ryzen 9 9900X3D</span>
|
||||
</div>
|
||||
<div class="sys-item">
|
||||
<span class="sys-label">Motherboard</span>
|
||||
<span>ASRock X870E Taichi</span>
|
||||
</div>
|
||||
<div class="sys-item">
|
||||
<span class="sys-label">RAM</span>
|
||||
<span>64GB Crucial Pro DDR5</span>
|
||||
</div>
|
||||
<div class="sys-item">
|
||||
<span class="sys-label">OS</span>
|
||||
<span>Fedora 43 (6.17.8-300.fc43.x86_64)</span>
|
||||
</div>
|
||||
<div class="sys-item">
|
||||
<span class="sys-label">ROCm</span>
|
||||
<span>7.2.53150 (Nightly Build)</span>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<!-- Modal Overlay -->
|
||||
<div id="modalOverlay" class="modal-overlay">
|
||||
<div class="modal">
|
||||
<div class="modal-header">
|
||||
<h3 id="modalTitle">Benchmark Info</h3>
|
||||
<button class="modal-close" onclick="closeModal()">×</button>
|
||||
</div>
|
||||
<div class="modal-body" id="modalContent">
|
||||
<!-- Dynamic Content -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// State
|
||||
let rawRuns = [];
|
||||
let tests = [];
|
||||
let state = {
|
||||
search: "",
|
||||
quant: "",
|
||||
activeTab: "Throughput",
|
||||
view: {
|
||||
tp1: true,
|
||||
tp2: true,
|
||||
triton: true,
|
||||
rocm: false
|
||||
}
|
||||
};
|
||||
|
||||
// Benchmark Metadata
|
||||
const BENCHMARK_INFO = {
|
||||
"Throughput": {
|
||||
short: "Maximum raw compute capacity (Tokens/Sec).",
|
||||
desc: "Measures the absolute maximum number of tokens the system can generate per second by fully saturating the GPU compute capability.",
|
||||
usecase: "Demonstrates the raw horsepower and architectural efficiency of the hardware/model combo under Heavy Load. This is the theoretical speed limit of the system.",
|
||||
details: "Command: `vllm bench throughput`\nParams: --num-prompts 100 --output-len 512\nMetric: Tokens per Second (higher is better).",
|
||||
unit: " tok/s"
|
||||
},
|
||||
"TTFT": {
|
||||
short: "Time To First Token (Response Latency).",
|
||||
desc: "The 'Time To First Token' is the delay between sending a request and seeing the first character of the response.",
|
||||
usecase: "<b>Responsiveness</b>. Low TTFT makes the AI feel 'snappy' and instant. High TTFT feels like the AI is ignoring you or lagging. We measure at different QPS loads to ensure the server doesn't 'choke' when busy.",
|
||||
context: "<b>QPS = Queries Per Second (Traffic Load)</b>.<br>• QPS 1.0 = 1 user sending a request every second.<br>• QPS 4.0 = 4 users sending requests every second (Simulates High Load).",
|
||||
details: "Command: `vllm bench serve`\nParams: --random-input-len 1024 --random-output-len 512\nMetric: Milliseconds (lower is better).",
|
||||
unit: " ms"
|
||||
},
|
||||
"TPOT": {
|
||||
short: "Time Per Output Token (Streaming Speed).",
|
||||
desc: "The 'Time Per Output Token' measures how fast the text generates *after* the first token appears.",
|
||||
usecase: "<b>1. Fluidity</b>: Industry standard is <50ms (>20 tok/s) for a 'fluid' feeling. Slower feels laggy.<br><b>2. Bottlenecks</b>: We test at <b>QPS 4.0</b> to find memory bandwidth bottlenecks where the GPU can't keep up with multiple users.",
|
||||
context: "<b>QPS = Queries Per Second (Traffic Load)</b>.<br>• QPS 1.0 = Light Load (Ideal conditions)<br>• QPS 4.0 = Heavy Load (Stress Test)",
|
||||
details: "Command: `vllm bench serve`\nParams: --random-input-len 1024 --random-output-len 512\nMetric: Milliseconds (lower is better).",
|
||||
unit: " ms"
|
||||
}
|
||||
};
|
||||
|
||||
const $ = id => document.getElementById(id);
|
||||
|
||||
async function init() {
|
||||
try {
|
||||
const res = await fetch('results.json');
|
||||
const data = await res.json();
|
||||
rawRuns = data.runs || [];
|
||||
processData();
|
||||
render();
|
||||
populateFilters();
|
||||
} catch (e) {
|
||||
$('loading').textContent = "Error loading results.json: " + e.message;
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
function processData() {
|
||||
const testGroups = {};
|
||||
|
||||
rawRuns.forEach(run => {
|
||||
if (!run.test) return;
|
||||
if (!testGroups[run.test]) {
|
||||
testGroups[run.test] = {
|
||||
name: run.test,
|
||||
models: {}
|
||||
};
|
||||
}
|
||||
|
||||
// Normalize model name
|
||||
const modelName = run.model_clean || run.model;
|
||||
|
||||
if (!testGroups[run.test].models[modelName]) {
|
||||
testGroups[run.test].models[modelName] = {
|
||||
name: modelName,
|
||||
quant: run.quant,
|
||||
params: run.params_b || run.name_params_b,
|
||||
tp1: null,
|
||||
tp1_rocm: null,
|
||||
tp2: null,
|
||||
tp2_rocm: null
|
||||
};
|
||||
}
|
||||
|
||||
const m = testGroups[run.test].models[modelName];
|
||||
|
||||
// Assign TP value
|
||||
if (run.env === "TP1") {
|
||||
if (run.variant === "rocm") m.tp1_rocm = run.tps_mean;
|
||||
else m.tp1 = run.tps_mean;
|
||||
}
|
||||
if (run.env === "TP2") {
|
||||
if (run.variant === "rocm") m.tp2_rocm = run.tps_mean;
|
||||
else m.tp2 = run.tps_mean;
|
||||
}
|
||||
});
|
||||
|
||||
// Convert map to array for sorting
|
||||
tests = Object.values(testGroups).map(group => {
|
||||
return {
|
||||
name: group.name,
|
||||
models: Object.values(group.models)
|
||||
};
|
||||
});
|
||||
|
||||
// Sort tests: Throughput first, then others alphabetically
|
||||
tests.sort((a, b) => {
|
||||
if (a.name === "Throughput") return -1;
|
||||
if (b.name === "Throughput") return 1;
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
|
||||
// Set default tab if not set
|
||||
if (!state.activeTab && tests.length > 0) {
|
||||
state.activeTab = tests[0].name;
|
||||
}
|
||||
}
|
||||
|
||||
function populateFilters() {
|
||||
const quants = new Set(rawRuns.map(r => r.quant).filter(Boolean));
|
||||
const sel = $('quantFilter');
|
||||
[...quants].sort().forEach(q => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = q;
|
||||
opt.textContent = q;
|
||||
sel.appendChild(opt);
|
||||
});
|
||||
|
||||
$('searchInput').addEventListener('input', e => {
|
||||
state.search = e.target.value.toLowerCase();
|
||||
render();
|
||||
});
|
||||
|
||||
sel.addEventListener('change', e => {
|
||||
state.quant = e.target.value;
|
||||
render();
|
||||
});
|
||||
|
||||
const bindCheck = (id, key) => {
|
||||
$(id).addEventListener('change', e => {
|
||||
state.view[key] = e.target.checked;
|
||||
render();
|
||||
});
|
||||
};
|
||||
|
||||
bindCheck('chkTP1', 'tp1');
|
||||
bindCheck('chkTP2', 'tp2');
|
||||
bindCheck('chkTriton', 'triton');
|
||||
bindCheck('chkROCm', 'rocm');
|
||||
}
|
||||
|
||||
function getBenchmarkMeta(testName) {
|
||||
if (testName.includes("Throughput")) return BENCHMARK_INFO["Throughput"];
|
||||
if (testName.includes("TTFT")) return BENCHMARK_INFO["TTFT"];
|
||||
if (testName.includes("TPOT")) return BENCHMARK_INFO["TPOT"];
|
||||
return null;
|
||||
}
|
||||
|
||||
function render() {
|
||||
const container = $('dashboard');
|
||||
const tabNav = $('tabNav');
|
||||
|
||||
// Render Tabs
|
||||
tabNav.innerHTML = "";
|
||||
tests.forEach(test => {
|
||||
const btn = document.createElement('button');
|
||||
btn.className = `tab-btn ${test.name === state.activeTab ? 'active' : ''}`;
|
||||
btn.textContent = test.name;
|
||||
btn.onclick = () => {
|
||||
state.activeTab = test.name;
|
||||
render();
|
||||
};
|
||||
tabNav.appendChild(btn);
|
||||
});
|
||||
|
||||
// Ensure active tab exists (if search filtered it out logic?)
|
||||
// Actually tabs are based on 'tests' which is processed from raw data, so they exist regardless of filters unless we want to hide tabs with no results.
|
||||
// For now, let's keep tabs static based on available data types.
|
||||
|
||||
container.innerHTML = "";
|
||||
|
||||
// Find active test
|
||||
const activeTest = tests.find(t => t.name === state.activeTab);
|
||||
|
||||
if (!activeTest) {
|
||||
// If invalid tab (e.g. on first load if default doesn't exist), switch to first
|
||||
if (tests.length > 0) {
|
||||
state.activeTab = tests[0].name;
|
||||
// Re-render immediately
|
||||
setTimeout(render, 0);
|
||||
}
|
||||
container.innerHTML = '<div id="loading">No data available.</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Render Active Tab Content
|
||||
const test = activeTest;
|
||||
|
||||
// Filter models within this test
|
||||
const models = test.models.filter(m => {
|
||||
const s = state.search;
|
||||
const matchSearch = !s || m.name.toLowerCase().includes(s);
|
||||
|
||||
const q = state.quant;
|
||||
const matchQuant = !q || m.quant === q;
|
||||
|
||||
return matchSearch && matchQuant;
|
||||
});
|
||||
|
||||
if (models.length === 0) {
|
||||
container.innerHTML = '<div id="loading">No models match current filters in this category.</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Sorting models by size (small to large), then name
|
||||
models.sort((a, b) => {
|
||||
const pA = parseFloat(a.params) || 0;
|
||||
const pB = parseFloat(b.params) || 0;
|
||||
if (pA !== pB) return pA - pB;
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
|
||||
const card = document.createElement('div');
|
||||
card.className = "section-card";
|
||||
|
||||
// Metadata resolution
|
||||
const meta = getBenchmarkMeta(test.name);
|
||||
const shortDesc = meta ? `<span class="section-desc">${meta.short}</span>` : "";
|
||||
const helpBtn = meta ? `<button class="btn-help" onclick="openModal('${test.name}')">?</button>` : "";
|
||||
|
||||
const header = document.createElement('div');
|
||||
header.className = "section-header";
|
||||
header.innerHTML = `
|
||||
<div class="section-title-row">
|
||||
<h2>${test.name}</h2>
|
||||
${helpBtn}
|
||||
</div>
|
||||
${shortDesc}
|
||||
`;
|
||||
card.appendChild(header);
|
||||
|
||||
const tableResp = document.createElement('div');
|
||||
tableResp.className = "table-responsive";
|
||||
|
||||
const table = document.createElement('table');
|
||||
const thead = document.createElement('thead');
|
||||
|
||||
// Build dynamic headers
|
||||
let headerHTML = '<tr><th class="col-model">Model</th>';
|
||||
|
||||
if (state.view.tp1) {
|
||||
if (state.view.triton) headerHTML += '<th class="col-data">TP1<br><span style="font-weight:400; font-size:0.85em; text-transform:none;">Triton</span></th>';
|
||||
if (state.view.rocm) headerHTML += '<th class="col-data" style="background:#fefce8;">TP1<br><span style="font-weight:400; font-size:0.85em; text-transform:none;">ROCm</span></th>';
|
||||
}
|
||||
if (state.view.tp2) {
|
||||
if (state.view.triton) headerHTML += '<th class="col-data">TP2<br><span style="font-weight:400; font-size:0.85em; text-transform:none;">Triton</span></th>';
|
||||
if (state.view.rocm) headerHTML += '<th class="col-data" style="background:#fefce8;">TP2<br><span style="font-weight:400; font-size:0.85em; text-transform:none;">ROCm</span></th>';
|
||||
}
|
||||
|
||||
headerHTML += '</tr>';
|
||||
thead.innerHTML = headerHTML;
|
||||
table.appendChild(thead);
|
||||
|
||||
const tbody = document.createElement('tbody');
|
||||
models.forEach(m => {
|
||||
const tr = document.createElement('tr');
|
||||
|
||||
// Meta tags
|
||||
let metaHtml = "";
|
||||
if (m.quant) metaHtml += `<span class="tag">${m.quant}</span>`;
|
||||
if (m.params) metaHtml += `<span class="tag">${m.params}B</span>`;
|
||||
|
||||
// Values
|
||||
const unit = meta ? meta.unit : "";
|
||||
|
||||
let rowHTML = `
|
||||
<td>
|
||||
<div class="model-cell">
|
||||
<a href="https://huggingface.co/${m.name}" target="_blank" class="model-name" style="text-decoration: none; color: inherit; border-bottom: 1px dotted #ccc;">${m.name}</a>
|
||||
<div class="model-meta">${metaHtml}</div>
|
||||
</div>
|
||||
</td>`;
|
||||
|
||||
if (state.view.tp1) {
|
||||
if (state.view.triton) rowHTML += `<td class="col-data">${formatVal(m.tp1, unit)}</td>`;
|
||||
if (state.view.rocm) rowHTML += `<td class="col-data" style="background:#fffbeb;">${formatVal(m.tp1_rocm, unit)}</td>`;
|
||||
}
|
||||
if (state.view.tp2) {
|
||||
if (state.view.triton) rowHTML += `<td class="col-data">${formatVal(m.tp2, unit)}</td>`;
|
||||
if (state.view.rocm) rowHTML += `<td class="col-data" style="background:#fffbeb;">${formatVal(m.tp2_rocm, unit)}</td>`;
|
||||
}
|
||||
|
||||
tr.innerHTML = rowHTML;
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
table.appendChild(tbody);
|
||||
tableResp.appendChild(table);
|
||||
card.appendChild(tableResp);
|
||||
|
||||
container.appendChild(card);
|
||||
}
|
||||
|
||||
function formatVal(v, unit) {
|
||||
if (v === null || v === undefined) return '<span class="val-na" style="color:var(--text-muted); opacity:0.5;">X</span>';
|
||||
if (v === 0) return '<span class="val-na" style="color:var(--primary); font-weight:600;">FAIL</span>';
|
||||
return `<span class="val">${v.toFixed(2)}<span style="font-size:0.8em; color:#888;">${unit}</span></span>`;
|
||||
}
|
||||
|
||||
// Modal Logic
|
||||
function openModal(testName) {
|
||||
const meta = getBenchmarkMeta(testName);
|
||||
if (!meta) return;
|
||||
|
||||
$('modalTitle').textContent = testName;
|
||||
|
||||
let content = `
|
||||
<div class="modal-section">
|
||||
<h4>What is this?</h4>
|
||||
<p>${meta.desc}</p>
|
||||
</div>
|
||||
<div class="modal-section">
|
||||
<h4>Why it matters?</h4>
|
||||
<p>${meta.usecase}</p>
|
||||
</div>`;
|
||||
|
||||
if (meta.context) {
|
||||
content += `
|
||||
<div class="modal-section">
|
||||
<h4>Terminology</h4>
|
||||
<p>${meta.context}</p>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
content += `
|
||||
<div class="modal-section">
|
||||
<h4>Technical Details</h4>
|
||||
<div class="code-block">${meta.details}</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
$('modalContent').innerHTML = content;
|
||||
$('modalOverlay').classList.add('active');
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
$('modalOverlay').classList.remove('active');
|
||||
}
|
||||
|
||||
// Close on click outside
|
||||
$('modalOverlay').addEventListener('click', e => {
|
||||
if (e.target === $('modalOverlay')) closeModal();
|
||||
});
|
||||
|
||||
// Close on Escape
|
||||
document.addEventListener('keydown', e => {
|
||||
if (e.key === "Escape") closeModal();
|
||||
});
|
||||
|
||||
init();
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,139 @@
|
||||
|
||||
import os
|
||||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
# Config
|
||||
BENCHMARK_DIR = Path("../benchmarks/benchmark_results_amd-r9700")
|
||||
OUTPUT_FILE = Path("results.json")
|
||||
|
||||
# Regex to parse model name for quantization and parameters
|
||||
# Examples:
|
||||
# "meta-llama/Meta-Llama-3.1-8B-Instruct"
|
||||
# "cpatonn/Qwen3-Coder-30B-A3B-Instruct-GPTQ-4bit"
|
||||
# "RedHatAI/Llama-3.1-8B-Instruct-FP8-block"
|
||||
PARAMS_REGEX = r"(\d+(?:\.\d+)?)B"
|
||||
QUANT_REGEX = r"(FP8|AWQ|GPTQ|BF16|4bit|Int4)"
|
||||
|
||||
def extract_meta(model_name):
|
||||
# Params
|
||||
params_match = re.search(PARAMS_REGEX, model_name, re.IGNORECASE)
|
||||
params_b = float(params_match.group(1)) if params_match else None
|
||||
|
||||
# Quant
|
||||
quant_match = re.search(QUANT_REGEX, model_name, re.IGNORECASE)
|
||||
quant = quant_match.group(1).upper() if quant_match else "BF16" # Default assumption if no tag? Or unknown.
|
||||
# Refine quant if 4bit
|
||||
if quant == "4BIT" or quant == "INT4":
|
||||
if "GPTQ" in model_name: quant = "GPTQ-4bit"
|
||||
elif "AWQ" in model_name: quant = "AWQ-4bit"
|
||||
else: quant = "4-bit"
|
||||
|
||||
return params_b, quant
|
||||
|
||||
def parse_logs():
|
||||
runs = []
|
||||
|
||||
# Define directories and their tags
|
||||
# (Path, variant_tag)
|
||||
dirs = [
|
||||
(BENCHMARK_DIR, "default"),
|
||||
(Path("../benchmarks/benchmark_results_amd-r9700-rocm_atten"), "rocm")
|
||||
]
|
||||
|
||||
for b_dir, variant in dirs:
|
||||
if not b_dir.exists():
|
||||
print(f"Warning: {b_dir} does not exist, skipping.")
|
||||
continue
|
||||
|
||||
print(f"Scanning {b_dir} [{variant}]...")
|
||||
|
||||
files = list(b_dir.glob("*.json"))
|
||||
|
||||
for f in files:
|
||||
fname = f.name
|
||||
try:
|
||||
data = json.loads(f.read_text())
|
||||
except:
|
||||
print(f"Skipping bad JSON: {fname}")
|
||||
continue
|
||||
|
||||
# Infer metadata from filename
|
||||
parts = fname.split("_tp")
|
||||
if len(parts) < 2: continue
|
||||
|
||||
model_part = parts[0]
|
||||
rest = parts[1]
|
||||
|
||||
# TP
|
||||
tp_match = re.match(r"^(\d+)", rest)
|
||||
if not tp_match: continue
|
||||
tp = int(tp_match.group(1))
|
||||
|
||||
env = f"TP{tp}"
|
||||
|
||||
# Model Name Restoration
|
||||
if "_" in model_part:
|
||||
model_display = model_part.replace("_", "/", 1)
|
||||
else:
|
||||
model_display = model_part
|
||||
|
||||
params_b, quant = extract_meta(model_display)
|
||||
|
||||
base_run = {
|
||||
"model": model_display,
|
||||
"model_clean": model_display,
|
||||
"env": env,
|
||||
"variant": variant,
|
||||
"gpu_config": "dual" if tp > 1 else "single",
|
||||
"quant": quant,
|
||||
"params_b": params_b,
|
||||
"name_params_b": params_b,
|
||||
"backend": "vLLM",
|
||||
"error": False
|
||||
}
|
||||
|
||||
if "throughput" in fname:
|
||||
tps = data.get("tokens_per_second", 0)
|
||||
run = base_run.copy()
|
||||
run["test"] = "Throughput"
|
||||
run["tps_mean"] = tps
|
||||
if tps == 0 and "error" in str(data).lower():
|
||||
run["error"] = True
|
||||
runs.append(run)
|
||||
|
||||
elif "latency" in fname:
|
||||
raw = data.get("raw_output", "")
|
||||
qps_match = re.search(r"_qps([\d\.]+)_", fname)
|
||||
qps = qps_match.group(1) if qps_match else "?"
|
||||
|
||||
ttft_m = re.search(r"(?:Mean TTFT|TTFT).*?([\d\.]+)", raw)
|
||||
ttft = float(ttft_m.group(1)) if ttft_m else 0.0
|
||||
|
||||
tpot_m = re.search(r"(?:Mean TPOT|TPOT).*?([\d\.]+)", raw)
|
||||
tpot = float(tpot_m.group(1)) if tpot_m else 0.0
|
||||
|
||||
# Entry 1: TTFT
|
||||
r1 = base_run.copy()
|
||||
r1["test"] = f"TTFT @ QPS {qps}"
|
||||
r1["tps_mean"] = ttft
|
||||
runs.append(r1)
|
||||
|
||||
# Entry 2: TPOT
|
||||
r2 = base_run.copy()
|
||||
r2["test"] = f"TPOT @ QPS {qps}"
|
||||
r2["tps_mean"] = tpot
|
||||
runs.append(r2)
|
||||
|
||||
return runs
|
||||
|
||||
if __name__ == "__main__":
|
||||
data = {"runs": parse_logs()}
|
||||
|
||||
runs_count = len(data["runs"])
|
||||
print(f"Parsed {runs_count} runs.")
|
||||
|
||||
with open(OUTPUT_FILE, "w") as f:
|
||||
json.dump(data, f, indent=2)
|
||||
print(f"Written to {OUTPUT_FILE}")
|
||||
+1404
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user