Skip to main content

cubecl_runtime/tune/
log.rs

1use crate::config::{Logger, autotune::AutotuneLogLevel};
2use crate::tune::{AutotuneKey, AutotuneOutcome, AutotuneResult};
3#[cfg(std_io)]
4use alloc::borrow::Cow;
5use alloc::format;
6use alloc::string::String;
7use alloc::vec::Vec;
8use core::time::Duration;
9
10/// Events that occurred during autotuning, useful for observability and logging.
11#[derive(Debug, Clone)]
12#[cfg_attr(std_io, derive(serde::Serialize, serde::Deserialize))]
13pub enum AutotuneLogEvent {
14    /// Tracks a tunable kernel that was executed during autotuning.
15    TuningStep(String, Duration),
16    /// A short circuit event where autotuning stopped early because this candidate
17    /// achieved sufficient throughput.
18    ShortCircuit(String),
19}
20
21/// The context containing bounds, limits, and events that happened during autotuning.
22#[derive(Debug, Clone, Default)]
23#[cfg_attr(std_io, derive(serde::Serialize, serde::Deserialize))]
24pub struct AutotuneLogContext {
25    /// Calculated bounds for autotuning.
26    pub bounds: Option<crate::tune::Bounds>,
27    /// The time limit to exceed for early short-circuiting.
28    pub limit: Option<Duration>,
29    /// The chronological list of tuning events.
30    pub events: Vec<AutotuneLogEvent>,
31    /// The results of the checks.
32    pub checks: Option<Vec<crate::tune::log::CheckResult>>,
33}
34
35impl AutotuneLogContext {
36    /// Creates a new log context if either the human logger or the machine-readable recorder is
37    /// enabled. The recorder is independent of the logger, so records are still populated when the
38    /// logger is disabled.
39    pub fn new(logger: &mut Logger) -> Option<Self> {
40        let logging = !matches!(logger.log_level_autotune(), AutotuneLogLevel::Disabled);
41        if logging || logger.autotune_recording_enabled() {
42            Some(Self {
43                bounds: None,
44                limit: None,
45                events: Vec::new(),
46                checks: None,
47            })
48        } else {
49            None
50        }
51    }
52}
53
54impl core::fmt::Display for AutotuneLogContext {
55    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
56        for event in &self.events {
57            match event {
58                AutotuneLogEvent::TuningStep(step, duration) => {
59                    write!(f, "\n - Tuning: {step} (compilation & bench: {duration:?})")?
60                }
61                AutotuneLogEvent::ShortCircuit(name) => write!(
62                    f,
63                    "\nShort circuiting autotune. {name} is close enough to peak throughput."
64                )?,
65            }
66        }
67        Ok(())
68    }
69}
70
71/// Extension trait for `Option<AutotuneLogContext>` and `Option<&mut AutotuneLogContext>`.
72pub trait AutotuneLoggerExt {
73    /// Pushes a short circuit event if logging is enabled.
74    fn push_short_circuit(&mut self, name: String);
75    /// Pushes a tuning step event if logging is enabled.
76    fn push_tuning_step(&mut self, name: String, duration: Duration);
77    /// Sets the tuning bounds if logging is active.
78    fn set_bounds(&mut self, bounds: Option<crate::tune::Bounds>);
79    /// Sets the tuning limit if logging is active.
80    fn set_limit(&mut self, limit: Option<Duration>);
81    /// Sets checks if logging is active.
82    fn set_checks(&mut self, checks: impl FnOnce() -> Vec<CheckResult>);
83    /// Logs the benchmark result if logging is enabled.
84    fn log_result<K: AutotuneKey>(&self, logger: &mut Logger, key: &K, results: &[AutotuneResult]);
85}
86
87/// Implements `AutotuneLoggerExt` for an `Option`-like wrapper around `AutotuneLogContext`.
88/// The two concrete types (`Option<AutotuneLogContext>` and `Option<&mut AutotuneLogContext>`)
89/// differ only in how they reach the inner `&mut AutotuneLogContext`: `.as_mut()` vs
90/// `.as_deref_mut()`, and `.as_ref()` vs `.as_deref()`.
91macro_rules! impl_autotune_logger_ext {
92    ($ty:ty, $as_mut:ident, $as_ref:ident) => {
93        impl AutotuneLoggerExt for $ty {
94            fn push_short_circuit(&mut self, name: String) {
95                if let Some(ctx) = self.$as_mut() {
96                    ctx.events.push(AutotuneLogEvent::ShortCircuit(name));
97                }
98            }
99
100            fn push_tuning_step(&mut self, name: String, duration: Duration) {
101                if let Some(ctx) = self.$as_mut() {
102                    ctx.events
103                        .push(AutotuneLogEvent::TuningStep(name, duration));
104                }
105            }
106
107            fn set_bounds(&mut self, bounds: Option<crate::tune::Bounds>) {
108                if let Some(ctx) = self.$as_mut() {
109                    ctx.bounds = bounds;
110                }
111            }
112
113            fn set_limit(&mut self, limit: Option<Duration>) {
114                if let Some(ctx) = self.$as_mut() {
115                    ctx.limit = limit;
116                }
117            }
118
119            fn set_checks(&mut self, checks: impl FnOnce() -> Vec<CheckResult>) {
120                if let Some(ctx) = self.$as_mut() {
121                    ctx.checks = Some(checks());
122                }
123            }
124
125            fn log_result<K: AutotuneKey>(
126                &self,
127                logger: &mut Logger,
128                key: &K,
129                results: &[AutotuneResult],
130            ) {
131                log_result(logger, key, results, self.$as_ref());
132            }
133        }
134    };
135}
136
137impl_autotune_logger_ext!(Option<AutotuneLogContext>, as_mut, as_ref);
138impl_autotune_logger_ext!(Option<&'_ mut AutotuneLogContext>, as_deref_mut, as_deref);
139
140/// The complete record of one tuning decision, written as JSON when the autotune recorder has a
141/// sink configured. One record per line, per decision, in a fixed schema for tools to read back.
142#[cfg(std_io)]
143#[derive(serde::Serialize, serde::Deserialize)]
144#[serde(bound(deserialize = "K: Clone + serde::Deserialize<'de>"))]
145pub struct AutotuneRecord<'a, K: Clone> {
146    /// The key for the autotuning job.
147    pub key: Cow<'a, K>,
148    /// The index of the fastest candidate.
149    pub fastest_index: usize,
150    /// The time taken by the fastest candidate.
151    pub fastest_time: Duration,
152    /// All benchmarking results.
153    pub results: Cow<'a, [AutotuneResult]>,
154    /// Logging context with bounds, limit, and events.
155    pub log_context: Option<Cow<'a, AutotuneLogContext>>,
156    /// Check results if autotune-checks is enabled else None.
157    pub checks: Option<Cow<'a, [CheckResult]>>,
158}
159
160/// The check result for a single benchmark.
161#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
162pub struct CheckResult {
163    /// The name of the benchmark.
164    pub name: String,
165    /// Whether the check passed.
166    pub passed: bool,
167}
168
169/// Emit the autotune result: a line for humans at the logger's level and, independently, the
170/// [`AutotuneRecord`] for tools if the recorder has a sink. Either, both, or neither.
171fn log_result<K: AutotuneKey>(
172    logger: &mut Logger,
173    key: &K,
174    results: &[AutotuneResult],
175    log_context: Option<&AutotuneLogContext>,
176) {
177    let level = logger.log_level_autotune();
178    let recording = logger.autotune_recording_enabled();
179    if matches!(level, AutotuneLogLevel::Disabled) && !recording {
180        return;
181    }
182
183    // Shared by both sinks, and resolved only once one of them is listening: it assumes a candidate
184    // succeeded, which is not true of every tuning pass.
185    let fastest = results
186        .first()
187        .expect("At least one kernel needed.")
188        .outcome
189        .as_ref()
190        .expect("At least one kernel has to succeed.");
191
192    if recording {
193        write_record(logger, key, results, log_context, fastest);
194    }
195    write_log(logger, level, key, results, log_context, fastest);
196}
197
198/// The record, for tools: one JSON object on the recorder's sink.
199#[cfg_attr(not(std_io), allow(unused_variables))]
200fn write_record<K: AutotuneKey>(
201    logger: &mut Logger,
202    key: &K,
203    results: &[AutotuneResult],
204    log_context: Option<&AutotuneLogContext>,
205    fastest: &AutotuneOutcome,
206) {
207    #[cfg(std_io)]
208    {
209        let record = AutotuneRecord {
210            key: Cow::Borrowed(key),
211            fastest_index: fastest.index,
212            fastest_time: fastest.computation.median,
213            results: Cow::Borrowed(results),
214            log_context: log_context.map(Cow::Borrowed),
215            checks: log_context
216                .and_then(|c| c.checks.as_deref())
217                .map(Cow::Borrowed),
218        };
219
220        let msg = serde_json::to_string(&record).unwrap_or_else(|err| {
221            format!("{{\"error\": \"Failed to serialize the autotune record: {err}\"}}")
222        });
223        logger.log_autotune_record(&msg);
224    }
225    #[cfg(not(std_io))]
226    {
227        logger.log_autotune_record(
228            &"{\"error\": \"Recording autotune is not available without std_io\"}",
229        );
230    }
231}
232
233/// The line for humans, at whatever the logger's level asks for.
234fn write_log<K: AutotuneKey>(
235    logger: &mut Logger,
236    level: AutotuneLogLevel,
237    key: &K,
238    results: &[AutotuneResult],
239    log_context: Option<&AutotuneLogContext>,
240    fastest: &AutotuneOutcome,
241) {
242    match level {
243        AutotuneLogLevel::Minimal => {
244            let top_times = results
245                .iter()
246                .filter_map(|r| {
247                    r.outcome
248                        .as_ref()
249                        .ok()
250                        .map(|o| (o.index, o.computation.median))
251                })
252                .take(3)
253                .collect::<Vec<_>>();
254
255            let context_str = log_context
256                .map(|c| format!(", context: {}", c))
257                .unwrap_or_default();
258            logger.log_autotune(&format!(
259                "Fastest result {}-{key}. Top 3 times: {top_times:?}{context_str}",
260                fastest.name,
261            ));
262        }
263        AutotuneLogLevel::Full => {
264            let mut context_str = String::new();
265            if let Some(ctx) = log_context {
266                use core::fmt::Write;
267                if let Some(b) = &ctx.bounds {
268                    let _ = writeln!(
269                        &mut context_str,
270                        "Calculated bounds: {:?} - limit: {:?}",
271                        b, ctx.limit
272                    );
273                }
274                let _ = write!(&mut context_str, "{}", ctx);
275            }
276
277            logger.log_autotune(&format!(
278                "Fastest result {}-{key}.\nContext:\n{context_str}",
279                fastest.name,
280            ));
281
282            for result in results.iter() {
283                match &result.outcome {
284                    Ok(val) => {
285                        logger.log_autotune(&format!("{val}"));
286                    }
287                    Err(err) => logger.log_autotune(&format!("{err}")),
288                }
289            }
290        }
291        AutotuneLogLevel::Disabled => {}
292    }
293}