use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
use std::sync::{Mutex, OnceLock};
use crate::prelude::{ComputeClient, Runtime};
pub struct Variant<'a, T> {
pub name: &'static str,
f: Box<dyn Fn(usize) -> (T, f64) + 'a>,
}
impl<'a, T> Variant<'a, T> {
pub fn new(name: &'static str, f: impl Fn(usize) -> (T, f64) + 'a) -> Self {
Variant { name, f: Box::new(f) }
}
}
#[derive(Debug)]
pub struct Pick<T> {
pub output: T,
pub winner: String,
pub from_cache: bool,
pub benched: usize,
pub timings: Vec<(String, f64)>,
}
const TUNE_ITERS: usize = 25;
#[derive(Default)]
struct Inner {
winners: HashMap<(String, String, String), String>,
loaded: HashSet<String>,
}
pub struct Tuner {
cache_root: PathBuf,
inner: Mutex<Inner>,
}
impl Tuner {
pub fn new(cache_root: impl Into<PathBuf>) -> Self {
Tuner { cache_root: cache_root.into(), inner: Mutex::new(Inner::default()) }
}
fn device_file(&self, device: &str) -> PathBuf {
self.cache_root.join("hanzo-kernel").join("autotune").join(format!("{}.tsv", sanitize(device)))
}
pub fn select<T>(&self, device: &str, op: &str, key: &str, variants: Vec<Variant<'_, T>>) -> Pick<T> {
assert!(!variants.is_empty(), "tune::select needs at least one variant");
self.ensure_loaded(device);
let ck = (device.to_string(), op.to_string(), key.to_string());
let cached = self.inner.lock().expect("tuner cache poisoned").winners.get(&ck).cloned();
if let Some(winner) = cached {
if let Some(v) = variants.iter().find(|v| v.name == winner) {
let (output, _ms) = (v.f)(1);
return Pick { output, winner, from_cache: true, benched: 0, timings: Vec::new() };
}
}
let mut timings: Vec<(String, f64)> = Vec::with_capacity(variants.len());
let mut best: Option<(usize, f64)> = None;
let mut outputs: Vec<Option<T>> = Vec::with_capacity(variants.len());
for (i, v) in variants.iter().enumerate() {
let (out, ms) = (v.f)(TUNE_ITERS);
timings.push((v.name.to_string(), ms));
outputs.push(Some(out));
if best.map(|(_, bms)| ms < bms).unwrap_or(true) {
best = Some((i, ms));
}
}
let (bi, _) = best.expect("at least one variant timed");
let winner = variants[bi].name.to_string();
let output = outputs[bi].take().expect("winner output present");
self.record(device, op, key, &winner);
timings.sort_by(|a, b| a.1.total_cmp(&b.1));
Pick { output, winner, from_cache: false, benched: variants.len(), timings }
}
fn ensure_loaded(&self, device: &str) {
{
let inner = self.inner.lock().expect("tuner cache poisoned");
if inner.loaded.contains(device) {
return;
}
}
let entries = read_device_file(&self.device_file(device));
let mut inner = self.inner.lock().expect("tuner cache poisoned");
for (op, key, winner) in entries {
inner.winners.insert((device.to_string(), op, key), winner);
}
inner.loaded.insert(device.to_string());
}
fn record(&self, device: &str, op: &str, key: &str, winner: &str) {
{
let mut inner = self.inner.lock().expect("tuner cache poisoned");
inner
.winners
.insert((device.to_string(), op.to_string(), key.to_string()), winner.to_string());
}
append_device_file(&self.device_file(device), op, key, winner);
}
pub fn cached_winner(&self, device: &str, op: &str, key: &str) -> Option<String> {
self.ensure_loaded(device);
self.inner
.lock()
.expect("tuner cache poisoned")
.winners
.get(&(device.to_string(), op.to_string(), key.to_string()))
.cloned()
}
}
pub fn global() -> &'static Tuner {
static G: OnceLock<Tuner> = OnceLock::new();
G.get_or_init(|| Tuner::new(xdg_cache_dir()))
}
pub fn xdg_cache_dir() -> PathBuf {
if let Some(x) = std::env::var_os("XDG_CACHE_HOME") {
if !x.is_empty() {
return PathBuf::from(x);
}
}
if let Some(h) = std::env::var_os("HOME") {
if !h.is_empty() {
return PathBuf::from(h).join(".cache");
}
}
PathBuf::from(".")
}
pub fn device_id<R: Runtime>(_client: &ComputeClient<R>) -> String {
let name = std::any::type_name::<R>();
name.rsplit("::").next().unwrap_or(name).to_string()
}
fn sanitize(s: &str) -> String {
s.chars().map(|c| if c.is_ascii_alphanumeric() || c == '-' || c == '_' { c } else { '_' }).collect()
}
fn cell(s: &str) -> String {
s.chars().map(|c| if c == '\t' || c == '\n' || c == '\r' { ' ' } else { c }).collect()
}
fn read_device_file(path: &Path) -> Vec<(String, String, String)> {
let Ok(text) = std::fs::read_to_string(path) else {
return Vec::new();
};
let mut map: HashMap<(String, String), String> = HashMap::new();
for line in text.lines() {
let mut it = line.splitn(3, '\t');
if let (Some(op), Some(key), Some(winner)) = (it.next(), it.next(), it.next()) {
map.insert((op.to_string(), key.to_string()), winner.to_string());
}
}
map.into_iter().map(|((op, key), winner)| (op, key, winner)).collect()
}
fn append_device_file(path: &Path, op: &str, key: &str, winner: &str) {
use std::io::Write;
if let Some(dir) = path.parent() {
let _ = std::fs::create_dir_all(dir);
}
if let Ok(mut f) = std::fs::OpenOptions::new().create(true).append(true).open(path) {
let _ = writeln!(f, "{}\t{}\t{}", cell(op), cell(key), cell(winner));
}
}
pub struct Tuned<'a, T> {
op: &'static str,
key: String,
variants: Vec<Variant<'a, T>>,
}
impl<'a, T> Tuned<'a, T> {
pub fn new(op: &'static str, key: impl Into<String>) -> Self {
Tuned { op, key: key.into(), variants: Vec::new() }
}
pub fn variant(mut self, name: &'static str, f: impl Fn(usize) -> (T, f64) + 'a) -> Self {
self.variants.push(Variant::new(name, f));
self
}
pub fn pick_with(self, tuner: &Tuner, device: &str) -> Pick<T> {
tuner.select(device, self.op, &self.key, self.variants)
}
pub fn pick<R: Runtime>(self, client: &ComputeClient<R>) -> Pick<T> {
let device = device_id::<R>(client);
self.pick_with(global(), &device)
}
pub fn run<R: Runtime>(self, client: &ComputeClient<R>) -> T {
self.pick(client).output
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn select_caches_and_reloads_from_disk() {
let dir = tmp_dir("select");
let variants = || {
vec![
Variant::new("slow", |_it| (10u32, 9.0)),
Variant::new("fast", |_it| (20u32, 1.0)), Variant::new("mid", |_it| (30u32, 5.0)),
]
};
let t1 = Tuner::new(&dir);
let p = t1.select("cpu", "op", "s1", variants());
assert!(!p.from_cache);
assert_eq!(p.benched, 3);
assert_eq!(p.winner, "fast");
assert_eq!(p.output, 20);
assert_eq!(p.timings.first().unwrap().0, "fast");
let p = t1.select("cpu", "op", "s1", variants());
assert!(p.from_cache);
assert_eq!(p.benched, 0);
assert_eq!(p.winner, "fast");
assert_eq!(p.output, 20);
let t2 = Tuner::new(&dir);
assert_eq!(t2.cached_winner("cpu", "op", "s1").as_deref(), Some("fast"));
let p = t2.select("cpu", "op", "s1", variants());
assert!(p.from_cache);
assert_eq!(p.benched, 0);
assert_eq!(p.winner, "fast");
let p = t2.select("cpu", "op", "s2", variants());
assert!(!p.from_cache);
assert_eq!(p.benched, 3);
let shrunk = vec![Variant::new("slow", |_it| (10u32, 9.0)), Variant::new("mid", |_it| (30u32, 2.0))];
let p = t2.select("cpu", "op", "s1", shrunk);
assert!(!p.from_cache);
assert_eq!(p.winner, "mid");
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn disk_format_round_trips() {
let dir = tmp_dir("disk");
let t = Tuner::new(&dir);
let file = t.device_file("CpuRuntime");
append_device_file(&file, "rms_norm", "rows=8,n=4", "b64_r1");
append_device_file(&file, "matvec", "rows=8,k=64", "b128_v2");
append_device_file(&file, "rms_norm", "rows=8,n=4", "b256_r1");
let mut got = read_device_file(&file);
got.sort();
assert_eq!(
got,
vec![
("matvec".to_string(), "rows=8,k=64".to_string(), "b128_v2".to_string()),
("rms_norm".to_string(), "rows=8,n=4".to_string(), "b256_r1".to_string()),
]
);
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn device_file_path_is_xdg_shaped() {
let t = Tuner::new("/x/y");
assert_eq!(t.device_file("Cuda::Runtime").to_str().unwrap(), "/x/y/hanzo-kernel/autotune/Cuda__Runtime.tsv");
}
fn tmp_dir(tag: &str) -> PathBuf {
let p = std::env::temp_dir().join(format!(
"hanzo-kernel-tune-{tag}-{}-{}",
std::process::id(),
std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_nanos()
));
std::fs::create_dir_all(&p).unwrap();
p
}
}