feat(bench): show GPUs as the resource name instead of hostnames
All checks were successful
build-prerelease / Resolve version stamps + change detection (push) Successful in 31s
build-prerelease / Build neuron-blackwell (push) Has been skipped
build-prerelease / Build neuron-ampere (push) Has been skipped
build-prerelease / Build neuron-ada (push) Has been skipped
build-prerelease / Package helexa-neuron-ada RPM (push) Has been skipped
build-prerelease / Package helexa-neuron-ampere RPM (push) Has been skipped
build-prerelease / Package helexa-neuron-blackwell RPM (push) Has been skipped
build-prerelease / Build cortex binary (push) Has been skipped
build-prerelease / Package cortex RPM (push) Has been skipped
build-prerelease / Build helexa-bench binary (push) Successful in 2m34s
build-prerelease / Lint (fmt + clippy) (push) Successful in 2m54s
build-prerelease / Package helexa-bench RPM (push) Successful in 1m15s
build-prerelease / Test (push) Successful in 5m11s
build-prerelease / Publish to rpm.lair.cafe (unstable) (push) Successful in 56s

Public visitors don't know the hostnames, so surface each host's GPU(s)
as the resource name across the UI.

- store: gpu_label() turns the stored gpus_json into a compact label
  ("2× RTX 5090", "RTX 4090"); add `gpu` to ReportRow + RunRow and
  `host_gpus`/`model_gpus` maps to /api/dimensions (from each one's
  latest run). render_json gains gpu too.
- UI: Overview + Runs show a "GPU" column (gpu, fallback host); Runs'
  filter is now GPU-labelled (still filters by host underneath); Trends
  shows a "Measured on <gpu>" line for the selected model.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-14 16:29:13 +03:00
parent e3879f093a
commit d04f4ad704
6 changed files with 124 additions and 7 deletions

View File

