This commit is contained in:
2026-03-26 20:50:00 +08:00
commit a04b9ef9c5
208 changed files with 71242 additions and 0 deletions
+401
View File
@@ -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;
}
+542
View File
@@ -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;
}
+804
View File
@@ -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;">&larr; 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()">&times;</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>
+54
View File
@@ -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
}
}
]
+107
View File
@@ -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
View File
@@ -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 &rarr;</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>
+139
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff