use crate::bench::{do_bench, BenchOptions, Measurement};
use crate::error::Error;
use cuda_core::Stream;
use cutile_compiler::jit_cache::L2_KEY_SCHEMA;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::io::Write;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::{Duration, Instant};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Config {
pub id: String,
pub params: BTreeMap<String, ParamValue>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum ParamValue {
Int(i64),
Str(String),
}
impl Config {
pub fn new<I, K>(params: I) -> Self
where
I: IntoIterator<Item = (K, ParamValue)>,
K: Into<String>,
{
let params: BTreeMap<String, ParamValue> =
params.into_iter().map(|(k, v)| (k.into(), v)).collect();
let id = params
.iter()
.map(|(k, v)| {
let key = if k.contains(['=', ',', '"']) {
serde_json::to_string(k).unwrap_or_else(|_| format!("{k:?}"))
} else {
k.clone()
};
match v {
ParamValue::Int(i) => format!("{key}={i}"),
ParamValue::Str(s) => format!(
"{key}={}",
serde_json::to_string(s).unwrap_or_else(|_| format!("{s:?}"))
),
}
})
.collect::<Vec<_>>()
.join(",");
Self { id, params }
}
pub fn int(&self, key: &str) -> Option<i64> {
match self.params.get(key) {
Some(ParamValue::Int(i)) => Some(*i),
_ => None,
}
}
pub fn str(&self, key: &str) -> Option<&str> {
match self.params.get(key) {
Some(ParamValue::Str(s)) => Some(s.as_str()),
_ => None,
}
}
}
pub fn space_hash(configs: &[Config]) -> String {
let mut ids: Vec<&str> = configs.iter().map(|c| c.id.as_str()).collect();
ids.sort_unstable();
let mut h: u64 = 0xcbf2_9ce4_8422_2325;
for id in ids {
for b in id.as_bytes().iter().chain(&[0u8]) {
h ^= u64::from(*b);
h = h.wrapping_mul(0x1000_0000_01b3);
}
}
format!("{h:016x}")
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct Trial {
pub config_id: String,
pub state: TrialState,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub enum TrialState {
Measured {
median_ms: f32,
min_ms: f32,
reps: usize,
},
Invalid { reason: String },
}
impl TrialState {
fn measured_or_invalid(median_ms: f32, min_ms: f32, reps: usize) -> Self {
if !median_ms.is_finite() || !min_ms.is_finite() {
return TrialState::Invalid {
reason: format!("non-finite timing (median {median_ms}, min {min_ms})"),
};
}
TrialState::Measured {
median_ms,
min_ms,
reps,
}
}
}
impl Trial {
pub fn measured(
config_id: impl Into<String>,
median_ms: f32,
min_ms: f32,
reps: usize,
) -> Self {
Self {
config_id: config_id.into(),
state: TrialState::measured_or_invalid(median_ms, min_ms, reps),
}
}
pub fn invalid(config_id: impl Into<String>, reason: impl Into<String>) -> Self {
Self {
config_id: config_id.into(),
state: TrialState::Invalid {
reason: reason.into(),
},
}
}
pub fn median_ms(&self) -> Option<f32> {
match &self.state {
TrialState::Measured { median_ms, .. } => Some(*median_ms),
TrialState::Invalid { .. } => None,
}
}
}
pub trait Objective {
fn configs(&self) -> &[Config];
fn measure(&mut self, index: usize) -> Trial;
fn budget_remaining(&self) -> Option<Duration>;
}
pub trait Searcher {
fn search(&mut self, objective: &mut dyn Objective) -> Vec<Trial>;
}
#[derive(Default)]
pub struct GridSearch {
known: Vec<Trial>,
}
impl GridSearch {
pub fn new() -> Self {
Self::default()
}
pub fn resume(mut self, known: Vec<Trial>) -> Self {
self.known = known;
self
}
}
impl Searcher for GridSearch {
fn search(&mut self, objective: &mut dyn Objective) -> Vec<Trial> {
let current: std::collections::BTreeSet<&str> =
objective.configs().iter().map(|c| c.id.as_str()).collect();
let mut trials: Vec<Trial> = std::mem::take(&mut self.known)
.into_iter()
.filter(|t| current.contains(t.config_id.as_str()) && t.median_ms().is_some())
.collect();
let visited: std::collections::BTreeSet<String> =
trials.iter().map(|t| t.config_id.clone()).collect();
let todo: Vec<usize> = (0..objective.configs().len())
.filter(|i| !visited.contains(&objective.configs()[*i].id))
.collect();
for index in todo {
if objective.budget_remaining() == Some(Duration::ZERO) {
break;
}
trials.push(objective.measure(index));
}
trials
}
}
pub fn best_config<'a>(configs: &'a [Config], trials: &[Trial]) -> Option<&'a Config> {
let mut best: Option<(&'a Config, f32)> = None;
for t in trials {
let Some(ms) = t.median_ms() else { continue };
if !ms.is_finite() {
continue;
}
let Some(config) = configs.iter().find(|c| c.id == t.config_id) else {
continue;
};
if best.is_none_or(|(_, b)| ms < b) {
best = Some((config, ms));
}
}
best.map(|(c, _)| c)
}
type PrunePredicate = Box<dyn Fn(&Config) -> bool>;
pub struct Autotuner {
pub name: String,
configs: Vec<Config>,
prune: Vec<PrunePredicate>,
required: Vec<Config>,
budget: Option<Duration>,
bench: BenchOptions,
log_path: Option<PathBuf>,
provenance: LogProvenance,
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct Output {
pub trials: Vec<Trial>,
pub best: Option<Config>,
}
impl Autotuner {
pub fn new(name: &str) -> Self {
Self {
name: name.to_string(),
configs: Vec::new(),
prune: Vec::new(),
required: Vec::new(),
budget: None,
bench: BenchOptions::default(),
log_path: None,
provenance: LogProvenance::default(),
}
}
pub fn arch(mut self, arch: impl Into<String>) -> Self {
self.provenance.arch = Some(arch.into());
self
}
pub fn source_hash(mut self, source_hash: impl Into<String>) -> Self {
self.provenance.source_hash = Some(source_hash.into());
self
}
pub fn tileiras_fingerprint(mut self, fingerprint: impl Into<String>) -> Self {
self.provenance.tileiras_fingerprint = Some(fingerprint.into());
self
}
pub fn provenance(mut self, provenance: LogProvenance) -> Self {
self.provenance = provenance;
self
}
pub fn configs(mut self, configs: Vec<Config>) -> Self {
self.configs = configs;
self
}
pub fn require(mut self, configs: Vec<Config>) -> Self {
self.required.extend(configs);
self
}
pub fn prune(mut self, keep: impl Fn(&Config) -> bool + 'static) -> Self {
self.prune.push(Box::new(keep));
self
}
pub fn budget(mut self, budget: Duration) -> Self {
self.budget = Some(budget);
self
}
pub fn bench(mut self, bench: BenchOptions) -> Self {
self.bench = bench;
self
}
pub fn log(mut self, path: impl Into<PathBuf>) -> Self {
self.log_path = Some(path.into());
self
}
pub fn run<S, F>(mut self, stream: &Arc<Stream>, setup: S) -> Result<Output, Error>
where
S: FnMut(&Arc<Stream>, &Config) -> Result<F, Error>,
F: FnMut(&Arc<Stream>) -> Result<(), Error>,
{
self.apply_prune();
let mut log = TrialLog::open(
self.log_path.as_deref(),
&self.name,
&space_hash(&self.configs),
&self.provenance,
)?;
let searcher = GridSearch::new().resume(log.existing_trials());
self.run_searcher(searcher, stream, setup, &mut log)
}
pub fn run_with<S, F>(
mut self,
searcher: impl Searcher,
stream: &Arc<Stream>,
setup: S,
) -> Result<Output, Error>
where
S: FnMut(&Arc<Stream>, &Config) -> Result<F, Error>,
F: FnMut(&Arc<Stream>) -> Result<(), Error>,
{
self.apply_prune();
let mut log = TrialLog::open(
self.log_path.as_deref(),
&self.name,
&space_hash(&self.configs),
&self.provenance,
)?;
self.run_searcher(searcher, stream, setup, &mut log)
}
pub fn run_objective(self, objective: &mut dyn Objective) -> Result<Output, Error> {
let mut log = TrialLog::open(
self.log_path.as_deref(),
&self.name,
&space_hash(objective.configs()),
&self.provenance,
)?;
let searcher = GridSearch::new().resume(log.existing_trials());
self.run_objective_searcher(searcher, objective, &mut log)
}
pub fn run_objective_with(
self,
searcher: impl Searcher,
objective: &mut dyn Objective,
) -> Result<Output, Error> {
let mut log = TrialLog::open(
self.log_path.as_deref(),
&self.name,
&space_hash(objective.configs()),
&self.provenance,
)?;
self.run_objective_searcher(searcher, objective, &mut log)
}
fn run_objective_searcher(
self,
mut searcher: impl Searcher,
objective: &mut dyn Objective,
log: &mut TrialLog,
) -> Result<Output, Error> {
let required = required_indices(&self.required, objective.configs())?;
let existing = log.existing_trials();
let resumed: std::collections::BTreeSet<String> = existing
.iter()
.filter(|t| t.median_ms().is_some())
.map(|t| t.config_id.clone())
.collect();
let deadline = self.budget.map(|b| Instant::now() + b);
let mut logging = LoggingObjective {
inner: objective,
log,
deadline,
};
let mut cache = std::collections::BTreeMap::new();
for index in &required {
let id = logging.configs()[*index].id.clone();
if resumed.contains(&id) {
if let Some(t) = existing
.iter()
.find(|t| t.config_id == id && t.median_ms().is_some())
{
cache.insert(*index, t.clone());
}
} else {
cache.insert(*index, logging.measure(*index));
}
}
require_measured(logging.configs(), &required, &cache)?;
let pre_measured: Vec<Trial> = cache.values().cloned().collect();
let mut trials = {
let mut wrapped = RequiredFirst {
inner: &mut logging,
cache,
};
searcher.search(&mut wrapped)
};
merge_unclaimed(&mut trials, pre_measured);
let best = best_config(objective.configs(), &trials).cloned();
Ok(Output { trials, best })
}
fn apply_prune(&mut self) {
let prune = std::mem::take(&mut self.prune);
self.configs.retain(|c| prune.iter().all(|keep| keep(c)));
}
fn run_searcher<S, F>(
mut self,
mut searcher: impl Searcher,
stream: &Arc<Stream>,
setup: S,
log: &mut TrialLog,
) -> Result<Output, Error>
where
S: FnMut(&Arc<Stream>, &Config) -> Result<F, Error>,
F: FnMut(&Arc<Stream>) -> Result<(), Error>,
{
let required = std::mem::take(&mut self.required);
let existing = log.existing_trials();
let resumed: std::collections::BTreeSet<String> = existing
.iter()
.filter(|t| t.median_ms().is_some())
.map(|t| t.config_id.clone())
.collect();
let mut objective = BenchObjective {
configs: std::mem::take(&mut self.configs),
stream: stream.clone(),
setup,
bench: self.bench.clone(),
deadline: self.budget.map(|b| Instant::now() + b),
log,
};
let required = required_indices(&required, &objective.configs)?;
let mut cache = std::collections::BTreeMap::new();
for index in &required {
let id = objective.configs[*index].id.clone();
if resumed.contains(&id) {
if let Some(t) = existing
.iter()
.find(|t| t.config_id == id && t.median_ms().is_some())
{
cache.insert(*index, t.clone());
}
} else {
cache.insert(*index, objective.measure(*index));
}
}
require_measured(&objective.configs, &required, &cache)?;
let pre_measured: Vec<Trial> = cache.values().cloned().collect();
let mut trials = {
let mut wrapped = RequiredFirst {
inner: &mut objective,
cache,
};
searcher.search(&mut wrapped)
};
merge_unclaimed(&mut trials, pre_measured);
let best = match top_two(&objective.configs, &trials) {
None => None,
Some((only, None)) => Some(only.clone()),
Some((a, Some(b))) => {
let (a, b) = (a.clone(), b.clone());
if objective.budget_remaining() == Some(Duration::ZERO) {
Some(a)
} else {
let result = objective.runoff(&a, &b);
Some(runoff_verdict(a, b, result, &mut trials, objective.log))
}
}
};
Ok(Output { trials, best })
}
}
fn required_indices(required: &[Config], configs: &[Config]) -> Result<Vec<usize>, Error> {
let mut indices = Vec::new();
for r in required {
let index = configs.iter().position(|c| c.id == r.id).ok_or_else(|| {
crate::error::tensor_error(&format!(
"required config `{}` is not in the declared space \
(after pruning); the search cannot cover it",
r.id
))
})?;
if !indices.contains(&index) {
indices.push(index);
}
}
Ok(indices)
}
fn require_measured(
configs: &[Config],
required: &[usize],
cache: &std::collections::BTreeMap<usize, Trial>,
) -> Result<(), Error> {
for index in required {
if let Some(trial) = cache.get(index) {
if trial.median_ms().is_none() {
let reason = match &trial.state {
TrialState::Invalid { reason } => reason.as_str(),
_ => "not measured",
};
return Err(crate::error::tensor_error(&format!(
"required config `{}` failed to measure ({reason}); \
cannot guarantee the winner beat it",
configs[*index].id
)));
}
}
}
Ok(())
}
fn merge_unclaimed(trials: &mut Vec<Trial>, pre_measured: Vec<Trial>) {
for trial in pre_measured {
match trials.iter_mut().find(|t| t.config_id == trial.config_id) {
Some(existing) if existing.median_ms().is_none() && trial.median_ms().is_some() => {
*existing = trial;
}
Some(_) => {}
None => trials.push(trial),
}
}
}
struct RequiredFirst<'a> {
inner: &'a mut dyn Objective,
cache: std::collections::BTreeMap<usize, Trial>,
}
impl Objective for RequiredFirst<'_> {
fn configs(&self) -> &[Config] {
self.inner.configs()
}
fn measure(&mut self, index: usize) -> Trial {
match self.cache.remove(&index) {
Some(trial) => trial,
None => self.inner.measure(index),
}
}
fn budget_remaining(&self) -> Option<Duration> {
self.inner.budget_remaining()
}
}
struct LoggingObjective<'a> {
inner: &'a mut dyn Objective,
log: &'a mut TrialLog,
deadline: Option<Instant>,
}
impl Objective for LoggingObjective<'_> {
fn configs(&self) -> &[Config] {
self.inner.configs()
}
fn measure(&mut self, index: usize) -> Trial {
let authoritative_id = self.inner.configs().get(index).map(|c| c.id.clone());
let mut trial = self.inner.measure(index);
if let Some(id) = authoritative_id {
trial.config_id = id;
}
self.log.append(&trial);
trial
}
fn budget_remaining(&self) -> Option<Duration> {
let inner = self.inner.budget_remaining();
let own = self
.deadline
.map(|d| d.saturating_duration_since(Instant::now()));
match (own, inner) {
(Some(a), Some(b)) => Some(a.min(b)),
(Some(a), None) => Some(a),
(None, b) => b,
}
}
}
fn top_two<'a>(
configs: &'a [Config],
trials: &[Trial],
) -> Option<(&'a Config, Option<&'a Config>)> {
let mut ranked: Vec<(&Config, f32)> = Vec::new();
for t in trials {
let Some(ms) = t.median_ms() else { continue };
if !ms.is_finite() {
continue;
}
if let Some(c) = configs.iter().find(|c| c.id == t.config_id) {
match ranked.iter_mut().find(|(rc, _)| rc.id == c.id) {
Some(entry) => entry.1 = entry.1.min(ms),
None => ranked.push((c, ms)),
}
}
}
ranked.sort_by(|a, b| a.1.total_cmp(&b.1));
let mut it = ranked.into_iter();
let first = it.next()?.0;
Some((first, it.next().map(|(c, _)| c)))
}
struct BenchObjective<'l, S> {
configs: Vec<Config>,
stream: Arc<Stream>,
setup: S,
bench: BenchOptions,
deadline: Option<Instant>,
log: &'l mut TrialLog,
}
impl<S, F> Objective for BenchObjective<'_, S>
where
S: FnMut(&Arc<Stream>, &Config) -> Result<F, Error>,
F: FnMut(&Arc<Stream>) -> Result<(), Error>,
{
fn configs(&self) -> &[Config] {
&self.configs
}
fn measure(&mut self, index: usize) -> Trial {
let config = &self.configs[index];
let state = match (self.setup)(&self.stream, config) {
Err(e) => TrialState::Invalid {
reason: e.to_string(),
},
Ok(mut f) => match do_bench(&self.stream, &self.bench, |s| f(s)) {
Err(e) => TrialState::Invalid {
reason: e.to_string(),
},
Ok(m) => measured(&m),
},
};
let trial = Trial {
config_id: config.id.clone(),
state,
};
self.log.append(&trial);
trial
}
fn budget_remaining(&self) -> Option<Duration> {
self.deadline
.map(|d| d.saturating_duration_since(Instant::now()))
}
}
impl<S, F> BenchObjective<'_, S>
where
S: FnMut(&Arc<Stream>, &Config) -> Result<F, Error>,
F: FnMut(&Arc<Stream>) -> Result<(), Error>,
{
fn runoff(
&mut self,
a: &Config,
b: &Config,
) -> Result<(Measurement, Measurement), RunoffError> {
let mut fa = (self.setup)(&self.stream, a).map_err(|error| RunoffError::Setup {
b_failed: false,
error,
})?;
let mut fb = (self.setup)(&self.stream, b).map_err(|error| RunoffError::Setup {
b_failed: true,
error,
})?;
crate::bench::do_bench_paired(&self.stream, &self.bench, |s| fa(s), |s| fb(s))
.map_err(RunoffError::Bench)
}
}
enum RunoffError {
Setup { b_failed: bool, error: Error },
#[allow(dead_code)] Bench(Error),
}
fn runoff_verdict(
a: Config,
b: Config,
result: Result<(Measurement, Measurement), RunoffError>,
trials: &mut Vec<Trial>,
log: &mut TrialLog,
) -> Config {
match result {
Err(RunoffError::Setup { b_failed, error }) => {
let (loser, winner) = if b_failed { (b, a) } else { (a, b) };
let t = Trial {
config_id: loser.id.clone(),
state: TrialState::Invalid {
reason: format!("runoff setup failed: {error}"),
},
};
log.append(&t);
trials.push(t);
winner
}
Err(RunoffError::Bench(_)) => a,
Ok((ma, mb)) => {
let (oa, ob) = (measured(&ma), measured(&mb));
for (cfg, o) in [(&a, &oa), (&b, &ob)] {
let t = Trial {
config_id: cfg.id.clone(),
state: o.clone(),
};
log.append(&t);
trials.push(t);
}
let key = |o: &TrialState| match o {
TrialState::Measured { median_ms, .. } if median_ms.is_finite() => *median_ms,
_ => f32::INFINITY,
};
if key(&oa) <= key(&ob) {
a
} else {
b
}
}
}
}
fn measured(m: &Measurement) -> TrialState {
if m.reps() == 0 {
return TrialState::Invalid {
reason: "no timed reps (check BenchOptions)".into(),
};
}
TrialState::measured_or_invalid(m.median_ms(), m.min_ms(), m.reps())
}
#[derive(Debug)]
pub struct TrialLog {
file: Option<std::fs::File>,
existing: Vec<Trial>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct LogProvenance {
pub arch: Option<String>,
pub source_hash: Option<String>,
pub tileiras_fingerprint: Option<String>,
}
impl LogProvenance {
pub fn from_workspace(ws: &Workspace) -> Self {
Self {
arch: Some(ws.arch.clone()),
source_hash: Some(ws.source_hash.clone()),
tileiras_fingerprint: Some(ws.tileiras_fingerprint.clone()),
}
}
fn axes<'a>(
&'a self,
other: &'a LogProvenance,
) -> [(&'static str, &'a Option<String>, &'a Option<String>); 3] {
[
("arch", &self.arch, &other.arch),
("source_hash", &self.source_hash, &other.source_hash),
(
"tileiras fingerprint",
&self.tileiras_fingerprint,
&other.tileiras_fingerprint,
),
]
}
}
#[derive(Serialize, Deserialize)]
struct LogHeader {
log_schema: u32,
tuner: String,
space: String,
#[serde(default)]
arch: Option<String>,
#[serde(default)]
source_hash: Option<String>,
#[serde(default)]
tileiras_fingerprint: Option<String>,
}
impl LogHeader {
fn provenance(&self) -> LogProvenance {
LogProvenance {
arch: self.arch.clone(),
source_hash: self.source_hash.clone(),
tileiras_fingerprint: self.tileiras_fingerprint.clone(),
}
}
}
impl TrialLog {
pub fn open(
path: Option<&Path>,
tuner: &str,
space: &str,
provenance: &LogProvenance,
) -> Result<Self, Error> {
let Some(path) = path else {
return Ok(Self {
file: None,
existing: Vec::new(),
});
};
let expected = LogHeader {
log_schema: 1,
tuner: tuner.to_string(),
space: space.to_string(),
arch: provenance.arch.clone(),
source_hash: provenance.source_hash.clone(),
tileiras_fingerprint: provenance.tileiras_fingerprint.clone(),
};
let mut existing = Vec::new();
let mut needs_newline = false;
let mut fresh = true;
match std::fs::read_to_string(path) {
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
Err(e) => {
return Err(crate::error::tensor_error(&format!(
"trial log {} is unreadable: {e}",
path.display()
)));
}
Ok(contents) if contents.trim().is_empty() => {}
Ok(contents) => {
let mut lines = contents.lines();
let header: LogHeader = lines
.next()
.and_then(|l| serde_json::from_str(l).ok())
.ok_or_else(|| {
crate::error::tensor_error(&format!(
"trial log {} has no valid header; delete it or point .log() elsewhere",
path.display()
))
})?;
let mut diffs = Vec::new();
if header.log_schema != expected.log_schema {
diffs.push(format!(
"log schema {} (this cutile writes {})",
header.log_schema, expected.log_schema
));
}
if header.tuner != expected.tuner {
diffs.push(format!(
"tuner {:?} (running {:?})",
header.tuner, expected.tuner
));
}
if header.space != expected.space {
diffs.push(format!(
"space {} (running {})",
header.space, expected.space
));
}
for (axis, stored, running) in header.provenance().axes(provenance) {
match (stored, running) {
(Some(h), Some(e)) if h != e => {
diffs.push(format!("{axis} {h:?} (running {e:?})"));
}
(None, Some(e)) => {
eprintln!(
"cutile::tune: resuming trial log {} that records no {axis} \
while this run has {e:?} — its timings are adopted \
unchecked; re-tune from scratch if it may predate a \
change on that axis",
path.display()
);
}
_ => {}
}
}
if !diffs.is_empty() {
return Err(crate::error::tensor_error(&format!(
"trial log {} belongs to a different search — {}; \
delete it or point .log() elsewhere",
path.display(),
diffs.join(", "),
)));
}
existing = lines
.enumerate()
.filter_map(|(i, l)| match serde_json::from_str::<Trial>(l) {
Ok(trial) => Some(trial),
Err(e) => {
eprintln!(
"cutile::tune: skipping unparseable trial-log line {} in {}: {e}",
i + 2, path.display(),
);
None
}
})
.collect();
needs_newline = !contents.ends_with('\n');
fresh = false;
}
}
let mut opts = std::fs::OpenOptions::new();
if fresh {
opts.create(true).write(true).truncate(true);
} else {
opts.append(true);
}
let mut file = opts.open(path).map_err(|e| {
crate::error::tensor_error(&format!(
"trial log {} cannot be opened for append: {e}",
path.display()
))
})?;
if needs_newline {
file.write_all(b"\n").map_err(|e| {
crate::error::tensor_error(&format!(
"trial log {} could not be repaired for append: {e}",
path.display()
))
})?;
}
if fresh {
let line = serde_json::to_string(&expected).map_err(|e| {
crate::error::tensor_error(&format!("trial log header is unserializable: {e}"))
})?;
file.write_all(format!("{line}\n").as_bytes())
.map_err(|e| {
crate::error::tensor_error(&format!(
"trial log {} could not be headed: {e}",
path.display()
))
})?;
}
Ok(Self {
file: Some(file),
existing,
})
}
pub fn existing_trials(&self) -> Vec<Trial> {
self.existing.clone()
}
pub fn append(&mut self, trial: &Trial) {
if let (Some(file), Ok(line)) = (self.file.as_mut(), serde_json::to_string(trial)) {
if let Err(e) = file.write_all(format!("{line}\n").as_bytes()) {
eprintln!("cutile::tune: failed to append trial to log: {e}");
}
}
}
}
const RECORD_SCHEMA: u32 = 1;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Record {
pub schema: u32,
pub kernel: String,
pub source_hash: String,
pub cutile_version: String,
pub tileiras_fingerprint: String,
pub arch: String,
pub machine: String,
pub created_unix_secs: u64,
#[serde(default)]
pub space_hash: Option<String>,
#[serde(default)]
pub gate: Option<String>,
pub entries: Vec<RecordEntry>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum L2Key {
Tagged {
schema: u32,
digest: String,
},
Untagged(String),
}
impl L2Key {
pub fn current(digest: String) -> Self {
Self::Tagged {
schema: L2_KEY_SCHEMA,
digest,
}
}
pub fn digest(&self) -> &str {
match self {
Self::Tagged { digest, .. } | Self::Untagged(digest) => digest,
}
}
pub fn comparable(&self) -> Result<&str, String> {
match self {
Self::Tagged { schema, digest } if *schema == L2_KEY_SCHEMA => Ok(digest),
Self::Tagged { schema, .. } => Err(format!(
"was computed under l2 key encoding {schema}, workspace uses {L2_KEY_SCHEMA}"
)),
Self::Untagged(_) => Err(format!(
"predates l2 key encoding tags, workspace uses {L2_KEY_SCHEMA}"
)),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RecordEntry {
pub bucket: String,
pub config: Config,
pub median_ms: f32,
pub samples: usize,
pub l2_key: Option<L2Key>,
}
pub struct Workspace {
pub kernel: String,
pub source_hash: String,
pub arch: String,
pub tileiras_fingerprint: String,
pub space_hash: Option<String>,
}
impl Record {
pub fn new(ws: &Workspace) -> Self {
Self {
schema: RECORD_SCHEMA,
kernel: ws.kernel.clone(),
source_hash: ws.source_hash.clone(),
cutile_version: env!("CARGO_PKG_VERSION").to_string(),
tileiras_fingerprint: ws.tileiras_fingerprint.clone(),
arch: ws.arch.clone(),
machine: hostname(),
created_unix_secs: std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0),
space_hash: ws.space_hash.clone(),
gate: None,
entries: Vec::new(),
}
}
pub fn insert(&mut self, entry: RecordEntry) {
match self.entries.iter_mut().find(|e| e.bucket == entry.bucket) {
Some(slot) => *slot = entry,
None => self.entries.push(entry),
}
}
pub fn get(&self, bucket: &str) -> Option<&RecordEntry> {
self.entries.iter().find(|e| e.bucket == bucket)
}
pub fn save(&self, path: &Path) -> Result<(), Error> {
let json = serde_json::to_string_pretty(self)
.map_err(|e| crate::error::tensor_error(&format!("record serialize: {e}")))?;
std::fs::write(path, json)
.map_err(|e| crate::error::tensor_error(&format!("record write: {e}")))
}
pub fn load(path: &Path) -> Result<Self, Error> {
let contents = std::fs::read_to_string(path)
.map_err(|e| crate::error::tensor_error(&format!("record read: {e}")))?;
serde_json::from_str(&contents)
.map_err(|e| crate::error::tensor_error(&format!("record parse: {e}")))
}
pub fn load_verified(
path: &Path,
ws: &Workspace,
mut verify_l2: impl FnMut(&RecordEntry) -> Result<Option<String>, Error>,
) -> Result<(Self, Vec<String>), Error> {
let record = Self::load(path)?;
let refuse = |what: &str, stored: &str, current: &str| {
Err(crate::error::tensor_error(&format!(
"stale tuning record at {}: {what} mismatch (record: {stored}, workspace: {current}); re-tune or delete it",
path.display(),
)))
};
if record.schema != RECORD_SCHEMA {
return refuse(
"schema",
&record.schema.to_string(),
&RECORD_SCHEMA.to_string(),
);
}
if record.kernel != ws.kernel {
return refuse("kernel", &record.kernel, &ws.kernel);
}
if record.arch != ws.arch {
return refuse("arch", &record.arch, &ws.arch);
}
if record.source_hash != ws.source_hash {
return refuse("source_hash", &record.source_hash, &ws.source_hash);
}
if let (Some(stored), Some(current)) = (&record.space_hash, &ws.space_hash) {
if stored != current {
return refuse("space_hash", stored, current);
}
}
{
let mut seen = std::collections::BTreeSet::new();
for e in &record.entries {
if !seen.insert(e.bucket.as_str()) {
return Err(crate::error::tensor_error(&format!(
"tuning record at {} has duplicate entries for bucket {:?}; fix or re-tune it",
path.display(),
e.bucket,
)));
}
let derived = Config::new(e.config.params.clone()).id;
if e.config.id != derived {
return refuse(
&format!("config id for bucket {:?}", e.bucket),
&e.config.id,
&derived,
);
}
}
}
let mut warnings = Vec::new();
if record.space_hash.is_none() && ws.space_hash.is_some() {
warnings.push(
"tuning record carries no space_hash; the search-space match was not checked"
.to_string(),
);
}
if record.tileiras_fingerprint != ws.tileiras_fingerprint {
warnings.push(format!(
"tuning record was produced by a different tileiras ({} vs {}); configs remain valid but timings may have shifted and per-entry key verification was skipped — consider re-tuning",
record.tileiras_fingerprint, ws.tileiras_fingerprint,
));
} else {
for entry in &record.entries {
match entry.l2_key.as_ref().map(L2Key::comparable) {
None => warnings.push(format!(
"bucket {:?} carries no l2 key; only source-level staleness checks applied",
entry.bucket
)),
Some(Err(why)) => warnings.push(format!(
"bucket {:?}: stored l2 key {why}; the key check was skipped — configs remain valid, consider re-tuning",
entry.bucket
)),
Some(Ok(stored)) => match verify_l2(entry)? {
None => warnings.push(format!(
"bucket {:?}: verifier declined to recompute the l2 key; stored key not checked",
entry.bucket
)),
Some(current) => {
if current != stored {
return refuse(
&format!("l2 key for bucket {:?}", entry.bucket),
stored,
¤t,
);
}
}
},
}
}
}
if record.cutile_version != env!("CARGO_PKG_VERSION") {
warnings.push(format!(
"tuning record was produced by cutile {} (running {})",
record.cutile_version,
env!("CARGO_PKG_VERSION"),
));
}
Ok((record, warnings))
}
}
fn hostname() -> String {
std::fs::read_to_string("/etc/hostname")
.map(|s| s.trim().to_string())
.ok()
.filter(|s| !s.is_empty())
.or_else(|| std::env::var("HOSTNAME").ok())
.unwrap_or_else(|| "unknown".to_string())
}
#[cfg(test)]
mod tests {
use super::*;
fn cfg(bn: i64, splits: i64) -> Config {
Config::new([
("BN", ParamValue::Int(bn)),
("SPLITS", ParamValue::Int(splits)),
])
}
struct FakeObjective {
configs: Vec<Config>,
cost: fn(&Config) -> Option<f32>,
measured: Vec<String>,
budget: Option<Duration>,
}
impl Objective for FakeObjective {
fn configs(&self) -> &[Config] {
&self.configs
}
fn measure(&mut self, index: usize) -> Trial {
let c = &self.configs[index];
self.measured.push(c.id.clone());
let state = match (self.cost)(c) {
Some(ms) => TrialState::Measured {
median_ms: ms,
min_ms: ms,
reps: 3,
},
None => TrialState::Invalid {
reason: "gate failed".into(),
},
};
Trial {
config_id: c.id.clone(),
state,
}
}
fn budget_remaining(&self) -> Option<Duration> {
self.budget
}
}
#[test]
fn config_ids_are_stable_and_param_order_independent() {
let a = Config::new([("SPLITS", ParamValue::Int(8)), ("BN", ParamValue::Int(64))]);
let b = cfg(64, 8);
assert_eq!(a.id, b.id);
assert_eq!(a.id, "BN=64,SPLITS=8");
assert_eq!(a.int("BN"), Some(64));
assert_eq!(a.int("missing"), None);
}
#[test]
fn grid_search_visits_everything_once_and_picks_best() {
let configs = vec![cfg(32, 2), cfg(64, 4), cfg(128, 8)];
let mut objective = FakeObjective {
configs: configs.clone(),
cost: |c| Some(c.int("BN").unwrap() as f32), measured: Vec::new(),
budget: None,
};
let trials = GridSearch::new().search(&mut objective);
assert_eq!(objective.measured.len(), 3);
assert_eq!(trials.len(), 3);
let best = best_config(&configs, &trials).unwrap();
assert_eq!(best.int("BN"), Some(32));
}
#[test]
fn invalid_candidates_are_recorded_not_fatal() {
let configs = vec![cfg(32, 2), cfg(64, 4)];
let mut objective = FakeObjective {
configs: configs.clone(),
cost: |c| (c.int("BN") != Some(32)).then_some(1.0), measured: Vec::new(),
budget: None,
};
let trials = GridSearch::new().search(&mut objective);
assert_eq!(trials.len(), 2);
assert!(matches!(trials[0].state, TrialState::Invalid { .. }));
let best = best_config(&configs, &trials).unwrap();
assert_eq!(best.int("BN"), Some(64), "invalid one never wins");
}
#[test]
fn resume_skips_known_trials() {
let configs = vec![cfg(32, 2), cfg(64, 4), cfg(128, 8)];
let known = vec![Trial {
config_id: configs[1].id.clone(),
state: TrialState::Measured {
median_ms: 0.5,
min_ms: 0.5,
reps: 3,
},
}];
let mut objective = FakeObjective {
configs: configs.clone(),
cost: |_| Some(9.0),
measured: Vec::new(),
budget: None,
};
let trials = GridSearch::new().resume(known).search(&mut objective);
assert_eq!(
objective.measured.len(),
2,
"known candidate not re-measured"
);
assert_eq!(trials.len(), 3, "known trial still in the result set");
let best = best_config(&configs, &trials).unwrap();
assert_eq!(best.int("BN"), Some(64), "resumed trial can win");
}
#[test]
fn exhausted_budget_stops_the_search() {
let configs = vec![cfg(32, 2), cfg(64, 4), cfg(128, 8)];
let mut objective = FakeObjective {
configs,
cost: |_| Some(1.0),
measured: Vec::new(),
budget: Some(Duration::ZERO),
};
let trials = GridSearch::new().search(&mut objective);
assert!(trials.is_empty(), "zero budget measures nothing");
}
fn ws() -> Workspace {
Workspace {
kernel: "fmha_decode".into(),
source_hash: "abc123".into(),
arch: "sm_120".into(),
tileiras_fingerprint: "release 13.3, V13.3.36".into(),
space_hash: None,
}
}
fn record_path(label: &str) -> std::path::PathBuf {
std::env::temp_dir().join(format!("cutile_record_{label}_{}.json", std::process::id()))
}
#[test]
fn record_roundtrips_and_verifies() {
let path = record_path("roundtrip");
let mut a = Record::new(&ws());
a.insert(RecordEntry {
bucket: "tg<=512".into(),
config: cfg(64, 8),
median_ms: 1.25,
samples: 12,
l2_key: Some(L2Key::current("f".repeat(64))),
});
a.save(&path).unwrap();
let (loaded, warnings) = Record::load_verified(&path, &ws(), |e| {
Ok(e.l2_key.as_ref().map(|k| k.digest().to_string()))
})
.unwrap();
assert!(warnings.is_empty());
let entry = loaded.get("tg<=512").unwrap();
assert_eq!(entry.config.int("BN"), Some(64));
assert_eq!(entry.samples, 12);
let _ = std::fs::remove_file(&path);
}
#[test]
fn record_refuses_source_hash_and_arch_mismatch() {
let path = record_path("refuse");
Record::new(&ws()).save(&path).unwrap();
let mut other = ws();
other.source_hash = "different".into();
let err = Record::load_verified(&path, &other, |_| Ok(None)).unwrap_err();
assert!(err.to_string().contains("source_hash mismatch"));
assert!(err.to_string().contains("re-tune"));
let mut other = ws();
other.arch = "sm_100".into();
let err = Record::load_verified(&path, &other, |_| Ok(None)).unwrap_err();
assert!(err.to_string().contains("arch mismatch"));
let mut other = ws();
other.kernel = "other_kernel".into();
let err = Record::load_verified(&path, &other, |_| Ok(None)).unwrap_err();
assert!(err.to_string().contains("kernel mismatch"));
let _ = std::fs::remove_file(&path);
}
#[test]
fn record_refuses_space_mismatch_and_duplicate_buckets() {
let path = record_path("space");
let mut with_space = ws();
with_space.space_hash = Some(space_hash(&[cfg(64, 8), cfg(128, 8)]));
Record::new(&with_space).save(&path).unwrap();
let mut other = ws();
other.space_hash = Some(space_hash(&[cfg(64, 8)]));
let err = Record::load_verified(&path, &other, |_| Ok(None)).unwrap_err();
assert!(err.to_string().contains("space_hash mismatch"));
let (_, _) = Record::load_verified(&path, &ws(), |_| Ok(None)).unwrap();
let mut dup = Record::new(&ws());
for _ in 0..2 {
dup.entries.push(RecordEntry {
bucket: "b".into(),
config: cfg(64, 8),
median_ms: 1.0,
samples: 3,
l2_key: None,
});
}
dup.save(&path).unwrap();
let err = Record::load_verified(&path, &ws(), |_| Ok(None)).unwrap_err();
assert!(err.to_string().contains("duplicate entries for bucket"));
let _ = std::fs::remove_file(&path);
}
#[test]
fn record_refuses_l2_key_drift_and_warns_on_fingerprint_drift() {
let path = record_path("l2");
let mut a = Record::new(&ws());
a.insert(RecordEntry {
bucket: "b".into(),
config: cfg(64, 8),
median_ms: 1.0,
samples: 5,
l2_key: Some(L2Key::current("a".repeat(64))),
});
a.save(&path).unwrap();
let err = Record::load_verified(&path, &ws(), |_| Ok(Some("b".repeat(64)))).unwrap_err();
assert!(err.to_string().contains("l2 key for bucket"));
let mut drifted = ws();
drifted.tileiras_fingerprint = "release 13.4, V13.4.1".into();
let (_, warnings) = Record::load_verified(&path, &drifted, |_| Ok(None)).unwrap();
assert_eq!(warnings.len(), 1);
assert!(warnings[0].contains("different tileiras"));
let _ = std::fs::remove_file(&path);
}
#[test]
fn key_encoding_drift_skips_l2_verification_instead_of_refusing() {
let path = record_path("keyencoding");
let mut a = Record::new(&ws());
a.insert(RecordEntry {
bucket: "older".into(),
config: cfg(64, 8),
median_ms: 1.0,
samples: 5,
l2_key: Some(L2Key::Tagged {
schema: L2_KEY_SCHEMA - 1,
digest: "a".repeat(64),
}),
});
a.insert(RecordEntry {
bucket: "untagged".into(),
config: cfg(128, 8),
median_ms: 1.0,
samples: 5,
l2_key: Some(L2Key::Untagged("b".repeat(64))),
});
a.save(&path).unwrap();
let json = std::fs::read_to_string(&path).unwrap();
assert!(json.contains(&format!("\"l2_key\": \"{}\"", "b".repeat(64))));
let mut called = false;
let (_, warnings) = Record::load_verified(&path, &ws(), |_| {
called = true;
Ok(Some("c".repeat(64))) })
.unwrap();
assert!(!called, "verifier must not run on an incomparable key");
assert!(warnings[0].contains("l2 key encoding"));
assert!(warnings[1].contains("predates l2 key encoding tags"));
let _ = std::fs::remove_file(&path);
}
#[test]
fn per_entry_encoding_keeps_mixed_records_honest() {
let path = record_path("mixed");
let mut a = Record::new(&ws());
a.insert(RecordEntry {
bucket: "retuned".into(),
config: cfg(64, 8),
median_ms: 1.0,
samples: 5,
l2_key: Some(L2Key::current("a".repeat(64))),
});
a.insert(RecordEntry {
bucket: "carried over".into(),
config: cfg(128, 8),
median_ms: 2.0,
samples: 5,
l2_key: Some(L2Key::Untagged("b".repeat(64))),
});
a.save(&path).unwrap();
let mut checked = Vec::new();
let err = Record::load_verified(&path, &ws(), |e| {
checked.push(e.bucket.clone());
Ok(Some("z".repeat(64)))
})
.unwrap_err();
assert_eq!(checked, ["retuned"], "only the comparable key is verified");
assert!(err.to_string().contains("l2 key for bucket \"retuned\""));
let _ = std::fs::remove_file(&path);
}
#[test]
fn record_insert_replaces_bucket_winner() {
let mut a = Record::new(&ws());
for (bn, ms) in [(64, 2.0), (128, 1.0)] {
a.insert(RecordEntry {
bucket: "b".into(),
config: cfg(bn, 4),
median_ms: ms,
samples: 3,
l2_key: None,
});
}
assert_eq!(a.entries.len(), 1, "one winner per bucket");
assert_eq!(a.get("b").unwrap().config.int("BN"), Some(128));
}
#[test]
fn record_refuses_schema_and_id_param_mismatch() {
let path = record_path("schema");
let mut a = Record::new(&ws());
a.schema = RECORD_SCHEMA + 1;
a.save(&path).unwrap();
let err = Record::load_verified(&path, &ws(), |_| Ok(None)).unwrap_err();
assert!(err.to_string().contains("schema mismatch"));
let mut a = Record::new(&ws());
let mut config = cfg(64, 8);
config.id = "BN=128,SPLITS=8".into();
a.insert(RecordEntry {
bucket: "b".into(),
config,
median_ms: 1.0,
samples: 3,
l2_key: None,
});
a.save(&path).unwrap();
let err = Record::load_verified(&path, &ws(), |_| Ok(None)).unwrap_err();
assert!(err.to_string().contains("config id for bucket"));
let _ = std::fs::remove_file(&path);
}
#[test]
fn record_without_space_hash_warns_when_workspace_expects_one() {
let path = record_path("nospace");
Record::new(&ws()).save(&path).unwrap(); let mut expecting = ws();
expecting.space_hash = Some(space_hash(&[cfg(64, 8)]));
let (_, warnings) = Record::load_verified(&path, &expecting, |_| Ok(None)).unwrap();
assert_eq!(warnings.len(), 1);
assert!(warnings[0].contains("no space_hash"));
let _ = std::fs::remove_file(&path);
}
#[test]
fn fingerprint_drift_skips_l2_verification_instead_of_refusing() {
let path = record_path("driftorder");
let mut a = Record::new(&ws());
a.insert(RecordEntry {
bucket: "b".into(),
config: cfg(64, 8),
median_ms: 1.0,
samples: 5,
l2_key: Some(L2Key::current("a".repeat(64))),
});
a.save(&path).unwrap();
let mut drifted = ws();
drifted.tileiras_fingerprint = "release 13.4, V13.4.1".into();
let mut called = false;
let (_, warnings) = Record::load_verified(&path, &drifted, |_| {
called = true;
Ok(Some("b".repeat(64))) })
.unwrap();
assert!(!called, "verifier must not run under fingerprint drift");
assert_eq!(warnings.len(), 1);
assert!(warnings[0].contains("skipped"));
let _ = std::fs::remove_file(&path);
}
#[test]
fn record_version_drift_warns() {
let path = record_path("version");
let mut a = Record::new(&ws());
a.cutile_version = "0.0.0-elsewhere".into();
a.save(&path).unwrap();
let (_, warnings) = Record::load_verified(&path, &ws(), |_| Ok(None)).unwrap();
assert_eq!(warnings.len(), 1);
assert!(warnings[0].contains("produced by cutile 0.0.0-elsewhere"));
let _ = std::fs::remove_file(&path);
}
#[test]
fn trial_log_roundtrips_and_resumes() {
let dir = std::env::temp_dir().join(format!("cutile_tune_log_{}", std::process::id()));
let _ = std::fs::remove_file(&dir);
{
let mut log =
TrialLog::open(Some(dir.as_path()), "t", "s", &LogProvenance::default()).unwrap();
log.append(&Trial {
config_id: "BN=64".into(),
state: TrialState::Measured {
median_ms: 1.5,
min_ms: 1.4,
reps: 5,
},
});
log.append(&Trial {
config_id: "BN=128".into(),
state: TrialState::Invalid {
reason: "launch check".into(),
},
});
}
let log = TrialLog::open(Some(dir.as_path()), "t", "s", &LogProvenance::default()).unwrap();
let existing = log.existing_trials();
assert_eq!(existing.len(), 2);
assert_eq!(existing[0].median_ms(), Some(1.5));
assert!(existing[1].median_ms().is_none());
let err = TrialLog::open(Some(dir.as_path()), "other", "s", &LogProvenance::default())
.unwrap_err();
assert!(err.to_string().contains("different search"));
assert!(err.to_string().contains("tuner"));
let err = TrialLog::open(
Some(dir.as_path()),
"t",
"different",
&LogProvenance::default(),
)
.unwrap_err();
assert!(err.to_string().contains("different search"));
assert!(err.to_string().contains("space"));
{
use std::io::Write as _;
let mut f = std::fs::OpenOptions::new().append(true).open(&dir).unwrap();
write!(f, "{{\"config_id\":\"torn").unwrap();
}
{
let mut log =
TrialLog::open(Some(dir.as_path()), "t", "s", &LogProvenance::default()).unwrap();
log.append(&Trial {
config_id: "BN=256".into(),
state: TrialState::Measured {
median_ms: 2.0,
min_ms: 2.0,
reps: 3,
},
});
}
let log = TrialLog::open(Some(dir.as_path()), "t", "s", &LogProvenance::default()).unwrap();
assert_eq!(
log.existing_trials().len(),
3,
"torn line dropped, new record intact"
);
let _ = std::fs::remove_file(&dir);
}
#[test]
fn arch_mismatch_refuses_but_none_on_either_side_resumes() {
let dir = std::env::temp_dir().join(format!("cutile_tune_arch_{}", std::process::id()));
let _ = std::fs::remove_file(&dir);
{
let mut log =
TrialLog::open(Some(dir.as_path()), "t", "s", &arch_only("sm_120")).unwrap();
log.append(&Trial::measured("BN=64", 1.5, 1.4, 5));
}
let err = TrialLog::open(Some(dir.as_path()), "t", "s", &arch_only("sm_100")).unwrap_err();
assert!(err.to_string().contains("different search"), "{err}");
assert!(err.to_string().contains("arch"), "{err}");
let log = TrialLog::open(Some(dir.as_path()), "t", "s", &arch_only("sm_120")).unwrap();
assert_eq!(log.existing_trials().len(), 1);
let log = TrialLog::open(Some(dir.as_path()), "t", "s", &LogProvenance::default()).unwrap();
assert_eq!(log.existing_trials().len(), 1);
let _ = std::fs::remove_file(&dir);
{
let mut log =
TrialLog::open(Some(dir.as_path()), "t", "s", &LogProvenance::default()).unwrap();
log.append(&Trial::measured("BN=64", 1.5, 1.4, 5));
}
let log = TrialLog::open(Some(dir.as_path()), "t", "s", &arch_only("sm_100")).unwrap();
assert_eq!(log.existing_trials().len(), 1);
let _ = std::fs::remove_file(&dir);
}
fn arch_only(arch: &str) -> LogProvenance {
LogProvenance {
arch: Some(arch.to_string()),
..LogProvenance::default()
}
}
#[test]
fn source_hash_and_tileiras_mismatch_refuse_resume() {
let dir = std::env::temp_dir().join(format!("cutile_tune_prov_{}", std::process::id()));
let _ = std::fs::remove_file(&dir);
let tagged = LogProvenance {
arch: Some("sm_120".into()),
source_hash: Some("abc123".into()),
tileiras_fingerprint: Some("release 13.3, V13.3.36".into()),
};
{
let mut log = TrialLog::open(Some(dir.as_path()), "t", "s", &tagged).unwrap();
log.append(&Trial::measured("BN=64", 1.5, 1.4, 5));
}
let log = TrialLog::open(Some(dir.as_path()), "t", "s", &tagged).unwrap();
assert_eq!(log.existing_trials().len(), 1);
let mut edited = tagged.clone();
edited.source_hash = Some("def456".into());
let err = TrialLog::open(Some(dir.as_path()), "t", "s", &edited).unwrap_err();
assert!(err.to_string().contains("different search"), "{err}");
assert!(err.to_string().contains("source_hash"), "{err}");
let mut toolkit = tagged.clone();
toolkit.tileiras_fingerprint = Some("release 13.4, V13.4.1".into());
let err = TrialLog::open(Some(dir.as_path()), "t", "s", &toolkit).unwrap_err();
assert!(err.to_string().contains("tileiras fingerprint"), "{err}");
let mut both = edited.clone();
both.tileiras_fingerprint = toolkit.tileiras_fingerprint.clone();
let err = TrialLog::open(Some(dir.as_path()), "t", "s", &both).unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("source_hash") && msg.contains("tileiras fingerprint"),
"{msg}"
);
let log = TrialLog::open(Some(dir.as_path()), "t", "s", &LogProvenance::default()).unwrap();
assert_eq!(log.existing_trials().len(), 1);
let _ = std::fs::remove_file(&dir);
}
#[test]
fn legacy_header_without_provenance_still_resumes() {
let dir = std::env::temp_dir().join(format!("cutile_tune_legacy_{}", std::process::id()));
std::fs::write(&dir, "{\"log_schema\":1,\"tuner\":\"t\",\"space\":\"s\"}\n").unwrap();
{
let mut log =
TrialLog::open(Some(dir.as_path()), "t", "s", &LogProvenance::default()).unwrap();
log.append(&Trial::measured("BN=64", 1.5, 1.4, 5));
}
let tagged = LogProvenance {
arch: Some("sm_120".into()),
source_hash: Some("abc123".into()),
tileiras_fingerprint: Some("release 13.3, V13.3.36".into()),
};
let log = TrialLog::open(Some(dir.as_path()), "t", "s", &tagged).unwrap();
assert_eq!(
log.existing_trials().len(),
1,
"None on the header side skips each axis"
);
let _ = std::fs::remove_file(&dir);
}
#[test]
fn provenance_from_workspace_fills_every_axis() {
let p = LogProvenance::from_workspace(&ws());
assert_eq!(p.arch.as_deref(), Some("sm_120"));
assert_eq!(p.source_hash.as_deref(), Some("abc123"));
assert_eq!(
p.tileiras_fingerprint.as_deref(),
Some("release 13.3, V13.3.36")
);
}
#[test]
fn config_ids_do_not_alias_across_types_or_separators() {
let int1 = Config::new([("A", ParamValue::Int(1))]);
let str1 = Config::new([("A", ParamValue::Str("1".into()))]);
assert_ne!(int1.id, str1.id, "int 1 and string \"1\" must differ");
let sneaky = Config::new([("x", ParamValue::Str("1,y=2".into()))]);
let honest = Config::new([
("x", ParamValue::Str("1".into())),
("y", ParamValue::Int(2)),
]);
assert_ne!(sneaky.id, honest.id, "separator injection must not alias");
}
#[test]
fn stale_resumed_trials_neither_win_nor_block_a_winner() {
let configs = vec![cfg(64, 4)];
let stale = Trial {
config_id: "BN=16,SPLITS=2".into(),
state: TrialState::Measured {
median_ms: 0.1,
min_ms: 0.1,
reps: 3,
},
};
let mut objective = FakeObjective {
configs: configs.clone(),
cost: |_| Some(1.0),
measured: Vec::new(),
budget: None,
};
let trials = GridSearch::new().resume(vec![stale]).search(&mut objective);
assert_eq!(trials.len(), 1, "stale trial dropped from results");
let best = best_config(&configs, &trials).expect("valid winner survives");
assert_eq!(best.int("BN"), Some(64));
}
#[test]
fn resumed_invalid_trials_are_retried() {
let configs = vec![cfg(64, 4)];
let invalid = Trial {
config_id: configs[0].id.clone(),
state: TrialState::Invalid {
reason: "transient".into(),
},
};
let mut objective = FakeObjective {
configs: configs.clone(),
cost: |_| Some(1.0),
measured: Vec::new(),
budget: None,
};
let trials = GridSearch::new()
.resume(vec![invalid])
.search(&mut objective);
assert_eq!(objective.measured.len(), 1, "previously-Invalid retried");
assert!(trials.iter().any(|t| t.median_ms() == Some(1.0)));
}
#[test]
fn prune_composes_with_configs_in_either_order() {
let mk = || vec![cfg(32, 2), cfg(64, 4)];
let mut a = Autotuner::new("t")
.prune(|c| c.int("BN") != Some(32))
.configs(mk());
a.apply_prune();
assert_eq!(a.configs.len(), 1);
assert_eq!(a.configs[0].int("BN"), Some(64));
let mut b = Autotuner::new("t")
.configs(mk())
.prune(|c| c.int("BN") != Some(32));
b.apply_prune();
assert_eq!(b.configs.len(), 1);
assert_eq!(b.configs[0].int("BN"), Some(64));
}
#[test]
fn best_config_skips_non_finite_medians() {
let configs = vec![cfg(64, 4), cfg(128, 8)];
let trials = vec![
Trial {
config_id: configs[0].id.clone(),
state: TrialState::Measured {
median_ms: f32::NAN,
min_ms: f32::NAN,
reps: 3,
},
},
Trial {
config_id: configs[1].id.clone(),
state: TrialState::Measured {
median_ms: 2.0,
min_ms: 2.0,
reps: 3,
},
},
];
let best = best_config(&configs, &trials).unwrap();
assert_eq!(best.int("BN"), Some(128), "NaN never wins");
}
fn meas(times: &[f32]) -> Measurement {
Measurement::from_times_ms(times.to_vec())
}
fn silent_log() -> TrialLog {
TrialLog::open(None, "t", "s", &LogProvenance::default()).unwrap()
}
#[test]
fn runoff_setup_failure_forfeits_to_the_other_finalist() {
let (a, b) = (cfg(32, 2), cfg(64, 4));
for (b_failed, winner_id, loser_id) in [
(false, b.id.clone(), a.id.clone()),
(true, a.id.clone(), b.id.clone()),
] {
let mut trials = Vec::new();
let err = RunoffError::Setup {
b_failed,
error: crate::error::tensor_error("boom"),
};
let winner = runoff_verdict(
a.clone(),
b.clone(),
Err(err),
&mut trials,
&mut silent_log(),
);
assert_eq!(winner.id, winner_id);
assert_eq!(trials.len(), 1);
assert_eq!(
trials[0].config_id, loser_id,
"failure blamed on the failing finalist"
);
assert!(matches!(
&trials[0].state,
TrialState::Invalid { reason } if reason.contains("runoff setup failed")
));
}
}
#[test]
fn runoff_bench_failure_keeps_the_sequential_leader() {
let (a, b) = (cfg(32, 2), cfg(64, 4));
let mut trials = Vec::new();
let winner = runoff_verdict(
a.clone(),
b,
Err(RunoffError::Bench(crate::error::tensor_error(
"stream died",
))),
&mut trials,
&mut silent_log(),
);
assert_eq!(winner.id, a.id);
assert!(
trials.is_empty(),
"unattributable failures mark nobody Invalid"
);
}
#[test]
fn non_finite_runoff_median_never_wins() {
let (a, b) = (cfg(32, 2), cfg(64, 4));
let winner = runoff_verdict(
a.clone(),
b.clone(),
Ok((meas(&[2.0, 2.0, 2.0]), meas(&[f32::NAN, f32::NAN]))),
&mut Vec::new(),
&mut silent_log(),
);
assert_eq!(winner.id, a.id);
let winner = runoff_verdict(
a,
b.clone(),
Ok((meas(&[f32::NAN]), meas(&[3.0]))),
&mut Vec::new(),
&mut silent_log(),
);
assert_eq!(winner.id, b.id);
}
#[test]
fn runoff_trials_carry_real_measurement_metadata() {
let (a, b) = (cfg(32, 2), cfg(64, 4));
let mut trials = Vec::new();
let winner = runoff_verdict(
a.clone(),
b,
Ok((meas(&[1.0, 2.0, 3.0]), meas(&[4.0, 5.0, 6.0]))),
&mut trials,
&mut silent_log(),
);
assert_eq!(winner.id, a.id);
assert_eq!(trials.len(), 2);
match &trials[0].state {
TrialState::Measured {
median_ms,
min_ms,
reps,
} => {
assert_eq!(*median_ms, 2.0);
assert_eq!(*min_ms, 1.0);
assert_eq!(*reps, 3, "reps reflect the actual paired measurement");
}
other => panic!("expected Measured, got {other:?}"),
}
}
#[test]
fn config_ids_do_not_alias_across_key_separators() {
let smuggled = Config::new([("A=1,B", ParamValue::Int(2))]);
let honest = Config::new([("A", ParamValue::Int(1)), ("B", ParamValue::Int(2))]);
assert_ne!(smuggled.id, honest.id);
let quoted = Config::new([("\"A\"", ParamValue::Int(1))]);
let plain = Config::new([("A", ParamValue::Int(1))]);
assert_ne!(quoted.id, plain.id);
}
#[test]
fn whitespace_only_log_is_headed_and_resumable() {
let dir = std::env::temp_dir().join(format!("cutile_tune_ws_{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("trials.jsonl");
std::fs::write(&path, "\n").unwrap();
{
let mut log = TrialLog::open(Some(&path), "t", "s", &LogProvenance::default()).unwrap();
log.append(&Trial {
config_id: cfg(1, 1).id,
state: TrialState::Invalid { reason: "x".into() },
});
}
let log = TrialLog::open(Some(&path), "t", "s", &LogProvenance::default()).unwrap();
assert_eq!(log.existing_trials().len(), 1);
let _ = std::fs::remove_dir_all(&dir);
}
fn indexed_objective(times: &[f32]) -> FakeObjective {
fn cost(c: &Config) -> Option<f32> {
c.int("i").map(|i| [3.0f32, 1.0, 2.0][i as usize])
}
let _ = times; FakeObjective {
configs: (0..3)
.map(|i| Config::new([("i", ParamValue::Int(i))]))
.collect(),
cost,
measured: Vec::new(),
budget: None,
}
}
#[test]
fn trial_constructors_round_trip_through_serde() {
let m = Trial::measured("c1", 1.5, 1.2, 7);
let i = Trial::invalid("c2", "launch check failed");
for t in [&m, &i] {
let line = serde_json::to_string(t).unwrap();
let back: Trial = serde_json::from_str(&line).unwrap();
assert_eq!(back.config_id, t.config_id);
assert_eq!(back.median_ms(), t.median_ms());
}
assert_eq!(m.median_ms(), Some(1.5));
assert_eq!(i.median_ms(), None);
}
#[test]
fn run_objective_logs_every_trial_and_resumes() {
let dir =
std::env::temp_dir().join(format!("cutile_tune_objective_test_{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("trials.jsonl");
let _ = std::fs::remove_file(&path);
let mut objective = indexed_objective(&[3.0, 1.0, 2.0]);
let out = Autotuner::new("objective_test")
.log(&path)
.run_objective(&mut objective)
.unwrap();
assert_eq!(objective.measured.len(), 3);
assert_eq!(out.trials.len(), 3);
assert_eq!(out.best.as_ref().unwrap().int("i"), Some(1));
let mut objective = indexed_objective(&[3.0, 1.0, 2.0]);
let out = Autotuner::new("objective_test")
.log(&path)
.run_objective(&mut objective)
.unwrap();
assert!(
objective.measured.is_empty(),
"resumed run must not re-measure"
);
assert_eq!(out.best.as_ref().unwrap().int("i"), Some(1));
}
#[test]
fn require_rejects_a_config_outside_the_space() {
let mut objective = indexed_objective(&[3.0, 1.0, 2.0]);
let missing = Config::new([("i", ParamValue::Int(99))]);
let err = match Autotuner::new("t")
.require(vec![missing])
.run_objective(&mut objective)
{
Ok(_) => panic!("a required config outside the space must error"),
Err(err) => err,
};
assert!(
format!("{err}").contains("not in the declared space"),
"must name the coverage violation: {err}"
);
assert!(objective.measured.is_empty(), "no measurement on error");
}
#[test]
fn required_configs_measure_first_and_only_once() {
let mut objective = indexed_objective(&[3.0, 1.0, 2.0]);
let incumbent = objective.configs[2].clone();
let incumbent_id = incumbent.id.clone();
let out = Autotuner::new("t")
.require(vec![incumbent])
.run_objective(&mut objective)
.unwrap();
assert_eq!(
objective.measured[0], incumbent_id,
"the incumbent must be visited first"
);
assert_eq!(objective.measured.len(), 3, "no candidate measured twice");
assert_eq!(out.trials.len(), 3);
}
#[test]
fn required_trial_survives_an_exhausted_budget() {
let mut objective = indexed_objective(&[3.0, 1.0, 2.0]);
objective.budget = Some(Duration::ZERO);
let incumbent = objective.configs[2].clone();
let incumbent_id = incumbent.id.clone();
let out = Autotuner::new("t")
.require(vec![incumbent])
.run_objective(&mut objective)
.unwrap();
assert!(
out.trials.iter().any(|t| t.config_id == incumbent_id),
"the incumbent's trial must be reported even when the budget \
stops the searcher: {:?}",
out.trials
);
}
#[test]
fn duplicate_require_measures_the_incumbent_once() {
let mut objective = indexed_objective(&[3.0, 1.0, 2.0]);
let incumbent = objective.configs[2].clone();
let incumbent_id = incumbent.id.clone();
let out = Autotuner::new("t")
.require(vec![incumbent.clone(), incumbent])
.run_objective(&mut objective)
.unwrap();
assert_eq!(
objective
.measured
.iter()
.filter(|id| **id == incumbent_id)
.count(),
1,
"a duplicated required config is measured only once: {:?}",
objective.measured
);
assert_eq!(out.trials.len(), 3, "no duplicate trials");
}
#[test]
fn required_measurement_failure_is_an_error() {
fn cost(c: &Config) -> Option<f32> {
c.int("i")
.filter(|i| *i != 0)
.map(|i| [0.0f32, 1.0, 2.0][i as usize])
}
let mut objective = FakeObjective {
configs: (0..3)
.map(|i| Config::new([("i", ParamValue::Int(i))]))
.collect(),
cost,
measured: Vec::new(),
budget: None,
};
let failing = objective.configs[0].clone();
let err = Autotuner::new("t")
.require(vec![failing])
.run_objective(&mut objective)
.unwrap_err();
assert!(
err.to_string().contains("failed to measure"),
"unexpected error: {err}"
);
}
#[test]
fn a_mislabeled_config_id_is_stamped_from_the_dispatched_index() {
struct Mislabel {
configs: Vec<Config>,
}
impl Objective for Mislabel {
fn configs(&self) -> &[Config] {
&self.configs
}
fn measure(&mut self, index: usize) -> Trial {
Trial::measured(format!("BOGUS-{index}"), [3.0f32, 1.0, 2.0][index], 1.0, 3)
}
fn budget_remaining(&self) -> Option<Duration> {
None
}
}
let mut objective = Mislabel {
configs: (0..3)
.map(|i| Config::new([("i", ParamValue::Int(i))]))
.collect(),
};
let want = objective.configs[1].id.clone(); let out = Autotuner::new("t").run_objective(&mut objective).unwrap();
assert_eq!(
out.best.as_ref().map(|c| c.id.clone()),
Some(want),
"winner should be the real best config despite mislabeled ids"
);
assert!(
out.trials.iter().all(|t| !t.config_id.starts_with("BOGUS")),
"trial ids should be stamped, not the bogus echoes: {:?}",
out.trials
);
}
#[test]
fn a_pre_measured_trial_supersedes_a_searcher_invalid_of_the_same_config() {
let mut trials = vec![Trial::invalid("c", "transient re-measure")];
let pre_measured = vec![Trial::measured("c", 1.0, 1.0, 3)];
merge_unclaimed(&mut trials, pre_measured);
assert_eq!(trials.len(), 1, "no duplicate trial for the same config");
assert_eq!(
trials[0].median_ms(),
Some(1.0),
"the Measured pre-measurement replaced the searcher's Invalid: {:?}",
trials
);
}
#[test]
fn measured_with_a_non_finite_min_is_invalid() {
assert!(
Trial::measured("c", 1.0, f32::INFINITY, 3)
.median_ms()
.is_none(),
"non-finite min must be recorded Invalid, not Measured"
);
assert!(
Trial::measured("c", f32::NAN, 1.0, 3).median_ms().is_none(),
"non-finite median must be recorded Invalid"
);
assert!(
Trial::measured("c", 1.0, 0.5, 3).median_ms().is_some(),
"finite timings remain Measured"
);
}
}