cubecl_runtime/config/compilation.rs
1use super::logger::{LogLevel, LoggerConfig};
2use cubecl_ir::settings::DebugInfo;
3
4/// The debug data the cargo profile asks for: [`LineTables`](DebugInfo::LineTables) when the
5/// profile sets `debug`, as `dev` does by default, else [`None`](DebugInfo::None).
6pub const PROFILE_DEBUG_INFO: DebugInfo = if cfg!(cubecl_debug_info) {
7 DebugInfo::LineTables
8} else {
9 DebugInfo::None
10};
11
12/// The debug data of a kernel that asks for `requested`, with the global configuration: the same
13/// level as [`CompilationConfig::resolve_debug_info`]. It reads the configuration once, so a kernel
14/// id can call it at each launch.
15pub fn effective_debug_info(requested: DebugInfo) -> DebugInfo {
16 use super::{CubeClRuntimeConfig, RuntimeConfig};
17 use core::sync::atomic::{AtomicU8, Ordering};
18
19 const UNREAD: u8 = u8::MAX;
20 const NO_LIMIT: u8 = u8::MAX - 1;
21 static LIMIT: AtomicU8 = AtomicU8::new(UNREAD);
22
23 let mut limit = LIMIT.load(Ordering::Relaxed);
24 if limit == UNREAD {
25 limit = CubeClRuntimeConfig::get()
26 .compilation
27 .debug_info
28 .map_or(NO_LIMIT, |level| level as u8);
29 LIMIT.store(limit, Ordering::Relaxed);
30 }
31 // Decode with the casts that encoded the level, so the order of the variants does not matter.
32 let limit = [DebugInfo::None, DebugInfo::LineTables, DebugInfo::Full]
33 .into_iter()
34 .find(|level| *level as u8 == limit);
35 resolve_debug_info(requested, limit)
36}
37
38/// At least [`PROFILE_DEBUG_INFO`], at most `limit`.
39fn resolve_debug_info(requested: DebugInfo, limit: Option<DebugInfo>) -> DebugInfo {
40 let level = requested.max(PROFILE_DEBUG_INFO);
41 limit.map_or(level, |limit| level.min(limit))
42}
43
44/// Configuration for compilation settings in `CubeCL`.
45#[derive(Default, Clone, Debug, serde::Serialize, serde::Deserialize)]
46pub struct CompilationConfig {
47 /// Logger configuration for compilation logs, using binary log levels.
48 #[serde(default)]
49 pub logger: LoggerConfig<CompilationLogLevel>,
50 /// Whether compiled kernels are cached in the active environment.
51 #[serde(default)]
52 #[cfg(persistence)]
53 pub cache: bool,
54 /// Controls whether kernel launches enforce bounds checks.
55 #[serde(default)]
56 pub check_mode: BoundsCheckMode,
57 /// How far the CPU runtime carries an f16 intermediate in f32 before rounding it. `None`
58 /// chooses by whether the host computes in f16 directly. Other runtimes ignore it.
59 #[serde(default)]
60 pub f16_evaluation: Option<F16Evaluation>,
61 /// Directory for compiler dumps. Each kernel writes its IR after every compiler pass, and
62 /// its final source, to a subdirectory named after the kernel. Set by `CUBECL_DEBUG_PLIRON`.
63 #[serde(default)]
64 #[cfg(feature = "std")]
65 pub dump_dir: Option<std::path::PathBuf>,
66 /// Log how long each compiler pass takes, at the `info` level. Set by `CUBECL_TIME_PASSES`.
67 #[serde(default)]
68 pub time_passes: bool,
69 /// The most debug data a kernel may carry. `None` keeps what the cargo profile and the kernel
70 /// ask for. It can only lower that level. Set by `CUBECL_DEBUG_INFO`.
71 #[serde(default)]
72 pub debug_info: Option<DebugInfo>,
73 /// The format of the debug data in SPIR-V kernels. Set by `CUBECL_SPIRV_DEBUG_FORMAT`.
74 #[serde(default)]
75 pub spirv_debug_format: SpirvDebugFormat,
76 /// A directory for the source text of kernels with full debug data. When no directory on this
77 /// computer has the source files of a kernel, cubecl writes the texts into this directory, and
78 /// the debug data points to them. `None` writes no files. Set by `CUBECL_SOURCE_CACHE`.
79 #[serde(default)]
80 #[cfg(feature = "std")]
81 pub source_cache: Option<std::path::PathBuf>,
82 /// How many threads a server compiles a queue of kernels on — see
83 /// [`LaunchMode::Queue`](crate::execution::LaunchMode::Queue). The
84 /// queue itself is as long as what was queued; each thread takes the next
85 /// kernel as it finishes one. `None` uses every core the process may run
86 /// on; a smaller number leaves cores to other work. It does not bound
87 /// memory: a batch holds every queued kernel's artifacts until it ends.
88 #[serde(default)]
89 pub parallelism: Option<usize>,
90}
91
92impl CompilationConfig {
93 /// The debug data of a kernel that asks for `requested`: at least [`PROFILE_DEBUG_INFO`], at
94 /// most [`debug_info`](Self::debug_info).
95 #[must_use]
96 pub fn resolve_debug_info(&self, requested: DebugInfo) -> DebugInfo {
97 resolve_debug_info(requested, self.debug_info)
98 }
99
100 /// How many threads compile at once: the configured count, or every core
101 /// the process may run on. Never zero, and one without threads — on wasm
102 /// too, whatever is configured, since it cannot spawn them.
103 pub fn parallelism(&self) -> usize {
104 #[cfg(all(feature = "std", not(target_family = "wasm")))]
105 let threads = self
106 .parallelism
107 .unwrap_or_else(|| std::thread::available_parallelism().map_or(1, |cores| cores.get()));
108 #[cfg(any(not(feature = "std"), target_family = "wasm"))]
109 let threads = 1;
110
111 threads.max(1)
112 }
113}
114
115/// How far an f32 intermediate is allowed to travel before it is rounded back to f16.
116#[derive(Default, Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
117pub enum F16Evaluation {
118 /// Round after every operation, which is what a GPU does. Where f16 is not native it costs a
119 /// convert pair per operation.
120 #[serde(rename = "per-operation")]
121 PerOperation,
122 /// Round where a value is stored, a `let mut` included, or read by anything but arithmetic.
123 /// The default where f16 is not native.
124 #[default]
125 #[serde(rename = "chain")]
126 Chain,
127 /// Also hold a private f16 variable in f32 where that removes more converts than it adds, so
128 /// a running total read often enough inside its loop stays in f16. Costs vector registers, so
129 /// a wide kernel may want a narrower line.
130 #[serde(rename = "accumulators")]
131 Accumulators,
132}
133
134impl F16Evaluation {
135 /// The mode for a host that does or does not compute in f16 directly. Where it does, a chain
136 /// held in f32 only adds converts.
137 pub fn for_native_f16(native: bool) -> Self {
138 match native {
139 true => Self::PerOperation,
140 false => Self::Chain,
141 }
142 }
143}
144
145impl core::fmt::Display for F16Evaluation {
146 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
147 f.write_str(match self {
148 Self::PerOperation => "per-operation",
149 Self::Chain => "chain",
150 Self::Accumulators => "accumulators",
151 })
152 }
153}
154
155/// The format of the debug data in SPIR-V kernels. A kernel without debug data ignores it.
156#[derive(Default, Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
157pub enum SpirvDebugFormat {
158 /// `NonSemantic` when the device supports it, else `OpLine`.
159 #[default]
160 #[serde(rename = "auto")]
161 Auto,
162 /// Core `OpLine`: the line of the innermost `#[cube]` function only. All devices support it.
163 #[serde(rename = "op-line")]
164 OpLine,
165 /// `NonSemantic.Shader.DebugInfo.100`: each inlined `#[cube]` function is a separate frame.
166 /// A device without support gets `OpLine`.
167 #[serde(rename = "non-semantic")]
168 NonSemantic,
169}
170
171/// Bounds checks options.
172#[derive(Default, Clone, Copy, Debug, serde::Serialize, serde::Deserialize)]
173pub enum BoundsCheckMode {
174 #[serde(rename = "enforce")]
175 /// Always enforce bounds checks on every kernel launch.
176 Enforce,
177 #[serde(rename = "validate")]
178 /// Always enforce bounds checks on every kernel launch, and validate unchecked kernels for OOB.
179 Validate,
180 /// Enforce bounds checking on standard launches, but skip checks on
181 /// explicitly unchecked launches for better performance.
182 #[default]
183 #[serde(rename = "auto")]
184 Auto,
185}
186
187/// Log levels for compilation in `CubeCL`.
188#[derive(Default, Clone, Copy, Debug, serde::Serialize, serde::Deserialize)]
189pub enum CompilationLogLevel {
190 /// Compilation logging is disabled.
191 #[default]
192 #[serde(rename = "disabled")]
193 Disabled,
194
195 /// Basic compilation information is logged such as when kernels are compiled.
196 #[serde(rename = "basic")]
197 Basic,
198
199 /// Full compilation details are logged including source code.
200 #[serde(rename = "full")]
201 Full,
202}
203
204impl LogLevel for CompilationLogLevel {}
205
206#[cfg(test)]
207mod tests {
208 use super::*;
209
210 #[test]
211 fn debug_info_follows_the_profile_and_the_kernel() {
212 let config = CompilationConfig::default();
213 assert_eq!(
214 config.resolve_debug_info(DebugInfo::None),
215 PROFILE_DEBUG_INFO
216 );
217 assert_eq!(config.resolve_debug_info(DebugInfo::Full), DebugInfo::Full);
218 }
219
220 #[test]
221 fn debug_info_limit_only_lowers() {
222 let config = CompilationConfig {
223 debug_info: Some(DebugInfo::LineTables),
224 ..Default::default()
225 };
226 assert_eq!(
227 config.resolve_debug_info(DebugInfo::Full),
228 DebugInfo::LineTables
229 );
230
231 let config = CompilationConfig {
232 debug_info: Some(DebugInfo::Full),
233 ..Default::default()
234 };
235 assert_eq!(
236 config.resolve_debug_info(DebugInfo::None),
237 PROFILE_DEBUG_INFO
238 );
239 }
240}