use std::alloc::{GlobalAlloc, Layout, System};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::Instant;
use tract_nnef::internal::*;
use tract_nnef::tract_core::transform::get_transform;
use tract_transformers::WithTractTransformers;
struct Counting;
static ALLOCS: AtomicUsize = AtomicUsize::new(0);
unsafe impl GlobalAlloc for Counting {
unsafe fn alloc(&self, l: Layout) -> *mut u8 {
ALLOCS.fetch_add(1, Ordering::Relaxed);
unsafe { System.alloc(l) }
}
unsafe fn dealloc(&self, p: *mut u8, l: Layout) {
unsafe { System.dealloc(p, l) }
}
unsafe fn realloc(&self, p: *mut u8, l: Layout, n: usize) -> *mut u8 {
ALLOCS.fetch_add(1, Ordering::Relaxed);
unsafe { System.realloc(p, l, n) }
}
}
#[global_allocator]
static GLOBAL: Counting = Counting;
fn allocs() -> usize {
ALLOCS.load(Ordering::Relaxed)
}
fn count_kv_caches(model: &TypedModel) -> usize {
model
.nodes()
.iter()
.filter(|n| n.op().name().contains("KeyValueCache") || n.op().name().contains("KvCache"))
.count()
}
fn main() -> TractResult<()> {
let mut args = std::env::args().skip(1);
let model_path =
args.next().expect("usage: llm_decode_bench <model.nnef.tgz> [prefill] [decode]");
let prefill_len: usize = args.next().map(|s| s.parse().unwrap()).unwrap_or(16);
let decode_tokens: usize = args.next().map(|s| s.parse().unwrap()).unwrap_or(64);
eprintln!("Loading {model_path} ...");
let t_load = Instant::now();
let nnef = tract_nnef::nnef().with_tract_transformers();
let mut model = nnef.model_for_path(&model_path)?.into_decluttered()?;
get_transform("transformers_detect_all")?
.expect("transformers_detect_all not registered")
.transform(&mut model)?;
let n_kv = count_kv_caches(&model);
let runnable = model.into_optimized()?.into_runnable()?;
eprintln!("Loaded + optimized in {:?} ({n_kv} KV-cache ops)", t_load.elapsed());
let mut state = runnable.spawn()?;
let prefill: TValue = Tensor::from_shape(&[1, prefill_len], &vec![1i64; prefill_len])?.into();
let one_tok: TValue = Tensor::from_shape(&[1, 1], &[1i64])?.into();
let _ = state.run(tvec![prefill])?;
for _ in 0..4 {
let o = state.run(tvec![one_tok.clone()])?;
std::hint::black_box(&o);
}
let a0 = allocs();
let t0 = Instant::now();
let mut per_token_ns: Vec<u128> = Vec::with_capacity(decode_tokens);
for _ in 0..decode_tokens {
let ts = Instant::now();
let o = state.run(tvec![one_tok.clone()])?;
per_token_ns.push(ts.elapsed().as_nanos());
std::hint::black_box(&o);
}
let elapsed = t0.elapsed();
let a1 = allocs();
let allocs_per_tok = (a1 - a0) as f64 / decode_tokens as f64;
let ms_per_tok = elapsed.as_secs_f64() * 1e3 / decode_tokens as f64;
let toks_per_s = decode_tokens as f64 / elapsed.as_secs_f64();
per_token_ns.sort_unstable();
let median_ms = per_token_ns[per_token_ns.len() / 2] as f64 / 1e6;
println!(
"--- E2E folded decode ({n_kv} KV-cache ops, prefill={prefill_len}, decode={decode_tokens}) ---"
);
println!("allocs/token : {allocs_per_tok:.1}");
println!("ms/token : {ms_per_tok:.3} (median {median_ms:.3})");
println!("tokens/sec : {toks_per_s:.2}");
Ok(())
}