himada-dispatch 0.1.1

Adaptive SIMD dispatch for Himada — auto-selects fastest kernel at runtime
use std::path::PathBuf;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
use std::thread::{self, JoinHandle};
use std::time::{Duration, Instant};

use himada_core::thermal::ThermalState;
use serde::{Deserialize, Serialize};

use crate::{
    AddF32Kernel, ArgmaxF32Kernel, ArgmaxKernel, BinaryOpKernel, ClampKernel, Dispatch,
    DotF32Kernel, DotKernel, FnBench, HwdnaError, MatMulBiasReluKernel, MatMulF32Kernel,
    MatMulKernel, MemchrKernel, ReduceKernel, ReduceSumF32Kernel, SoftmaxKernel, UnaryOpF32Kernel,
};

#[derive(Serialize, Deserialize)]
struct AdaptEvent {
    timestamp: String,
    kernel_before: String,
    kernel_after: String,
    reason: String,
}

fn adapt_log_path() -> PathBuf {
    let base = std::env::var("HWDNA_HOME")
        .ok()
        .map(PathBuf::from)
        .or_else(dirs::home_dir)
        .unwrap_or_else(|| PathBuf::from("."));
    let dir = base.join(".himada");
    std::fs::create_dir_all(&dir).ok();
    dir.join("adapt.json")
}

fn log_adapt(dispatch_name: &str, from: &str, to: &str, reason: &str) {
    let path = adapt_log_path();
    let mut history: Vec<AdaptEvent> = std::fs::read_to_string(&path)
        .ok()
        .and_then(|s| serde_json::from_str(&s).ok())
        .unwrap_or_default();

    history.push(AdaptEvent {
        timestamp: format!("{:?}", std::time::SystemTime::now()),
        kernel_before: from.into(),
        kernel_after: to.into(),
        reason: reason.into(),
    });

    if let Ok(json) = serde_json::to_string_pretty(&history) {
        std::fs::write(&path, json).ok();
    }

    eprintln!(
        "[himada::{}] adapt: {} -> {} ({})",
        dispatch_name, from, to, reason
    );
}

/// Adaptive engine that periodically re-benchmarks kernels and switches
/// based on performance or thermal conditions.
pub struct AdaptiveEngine<F> {
    dispatch: Arc<Mutex<Dispatch<F>>>,
    stop: Arc<AtomicBool>,
    interval: Duration,
    handle: Option<JoinHandle<()>>,
}

impl<F> AdaptiveEngine<F> {
    pub fn new(dispatch: Dispatch<F>, interval: Duration) -> Self {
        Self {
            dispatch: Arc::new(Mutex::new(dispatch)),
            stop: Arc::new(AtomicBool::new(false)),
            interval,
            handle: None,
        }
    }

    pub fn stop(&mut self) {
        self.stop.store(true, Ordering::Relaxed);
        if let Some(handle) = self.handle.take() {
            handle.join().ok();
        }
    }

    pub fn selected_name(&self) -> String {
        self.dispatch
            .lock()
            .map(|g| g.selected_name().to_string())
            .unwrap_or_else(|_| "poisoned".into())
    }
}

impl<F: FnBench> AdaptiveEngine<F> {
    pub fn start(&mut self) {
        let dispatch = self.dispatch.clone();
        let stop = self.stop.clone();
        let interval = self.interval;

        self.handle = Some(thread::spawn(move || {
            while !stop.load(Ordering::Relaxed) {
                thread::sleep(interval);
                if stop.load(Ordering::Relaxed) {
                    break;
                }

                let thermal = ThermalState::collect();
                let mut guard = match dispatch.lock() {
                    Ok(g) => g,
                    Err(_) => continue,
                };
                let available = guard.available_indices();
                if available.is_empty() {
                    continue;
                }

                // Thermal-aware re-selection
                if let Some(pressure) = thermal.pressure {
                    if pressure > 50 {
                        let coolest = available
                            .iter()
                            .min_by_key(|&&i| guard.supported[i].thermal_priority)
                            .copied();
                        if let Some(idx) = coolest {
                            let current = guard.selected;
                            if current != Some(idx) {
                                let before = guard.selected_name().to_string();
                                guard.selected = Some(idx);
                                log_adapt(
                                    &guard.name,
                                    &before,
                                    guard.selected_name(),
                                    &format!("thermal(pressure={})", pressure),
                                );
                            }
                        }
                        continue;
                    }
                }

                // Use real-world timing history for trend analysis
                let trend = guard.timing_trend();
                let history_len = guard.timing_history.len();

                // Re-benchmark if we have enough observations and trend shows degradation
                // or periodically regardless
                let should_rebench = if trend > 0.05 && history_len >= 8 {
                    // 5%+ slowdown detected — re-evaluate
                    true
                } else if guard.observation_count >= 10_000 {
                    // Periodic full re-benchmark
                    true
                } else {
                    false
                };

                if should_rebench {
                    let before = guard.selected_name().to_string();
                    if let Ok(new_idx) = guard.rebench() {
                        if guard.selected != Some(new_idx) {
                            log_adapt(
                                &guard.name,
                                &before,
                                guard.selected_name(),
                                &format!(
                                    "online(trend={:.2}%, obs={})",
                                    trend * 100.0,
                                    guard.observation_count
                                ),
                            );
                        }
                    }
                }
            }
        }));
    }
}

