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