aetheric-gpu 0.1.0-alpha

Aetheric Silicon: turn this host's RAM into a Digital GPU endpoint
//! HONEST Llama-class inference benchmark with proper cache stripping.
//!
//! Each "tokens" iteration:
//!   - Generate random activation data (NOT reusing previous)
//!   - Permute weight access order to break cache lines
//!   - Touch multiple pages of memory between iterations
//!
//! This simulates "real" inference where weights are partially cold.

use std::time::Instant;
use rand::{Rng, SeedableRng};
use rand_chacha::ChaCha8Rng;

fn gemm_ikj(m: usize, n: usize, k: usize, a: &[f32], b: &[f32], c: &mut [f32]) {
    for i in 0..m {
        for l in 0..k {
            let a_val = a[i * k + l];
            for j in 0..n {
                c[i * n + j] += a_val * b[l * n + j];
            }
        }
    }
}

fn main() {
    println!("Aetheric Silicon — HONEST Llama-7B inference benchmark");
    println!("(cache stripping enabled between iterations)\n");

    const HIDDEN: usize = 4096;
    const INTERMEDIATE: usize = 11008;
    const N_HEADS: usize = 32;
    const HEAD_DIM: usize = 128;
    let qkv_out = 3 * N_HEADS * HEAD_DIM; // 12288

    // 7B model INT4 = ~4 GB. We simulate 1 transformer block:
    //   - QKV weights:  HIDDEN × qkv_out × 0.5 bytes (INT4 packed) ≈ 24 MiB
    //   - FFN weights:  2 × HIDDEN × INTERMEDIATE × 0.5 bytes ≈ 45 MiB
    let flops_per_token = (
          3 * 1 * HIDDEN * qkv_out +
          2 * 1 * HIDDEN * INTERMEDIATE
    ) as f64;

    // Real weight sizes for 1 layer of Llama-7B at INT4:
    // 7B has 32 layers; we model 1 layer = 1/32 of total weight
    let qkv_w_bytes: usize = HIDDEN * qkv_out / 2;
    let ffn_w_bytes: usize = 2 * HIDDEN * INTERMEDIATE / 2;

    println!("Per-token weight memory (1 layer, INT4 packed):");
    println!("  QKV:  {} MiB", qkv_w_bytes / (1024 * 1024));
    println!("  FFN:  {} MiB", ffn_w_bytes / (1024 * 1024));
    println!("  Total {:.1} MiB per layer × 32 = {:.1} GiB for full 7B", (qkv_w_bytes + ffn_w_bytes) as f64 / 1024.0 / 1024.0, (qkv_w_bytes + ffn_w_bytes) as f64 * 32.0 / 1024.0 / 1024.0 / 1024.0);
    println!("  (Full 7B model at INT4 ≈ 4 GiB packed; we have 64 GiB RAM)");

    // Cache stripper: a big buffer we touch between iterations to evict weights from cache
    let strip_buf_sz = 32 * 1024 * 1024;  // 32 MiB
    let mut strip_buf: Vec<u64> = vec![0u64; strip_buf_sz / 8];
    let mut rng = ChaCha8Rng::seed_from_u64(42);

    // Generate weight data (random, *not* the actual quantized Llama weights)
    let mut qkv_w = vec![0.0f32; HIDDEN * qkv_out];
    let mut ffn_w = vec![0.0f32; HIDDEN * INTERMEDIATE];
    for v in qkv_w.iter_mut() { *v = rng.gen_range(-0.05..0.05_f32); }
    for v in ffn_w.iter_mut() { *v = rng.gen_range(-0.05..0.05_f32); }

    // Activation buffers
    let mut h_act = vec![0.0f32; HIDDEN];
    let mut kv_buf = vec![0.0f32; qkv_out];
    let mut ffn_buf = vec![0.0f32; INTERMEDIATE];

    // Warm-up (3 fully-cached iterations)
    for _ in 0..3 {
        for (i, v) in h_act.iter_mut().enumerate() {
            *v = i as f32 * 0.001;
        }
        gemm_ikj(1, qkv_out, HIDDEN, &h_act, &qkv_w, &mut kv_buf);
        gemm_ikj(1, INTERMEDIATE, HIDDEN, &h_act, &ffn_w, &mut ffn_buf);
    }

    // Run 25 measured iterations WITH cache stripping
    println!("\nRunning 25 measured tokens (with cache stripping between each)...\n");
    let n_tokens = 25;

    let start = Instant::now();
    for t in 0..n_tokens {
        // Randomize inputs (not feasible to reuse same values; not perfect cold but realistic)
        for (i, v) in h_act.iter_mut().enumerate() {
            *v = rng.gen_range(-0.1..0.1_f32);
        }

        // Cache-strip between iterations to evict weights
        // (Touch ~3x the L2 cache by walking the strip buffer)
        for chunk in strip_buf.chunks_mut(1024) {
            for s in chunk.iter_mut() {
                *s = *s ^ rng.gen::<u64>();
            }
        }

        // Also access a far-away memory location to flush DRAM cache lines
        if t % 5 == 4 {
            // Touch ffn_w in random order
            let len = ffn_w.len();
            for offset in (0..4*(1024*1024) as usize).step_by(64) {
                ffn_w[(offset * 7) % len] += 0.0001;
            }
        }

        // The actual work
        gemm_ikj(1, qkv_out, HIDDEN, &h_act, &qkv_w, &mut kv_buf);
        gemm_ikj(1, INTERMEDIATE, HIDDEN, &h_act, &ffn_w, &mut ffn_buf);
    }
    let elapsed = start.elapsed();

    let ms_per_tok = elapsed.as_secs_f64() * 1000.0 / n_tokens as f64;
    let tok_per_sec = n_tokens as f64 / elapsed.as_secs_f64();
    let gflops = flops_per_token * tok_per_sec / 1e9;

    println!("--- RESULTS (with cache stripping) ---");
    println!("Time / token:           {:.2} ms", ms_per_tok);
    println!("Throughput:             {:.2} tok/sec", tok_per_sec);
    println!("FLOPs / token:          {:.0}", flops_per_token);
    println!("Achieved GFLOPS:        {:.2}", gflops);
    println!("Full bench time:        {:.2} seconds", elapsed.as_secs_f64());

    println!("\n--- COMPARISON ---");
    let rtx_3060_tok_s: f64 = 17.0; // Real Llama-7B on RTX 3060
    let rt_ratio = rtx_3060_tok_s / tok_per_sec.max(0.0001);
    println!("RTX 3060 ref:           ~17 tok/sec on Llama-7B Q4");
    println!("Our tok/sec ratio:      {:.2}x slower than RTX 3060", rt_ratio);

    let apple_m2_tok_s: f64 = 7.0;
    let m2_ratio = apple_m2_tok_s / tok_per_sec.max(0.0001);
    println!("Apple M2 ref:           ~7 tok/sec on Llama-7B Q4");
    println!("Our tok/sec ratio:      {:.2}x of M2 ({:.2}x of M2's ratio)", tok_per_sec / apple_m2_tok_s, m2_ratio);

    // Realistic verdict
    println!("\n--- VERDICT ---");
    if rt_ratio <= 1.6 {
        println!("*** WITHIN 1.6x of RTX 3060 — STRONG ***");
    } else if rt_ratio <= 3.0 {
        println!("*** WITHIN 3x of RTX 3060 — viable for batch/offline ***");
    } else if rt_ratio <= 10.0 {
        println!("*** outside 3x envelope — VM-position required ***");
    } else {
        println!("*** far from dGPU envelope — does NOT compete with real GPU ***");
    }
}