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>
110 lines
3.4 KiB
Rust
110 lines
3.4 KiB
Rust
//! Render the SQLite store as a results table — the automated
|
||
//! replacement for hand-editing `doc/benchmarks.md`. Columns match that
|
||
//! doc: engine, model, prompt tok, TTFT (s), decode tok/s, total (s),
|
||
//! plus the build SHA each cell was measured against.
|
||
|
||
use crate::store::ReportRow;
|
||
use anyhow::Result;
|
||
|
||
pub fn render_markdown(rows: &[ReportRow]) -> String {
|
||
let mut out = String::new();
|
||
out.push_str(
|
||
"| engine | model | prompt tok | TTFT (s) | decode tok/s | total (s) | build | n |\n",
|
||
);
|
||
out.push_str("|---|---|---:|---:|---:|---:|---|---:|\n");
|
||
for r in rows {
|
||
let ptok = r
|
||
.prompt_tokens
|
||
.map(|t| t.to_string())
|
||
.unwrap_or_else(|| format!("~{}", r.prompt_size_approx));
|
||
out.push_str(&format!(
|
||
"| {} | {} | {} | {} | {} | {} | `{}` | {} |\n",
|
||
r.target_name,
|
||
r.model_id,
|
||
ptok,
|
||
fmt_opt(r.ttft_s_median, 3),
|
||
fmt_opt(r.decode_tps_median, 1),
|
||
fmt_opt(r.total_s_median, 3),
|
||
r.git_sha,
|
||
r.samples,
|
||
));
|
||
}
|
||
out
|
||
}
|
||
|
||
pub fn render_json(rows: &[ReportRow]) -> Result<String> {
|
||
let arr: Vec<serde_json::Value> = rows
|
||
.iter()
|
||
.map(|r| {
|
||
serde_json::json!({
|
||
"engine": r.target_name,
|
||
"model": r.model_id,
|
||
"scenario": r.scenario_id,
|
||
"prompt_size_approx": r.prompt_size_approx,
|
||
"prompt_tokens": r.prompt_tokens,
|
||
"ttft_s_median": r.ttft_s_median,
|
||
"decode_tps_median": r.decode_tps_median,
|
||
"total_s_median": r.total_s_median,
|
||
"git_sha": r.git_sha,
|
||
"samples": r.samples,
|
||
"gpu": r.gpu,
|
||
})
|
||
})
|
||
.collect();
|
||
Ok(serde_json::to_string_pretty(&arr)?)
|
||
}
|
||
|
||
fn fmt_opt(v: Option<f64>, places: usize) -> String {
|
||
match v {
|
||
Some(x) => format!("{x:.places$}"),
|
||
None => "—".to_string(),
|
||
}
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
|
||
#[test]
|
||
fn markdown_has_header_and_row() {
|
||
let rows = vec![ReportRow {
|
||
target_name: "beast".into(),
|
||
model_id: "Qwen/Qwen3.6-27B".into(),
|
||
scenario_id: "chat:128".into(),
|
||
prompt_size_approx: 128,
|
||
git_sha: "30d50d6".into(),
|
||
prompt_tokens: Some(130),
|
||
ttft_s_median: Some(0.123),
|
||
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 |"));
|
||
assert!(md.contains("beast"));
|
||
assert!(md.contains("`30d50d6`"));
|
||
assert!(md.contains("0.123"));
|
||
}
|
||
|
||
#[test]
|
||
fn missing_decode_renders_dash() {
|
||
let rows = vec![ReportRow {
|
||
target_name: "benjy".into(),
|
||
model_id: "m".into(),
|
||
scenario_id: "chat:128".into(),
|
||
prompt_size_approx: 128,
|
||
git_sha: "abc".into(),
|
||
prompt_tokens: None,
|
||
ttft_s_median: Some(0.1),
|
||
decode_tps_median: None,
|
||
total_s_median: Some(0.5),
|
||
samples: 1,
|
||
gpu: None,
|
||
}];
|
||
let md = render_markdown(&rows);
|
||
assert!(md.contains("~128"));
|
||
assert!(md.contains("—"));
|
||
}
|
||
}
|