use core::cmp::Reverse;
use std::sync::Arc;
use cubecl::tile::launch::Refusal;
use cubecl::{
CubeDim,
client::Client,
features::{AtomicUsage, MmaConfig, Plane, ScaledMmaConfig, Tma},
ir::{DeviceProperties, ElemType, FloatKind, OpaqueType, TargetProperties, Type},
};
#[derive(Clone, Debug)]
pub struct Hardware {
pub properties: Arc<DeviceProperties>,
pub accumulator_as_a: bool,
}
pub const GPU_TRANSACTION_BYTES: usize = 128;
pub const CPU_CACHE_LINE_BYTES: usize = 64;
impl Hardware {
pub fn new(properties: Arc<DeviceProperties>, target: &TargetProperties) -> Self {
let mma = &target.mma;
Self {
properties,
accumulator_as_a: mma.register_layout_a == mma.register_layout_acc
&& mma.register_duplication_a == mma.register_duplication_acc,
}
}
pub fn from_client(client: &Client) -> Self {
Self::new(client.properties_shared(), client.target_properties())
}
pub fn plane_dim(&self) -> Result<u32, Refusal> {
let hardware = &self.properties.hardware;
if !self.properties.features.plane.contains(Plane::Ops) {
return Err(Refusal::PlanesUnavailable);
}
if hardware.plane_size_min != hardware.plane_size_max {
return Err(Refusal::ImprecisePlaneDim {
min: hardware.plane_size_min,
max: hardware.plane_size_max,
});
}
Ok(hardware.plane_size_max)
}
pub fn plane_size(&self) -> Result<u32, Refusal> {
let hardware = &self.properties.hardware;
let single_unit = !self.properties.features.plane.contains(Plane::Ops)
&& hardware.plane_size_min == 1
&& hardware.plane_size_max == 1;
match single_unit {
true => Ok(1),
false => self.plane_dim(),
}
}
pub fn dispatch_plane_size(&self) -> u32 {
self.properties.hardware.plane_size_max.max(1)
}
pub fn full_cube_planes(&self) -> usize {
(self.properties.hardware.max_units_per_cube / self.dispatch_plane_size()).max(1) as usize
}
pub fn cube_dim_1d(&self, units: u32) -> CubeDim {
CubeDim::new_1d(
units
.min(self.properties.hardware.max_units_per_cube)
.max(1),
)
}
pub fn shared_memory_budget(&self) -> usize {
let hardware = &self.properties.hardware;
hardware
.max_shared_memory_size
.saturating_sub(hardware.cube_mma_reserved_shared_memory)
}
pub fn workers(&self) -> Option<u32> {
let hardware = &self.properties.hardware;
hardware
.num_cpu_cores
.or(hardware.num_streaming_multiprocessors)
}
pub fn units_to_fill(&self) -> Option<u32> {
const UNITS_PER_GPU_WORKER: u32 = 512;
let hardware = &self.properties.hardware;
match hardware.num_cpu_cores {
Some(cores) => Some(cores.max(1)),
None => Some(
hardware
.num_streaming_multiprocessors?
.saturating_mul(UNITS_PER_GPU_WORKER)
.max(1),
),
}
}
pub fn cubes_to_fill(&self, units_per_cube: u32) -> Option<u32> {
Some((self.units_to_fill()? / units_per_cube.max(1)).max(1))
}
pub fn load_values(&self, elem: ElemType) -> usize {
(self.properties.hardware.load_width as usize / elem.size_bits().max(1)).max(1)
}
pub fn vector_ceiling(&self, elem: ElemType) -> usize {
(self.properties.io_width() as usize / elem.size_bits().max(1))
.max(1)
.min(self.properties.hardware.max_vector_size.max(1))
}
pub fn transaction_bytes(&self) -> usize {
match self.properties.hardware.num_cpu_cores {
Some(_) => CPU_CACHE_LINE_BYTES,
None => GPU_TRANSACTION_BYTES,
}
}
pub fn staggered_pitch(&self, run: usize, elem: ElemType) -> usize {
let elem_bits = elem.size_bits().max(1);
let transaction_bits = self.transaction_bytes() * 8;
let transactions = (run * elem_bits).div_ceil(transaction_bits).max(1);
let odd = match transactions.is_multiple_of(2) {
true => transactions + 1,
false => transactions,
};
odd * transaction_bits / elem_bits
}
pub fn plane_count_per_cube(&self, working_units: usize, plane_dim: u32) -> u32 {
const NUM_PLANE_MAX: u32 = 8;
let hardware = &self.properties.hardware;
let plane_count = match hardware.num_cpu_cores {
Some(num_cores) => num_cores.min(working_units as u32),
None => {
let plane_count_max = (working_units / plane_dim as usize).max(1);
2u32.pow(NUM_PLANE_MAX.ilog2().min(plane_count_max.ilog2()))
}
};
let max_plane_per_cube = (hardware.max_units_per_cube / plane_dim).max(1);
plane_count.min(max_plane_per_cube).max(1)
}
pub fn strip_budget(&self, plane_size: usize) -> usize {
let hardware = &self.properties.hardware;
let cube = hardware.max_units_per_cube as usize / plane_size.max(1);
cube.min(hardware.max_cube_dim.1 as usize).max(1)
}
pub fn shared_f32_atomic_add(&self) -> bool {
self.properties
.shared_atomic_type_usage(Type::atomic(ElemType::Float(FloatKind::F32)))
.contains(AtomicUsage::Add)
}
pub fn bulk_tensor_copy(&self) -> bool {
self.properties.features.tma.contains(Tma::Base) && self.barrier_ops()
}
pub fn unit_async_copy(&self) -> bool {
self.properties.features.copy_async && self.barrier_ops()
}
pub fn barrier_ops(&self) -> bool {
self.properties
.features
.types
.opaque
.contains(&OpaqueType::Barrier)
}
}
impl From<&Client> for Hardware {
fn from(client: &Client) -> Self {
Self::new(client.properties_shared(), client.target_properties())
}
}
pub trait Instructions: Iterator<Item = MmaConfig> + Clone {
fn for_triple(self, lhs: ElemType, rhs: ElemType, acc: ElemType) -> impl Instructions {
self.filter(move |c| c.a_type == lhs && c.b_type == rhs && c.cd_type == acc)
}
fn for_attention(self, elem: ElemType) -> impl Instructions {
self.for_triple(elem, elem, ElemType::Float(FloatKind::F32))
}
fn largest(self) -> Option<MmaConfig> {
self.max_by_key(|shape| (shape.m as usize * shape.n as usize, shape.k))
}
fn largest_first(self) -> impl Iterator<Item = MmaConfig> {
let first = self.clone().min_by_key(size_rank);
core::iter::successors(first, move |last| {
self.clone()
.filter(|shape| size_rank(shape) > size_rank(last))
.min_by_key(size_rank)
})
}
fn deepest(self) -> impl Instructions {
let offered = self.clone();
self.filter(move |shape| {
!offered.clone().any(|other| {
other.k > shape.k
&& MmaConfig {
k: shape.k,
..other
} == *shape
})
})
}
}
impl<I: Iterator<Item = MmaConfig> + Clone> Instructions for I {}
#[allow(clippy::type_complexity)]
fn size_rank(shape: &MmaConfig) -> (Reverse<usize>, usize, u32, u32, u32, MmaConfig) {
let (m, n, k) = (shape.m as usize, shape.n as usize, shape.k as usize);
(
Reverse(m * n * k),
m.abs_diff(n),
shape.m,
shape.n,
shape.k,
*shape,
)
}
pub fn manual_mma(hw: &Hardware) -> impl Instructions + '_ {
let matmul = &hw.properties.features.matmul;
let e2m1 = ElemType::Float(FloatKind::E2M1);
matmul
.mma
.iter()
.copied()
.filter(move |config| config.a_type != e2m1 && config.b_type != e2m1)
.chain(matmul.scaled_mma.iter().filter_map(nvfp4_instruction))
}
pub fn block_scaled_nvfp4(hw: &Hardware) -> bool {
let e2m1 = ElemType::Float(FloatKind::E2M1);
manual_mma(hw)
.for_triple(e2m1, e2m1, ElemType::Float(FloatKind::F32))
.next()
.is_some()
}
fn nvfp4_instruction(config: &ScaledMmaConfig) -> Option<MmaConfig> {
let fp4 = ElemType::Float(FloatKind::E2M1x2);
let nvfp4 = config.a_type == fp4
&& config.b_type == fp4
&& config.scales_type == ElemType::Float(FloatKind::E4M3)
&& config.scales_factor as usize * NVFP4_BLOCK == config.k as usize;
nvfp4.then_some(MmaConfig {
a_type: ElemType::Float(FloatKind::E2M1),
b_type: ElemType::Float(FloatKind::E2M1),
cd_type: config.cd_type,
m: config.m,
n: config.n,
k: config.k,
})
}
pub const TMA_ALIGNMENT_BYTES: usize = 16;
pub const TMA_ROW_BYTES: usize = 64;
pub const UNIT_REGISTERS: u32 = 255;
pub const NVFP4_BLOCK: usize = 16;
#[cfg(test)]
mod tests {
use super::*;
use crate::devices;
use cubecl::{
features::Features,
ir::{DeviceIdentity, HardwareProperties, MemoryDeviceProperties},
};
use cubecl_common::profile::TimingMethod;
fn device(
plane_size_min: u32,
plane_size_max: u32,
plane_ops: bool,
num_cpu_cores: Option<u32>,
) -> Hardware {
let mut features = Features::default();
if plane_ops {
features.plane.insert(Plane::Ops);
}
let hardware = HardwareProperties {
load_width: 128,
vector_register_count: None,
plane_size_min,
plane_size_max,
max_bindings: 8,
max_shared_memory_size: 64 * 1024,
max_cube_count: (u32::MAX, u32::MAX, u32::MAX),
max_units_per_cube: 1024,
max_cube_dim: (1024, 1024, 1024),
num_streaming_multiprocessors: num_cpu_cores.is_none().then_some(19),
num_cpu_cores,
last_level_cache_size: None,
num_tensor_cores: None,
min_tensor_cores_dim: None,
max_vector_size: 4,
cube_mma_reserved_shared_memory: 0,
};
let identity = DeviceIdentity {
name: "fixture".into(),
fingerprint: "fixture".into(),
physical: None,
};
Hardware {
properties: Arc::new(DeviceProperties::new(
features,
MemoryDeviceProperties::new(u64::MAX, 4),
hardware,
TimingMethod::System,
identity,
)),
accumulator_as_a: false,
}
}
fn shape(m: u32, n: u32, k: u32) -> MmaConfig {
let f16 = ElemType::Float(FloatKind::F16);
MmaConfig {
a_type: f16,
b_type: f16,
cd_type: ElemType::Float(FloatKind::F32),
m,
n,
k,
}
}
#[test]
fn a_staggered_pitch_is_an_odd_count_of_transactions() {
let f16 = ElemType::Float(FloatKind::F16);
let f32 = ElemType::Float(FloatKind::F32);
let gpu = device(32, 32, true, None);
assert_eq!(gpu.staggered_pitch(4096, f16), 4096 + 64);
assert_eq!(gpu.staggered_pitch(12288, f16), 12288 + 64);
assert_eq!(gpu.staggered_pitch(4096, f32), 4096 + 32);
assert_eq!(gpu.staggered_pitch(4160, f16), 4160);
assert_eq!(gpu.staggered_pitch(100, f16), 192);
assert_eq!(gpu.staggered_pitch(8, f16), 64);
let cpu = device(1, 1, false, Some(12));
assert_eq!(cpu.staggered_pitch(4096, f16), 4096 + 32);
for run in [1, 63, 64, 65, 1000, 4096, 12288] {
let pitch = gpu.staggered_pitch(run, f16);
let transactions = pitch * 2 / gpu.transaction_bytes();
assert!(pitch >= run && transactions % 2 == 1, "{run} -> {pitch}");
}
}
#[test]
fn only_a_committed_plane_width_is_a_width() {
assert_eq!(device(32, 32, true, None).plane_dim(), Ok(32));
assert_eq!(device(1, 1, false, Some(12)).plane_size(), Ok(1));
assert_eq!(
device(8, 128, false, None).plane_dim(),
Err(Refusal::PlanesUnavailable)
);
assert_eq!(
device(32, 64, true, None).plane_dim(),
Err(Refusal::ImprecisePlaneDim { min: 32, max: 64 })
);
}
#[test]
fn plane_count_never_collapses_to_zero() {
let mut narrow = device(8, 128, false, None);
Arc::make_mut(&mut narrow.properties)
.hardware
.max_units_per_cube = 64;
assert!(narrow.plane_count_per_cube(1024, 128) >= 1);
}
#[test]
fn a_fill_counts_units_on_the_workers_reported() {
let mut unknown = device(32, 32, true, None);
Arc::make_mut(&mut unknown.properties)
.hardware
.num_streaming_multiprocessors = None;
assert_eq!(unknown.units_to_fill(), None);
assert_eq!(unknown.cubes_to_fill(32), None);
assert_eq!(device(1, 1, false, Some(12)).units_to_fill(), Some(12));
let gpu = device(32, 32, true, None);
assert_eq!(
gpu.cubes_to_fill(32).unwrap(),
gpu.cubes_to_fill(256).unwrap() * 8
);
}
#[test]
fn only_the_deepest_shape_of_a_tile_is_a_candidate() {
let bf16 = ElemType::Float(FloatKind::BF16);
let shallow_bf16 = MmaConfig {
a_type: bf16,
b_type: bf16,
..shape(16, 8, 8)
};
let offered = [shape(16, 8, 16), shape(16, 8, 8), shallow_bf16];
assert_eq!(
offered.into_iter().deepest().collect::<Vec<_>>(),
vec![shape(16, 8, 16), shallow_bf16]
);
}
#[test]
fn the_largest_shape_is_offered_first() {
let order = |offered: [MmaConfig; 3]| -> Vec<_> {
offered
.into_iter()
.largest_first()
.map(|shape| (shape.m, shape.n, shape.k))
.collect()
};
assert_eq!(
order([shape(16, 16, 16), shape(32, 32, 8), shape(16, 16, 32)]),
[(16, 16, 32), (32, 32, 8), (16, 16, 16)]
);
assert_eq!(
order([shape(8, 32, 16), shape(32, 8, 16), shape(16, 16, 16)]),
[(16, 16, 16), (8, 32, 16), (32, 8, 16)]
);
}
#[test]
fn a_block_scaled_instruction_is_offered_over_its_values() {
let e2m1 = ElemType::Float(FloatKind::E2M1);
let unscaled = MmaConfig {
a_type: e2m1,
b_type: e2m1,
cd_type: ElemType::Float(FloatKind::F32),
m: 16,
n: 8,
k: 32,
};
let hw = devices::offering_mma(devices::h100(), [unscaled]);
assert!(!block_scaled_nvfp4(&hw));
let hw = devices::offering_nvfp4(hw);
assert_eq!(
manual_mma(&hw).collect::<Vec<_>>(),
[MmaConfig { k: 64, ..unscaled }]
);
assert!(block_scaled_nvfp4(&hw));
}
}