|
| 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