1use std::fmt;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub struct Capabilities {
26 pub supports_fp16: bool,
28 pub supports_bf16: bool,
30 pub supports_fp8: bool,
32 pub tensor_cores: bool,
34 pub peer_access: bool,
36 pub unified_memory: bool,
38 pub cluster_launch: bool,
40 pub async_copy: bool,
42 pub max_threads_per_block: u32,
44 pub max_shared_mem_per_block: u32,
46 pub warp_size: u32,
48}
49
50impl Capabilities {
51 #[must_use]
56 pub const fn cpu() -> Self {
57 Self {
58 supports_fp16: false,
59 supports_bf16: false,
60 supports_fp8: false,
61 tensor_cores: false,
62 peer_access: false,
63 unified_memory: true, cluster_launch: false,
65 async_copy: false,
66 max_threads_per_block: 1024,
67 max_shared_mem_per_block: 48 * 1024,
68 warp_size: 1,
69 }
70 }
71
72 #[must_use]
74 pub const fn supports_mixed_precision(&self) -> bool {
75 self.supports_fp16 || self.supports_bf16 || self.supports_fp8
76 }
77
78 #[must_use]
82 pub const fn can_use_tensor_cores(&self, m: usize, n: usize, k: usize) -> bool {
83 self.tensor_cores && m % 16 == 0 && n % 16 == 0 && k % 16 == 0
84 }
85}
86
87impl Default for Capabilities {
88 fn default() -> Self {
89 Self::cpu()
90 }
91}
92
93impl fmt::Display for Capabilities {
94 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
95 write!(
96 f,
97 "fp16={} bf16={} fp8={} tensor_cores={} peer={} unified={} cluster={} async_copy={} \
98 max_threads/block={} max_smem/block={}B warp={}",
99 self.supports_fp16,
100 self.supports_bf16,
101 self.supports_fp8,
102 self.tensor_cores,
103 self.peer_access,
104 self.unified_memory,
105 self.cluster_launch,
106 self.async_copy,
107 self.max_threads_per_block,
108 self.max_shared_mem_per_block,
109 self.warp_size,
110 )
111 }
112}
113
114#[derive(Debug, Clone, Copy, PartialEq, Eq)]
119pub struct TileShape {
120 pub tile_m: usize,
122 pub tile_n: usize,
124 pub tile_k: usize,
126}
127
128impl TileShape {
129 #[must_use]
131 pub const fn new(tile_m: usize, tile_n: usize, tile_k: usize) -> Self {
132 Self {
133 tile_m,
134 tile_n,
135 tile_k,
136 }
137 }
138
139 #[must_use]
141 pub const fn output_elems(&self) -> usize {
142 self.tile_m * self.tile_n
143 }
144}
145
146impl fmt::Display for TileShape {
147 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
148 write!(f, "{}x{}x{}", self.tile_m, self.tile_n, self.tile_k)
149 }
150}
151
152#[must_use]
160pub fn default_tile_for(m: usize, n: usize, k: usize, caps: &Capabilities) -> TileShape {
161 let smallest = m.min(n).min(k);
162 if caps.tensor_cores && m % 16 == 0 && n % 16 == 0 && k % 16 == 0 {
163 return if smallest >= 512 {
165 TileShape::new(128, 128, 32)
166 } else {
167 TileShape::new(64, 64, 16)
168 };
169 }
170 if smallest <= 64 {
171 TileShape::new(16, 16, 16)
172 } else if smallest <= 512 {
173 TileShape::new(64, 64, 16)
174 } else {
175 TileShape::new(128, 128, 8)
176 }
177}
178
179#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
183pub enum MemoryKind {
184 Device,
186 HostPinned,
188 Unified,
190 Host,
192}
193
194impl fmt::Display for MemoryKind {
195 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
196 match self {
197 Self::Device => write!(f, "device"),
198 Self::HostPinned => write!(f, "host-pinned"),
199 Self::Unified => write!(f, "unified"),
200 Self::Host => write!(f, "host"),
201 }
202 }
203}
204
205#[derive(Debug, Clone, PartialEq, Eq)]
212pub struct DeviceInfo {
213 pub ordinal: usize,
215 pub name: String,
217 pub compute_capability: (u32, u32),
219 pub total_memory_bytes: u64,
221 pub memory_kind: MemoryKind,
223 pub capabilities: Capabilities,
225}
226
227impl DeviceInfo {
228 #[must_use]
230 pub fn cpu_reference(total_memory_bytes: u64) -> Self {
231 Self {
232 ordinal: 0,
233 name: "CPU (reference)".to_string(),
234 compute_capability: (0, 0),
235 total_memory_bytes,
236 memory_kind: MemoryKind::Host,
237 capabilities: Capabilities::cpu(),
238 }
239 }
240}
241
242impl fmt::Display for DeviceInfo {
243 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
244 let mem_mb = self.total_memory_bytes / (1024 * 1024);
245 let (major, minor) = self.compute_capability;
246 write!(
247 f,
248 "Device[{}] {} (cc {major}.{minor}, {mem_mb} MB, {})",
249 self.ordinal, self.name, self.memory_kind,
250 )
251 }
252}
253
254#[cfg(test)]
255mod tests {
256 use super::*;
257
258 #[test]
259 fn cpu_capabilities_are_conservative() {
260 let caps = Capabilities::cpu();
261 assert!(!caps.supports_fp16);
262 assert!(!caps.supports_bf16);
263 assert!(!caps.supports_fp8);
264 assert!(!caps.tensor_cores);
265 assert!(!caps.peer_access);
266 assert!(caps.unified_memory);
267 assert!(!caps.supports_mixed_precision());
268 assert_eq!(Capabilities::default(), Capabilities::cpu());
269 }
270
271 #[test]
272 fn mixed_precision_detection() {
273 let mut caps = Capabilities::cpu();
274 assert!(!caps.supports_mixed_precision());
275 caps.supports_bf16 = true;
276 assert!(caps.supports_mixed_precision());
277 }
278
279 #[test]
280 fn tensor_core_eligibility_requires_alignment_and_units() {
281 let mut caps = Capabilities::cpu();
282 assert!(!caps.can_use_tensor_cores(256, 256, 256));
284 caps.tensor_cores = true;
285 assert!(caps.can_use_tensor_cores(256, 256, 256));
287 assert!(!caps.can_use_tensor_cores(256, 256, 255));
289 }
290
291 #[test]
292 fn tile_heuristic_scales_with_problem_size() {
293 let caps = Capabilities::cpu();
294 assert_eq!(
295 default_tile_for(32, 32, 32, &caps),
296 TileShape::new(16, 16, 16)
297 );
298 assert_eq!(
299 default_tile_for(256, 256, 256, &caps),
300 TileShape::new(64, 64, 16)
301 );
302 assert_eq!(
303 default_tile_for(4096, 4096, 4096, &caps),
304 TileShape::new(128, 128, 8)
305 );
306 }
307
308 #[test]
309 fn tile_heuristic_snaps_to_wmma_when_tensor_cores() {
310 let mut caps = Capabilities::cpu();
311 caps.tensor_cores = true;
312 let t = default_tile_for(1024, 1024, 1024, &caps);
314 assert_eq!(t, TileShape::new(128, 128, 32));
315 assert_eq!(t.tile_m % 16, 0);
316 assert_eq!(t.tile_n % 16, 0);
317 assert_eq!(t.tile_k % 16, 0);
318 assert_eq!(
320 default_tile_for(64, 64, 64, &caps),
321 TileShape::new(64, 64, 16)
322 );
323 }
324
325 #[test]
326 fn tile_output_elems() {
327 assert_eq!(TileShape::new(128, 64, 8).output_elems(), 128 * 64);
328 }
329
330 #[test]
331 fn memory_kind_display() {
332 assert_eq!(MemoryKind::Device.to_string(), "device");
333 assert_eq!(MemoryKind::HostPinned.to_string(), "host-pinned");
334 assert_eq!(MemoryKind::Unified.to_string(), "unified");
335 assert_eq!(MemoryKind::Host.to_string(), "host");
336 }
337
338 #[test]
339 fn device_info_cpu_reference() {
340 let dev = DeviceInfo::cpu_reference(8 * 1024 * 1024 * 1024);
341 assert_eq!(dev.ordinal, 0);
342 assert_eq!(dev.compute_capability, (0, 0));
343 assert_eq!(dev.memory_kind, MemoryKind::Host);
344 assert_eq!(dev.capabilities, Capabilities::cpu());
345 assert!(dev.to_string().contains("CPU (reference)"));
346 assert!(dev.to_string().contains("8192 MB"));
347 }
348
349 #[test]
350 fn capabilities_display_round_trips_fields() {
351 let s = Capabilities::cpu().to_string();
352 assert!(s.contains("unified=true"));
353 assert!(s.contains("tensor_cores=false"));
354 assert!(s.contains("warp=1"));
355 }
356}