Skip to main content

cubecl_ir/
properties.rs

1use alloc::string::String;
2use core::{
3    fmt,
4    hash::{BuildHasher, Hash, Hasher},
5    str::FromStr,
6};
7
8use crate::{
9    AddressType, ElemType, EnumSet, EnumSetType, OpaqueType, SemanticType, Type, TypeHash,
10    VectorSize,
11    features::{AtomicUsage, ComplexUsage, Features, TypeUsage},
12};
13use cubecl_common::profile::TimingMethod;
14
15/// Properties of the device related to the accelerator hardware.
16///
17/// # Plane size min/max
18///
19/// This is a range of possible values for the plane size.
20///
21/// For Nvidia GPUs and HIP, this is a single fixed value.
22///
23/// For wgpu with AMD GPUs this is a range of possible values, but the actual configured value
24/// is undefined and can only be queried at runtime. Should usually be 32, but not guaranteed.
25///
26/// For Intel GPUs, this is variable based on the number of registers used in the kernel. No way to
27/// query this at compile time is currently available. As a result, the minimum value should usually
28/// be assumed.
29#[derive(Debug, Clone, PartialEq, Eq, Hash)]
30pub struct HardwareProperties {
31    /// The maximum size of a single load instruction, in bits. Used for optimized vector sizes.
32    pub load_width: u32,
33    /// The minimum size of a plane on this device
34    pub plane_size_min: u32,
35    /// The maximum size of a plane on this device
36    pub plane_size_max: u32,
37    /// minimum number of bindings for a kernel that can be used at once.
38    pub max_bindings: u32,
39    /// Maximum amount of shared memory, in bytes
40    pub max_shared_memory_size: usize,
41    /// Maximum `CubeCount` in x, y and z dimensions
42    pub max_cube_count: (u32, u32, u32),
43    /// Maximum number of total units in a cube
44    pub max_units_per_cube: u32,
45    /// Maximum `CubeDim` in x, y, and z dimensions
46    pub max_cube_dim: (u32, u32, u32),
47    /// Number of streaming multiprocessors (SM), if available
48    pub num_streaming_multiprocessors: Option<u32>,
49    /// Number of available parallel cpu units, if the runtime is CPU.
50    pub num_cpu_cores: Option<u32>,
51    /// Bytes of the device's last level cache, and `None`, never `Some(0)`,
52    /// where the runtime cannot read one.
53    ///
54    /// The size a working set has to outgrow before what it reaches is set by
55    /// memory rather than by the chip.
56    pub last_level_cache_size: Option<usize>,
57    /// Number of tensor cores per SM, if any
58    pub num_tensor_cores: Option<u32>,
59    /// The minimum tiling dimension for a single axis in tensor cores.
60    ///
61    /// For a backend that only supports 16x16x16, the value would be 16.
62    /// For a backend that also supports 32x8x16, the value would be 8.
63    pub min_tensor_cores_dim: Option<u32>,
64    /// Maximum vector size supported by the device
65    pub max_vector_size: VectorSize,
66    /// Memory reserved for the driver when using cube-scoped matrices
67    pub cube_mma_reserved_shared_memory: usize,
68}
69
70/// Properties of the device related to allocation.
71#[derive(Debug, Clone, PartialEq, Eq, Hash)]
72#[non_exhaustive]
73pub struct MemoryDeviceProperties {
74    /// The maximum nr. of bytes that can be allocated in one go.
75    pub max_page_size: u64,
76    /// The required memory offset alignment in bytes.
77    pub alignment: u64,
78    /// Private because [`set_max_memory`](Self::set_max_memory) is its only
79    /// writer, and that is where a zero becomes `None`.
80    max_memory: Option<u64>,
81}
82
83impl MemoryDeviceProperties {
84    /// Properties that state no capacity. A runtime that can read one adds it
85    /// with [`with_max_memory`](Self::with_max_memory).
86    pub const fn new(max_page_size: u64, alignment: u64) -> Self {
87        Self {
88            max_page_size,
89            alignment,
90            max_memory: None,
91        }
92    }
93
94    /// How many bytes this memory may be asked to hold at once, or `None`,
95    /// never `Some(0)`, where the runtime has no figure.
96    ///
97    /// This sizes a whole workload, while
98    /// [`max_page_size`](Self::max_page_size) bounds a single allocation and
99    /// is often derived from it. It is a budget, not a hardware census: each
100    /// runtime reports the largest figure its own API stands behind, and
101    /// staying under it is what keeps the device off its paging path.
102    ///
103    /// A runtime with no figure leaves it `None` rather than guess, because a
104    /// guess reads as a measurement to every caller downstream.
105    pub const fn max_memory(&self) -> Option<u64> {
106        self.max_memory
107    }
108
109    /// States the capacity, as [`set_max_memory`](Self::set_max_memory) does.
110    pub const fn with_max_memory(mut self, max_memory: u64) -> Self {
111        self.set_max_memory(max_memory);
112        self
113    }
114
115    /// States the capacity, dropping a zero: an API with nothing to report
116    /// reports `0`, and [`max_memory`](Self::max_memory) says that as `None`.
117    pub const fn set_max_memory(&mut self, max_memory: u64) {
118        self.max_memory = match max_memory {
119            0 => None,
120            size => Some(size),
121        };
122    }
123}
124
125/// Who a device is, and what its compiled code is keyed to.
126///
127/// `name` and `fingerprint` answer different questions and must not be confused. `name`
128/// is for people: it names the physical part, and two machines holding the same
129/// part report the same name. `fingerprint` is for correctness: it is verbatim
130/// the string this runtime passes to
131/// [`compilation_store`](../../cubecl_runtime/compiler/fn.compilation_store.html),
132/// which is what puts a compiled artifact out of reach of a machine that cannot
133/// run it.
134///
135/// Reporting the fingerprint here rather than recomputing it is the whole
136/// point: a backend derives it once and hands it to both consumers, so the
137/// identity a bundle is stamped with and the namespace its kernels live under
138/// cannot drift apart.
139#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
140pub struct DeviceIdentity {
141    /// The device as it names itself — `AMD Radeon 8060S Graphics`,
142    /// `NVIDIA H100 PCIe`. Distinct parts may share a name, so no capability
143    /// and no compiled artifact may be keyed to it.
144    pub name: String,
145    /// What this runtime compiles *for* — `hip-kernel_gfx1151`, `ptx_sm90`.
146    /// Verbatim the `compilation_store` fingerprint, so a namespace read back
147    /// out of a bundle compares against it directly.
148    pub fingerprint: String,
149    /// The card behind the device, `None` for a device that is no card: a CPU, or a software
150    /// rasterizer.
151    pub physical: Option<PhysicalDevice>,
152}
153
154/// The card a device runs on. Two runtimes report different fields for one card, so compare with
155/// [`is_same_card`](Self::is_same_card), not `==`.
156#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
157#[non_exhaustive]
158pub struct PhysicalDevice {
159    /// The key every runtime reports alike on Linux.
160    pub pci_address: Option<PciAddress>,
161    /// The key every runtime reports alike on Windows.
162    pub luid: Option<AdapterLuid>,
163    pub vendor: Option<PciVendor>,
164}
165
166impl PhysicalDevice {
167    /// Whether `other` is known to be this card through another runtime: by PCI address, otherwise
168    /// by LUID. A card with neither matches nothing, itself included, since two such cards of one
169    /// make compare equal.
170    pub fn is_same_card(&self, other: &Self) -> bool {
171        if let (Some(mine), Some(theirs)) = (self.pci_address, other.pci_address) {
172            return mine == theirs;
173        }
174        matches!((self.luid, other.luid), (Some(mine), Some(theirs)) if mine == theirs)
175    }
176
177    /// Takes what this runtime left out from `other`, the same card seen through another runtime.
178    pub fn fill_from(&mut self, other: &Self) {
179        let Self {
180            pci_address,
181            luid,
182            vendor,
183        } = *other;
184        self.pci_address = self.pci_address.or(pci_address);
185        self.luid = self.luid.or(luid);
186        self.vendor = self.vendor.or(vendor);
187    }
188}
189
190/// The id Windows gives a graphics adapter. It changes on restart, so it has no serialization or
191/// text form: a stored key wants [`PhysicalDevice::pci_address`].
192#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
193pub struct AdapterLuid([u8; 8]);
194
195impl AdapterLuid {
196    /// From the eight bytes of a Windows `LUID`, low part first.
197    pub fn new(bytes: [u8; 8]) -> Self {
198        Self(bytes)
199    }
200
201    pub fn from_parts(low_part: u32, high_part: i32) -> Self {
202        let mut bytes = [0; 8];
203        bytes[..4].copy_from_slice(&low_part.to_le_bytes());
204        bytes[4..].copy_from_slice(&high_part.to_le_bytes());
205        Self(bytes)
206    }
207
208    pub fn bytes(self) -> [u8; 8] {
209        self.0
210    }
211}
212
213/// The maker of a card, by PCI vendor id.
214#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
215pub enum PciVendor {
216    Nvidia,
217    Amd,
218    Intel,
219    Apple,
220    /// Mali GPUs.
221    Arm,
222    /// Adreno GPUs.
223    Qualcomm,
224    Other(u32),
225}
226
227impl PciVendor {
228    pub fn id(self) -> u32 {
229        match self {
230            Self::Nvidia => 0x10de,
231            Self::Amd => 0x1002,
232            Self::Intel => 0x8086,
233            Self::Apple => 0x106b,
234            Self::Arm => 0x13b5,
235            Self::Qualcomm => 0x5143,
236            Self::Other(id) => id,
237        }
238    }
239}
240
241impl From<u32> for PciVendor {
242    fn from(id: u32) -> Self {
243        match id {
244            0x10de => Self::Nvidia,
245            0x1002 => Self::Amd,
246            0x8086 => Self::Intel,
247            0x106b => Self::Apple,
248            0x13b5 => Self::Arm,
249            0x5143 => Self::Qualcomm,
250            other => Self::Other(other),
251        }
252    }
253}
254
255impl fmt::Display for PciVendor {
256    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
257        match self {
258            Self::Nvidia => f.write_str("NVIDIA"),
259            Self::Amd => f.write_str("AMD"),
260            Self::Intel => f.write_str("Intel"),
261            Self::Apple => f.write_str("Apple"),
262            Self::Arm => f.write_str("Arm"),
263            Self::Qualcomm => f.write_str("Qualcomm"),
264            Self::Other(id) => write!(f, "{id:#06x}"),
265        }
266    }
267}
268
269/// `domain:bus:device.function`, written `0000:07:00.0`. CUDA and NVML call it the bus id.
270#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
271pub struct PciAddress {
272    pub domain: u32,
273    pub bus: u8,
274    pub device: u8,
275    pub function: u8,
276}
277
278impl fmt::Display for PciAddress {
279    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
280        write!(
281            f,
282            "{:04x}:{:02x}:{:02x}.{:x}",
283            self.domain, self.bus, self.device, self.function
284        )
285    }
286}
287
288#[derive(Debug, Clone, PartialEq, Eq)]
289pub struct PciAddressError(pub String);
290
291impl fmt::Display for PciAddressError {
292    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
293        write!(f, "not a PCI address: {}", self.0)
294    }
295}
296
297impl core::error::Error for PciAddressError {}
298
299impl FromStr for PciAddress {
300    type Err = PciAddressError;
301
302    /// Also accepts the domainless `07:00.0` CUDA emits.
303    fn from_str(text: &str) -> Result<Self, Self::Err> {
304        let err = || PciAddressError(String::from(text));
305        let (rest, function) = text.rsplit_once('.').ok_or_else(err)?;
306        let mut parts = rest.rsplitn(3, ':');
307        let device = parts.next().ok_or_else(err)?;
308        let bus = parts.next().ok_or_else(err)?;
309        let domain = parts.next().unwrap_or("0");
310        Ok(Self {
311            domain: u32::from_str_radix(domain, 16).map_err(|_| err())?,
312            bus: u8::from_str_radix(bus, 16).map_err(|_| err())?,
313            device: u8::from_str_radix(device, 16).map_err(|_| err())?,
314            function: u8::from_str_radix(function, 16).map_err(|_| err())?,
315        })
316    }
317}
318
319/// Properties of what the device can do, like what `Feature` are
320/// supported by it and what its memory properties are.
321#[derive(Debug, Clone, PartialEq, Eq)]
322pub struct DeviceProperties {
323    /// The features supported by the runtime.
324    pub features: Features,
325    /// The memory properties of this client.
326    pub memory: MemoryDeviceProperties,
327    /// The topology properties of this client.
328    pub hardware: HardwareProperties,
329    /// The method used for profiling on the device.
330    pub timing_method: TimingMethod,
331    /// Who the device is, and what its kernels are keyed to.
332    pub identity: DeviceIdentity,
333}
334
335impl TypeHash for DeviceProperties {
336    fn write_hash(_hasher: &mut impl core::hash::Hasher) {
337        // ignored.
338    }
339}
340
341impl DeviceProperties {
342    /// Create a new feature set with the given features and memory properties.
343    ///
344    /// `identity` is a required argument rather than something a backend may
345    /// fill in afterwards, so a runtime cannot ship reporting an anonymous
346    /// device — the failure mode that leaves a bundle unable to say what it was
347    /// built for.
348    pub fn new(
349        features: Features,
350        memory_props: MemoryDeviceProperties,
351        hardware: HardwareProperties,
352        timing_method: TimingMethod,
353        identity: DeviceIdentity,
354    ) -> Self {
355        DeviceProperties {
356            features,
357            memory: memory_props,
358            hardware,
359            timing_method,
360            identity,
361        }
362    }
363
364    /// Get the usages for a type
365    pub fn type_usage(&self, ty: ElemType) -> EnumSet<TypeUsage> {
366        self.features.type_usage(ty)
367    }
368
369    /// Get the complex capability families for a type.
370    pub fn complex_usage(&self, ty: ElemType) -> EnumSet<ComplexUsage> {
371        self.features.complex_usage(ty)
372    }
373
374    /// Whether a complex type supports the requested capability family.
375    pub fn supports_complex_usage(&self, ty: ElemType, usage: ComplexUsage) -> bool {
376        self.features.supports_complex_usage(ty, usage)
377    }
378
379    /// Get the usages for an atomic type
380    pub fn atomic_type_usage(&self, ty: Type) -> EnumSet<AtomicUsage> {
381        self.features.atomic_type_usage(ty)
382    }
383
384    /// Whether the type is supported in any way
385    pub fn supports_type(&self, ty: impl Into<Type>) -> bool {
386        self.features.supports_type(ty)
387    }
388
389    /// Whether the address type is supported in any way
390    pub fn supports_address(&self, ty: impl Into<AddressType>) -> bool {
391        self.features.supports_address(ty)
392    }
393
394    /// Register an address type to the features
395    pub fn register_address_type(&mut self, ty: impl Into<AddressType>) {
396        self.features.types.address.insert(ty.into());
397    }
398
399    /// Register an address type to the features
400    pub fn register_atomic_type_usage(&mut self, ty: Type, uses: impl Into<EnumSet<AtomicUsage>>) {
401        *self.features.types.atomic.entry(ty).or_default() |= uses.into();
402    }
403
404    /// Register a storage type to the features
405    pub fn register_type_usage(
406        &mut self,
407        ty: impl Into<ElemType>,
408        uses: impl Into<EnumSet<TypeUsage>>,
409    ) {
410        *self.features.types.elem.entry(ty.into()).or_default() |= uses.into();
411    }
412
413    /// Register complex capability families for an element type.
414    pub fn register_complex_usage(
415        &mut self,
416        ty: impl Into<ElemType>,
417        uses: impl Into<EnumSet<ComplexUsage>>,
418    ) {
419        *self.features.types.complex.entry(ty.into()).or_default() |= uses.into();
420    }
421
422    /// Register a semantic type to the features
423    pub fn register_semantic_type(&mut self, ty: SemanticType) {
424        self.features.types.semantic.insert(ty);
425    }
426
427    /// Register an opaque type to the features
428    pub fn register_opaque_type(&mut self, ty: OpaqueType) {
429        self.features.types.opaque.insert(ty);
430    }
431
432    /// Create a stable hash of all device properties relevant to kernel compilation. Can be used
433    /// as a stable checksum for a compilation cache.
434    pub fn checksum(&self) -> u64 {
435        let state = foldhash::fast::FixedState::default();
436        let mut hasher = state.build_hasher();
437        self.features.hash(&mut hasher);
438        self.hardware.hash(&mut hasher);
439        hasher.finish()
440    }
441}
442
443/// Unchecked optimizations for float operations. May cause precision differences, or undefined
444/// behaviour if the relevant conditions are not followed.
445#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
446#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, TypeHash, EnumSetType)]
447pub enum FastMath {
448    /// Assume values are never `NaN`. If they are, the result is considered undefined behaviour.
449    NotNaN,
450    /// Assume values are never `Inf`/`-Inf`. If they are, the result is considered undefined
451    /// behaviour.
452    NotInf,
453    /// Ignore sign on zero values.
454    UnsignedZero,
455    /// Allow swapping float division with a reciprocal, even if that swap would change precision.
456    AllowReciprocal,
457    /// Allow contracting float operations into fewer operations, even if the precision could
458    /// change.
459    AllowContraction,
460    /// Allow reassociation for float operations, even if the precision could change.
461    AllowReassociation,
462    /// Allow all mathematical transformations for float operations, including contraction and
463    /// reassociation, even if the precision could change.
464    AllowTransform,
465    /// Allow using lower precision intrinsics
466    ReducedPrecision,
467}
468
469impl FastMath {
470    pub const fn all() -> EnumSet<FastMath> {
471        EnumSet::all()
472    }
473}
474
475#[cfg(test)]
476mod tests {
477    use super::*;
478    use alloc::string::ToString;
479
480    /// A capacity is stated only by the runtime that read one.
481    ///
482    /// Properties built without one must answer `None`, or a caller would
483    /// size a workload against a figure nobody measured.
484    #[test]
485    fn a_memory_states_no_capacity_until_one_is_read() {
486        let props = MemoryDeviceProperties::new(1024, 32);
487        assert_eq!(props.max_memory(), None);
488        assert_eq!(props.clone().with_max_memory(4096).max_memory(), Some(4096));
489    }
490
491    /// A zero from the runtime's API reads as no capacity.
492    ///
493    /// An API with nothing to report reports `0`, which must not reach a
494    /// caller as a device that holds nothing.
495    #[test]
496    fn a_capacity_of_zero_is_no_capacity() {
497        let mut props = MemoryDeviceProperties::new(1024, 32).with_max_memory(4096);
498        props.set_max_memory(0);
499        assert_eq!(props.max_memory(), None);
500    }
501
502    #[test]
503    fn a_vendor_keeps_its_id_whether_named_or_not() {
504        for id in [0x10de, 0x1002, 0x8086, 0x106b, 0x13b5, 0x5143, 0x1af4] {
505            assert_eq!(PciVendor::from(id).id(), id);
506        }
507        assert_eq!(PciVendor::from(0x10de), PciVendor::Nvidia);
508        assert_eq!(PciVendor::from(0x1af4), PciVendor::Other(0x1af4));
509        assert_eq!(PciVendor::Other(0x1af4).to_string(), "0x1af4");
510    }
511
512    #[test]
513    fn a_luid_from_parts_is_the_bytes_vulkan_reports() {
514        let bytes = [0x8a, 0x1d, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00];
515        assert_eq!(
516            AdapterLuid::from_parts(0x0001_1d8a, 0),
517            AdapterLuid::new(bytes)
518        );
519        assert_eq!(
520            AdapterLuid::from_parts(1, -1).bytes(),
521            [1, 0, 0, 0, 0xff, 0xff, 0xff, 0xff]
522        );
523    }
524
525    #[test]
526    fn a_card_is_matched_by_address_then_by_luid() {
527        let address = |bus| {
528            Some(PciAddress {
529                domain: 0,
530                bus,
531                device: 0,
532                function: 0,
533            })
534        };
535        let luid = |low| Some(AdapterLuid::from_parts(low, 0));
536        let card = |pci_address, luid| PhysicalDevice {
537            pci_address,
538            luid,
539            vendor: None,
540        };
541
542        assert!(card(address(7), None).is_same_card(&card(address(7), luid(1))));
543        assert!(!card(address(7), luid(1)).is_same_card(&card(address(8), luid(1))));
544        assert!(card(None, luid(1)).is_same_card(&card(address(7), luid(1))));
545        assert!(!card(None, luid(1)).is_same_card(&card(None, luid(2))));
546        assert!(!card(None, None).is_same_card(&card(None, None)));
547    }
548
549    #[test]
550    fn a_card_filled_from_another_runtime_keeps_what_it_reported() {
551        let address = |bus| {
552            Some(PciAddress {
553                domain: 0,
554                bus,
555                device: 0,
556                function: 0,
557            })
558        };
559        let luid = Some(AdapterLuid::from_parts(1, 0));
560        let mut card = PhysicalDevice {
561            pci_address: address(7),
562            luid: None,
563            vendor: None,
564        };
565
566        card.fill_from(&PhysicalDevice {
567            pci_address: address(8),
568            luid,
569            vendor: Some(PciVendor::Nvidia),
570        });
571
572        assert_eq!(
573            card,
574            PhysicalDevice {
575                pci_address: address(7),
576                luid,
577                vendor: Some(PciVendor::Nvidia),
578            }
579        );
580    }
581
582    #[test]
583    fn a_pci_address_round_trips_and_defaults_its_domain() {
584        let id = PciAddress {
585            domain: 0,
586            bus: 7,
587            device: 0,
588            function: 0,
589        };
590        assert_eq!(id.to_string(), "0000:07:00.0");
591        assert_eq!("0000:07:00.0".parse::<PciAddress>(), Ok(id));
592        assert_eq!("07:00.0".parse::<PciAddress>(), Ok(id));
593        assert_eq!(
594            "0001:a3:1f.7".parse::<PciAddress>(),
595            Ok(PciAddress {
596                domain: 1,
597                bus: 0xa3,
598                device: 0x1f,
599                function: 7
600            })
601        );
602        assert!("07:00".parse::<PciAddress>().is_err());
603        assert!("gpu".parse::<PciAddress>().is_err());
604    }
605}