//! kfd gpu performance benchmarks. //! run: cargo test --release --test kfd_bench -- --nocapture --test-threads=1 //! //! measures gpu dispatch latency vs cpu at various matrix sizes //! to find the crossover point where gpu wins. use isis::host::device::kfd::{self, HsaDevice}; use isis::host::device::kfd::dispatch::KernArgs; use isis::organism::compute::{ComputeBackend, CpuBackend}; use std::time::Instant; fn open_gpu() -> Option> { use std::sync::{OnceLock, Mutex}; static GPU: OnceLock>> = OnceLock::new(); GPU.get_or_init(|| { if !kfd::is_available() { return None; } HsaDevice::open().ok().map(Mutex::new) }).as_ref().map(|m| m.lock().unwrap()) } /// cpu reference matvec fn cpu_matvec_timed(cpu: &CpuBackend, w: &[f32], b: &[f32], x: &[f32], y: &mut [f32], out: usize, inp: usize, iters: usize) -> f64 { let t = Instant::now(); for _ in 0..iters { cpu.matvec(w, b, x, y, out, inp); } t.elapsed().as_micros() as f64 / iters as f64 } /// gpu matvec — allocate once, dispatch N times fn gpu_matvec_timed(dev: &mut HsaDevice, w: &[f32], b: &[f32], x: &[f32], out: u32, inp: u32, iters: usize) -> f64 { let w_buf = dev.upload_f32(w).unwrap(); let b_buf = dev.upload_f32(b).unwrap(); let x_buf = dev.upload_f32(x).unwrap(); let y_buf = dev.alloc.alloc_userptr_public(((out as u64) * 4 + 4095) & !4095).unwrap(); let mut args = KernArgs::new(); args.push_ptr(&w_buf); args.push_ptr(&b_buf); args.push_ptr(&x_buf); args.push_ptr(&y_buf); args.push_u32(out); args.push_u32(inp); let args_buf = args.upload(&dev.alloc).unwrap(); let block = out.min(256); // warmup dev.dispatch_kernel("matvec", &args_buf, [out, 1, 1], [block, 1, 1]); let t = Instant::now(); for _ in 0..iters { dev.dispatch_kernel("matvec", &args_buf, [out, 1, 1], [block, 1, 1]); } t.elapsed().as_micros() as f64 / iters as f64 } #[test] fn bench_matvec_sizes() { let mut dev = match open_gpu() { Some(d) => d, None => { eprintln!(" SKIP: no gpu"); return; }}; if dev.kernels.is_none() { eprintln!(" SKIP: no kernels"); return; } let cpu = CpuBackend::new(); println!(); println!(" {:>6} x {:<6} {:>10} {:>10} {:>8} winner", "out", "in", "cpu (us)", "gpu (us)", "speedup"); println!(" {}", "-".repeat(62)); for &(out, inp) in &[ (4, 4), (8, 8), (16, 16), (32, 32), (64, 64), (128, 128), (256, 256), (512, 512), (1024, 1024), (64, 1024), (1024, 64), ] { let n = out * inp; let w: Vec = (0..n).map(|i| ((i * 7 + 3) % 100) as f32 / 100.0 - 0.5).collect(); let b: Vec = (0..out).map(|i| (i % 10) as f32 * 0.1).collect(); let x: Vec = (0..inp).map(|i| ((i * 13 + 5) % 100) as f32 / 100.0).collect(); let mut y_cpu = vec![0.0f32; out]; let iters = if n < 10000 { 1000 } else if n < 100000 { 100 } else { 10 }; let cpu_us = cpu_matvec_timed(&cpu, &w, &b, &x, &mut y_cpu, out, inp, iters); let gpu_us = gpu_matvec_timed(&mut dev, &w, &b, &x, out as u32, inp as u32, iters); let speedup = cpu_us / gpu_us; let winner = if gpu_us < cpu_us { "gpu" } else { "cpu" }; println!(" {:>6} x {:<6} {:>10.1} {:>10.1} {:>7.2}x {}", out, inp, cpu_us, gpu_us, speedup, winner); } println!(); // isis-relevant sizes (from CTM config) println!(" isis workload sizes:"); println!(" {:>6} x {:<6} {:>10} {:>10} {:>8} context", "out", "in", "cpu (us)", "gpu (us)", "speedup"); println!(" {}", "-".repeat(72)); let isis_sizes = [ (128, 20, "input synapse (d_input=20 → 128 neurons)"), (256, 128, "attention synapse (128 → 256)"), (256, 256, "output synapse (256 → 256)"), (128, 256, "motor synapse (256 → 128)"), (1024, 20, "large input (d_input=20 → 1024)"), (1024, 1024, "large attention (1024 → 1024)"), (4096, 1024, "xl attention (1024 → 4096)"), (256, 4096, "sync accumulator readout"), ]; for &(out, inp, label) in &isis_sizes { let n = out * inp; let w: Vec = (0..n).map(|i| ((i * 7 + 3) % 100) as f32 / 100.0 - 0.5).collect(); let b: Vec = (0..out).map(|i| (i % 10) as f32 * 0.1).collect(); let x: Vec = (0..inp).map(|i| ((i * 13 + 5) % 100) as f32 / 100.0).collect(); let mut y_cpu = vec![0.0f32; out]; let iters = if n < 10000 { 1000 } else if n < 100000 { 100 } else { 10 }; let cpu_us = cpu_matvec_timed(&cpu, &w, &b, &x, &mut y_cpu, out, inp, iters); let gpu_us = gpu_matvec_timed(&mut dev, &w, &b, &x, out as u32, inp as u32, iters); let speedup = cpu_us / gpu_us; println!(" {:>6} x {:<6} {:>10.1} {:>10.1} {:>7.2}x {}", out, inp, cpu_us, gpu_us, speedup, label); } // llm-scale matmul sizes (matvec = batch=1 inference) println!(" llm inference sizes (batch=1 matvec):"); println!(" {:>6} x {:<6} {:>10} {:>10} {:>8} context", "out", "in", "cpu (us)", "gpu (us)", "speedup"); println!(" {}", "-".repeat(72)); let llm_sizes = [ (896, 896, "qwen2.5-0.5B hidden"), (4864, 896, "qwen2.5-0.5B mlp up"), (896, 4864, "qwen2.5-0.5B mlp down"), (2048, 2048, "llama-1B hidden"), (5632, 2048, "llama-1B mlp up"), ]; for &(out, inp, label) in &llm_sizes { let n = out * inp; let w: Vec = (0..n).map(|i| ((i * 7 + 3) % 100) as f32 / 100.0 - 0.5).collect(); let b = vec![0.0f32; out]; let x: Vec = (0..inp).map(|i| ((i * 13 + 5) % 100) as f32 / 100.0).collect(); let mut y_cpu = vec![0.0f32; out]; let iters = 10; let cpu_us = cpu_matvec_timed(&cpu, &w, &b, &x, &mut y_cpu, out, inp, iters); let gpu_us = gpu_matvec_timed(&mut dev, &w, &b, &x, out as u32, inp as u32, iters); let speedup = cpu_us / gpu_us; let flops = (out * inp * 2) as f64; let gpu_gflops = flops / (gpu_us * 1e3); // GFLOP/s let cpu_gflops = flops / (cpu_us * 1e3); println!(" {:>6} x {:<6} {:>8.0} ({:>4.1} GF/s) {:>8.0} ({:>4.1} GF/s) {:>5.2}x {}", out, inp, cpu_us, cpu_gflops, gpu_us, gpu_gflops, speedup, label); } println!(); }