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}