Skip to main content

cubecl_runtime/execution/
process_mode.rs

1use super::{StatisticsCollector, StatisticsRecorder, StreamMode};
2use cubecl_environment::sync::{AtomicUsize, Mutex, Ordering};
3
4/// What the process does with the work it is asked to run.
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub enum ProcessMode {
7    /// Launches run. What the process does when no override is open.
8    Execute,
9    /// Launches and tune candidates queue their kernels and are discarded. The
10    /// queue compiles in one batch, at the next launch that loads a kernel. A
11    /// tune measures and decides nothing.
12    CompileOnly,
13    /// Launches compile their kernels and are discarded. A tune measures its
14    /// candidates for real.
15    CompileAndAutotune,
16}
17
18impl ProcessMode {
19    /// The process mode now.
20    pub fn current() -> Self {
21        OpenState::load().mode()
22    }
23
24    /// The mode every stream has under it, unless a
25    /// [`StreamModeOverride`](super::StreamModeOverride) sets its own.
26    pub fn stream_mode(self) -> StreamMode {
27        match self {
28            Self::Execute => StreamMode::Execute,
29            Self::CompileOnly | Self::CompileAndAutotune => StreamMode::Discard,
30        }
31    }
32}
33
34/// Applies a process mode to the whole process while it lives, counting what it
35/// triggers into a collector, on every thread and every device.
36///
37/// A build that compiles everything and then tunes with it compiled opens
38/// two, one after the other, with one collector:
39///
40/// ```no_run
41/// # fn warm_up() {}
42/// use cubecl_runtime::execution::{ProcessModeOverride, ProcessMode, StatisticsCollector};
43///
44/// let collector = StatisticsCollector::new();
45///
46/// // Gather every kernel the warm-up reaches, and compile them together...
47/// let compile = ProcessModeOverride::new(ProcessMode::CompileOnly, &collector);
48/// warm_up();
49/// core::mem::drop(compile);
50///
51/// // ...then tune with them compiled.
52/// let tune = ProcessModeOverride::new(ProcessMode::CompileAndAutotune, &collector);
53/// warm_up();
54/// core::mem::drop(tune);
55///
56/// let statistics = collector.statistics();
57/// assert_eq!(statistics.compilation.settled(), statistics.compilation.registered);
58/// ```
59///
60/// Overlapping overrides of one process mode and one collector compose, so one
61/// opened while another is still open leaves the process mode until the last of
62/// them drops. One process mode and one collector at a time: the process mode is
63/// process-wide, and the work under it counts to one collector.
64///
65/// The process mode is read where a launch is issued, with relaxed ordering, so a
66/// launch another thread had already begun issuing may still execute. What
67/// is guaranteed is the launches issued by the thread that opened it, and
68/// every launch issued after other threads observe it.
69///
70/// There is deliberately no configuration file or environment variable for
71/// it. An override left open by accident turns the rest of the process into
72/// launches that quietly do nothing and read back uninitialized memory, so
73/// its lifetime belongs to a scope in the code that wants it.
74#[derive(Debug)]
75pub struct ProcessModeOverride {
76    mode: ProcessMode,
77}
78
79impl ProcessModeOverride {
80    /// # Panics
81    ///
82    /// If an override of another process mode, or of another collector, is open.
83    pub fn new(mode: ProcessMode, collector: &StatisticsCollector) -> Self {
84        let mut active = ACTIVE.lock();
85        let open = OpenState::load();
86        match Opening::new(open, active.as_ref(), mode, collector) {
87            Opening::First => {
88                *active = Some(collector.recorder());
89                OpenState::first(mode).store();
90            }
91            Opening::Joins => open.joined().store(),
92            Opening::OtherCollector => {
93                drop(active);
94                panic!("an override cannot open while one of another collector is")
95            }
96            Opening::OtherMode => {
97                drop(active);
98                panic!(
99                    "an override of {mode:?} cannot open while one of {:?} is",
100                    open.mode()
101                )
102            }
103        }
104        Self { mode }
105    }
106
107    /// The process mode it applies.
108    pub fn mode(&self) -> ProcessMode {
109        self.mode
110    }
111
112    /// Where the work the open override triggers is tallied, or `None` when
113    /// none is open: what a [`Registration`](super::Registration) holds.
114    ///
115    /// A launch reads it where it is issued and carries it to the thread that
116    /// runs it ([`IssuedRecorder`](super::IssuedRecorder)), so its kernels
117    /// count to the override open then. Read without a lock first, then under
118    /// [`ACTIVE`]'s: a launch issued as one override closes while another
119    /// collector's opens on another thread counts to the one opening.
120    pub(crate) fn active_recorder() -> Option<StatisticsRecorder> {
121        if !OpenState::load().is_open() {
122            return None;
123        }
124        ACTIVE.lock().clone()
125    }
126}
127
128impl Drop for ProcessModeOverride {
129    fn drop(&mut self) {
130        // The last guard out closes the override, its process mode and its
131        // collector with it.
132        let mut active = ACTIVE.lock();
133        let left = OpenState::load().left();
134        if !left.is_open() {
135            *active = None;
136        }
137        left.store();
138    }
139}
140
141/// What opening an override meets.
142enum Opening {
143    /// Nothing open: it opens.
144    First,
145    /// One of its process mode and its collector: it joins.
146    Joins,
147    /// One of another collector.
148    OtherCollector,
149    /// One of its collector, of another process mode.
150    OtherMode,
151}
152
153impl Opening {
154    fn new(
155        open: OpenState,
156        active: Option<&StatisticsRecorder>,
157        mode: ProcessMode,
158        collector: &StatisticsCollector,
159    ) -> Self {
160        let Some(active) = active.filter(|_| open.is_open()) else {
161            return Self::First;
162        };
163        if !active.tallies_into(collector) {
164            Self::OtherCollector
165        } else if open.mode() != mode {
166            Self::OtherMode
167        } else {
168            Self::Joins
169        }
170    }
171}
172
173/// The open override as the process holds it: its process mode's level in the low
174/// [`LEVEL_BITS`](Self::LEVEL_BITS), and above them how many guards hold it
175/// open. Written only under [`ACTIVE`]'s lock, and read without it on every
176/// launch.
177#[derive(Debug, Clone, Copy)]
178struct OpenState(usize);
179
180/// [`OpenState`] as the process holds it.
181static OPEN: AtomicUsize = AtomicUsize::new(0);
182/// Where the open override tallies, while one is.
183static ACTIVE: Mutex<Option<StatisticsRecorder>> = Mutex::new(None);
184
185impl OpenState {
186    /// The bits that hold the process mode's level.
187    const LEVEL_BITS: u32 = 2;
188    /// Masks the state down to the process mode's level.
189    const LEVEL_MASK: usize = (1 << Self::LEVEL_BITS) - 1;
190    /// One guard, in the state's count.
191    const GUARD: usize = 1 << Self::LEVEL_BITS;
192
193    fn load() -> Self {
194        Self(OPEN.load(Ordering::Relaxed))
195    }
196
197    fn store(self) {
198        OPEN.store(self.0, Ordering::Relaxed);
199    }
200
201    /// One guard holding `mode` open. Every process mode has a level of its own,
202    /// [`Execute`](ProcessMode::Execute) included, so an override of it
203    /// is told apart from none: it tallies what a real run compiles and
204    /// tunes.
205    fn first(mode: ProcessMode) -> Self {
206        let level = match mode {
207            ProcessMode::CompileOnly => 1,
208            ProcessMode::CompileAndAutotune => 2,
209            ProcessMode::Execute => 3,
210        };
211        Self(Self::GUARD | level)
212    }
213
214    fn is_open(self) -> bool {
215        self.0 != 0
216    }
217
218    /// The open process mode; none open executes.
219    fn mode(self) -> ProcessMode {
220        match self.0 & Self::LEVEL_MASK {
221            1 => ProcessMode::CompileOnly,
222            2 => ProcessMode::CompileAndAutotune,
223            _ => ProcessMode::Execute,
224        }
225    }
226
227    /// One more guard.
228    fn joined(self) -> Self {
229        Self(self.0 + Self::GUARD)
230    }
231
232    /// One guard fewer, closed with the last.
233    fn left(self) -> Self {
234        match self.0 - Self::GUARD {
235            rest if rest < Self::GUARD => Self(0),
236            rest => Self(rest),
237        }
238    }
239}
240
241#[cfg(test)]
242mod tests {
243    use super::super::statistics::{KernelHandling, KernelOutcome, TuneOutcome, TunePick};
244    use super::super::{CompilationStatistics, KernelRegistration, TuneRegistration};
245    use super::*;
246    // `serial_test`'s macro expands to `vec!`, which a `no_std` crate has to
247    // bring in itself.
248    use alloc::vec;
249    use alloc::vec::Vec;
250
251    /// One process mode is open at a time: an override of another refuses to open
252    /// beside it.
253    #[test]
254    #[serial_test::serial]
255    #[should_panic(
256        expected = "an override of CompileOnly cannot open while one of CompileAndAutotune is"
257    )]
258    fn process_modes_do_not_overlap() {
259        let collector = StatisticsCollector::new();
260        let opened = ProcessModeOverride::new(ProcessMode::CompileAndAutotune, &collector);
261        let refused = ProcessModeOverride::new(ProcessMode::CompileOnly, &collector);
262        core::mem::drop(opened);
263        core::mem::drop(refused);
264    }
265
266    /// The work under an override counts to one collector: another's
267    /// refuses to open beside it, whatever its process mode.
268    #[test]
269    #[serial_test::serial]
270    #[should_panic(expected = "cannot open while one of another collector is")]
271    fn two_collectors_do_not_overlap() {
272        let (first, second) = (StatisticsCollector::new(), StatisticsCollector::new());
273        let opened = ProcessModeOverride::new(ProcessMode::CompileOnly, &first);
274        let refused = ProcessModeOverride::new(ProcessMode::CompileOnly, &second);
275        core::mem::drop(opened);
276        core::mem::drop(refused);
277    }
278
279    /// A refused open leaves the open override as it was.
280    #[test]
281    #[serial_test::serial]
282    fn a_refused_open_leaves_the_open_process_mode() {
283        let collector = StatisticsCollector::new();
284        let tune = ProcessModeOverride::new(ProcessMode::CompileAndAutotune, &collector);
285        let refused = std::panic::catch_unwind(|| {
286            ProcessModeOverride::new(ProcessMode::CompileOnly, &collector)
287        });
288        assert!(refused.is_err());
289        assert_eq!(ProcessMode::current(), ProcessMode::CompileAndAutotune);
290        core::mem::drop(tune);
291        assert_eq!(ProcessMode::current(), ProcessMode::Execute);
292    }
293
294    /// Overlapping overrides of one process mode and collector compose: the last
295    /// out closes it.
296    #[test]
297    #[serial_test::serial]
298    fn overrides_nest() {
299        let collector = StatisticsCollector::new();
300        {
301            let outer = ProcessModeOverride::new(ProcessMode::CompileOnly, &collector);
302            {
303                let inner = ProcessModeOverride::new(ProcessMode::CompileOnly, &collector);
304                core::mem::drop(inner);
305            }
306            assert_eq!(
307                ProcessMode::current(),
308                ProcessMode::CompileOnly,
309                "the outer is in force"
310            );
311            core::mem::drop(outer);
312        }
313        assert_eq!(ProcessMode::current(), ProcessMode::Execute);
314        assert!(ProcessModeOverride::active_recorder().is_none());
315    }
316
317    /// What an override triggers counts to its collector, over every
318    /// override of it — an `Execute` one included — and a registration made
319    /// under one settles there after it closes; what happens to settled work
320    /// counts once; a reader reads the same counts.
321    #[test]
322    #[serial_test::serial]
323    fn work_counts_to_the_collector_of_the_open_override() {
324        assert!(
325            KernelRegistration::register().is_none(),
326            "nothing counts outside an override"
327        );
328        let collector = StatisticsCollector::new();
329        let reader = collector.reader();
330
331        let compile = ProcessModeOverride::new(ProcessMode::CompileOnly, &collector);
332        let queued: Vec<KernelRegistration> = (0..4)
333            .filter_map(|_| KernelRegistration::register())
334            .collect();
335        let gathered = TuneRegistration::register().expect("an override is open");
336        core::mem::drop(compile);
337
338        // Kernels queued in one override compile in the next.
339        let tune = ProcessModeOverride::new(ProcessMode::CompileAndAutotune, &collector);
340        let mut queued = queued.into_iter();
341        let mut compiled = queued.next().unwrap().settle(KernelOutcome::Compiled);
342        compiled.record(KernelHandling::Refused);
343        compiled.record(KernelHandling::Refused);
344        let mut loaded = queued.next().unwrap().settle(KernelOutcome::Loaded);
345        loaded.record(KernelHandling::Stored);
346        let _failed = queued.next().unwrap().settle(KernelOutcome::Failed);
347        let mut measured = gathered.settle(TuneOutcome::Measured);
348        measured.record(TunePick::Persisted);
349
350        let statistics = collector.statistics();
351        assert_eq!(
352            statistics.compilation,
353            CompilationStatistics {
354                registered: 4,
355                compiled: 1,
356                loaded: 1,
357                failed: 1,
358                abandoned: 0,
359                refused: 1,
360                stored: 1,
361            },
362            "a refusal told twice counts once"
363        );
364        assert_eq!(
365            statistics.autotune.settled(),
366            statistics.autotune.registered
367        );
368        assert_eq!(statistics.autotune.persisted, 1);
369        assert_eq!(reader.statistics(), statistics);
370        core::mem::drop(tune);
371
372        let execute = ProcessModeOverride::new(ProcessMode::Execute, &collector);
373        let _measured = TuneRegistration::register()
374            .expect("an execute override counts too")
375            .settle(TuneOutcome::Measured);
376        core::mem::drop(execute);
377        assert_eq!(collector.statistics().autotune.measured, 2);
378        // The fourth kernel, never settled, is abandoned as it drops.
379        drop(queued);
380        assert_eq!(collector.statistics().compilation.abandoned, 1);
381    }
382
383    /// A registration dropped before it settled — its batch or its tune
384    /// panicked — settles as abandoned.
385    #[test]
386    #[serial_test::serial]
387    fn a_dropped_registration_settles_as_abandoned() {
388        let collector = StatisticsCollector::new();
389        let compile = ProcessModeOverride::new(ProcessMode::CompileOnly, &collector);
390        drop(KernelRegistration::register());
391        drop(TuneRegistration::register());
392        let statistics = collector.statistics();
393        assert_eq!(
394            (
395                statistics.compilation.abandoned,
396                statistics.compilation.settled()
397            ),
398            (1, 1)
399        );
400        assert_eq!(
401            (statistics.autotune.abandoned, statistics.autotune.settled()),
402            (1, 1)
403        );
404        core::mem::drop(compile);
405    }
406}