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
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:
@@ -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}`}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
) : (
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"));
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user