@@ -30,7 +30,7 @@ export default function Overview() {
<Table striped bordered hover responsive size="sm">
<thead>
<tr>
<th>host</th>
<th>GPU</th>
<th>model</th>
<th className="text-end">prompt tok</th>
<th className="text-end">TTFT (s)</th>
@@ -43,7 +43,7 @@ export default function Overview() {
<tbody>
{rows.map((r, i) => (
<tr key={i}>
<td>{r.target_name}</td>
<td>{r.gpu ?? r.target_name}</td>
<td>{r.model_id}</td>
<td className="text-end">
{r.prompt_tokens ?? `~${r.prompt_size_approx}`}

View File

@@ -66,7 +66,18 @@ export default function Runs() {
<h3 className="mb-3">Runs</h3>
{dims && (
<Row className="g-3 mb-3">
<Picker label="Host" value={host} set={setHost} options={dims.hosts} />
{/* GPU filter — labelled by GPU, but filters by the underlying host. */}
<Form.Group as={Col}>
<Form.Label>GPU</Form.Label>
<Form.Select value={host} onChange={(e) => setHost(e.target.value)}>
<option value="">(all)</option>
{dims.hosts.map((h) => (
<option key={h} value={h}>
{dims.host_gpus[h] ?? h}
</option>
))}
</Form.Select>
</Form.Group>
<Picker
label="Model"
value={model}
@@ -88,7 +99,7 @@ export default function Runs() {
<thead>
<tr>
<th>ts</th>
<th>host</th>
<th>GPU</th>
<th>model</th>
<th>scenario</th>
<th>build</th>
@@ -102,7 +113,7 @@ export default function Runs() {
{rows.map((r) => (
<tr key={r.id}>
<td>{r.ts}</td>
<td>{r.host}</td>
<td>{r.gpu ?? r.host}</td>
<td>{r.model_id}</td>
<td>{r.scenario_id}</td>
<td>

View File

@@ -116,6 +116,12 @@ export default function Trends() {
/>
</Row>
{dims.model_gpus[model] && (
<p className="text-muted mb-3">
Measured on <strong>{dims.model_gpus[model]}</strong>.
</p>
)}
{data.length === 0 ? (
<Alert variant="info">No data for this selection yet.</Alert>
) : (

View File

@@ -11,6 +11,10 @@ export interface Dimensions {
models: string[];
scenarios: string[];
builds: BuildRef[];
/** host → GPU label, e.g. "2× RTX 5090". */
host_gpus: Record<string, string>;
/** model → GPU label (model maps to one host today). */
model_gpus: Record<string, string>;
}
/** Latest-SHA-per-cell medians (the report table). */
@@ -25,6 +29,8 @@ export interface ReportRow {
decode_tps_median: number | null;
total_s_median: number | null;
samples: number;
/** Public-facing resource name (the host's GPU(s)). */
gpu: string | null;
}
/** One point in a per-build time-series for a (host, model, scenario) cell. */
@@ -42,6 +48,8 @@ export interface RunRow {
id: number;
ts: string;
host: string;
/** Public-facing resource name (the host's GPU(s)). */
gpu: string | null;
hostname: string | null;
git_sha: string;
build_timestamp: string | null;

View File

@@ -47,6 +47,7 @@ pub fn render_json(rows: &[ReportRow]) -> Result<String> {
"total_s_median": r.total_s_median,
"git_sha": r.git_sha,
"samples": r.samples,
"gpu": r.gpu,
})
})
.collect();
@@ -77,6 +78,7 @@ mod tests {
decode_tps_median: Some(45.6),
total_s_median: Some(1.234),
samples: 5,
gpu: Some("2× RTX 5090".into()),
}];
let md = render_markdown(&rows);
assert!(md.contains("| engine |"));
@@ -98,6 +100,7 @@ mod tests {
decode_tps_median: None,
total_s_median: Some(0.5),
samples: 1,
gpu: None,
}];
let md = render_markdown(&rows);
assert!(md.contains("~128"));

View File

@@ -224,7 +224,7 @@ impl Store {
// successful run, then median that SHA's samples.
let mut stmt = self.conn.prepare(
"SELECT target_name, model_id, scenario_id, prompt_size_approx, git_sha,
ttft_s, decode_tps, total_s, prompt_tokens_actual
ttft_s, decode_tps, total_s, prompt_tokens_actual, gpus_json
FROM runs
WHERE ok=1
ORDER BY target_name, model_id, scenario_id, id",
@@ -240,6 +240,7 @@ impl Store {
decode_tps: row.get(6)?,
total_s: row.get(7)?,
prompt_tokens_actual: row.get(8)?,
gpus_json: row.get(9)?,
})
})?;
let raws: Vec<RawRow> = rows.collect::<rusqlite::Result<_>>()?;
@@ -283,11 +284,35 @@ impl Store {
})?
.collect::<rusqlite::Result<_>>()?;
// host/model → GPU label, taken from each one's most recent run.
let gpu_map = |group_col: &str| -> Result<std::collections::HashMap<String, String>> {
let sql = format!(
"SELECT {group_col}, gpus_json FROM runs \
WHERE id IN (SELECT MAX(id) FROM runs GROUP BY {group_col})"
);
let mut stmt = self.conn.prepare(&sql)?;
let rows = stmt.query_map([], |r| {
Ok((r.get::<_, String>(0)?, r.get::<_, Option<String>>(1)?))
})?;
let mut out = std::collections::HashMap::new();
for row in rows {
let (key, gpus) = row?;
if let Some(label) = gpus.as_deref().and_then(gpu_label) {
out.insert(key, label);
}
}
Ok(out)
};
let host_gpus = gpu_map("target_name")?;
let model_gpus = gpu_map("model_id")?;
Ok(Dimensions {
hosts,
models,
scenarios,
builds,
host_gpus,
model_gpus,
})
}
@@ -353,7 +378,8 @@ impl Store {
let mut sql = String::from(
"SELECT id, ts, target_name, hostname, git_sha, build_timestamp, package_version,
model_id, harness, scenario_id, prompt_size_approx, prompt_tokens_actual,
max_tokens, ttft_s, decode_tps, total_s, completion_tokens, ok, error
max_tokens, ttft_s, decode_tps, total_s, completion_tokens, ok, error,
gpus_json
FROM runs",
);
let mut conds: Vec<String> = Vec::new();
@@ -387,10 +413,12 @@ impl Store {
let mut stmt = self.conn.prepare(&sql)?;
let rows = stmt
.query_map(rusqlite::params_from_iter(args.iter()), |r| {
let gpus_json: Option<String> = r.get(19)?;
Ok(RunRow {
id: r.get(0)?,
ts: r.get(1)?,
host: r.get(2)?,
gpu: gpus_json.as_deref().and_then(gpu_label),
hostname: r.get(3)?,
git_sha: r.get(4)?,
build_timestamp: r.get(5)?,
@@ -422,6 +450,11 @@ pub struct Dimensions {
pub models: Vec<String>,
pub scenarios: Vec<String>,
pub builds: Vec<BuildRef>,
/// host → GPU label (latest run), so the UI can show the GPU as the
/// resource name instead of the internal hostname.
pub host_gpus: std::collections::HashMap<String, String>,
/// model → GPU label (latest run); model maps to one host today.
pub model_gpus: std::collections::HashMap<String, String>,
}
#[derive(Debug, Clone, serde::Serialize)]
@@ -505,6 +538,8 @@ pub struct RunRow {
pub id: i64,
pub ts: String,
pub host: String,
/// Public-facing resource name (the host's GPU(s)), e.g. "RTX 4090".
pub gpu: Option<String>,
pub hostname: Option<String>,
pub git_sha: String,
pub build_timestamp: Option<String>,
@@ -533,6 +568,7 @@ struct RawRow {
decode_tps: Option<f64>,
total_s: Option<f64>,
prompt_tokens_actual: Option<u64>,
gpus_json: Option<String>,
}
/// An aggregated cell ready for the report table.
@@ -548,6 +584,8 @@ pub struct ReportRow {
pub decode_tps_median: Option<f64>,
pub total_s_median: Option<f64>,
pub samples: usize,
/// Public-facing resource name (the host's GPU(s)), e.g. "2× RTX 5090".
pub gpu: Option<String>,
}
/// Group by (target, model, scenario), keep only the latest SHA's rows
@@ -584,11 +622,51 @@ fn aggregate(raws: Vec<RawRow>) -> Vec<ReportRow> {
decode_tps_median: median(cell.iter().filter_map(|r| r.decode_tps)),
total_s_median: median(cell.iter().filter_map(|r| r.total_s)),
samples: cell.len(),
gpu: cell
.iter()
.find_map(|r| r.gpus_json.as_deref().and_then(gpu_label)),
});
}
out
}
/// Compact GPU label from a run's stored `gpus_json` (the discovery device
/// list) — e.g. "2× RTX 5090", "RTX 4090". `None` when empty/absent. Used
/// as the public-facing resource name in place of internal hostnames.
fn gpu_label(gpus_json: &str) -> Option<String> {
let devices: Vec<serde_json::Value> = serde_json::from_str(gpus_json).ok()?;
if devices.is_empty() {
return None;
}
let mut order: Vec<String> = Vec::new();
let mut counts: std::collections::HashMap<String, usize> = std::collections::HashMap::new();
for d in &devices {
let name = d.get("name").and_then(|v| v.as_str()).unwrap_or("GPU");
let short = name
.trim_start_matches("NVIDIA GeForce ")
.trim_start_matches("NVIDIA ")
.to_string();
if !counts.contains_key(&short) {
order.push(short.clone());
}
*counts.entry(short).or_insert(0) += 1;
}
Some(
order
.iter()
.map(|n| {
let c = counts[n];
if c > 1 {
format!("{c}× {n}")
} else {
n.clone()
}
})
.collect::<Vec<_>>()
.join(" + "),
)
}
fn median(values: impl Iterator<Item = f64>) -> Option<f64> {
let mut v: Vec<f64> = values.collect();
if v.is_empty() {
@@ -676,4 +754,15 @@ mod tests {
assert_eq!(rows[0].samples, 2);
assert!((rows[0].ttft_s_median.unwrap() - 0.3).abs() < 1e-9);
}
#[test]
fn gpu_label_formats() {
let two = r#"[{"name":"NVIDIA GeForce RTX 5090"},{"name":"NVIDIA GeForce RTX 5090"}]"#;
assert_eq!(gpu_label(two).as_deref(), Some("2× RTX 5090"));
let one = r#"[{"name":"NVIDIA GeForce RTX 4090"}]"#;
assert_eq!(gpu_label(one).as_deref(), Some("RTX 4090"));
let dc = r#"[{"name":"NVIDIA H100"}]"#;
assert_eq!(gpu_label(dc).as_deref(), Some("H100"));
assert_eq!(gpu_label("[]"), None);
}
}