use anyhow::Result;
use crate::scheduler::Scheduler;
const LAMBDA_ZERO_EPS: f64 = 1e-8;
const TAU_SCALE_NS: f64 = 1.6e8; const TAU_FLOOR_NS: u64 = 1_000_000; const TAU_CEIL_NS: u64 = 40_000_000;
const C_EQ_FLOOR_NS: u64 = 200_000; const C_EQ_CEIL_NS: u64 = 8_000_000;
#[derive(Clone, Copy, Debug)]
pub struct TopologySpectrum {
pub fiedler: f64, pub tau_ns: u64, pub codel_eq_ns: u64, }
fn extract_fiedler(eigenvalues: &[f64]) -> f64 {
let mut v: Vec<f64> = eigenvalues.to_vec();
v.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
v.into_iter()
.find(|&x| x > LAMBDA_ZERO_EPS)
.unwrap_or(LAMBDA_ZERO_EPS)
}
fn compute_tau_ns(fiedler: f64) -> u64 {
let raw = TAU_SCALE_NS / fiedler.max(LAMBDA_ZERO_EPS);
(raw as u64).clamp(TAU_FLOOR_NS, TAU_CEIL_NS)
}
fn compute_codel_eq_ns(eigenvalues: &[f64], n: usize, tau_ns: u64) -> u64 {
if n == 0 {
return TAU_FLOOR_NS;
}
let mut sum_inv_lambda = 0.0f64;
let mut sum_lambda = 0.0f64;
for &lambda in eigenvalues {
sum_lambda += lambda;
if lambda > LAMBDA_ZERO_EPS {
sum_inv_lambda += 1.0 / lambda;
}
}
let avg_reff = sum_inv_lambda / n as f64;
let two_m = sum_lambda;
let raw_ns = avg_reff * two_m * tau_ns as f64;
(raw_ns as u64).clamp(C_EQ_FLOOR_NS, C_EQ_CEIL_NS)
}
#[allow(dead_code)]
pub struct CpuTopology {
pub nr_cpus: usize,
pub l2_domain: Vec<u32>, pub l2_groups: Vec<Vec<u32>>, pub socket_domain: Vec<u32>, pub nr_sockets: u32,
}
impl CpuTopology {
pub fn detect(nr_cpus: usize) -> Result<Self> {
let mut l2_domain = vec![0u32; nr_cpus];
let mut seen_groups: Vec<Vec<u32>> = Vec::new();
for cpu in 0..nr_cpus {
let path = format!(
"/sys/devices/system/cpu/cpu{}/cache/index2/shared_cpu_list",
cpu
);
let content = match std::fs::read_to_string(&path) {
Ok(s) => s,
Err(_) => {
l2_domain[cpu] = cpu as u32;
continue;
}
};
let members = parse_cpu_list(content.trim());
let group_id = match seen_groups.iter().position(|g| *g == members) {
Some(id) => id as u32,
None => {
let id = seen_groups.len() as u32;
seen_groups.push(members.clone());
id
}
};
l2_domain[cpu] = group_id;
}
let mut socket_domain = vec![0u32; nr_cpus];
let mut seen_sockets: Vec<u32> = Vec::new();
for cpu in 0..nr_cpus {
let path = format!(
"/sys/devices/system/cpu/cpu{}/topology/physical_package_id",
cpu
);
let pkg_id = match std::fs::read_to_string(&path) {
Ok(s) => s.trim().parse::<u32>().unwrap_or(0),
Err(_) => 0,
};
if !seen_sockets.contains(&pkg_id) {
seen_sockets.push(pkg_id);
}
let socket_idx = seen_sockets.iter().position(|&s| s == pkg_id).unwrap() as u32;
socket_domain[cpu] = socket_idx;
}
let nr_sockets = seen_sockets.len() as u32;
Ok(Self {
nr_cpus,
l2_domain,
l2_groups: seen_groups,
socket_domain,
nr_sockets,
})
}
pub fn populate_bpf_map(&self, sched: &Scheduler) -> Result<()> {
for cpu in 0..self.nr_cpus {
sched.write_cache_domain(cpu as u32, self.l2_domain[cpu])?;
}
Ok(())
}
pub fn populate_l2_siblings_map(&self, sched: &Scheduler) -> Result<()> {
const MAX_L2_SIBLINGS: usize = 8;
for (gid, members) in self.l2_groups.iter().enumerate() {
for (slot, &cpu) in members.iter().enumerate().take(MAX_L2_SIBLINGS) {
sched.write_l2_sibling(gid as u32, slot as u32, cpu)?;
}
if members.len() < MAX_L2_SIBLINGS {
sched.write_l2_sibling(gid as u32, members.len() as u32, u32::MAX)?;
}
}
Ok(())
}
const CONDUCTANCE_L2: f64 = 10.0;
const CONDUCTANCE_SOCKET: f64 = 1.0;
const CONDUCTANCE_CROSS: f64 = 0.3;
fn build_laplacian(&self) -> Vec<f64> {
let n = self.nr_cpus;
let mut l = vec![0.0f64; n * n];
for i in 0..n {
for j in (i + 1)..n {
let w = if self.l2_domain[i] == self.l2_domain[j] {
Self::CONDUCTANCE_L2
} else if self.socket_domain[i] == self.socket_domain[j] {
Self::CONDUCTANCE_SOCKET
} else {
Self::CONDUCTANCE_CROSS
};
l[i * n + j] = -w;
l[j * n + i] = -w;
l[i * n + i] += w;
l[j * n + j] += w;
}
}
l
}
fn symmetric_eigen(mat: &[f64], n: usize) -> (Vec<f64>, Vec<f64>) {
let mut a = mat.to_vec();
let mut v = vec![0.0f64; n * n];
for i in 0..n {
v[i * n + i] = 1.0;
}
let max_iter = 100 * n * n;
for _ in 0..max_iter {
let mut max_val = 0.0f64;
let mut p = 0;
let mut q = 1;
for i in 0..n {
for j in (i + 1)..n {
let val = a[i * n + j].abs();
if val > max_val {
max_val = val;
p = i;
q = j;
}
}
}
if max_val < 1e-12 {
break;
}
let app = a[p * n + p];
let aqq = a[q * n + q];
let apq = a[p * n + q];
let theta = if (app - aqq).abs() < 1e-15 {
std::f64::consts::FRAC_PI_4
} else {
0.5 * (2.0 * apq / (app - aqq)).atan()
};
let c = theta.cos();
let s = theta.sin();
for i in 0..n {
if i == p || i == q {
continue;
}
let aip = a[i * n + p];
let aiq = a[i * n + q];
a[i * n + p] = c * aip + s * aiq;
a[p * n + i] = a[i * n + p];
a[i * n + q] = -s * aip + c * aiq;
a[q * n + i] = a[i * n + q];
}
let new_pp = c * c * app + 2.0 * s * c * apq + s * s * aqq;
let new_qq = s * s * app - 2.0 * s * c * apq + c * c * aqq;
a[p * n + p] = new_pp;
a[q * n + q] = new_qq;
a[p * n + q] = 0.0;
a[q * n + p] = 0.0;
for i in 0..n {
let vip = v[i * n + p];
let viq = v[i * n + q];
v[i * n + p] = c * vip + s * viq;
v[i * n + q] = -s * vip + c * viq;
}
}
let eigenvalues: Vec<f64> = (0..n).map(|i| a[i * n + i]).collect();
(eigenvalues, v)
}
fn compute_pseudoinverse(eigenvalues: &[f64], eigenvectors: &[f64], n: usize) -> Vec<f64> {
let mut l_pinv = vec![0.0f64; n * n];
for k in 0..n {
if eigenvalues[k].abs() < 1e-8 {
continue; }
let inv_lambda = 1.0 / eigenvalues[k];
for i in 0..n {
for j in 0..n {
l_pinv[i * n + j] +=
inv_lambda * eigenvectors[i * n + k] * eigenvectors[j * n + k];
}
}
}
l_pinv
}
fn extract_reff(l_pinv: &[f64], n: usize) -> Vec<f64> {
let mut r = vec![0.0f64; n * n];
for i in 0..n {
for j in (i + 1)..n {
let val = l_pinv[i * n + i] + l_pinv[j * n + j] - 2.0 * l_pinv[i * n + j];
r[i * n + j] = val.max(0.0);
r[j * n + i] = r[i * n + j];
}
}
r
}
fn build_affinity_rank(reff: &[f64], n: usize) -> Vec<u32> {
let mut rank = vec![0u32; n * n];
for cpu in 0..n {
let mut others: Vec<(u64, u32)> = (0..n)
.filter(|&c| c != cpu)
.map(|c| {
let key = (reff[cpu * n + c] * 1_000_000.0) as u64;
(key, c as u32)
})
.collect();
others.sort();
for (slot, &(_, target)) in others.iter().enumerate() {
rank[cpu * n + slot] = target;
}
for slot in others.len()..n {
rank[cpu * n + slot] = u32::MAX;
}
}
rank
}
pub fn compute_resistance_affinity(&self) -> (Vec<f64>, Vec<u32>, TopologySpectrum) {
let n = self.nr_cpus;
let laplacian = self.build_laplacian();
let (eigenvalues, eigenvectors) = Self::symmetric_eigen(&laplacian, n);
let fiedler = extract_fiedler(&eigenvalues);
let tau_ns = compute_tau_ns(fiedler);
let l_pinv = Self::compute_pseudoinverse(&eigenvalues, &eigenvectors, n);
let reff = Self::extract_reff(&l_pinv, n);
let rank = Self::build_affinity_rank(&reff, n);
let codel_eq_ns = compute_codel_eq_ns(&eigenvalues, n, tau_ns);
(
reff,
rank,
TopologySpectrum {
fiedler,
tau_ns,
codel_eq_ns,
},
)
}
pub fn populate_affinity_rank_map(&self, sched: &Scheduler, rank: &[u32]) -> Result<()> {
let stride = crate::bpf_intf::MAX_AFFINITY_CANDIDATES as usize;
let valid = self.nr_cpus.saturating_sub(1).min(stride);
for cpu in 0..self.nr_cpus {
for slot in 0..valid {
let val = rank[cpu * self.nr_cpus + slot];
sched.write_affinity_rank(cpu as u32, slot as u32, val)?;
}
for slot in valid..stride {
sched.write_affinity_rank(cpu as u32, slot as u32, u32::MAX)?;
}
}
Ok(())
}
pub fn log_resistance_affinity(&self, reff: &[f64], rank: &[u32], spectrum: TopologySpectrum) {
log_info!(
"TOPOLOGY SPECTRUM: lambda2={:.4} tau={}ms codel_eq={}us",
spectrum.fiedler,
spectrum.tau_ns / 1_000_000,
spectrum.codel_eq_ns / 1_000
);
let n = self.nr_cpus;
let mut parts = Vec::new();
for slot in 0..3.min(n - 1) {
let target = rank[slot] as usize;
if target >= n {
break;
}
let r = reff[target];
parts.push(format!("CPU{}(R={:.3})", target, r));
}
log_info!("RESISTANCE AFFINITY: CPU 0 rank: {}", parts.join(", "));
if n >= 2 {
let l2_sib = rank[0] as usize;
let non_l2 = rank[1.min(n - 2)] as usize;
log_info!(
"RESISTANCE AFFINITY: R_eff L2={:.4} non-L2={:.4} ratio={:.1}x",
reff[l2_sib],
reff[non_l2],
if reff[l2_sib] > 0.0 {
reff[non_l2] / reff[l2_sib]
} else {
0.0
}
);
}
}
pub fn log_summary(&self) {
for (gid, members) in self.l2_groups.iter().enumerate() {
let cpus: Vec<String> = members.iter().map(|c| c.to_string()).collect();
log_info!("L2 GROUP {}: [{}]", gid, cpus.join(","));
}
log_info!(
"L2 GROUPS: {} across {} CPUs, {} SOCKETS",
self.l2_groups.len(),
self.nr_cpus,
self.nr_sockets
);
}
}
fn parse_cpu_list(s: &str) -> Vec<u32> {
let mut result = Vec::new();
for part in s.split(',') {
let part = part.trim();
if part.is_empty() {
continue;
}
if let Some((start, end)) = part.split_once('-') {
if let (Ok(s), Ok(e)) = (start.parse::<u32>(), end.parse::<u32>()) {
for cpu in s..=e {
result.push(cpu);
}
}
} else if let Ok(cpu) = part.parse::<u32>() {
result.push(cpu);
}
}
result.sort();
result.dedup();
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_single() {
assert_eq!(parse_cpu_list("3"), vec![3]);
}
#[test]
fn parse_comma() {
assert_eq!(parse_cpu_list("0,6"), vec![0, 6]);
}
#[test]
fn parse_range() {
assert_eq!(parse_cpu_list("0-2,6-8"), vec![0, 1, 2, 6, 7, 8]);
}
#[test]
fn parse_mixed() {
assert_eq!(parse_cpu_list("0-2,5,9-11"), vec![0, 1, 2, 5, 9, 10, 11]);
}
#[test]
fn parse_empty() {
assert_eq!(parse_cpu_list(""), Vec::<u32>::new());
}
#[test]
fn detect_topology() {
let nr_cpus = std::fs::read_dir("/sys/devices/system/cpu")
.unwrap()
.filter(|e| {
e.as_ref()
.map(|e| {
e.file_name().to_string_lossy().starts_with("cpu")
&& e.file_name().to_string_lossy()[3..].parse::<u32>().is_ok()
})
.unwrap_or(false)
})
.count();
if nr_cpus == 0 {
return; }
let topo = CpuTopology::detect(nr_cpus).unwrap();
assert_eq!(topo.nr_cpus, nr_cpus);
assert_eq!(topo.l2_domain.len(), nr_cpus);
let max_group = topo.l2_groups.len() as u32;
for cpu in 0..nr_cpus {
assert!(
topo.l2_domain[cpu] < max_group || topo.l2_domain[cpu] == cpu as u32,
"CPU {} has invalid l2 group {}",
cpu,
topo.l2_domain[cpu]
);
}
assert!(!topo.l2_groups.is_empty());
assert_eq!(topo.socket_domain.len(), nr_cpus);
assert!(topo.nr_sockets >= 1);
for cpu in 0..nr_cpus {
assert!(
topo.socket_domain[cpu] < topo.nr_sockets,
"CPU {} socket {} >= nr_sockets {}",
cpu,
topo.socket_domain[cpu],
topo.nr_sockets
);
}
let (reff, rank, spectrum) = topo.compute_resistance_affinity();
assert!(
spectrum.fiedler > 0.0,
"lambda_2 must be positive for connected graph"
);
assert!(spectrum.tau_ns >= 1_000_000, "tau must be >= 1ms floor");
assert!(spectrum.tau_ns <= 40_000_000, "tau must be <= 40ms ceiling");
assert_eq!(reff[0], 0.0);
if nr_cpus >= 2 {
let best = rank[0] as usize;
assert!(best < nr_cpus);
let r_best = reff[best];
assert!(r_best > 0.0);
for c in 1..nr_cpus {
let r_c = reff[c];
assert!(
r_c >= r_best - 1e-9,
"CPU {} R_eff {:.6} < best {:.6}",
c,
r_c,
r_best
);
}
}
}
}