use crate::config::{Logger, autotune::AutotuneLogLevel};
use crate::tune::{AutotuneKey, AutotuneOutcome, AutotuneResult};
#[cfg(std_io)]
use alloc::borrow::Cow;
use alloc::format;
use alloc::string::String;
use alloc::vec::Vec;
use core::time::Duration;
#[derive(Debug, Clone)]
#[cfg_attr(std_io, derive(serde::Serialize, serde::Deserialize))]
pub enum AutotuneLogEvent {
TuningStep(String, Duration),
ShortCircuit(String),
}
#[derive(Debug, Clone, Default)]
#[cfg_attr(std_io, derive(serde::Serialize, serde::Deserialize))]
pub struct AutotuneLogContext {
pub bounds: Option<crate::tune::Bounds>,
pub limit: Option<Duration>,
pub events: Vec<AutotuneLogEvent>,
pub checks: Option<Vec<crate::tune::log::CheckResult>>,
}
impl AutotuneLogContext {
pub fn new(logger: &mut Logger) -> Option<Self> {
let logging = !matches!(logger.log_level_autotune(), AutotuneLogLevel::Disabled);
if logging || logger.autotune_recording_enabled() {
Some(Self {
bounds: None,
limit: None,
events: Vec::new(),
checks: None,
})
} else {
None
}
}
}
impl core::fmt::Display for AutotuneLogContext {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
for event in &self.events {
match event {
AutotuneLogEvent::TuningStep(step, duration) => {
write!(f, "\n - Tuning: {step} (compilation & bench: {duration:?})")?
}
AutotuneLogEvent::ShortCircuit(name) => write!(
f,
"\nShort circuiting autotune. {name} is close enough to peak throughput."
)?,
}
}
Ok(())
}
}
pub trait AutotuneLoggerExt {
fn push_short_circuit(&mut self, name: String);
fn push_tuning_step(&mut self, name: String, duration: Duration);
fn set_bounds(&mut self, bounds: Option<crate::tune::Bounds>);
fn set_limit(&mut self, limit: Option<Duration>);
fn set_checks(&mut self, checks: impl FnOnce() -> Vec<CheckResult>);
fn log_result<K: AutotuneKey>(&self, logger: &mut Logger, key: &K, results: &[AutotuneResult]);
}
macro_rules! impl_autotune_logger_ext {
($ty:ty, $as_mut:ident, $as_ref:ident) => {
impl AutotuneLoggerExt for $ty {
fn push_short_circuit(&mut self, name: String) {
if let Some(ctx) = self.$as_mut() {
ctx.events.push(AutotuneLogEvent::ShortCircuit(name));
}
}
fn push_tuning_step(&mut self, name: String, duration: Duration) {
if let Some(ctx) = self.$as_mut() {
ctx.events
.push(AutotuneLogEvent::TuningStep(name, duration));
}
}
fn set_bounds(&mut self, bounds: Option<crate::tune::Bounds>) {
if let Some(ctx) = self.$as_mut() {
ctx.bounds = bounds;
}
}
fn set_limit(&mut self, limit: Option<Duration>) {
if let Some(ctx) = self.$as_mut() {
ctx.limit = limit;
}
}
fn set_checks(&mut self, checks: impl FnOnce() -> Vec<CheckResult>) {
if let Some(ctx) = self.$as_mut() {
ctx.checks = Some(checks());
}
}
fn log_result<K: AutotuneKey>(
&self,
logger: &mut Logger,
key: &K,
results: &[AutotuneResult],
) {
log_result(logger, key, results, self.$as_ref());
}
}
};
}
impl_autotune_logger_ext!(Option<AutotuneLogContext>, as_mut, as_ref);
impl_autotune_logger_ext!(Option<&'_ mut AutotuneLogContext>, as_deref_mut, as_deref);
#[cfg(std_io)]
#[derive(serde::Serialize, serde::Deserialize)]
#[serde(bound(deserialize = "K: Clone + serde::Deserialize<'de>"))]
pub struct AutotuneRecord<'a, K: Clone> {
pub key: Cow<'a, K>,
pub fastest_index: usize,
pub fastest_time: Duration,
pub results: Cow<'a, [AutotuneResult]>,
pub log_context: Option<Cow<'a, AutotuneLogContext>>,
pub checks: Option<Cow<'a, [CheckResult]>>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct CheckResult {
pub name: String,
pub passed: bool,
}
fn log_result<K: AutotuneKey>(
logger: &mut Logger,
key: &K,
results: &[AutotuneResult],
log_context: Option<&AutotuneLogContext>,
) {
let level = logger.log_level_autotune();
let recording = logger.autotune_recording_enabled();
if matches!(level, AutotuneLogLevel::Disabled) && !recording {
return;
}
let fastest = results
.first()
.expect("At least one kernel needed.")
.outcome
.as_ref()
.expect("At least one kernel has to succeed.");
if recording {
write_record(logger, key, results, log_context, fastest);
}
write_log(logger, level, key, results, log_context, fastest);
}
#[cfg_attr(not(std_io), allow(unused_variables))]
fn write_record<K: AutotuneKey>(
logger: &mut Logger,
key: &K,
results: &[AutotuneResult],
log_context: Option<&AutotuneLogContext>,
fastest: &AutotuneOutcome,
) {
#[cfg(std_io)]
{
let record = AutotuneRecord {
key: Cow::Borrowed(key),
fastest_index: fastest.index,
fastest_time: fastest.computation.median,
results: Cow::Borrowed(results),
log_context: log_context.map(Cow::Borrowed),
checks: log_context
.and_then(|c| c.checks.as_deref())
.map(Cow::Borrowed),
};
let msg = serde_json::to_string(&record).unwrap_or_else(|err| {
format!("{{\"error\": \"Failed to serialize the autotune record: {err}\"}}")
});
logger.log_autotune_record(&msg);
}
#[cfg(not(std_io))]
{
logger.log_autotune_record(
&"{\"error\": \"Recording autotune is not available without std_io\"}",
);
}
}
fn write_log<K: AutotuneKey>(
logger: &mut Logger,
level: AutotuneLogLevel,
key: &K,
results: &[AutotuneResult],
log_context: Option<&AutotuneLogContext>,
fastest: &AutotuneOutcome,
) {
match level {
AutotuneLogLevel::Minimal => {
let top_times = results
.iter()
.filter_map(|r| {
r.outcome
.as_ref()
.ok()
.map(|o| (o.index, o.computation.median))
})
.take(3)
.collect::<Vec<_>>();
let context_str = log_context
.map(|c| format!(", context: {}", c))
.unwrap_or_default();
logger.log_autotune(&format!(
"Fastest result {}-{key}. Top 3 times: {top_times:?}{context_str}",
fastest.name,
));
}
AutotuneLogLevel::Full => {
let mut context_str = String::new();
if let Some(ctx) = log_context {
use core::fmt::Write;
if let Some(b) = &ctx.bounds {
let _ = writeln!(
&mut context_str,
"Calculated bounds: {:?} - limit: {:?}",
b, ctx.limit
);
}
let _ = write!(&mut context_str, "{}", ctx);
}
logger.log_autotune(&format!(
"Fastest result {}-{key}.\nContext:\n{context_str}",
fastest.name,
));
for result in results.iter() {
match &result.outcome {
Ok(val) => {
logger.log_autotune(&format!("{val}"));
}
Err(err) => logger.log_autotune(&format!("{err}")),
}
}
}
AutotuneLogLevel::Disabled => {}
}
}