use memra_engine::forward::argmax;
use memra_engine::hybrid::HybridModel;
use memra_engine::Engine;
use memra_gguf::GgufFile;
use std::collections::VecDeque;
use std::time::Instant;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let path = std::env::args()
.nth(1)
.expect("usage: ppn-bench <model.gguf> [P=32] [G=128] [reps=5]");
let p: usize = std::env::args().nth(2).and_then(|s| s.parse().ok()).unwrap_or(32);
let g: usize = std::env::args().nth(3).and_then(|s| s.parse().ok()).unwrap_or(128);
let reps: usize = std::env::args().nth(4).and_then(|s| s.parse().ok()).unwrap_or(5);
let stages_env = std::env::var("MEMRA_PP_STAGES").ok().filter(|v| {
!v.is_empty() && v != "0" && v != "1"
});
let door_open = stages_env.is_some();
let devices_env = std::env::var("MEMRA_PP_DEVICES").unwrap_or_default();
let primary_dev: usize = devices_env
.split(',')
.next()
.and_then(|s| s.trim().parse().ok())
.unwrap_or(0);
let knobs = format!(
"stages={} devices={} splits={} shard={} streams={}",
stages_env.clone().unwrap_or_else(|| "OFF".into()),
if devices_env.is_empty() { "default(primary)" } else { &devices_env },
std::env::var("MEMRA_PP_SPLITS").unwrap_or_else(|_| "default(even)".into()),
if memra_engine::pp::pp_shard_off() { "OFF" } else { "per-stage" },
if memra_engine::pp::pp2_streams_off() { "OFF(inc1)" } else { "per-stage" },
);
println!("ppn-bench M2 config: {knobs} P={p} G={g} reps={reps} model={path}");
let e = Engine::new(primary_dev)?;
let gf = GgufFile::open(&path)?;
let m = HybridModel::load(&e, &gf)?; let n_layers = m.layers.len();
if door_open {
let fence = memra_engine::pp::pp_cuts(n_layers)
.expect("ppn-bench: door env set but pp_cuts is None");
println!("stage fence: {fence:?} over {n_layers} layers");
}
let prompt: Vec<u32> = (0..p).map(|i| (100 + (i * 7) % 900) as u32).collect();
let saved_stages = stages_env.clone();
unsafe {
std::env::remove_var("MEMRA_PP_STAGES");
}
let mut inputs: Vec<u32> = Vec::with_capacity(p + g);
{
let mut cache = memra_engine::cache::Cache::new(&e, &m.cfg, p + g + 8)?;
let mut next = 0u32;
for step in 0..p + g {
let tok = if step < p { prompt[step] } else { next };
inputs.push(tok);
let ll = m.decode_step(&e, tok, &mut cache)?;
next = argmax(&ll) as u32;
}
}
#[derive(Clone, Copy, PartialEq)]
enum Arm {
SerialOff,
SerialPp,
PipelinedPp,
}
let arms: Vec<(Arm, &str)> = if door_open {
let mut v = vec![(Arm::SerialPp, "serial-pp")];
if m.cfg.gemma4.is_none() && !memra_engine::pp::pp2_streams_off() {
v.push((Arm::PipelinedPp, "pipelined-pp"));
}
v
} else {
vec![(Arm::SerialOff, "serial-off")]
};
let mut times: Vec<(String, Vec<f64>)> =
arms.iter().map(|(_, name)| (name.to_string(), Vec::new())).collect();
for rep in 0..reps {
for (ai, &(arm, name)) in arms.iter().enumerate() {
unsafe {
match arm {
Arm::SerialOff => std::env::remove_var("MEMRA_PP_STAGES"),
_ => std::env::set_var("MEMRA_PP_STAGES", saved_stages.clone().unwrap()),
}
if arm == Arm::PipelinedPp {
std::env::set_var("MEMRA_PP_OVERLAP", "1");
} else {
std::env::set_var("MEMRA_PP_OVERLAP", "0");
}
}
let mut cache = memra_engine::pp::new_cache(&e, &m.cfg, p + g + 8)?;
for &tok in inputs.iter().take(p) {
m.decode_step(&e, tok, &mut cache)?;
}
let ms = match arm {
Arm::SerialOff | Arm::SerialPp => {
let t0 = Instant::now();
for &tok in inputs.iter().skip(p) {
m.decode_step(&e, tok, &mut cache)?;
}
t0.elapsed().as_secs_f64() * 1e3
}
Arm::PipelinedPp => {
let mut pend: VecDeque<memra_engine::pp::PendingLogits> = VecDeque::new();
let t0 = Instant::now();
for &tok in inputs.iter().skip(p) {
pend.push_back(m.decode_step_h_ppn_deferred(&e, tok, &mut cache)?);
if pend.len() >= 3 {
pend.pop_front().unwrap().wait()?;
}
}
while let Some(pl) = pend.pop_front() {
pl.wait()?;
}
t0.elapsed().as_secs_f64() * 1e3
}
};
let us_tok = ms * 1e3 / g as f64;
println!(
"{{\"arm\":\"{name}\",\"rep\":{rep},\"g\":{g},\"ms\":{ms:.3},\
\"us_per_tok\":{us_tok:.2},\"tok_s\":{:.2}}}",
g as f64 / (ms / 1e3)
);
times[ai].1.push(us_tok);
}
}
println!("---- medians (us/token over {g} gen steps, N={reps} reps, interleaved) ----");
for (name, mut v) in times {
v.sort_by(|a, b| a.partial_cmp(b).unwrap());
let med = v[v.len() / 2];
println!(
"{name}: median {med:.2} us/tok ({:.2} tok/s) all={:?}",
1e6 / med,
v.iter().map(|x| (x * 100.0).round() / 100.0).collect::<Vec<_>>()
);
}
Ok(())
}