macro_rules! make_ae_compute {
    ($kernel:ty, ($($arg:ident: $argty:ty),*) -> $ret:ty, $call:expr) => {
        impl AdaptiveEngine<$kernel> {
            pub fn try_compute(&self, $($arg: $argty),*) -> Result<$ret, HwdnaError> {
                let mut guard = self.dispatch.lock().map_err(|_| HwdnaError::MutexPoisoned)?;
                let start = Instant::now();
                let result = guard.try_compute($($arg),*);
                let elapsed = start.elapsed();
                guard.observe(elapsed);
                result
            }
            pub fn compute(&self, $($arg: $argty),*) -> $ret {
                self.try_compute($($arg),*)
                    .expect("AdaptiveEngine::compute failed")
            }
        }
    };
}

make_ae_compute!(DotKernel, (a: &[f64], b: &[f64]) -> f64, |s, a, b| s.compute(a, b));
make_ae_compute!(ReduceKernel, (a: &[f64]) -> f64, |s, a| s.compute(a));
make_ae_compute!(SoftmaxKernel, (input: &[f64], output: &mut [f64]) -> (), |s, input, output| s.compute(input, output));
make_ae_compute!(MemchrKernel, (byte: u8, data: &[u8]) -> Option<usize>, |s, byte, data| s.compute(byte, data));
make_ae_compute!(MatMulKernel, (a: &[f64], b: &[f64], c: &mut [f64], n: usize) -> (), |s, a, b, c, n| s.compute(a, b, c, n));
make_ae_compute!(ArgmaxKernel, (a: &[f64]) -> usize, |s, a| s.compute(a));
make_ae_compute!(BinaryOpKernel, (a: &[f64], b: &[f64], c: &mut [f64]) -> (), |s, a, b, c| s.compute(a, b, c));
make_ae_compute!(ClampKernel, (a: &[f64], lo: f64, hi: f64, b: &mut [f64]) -> (), |s, a, lo, hi, b| s.compute(a, lo, hi, b));
make_ae_compute!(DotF32Kernel, (a: &[f32], b: &[f32]) -> f32, |s, a, b| s.compute(a, b));
make_ae_compute!(ReduceSumF32Kernel, (a: &[f32]) -> f32, |s, a| s.compute(a));
make_ae_compute!(AddF32Kernel, (a: &[f32], b: &[f32], c: &mut [f32]) -> (), |s, a, b, c| s.compute(a, b, c));
make_ae_compute!(UnaryOpF32Kernel, (a: &[f32], b: &mut [f32]) -> (), |s, a, b| s.compute(a, b));
make_ae_compute!(ArgmaxF32Kernel, (a: &[f32]) -> usize, |s, a| s.compute(a));
make_ae_compute!(MatMulF32Kernel, (a: &[f32], b: &[f32], c: &mut [f32], n: usize) -> (), |s, a, b, c, n| s.compute(a, b, c, n));
make_ae_compute!(MatMulBiasReluKernel, (a: &[f64], w: &[f64], bias: &[f64], c: &mut [f64], m: usize, k: usize, n: usize) -> (), |s, a, w, bias, c, m, k, n| s.compute(a, w, bias, c, m, k, n));

impl<F> Drop for AdaptiveEngine<F> {
    fn drop(&mut self) {
        self.stop();
    }
}