Files
amd-strix-halo-toolboxes/docs/index.html
T
2025-09-28 09:38:04 +01:00

781 lines
26 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>AMD Ryzen AI MAX+ 395 "Strix Halo" — Llama.cpp Backend Performance Comparison</title>
<style>
:root {
--bg: #ffffff;
--ink: #000000;
--muted: #555555;
--accent: #0645ad;
--card: #f9f9f9;
--border: #cccccc;
--sticky: #f4f4f4;
--pill: #e0e0e0;
--roc: #d3caff;
--roc-text: #000000;
--warn: #d88f00;
--bad: #d32f2f;
--model-w: 300px;
--winner-w: 220px;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
background: var(--bg);
color: var(--ink);
font: 15px/1.5 system-ui, -apple-system, Segoe UI, Roboto, Inter;
}
a {
color: var(--accent);
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
header {
padding: 14px 20px 0;
}
header h1 {
margin: 0 0 6px;
font-size: 22px;
}
header p {
margin: 4px 0 12px;
color: var(--muted);
font-size: 13px;
}
.controls {
display: grid;
grid-template-columns: 1fr 160px 260px 180px;
gap: 10px;
align-items: end;
padding: 10px 20px;
}
label {
display: block;
font-size: 12px;
color: var(--muted);
margin: 0 0 6px;
}
input[type="text"],
select,
input[type="number"] {
width: 100%;
background: #fff;
color: var(--ink);
border: 1px solid var(--border);
border-radius: 10px;
padding: 8px 10px;
outline: none;
}
.slider {
display: grid;
grid-template-columns: 1fr 90px;
gap: 8px;
align-items: center;
}
.range-wrap {
position: relative;
height: 30px;
}
.range-wrap input[type="range"] {
position: absolute;
inset: 0;
width: 100%;
margin: 0;
background: transparent;
pointer-events: none;
appearance: none;
}
.range-wrap input[type="range"]::-webkit-slider-thumb {
pointer-events: auto;
appearance: none;
height: 16px;
width: 16px;
border-radius: 50%;
background: var(--accent);
border: 2px solid #fff;
box-shadow: 0 0 2px rgba(0, 0, 0, 0.3);
cursor: pointer;
transition: background 0.2s, transform 0.2s;
}
.range-wrap input[type="range"]::-webkit-slider-thumb:hover {
background: #0451cc;
transform: scale(1.1);
}
.range-wrap input[type="range"]::-moz-range-thumb {
pointer-events: auto;
height: 16px;
width: 16px;
border-radius: 50%;
background: var(--accent);
border: 2px solid #fff;
box-shadow: 0 0 2px rgba(0, 0, 0, 0.3);
cursor: pointer;
transition: background 0.2s, transform 0.2s;
}
.range-wrap input[type="range"]::-moz-range-thumb:hover {
background: #0451cc;
transform: scale(1.1);
}
.range-track {
position: absolute;
top: 50%;
height: 6px;
width: 100%;
transform: translateY(-50%);
border-radius: 999px;
background: #e5e5e5;
}
.range-values {
margin-top: 4px;
font-size: 12px;
color: var(--muted);
text-align: center;
}
input[type="range"] {
width: 100%;
}
.ticks {
display: flex;
justify-content: space-between;
color: var(--muted);
font-size: 11px;
margin-top: 2px;
}
.cols {
display: flex;
gap: 8px;
flex-wrap: wrap;
padding: 0 20px 8px;
}
.colbox {
border: 1px solid var(--border);
border-radius: 12px;
padding: 8px 10px;
min-width: 220px;
background: var(--card);
}
.colbox label {
display: flex;
gap: 8px;
align-items: center;
margin: 0;
}
.colbox input {
transform: translateY(1px);
}
.badge {
display: inline-block;
padding: 2px 8px;
border-radius: 999px;
background: var(--pill);
font-size: 11px;
margin-left: 6px;
}
.roc {
background: var(--roc);
color: var(--roc-text);
border: 1px solid #b19cff55;
}
.faall {
background: #cfe9ff;
/* light blue chip */
color: #000000;
border: 1px solid #9bc9ff55;
}
.meta {
padding: 0 20px 14px;
color: var(--muted);
font-size: 12px;
}
.section {
padding: 6px 20px 0;
}
.section h2 {
margin: 6px 0 8px;
font-size: 16px;
color: var(--ink);
font-weight: 600;
}
.tablewrap {
margin: 0 0 16px;
border-top: 1px solid var(--border);
}
.scroller {
overflow-x: auto;
overflow-y: hidden;
}
.scroller::-webkit-scrollbar {
height: 10px;
}
.scroller::-webkit-scrollbar-thumb {
background: var(--border);
border-radius: 8px;
}
.scroller-top {
overflow-x: auto;
overflow-y: hidden;
height: 12px;
/* slim */
margin: 0 0 6px;
/* a little gap above the table */
}
.scroller-top .scroller-spacer {
height: 1px;
/* tiny content so the bar renders */
}
table {
width: max-content;
min-width: 100vw;
border-collapse: separate;
border-spacing: 0;
}
thead th {
position: sticky;
top: 0;
background: var(--card);
z-index: 2;
}
th,
td {
padding: 10px 12px;
border-bottom: 1px solid var(--border);
vertical-align: top;
}
th {
white-space: nowrap;
}
td {
white-space: normal;
}
.col-model {
position: sticky;
left: 0;
background: var(--sticky);
z-index: 3;
max-width: var(--model-w);
width: var(--model-w);
}
.col-winner {
position: sticky;
left: calc(var(--model-w));
background: var(--sticky);
z-index: 3;
max-width: var(--winner-w);
width: var(--winner-w);
}
.model-title {
font-weight: 600;
word-wrap: break-word;
overflow-wrap: anywhere;
}
.model-meta {
color: var(--muted);
font-size: 12px;
margin-top: 2px;
}
.cell {
min-width: 120px;
text-align: center;
}
.tps {
display: block;
text-align: center;
}
.std {
color: var(--muted);
font-size: 12px;
text-align: center;
}
.warn {
color: var(--warn);
}
.err {
color: var(--bad);
}
.winner-pill {
display: inline-block;
background: var(--pill);
border-radius: 999px;
padding: 2px 8px;
margin: 4px 4px 4px 0;
font-size: 12px;
}
.winner-wrap {
display: flex;
flex-wrap: wrap;
}
.env-sub {
color: var(--muted);
font-size: 12px;
}
</style>
</head>
<body>
<header>
<h1>AMD Ryzen AI MAX+ 395 "Strix Halo" — Llama.cpp Backend Performance Comparison</h1>
<p class="muted">
Compare model throughput across backends (pp512 & tg128).
Repo: <a href="https://github.com/kyuz0/amd-strix-halo-toolboxes" target="_blank"
rel="noreferrer">kyuz0/amd-strix-halo-toolboxes</a>
</p>
<p class="muted">Platform: Framework Desktop, 128GB Unified RAM (accelerator-performance tuned profile)</p>
<p id="meta-line">Loading meta…</p>
</header>
<div class="controls">
<div>
<label for="search">Search model</label>
<input id="search" type="text" placeholder="e.g. llama, qwen, 30B, Q8_K…">
</div>
<div>
<label for="quant">Quant</label>
<select id="quant">
<option value="">Any</option>
</select>
</div>
<div>
<label>Model params</label>
<div class="range-wrap" id="sizeRange">
<input type="range" id="sizeLo" step="1">
<input type="range" id="sizeHi" step="1">
<div class="range-track" id="sizeTrack"></div>
</div>
<div class="ticks">
<span>4B</span><span>50B</span><span>96B</span><span>143B</span><span>189B</span><span>235B</span>
</div>
<div class="range-values">
<span id="sizeLoVal">4B</span> <span id="sizeHiVal">235B</span>
</div>
</div>
<div>
<label for="faMode">Flash Attention</label>
<select id="faMode">
<option value="off">FA off</option>
<option value="on" selected>FA on</option> <!-- default ON -->
<option value="both">Both</option>
</select>
</div>
</div>
<div class="cols" id="columns"></div>
<div class="meta">Winner = every selected backend within the bests uncertainty range, combining ± errors from both
results.</div>
<div class="section">
<h2>Prompt Processing (pp512) — tokens/second</h2>
</div>
<section class="tablewrap">
<div class="scroller scroller-top" data-for="pp">
<div class="scroller-spacer"></div>
</div>
<div class="scroller" id="scroller-pp">
<table id="tbl-pp">
<thead>
<tr></tr>
</thead>
<tbody></tbody>
</table>
</div>
</section>
<div class="section">
<h2>Text Generation (tg128) — tokens/second</h2>
</div>
<section class="tablewrap">
<div class="scroller scroller-top" data-for="tg">
<div class="scroller-spacer"></div>
</div>
<div class="scroller" id="scroller-tg">
<table id="tbl-tg">
<thead>
<tr></tr>
</thead>
<tbody></tbody>
</table>
</div>
</section>
<script>
(async function () {
const K_SIGMA = 1.0;
const MIN_TOL = 0.25;
const res = await fetch('results.json');
const data = await res.json();
const allRuns = data.runs || [];
const perfRuns = allRuns.filter(r => r.test === 'pp512' || r.test === 'tg128');
const metaLine = document.getElementById('meta-line');
const search = document.getElementById('search');
const quantSel = document.getElementById('quant');
const colPicker = document.getElementById('columns');
const faMode = document.getElementById('faMode');
// NEW dual slider elements
const sizeLo = document.getElementById('sizeLo');
const sizeHi = document.getElementById('sizeHi');
const sizeTrack = document.getElementById('sizeTrack');
const sizeLoVal = document.getElementById('sizeLoVal');
const sizeHiVal = document.getElementById('sizeHiVal');
metaLine.textContent = `${data.meta?.os_kernel || ''} — llama.cpp ${(data.meta?.llamacpp_builds || []).map(b => b.hash).join(', ') || 'unknown'} — generated ${data.meta?.generated_at || ''}`;
const models = [...new Set(perfRuns.map(r => r.model_clean || r.model))].sort((a, b) => a.localeCompare(b));
const byModel = {};
for (const name of models) { byModel[name] = { pp512: {}, tg128: {}, quant: null, sizeB: null }; }
for (const r of perfRuns) {
const name = r.model_clean || r.model;
const cfg = `${r.env}|fa=${r.fa ? 1 : 0}`;
byModel[name][r.test][cfg] = { mean: r.tps_mean, std: r.tps_std, err: r.error, et: r.error_type };
if (!byModel[name].quant && r.quant) byModel[name].quant = r.quant;
if (byModel[name].sizeB == null) byModel[name].sizeB = (r.name_params_b ?? r.params_b);
}
for (const r of allRuns) {
if (r.test || !r.error) continue;
const name = r.model_clean || r.model; if (!byModel[name]) continue;
const cfg = `${r.env}|fa=${r.fa ? 1 : 0}`;
for (const t of ['pp512', 'tg128']) {
if (!byModel[name][t][cfg]) byModel[name][t][cfg] = { mean: null, std: null, err: true, et: r.error_type || 'error' };
}
}
const envs = [...new Set(allRuns.map(r => r.env))].sort();
function envBox(env) {
const roc = env.includes('rocwmma');
const hipBLASTt_off = env.includes('hblt0');
const fa_all_quants = env.includes('fa_all_quants');
const id = `env_${env.replace(/[^a-z0-9_-]/gi, '_')}`;
return `
<div class="colbox">
<label for="${id}">
<input id="${id}" type="checkbox" data-env="${env}"
${/(vulkan_amdvlk|vulkan_radv|rocm6_4_4-rocwmma|rocm7_rc-rocwmma)(?![-\w])/.test(env.trim()) ? 'checked' : ''}>
<span>
<strong>${env}</strong>
${roc ? '<span class="badge roc">rocWMMA</span>' : ''}
${fa_all_quants ? '<span class="badge faall">FA all quants</span>' : ''}
${hipBLASTt_off ? '<span class="badge roc">hipBLASTt OFF</span>' : ''}
</span>
</label>
</div>`;
}
colPicker.innerHTML = envs.map(envBox).join('');
const selectedEnvs = () => [...colPicker.querySelectorAll('input[type="checkbox"]')].filter(i => i.checked).map(i => i.dataset.env);
const quants = [...new Set(Object.values(byModel).map(m => (m.quant || 'Unknown').toUpperCase()))].sort();
quants.forEach(q => { const o = document.createElement('option'); o.value = (q === 'UNKNOWN' ? '' : q); o.textContent = q; quantSel.appendChild(o); });
// --- Dual range slider setup ---
const sizes = Object.values(byModel)
.map(m => m.sizeB)
.filter(v => typeof v === 'number')
.sort((a, b) => a - b);
// force clean integer min/max
const MIN_B = Math.floor(sizes[0] ?? 0);
const MAX_B = Math.ceil(sizes[sizes.length - 1] ?? 100);
[sizeLo, sizeHi].forEach(inp => {
inp.min = MIN_B;
inp.max = MAX_B;
inp.step = 1; // integers only
});
sizeLo.value = MIN_B;
sizeHi.value = MAX_B;
const filters = { sizeLo: MIN_B, sizeHi: MAX_B };
function fmtB(n) {
return `${Number(n).toFixed(0)}B`;
}
function clampRange() {
if (+sizeLo.value > +sizeHi.value) {
if (document.activeElement === sizeLo) {
sizeHi.value = sizeLo.value;
} else {
sizeLo.value = sizeHi.value;
}
}
}
function paintTrack() {
const a = (+sizeLo.value - MIN_B) / (MAX_B - MIN_B) * 100;
const b = (+sizeHi.value - MIN_B) / (MAX_B - MIN_B) * 100;
sizeTrack.style.background = `
linear-gradient(to right,
#e5e5e5 ${a}%,
var(--accent) ${a}%,
var(--accent) ${b}%,
#e5e5e5 ${b}%)`;
}
function updateSizeUI(pushRender = true) {
clampRange();
// Fix rounding: snap to min/max if slider is at extremes
if (Math.abs(sizeLo.value - MIN_B) < 0.0001) sizeLo.value = MIN_B;
if (Math.abs(sizeHi.value - MAX_B) < 0.0001) sizeHi.value = MAX_B;
sizeLoVal.textContent = fmtB(sizeLo.value);
sizeHiVal.textContent = fmtB(sizeHi.value);
filters.sizeLo = +sizeLo.value;
filters.sizeHi = +sizeHi.value;
paintTrack();
if (pushRender) render();
}
sizeLo.addEventListener('input', () => updateSizeUI(true));
sizeHi.addEventListener('input', () => updateSizeUI(true));
updateSizeUI(false);
function setupScrollSync(prefix) {
const top = document.querySelector(`.scroller-top[data-for="${prefix}"]`);
const bottom = document.getElementById(`scroller-${prefix}`);
const spacer = top.querySelector('.scroller-spacer');
if (!top || !bottom || !spacer) return () => { };
function syncWidth() {
// Match the tables full scrollable width
spacer.style.width = bottom.scrollWidth + 'px';
// Keep positions aligned after rerender
top.scrollLeft = bottom.scrollLeft;
}
// Two-way sync
let lock = false;
top.addEventListener('scroll', () => {
if (lock) return; lock = true;
bottom.scrollLeft = top.scrollLeft;
lock = false;
});
bottom.addEventListener('scroll', () => {
if (lock) return; lock = true;
top.scrollLeft = bottom.scrollLeft;
lock = false;
});
// Resize/refresh on content changes
const ro = new ResizeObserver(syncWidth);
ro.observe(bottom);
syncWidth();
// Return a refresher for manual calls after rerender
return syncWidth;
}
// --- Filters ---
function matchesFilters(name) {
const q = (search.value || '').toLowerCase();
if (q && !name.toLowerCase().includes(q)) return false;
const info = byModel[name];
if (quantSel.value && (info.quant || '').toUpperCase() !== quantSel.value.toUpperCase()) return false;
if (info.sizeB != null && (info.sizeB < filters.sizeLo - 1e-9 || info.sizeB > filters.sizeHi + 1e-9)) return false;
return true;
}
function cleanTitle(name, quant) {
const qTokens = ['BF16', 'F16', 'F32', 'FP16', 'MXFP2', 'MXFP4', 'MXFP8'];
if (quant) qTokens.push(quant.toUpperCase());
let t = name;
t = t.replace(/[-_](Q\d+(?:_[A-Z0-9]+)*)/gi, '');
t = t.replace(new RegExp(`[-_]?(${qTokens.join('|')})\\b`, 'gi'), '');
t = t.replace(/-UD\b/gi, '');
t = t.replace(/[-_]{2,}/g, '-')
.replace(/\s{2,}/g, ' ')
.replace(/[-_]+$/, '')
.trim();
return t;
}
function buildColDefs(envSel, mode) {
const cols = [];
for (const e of envSel) {
if (mode === 'both') {
cols.push({ env: e, key: `${e}|fa=0`, label: `${e}<div class="env-sub">FA off</div>` });
cols.push({ env: e, key: `${e}|fa=1`, label: `${e}<div class="env-sub">FA on</div>` });
} else {
const k = `${e}|fa=${mode === 'on' ? 1 : 0}`;
cols.push({ env: e, key: k, label: `${e}<div class="env-sub">FA ${mode === 'on' ? 'on' : 'off'}</div>` });
}
}
return cols;
}
function headerHTML(test, colDefs) {
return `<th class="col-model">Model</th><th class="col-winner">Winner (${test})</th>` +
colDefs.map(c => `<th>${c.label}</th>`).join('');
}
function cellHTML(name, test, key) {
const v = byModel[name][test][key];
if (!v) return '<div class="cell"><span class="muted">—</span></div>';
if (v.err || v.mean == null) return `<div class="cell"><span class="warn">⚠ ${v.et || 'error'}</span></div>`;
return `<div class="cell"><span class="tps">${v.mean.toFixed(2)}</span><span class="std">± ${v.std?.toFixed(2) ?? '—'}</span></div>`;
}
function winnersHTML(name, test, colDefs) {
const vals = [];
for (const c of colDefs) {
const v = byModel[name][test][c.key];
if (v && !v.err && typeof v.mean === 'number') {
vals.push({
env: c.env,
fa: c.key.endsWith('|fa=1') ? 'on' : 'off',
mean: v.mean,
std: (typeof v.std === 'number' ? v.std : 0)
});
}
}
if (!vals.length) return '<span class="muted">—</span>';
let best = vals[0];
for (const v of vals) if (v.mean > best.mean) best = v;
const winners = [];
for (const v of vals) {
const pooled = Math.sqrt((best.std || 0) ** 2 + (v.std || 0) ** 2);
const tol = Math.max(MIN_TOL, K_SIGMA * pooled);
if ((best.mean - v.mean) <= tol) winners.push(v);
}
if (faMode.value !== 'both') {
const envs = [...new Set(winners.map(w => w.env))];
return `<div class="winner-wrap">${envs.map(env =>
`<span class="winner-pill">🏆 ${env}</span>`).join('')}</div>`;
} else {
const byEnv = {};
for (const w of winners) (byEnv[w.env] ||= new Set()).add(w.fa);
return `<div class="winner-wrap">${Object.entries(byEnv).map(([env, fas]) => {
const list = [...fas].sort().map(x => `FA ${x}`).join(' & ');
return `<span class="winner-pill">🏆 ${env} (${list})</span>`;
}).join('')
}</div>`;
}
}
function renderTable(prefix, test) {
const envSel = selectedEnvs(); const mode = faMode.value;
const colDefs = buildColDefs(envSel, mode);
const h = document.querySelector(`#tbl-${prefix} thead tr`) || document.getElementById(`${prefix}-h`);
if (!h) return; // nothing to render into; bail safely
h.innerHTML = headerHTML(test, colDefs);
const tbody = document.querySelector(`#tbl-${prefix} tbody`);
tbody.innerHTML = '';
for (const name of models.filter(matchesFilters)) {
const tr = document.createElement('tr');
const tdM = document.createElement('td');
tdM.className = 'col-model';
const info = byModel[name];
tdM.innerHTML = `<div class="model-title">${cleanTitle(name, info.quant)}</div><div class="model-meta">${info.quant || '—'} · ${(info.sizeB ?? '—')}B</div>`;
tr.appendChild(tdM);
const tdW = document.createElement('td');
tdW.className = 'col-winner';
tdW.innerHTML = winnersHTML(name, test, colDefs);
tr.appendChild(tdW);
for (const c of colDefs) {
const td = document.createElement('td');
td.innerHTML = cellHTML(name, test, c.key);
tr.appendChild(td);
}
tbody.appendChild(tr);
}
}
let refreshScrollPP = null, refreshScrollTG = null;
function render() {
renderTable('pp', 'pp512');
renderTable('tg', 'tg128');
// Update top scrollbar widths after content changes
if (typeof refreshScrollPP === 'function') refreshScrollPP();
if (typeof refreshScrollTG === 'function') refreshScrollTG();
}
[search, quantSel, faMode].forEach(el => el.addEventListener('input', render));
colPicker.addEventListener('change', render);
render();
refreshScrollPP = setupScrollSync('pp');
refreshScrollTG = setupScrollSync('tg');
})();
</script>
</body>
</html>