|
|
@@ -30,6 +30,7 @@
|
|
|
td.recipe:hover .copy { visibility: visible; }
|
|
|
td.recipe .copy:hover { background: color-mix(in srgb, currentColor 12%, canvas); }
|
|
|
td.recipe .copied { color: green; font-weight: 600; }
|
|
|
+ .half { -webkit-background-clip: text; background-clip: text; color: transparent; }
|
|
|
td.num { text-align: right; white-space: nowrap; }
|
|
|
#count { color: gray; }
|
|
|
#live { color: #2a9d2a; font-size: 11px; white-space: nowrap; }
|
|
|
@@ -66,6 +67,15 @@
|
|
|
<label>TP:
|
|
|
<select id="tp"><option value="">All</option></select>
|
|
|
</label>
|
|
|
+ <label>Medals:
|
|
|
+ <select id="medal">
|
|
|
+ <option value="" selected>All</option>
|
|
|
+ <option value="m">🥇🥈🥉</option>
|
|
|
+ <option value="1">🥇 gold</option>
|
|
|
+ <option value="2">🥈 silver</option>
|
|
|
+ <option value="3">🥉 bronze</option>
|
|
|
+ </select>
|
|
|
+ </label>
|
|
|
<details id="cols"><summary>Columns ▾</summary><div id="colbox"></div></details>
|
|
|
<label>Rows/page:
|
|
|
<select id="size"><option>25</option><option selected>50</option><option>100</option><option value="0">All</option></select>
|
|
|
@@ -86,10 +96,30 @@ const COLS = [
|
|
|
["name", "Recipe"], ["title", "Title"], ["model", "Model"], ["quant", "Quant"],
|
|
|
["engine", "Engine"], ["image", "Image"], ["nodes", "Nodes"], ["tp", "TP"],
|
|
|
["deploy", "Deploy"], ["port", "Port"], ["n", "Reports"], ["last", "Last bench"],
|
|
|
- ["points", "Points"], ["pct", "Score /100"],
|
|
|
- ["tps1", "Tok/s c1"], ["tps2", "Tok/s c2"],
|
|
|
+ ["points", "Points"], ["pct", "Score /100"], ["stars", "Bench"],
|
|
|
+ ["tps1", "Tok/s c1"], ["tps2", "Tok/s c2"], ["parent", "Parent"],
|
|
|
];
|
|
|
-const NUM = new Set(["nodes", "tp", "port", "n", "pct", "tps1", "tps2"]);
|
|
|
+const NUM = new Set(["nodes", "tp", "port", "n", "pct", "tps1", "tps2", "stars"]);
|
|
|
+const MEDAL = { 1: "🥇 ", 2: "🥈 ", 3: "🥉 " };
|
|
|
+
|
|
|
+// star grade of the latest completed bench: half-star precision (1 star =
|
|
|
+// 20 points), colored by score band — sort key is the ABSOLUTE score
|
|
|
+function starInfo(r) {
|
|
|
+ const e = latest(r, x => x.done && x.pct != null);
|
|
|
+ return e ? { pct: e.pct, s: Math.round(e.pct / 10) / 2 } : null;
|
|
|
+}
|
|
|
+const STAR_BAND = p => p < 20 ? "red" : p < 40 ? "brown" : p < 60 ? "orange" : p < 80 ? "gold" : "green";
|
|
|
+function starGlyphs(si) {
|
|
|
+ const col = STAR_BAND(si.pct);
|
|
|
+ let out = "";
|
|
|
+ for (let i = 1; i <= 5; i++) {
|
|
|
+ if (si.s >= i) out += `<span style="color:${col}">★</span>`;
|
|
|
+ else if (si.s >= i - 0.5)
|
|
|
+ out += `<span class="half" style="background:linear-gradient(90deg,${col} 50%,color-mix(in srgb,currentColor 25%,transparent) 50%)">★</span>`;
|
|
|
+ else out += '<span class="dim">☆</span>';
|
|
|
+ }
|
|
|
+ return out;
|
|
|
+}
|
|
|
|
|
|
// decode speed (tg t/s) from a run's llama-benchy perf cells
|
|
|
function tg(e, d, c) { const p = e && e.perf ? e.perf.find(p => p.d === d && p.c === c) : null; return p ? p.tg : null; }
|
|
|
@@ -102,6 +132,7 @@ function cell(r, k) {
|
|
|
if (k === "points") { const e = latest(r, e => e.points); return e ? e.points : ""; }
|
|
|
if (k === "pct") { const e = latest(r, e => e.pct != null); return e ? e.pct : ""; }
|
|
|
if (k === "tps1" || k === "tps2") { const e = latest(r, e => e.perf && e.perf.length); return tg(e, 0, k === "tps1" ? 1 : 2) ?? ""; }
|
|
|
+ if (k === "stars") { const si = starInfo(r); return si ? si.pct : -1; } // sort by the absolute score
|
|
|
return r[k];
|
|
|
}
|
|
|
|
|
|
@@ -236,15 +267,17 @@ const S = {
|
|
|
dir: P.get("dir") ? +P.get("dir") : 1,
|
|
|
size: [25, 50, 100, 0].includes(size0) ? size0 : 50,
|
|
|
page: Math.max(0, +(P.get("page") ?? 0)),
|
|
|
- hidden: new Set((P.get("hidden") ?? "title,image,port").split(",").filter(Boolean)),
|
|
|
+ hidden: new Set((P.get("hidden") ?? "title,image,port,parent").split(",").filter(Boolean)),
|
|
|
closed: new Set((P.get("closed") ?? "").split("|").map(decodeURIComponent).filter(Boolean)),
|
|
|
openRow: P.get("open") ?? null,
|
|
|
+ medal: P.get("medal") ?? "",
|
|
|
tp: tpParam != null && TPVALS.includes(+tpParam) ? tpParam : "",
|
|
|
};
|
|
|
document.getElementById("q").value = S.q;
|
|
|
document.getElementById("group").value = S.group;
|
|
|
document.getElementById("size").value = String(S.size);
|
|
|
document.getElementById("tp").value = S.tp;
|
|
|
+document.getElementById("medal").value = S.medal;
|
|
|
function syncUrl() {
|
|
|
const p = new URLSearchParams();
|
|
|
if (S.q) p.set("q", S.q);
|
|
|
@@ -258,6 +291,7 @@ function syncUrl() {
|
|
|
const closed = [...S.closed].map(encodeURIComponent);
|
|
|
if (closed.length) p.set("closed", closed.join("|"));
|
|
|
if (S.openRow) p.set("open", S.openRow);
|
|
|
+ if (S.medal) p.set("medal", S.medal);
|
|
|
if (S.tp) p.set("tp", S.tp);
|
|
|
const qs = p.toString();
|
|
|
history.replaceState(null, "", qs ? "?" + qs : location.pathname);
|
|
|
@@ -268,12 +302,13 @@ const esc = s => String(s).replace(/[&<>"]/g, c => ({"&":"&","<":"<",">":
|
|
|
function filtered() {
|
|
|
const q = S.q.toLowerCase();
|
|
|
return DATA.filter(r => (!S.tp || String(r.tp) === S.tp) &&
|
|
|
- (!q || (r.name + " " + r.title + " " + r.model + " " + r.quant + " " +
|
|
|
+ (!S.medal || (S.medal === "m" ? r.medal != null : String(r.medal) === S.medal)) &&
|
|
|
+ (!q || (r.name + " " + r.title + " " + r.model + " " + r.parent + " " + r.quant + " " +
|
|
|
r.engine + " " + r.image + " " + r.deploy + " " +
|
|
|
r.reports.map(e => e.date + " " + e.label).join(" ")).toLowerCase().includes(q)));
|
|
|
}
|
|
|
function sorted(rows) {
|
|
|
- const gk = { mq: r => r.model + "\0" + r.quant, model: r => r.model,
|
|
|
+ const gk = { mq: r => r.parent + "\0" + r.quant, model: r => r.parent,
|
|
|
quant: r => r.quant, engine: r => r.engine }[S.group];
|
|
|
const k = S.sort, d = S.dir;
|
|
|
const val = r => {
|
|
|
@@ -340,13 +375,30 @@ function render() {
|
|
|
const tip = c[0] === "last" && lr
|
|
|
? ` title="${esc(lr.date + (lr.time ? " " + lr.time : ""))}"` : "";
|
|
|
const cls = (NUM.has(c[0]) ? "num" : "") + (c[0] === "name" ? " recipe" : "");
|
|
|
- const body = v === "" || v == null
|
|
|
- ? '<span class="dim">—</span>'
|
|
|
- : esc(v) + (c[0] === "name" ? `<button class="copy" data-copy="${esc(r.name)}" title="copy recipe name">⧉</button>` : "");
|
|
|
+ let body;
|
|
|
+ if (c[0] === "stars") {
|
|
|
+ const si = starInfo(r);
|
|
|
+ const pe = si && latest(r, e => e.points); // both scores in the hint
|
|
|
+ body = !si
|
|
|
+ ? r.incomplete
|
|
|
+ ? '<span class="dim" title="bench started, not finished">◐</span>'
|
|
|
+ : '<span class="dim" title="no bench reports">—</span>'
|
|
|
+ : `<span title="${si.pct} /100${pe && pe.points ? " (" + pe.points + ")" : ""}">${starGlyphs(si)}</span>`;
|
|
|
+ } else if (v === "" || v == null) {
|
|
|
+ body = '<span class="dim">—</span>';
|
|
|
+ } else if (c[0] === "name") {
|
|
|
+ body = (MEDAL[r.medal] || "") + esc(v) +
|
|
|
+ `<button class="copy" data-copy="${esc(r.name)}" title="copy recipe name">⧉</button>`;
|
|
|
+ } else {
|
|
|
+ body = esc(v);
|
|
|
+ }
|
|
|
return `<td${cls ? ` class="${cls}"` : ""}${tip}>${body}</td>`;
|
|
|
}).join("") + "</tr>");
|
|
|
- if (S.openRow === r.name)
|
|
|
- out.push(`<tr class="hist"><td colspan="${vis.length}">${r.reports.length ? r.reports.map(e => {
|
|
|
+ if (S.openRow === r.name) {
|
|
|
+ // one toml section per recipe (the recipe.toml is not versioned);
|
|
|
+ // per-run report files stay on each line
|
|
|
+ const toml = `<a class="open" href="/r/${encodeURIComponent(r.name)}/recipe.toml" target="_blank" title="recipe.toml (highlighted)">⚙ toml</a>`;
|
|
|
+ const lines = r.reports.map(e => {
|
|
|
const fl = e.files && e.files.length ? e.files : [e.file];
|
|
|
const ts = (e.perf && e.perf.length)
|
|
|
? ` <span title="${esc(e.perf.map(p => "d" + p.d + " c" + p.c + ": " + p.tg + " t/s").join(" · "))}"> ${tg(e, 0, 1) ?? "—"} / ${tg(e, 0, 2) ?? "—"} t/s</span>`
|
|
|
@@ -354,7 +406,9 @@ function render() {
|
|
|
return `<span>${fl.map(f =>
|
|
|
`<a class="open" href="/r/${encodeURIComponent(r.name)}/${encodeURIComponent(f)}" target="_blank" title="open ${esc(f)}">${esc(f.split('.').pop())}</a>`).join(" ")}</span> ` +
|
|
|
`${esc(e.date)}${e.time ? "·" + esc(e.time) : ""} ${esc(e.label)} — ${e.pct != null ? esc(e.pct) + " /100" : '<span class="dim">no score</span>'}${e.points ? " (" + esc(e.points) + ")" : ""}${ts}`;
|
|
|
- }).join("<br>") : "no reports"}</td></tr>`);
|
|
|
+ }).join("<br>");
|
|
|
+ out.push(`<tr class="hist"><td colspan="${vis.length}">${r.reports.length ? toml + " " + lines : "no reports"}</td></tr>`);
|
|
|
+ }
|
|
|
}
|
|
|
document.querySelector("tbody").innerHTML = out.join("");
|
|
|
}
|
|
|
@@ -392,6 +446,7 @@ document.querySelector("tbody").addEventListener("click", e => {
|
|
|
document.getElementById("q").addEventListener("input", e => { S.q = e.target.value; S.page = 0; render(); });
|
|
|
document.getElementById("group").addEventListener("change", e => { S.group = e.target.value; S.closed.clear(); S.page = 0; render(); });
|
|
|
document.getElementById("tp").addEventListener("change", e => { S.tp = e.target.value; S.page = 0; render(); });
|
|
|
+document.getElementById("medal").addEventListener("change", e => { S.medal = e.target.value; S.page = 0; render(); });
|
|
|
document.getElementById("size").addEventListener("change", () => { S.page = 0; render(); });
|
|
|
document.getElementById("prev").addEventListener("click", () => { if (S.page > 0) { S.page--; render(); } });
|
|
|
document.getElementById("next").addEventListener("click", () => { S.page++; render(); });
|