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
);
}
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;
}
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;
}
}
let trend = guard.timing_trend();
let history_len = guard.timing_history.len();
let should_rebench = if trend > 0.05 && history_len >= 8 {
true
} else if guard.observation_count >= 10_000 {
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();
}
}