Skip to main content

cubecl_monitoring/
utilization.rs

1use alloc::string::String;
2use core::fmt;
3
4use cubecl_ir::PciVendor;
5
6/// How busy a device is, as its counter read it.
7#[derive(Clone, Copy, Debug, PartialEq)]
8#[non_exhaustive]
9pub struct DeviceUtilization {
10    /// The percent of the counter's last period the device spent running work, from 0 to 100.
11    pub busy_percent: f32,
12    /// The percent of the counter's last period its memory spent reading or writing, from 0
13    /// to 100: how much of its bandwidth was in use. `None` when the counter keeps no such
14    /// figure.
15    pub memory_busy_percent: Option<f32>,
16}
17
18impl DeviceUtilization {
19    /// Holds a reading outside 0 to 100 to the nearer bound.
20    pub fn new(busy_percent: f32) -> Self {
21        Self {
22            busy_percent: busy_percent.clamp(0.0, 100.0),
23            memory_busy_percent: None,
24        }
25    }
26
27    /// Add the memory's busy percent, held from 0 to 100.
28    pub fn with_memory_busy_percent(mut self, memory_busy_percent: f32) -> Self {
29        self.memory_busy_percent = Some(memory_busy_percent.clamp(0.0, 100.0));
30        self
31    }
32}
33
34/// The temperature of the device at the time of reading, from its sensor.
35#[derive(Clone, Copy, Debug, PartialEq)]
36#[non_exhaustive]
37pub struct DeviceTemperature {
38    /// The temperature in degrees Celsius.
39    pub celsius: f32,
40}
41
42impl DeviceTemperature {
43    /// A reading of `celsius` degrees.
44    pub fn new(celsius: f32) -> Self {
45        Self { celsius }
46    }
47}
48
49/// The power usage of the device, read by its sensor.
50#[derive(Clone, Copy, Debug, PartialEq)]
51#[non_exhaustive]
52pub struct DevicePower {
53    /// The card's power draw, in watts, averaged over the driver's last sample window.
54    pub watts: f32,
55}
56
57impl DevicePower {
58    /// A reading of `watts`.
59    pub fn new(watts: f32) -> Self {
60        Self { watts }
61    }
62}
63
64/// Why a device's utilization, temperature or power could not be read: each comes from the
65/// counter its card's driver or its platform keeps.
66#[derive(Clone, Debug, PartialEq, Eq)]
67#[non_exhaustive]
68pub enum UtilizationUnavailable {
69    /// This build does not link the device's runtime, which is what says which card the device
70    /// is.
71    RuntimeNotLinked,
72    /// The device's runtime reads no counter for its devices.
73    RuntimeReadsNoCounter,
74    /// The runtime reports no card behind the device: a software adapter.
75    NoCard,
76    /// This platform keeps no counter read here for a card of this vendor, or of no vendor the
77    /// runtime named.
78    NoCounterForCard(Option<PciVendor>),
79    /// The card's counter is found by an address the runtime did not report: the PCI address on
80    /// Linux, the LUID on Windows, and on macOS the registry entry id, where the machine has more
81    /// than one GPU.
82    CardAddressNotReported,
83    /// The driver's library would not load: no driver installed, or none on the loader's path.
84    /// Holds the loader's message.
85    DriverLibraryNotFound(String),
86    /// The counter measures the time between two readings, and this was the device's first, or
87    /// the first since the counter restarted.
88    NoPreviousReading,
89    /// The counter was reached and gave no reading. Holds the driver's, the platform's or the
90    /// filesystem's message.
91    QueryFailed(String),
92    /// The counter that answers for the card keeps no such reading.
93    NoSuchReading,
94}
95
96impl fmt::Display for UtilizationUnavailable {
97    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
98        match self {
99            Self::RuntimeNotLinked => f.write_str("this build does not link the device's runtime"),
100            Self::RuntimeReadsNoCounter => {
101                f.write_str("the device's runtime reads no utilization counter")
102            }
103            Self::NoCard => f.write_str("the device is no card: a software adapter"),
104            Self::NoCounterForCard(Some(vendor)) => {
105                write!(f, "no counter on this platform for a {vendor} card")
106            }
107            Self::NoCounterForCard(None) => {
108                f.write_str("no counter on this platform for a card of no known vendor")
109            }
110            Self::CardAddressNotReported => {
111                f.write_str("the runtime did not report the address the card's counter is found by")
112            }
113            Self::DriverLibraryNotFound(message) => {
114                write!(f, "the driver's library would not load: {message}")
115            }
116            Self::NoPreviousReading => {
117                f.write_str("the counter measures between two readings, and this was the first")
118            }
119            Self::QueryFailed(message) => write!(f, "the counter gave no reading: {message}"),
120            Self::NoSuchReading => f.write_str("the card's counter keeps no such reading"),
121        }
122    }
123}
124
125impl core::error::Error for UtilizationUnavailable {}
126
127#[cfg(test)]
128mod tests {
129    use super::*;
130
131    #[test]
132    fn a_reading_from_0_to_100_is_kept_as_it_is() {
133        for busy_percent in [0.0, 37.5, 100.0] {
134            assert_eq!(
135                DeviceUtilization::new(busy_percent).busy_percent,
136                busy_percent
137            );
138        }
139    }
140
141    #[test]
142    fn the_memory_reading_is_held_to_0_to_100_and_absent_unless_read() {
143        assert_eq!(DeviceUtilization::new(50.0).memory_busy_percent, None);
144        assert_eq!(
145            DeviceUtilization::new(50.0)
146                .with_memory_busy_percent(140.0)
147                .memory_busy_percent,
148            Some(100.0)
149        );
150    }
151
152    #[test]
153    fn a_reading_above_100_is_held_to_100() {
154        for busy_percent in [100.5, f32::MAX] {
155            assert_eq!(DeviceUtilization::new(busy_percent).busy_percent, 100.0);
156        }
157    }
158}