Skip to main content

cubecl_runtime/config/
autotune.rs

1use super::logger::{LogLevel, LoggerConfig};
2
3/// Configuration for autotuning in `CubeCL`.
4#[derive(Default, Clone, Debug, serde::Serialize, serde::Deserialize)]
5pub struct AutotuneConfig {
6    /// Logger configuration for autotune logs, using autotune-specific log levels.
7    #[serde(default)]
8    pub logger: LoggerConfig<AutotuneLogLevel>,
9
10    /// Recorder configuration: where to write one [`AutotuneRecord`](crate::tune::AutotuneRecord)
11    /// per tuning decision, as JSON, for a tool to read back.
12    ///
13    /// Independent of [`logger`](Self::logger), because the two answer different questions and both
14    /// can be wanted at once: the logger's level says how much to tell a human, the recorder says
15    /// where to put the machine-readable record.
16    #[serde(default)]
17    pub recorder: LoggerConfig<RecorderLevel>,
18
19    /// Autotune level, controlling the intensity of autotuning.
20    #[serde(default)]
21    pub level: AutotuneLevel,
22
23    /// Whether to disable the persistent cache of autotune results.
24    ///
25    /// The in-memory cache is unaffected: a key is still tuned only once per process.
26    #[serde(default)]
27    pub disable_cache: bool,
28
29    /// Whether to disable the short circuit logic during autotuning.
30    #[serde(default)]
31    pub disable_short_circuit: bool,
32
33    /// Sampling budget and elimination thresholds used while benchmarking candidates.
34    #[serde(default)]
35    pub bench: BenchConfig,
36}
37
38/// Controls how many samples autotune collects per candidate and when candidates are dropped.
39///
40/// Only [`max_samples`](Self::max_samples) and [`adaptive`](Self::adaptive) mean anything to the
41/// fixed-count pass; the rest describe elimination, which only the adaptive scheduler performs.
42/// Each field says so, because a knob that silently does nothing on the strategy actually running
43/// is worse than no knob at all — and `adaptive` is native-only, so on wasm that is every run.
44#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
45#[serde(default)]
46pub struct BenchConfig {
47    /// Samples every surviving candidate gets before any elimination happens.
48    ///
49    /// Adaptive only: the fixed-count pass eliminates nothing, so every candidate gets
50    /// [`max_samples`](Self::max_samples) regardless.
51    pub min_samples: usize,
52
53    /// Upper bound on samples collected for a single candidate.
54    ///
55    /// Read by both strategies: the ceiling for the adaptive scheduler, and the flat count for
56    /// the fixed pass, which has no elimination to spend a smaller budget on.
57    pub max_samples: usize,
58
59    /// Samples that must independently land under the time limit to short circuit.
60    ///
61    /// A short circuit decision is written to the persistent cache and reused on later runs, so
62    /// it is confirmed rather than taken from a single possibly-lucky sample.
63    ///
64    /// Adaptive only: the fixed pass has the whole sample set in hand before it tests the limit,
65    /// so it has nothing to confirm.
66    pub short_circuit_samples: usize,
67
68    /// How many times slower than the current best a candidate may be before elimination.
69    ///
70    /// Adaptive only. Values below `1.0` are read as `1.0`, which eliminates every candidate
71    /// slower than the leader that the survivor floor allows.
72    pub speed_factor: f64,
73
74    /// Whether to use the adaptive round robin benchmark instead of a fixed sample count.
75    ///
76    /// Ignored on wasm, which cannot resolve samples between rounds and so always takes the
77    /// fixed-count pass.
78    pub adaptive: bool,
79}
80
81impl Default for BenchConfig {
82    fn default() -> Self {
83        Self {
84            min_samples: 3,
85            max_samples: 10,
86            short_circuit_samples: 2,
87            speed_factor: 1.5,
88            adaptive: true,
89        }
90    }
91}
92
93/// Every knob is read through an accessor that clamps it into its usable range, so a config file
94/// can hold a nonsensical value without any single call site having to remember the repair.
95impl BenchConfig {
96    /// The sample budget, clamped so the range is always usable.
97    pub fn samples(&self) -> (usize, usize) {
98        let min = self.min_samples.max(1);
99        (min, self.max_samples.max(min))
100    }
101
102    /// How many samples must land under the limit, clamped so a short circuit always needs one.
103    pub fn short_circuit_samples(&self) -> usize {
104        self.short_circuit_samples.max(1)
105    }
106
107    /// The elimination threshold, clamped so it can never sit below the leader's own time.
108    pub fn speed_factor(&self) -> f64 {
109        self.speed_factor.max(1.0)
110    }
111}
112
113/// Log levels for autotune logging in `CubeCL`.
114#[derive(Default, Clone, Copy, Debug, serde::Serialize, serde::Deserialize)]
115pub enum AutotuneLogLevel {
116    /// Autotune logging is disabled.
117    #[serde(rename = "disabled")]
118    Disabled,
119
120    /// Minimal autotune information is logged such as the fastest kernel selected and a few
121    /// statistics (default).
122    #[default]
123    #[serde(rename = "minimal")]
124    Minimal,
125
126    /// Full autotune details are logged.
127    #[serde(rename = "full")]
128    Full,
129}
130
131impl LogLevel for AutotuneLogLevel {}
132
133/// The recorder's (absent) verbosity.
134///
135/// A record is one fixed schema, which is the whole point: a tool reads it back and depends on its
136/// shape, so there is no "how much" to choose. The recorder is simply on when it has a sink
137/// (see [`AutotuneConfig::recording_enabled`]); this type exists only so it can reuse
138/// [`LoggerConfig`]'s sinks.
139#[derive(Default, Clone, Copy, Debug, serde::Serialize, serde::Deserialize)]
140pub struct RecorderLevel;
141
142impl LogLevel for RecorderLevel {}
143
144impl AutotuneConfig {
145    /// Whether tuning decisions are being recorded, i.e. the recorder has somewhere to write.
146    pub fn recording_enabled(&self) -> bool {
147        #[cfg(std_io)]
148        let has_file = self.recorder.file.is_some();
149        #[cfg(not(std_io))]
150        let has_file = false;
151
152        has_file || self.recorder.stdout || self.recorder.stderr
153    }
154}
155
156/// Autotune levels controlling the intensity of autotuning.
157#[derive(Default, Clone, Debug, serde::Serialize, serde::Deserialize)]
158pub enum AutotuneLevel {
159    /// Minimal autotuning effort.
160    #[serde(rename = "minimal")]
161    Minimal,
162
163    /// Balanced autotuning effort (default).
164    #[default]
165    #[serde(rename = "balanced")]
166    Balanced,
167
168    /// Increased autotuning effort.
169    #[serde(rename = "extensive")]
170    Extensive,
171
172    /// Maximum autotuning effort.
173    #[serde(rename = "full")]
174    Full,
175}