#[cfg(all(
feature = "std",
any(target_arch = "x86", target_arch = "x86_64"),
not(miri)
))]
mod cpuid;
#[cfg(all(feature = "std", target_os = "macos", not(miri)))]
mod sysctl;
#[cfg(all(feature = "std", target_os = "linux", not(miri)))]
mod sysfs;
#[cfg(feature = "std")]
use std::sync::OnceLock;
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct Level {
pub bytes: usize,
pub assoc: usize,
pub line: usize,
pub shared_by: usize,
}
impl Level {
#[inline]
pub fn effective_bytes(&self) -> usize {
self.bytes / self.shared_by.max(1)
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct CacheTopology {
pub l1d: Level,
pub l2: Level,
pub l3: Option<Level>,
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct Blocking {
pub mc: usize,
pub kc: usize,
pub nc: usize,
}
pub const ZEN5_FALLBACK: CacheTopology = CacheTopology {
l1d: Level {
bytes: 48 * 1024,
assoc: 12,
line: 64,
shared_by: 1,
},
l2: Level {
bytes: 1024 * 1024,
assoc: 16,
line: 64,
shared_by: 1,
},
l3: Some(Level {
bytes: 32 * 1024 * 1024,
assoc: 16,
line: 64,
shared_by: 1,
}),
};
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct Machine {
pub cache: CacheTopology,
pub page_size: usize,
}
#[cfg(feature = "std")]
static MACHINE: OnceLock<Machine> = OnceLock::new();
impl Machine {
#[cfg(feature = "std")]
pub fn current() -> &'static Machine {
MACHINE.get_or_init(|| Machine {
cache: detect(),
page_size: detect_page_size(),
})
}
#[cfg(not(feature = "std"))]
pub fn current() -> &'static Machine {
static FALLBACK: Machine = Machine {
cache: ZEN5_FALLBACK,
page_size: 4096,
};
&FALLBACK
}
}
pub fn topology() -> &'static CacheTopology {
&Machine::current().cache
}
#[cfg(all(unix, feature = "std", not(miri)))]
fn detect_page_size() -> usize {
unsafe extern "C" {
fn getpagesize() -> core::ffi::c_int;
}
let p = unsafe { getpagesize() } as usize;
if p.is_power_of_two() && (4096..=2 * 1024 * 1024).contains(&p) {
p
} else {
4096
}
}
#[cfg(all(feature = "std", not(all(unix, not(miri)))))]
fn detect_page_size() -> usize {
4096
}
pub(crate) fn page_size() -> usize {
Machine::current().page_size
}
pub(crate) fn lhs_pack_stride_bytes() -> usize {
match crate::tuning::lhs_pack_stride() {
0 => page_size() / 2,
v => v,
}
}
pub(crate) fn lhs_pack_span_bytes() -> usize {
match crate::tuning::lhs_pack_span() {
0 => 4 << 20,
v => v,
}
}
#[cfg(feature = "parallel")]
pub(crate) fn gemv_parallel_floor_bytes() -> usize {
match crate::tuning::gemv_parallel_bytes() {
0 => {
let t = topology();
#[cfg(target_arch = "aarch64")]
const NO_L3_DIV: usize = 8;
#[cfg(not(target_arch = "aarch64"))]
const NO_L3_DIV: usize = 2;
match t.l3 {
Some(l3) => (l3.effective_bytes() / 2).max(1),
None => (t.l2.bytes / NO_L3_DIV).max(1),
}
}
v => v,
}
}
pub(crate) fn gemv_regblock_engage_bytes() -> usize {
let t = topology();
match t.l3 {
Some(l3) => l3.effective_bytes().max(1),
None => t.l2.effective_bytes().max(1),
}
}
#[cfg(any(test, feature = "half"))]
pub(crate) fn deep_k_engage_bytes() -> usize {
match crate::tuning::deep_kc_bytes() {
0 => (topology().l2.effective_bytes() / 2).max(1),
v => v,
}
}
pub(crate) fn prefetch_ws_bytes() -> usize {
match crate::tuning::prefetch_min_bytes() {
0 => {
let t = topology();
match t.l3 {
Some(l3) => l3.effective_bytes().max(1),
None => t.l2.effective_bytes().max(1),
}
}
v => v,
}
}
#[cfg(feature = "std")]
fn detect() -> CacheTopology {
#[cfg(all(any(target_arch = "x86", target_arch = "x86_64"), not(miri)))]
if let Some(t) = cpuid::detect().filter(plausible) {
return t;
}
#[cfg(all(target_os = "linux", not(miri)))]
if let Some(t) = sysfs::detect().filter(plausible) {
return t;
}
#[cfg(all(target_os = "macos", not(miri)))]
if let Some(t) = sysctl::detect().filter(plausible) {
return t;
}
ZEN5_FALLBACK
}
#[cfg(feature = "std")]
#[cfg_attr(any(target_family = "wasm", miri), allow(dead_code))]
fn plausible(t: &CacheTopology) -> bool {
let ok = |l: &Level| l.bytes >= 4 * 1024 && l.line >= 16 && l.assoc >= 1;
ok(&t.l1d) && ok(&t.l2) && t.l3.as_ref().map(ok).unwrap_or(true)
}
#[inline]
fn gcd(mut a: usize, mut b: usize) -> usize {
while b != 0 {
let t = a % b;
a = b;
b = t;
}
a.max(1)
}
#[inline]
fn round_down(a: usize, b: usize) -> usize {
(a / b) * b
}
impl CacheTopology {
pub fn blocking(
&self,
mr: usize,
nr: usize,
sizeof: usize,
m: usize,
n: usize,
k: usize,
) -> Blocking {
if m == 0 || n == 0 || k == 0 {
return Blocking {
mc: m.max(mr),
kc: k.max(1),
nc: n.max(nr),
};
}
let tiny_dim = crate::tuning::tiny_block_dim();
let kc_cap = crate::tuning::kc();
let kc_floor = crate::tuning::kc_min();
let mc_panels = crate::tuning::mc_reg_panels();
let nc_panels = crate::tuning::nc_no_l3_panels();
let l1 = self.l1d.effective_bytes().max(32 * 1024);
let l2 = self.l2.effective_bytes();
let l3 = self.l3.map(|l| l.effective_bytes()).unwrap_or(0);
let line = self.l1d.line.max(64);
let l1_assoc = self.l1d.assoc.max(2);
let l2_assoc = self.l2.assoc.max(2);
let l3_assoc = self.l3.map(|l| l.assoc).unwrap_or(2).max(2);
let l1_n_sets = (l1 / (line * l1_assoc)).max(1);
if m <= tiny_dim && n <= tiny_dim {
let kc = k.clamp(1, kc_cap);
let mc = ((l2 / sizeof / kc) / mr * mr)
.min(m.next_multiple_of(mr))
.max(mr);
let nc = n.next_multiple_of(nr).max(nr);
return Blocking { mc, kc, nc };
}
let g = gcd(mr * sizeof, line * l1_n_sets);
let kc_0 = (line * l1_n_sets) / g;
let c_lhs = (mr * sizeof) / g;
let c_rhs = (nr * kc_0 * sizeof) / (line * l1_n_sets);
let kc_mult = (l1_assoc / (c_lhs + c_rhs).max(1)).max(1);
let mut kc = (kc_0 * kc_mult.next_power_of_two()).max(kc_floor).min(k);
let k_iter = k.div_ceil(kc).max(1);
kc = k.div_ceil(k_iter).max(1);
let rhs_micropanel = nr * kc * sizeof;
let rhs_l2_assoc = rhs_micropanel.div_ceil((l2 / l2_assoc).max(1));
let lhs_l2_assoc = l2_assoc.saturating_sub(1 + rhs_l2_assoc).max(1);
let mc_from = (lhs_l2_assoc * l2) / (l2_assoc * sizeof * kc).max(1);
let mut mc = round_down(mc_from, mr).max(mr);
let m_iter = m.div_ceil(mc).max(1);
mc = (m.div_ceil(m_iter.saturating_mul(mr).max(1)) * mr).max(mr);
mc = mc.min(mc_panels.saturating_mul(mr));
let nc = if l3 == 0 {
nc_panels
.saturating_mul(nr)
.min(n.next_multiple_of(nr))
.max(nr)
} else {
let rhs_l3_assoc = l3_assoc.saturating_sub(1).max(1);
let rhs_macro_max = (rhs_l3_assoc * l3) / l3_assoc;
let mut nc = round_down(rhs_macro_max / (sizeof * kc).max(1), nr).max(nr);
let n_iter = n.div_ceil(nc).max(1);
nc = (n.div_ceil(n_iter * nr) * nr).max(nr);
nc
};
Blocking { mc, kc, nc }
}
}
#[cfg(all(test, feature = "std"))]
mod tests {
use super::*;
#[test]
fn machine_aggregates_and_memoizes() {
let m = Machine::current();
assert!(
core::ptr::eq(m, Machine::current()),
"current() must return the one memoized instance"
);
assert_eq!(&m.cache, topology(), "topology() must read through Machine");
assert_eq!(
m.page_size,
page_size(),
"page_size() must read through Machine"
);
}
#[test]
fn page_size_is_plausible() {
let p = page_size();
assert!(p.is_power_of_two(), "page size {p} is not a power of two");
assert!(
(4096..=2 * 1024 * 1024).contains(&p),
"page size {p} out of range"
);
}
#[test]
fn lhs_pack_stride_gate_auto_and_override() {
crate::tuning::set_lhs_pack_stride(0);
let auto = lhs_pack_stride_bytes();
assert_eq!(auto, page_size() / 2, "auto gate must be half the page");
assert!(auto > 0, "auto gate must be non-zero");
crate::tuning::set_lhs_pack_stride(4096);
assert_eq!(lhs_pack_stride_bytes(), 4096, "override must pass through");
crate::tuning::set_lhs_pack_stride(0);
}
#[test]
fn deep_k_engage_gate_auto_and_override() {
let restore = crate::tuning::deep_kc_bytes();
crate::tuning::set_deep_kc_bytes(0);
let auto = deep_k_engage_bytes();
assert_eq!(
auto,
(topology().l2.effective_bytes() / 2).max(1),
"auto gate must be half the L2 effective bytes"
);
assert!(auto > 0, "auto gate must be non-zero");
crate::tuning::set_deep_kc_bytes(4096);
assert_eq!(deep_k_engage_bytes(), 4096, "override must pass through");
crate::tuning::set_deep_kc_bytes(restore);
}
#[test]
fn prefetch_ws_gate_auto_and_override() {
let restore = crate::tuning::prefetch_min_bytes();
crate::tuning::set_prefetch_min_bytes(0);
let auto = prefetch_ws_bytes();
let t = topology();
let expect = match t.l3 {
Some(l3) => l3.effective_bytes().max(1),
None => t.l2.effective_bytes().max(1),
};
assert_eq!(auto, expect, "auto gate must be the per-core-reachable LLC");
assert!(auto > 0, "auto gate must be non-zero");
crate::tuning::set_prefetch_min_bytes(4096);
assert_eq!(prefetch_ws_bytes(), 4096, "override must pass through");
crate::tuning::set_prefetch_min_bytes(restore);
}
#[test]
fn blocking_zero_dim_early_return() {
let t = topology();
let (mr, nr) = (16usize, 4usize);
let b = t.blocking(mr, nr, 4, 0, 8, 8);
assert_eq!((b.mc, b.kc, b.nc), (mr, 8, 8));
let b = t.blocking(mr, nr, 4, 8, 0, 8);
assert_eq!((b.mc, b.kc, b.nc), (16, 8, nr));
let b = t.blocking(mr, nr, 4, 8, 8, 0);
assert_eq!((b.mc, b.kc, b.nc), (16, 1, 8));
}
#[test]
fn blocking_no_l3_nc_arm() {
let topo = CacheTopology {
l1d: Level {
bytes: 48 * 1024,
assoc: 12,
line: 64,
shared_by: 1,
},
l2: Level {
bytes: 1024 * 1024,
assoc: 16,
line: 64,
shared_by: 1,
},
l3: None,
};
let (mr, nr) = (16usize, 4usize);
let (m, n, k) = (512usize, 512usize, 512usize); let b = topo.blocking(mr, nr, 4, m, n, k);
let expect_nc = (crate::tuning::nc_no_l3_panels() * nr)
.min(n.next_multiple_of(nr))
.max(nr);
assert_eq!(b.nc, expect_nc, "no-L3 NC must use the panel-count cap");
assert!(
b.mc >= mr && b.mc.is_multiple_of(mr),
"mc must be a positive mr multiple"
);
assert!(b.kc >= 1 && b.kc <= k, "kc must be within [1, k]");
}
}