Skip to content

Commit 7238d64

Browse files
[ci] Introduce GPULlama3 performance history visualization page
1 parent 2b05a0d commit 7238d64

1 file changed

Lines changed: 268 additions & 0 deletions

File tree

docs/index.html

Lines changed: 268 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<title>GPULlama3 Performance History</title>
7+
<script src="https://cdn.plot.ly/plotly-2.27.0.min.js"></script>
8+
<style>
9+
* { box-sizing: border-box; margin: 0; padding: 0; }
10+
body {
11+
font-family: system-ui, -apple-system, sans-serif;
12+
background: #0d1117;
13+
color: #e6edf3;
14+
padding: 2rem;
15+
}
16+
h1 { font-size: 1.5rem; margin-bottom: 1.5rem; color: #e6edf3; }
17+
.controls {
18+
display: flex;
19+
gap: 1.5rem;
20+
flex-wrap: wrap;
21+
margin-bottom: 1.5rem;
22+
background: #161b22;
23+
padding: 1rem 1.25rem;
24+
border-radius: 8px;
25+
border: 1px solid #30363d;
26+
}
27+
.control-group label {
28+
display: block;
29+
font-size: 0.75rem;
30+
font-weight: 600;
31+
text-transform: uppercase;
32+
letter-spacing: 0.05em;
33+
color: #8b949e;
34+
margin-bottom: 0.35rem;
35+
}
36+
select {
37+
padding: 0.4rem 0.6rem;
38+
border: 1px solid #30363d;
39+
border-radius: 6px;
40+
font-size: 0.9rem;
41+
background: #21262d;
42+
color: #e6edf3;
43+
cursor: pointer;
44+
min-width: 160px;
45+
}
46+
select option { background: #21262d; }
47+
#chart {
48+
background: #161b22;
49+
border-radius: 8px;
50+
border: 1px solid #30363d;
51+
padding: 1rem;
52+
min-height: 720px;
53+
}
54+
#empty {
55+
display: none;
56+
background: #161b22;
57+
border-radius: 8px;
58+
border: 1px solid #30363d;
59+
padding: 3rem;
60+
text-align: center;
61+
color: #8b949e;
62+
font-size: 1rem;
63+
}
64+
</style>
65+
</head>
66+
<body>
67+
<h1>GPULlama3 — Performance History</h1>
68+
69+
<div class="controls">
70+
<div class="control-group">
71+
<label>Model</label>
72+
<select id="model"><option value="all">All</option></select>
73+
</div>
74+
<div class="control-group">
75+
<label>Quantization</label>
76+
<select id="quantization"><option value="all">All</option></select>
77+
</div>
78+
<div class="control-group">
79+
<label>Backend</label>
80+
<select id="backend"><option value="all">All</option></select>
81+
</div>
82+
<div class="control-group">
83+
<label>Configuration</label>
84+
<select id="configuration"><option value="all">All</option></select>
85+
</div>
86+
</div>
87+
88+
<div id="chart"></div>
89+
<div id="empty">No performance history available yet.</div>
90+
91+
<script>
92+
const METRICS = [
93+
{ key: 'total_rate', label: 'Total tok/s', yaxis: 'y', domain: [0.70, 1.00] },
94+
{ key: 'prompt_eval_rate', label: 'Prefill tok/s', yaxis: 'y2', domain: [0.36, 0.64] },
95+
{ key: 'eval_rate', label: 'Decode tok/s', yaxis: 'y3', domain: [0.00, 0.30] },
96+
];
97+
98+
const DARK = {
99+
plot: '#0d1117',
100+
paper: '#161b22',
101+
grid: '#21262d',
102+
text: '#e6edf3',
103+
subhed: '#8b949e',
104+
};
105+
106+
async function loadHistory() {
107+
const resp = await fetch('perf-history.jsonl');
108+
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
109+
const text = await resp.text();
110+
return text.trim().split('\n').filter(Boolean).map(l => JSON.parse(l));
111+
}
112+
113+
function populateSelect(id, values) {
114+
const sel = document.getElementById(id);
115+
const existing = new Set([...sel.options].map(o => o.value));
116+
for (const v of [...values].sort()) {
117+
if (!existing.has(v)) {
118+
const opt = document.createElement('option');
119+
opt.value = v; opt.textContent = v;
120+
sel.appendChild(opt);
121+
}
122+
}
123+
}
124+
125+
function traceName(r, modelFilter, quantFilter) {
126+
const parts = [];
127+
if (modelFilter === 'all') parts.push(r.model);
128+
if (quantFilter === 'all') parts.push(r.quantization);
129+
parts.push(`${r.configuration} (${r.backend})`);
130+
return parts.join(' — ');
131+
}
132+
133+
function render(data) {
134+
const modelFilter = document.getElementById('model').value;
135+
const quantFilter = document.getElementById('quantization').value;
136+
const backFilter = document.getElementById('backend').value;
137+
const confFilter = document.getElementById('configuration').value;
138+
139+
const filtered = data.filter(r =>
140+
(modelFilter === 'all' || r.model === modelFilter) &&
141+
(quantFilter === 'all' || r.quantization === quantFilter) &&
142+
(backFilter === 'all' || r.backend === backFilter) &&
143+
(confFilter === 'all' || r.configuration === confFilter)
144+
);
145+
146+
const groups = {};
147+
for (const r of filtered) {
148+
const key = `${r.model}|${r.quantization}|${r.configuration}|${r.backend}`;
149+
if (!groups[key]) groups[key] = { rows: [], rep: r };
150+
groups[key].rows.push(r);
151+
}
152+
153+
const traces = [];
154+
for (const [groupKey, { rows, rep }] of Object.entries(groups)) {
155+
const name = traceName(rep, modelFilter, quantFilter);
156+
METRICS.forEach((m, mi) => {
157+
traces.push({
158+
type: 'scatter',
159+
mode: 'lines+markers',
160+
name,
161+
legendgroup: groupKey,
162+
showlegend: mi === 0,
163+
xaxis: 'x',
164+
yaxis: m.yaxis,
165+
x: rows.map(r => r.timestamp),
166+
y: rows.map(r => r[m.key]),
167+
customdata: rows.map(r => [r.run_id, r.run_number]),
168+
text: rows.map(r => {
169+
const dt = new Date(r.timestamp);
170+
const date = dt.toISOString().slice(0, 10);
171+
const time = dt.toISOString().slice(11, 16) + ' UTC';
172+
const runLabel = r.run_number ? `run #${r.run_number}` : '';
173+
return (
174+
`<b>${r[m.key]?.toFixed(2)} tok/s</b><br>` +
175+
`${m.label}<br>` +
176+
`${date} ${time}<br>` +
177+
`commit: ${r.short_commit || '—'}<br>` +
178+
`model: ${r.model} ${r.quantization}<br>` +
179+
`config: ${r.configuration} / ${r.backend}` +
180+
(runLabel ? `<br>${runLabel}` : '')
181+
);
182+
}),
183+
hovertemplate: '%{text}<extra>%{fullData.name}</extra>',
184+
marker: { size: 7 },
185+
});
186+
});
187+
}
188+
189+
const annotations = METRICS.map(m => ({
190+
text: `<b>${m.label}</b>`,
191+
xref: 'paper', yref: 'paper',
192+
x: 0, y: m.domain[1] + 0.015,
193+
xanchor: 'left', yanchor: 'bottom',
194+
showarrow: false,
195+
font: { size: 11, color: DARK.subhed },
196+
}));
197+
198+
const yAxisBase = {
199+
rangemode: 'tozero',
200+
gridcolor: DARK.grid,
201+
tickfont: { color: DARK.text },
202+
titlefont: { color: DARK.text },
203+
zerolinecolor: DARK.grid,
204+
};
205+
206+
Plotly.react('chart', traces, {
207+
xaxis: {
208+
title: 'Run date', type: 'date', anchor: 'y3',
209+
gridcolor: DARK.grid,
210+
tickfont: { color: DARK.text },
211+
titlefont: { color: DARK.text },
212+
zerolinecolor: DARK.grid,
213+
},
214+
yaxis: { ...yAxisBase, domain: METRICS[0].domain },
215+
yaxis2: { ...yAxisBase, domain: METRICS[1].domain, anchor: 'x' },
216+
yaxis3: { ...yAxisBase, domain: METRICS[2].domain, anchor: 'x' },
217+
annotations,
218+
plot_bgcolor: DARK.plot,
219+
paper_bgcolor: DARK.paper,
220+
font: { color: DARK.text },
221+
legend: { orientation: 'h', y: -0.12, font: { color: DARK.text }, bgcolor: 'rgba(0,0,0,0)' },
222+
margin: { t: 30, b: 80, l: 60, r: 20 },
223+
hovermode: 'closest',
224+
}, { responsive: true });
225+
}
226+
227+
async function main() {
228+
let data;
229+
try {
230+
data = await loadHistory();
231+
} catch (e) {
232+
document.getElementById('chart').style.display = 'none';
233+
document.getElementById('empty').style.display = 'block';
234+
return;
235+
}
236+
237+
if (!data.length) {
238+
document.getElementById('chart').style.display = 'none';
239+
document.getElementById('empty').style.display = 'block';
240+
return;
241+
}
242+
243+
populateSelect('model', new Set(data.map(r => r.model)));
244+
populateSelect('quantization', new Set(data.map(r => r.quantization)));
245+
populateSelect('backend', new Set(data.map(r => r.backend)));
246+
populateSelect('configuration', new Set(data.map(r => r.configuration)));
247+
248+
const rerender = () => render(data);
249+
['model', 'quantization', 'backend', 'configuration']
250+
.forEach(id => document.getElementById(id).addEventListener('change', rerender));
251+
252+
render(data);
253+
254+
document.getElementById('chart').on('plotly_click', function(evt) {
255+
const [runId] = evt.points[0]?.customdata ?? [];
256+
if (runId) {
257+
window.open(
258+
`https://github.com/beehive-lab/GPULlama3.java/actions/runs/${runId}`,
259+
'_blank'
260+
);
261+
}
262+
});
263+
}
264+
265+
main();
266+
</script>
267+
</body>
268+
</html>

0 commit comments

Comments
 (0)