Skip to main content

cubecl_runtime/execution/
statistics.rs

1//! What the overrides opened with a [`StatisticsCollector`] triggered: the
2//! kernels obtained and the autotune keys tuned, counted to it by the code
3//! doing the work.
4
5use core::marker::PhantomData;
6use cubecl_environment::sync::{Arc, AtomicUsize, Ordering};
7
8/// A snapshot of what a collector's overrides triggered. Every count only
9/// grows, over every override and on every device.
10#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
11pub struct ExecutionStatistics {
12    /// The kernels obtained.
13    pub compilation: CompilationStatistics,
14    /// The autotune keys tuned.
15    pub autotune: AutotuneStatistics,
16}
17
18/// The kernels the launches under a collector's overrides obtained.
19#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
20pub struct CompilationStatistics {
21    /// Queued, or asked for by a launch that found them neither loaded nor
22    /// queued. A kernel tried again after failing registers again.
23    pub registered: usize,
24    /// Compiled by the backend's compiler.
25    pub compiled: usize,
26    /// Read from the compilation store instead.
27    pub loaded: usize,
28    /// Failed to compile.
29    pub failed: usize,
30    /// Dropped before it settled: the batch compiling it panicked, or the
31    /// server holding it went away.
32    pub abandoned: usize,
33    /// Compiled or read from the store, then refused by the device when it
34    /// was loaded: counted among [`compiled`](Self::compiled) or
35    /// [`loaded`](Self::loaded) too, since those are counted as the batch
36    /// obtains them, before anything is loaded.
37    pub refused: usize,
38    /// Kept in the compilation store once loaded: what the environment grew
39    /// by. Read from the store under its own key, or refused, a kernel is not
40    /// stored again; one taken from the store by its source is, under its
41    /// own key.
42    pub stored: usize,
43}
44
45impl CompilationStatistics {
46    /// The kernels done, however they ended: never more than
47    /// [`registered`](Self::registered).
48    pub fn settled(&self) -> usize {
49        self.compiled + self.loaded + self.failed + self.abandoned
50    }
51}
52
53/// The autotune keys tuned under a collector's overrides. A key with one
54/// candidate is answered, not tuned, and counts nowhere.
55#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
56pub struct AutotuneStatistics {
57    /// Gathered by a [`CompileOnly`](super::ProcessMode::CompileOnly)
58    /// pass, or reached ungathered by a tune that measures or a read of the
59    /// persistent autotune cache. A key reached inside another key's
60    /// candidates is not gathered: it registers if it is ever measured or read.
61    pub registered: usize,
62    /// Tuned by measuring their candidates.
63    pub measured: usize,
64    /// Read from the persistent autotune cache instead of measured.
65    pub loaded: usize,
66    /// Every candidate failed: the pick was made unmeasured.
67    pub failed: usize,
68    /// Dropped before it settled: a candidate panicked, the device was lost
69    /// while it measured, or an environment switch dropped the key it was
70    /// gathered for.
71    pub abandoned: usize,
72    /// Measured, and kept in the persistent autotune cache: what the
73    /// environment grew by. An unmeasured pick is never kept.
74    pub persisted: usize,
75}
76
77impl AutotuneStatistics {
78    /// The keys done, however they ended: never more than
79    /// [`registered`](Self::registered).
80    pub fn settled(&self) -> usize {
81        self.measured + self.loaded + self.failed + self.abandoned
82    }
83}
84
85/// Collects what the [overrides](super::ProcessModeOverride) opened with it
86/// triggered.
87///
88/// Not `Clone`: whoever holds it opens overrides that count into it. A
89/// reader on another thread holds a [`StatisticsReader`].
90#[derive(Debug, Default)]
91pub struct StatisticsCollector {
92    tallies: Arc<CollectorTallies>,
93}
94
95/// Reads a [`StatisticsCollector`]'s statistics from any thread, and opens
96/// nothing.
97#[derive(Debug, Clone)]
98pub struct StatisticsReader {
99    tallies: Arc<CollectorTallies>,
100}
101
102/// Where the work under an override is tallied: what a [`Registration`]
103/// holds, and the open override hands out. A clone tallies into the same
104/// collector, which is how a kernel queued under one override is counted to
105/// it when its batch compiles.
106#[derive(Debug, Clone)]
107pub(crate) struct StatisticsRecorder {
108    tallies: Arc<CollectorTallies>,
109}
110
111impl StatisticsCollector {
112    /// A collector nothing has counted into yet.
113    pub fn new() -> Self {
114        Self::default()
115    }
116
117    /// What reads it from another thread.
118    pub fn reader(&self) -> StatisticsReader {
119        StatisticsReader {
120            tallies: self.tallies.clone(),
121        }
122    }
123
124    /// What its overrides triggered so far.
125    pub fn statistics(&self) -> ExecutionStatistics {
126        self.tallies.statistics()
127    }
128
129    /// Where an override opened with it tallies.
130    pub(crate) fn recorder(&self) -> StatisticsRecorder {
131        StatisticsRecorder {
132            tallies: self.tallies.clone(),
133        }
134    }
135}
136
137impl StatisticsReader {
138    /// What the collector's overrides triggered so far.
139    pub fn statistics(&self) -> ExecutionStatistics {
140        self.tallies.statistics()
141    }
142}
143
144impl StatisticsRecorder {
145    /// Whether it tallies into `collector`.
146    pub(crate) fn tallies_into(&self, collector: &StatisticsCollector) -> bool {
147        Arc::ptr_eq(&self.tallies, &collector.tallies)
148    }
149}
150
151/// How a registered kernel ended.
152#[doc(hidden)]
153#[derive(Debug, Clone, Copy, PartialEq, Eq)]
154pub enum KernelOutcome {
155    /// Compiled by the backend's compiler.
156    Compiled,
157    /// Read from the compilation store.
158    Loaded,
159    /// Failed to compile.
160    Failed,
161    /// Dropped before it settled.
162    Abandoned,
163}
164
165/// What the server did with a settled kernel: the device refused it, or the
166/// compilation store kept it.
167#[doc(hidden)]
168#[derive(Debug, Clone, Copy, PartialEq, Eq)]
169pub enum KernelHandling {
170    /// The device refused it.
171    Refused,
172    /// The compilation store kept it.
173    Stored,
174}
175
176/// How a registered autotune key ended.
177#[derive(Debug, Clone, Copy, PartialEq, Eq)]
178pub(crate) enum TuneOutcome {
179    /// Tuned by measuring its candidates.
180    Measured,
181    /// Answered from the persistent autotune cache instead.
182    Loaded,
183    /// Every candidate failed: the pick was made unmeasured.
184    Failed,
185    /// Dropped before it settled.
186    Abandoned,
187}
188
189/// What became of a settled key's pick.
190#[derive(Debug, Clone, Copy, PartialEq, Eq)]
191pub(crate) enum TunePick {
192    /// The persistent autotune cache kept it.
193    Persisted,
194}
195
196/// One kernel registered with the collector of the override open when it
197/// was.
198#[doc(hidden)]
199pub type KernelRegistration = Registration<KernelOutcome>;
200/// A registered kernel once its outcome is counted.
201#[doc(hidden)]
202pub type SettledKernel = Settled<KernelOutcome>;
203/// One autotune key registered with the collector of the override open when
204/// it was.
205pub(crate) type TuneRegistration = Registration<TuneOutcome>;
206
207/// One item of work registered with the collector of the override open when
208/// it was, which it is counted to once it [settles](Self::settle) —
209/// wherever and whenever that is, whether or not that override is still
210/// open. Dropped before it settled — its batch or its tune panicked, or the
211/// key it holds was dropped — it settles as abandoned.
212///
213/// What holds it decides when that is: a kernel queued and never loaded, or
214/// a key gathered and never measured again, stays registered until a later
215/// pass reaches it, its environment switches or its server goes away.
216///
217/// Public for the kernel loader, which lives in `cubecl-server`; counting is
218/// not for callers, which read their own [`StatisticsCollector`].
219#[doc(hidden)]
220#[must_use]
221pub struct Registration<O: Outcome> {
222    /// Taken as it settles, so a drop knows whether it did.
223    recorder: Option<StatisticsRecorder>,
224    outcome: PhantomData<O>,
225}
226
227/// A [`Registration`] whose outcome is counted: what alone can count what
228/// happens to the work afterwards, each thing once.
229#[doc(hidden)]
230#[derive(Debug)]
231pub struct Settled<O: Outcome> {
232    recorder: StatisticsRecorder,
233    /// The afterwards already counted, one bit each.
234    recorded: u8,
235    outcome: PhantomData<O>,
236}
237
238impl<O: Outcome> Registration<O> {
239    /// Register one item with the collector of the override open where the
240    /// launch running now was issued — or, outside a launch, of the one open
241    /// now — if one is.
242    pub fn register() -> Option<Self> {
243        let recorder = super::IssuedRecorder::running()
244            .unwrap_or_else(super::ProcessModeOverride::active_recorder)?;
245        O::tally(&recorder.tallies).register();
246        Some(Self {
247            recorder: Some(recorder),
248            outcome: PhantomData,
249        })
250    }
251
252    /// Count how it ended.
253    pub fn settle(mut self, outcome: O) -> Settled<O> {
254        let recorder = self.recorder.take().expect("settled once, by value");
255        O::tally(&recorder.tallies).settle(outcome.index());
256        Settled {
257            recorder,
258            recorded: 0,
259            outcome: PhantomData,
260        }
261    }
262}
263
264impl<O: Outcome> Drop for Registration<O> {
265    fn drop(&mut self) {
266        if let Some(recorder) = self.recorder.take() {
267            O::tally(&recorder.tallies).settle(O::ABANDONED.index());
268        }
269    }
270}
271
272impl<O: Outcome> core::fmt::Debug for Registration<O> {
273    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
274        f.debug_struct("Registration")
275            .field("settled", &self.recorder.is_none())
276            .finish()
277    }
278}
279
280impl<O: Outcome> Settled<O> {
281    /// Count `later`, once however often it is told.
282    pub fn record(&mut self, later: O::Later) {
283        let index = O::later_index(later);
284        let bit = 1 << index;
285        if self.recorded & bit == 0 {
286            self.recorded |= bit;
287            O::tally(&self.recorder.tallies).record(index);
288        }
289    }
290}
291
292/// A collector's tallies, shared by the collector, its readers and its
293/// recorders.
294#[derive(Debug, Default)]
295pub struct CollectorTallies {
296    kernels: Tally,
297    tunes: Tally,
298}
299
300impl CollectorTallies {
301    fn statistics(&self) -> ExecutionStatistics {
302        let kernels = self.kernels.read();
303        let tunes = self.tunes.read();
304        ExecutionStatistics {
305            compilation: CompilationStatistics {
306                registered: kernels.registered,
307                compiled: kernels.settled[KernelOutcome::Compiled.index()],
308                loaded: kernels.settled[KernelOutcome::Loaded.index()],
309                failed: kernels.settled[KernelOutcome::Failed.index()],
310                abandoned: kernels.settled[KernelOutcome::Abandoned.index()],
311                refused: kernels.later[KernelOutcome::later_index(KernelHandling::Refused)],
312                stored: kernels.later[KernelOutcome::later_index(KernelHandling::Stored)],
313            },
314            autotune: AutotuneStatistics {
315                registered: tunes.registered,
316                measured: tunes.settled[TuneOutcome::Measured.index()],
317                loaded: tunes.settled[TuneOutcome::Loaded.index()],
318                failed: tunes.settled[TuneOutcome::Failed.index()],
319                abandoned: tunes.settled[TuneOutcome::Abandoned.index()],
320                persisted: tunes.later[TuneOutcome::later_index(TunePick::Persisted)],
321            },
322        }
323    }
324}
325
326/// Room for every outcome a kind of work has.
327const OUTCOMES: usize = 4;
328/// Room for everything that can happen to a kind of work once it settled.
329const LATER: usize = 2;
330
331// Every kind of work fits its tally: a variant added past the room fails
332// here rather than out of bounds on a count.
333const _: () = assert!(KernelOutcome::OUTCOMES <= OUTCOMES && KernelOutcome::LATER <= LATER);
334const _: () = assert!(TuneOutcome::OUTCOMES <= OUTCOMES && TuneOutcome::LATER <= LATER);
335
336/// Items registered, how many ended in each outcome, and how many of those
337/// something happened to afterwards: the one counting mechanism every kind
338/// of work shares.
339///
340/// An item is registered before its outcome, and its outcome before
341/// anything after it; a read loads them the other way round, so a read
342/// never sees more settled than registered, nor more afterwards than
343/// settled.
344#[derive(Debug, Default)]
345pub struct Tally {
346    registered: AtomicUsize,
347    settled: [AtomicUsize; OUTCOMES],
348    later: [AtomicUsize; LATER],
349}
350
351/// A [`Tally`] as read at one moment.
352struct TallyRead {
353    registered: usize,
354    settled: [usize; OUTCOMES],
355    later: [usize; LATER],
356}
357
358impl Tally {
359    fn register(&self) {
360        self.registered.fetch_add(1, Ordering::Release);
361    }
362
363    fn settle(&self, outcome: usize) {
364        self.settled[outcome].fetch_add(1, Ordering::Release);
365    }
366
367    fn record(&self, later: usize) {
368        self.later[later].fetch_add(1, Ordering::Release);
369    }
370
371    fn read(&self) -> TallyRead {
372        let later = core::array::from_fn(|index| self.later[index].load(Ordering::Acquire));
373        let settled = core::array::from_fn(|index| self.settled[index].load(Ordering::Acquire));
374        TallyRead {
375            registered: self.registered.load(Ordering::Acquire),
376            settled,
377            later,
378        }
379    }
380}
381
382/// A kind of work a collector counts: how one item of it can end, and what
383/// can happen to it after.
384///
385/// Sealed: it, [`CollectorTallies`] and [`Tally`] are `pub` only so the
386/// public [`Registration`] can name them, in a module nothing outside the
387/// crate reaches, so no kind of work is added from outside — the tallies it
388/// picks are the collector's own.
389#[doc(hidden)]
390pub trait Outcome: Copy + 'static {
391    /// What can happen to an item once it settled.
392    type Later: Copy;
393    /// How many outcomes it has, which a [`Tally`] has room for.
394    const OUTCOMES: usize;
395    /// How many things can happen to it once settled.
396    const LATER: usize;
397    /// The outcome of an item dropped before it settled.
398    const ABANDONED: Self;
399
400    /// Its slot in a [`Tally`]'s outcomes.
401    fn index(self) -> usize;
402    /// `later`'s slot in a [`Tally`]'s afterwards.
403    fn later_index(later: Self::Later) -> usize;
404    /// The tally that counts this kind of work.
405    fn tally(tallies: &CollectorTallies) -> &Tally;
406}
407
408impl Outcome for KernelOutcome {
409    type Later = KernelHandling;
410    const OUTCOMES: usize = 4;
411    const LATER: usize = 2;
412    const ABANDONED: Self = Self::Abandoned;
413
414    fn index(self) -> usize {
415        match self {
416            Self::Compiled => 0,
417            Self::Loaded => 1,
418            Self::Failed => 2,
419            Self::Abandoned => 3,
420        }
421    }
422
423    fn later_index(later: KernelHandling) -> usize {
424        match later {
425            KernelHandling::Refused => 0,
426            KernelHandling::Stored => 1,
427        }
428    }
429
430    fn tally(tallies: &CollectorTallies) -> &Tally {
431        &tallies.kernels
432    }
433}
434
435impl Outcome for TuneOutcome {
436    type Later = TunePick;
437    const OUTCOMES: usize = 4;
438    const LATER: usize = 1;
439    const ABANDONED: Self = Self::Abandoned;
440
441    fn index(self) -> usize {
442        match self {
443            Self::Measured => 0,
444            Self::Failed => 1,
445            Self::Abandoned => 2,
446            Self::Loaded => 3,
447        }
448    }
449
450    fn later_index(later: TunePick) -> usize {
451        match later {
452            TunePick::Persisted => 0,
453        }
454    }
455
456    fn tally(tallies: &CollectorTallies) -> &Tally {
457        &tallies.tunes
458    }
459}