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.
184    // The sort puts any success first, so `None` here means *no* candidate
185    // measured — which still happens: an all-failed plan can decide an
186    // unmeasured winner (see `Schedule::run_plan`), and the report must not
187    // be what kills it. The schedule already warned with the full results;
188    // there is no measurement to record.
189    let Some(fastest) = results
190        .first()
191        .and_then(|result| result.outcome.as_ref().ok())
192    else {
193        return;
194    };
195
196    if recording {
197        write_record(logger, key, results, log_context, fastest);
198    }
199    write_log(logger, level, key, results, log_context, fastest);
200}
201
202/// The record, for tools: one JSON object on the recorder's sink.
203#[cfg_attr(not(std_io), allow(unused_variables))]
204fn write_record<K: AutotuneKey>(
205    logger: &mut Logger,
206    key: &K,
207    results: &[AutotuneResult],
208    log_context: Option<&AutotuneLogContext>,
209    fastest: &AutotuneOutcome,
210) {
211    #[cfg(std_io)]
212    {
213        let record = AutotuneRecord {
214            key: Cow::Borrowed(key),
215            fastest_index: fastest.index,
216            fastest_time: fastest.computation.median,
217            results: Cow::Borrowed(results),
218            log_context: log_context.map(Cow::Borrowed),
219            checks: log_context
220                .and_then(|c| c.checks.as_deref())
221                .map(Cow::Borrowed),
222        };
223
224        let msg = serde_json::to_string(&record).unwrap_or_else(|err| {
225            format!("{{\"error\": \"Failed to serialize the autotune record: {err}\"}}")
226        });
227        logger.log_autotune_record(&msg);
228    }
229    #[cfg(not(std_io))]
230    {
231        logger.log_autotune_record(
232            &"{\"error\": \"Recording autotune is not available without std_io\"}",
233        );
234    }
235}
236
237/// The line for humans, at whatever the logger's level asks for.
238fn write_log<K: AutotuneKey>(
239    logger: &mut Logger,
240    level: AutotuneLogLevel,
241    key: &K,
242    results: &[AutotuneResult],
243    log_context: Option<&AutotuneLogContext>,
244    fastest: &AutotuneOutcome,
245) {
246    match level {
247        AutotuneLogLevel::Minimal => {
248            let top_times = results
249                .iter()
250                .filter_map(|r| {
251                    r.outcome
252                        .as_ref()
253                        .ok()
254                        .map(|o| (o.index, o.computation.median))
255                })
256                .take(3)
257                .collect::<Vec<_>>();
258
259            let context_str = log_context
260                .map(|c| format!(", context: {}", c))
261                .unwrap_or_default();
262            logger.log_autotune(&format!(
263                "Fastest result {}-{key}. Top 3 times: {top_times:?}{context_str}",
264                fastest.name,
265            ));
266        }
267        AutotuneLogLevel::Full => {
268            let mut context_str = String::new();
269            if let Some(ctx) = log_context {
270                use core::fmt::Write;
271                if let Some(b) = &ctx.bounds {
272                    let _ = writeln!(
273                        &mut context_str,
274                        "Calculated bounds: {:?} - limit: {:?}",
275                        b, ctx.limit
276                    );
277                }
278                let _ = write!(&mut context_str, "{}", ctx);
279            }
280
281            logger.log_autotune(&format!(
282                "Fastest result {}-{key}.\nContext:\n{context_str}",
283                fastest.name,
284            ));
285
286            for result in results.iter() {
287                match &result.outcome {
288                    Ok(val) => {
289                        logger.log_autotune(&format!("{val}"));
290                    }
291                    Err(err) => logger.log_autotune(&format!("{err}")),
292                }
293            }
294        }
295        AutotuneLogLevel::Disabled => {}
296    }
297}