use super::workload::Measured;
use std::path::Path;
#[derive(Debug, Clone, Copy)]
pub(super) struct Header {
pub n_kv_max: usize,
pub n_ubatch: usize,
pub is_pp_shared: bool,
pub is_tg_separate: bool,
pub n_gpu_layers: usize,
pub n_threads: usize,
pub backend: &'static str,
}
pub(super) fn header_line(h: &Header) -> String {
format!(
"frink batched-bench: n_kv_max = {}, n_ubatch = {}, is_pp_shared = {}, \
is_tg_separate = {}, n_gpu_layers = {}, n_threads = {}, backend = {}",
h.n_kv_max,
h.n_ubatch,
h.is_pp_shared as u8,
h.is_tg_separate as u8,
h.n_gpu_layers,
h.n_threads,
h.backend
)
}
pub(super) const TABLE_HEADER: [&str; 2] = [
"| PP | TG | B | N_KV | T_PP s | S_PP t/s | T_TG s | S_TG t/s | T s | S t/s |",
"|-------|--------|------|--------|----------|----------|----------|----------|----------|----------|",
];
pub(super) fn md_row(m: &Measured) -> String {
format!(
"|{:>6} | {:>6} | {:>4} | {:>6} | {:>8.3} | {:>8.2} | {:>8.3} | {:>8.2} | {:>8.3} | {:>8.2} |",
m.combo.pp,
m.combo.tg,
m.combo.pl,
m.n_kv,
m.t_pp,
m.speed_pp,
m.t_tg,
m.speed_tg,
m.t,
m.speed
)
}
pub(super) fn jsonl_row(h: &Header, m: &Measured) -> String {
serde_json::json!({
"n_kv_max": h.n_kv_max,
"n_ubatch": h.n_ubatch,
"is_pp_shared": h.is_pp_shared as u8,
"is_tg_separate": h.is_tg_separate as u8,
"n_gpu_layers": h.n_gpu_layers,
"n_threads": h.n_threads,
"backend": h.backend,
"pp": m.combo.pp,
"tg": m.combo.tg,
"pl": m.combo.pl,
"n_kv": m.n_kv,
"t_pp": m.t_pp,
"speed_pp": m.speed_pp,
"t_tg": m.t_tg,
"speed_tg": m.speed_tg,
"t": m.t,
"speed": m.speed,
"workload_digest": m.digest.hex(),
})
.to_string()
}
pub(super) fn write_receipt(
dest: &Path,
mut receipt: serde_json::Map<String, serde_json::Value>,
h: &Header,
model: &str,
arch: &str,
rows: &[Measured],
) -> anyhow::Result<()> {
if let Some(parent) = dest.parent() {
std::fs::create_dir_all(parent)?;
}
let rows: Vec<serde_json::Value> = rows
.iter()
.map(|m| {
serde_json::json!({
"pp": m.combo.pp,
"tg": m.combo.tg,
"pl": m.combo.pl,
"n_kv": m.n_kv,
"t_pp": m.t_pp,
"speed_pp": m.speed_pp,
"t_tg": m.t_tg,
"speed_tg": m.speed_tg,
"t": m.t,
"speed": m.speed,
"workload_digest": m.digest.hex(),
})
})
.collect();
let serde_json::Value::Object(own) = serde_json::json!({
"schema": 1,
"kind": "batched",
"model_path": model,
"arch": arch,
"n_kv_max": h.n_kv_max,
"n_ubatch": h.n_ubatch,
"is_pp_shared": h.is_pp_shared,
"is_tg_separate": h.is_tg_separate,
"n_gpu_layers": h.n_gpu_layers,
"rows": rows,
}) else {
unreachable!("json! with braces is an object");
};
receipt.extend(own);
std::fs::write(
dest,
serde_json::to_string_pretty(&serde_json::Value::Object(receipt))? + "\n",
)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::super::workload::Combo;
use super::*;
use crate::bench_guard::WorkloadDigest;
fn sample() -> Measured {
Measured {
combo: Combo {
pp: 128,
tg: 128,
pl: 2,
},
n_kv: 512,
t_pp: 0.198,
speed_pp: 1295.19,
t_tg: 5.029,
speed_tg: 50.90,
t: 5.227,
speed: 97.95,
digest: WorkloadDigest::new(),
}
}
#[test]
fn a_row_prints_in_upstream_widths() {
assert_eq!(
md_row(&sample()),
"| 128 | 128 | 2 | 512 | 0.198 | 1295.19 | 5.029 | 50.90 | 5.227 | 97.95 |"
);
}
#[test]
fn the_header_separator_and_rows_are_the_same_width() {
let row = md_row(&sample());
assert_eq!(TABLE_HEADER[0].len(), row.len());
assert_eq!(TABLE_HEADER[1].len(), row.len());
for (i, (a, b)) in TABLE_HEADER[1].chars().zip(row.chars()).enumerate() {
if a == '|' {
assert_eq!(b, '|', "row column boundary drifted at byte {i}");
}
}
}
#[test]
fn the_table_header_is_upstreams_format_string_expanded() {
let expected = format!(
"|{:>6} | {:>6} | {:>4} | {:>6} | {:>8} | {:>8} | {:>8} | {:>8} | {:>8} | {:>8} |",
"PP", "TG", "B", "N_KV", "T_PP s", "S_PP t/s", "T_TG s", "S_TG t/s", "T s", "S t/s"
);
assert_eq!(TABLE_HEADER[0], expected);
let sep = format!(
"|{:>6}-|-{:>6}-|-{:>4}-|-{:>6}-|-{:>8}-|-{:>8}-|-{:>8}-|-{:>8}-|-{:>8}-|-{:>8}-|",
"------",
"------",
"----",
"------",
"--------",
"--------",
"--------",
"--------",
"--------",
"--------"
);
assert_eq!(TABLE_HEADER[1], sep);
}
fn header() -> Header {
Header {
n_kv_max: 2048,
n_ubatch: 512,
is_pp_shared: false,
is_tg_separate: false,
n_gpu_layers: 0,
n_threads: 6,
backend: "CPU",
}
}
#[test]
fn jsonl_rows_carry_upstreams_per_row_keys() {
let v: serde_json::Value = serde_json::from_str(&jsonl_row(&header(), &sample())).unwrap();
for key in [
"pp",
"tg",
"pl",
"n_kv",
"t_pp",
"speed_pp",
"t_tg",
"speed_tg",
"t",
"speed",
"n_kv_max",
"n_gpu_layers",
"n_threads",
"is_pp_shared",
] {
assert!(v.get(key).is_some(), "missing {key}");
}
assert_eq!(v["pl"], 2);
assert_eq!(v["n_kv"], 512);
assert_eq!(v["is_pp_shared"], 0, "upstream prints the flag as an int");
for key in ["n_batch", "flash_attn", "n_threads_batch"] {
assert!(v.get(key).is_none(), "{key} would be a fabricated value");
}
}
#[test]
fn the_header_line_prints_flags_as_ints_like_upstream() {
let line = header_line(&Header {
is_pp_shared: true,
..header()
});
assert!(line.contains("is_pp_shared = 1"), "{line}");
assert!(line.contains("is_tg_separate = 0"), "{line}");
assert!(line.contains("n_kv_max = 2048"), "{line}");
}
#[test]
fn a_receipt_keeps_the_envelope_and_adds_the_rows() {
let dir = std::env::temp_dir().join(format!(
"frink-batched-bench-receipt-{}",
std::process::id()
));
let dest = dir.join("nested").join("r.json");
let mut envelope = serde_json::Map::new();
envelope.insert("backend".into(), "cpu".into());
write_receipt(&dest, envelope, &header(), "m.gguf", "llama", &[sample()]).unwrap();
let v: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(&dest).unwrap()).unwrap();
assert_eq!(v["backend"], "cpu", "the envelope survives the merge");
assert_eq!(v["kind"], "batched");
assert_eq!(v["rows"][0]["n_kv"], 512);
assert_eq!(v["rows"][0]["workload_digest"], WorkloadDigest::new().hex());
let _ = std::fs::remove_dir_all(&dir);
}
}