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}