use anyhow::{Context, Result, bail};
use inferencelayer::bench_guard::{
ensure_quiet_machine, load_average_1m, nvidia_clock_note, read_pins, write_pins,
};
use inferencelayer::{GpuCtx, Lfm2Gpu, Scheduler, Weights};
use std::time::Instant;
const HOLD_FRACTION: f64 = 0.97;
fn main() {
let args: Vec<String> = std::env::args().skip(1).collect();
match run(&args) {
Ok(true) => {}
Ok(false) => std::process::exit(1),
Err(e) => {
eprintln!("scoreboard: {e:#}");
std::process::exit(2);
}
}
}
struct Opts {
model: String,
rows: Vec<String>,
pins_path: String,
write_pins: bool,
records: Vec<(String, f64)>,
ngen: usize,
streams: usize,
prefill_len: usize,
runs: usize,
}
fn parse_opts(args: &[String]) -> Result<Opts> {
let mut o = Opts {
model: std::env::var("OSFKB_QWEN_DIR").unwrap_or_else(|_| "data/qwen3-0.6b".into()),
rows: vec![],
pins_path: "bench/scoreboard_pins.json".into(),
write_pins: false,
records: vec![],
ngen: 128,
streams: 8,
prefill_len: 2048,
runs: 3,
};
let mut it = args.iter();
while let Some(a) = it.next() {
let mut val = |name: &str| {
it.next()
.cloned()
.with_context(|| format!("{name} needs a value"))
};
match a.as_str() {
"--model" => o.model = val("--model")?,
"--rows" => o.rows = val("--rows")?.split(',').map(str::to_string).collect(),
"--pins" => o.pins_path = val("--pins")?,
"--pin" => o.write_pins = true,
"--ngen" => o.ngen = val("--ngen")?.parse().context("--ngen")?,
"--streams" => o.streams = val("--streams")?.parse().context("--streams")?,
"--prefill-len" => {
o.prefill_len = val("--prefill-len")?.parse().context("--prefill-len")?
}
"--runs" => o.runs = val("--runs")?.parse().context("--runs")?,
"--record" => {
let kv = val("--record")?;
let (k, v) = kv
.split_once('=')
.with_context(|| format!("--record wants row=value, got {kv:?}"))?;
o.records
.push((k.to_string(), v.parse().context("--record value")?));
}
other => bail!("unknown flag {other:?}"),
}
}
if o.rows.is_empty() && o.records.is_empty() {
o.rows = vec!["mono".into(), "prefill".into(), "aggregate".into()];
}
Ok(o)
}
fn run(args: &[String]) -> Result<bool> {
let o = parse_opts(args)?;
let mut measured: Vec<(String, f64)> = o.records.clone();
let wants_local = o
.rows
.iter()
.any(|r| ["mono", "prefill", "aggregate"].contains(&r.as_str()));
let quiet = if wants_local {
let q = ensure_quiet_machine()?;
if o.write_pins && !q {
bail!(
"--pin refused: the quiet-machine guard was overridden, so these numbers carry \
contention and must never become the reference every later run is judged against. \
Re-measure on a quiet box to pin."
)
}
nvidia_clock_note();
q
} else {
eprintln!(
"record-only: {} row(s) ingested from another harness; the local quiet-machine guard \
does not apply and is skipped",
o.records.len()
);
true
};
let _ = quiet;
if wants_local {
let dir = std::path::Path::new(&o.model);
if !dir.join("model.safetensors").exists()
&& !dir.join("model.safetensors.index.json").exists()
{
bail!(
"no checkpoint at {} (pass --model or set OSFKB_QWEN_DIR)",
o.model
);
}
let ctx = GpuCtx::new().context("gpu adapter")?;
let w = Weights::load(&ctx, &o.model).context("load weights")?;
let vocab = w.cfg.vocab;
let gpu = Lfm2Gpu::new(&ctx, w);
let model_key = dir
.file_name()
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or_else(|| o.model.clone());
let _ = gpu.decode(&ctx, &synth_prompt(vocab, 16, 0), 16)?;
for row in &o.rows {
let (key, value) = match row.as_str() {
"mono" => {
let v = row_mono(&ctx, &gpu, vocab, o.ngen, o.runs)?;
(format!("mono@{model_key}"), v)
}
"prefill" => {
let v = row_prefill(&ctx, &gpu, vocab, o.prefill_len, o.runs)?;
(format!("prefill{}@{model_key}", o.prefill_len), v)
}
"aggregate" => {
let v = row_aggregate(&ctx, &gpu, vocab, o.streams, o.ngen)?;
(format!("aggregate{}@{model_key}", o.streams), v)
}
other => bail!("unknown local row {other:?} (fleet rows go through --record)"),
};
eprintln!(" {key}: {value:.1} tok/s");
measured.push((key, value));
}
}
if measured.is_empty() {
bail!("nothing to do: no local rows selected and no --record given");
}
let mut pins = read_pins(&o.pins_path)?;
let rows_obj = pins
.as_object_mut()
.and_then(|p| p.get_mut("rows"))
.and_then(|r| r.as_object_mut())
.context("pins file malformed: expected {\"rows\": {...}}")?;
let load = load_average_1m();
let mut all_hold = true;
for (key, value) in &measured {
match rows_obj
.get(key)
.and_then(|e| e.get("value"))
.and_then(|v| v.as_f64())
{
Some(pin) if *value < pin * HOLD_FRACTION => {
all_hold = false;
eprintln!(
"REGRESSION {key}: {value:.1} < pin {pin:.1} × {HOLD_FRACTION} = {:.1}",
pin * HOLD_FRACTION
);
}
Some(pin) => {
eprintln!("hold {key}: {value:.1} vs pin {pin:.1} ✓");
}
None => {
eprintln!(
"new {key}: {value:.1} (no pin yet{})",
if o.write_pins {
"; pinning"
} else {
" — run with --pin to record"
}
);
}
}
if o.write_pins {
let recorded = o.records.iter().any(|(k, _)| k == key);
let mut entry = serde_json::json!({
"value": value,
"captured_epoch_s": std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0),
});
let obj = entry.as_object_mut().expect("json object");
if recorded {
obj.insert("source".into(), serde_json::json!("record"));
} else {
obj.insert("load_1m".into(), serde_json::json!(load));
}
rows_obj.insert(key.clone(), entry);
}
}
if o.write_pins {
write_pins(&o.pins_path, &pins)?;
eprintln!("pins written to {}", o.pins_path);
}
Ok(all_hold)
}
fn synth_prompt(vocab: usize, len: usize, seed: usize) -> Vec<u32> {
let span = vocab.saturating_sub(20).max(1);
(0..len)
.map(|i| (10 + (seed * 7919 + i * 6151) % span) as u32)
.collect()
}
fn row_mono(ctx: &GpuCtx, gpu: &Lfm2Gpu, vocab: usize, ngen: usize, runs: usize) -> Result<f64> {
let mut rates = Vec::with_capacity(runs);
for r in 0..runs {
let prompt = synth_prompt(vocab, 32, r + 1);
let (_, decode_tok_s, _) = gpu.decode(ctx, &prompt, ngen)?;
rates.push(decode_tok_s);
}
Ok(median(rates))
}
fn row_prefill(ctx: &GpuCtx, gpu: &Lfm2Gpu, vocab: usize, plen: usize, runs: usize) -> Result<f64> {
let mut rates = Vec::with_capacity(runs);
for r in 0..runs {
let prompt = synth_prompt(vocab, plen, 100 + r);
let mut sched = Scheduler::new(gpu, ctx, 8, vec![]);
let id = sched.submit(prompt, 1)?;
let t0 = Instant::now();
loop {
let ems = sched.step(gpu, ctx)?;
if ems.iter().any(|e| e.id == id) {
break;
}
}
let ttft = t0.elapsed().as_secs_f64();
rates.push(plen as f64 / ttft.max(1e-9));
}
Ok(median(rates))
}
fn row_aggregate(
ctx: &GpuCtx,
gpu: &Lfm2Gpu,
vocab: usize,
streams: usize,
ngen: usize,
) -> Result<f64> {
let mut sched = Scheduler::new(
gpu,
ctx,
streams.min(inferencelayer::forward::MAX_SLOTS),
vec![],
);
for s in 0..streams {
sched.submit(synth_prompt(vocab, 32, 1000 + s), ngen)?;
}
let t0 = Instant::now();
let results = sched.run_to_completion(gpu, ctx)?;
let secs = t0.elapsed().as_secs_f64();
let total: usize = results.values().map(Vec::len).sum();
Ok(total as f64 / secs)
}
fn median(mut v: Vec<f64>) -> f64 {
v.sort_by(|a, b| a.partial_cmp(b).expect("finite"));
v[v.len() / 2]
}
#[cfg(test)]
mod tests {
use inferencelayer::bench_guard::parse_compute_apps;
const V100_CONTENDED: &str = "2110607, 9462 MiB, python\n\
2110983, 8896 MiB, python\n\
2111120, 18566 MiB, python\n";
#[test]
fn should_report_foreign_compute_procs_occupying_the_gpus() {
let procs = parse_compute_apps(V100_CONTENDED, 999);
assert_eq!(procs.len(), 3, "all three foreign jobs are contention");
assert!(
procs[2].contains("18566 MiB"),
"row kept verbatim for the operator"
);
}
#[test]
fn should_not_count_our_own_process_as_contention() {
let procs = parse_compute_apps("4242, 512 MiB, scoreboard\n", 4242);
assert!(
procs.is_empty(),
"our own pid is not a foreign job: {procs:?}"
);
}
#[test]
fn should_report_a_clear_gpu_as_having_no_foreign_procs() {
assert!(parse_compute_apps("", 1).is_empty());
assert!(
parse_compute_apps("\n \n", 1).is_empty(),
"blank rows are not jobs"
);
}
}