use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Capabilities {
pub supports_fp16: bool,
pub supports_bf16: bool,
pub supports_fp8: bool,
pub tensor_cores: bool,
pub peer_access: bool,
pub unified_memory: bool,
pub cluster_launch: bool,
pub async_copy: bool,
pub max_threads_per_block: u32,
pub max_shared_mem_per_block: u32,
pub warp_size: u32,
}
impl Capabilities {
#[must_use]
pub const fn cpu() -> Self {
Self {
supports_fp16: false,
supports_bf16: false,
supports_fp8: false,
tensor_cores: false,
peer_access: false,
unified_memory: true, cluster_launch: false,
async_copy: false,
max_threads_per_block: 1024,
max_shared_mem_per_block: 48 * 1024,
warp_size: 1,
}
}
#[must_use]
pub const fn supports_mixed_precision(&self) -> bool {
self.supports_fp16 || self.supports_bf16 || self.supports_fp8
}
#[must_use]
pub const fn can_use_tensor_cores(&self, m: usize, n: usize, k: usize) -> bool {
self.tensor_cores && m % 16 == 0 && n % 16 == 0 && k % 16 == 0
}
}
impl Default for Capabilities {
fn default() -> Self {
Self::cpu()
}
}
impl fmt::Display for Capabilities {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"fp16={} bf16={} fp8={} tensor_cores={} peer={} unified={} cluster={} async_copy={} \
max_threads/block={} max_smem/block={}B warp={}",
self.supports_fp16,
self.supports_bf16,
self.supports_fp8,
self.tensor_cores,
self.peer_access,
self.unified_memory,
self.cluster_launch,
self.async_copy,
self.max_threads_per_block,
self.max_shared_mem_per_block,
self.warp_size,
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TileShape {
pub tile_m: usize,
pub tile_n: usize,
pub tile_k: usize,
}
impl TileShape {
#[must_use]
pub const fn new(tile_m: usize, tile_n: usize, tile_k: usize) -> Self {
Self {
tile_m,
tile_n,
tile_k,
}
}
#[must_use]
pub const fn output_elems(&self) -> usize {
self.tile_m * self.tile_n
}
}
impl fmt::Display for TileShape {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}x{}x{}", self.tile_m, self.tile_n, self.tile_k)
}
}
#[must_use]
pub fn default_tile_for(m: usize, n: usize, k: usize, caps: &Capabilities) -> TileShape {
let smallest = m.min(n).min(k);
if caps.tensor_cores && m % 16 == 0 && n % 16 == 0 && k % 16 == 0 {
return if smallest >= 512 {
TileShape::new(128, 128, 32)
} else {
TileShape::new(64, 64, 16)
};
}
if smallest <= 64 {
TileShape::new(16, 16, 16)
} else if smallest <= 512 {
TileShape::new(64, 64, 16)
} else {
TileShape::new(128, 128, 8)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum MemoryKind {
Device,
HostPinned,
Unified,
Host,
}
impl fmt::Display for MemoryKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Device => write!(f, "device"),
Self::HostPinned => write!(f, "host-pinned"),
Self::Unified => write!(f, "unified"),
Self::Host => write!(f, "host"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DeviceInfo {
pub ordinal: usize,
pub name: String,
pub compute_capability: (u32, u32),
pub total_memory_bytes: u64,
pub memory_kind: MemoryKind,
pub capabilities: Capabilities,
}
impl DeviceInfo {
#[must_use]
pub fn cpu_reference(total_memory_bytes: u64) -> Self {
Self {
ordinal: 0,
name: "CPU (reference)".to_string(),
compute_capability: (0, 0),
total_memory_bytes,
memory_kind: MemoryKind::Host,
capabilities: Capabilities::cpu(),
}
}
}
impl fmt::Display for DeviceInfo {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mem_mb = self.total_memory_bytes / (1024 * 1024);
let (major, minor) = self.compute_capability;
write!(
f,
"Device[{}] {} (cc {major}.{minor}, {mem_mb} MB, {})",
self.ordinal, self.name, self.memory_kind,
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cpu_capabilities_are_conservative() {
let caps = Capabilities::cpu();
assert!(!caps.supports_fp16);
assert!(!caps.supports_bf16);
assert!(!caps.supports_fp8);
assert!(!caps.tensor_cores);
assert!(!caps.peer_access);
assert!(caps.unified_memory);
assert!(!caps.supports_mixed_precision());
assert_eq!(Capabilities::default(), Capabilities::cpu());
}
#[test]
fn mixed_precision_detection() {
let mut caps = Capabilities::cpu();
assert!(!caps.supports_mixed_precision());
caps.supports_bf16 = true;
assert!(caps.supports_mixed_precision());
}
#[test]
fn tensor_core_eligibility_requires_alignment_and_units() {
let mut caps = Capabilities::cpu();
assert!(!caps.can_use_tensor_cores(256, 256, 256));
caps.tensor_cores = true;
assert!(caps.can_use_tensor_cores(256, 256, 256));
assert!(!caps.can_use_tensor_cores(256, 256, 255));
}
#[test]
fn tile_heuristic_scales_with_problem_size() {
let caps = Capabilities::cpu();
assert_eq!(
default_tile_for(32, 32, 32, &caps),
TileShape::new(16, 16, 16)
);
assert_eq!(
default_tile_for(256, 256, 256, &caps),
TileShape::new(64, 64, 16)
);
assert_eq!(
default_tile_for(4096, 4096, 4096, &caps),
TileShape::new(128, 128, 8)
);
}
#[test]
fn tile_heuristic_snaps_to_wmma_when_tensor_cores() {
let mut caps = Capabilities::cpu();
caps.tensor_cores = true;
let t = default_tile_for(1024, 1024, 1024, &caps);
assert_eq!(t, TileShape::new(128, 128, 32));
assert_eq!(t.tile_m % 16, 0);
assert_eq!(t.tile_n % 16, 0);
assert_eq!(t.tile_k % 16, 0);
assert_eq!(
default_tile_for(64, 64, 64, &caps),
TileShape::new(64, 64, 16)
);
}
#[test]
fn tile_output_elems() {
assert_eq!(TileShape::new(128, 64, 8).output_elems(), 128 * 64);
}
#[test]
fn memory_kind_display() {
assert_eq!(MemoryKind::Device.to_string(), "device");
assert_eq!(MemoryKind::HostPinned.to_string(), "host-pinned");
assert_eq!(MemoryKind::Unified.to_string(), "unified");
assert_eq!(MemoryKind::Host.to_string(), "host");
}
#[test]
fn device_info_cpu_reference() {
let dev = DeviceInfo::cpu_reference(8 * 1024 * 1024 * 1024);
assert_eq!(dev.ordinal, 0);
assert_eq!(dev.compute_capability, (0, 0));
assert_eq!(dev.memory_kind, MemoryKind::Host);
assert_eq!(dev.capabilities, Capabilities::cpu());
assert!(dev.to_string().contains("CPU (reference)"));
assert!(dev.to_string().contains("8192 MB"));
}
#[test]
fn capabilities_display_round_trips_fields() {
let s = Capabilities::cpu().to_string();
assert!(s.contains("unified=true"));
assert!(s.contains("tensor_cores=false"));
assert!(s.contains("warp=1"));
}
}