use std::time::Instant;
use brain2qwerty::model::conv_conformer::ConvConformer;
use brain2qwerty::model_rlx::ConvConformerRlx;
use brain2qwerty::rlx_device::{parse_engine, resolve_rlx_device};
use brain2qwerty::tensor::{load_tensor_bin, Tensor};
use clap::Parser;
#[derive(Parser, Debug)]
#[command(about = "Benchmark Brain2Qwerty encoder")]
struct Args {
#[arg(long, default_value = "rust,rlx-cpu")]
engines: String,
#[arg(long, default_value = "cpu")]
device: String,
#[arg(long, default_value_t = 2)]
warmup: usize,
#[arg(long, default_value_t = 10)]
runs: usize,
}
fn main() -> anyhow::Result<()> {
let args = Args::parse();
let weights = brain2qwerty::data_paths::encoder_weights_path();
let neuros = load_tensor_bin(&brain2qwerty::parity_refs_dir().join("input_neuros.bin"))?;
let chan_pos = load_tensor_bin(&brain2qwerty::parity_refs_dir().join("input_chan_pos.bin"))?;
let days = load_tensor_bin(&brain2qwerty::parity_refs_dir().join("input_days.bin"))?;
let subject_ids: Vec<usize> = days.data.iter().map(|&x| x as usize).collect();
for engine in args.engines.split(',').map(str::trim) {
match engine {
"rust" | "pure-rust" => {
let model = ConvConformer::from_tiny_weights(&weights.to_string_lossy())?;
bench(engine, args.warmup, args.runs, || {
let _ = model.forward(&neuros, &subject_ids, Some(&chan_pos));
});
}
e if e.starts_with("rlx") => {
let dev = parse_engine(e)
.or_else(|| resolve_rlx_device(&args.device).ok())
.unwrap_or(rlx::Device::Cpu);
if !rlx::runtime::is_available(dev) {
eprintln!("SKIP {engine}: device unavailable");
continue;
}
let mut model = ConvConformerRlx::from_tiny_weights(&weights.to_string_lossy())?
.with_device(dev);
let prefix = model
.rust
.forward_prefix(&neuros, &subject_ids, Some(&chan_pos));
model.precompile_tail(&tail_spec(&prefix.z_transformer_in))?;
bench(engine, args.warmup, args.runs, || {
let _ = model.forward(&neuros, &subject_ids, Some(&chan_pos));
});
}
other => eprintln!("unknown engine {other}"),
}
}
Ok(())
}
fn tail_spec(z: &Tensor) -> brain2qwerty::model_rlx::graph::EncoderSpec {
let cfg = brain2qwerty::config::Brain2QwertyConfig::tiny();
let tc = &cfg.brain_model_config.transformer_config;
brain2qwerty::model_rlx::graph::EncoderSpec {
b: z.shape[0],
t: z.shape[1],
d: z.shape[2],
n_classes: cfg.inference.num_classes,
num_layers: tc.num_layers,
num_heads: tc.num_heads,
ffn_dim: tc.ffn_dim,
dw_kernel: tc.depthwise_conv_kernel_size,
aux: cfg.brain_model_config.aux_prediction,
}
}
fn bench(name: &str, warmup: usize, runs: usize, mut f: impl FnMut()) {
for _ in 0..warmup {
f();
}
let t0 = Instant::now();
for _ in 0..runs {
f();
}
let ms = t0.elapsed().as_secs_f64() * 1000.0 / runs as f64;
println!("{name}: {ms:.3} ms/forward (warmup={warmup} runs={runs})");
}