use alloc::string::String;
use core::{
fmt,
hash::{BuildHasher, Hash, Hasher},
str::FromStr,
};
use crate::{
AddressType, ElemType, EnumSet, EnumSetType, OpaqueType, SemanticType, Type, TypeHash,
VectorSize,
features::{AtomicUsage, ComplexUsage, Features, TypeUsage},
};
use cubecl_common::profile::TimingMethod;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct HardwareProperties {
pub load_width: u32,
pub vector_register_count: Option<u32>,
pub plane_size_min: u32,
pub plane_size_max: u32,
pub max_bindings: u32,
pub max_shared_memory_size: usize,
pub max_cube_count: (u32, u32, u32),
pub max_units_per_cube: u32,
pub max_cube_dim: (u32, u32, u32),
pub num_streaming_multiprocessors: Option<u32>,
pub num_cpu_cores: Option<u32>,
pub last_level_cache_size: Option<usize>,
pub num_tensor_cores: Option<u32>,
pub min_tensor_cores_dim: Option<u32>,
pub max_vector_size: VectorSize,
pub cube_mma_reserved_shared_memory: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct VectorRegisters {
count: usize,
register_vector_size: VectorSize,
max_vector_size: VectorSize,
}
impl VectorRegisters {
pub fn new(hardware: &HardwareProperties, elem_size: usize) -> Option<Self> {
Some(VectorRegisters {
count: hardware.vector_register_count? as usize,
register_vector_size: hardware.vector_size_in(hardware.load_width, elem_size),
max_vector_size: prev_power_of_two(hardware.max_vector_size.max(1)),
})
}
pub fn count(&self) -> usize {
self.count
}
pub fn registers_for(&self, vector_size: VectorSize) -> usize {
vector_size.div_ceil(self.register_vector_size).max(1)
}
pub fn widest_vector_size(&self, live: usize) -> VectorSize {
let registers = prev_power_of_two((self.count / live.max(1)).max(1));
(registers * self.register_vector_size).min(self.max_vector_size)
}
pub fn vectors_fitting(&self, vector_size: VectorSize, reserved: usize) -> usize {
self.count.saturating_sub(reserved) / self.registers_for(vector_size)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub struct MemoryDeviceProperties {
pub max_page_size: u64,
pub alignment: u64,
max_memory: Option<u64>,
}
impl MemoryDeviceProperties {
pub const fn new(max_page_size: u64, alignment: u64) -> Self {
Self {
max_page_size,
alignment,
max_memory: None,
}
}
pub const fn max_memory(&self) -> Option<u64> {
self.max_memory
}
pub const fn with_max_memory(mut self, max_memory: u64) -> Self {
self.set_max_memory(max_memory);
self
}
pub const fn set_max_memory(&mut self, max_memory: u64) {
self.max_memory = match max_memory {
0 => None,
size => Some(size),
};
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
pub struct DeviceIdentity {
pub name: String,
pub fingerprint: String,
pub physical: Option<PhysicalDevice>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
#[non_exhaustive]
pub struct PhysicalDevice {
pub pci_address: Option<PciAddress>,
pub luid: Option<AdapterLuid>,
pub registry_entry_id: Option<RegistryEntryId>,
pub vendor: Option<PciVendor>,
}
impl PhysicalDevice {
pub fn is_same_card(&self, other: &Self) -> bool {
if let (Some(mine), Some(theirs)) = (self.pci_address, other.pci_address) {
return mine == theirs;
}
if let (Some(mine), Some(theirs)) = (self.luid, other.luid) {
return mine == theirs;
}
matches!(
(self.registry_entry_id, other.registry_entry_id),
(Some(mine), Some(theirs)) if mine == theirs
)
}
pub fn fill_from(&mut self, other: &Self) {
let Self {
pci_address,
luid,
registry_entry_id,
vendor,
} = *other;
self.pci_address = self.pci_address.or(pci_address);
self.luid = self.luid.or(luid);
self.registry_entry_id = self.registry_entry_id.or(registry_entry_id);
self.vendor = self.vendor.or(vendor);
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct RegistryEntryId(u64);
impl RegistryEntryId {
pub fn new(id: u64) -> Self {
Self(id)
}
pub fn get(self) -> u64 {
self.0
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct AdapterLuid([u8; 8]);
impl AdapterLuid {
pub fn new(bytes: [u8; 8]) -> Self {
Self(bytes)
}
pub fn from_parts(low_part: u32, high_part: i32) -> Self {
let mut bytes = [0; 8];
bytes[..4].copy_from_slice(&low_part.to_le_bytes());
bytes[4..].copy_from_slice(&high_part.to_le_bytes());
Self(bytes)
}
pub fn bytes(self) -> [u8; 8] {
self.0
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum PciVendor {
Nvidia,
Amd,
Intel,
Apple,
Arm,
Qualcomm,
Other(u32),
}
impl PciVendor {
pub fn id(self) -> u32 {
match self {
Self::Nvidia => 0x10de,
Self::Amd => 0x1002,
Self::Intel => 0x8086,
Self::Apple => 0x106b,
Self::Arm => 0x13b5,
Self::Qualcomm => 0x5143,
Self::Other(id) => id,
}
}
}
impl From<u32> for PciVendor {
fn from(id: u32) -> Self {
match id {
0x10de => Self::Nvidia,
0x1002 => Self::Amd,
0x8086 => Self::Intel,
0x106b => Self::Apple,
0x13b5 => Self::Arm,
0x5143 => Self::Qualcomm,
other => Self::Other(other),
}
}
}
impl fmt::Display for PciVendor {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Nvidia => f.write_str("NVIDIA"),
Self::Amd => f.write_str("AMD"),
Self::Intel => f.write_str("Intel"),
Self::Apple => f.write_str("Apple"),
Self::Arm => f.write_str("Arm"),
Self::Qualcomm => f.write_str("Qualcomm"),
Self::Other(id) => write!(f, "{id:#06x}"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct PciAddress {
pub domain: u32,
pub bus: u8,
pub device: u8,
pub function: u8,
}
impl fmt::Display for PciAddress {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{:04x}:{:02x}:{:02x}.{:x}",
self.domain, self.bus, self.device, self.function
)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PciAddressError(pub String);
impl fmt::Display for PciAddressError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "not a PCI address: {}", self.0)
}
}
impl core::error::Error for PciAddressError {}
impl FromStr for PciAddress {
type Err = PciAddressError;
fn from_str(text: &str) -> Result<Self, Self::Err> {
let err = || PciAddressError(String::from(text));
let (rest, function) = text.rsplit_once('.').ok_or_else(err)?;
let mut parts = rest.rsplitn(3, ':');
let device = parts.next().ok_or_else(err)?;
let bus = parts.next().ok_or_else(err)?;
let domain = parts.next().unwrap_or("0");
Ok(Self {
domain: u32::from_str_radix(domain, 16).map_err(|_| err())?,
bus: u8::from_str_radix(bus, 16).map_err(|_| err())?,
device: u8::from_str_radix(device, 16).map_err(|_| err())?,
function: u8::from_str_radix(function, 16).map_err(|_| err())?,
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DeviceProperties {
pub features: Features,
pub memory: MemoryDeviceProperties,
pub hardware: HardwareProperties,
pub timing_method: TimingMethod,
pub identity: DeviceIdentity,
pub io_width_override: Option<u32>,
}
impl TypeHash for DeviceProperties {
fn write_hash(_hasher: &mut impl core::hash::Hasher) {
}
}
impl DeviceProperties {
pub fn new(
features: Features,
memory_props: MemoryDeviceProperties,
hardware: HardwareProperties,
timing_method: TimingMethod,
identity: DeviceIdentity,
) -> Self {
DeviceProperties {
features,
memory: memory_props,
hardware,
timing_method,
identity,
io_width_override: None,
}
}
pub fn io_width(&self) -> u32 {
self.io_width_override.unwrap_or(self.hardware.load_width)
}
pub fn io_optimized_vector_sizes(
&self,
elem_size: usize,
) -> impl Iterator<Item = VectorSize> + Clone {
vector_sizes_down_from(self.widest_io_vector_size(elem_size))
}
pub fn vector_sizes_in_registers(
&self,
elem_size: usize,
live: usize,
) -> impl Iterator<Item = VectorSize> + Clone {
let widest = match VectorRegisters::new(&self.hardware, elem_size) {
Some(registers) => registers.widest_vector_size(live),
None => self.widest_io_vector_size(elem_size),
};
vector_sizes_down_from(widest)
}
pub fn type_usage(&self, ty: ElemType) -> EnumSet<TypeUsage> {
self.features.type_usage(ty)
}
pub fn complex_usage(&self, ty: ElemType) -> EnumSet<ComplexUsage> {
self.features.complex_usage(ty)
}
pub fn supports_complex_usage(&self, ty: ElemType, usage: ComplexUsage) -> bool {
self.features.supports_complex_usage(ty, usage)
}
pub fn atomic_type_usage(&self, ty: Type) -> EnumSet<AtomicUsage> {
self.features.atomic_type_usage(ty)
}
pub fn supports_type(&self, ty: impl Into<Type>) -> bool {
self.features.supports_type(ty)
}
pub fn supports_address(&self, ty: impl Into<AddressType>) -> bool {
self.features.supports_address(ty)
}
pub fn register_address_type(&mut self, ty: impl Into<AddressType>) {
self.features.types.address.insert(ty.into());
}
pub fn register_atomic_type_usage(&mut self, ty: Type, uses: impl Into<EnumSet<AtomicUsage>>) {
*self.features.types.atomic.entry(ty).or_default() |= uses.into();
}
pub fn register_type_usage(
&mut self,
ty: impl Into<ElemType>,
uses: impl Into<EnumSet<TypeUsage>>,
) {
*self.features.types.elem.entry(ty.into()).or_default() |= uses.into();
}
pub fn register_complex_usage(
&mut self,
ty: impl Into<ElemType>,
uses: impl Into<EnumSet<ComplexUsage>>,
) {
*self.features.types.complex.entry(ty.into()).or_default() |= uses.into();
}
pub fn register_semantic_type(&mut self, ty: SemanticType) {
self.features.types.semantic.insert(ty);
}
pub fn register_opaque_type(&mut self, ty: OpaqueType) {
self.features.types.opaque.insert(ty);
}
pub fn checksum(&self) -> u64 {
let state = foldhash::fast::FixedState::default();
let mut hasher = state.build_hasher();
self.features.hash(&mut hasher);
self.hardware.hash(&mut hasher);
hasher.finish()
}
fn widest_io_vector_size(&self, elem_size: usize) -> VectorSize {
self.hardware.vector_size_in(self.io_width(), elem_size)
}
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, TypeHash, EnumSetType)]
pub enum FastMath {
NotNaN,
NotInf,
UnsignedZero,
AllowReciprocal,
AllowContraction,
AllowReassociation,
AllowTransform,
ReducedPrecision,
}
impl FastMath {
pub const fn all() -> EnumSet<FastMath> {
EnumSet::all()
}
}
impl HardwareProperties {
fn vector_size_in(&self, width: u32, elem_size: usize) -> VectorSize {
let elems = width as usize / (elem_size * 8);
prev_power_of_two(elems.min(self.max_vector_size).max(1))
}
}
fn prev_power_of_two(value: usize) -> usize {
1 << value.ilog2()
}
fn vector_sizes_down_from(widest: usize) -> impl Iterator<Item = VectorSize> + Clone {
(0..=widest.ilog2()).rev().map(|exponent| 1 << exponent)
}
#[cfg(test)]
mod tests {
use super::*;
use alloc::string::ToString;
#[test]
fn a_device_without_a_fixed_register_set_has_no_budget() {
assert_eq!(VectorRegisters::new(&hardware(128, None), 4), None);
}
#[test]
fn the_widest_vector_size_keeps_every_live_vector_in_registers() {
let f32 = registers(AVX2, 4);
assert_eq!(f32.widest_vector_size(6), 16);
assert_eq!(f32.widest_vector_size(8), 16);
assert_eq!(f32.widest_vector_size(9), 8);
assert_eq!(registers(AVX2, 8).widest_vector_size(6), 8);
assert_eq!(registers(AVX512, 4).widest_vector_size(6), 64);
}
#[test]
fn neon_and_avx2_budget_the_same_vector_sizes_from_equal_register_files() {
let (neon, avx2) = (registers(NEON, 4), registers(AVX2, 4));
assert_eq!(neon.widest_vector_size(3), avx2.widest_vector_size(3));
assert_eq!(neon.widest_vector_size(6), avx2.widest_vector_size(6));
}
#[test]
fn more_live_vectors_than_registers_still_get_one_register_each() {
assert_eq!(registers(AVX2, 4).widest_vector_size(40), 8);
assert_eq!(registers(NEON, 4).widest_vector_size(40), 4);
}
#[test]
fn vectors_fit_in_the_registers_left_unreserved() {
let f32 = registers(AVX2, 4);
assert_eq!(f32.registers_for(16), 2);
assert_eq!(f32.vectors_fitting(16, 0), 8);
assert_eq!(f32.vectors_fitting(8, 4), 12);
assert_eq!(f32.vectors_fitting(8, 20), 0);
}
#[test]
fn a_capped_vector_size_caps_the_register_vector_size() {
let mut capped = hardware(256, Some(16));
capped.max_vector_size = 4;
let f32 = VectorRegisters::new(&capped, 4).unwrap();
assert_eq!(f32.registers_for(8), 2);
assert_eq!(f32.widest_vector_size(1), 4);
}
#[test]
fn io_follows_a_load_width_raised_after_construction() {
let mut device = device(128);
device.hardware.load_width = 256;
assert_eq!(device.io_width(), 256);
}
#[test]
fn a_stated_io_width_outlasts_a_load_width_change() {
let mut device = device(256);
device.io_width_override = Some(512);
device.hardware.load_width = 128;
assert_eq!(device.io_width(), 512);
}
#[test]
fn a_cap_between_powers_of_two_rounds_down_alike() {
let mut device = device(512);
device.hardware.max_vector_size = 12;
device.hardware.vector_register_count = Some(32);
assert_eq!(
device
.io_optimized_vector_sizes(4)
.collect::<alloc::vec::Vec<_>>(),
alloc::vec![8, 4, 2, 1]
);
assert_eq!(device.vector_sizes_in_registers(4, 1).next(), Some(8));
}
#[test]
fn a_device_without_registers_sizes_by_its_io() {
let device = device(128);
assert!(
device
.vector_sizes_in_registers(4, 6)
.eq(device.io_optimized_vector_sizes(4))
);
}
#[test]
fn a_memory_states_no_capacity_until_one_is_read() {
let props = MemoryDeviceProperties::new(1024, 32);
assert_eq!(props.max_memory(), None);
assert_eq!(props.clone().with_max_memory(4096).max_memory(), Some(4096));
}
#[test]
fn a_capacity_of_zero_is_no_capacity() {
let mut props = MemoryDeviceProperties::new(1024, 32).with_max_memory(4096);
props.set_max_memory(0);
assert_eq!(props.max_memory(), None);
}
#[test]
fn a_vendor_keeps_its_id_whether_named_or_not() {
for id in [0x10de, 0x1002, 0x8086, 0x106b, 0x13b5, 0x5143, 0x1af4] {
assert_eq!(PciVendor::from(id).id(), id);
}
assert_eq!(PciVendor::from(0x10de), PciVendor::Nvidia);
assert_eq!(PciVendor::from(0x1af4), PciVendor::Other(0x1af4));
assert_eq!(PciVendor::Other(0x1af4).to_string(), "0x1af4");
}
#[test]
fn a_luid_from_parts_is_the_bytes_vulkan_reports() {
let bytes = [0x8a, 0x1d, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00];
assert_eq!(
AdapterLuid::from_parts(0x0001_1d8a, 0),
AdapterLuid::new(bytes)
);
assert_eq!(
AdapterLuid::from_parts(1, -1).bytes(),
[1, 0, 0, 0, 0xff, 0xff, 0xff, 0xff]
);
}
#[test]
fn a_card_is_matched_by_address_then_by_luid() {
let address = |bus| {
Some(PciAddress {
domain: 0,
bus,
device: 0,
function: 0,
})
};
let luid = |low| Some(AdapterLuid::from_parts(low, 0));
let card = |pci_address, luid| PhysicalDevice {
pci_address,
luid,
registry_entry_id: None,
vendor: None,
};
assert!(card(address(7), None).is_same_card(&card(address(7), luid(1))));
assert!(!card(address(7), luid(1)).is_same_card(&card(address(8), luid(1))));
assert!(card(None, luid(1)).is_same_card(&card(address(7), luid(1))));
assert!(!card(None, luid(1)).is_same_card(&card(None, luid(2))));
assert!(!card(None, None).is_same_card(&card(None, None)));
}
#[test]
fn a_card_filled_from_another_runtime_keeps_what_it_reported() {
let address = |bus| {
Some(PciAddress {
domain: 0,
bus,
device: 0,
function: 0,
})
};
let luid = Some(AdapterLuid::from_parts(1, 0));
let mut card = PhysicalDevice {
pci_address: address(7),
luid: None,
registry_entry_id: None,
vendor: None,
};
card.fill_from(&PhysicalDevice {
pci_address: address(8),
luid,
registry_entry_id: None,
vendor: Some(PciVendor::Nvidia),
});
assert_eq!(
card,
PhysicalDevice {
pci_address: address(7),
luid,
registry_entry_id: None,
vendor: Some(PciVendor::Nvidia),
}
);
}
#[test]
fn a_card_with_neither_address_nor_luid_is_matched_by_registry_entry_id() {
let card = |registry_entry_id| PhysicalDevice {
pci_address: None,
luid: None,
registry_entry_id: Some(RegistryEntryId::new(registry_entry_id)),
vendor: None,
};
assert!(card(0x1_0000_04c8).is_same_card(&card(0x1_0000_04c8)));
assert!(!card(0x1_0000_04c8).is_same_card(&card(0x1_0000_04c9)));
}
#[test]
fn the_address_and_the_luid_outrank_the_registry_entry_id() {
let address = |bus| {
Some(PciAddress {
domain: 0,
bus,
device: 0,
function: 0,
})
};
let luid = |low| Some(AdapterLuid::from_parts(low, 0));
let card = |pci_address, luid| PhysicalDevice {
pci_address,
luid,
registry_entry_id: Some(RegistryEntryId::new(0x1_0000_04c8)),
vendor: None,
};
assert!(!card(address(7), None).is_same_card(&card(address(8), None)));
assert!(!card(None, luid(1)).is_same_card(&card(None, luid(2))));
}
#[test]
fn a_card_filled_from_another_runtime_takes_the_registry_entry_id_it_left_out() {
let registry_entry_id = |id| Some(RegistryEntryId::new(id));
let card = |registry_entry_id| PhysicalDevice {
pci_address: None,
luid: None,
registry_entry_id,
vendor: None,
};
let mut without_one = card(None);
without_one.fill_from(&card(registry_entry_id(0x1_0000_04c8)));
let mut with_one = card(registry_entry_id(0x1_0000_04c8));
with_one.fill_from(&card(registry_entry_id(0x1_0000_04c9)));
assert_eq!(
without_one.registry_entry_id,
registry_entry_id(0x1_0000_04c8)
);
assert_eq!(with_one.registry_entry_id, registry_entry_id(0x1_0000_04c8));
}
#[test]
fn a_pci_address_round_trips_and_defaults_its_domain() {
let id = PciAddress {
domain: 0,
bus: 7,
device: 0,
function: 0,
};
assert_eq!(id.to_string(), "0000:07:00.0");
assert_eq!("0000:07:00.0".parse::<PciAddress>(), Ok(id));
assert_eq!("07:00.0".parse::<PciAddress>(), Ok(id));
assert_eq!(
"0001:a3:1f.7".parse::<PciAddress>(),
Ok(PciAddress {
domain: 1,
bus: 0xa3,
device: 0x1f,
function: 7
})
);
assert!("07:00".parse::<PciAddress>().is_err());
assert!("gpu".parse::<PciAddress>().is_err());
}
const AVX2: (u32, u32) = (256, 16);
const AVX512: (u32, u32) = (512, 32);
const NEON: (u32, u32) = (128, 32);
fn hardware(load_width: u32, vector_register_count: Option<u32>) -> HardwareProperties {
HardwareProperties {
load_width,
vector_register_count,
plane_size_min: 1,
plane_size_max: 1,
max_bindings: u32::MAX,
max_shared_memory_size: 32 * 1024,
max_cube_count: (u32::MAX, u32::MAX, u32::MAX),
max_units_per_cube: 16,
max_cube_dim: (16, 16, 16),
num_streaming_multiprocessors: None,
num_cpu_cores: Some(16),
last_level_cache_size: None,
num_tensor_cores: None,
min_tensor_cores_dim: None,
max_vector_size: VectorSize::MAX,
cube_mma_reserved_shared_memory: 0,
}
}
fn registers((width, count): (u32, u32), elem_size: usize) -> VectorRegisters {
VectorRegisters::new(&hardware(width, Some(count)), elem_size).unwrap()
}
fn device(load_width: u32) -> DeviceProperties {
DeviceProperties::new(
Features::default(),
MemoryDeviceProperties::new(1024, 32),
hardware(load_width, None),
TimingMethod::System,
DeviceIdentity::default(),
)
}
}