use super::{StatisticsCollector, StatisticsRecorder, StreamMode};
use cubecl_environment::sync::{AtomicUsize, Mutex, Ordering};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ProcessMode {
Execute,
CompileOnly,
CompileAndAutotune,
}
impl ProcessMode {
pub fn current() -> Self {
OpenState::load().mode()
}
pub fn stream_mode(self) -> StreamMode {
match self {
Self::Execute => StreamMode::Execute,
Self::CompileOnly | Self::CompileAndAutotune => StreamMode::Discard,
}
}
}
#[derive(Debug)]
pub struct ProcessModeOverride {
mode: ProcessMode,
}
impl ProcessModeOverride {
pub fn new(mode: ProcessMode, collector: &StatisticsCollector) -> Self {
let mut active = ACTIVE.lock();
let open = OpenState::load();
match Opening::new(open, active.as_ref(), mode, collector) {
Opening::First => {
*active = Some(collector.recorder());
OpenState::first(mode).store();
}
Opening::Joins => open.joined().store(),
Opening::OtherCollector => {
drop(active);
panic!("an override cannot open while one of another collector is")
}
Opening::OtherMode => {
drop(active);
panic!(
"an override of {mode:?} cannot open while one of {:?} is",
open.mode()
)
}
}
Self { mode }
}
pub fn mode(&self) -> ProcessMode {
self.mode
}
pub(crate) fn active_recorder() -> Option<StatisticsRecorder> {
if !OpenState::load().is_open() {
return None;
}
ACTIVE.lock().clone()
}
}
impl Drop for ProcessModeOverride {
fn drop(&mut self) {
let mut active = ACTIVE.lock();
let left = OpenState::load().left();
if !left.is_open() {
*active = None;
}
left.store();
}
}
enum Opening {
First,
Joins,
OtherCollector,
OtherMode,
}
impl Opening {
fn new(
open: OpenState,
active: Option<&StatisticsRecorder>,
mode: ProcessMode,
collector: &StatisticsCollector,
) -> Self {
let Some(active) = active.filter(|_| open.is_open()) else {
return Self::First;
};
if !active.tallies_into(collector) {
Self::OtherCollector
} else if open.mode() != mode {
Self::OtherMode
} else {
Self::Joins
}
}
}
#[derive(Debug, Clone, Copy)]
struct OpenState(usize);
static OPEN: AtomicUsize = AtomicUsize::new(0);
static ACTIVE: Mutex<Option<StatisticsRecorder>> = Mutex::new(None);
impl OpenState {
const LEVEL_BITS: u32 = 2;
const LEVEL_MASK: usize = (1 << Self::LEVEL_BITS) - 1;
const GUARD: usize = 1 << Self::LEVEL_BITS;
fn load() -> Self {
Self(OPEN.load(Ordering::Relaxed))
}
fn store(self) {
OPEN.store(self.0, Ordering::Relaxed);
}
fn first(mode: ProcessMode) -> Self {
let level = match mode {
ProcessMode::CompileOnly => 1,
ProcessMode::CompileAndAutotune => 2,
ProcessMode::Execute => 3,
};
Self(Self::GUARD | level)
}
fn is_open(self) -> bool {
self.0 != 0
}
fn mode(self) -> ProcessMode {
match self.0 & Self::LEVEL_MASK {
1 => ProcessMode::CompileOnly,
2 => ProcessMode::CompileAndAutotune,
_ => ProcessMode::Execute,
}
}
fn joined(self) -> Self {
Self(self.0 + Self::GUARD)
}
fn left(self) -> Self {
match self.0 - Self::GUARD {
rest if rest < Self::GUARD => Self(0),
rest => Self(rest),
}
}
}
#[cfg(test)]
mod tests {
use super::super::statistics::{KernelHandling, KernelOutcome, TuneOutcome, TunePick};
use super::super::{CompilationStatistics, KernelRegistration, TuneRegistration};
use super::*;
use alloc::vec;
use alloc::vec::Vec;
#[test]
#[serial_test::serial]
#[should_panic(
expected = "an override of CompileOnly cannot open while one of CompileAndAutotune is"
)]
fn process_modes_do_not_overlap() {
let collector = StatisticsCollector::new();
let opened = ProcessModeOverride::new(ProcessMode::CompileAndAutotune, &collector);
let refused = ProcessModeOverride::new(ProcessMode::CompileOnly, &collector);
core::mem::drop(opened);
core::mem::drop(refused);
}
#[test]
#[serial_test::serial]
#[should_panic(expected = "cannot open while one of another collector is")]
fn two_collectors_do_not_overlap() {
let (first, second) = (StatisticsCollector::new(), StatisticsCollector::new());
let opened = ProcessModeOverride::new(ProcessMode::CompileOnly, &first);
let refused = ProcessModeOverride::new(ProcessMode::CompileOnly, &second);
core::mem::drop(opened);
core::mem::drop(refused);
}
#[test]
#[serial_test::serial]
fn a_refused_open_leaves_the_open_process_mode() {
let collector = StatisticsCollector::new();
let tune = ProcessModeOverride::new(ProcessMode::CompileAndAutotune, &collector);
let refused = std::panic::catch_unwind(|| {
ProcessModeOverride::new(ProcessMode::CompileOnly, &collector)
});
assert!(refused.is_err());
assert_eq!(ProcessMode::current(), ProcessMode::CompileAndAutotune);
core::mem::drop(tune);
assert_eq!(ProcessMode::current(), ProcessMode::Execute);
}
#[test]
#[serial_test::serial]
fn overrides_nest() {
let collector = StatisticsCollector::new();
{
let outer = ProcessModeOverride::new(ProcessMode::CompileOnly, &collector);
{
let inner = ProcessModeOverride::new(ProcessMode::CompileOnly, &collector);
core::mem::drop(inner);
}
assert_eq!(
ProcessMode::current(),
ProcessMode::CompileOnly,
"the outer is in force"
);
core::mem::drop(outer);
}
assert_eq!(ProcessMode::current(), ProcessMode::Execute);
assert!(ProcessModeOverride::active_recorder().is_none());
}
#[test]
#[serial_test::serial]
fn work_counts_to_the_collector_of_the_open_override() {
assert!(
KernelRegistration::register().is_none(),
"nothing counts outside an override"
);
let collector = StatisticsCollector::new();
let reader = collector.reader();
let compile = ProcessModeOverride::new(ProcessMode::CompileOnly, &collector);
let queued: Vec<KernelRegistration> = (0..4)
.filter_map(|_| KernelRegistration::register())
.collect();
let gathered = TuneRegistration::register().expect("an override is open");
core::mem::drop(compile);
let tune = ProcessModeOverride::new(ProcessMode::CompileAndAutotune, &collector);
let mut queued = queued.into_iter();
let mut compiled = queued.next().unwrap().settle(KernelOutcome::Compiled);
compiled.record(KernelHandling::Refused);
compiled.record(KernelHandling::Refused);
let mut loaded = queued.next().unwrap().settle(KernelOutcome::Loaded);
loaded.record(KernelHandling::Stored);
let _failed = queued.next().unwrap().settle(KernelOutcome::Failed);
let mut measured = gathered.settle(TuneOutcome::Measured);
measured.record(TunePick::Persisted);
let statistics = collector.statistics();
assert_eq!(
statistics.compilation,
CompilationStatistics {
registered: 4,
compiled: 1,
loaded: 1,
failed: 1,
abandoned: 0,
refused: 1,
stored: 1,
},
"a refusal told twice counts once"
);
assert_eq!(
statistics.autotune.settled(),
statistics.autotune.registered
);
assert_eq!(statistics.autotune.persisted, 1);
assert_eq!(reader.statistics(), statistics);
core::mem::drop(tune);
let execute = ProcessModeOverride::new(ProcessMode::Execute, &collector);
let _measured = TuneRegistration::register()
.expect("an execute override counts too")
.settle(TuneOutcome::Measured);
core::mem::drop(execute);
assert_eq!(collector.statistics().autotune.measured, 2);
drop(queued);
assert_eq!(collector.statistics().compilation.abandoned, 1);
}
#[test]
#[serial_test::serial]
fn a_dropped_registration_settles_as_abandoned() {
let collector = StatisticsCollector::new();
let compile = ProcessModeOverride::new(ProcessMode::CompileOnly, &collector);
drop(KernelRegistration::register());
drop(TuneRegistration::register());
let statistics = collector.statistics();
assert_eq!(
(
statistics.compilation.abandoned,
statistics.compilation.settled()
),
(1, 1)
);
assert_eq!(
(statistics.autotune.abandoned, statistics.autotune.settled()),
(1, 1)
);
core::mem::drop(compile);
}
}