Skip to main content

cubecl_runtime/device/
hardware_identity.rs

1use alloc::format;
2use alloc::string::String;
3use core::fmt::{Display, Formatter};
4use cubecl_ir::{DeviceIdentity, DeviceProperties};
5
6/// Which hardware a device is: its [`DeviceIdentity`] without the card it sits in, so what is
7/// measured on one device holds for every device of the same hardware.
8///
9/// Its memory is left out too, since a cgroup limit or ECC moves it without changing which kernel
10/// wins.
11#[derive(Clone, Debug, PartialEq, Eq, Hash)]
12pub struct HardwareIdentity {
13    runtime: String,
14    name: String,
15    fingerprint: String,
16    parallelism: u32,
17}
18
19impl HardwareIdentity {
20    /// The hardware of a device of `runtime` reporting `properties`. Its core or multiprocessor
21    /// count tells apart hardware that shares a name.
22    pub fn new(runtime: &str, properties: &DeviceProperties) -> Self {
23        // Every field named, so a field added to `DeviceIdentity` is placed here or left out on
24        // purpose.
25        let DeviceIdentity {
26            name,
27            fingerprint,
28            physical: _,
29        } = &properties.identity;
30        let hardware = &properties.hardware;
31        Self {
32            runtime: segment(runtime),
33            name: segment(name),
34            fingerprint: segment(fingerprint),
35            parallelism: hardware
36                .num_cpu_cores
37                .or(hardware.num_streaming_multiprocessors)
38                .unwrap_or(0),
39        }
40    }
41
42    /// This identity written with `memory` installed, laid out as the throughput cache names its tables:
43    /// another layout would orphan every peak measured.
44    pub(crate) fn name_with_memory(&self, memory: u64) -> String {
45        format!(
46            "{}_{}_{}_mem{memory}_par{}",
47            self.runtime, self.fingerprint, self.name, self.parallelism
48        )
49    }
50}
51
52/// `text` made fit for one path segment, since a runtime names itself `wgpu<spirv>`. Done at
53/// construction, so two identities are equal exactly when they name the same table.
54fn segment(text: &str) -> String {
55    text.replace(|c: char| !c.is_ascii_alphanumeric(), "-")
56}
57
58impl Display for HardwareIdentity {
59    fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result {
60        write!(
61            f,
62            "{}_{}_{}_par{}",
63            self.runtime, self.fingerprint, self.name, self.parallelism
64        )
65    }
66}
67
68#[cfg(test)]
69mod tests {
70    use super::*;
71    use alloc::string::ToString;
72    use alloc::vec;
73    use cubecl_common::profile::TimingMethod;
74    use cubecl_ir::{HardwareProperties, MemoryDeviceProperties, VectorSize, features::Features};
75
76    #[test]
77    fn a_gpu_is_identified_by_its_runtime_target_name_and_multiprocessors() {
78        assert_eq!(
79            HardwareIdentity::new("cuda", &l4()),
80            HardwareIdentity {
81                runtime: "cuda".to_string(),
82                name: "NVIDIA-L4".to_string(),
83                fingerprint: "ptx-sm89".to_string(),
84                parallelism: 58,
85            }
86        );
87    }
88
89    #[test]
90    fn cores_count_ahead_of_multiprocessors() {
91        let mut properties = l4();
92        properties.hardware.num_cpu_cores = Some(8);
93
94        assert_eq!(HardwareIdentity::new("cuda", &properties).parallelism, 8);
95    }
96
97    #[test]
98    fn names_that_differ_only_in_punctuation_identify_the_same_hardware() {
99        let named = |name: &str| {
100            let mut properties = l4();
101            properties.identity.name = name.to_string();
102            HardwareIdentity::new("wgpu<spirv>", &properties)
103        };
104
105        assert_eq!(
106            named("Intel(R) Arc(tm) A770"),
107            named("Intel-R- Arc-tm- A770")
108        );
109    }
110
111    #[test]
112    fn an_identity_is_one_path_segment() {
113        let mut properties = l4();
114        properties.identity.name = "Intel(R) Arc(tm) B390 (PTL)".to_string();
115        properties.identity.fingerprint = "spirv_32902_45184".to_string();
116        let written = HardwareIdentity::new("wgpu<spirv>", &properties).to_string();
117
118        assert!(
119            written
120                .chars()
121                .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_'),
122            "{written}"
123        );
124    }
125
126    /// Throughput tables are named this way: another layout probes every peak again.
127    #[test]
128    fn the_name_with_memory_keeps_the_throughput_layout() {
129        assert_eq!(
130            HardwareIdentity::new("cuda", &l4()).name_with_memory(5 << 30),
131            "cuda_ptx-sm89_NVIDIA-L4_mem5368709120_par58"
132        );
133    }
134
135    #[test]
136    fn every_field_tells_hardware_apart() {
137        let identity = rtx_2060();
138        let others = vec![
139            HardwareIdentity {
140                runtime: "wgpu-spirv-".to_string(),
141                ..rtx_2060()
142            },
143            HardwareIdentity {
144                name: "NVIDIA-GeForce-GTX-1660-SUPER".to_string(),
145                ..rtx_2060()
146            },
147            HardwareIdentity {
148                fingerprint: "ptx-sm86".to_string(),
149                ..rtx_2060()
150            },
151            HardwareIdentity {
152                parallelism: 4,
153                ..rtx_2060()
154            },
155        ];
156
157        for other in others {
158            assert_ne!(identity.to_string(), other.to_string(), "{other:?}");
159        }
160    }
161
162    fn l4() -> DeviceProperties {
163        let hardware = HardwareProperties {
164            load_width: 128,
165            vector_register_count: None,
166            plane_size_min: 32,
167            plane_size_max: 32,
168            max_bindings: 32,
169            max_shared_memory_size: 101376,
170            max_cube_count: (u32::MAX, u16::MAX as u32, u16::MAX as u32),
171            max_units_per_cube: 1024,
172            max_cube_dim: (1024, 1024, 64),
173            num_streaming_multiprocessors: Some(58),
174            num_tensor_cores: None,
175            min_tensor_cores_dim: None,
176            num_cpu_cores: None,
177            last_level_cache_size: None,
178            max_vector_size: VectorSize::MAX,
179            cube_mma_reserved_shared_memory: 0,
180        };
181        DeviceProperties::new(
182            Features::default(),
183            MemoryDeviceProperties::new(5 << 30, 256),
184            hardware,
185            TimingMethod::Device,
186            DeviceIdentity {
187                name: "NVIDIA L4".to_string(),
188                fingerprint: "ptx_sm89".to_string(),
189                physical: None,
190            },
191        )
192    }
193
194    fn rtx_2060() -> HardwareIdentity {
195        HardwareIdentity {
196            runtime: "cuda".to_string(),
197            name: "NVIDIA-GeForce-RTX-2060".to_string(),
198            fingerprint: "ptx-sm75".to_string(),
199            parallelism: 30,
200        }
201    }
202}