brain2qwerty 0.0.1

Brain2Qwerty V1/V2 MEG neural decoding inference in Rust (parity-tested vs Python)
Documentation
//! Benchmark Brain2Qwerty encoder: pure-Rust vs RLX backends.

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})");
}