//! matmul kernel benchmark — pure rust dispatch.
//! run: cargo test --release --test kfd_matmul_bench -- --nocapture --test-threads=1

use isis::host::device::kfd::{self, HsaDevice};
use isis::host::device::kfd::dispatch::{CodeObject, KernArgs};
use isis::host::device::kfd::memory::GpuBuffer;
use std::time::Instant;

static MATMUL_BLOCKED_CO: &[u8] = include_bytes!("../src/host/device/kfd/kernels/matmul_blocked.co");
static MATMUL_SMALL_CO: &[u8] = include_bytes!("../src/host/device/kfd/kernels/matmul_small.co");

#[test]
fn matmul_bench() {
    if !kfd::is_available() { eprintln!("skip: no kfd"); return; }
    let mut dev = match HsaDevice::open() { Ok(d) => d, Err(e) => { eprintln!("skip: {e}"); return; }};

    // Load both matmul kernels
    for co_bytes in [MATMUL_BLOCKED_CO, MATMUL_SMALL_CO] {
        let co = CodeObject::load(&dev.alloc, co_bytes).unwrap();
        if dev.kernels.is_none() { dev.kernels = Some(std::collections::HashMap::new()); }
        for (name, entry) in &co.kernels {
            dev.kernels.as_mut().unwrap().insert(name.clone(), entry.clone());
        }
        std::mem::forget(co); // keep code in VRAM
    }

    if let Some(mhz) = dev.current_sclk_mhz() {
        println!("  sclk: {} mhz", mhz);
    }
    println!();

    // ---- Correctness check for matmul_blocked (TM=128) ----
    {
        let m = 128u32; let k = 8u32; let n = 32u32;
        let mut w_data = vec![0.0f32; (m * k) as usize];
        for i in 0..m as usize { w_data[i * k as usize] = (i + 1) as f32; }
        let b_data = vec![0.0f32; m as usize];
        let mut x_data = vec![0.0f32; (n * k) as usize];
        for j in 0..n as usize { x_data[j * k as usize] = 1.0; }

        let w_buf = dev.upload_f32(&w_data).unwrap();
        let b_buf = dev.upload_f32(&b_data).unwrap();
        let x_buf = dev.upload_f32(&x_data).unwrap();
        let y_buf = dev.alloc_output((n as usize * m as usize * 4 + 64) as usize).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(m); args.push_u32(k); args.push_u32(n);
        let args_buf = args.upload(&dev.alloc).unwrap();

        let nwg = ((m + 127) / 128) * ((n + 31) / 32);
        dev.dispatch_kernel("matmul_blocked", &args_buf, [nwg, 1, 1], [256, 1, 1]);

        let y_slice = unsafe { std::slice::from_raw_parts(y_buf.cpu_ptr as *const f32, (n * m) as usize) };
        print!("  matmul_blocked (TM=128): ");
        let mut ok = true;
        for j in 0..n as usize {
            for i in 0..m as usize {
                let expected = (i + 1) as f32;
                if (y_slice[j * m as usize + i] - expected).abs() > 0.1 { ok = false; break; }
            }
            if !ok { break; }
        }
        println!("{}", if ok { "PASS" } else { "FAIL" });
        assert!(ok, "matmul_blocked correctness failed");
    }

    // ---- Correctness check for matmul_small (TM=32) ----
    {
        let m = 32u32; let k = 8u32; let n = 32u32;
        let mut w_data = vec![0.0f32; (m * k) as usize];
        for i in 0..m as usize { w_data[i * k as usize] = (i + 1) as f32; }
        let b_data = vec![0.0f32; m as usize];
        let mut x_data = vec![0.0f32; (n * k) as usize];
        for j in 0..n as usize { x_data[j * k as usize] = 1.0; }

        let w_buf = dev.upload_f32_col_major(&w_data, m as usize, k as usize).unwrap();
        let b_buf = dev.upload_f32(&b_data).unwrap();
        let x_buf = dev.upload_f32(&x_data).unwrap();
        let y_buf = dev.alloc_output((n as usize * m as usize * 4 + 64) as usize).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(m); args.push_u32(k); args.push_u32(n);
        let args_buf = args.upload(&dev.alloc).unwrap();

        let nwg = ((m + 31) / 32) * ((n + 31) / 32);
        dev.dispatch_kernel("matmul_small", &args_buf, [nwg, 1, 1], [256, 1, 1]);

        let y_slice = unsafe { std::slice::from_raw_parts(y_buf.cpu_ptr as *const f32, (n * m) as usize) };
        print!("  matmul_small   (TM=32):  ");
        let mut ok = true;
        for j in 0..n as usize {
            for i in 0..m as usize {
                let expected = (i + 1) as f32;
                if (y_slice[j * m as usize + i] - expected).abs() > 0.1 { ok = false; break; }
            }
            if !ok { break; }
        }
        println!("{}", if ok { "PASS" } else { "FAIL" });
        assert!(ok, "matmul_small correctness failed");
    }
    println!();

    // ---- Performance benchmark: both kernels + dispatch selector ----
    let shapes: &[(u32, u32, u32, &str)] = &[
        (512,  512,  32, ""),
        (1024, 1024, 32, ""),
        (2048, 2048, 32, ""),
        (4096, 4096, 32, "qwen attn"),
        (4096, 11008, 32, "qwen mlp"),
        (4864, 896,  32, "isis layer"),
    ];

    // bench helper
    let bench_kernel = |dev: &mut HsaDevice, kernel: &str, m: u32, k: u32, n: u32,
                        w_buf: &GpuBuffer, b_buf: &GpuBuffer, x_buf: &GpuBuffer, y_buf: &GpuBuffer| -> f64 {
        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(m); args.push_u32(k); args.push_u32(n);
        let args_buf = args.upload(&dev.alloc).unwrap();

        let (nwg, block) = if kernel == "matmul_blocked" {
            (((m + 127) / 128) * ((n + 31) / 32), [256u32, 1, 1])
        } else {
            (((m + 31) / 32) * ((n + 31) / 32), [256u32, 1, 1])
        };
        let grid = [nwg, 1, 1];

        for _ in 0..50 { dev.dispatch_enqueue(kernel, &args_buf, grid, block); }
        assert!(dev.submit_wait(30_000), "warmup timeout");

        let iters = 500;
        let t0 = Instant::now();
        for _ in 0..iters { dev.dispatch_enqueue(kernel, &args_buf, grid, block); }
        assert!(dev.submit_wait(60_000), "bench timeout");
        t0.elapsed().as_nanos() as f64 / iters as f64 / 1000.0
    };

    println!("  {:>12}  {:>10}  {:>10}  {:>10}  {:>6}", "shape", "TM=128", "TM=32", "best", "pick");
    println!("  {}", "-".repeat(58));

    for &(m, k, n, label) in shapes {
        // Allocate buffers (both W formats)
        let w_data = vec![0.001f32; (m * k) as usize];
        let w_row = dev.upload_f32(&w_data).unwrap();
        let w_col = dev.upload_f32_col_major(&w_data, m as usize, k as usize).unwrap();
        let b_buf = dev.upload_f32(&vec![0.0f32; m as usize]).unwrap();
        let x_buf = dev.upload_f32(&vec![0.001f32; (n * k) as usize]).unwrap();
        let y_buf = dev.alloc_output((n as usize * m as usize * 4 + 64) as usize).unwrap();

        let us_128 = bench_kernel(&mut dev, "matmul_blocked", m, k, n, &w_row, &b_buf, &x_buf, &y_buf);
        let us_32  = bench_kernel(&mut dev, "matmul_small",   m, k, n, &w_col, &b_buf, &x_buf, &y_buf);

        let gf_128 = 2.0 * m as f64 * k as f64 * n as f64 / us_128 / 1e3;
        let gf_32  = 2.0 * m as f64 * k as f64 * n as f64 / us_32  / 1e3;
        let (best, pick) = if gf_128 >= gf_32 { (gf_128, "TM128") } else { (gf_32, "TM32") };

        let l = if label.is_empty() { String::new() } else { format!("  {}", label) };
        println!("  {:>5}x{:<5}  {:>8.0}  {:>8.0}  {:>8.0}  {:>6}{}",
            m, k, gf_128, gf_32, best, pick, l);
    }
    println!();
}
