1use anyhow::Result;
9
10use crate::scheduler::Scheduler;
11
12const LAMBDA_ZERO_EPS: f64 = 1e-8;
35const 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)]
48pub struct TopologySpectrum {
49 pub fiedler: f64, pub tau_ns: u64, pub codel_eq_ns: u64, }
53
54fn extract_fiedler(eigenvalues: &[f64]) -> f64 {
55 let mut v: Vec<f64> = eigenvalues.to_vec();
60 v.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
61 v.into_iter()
62 .find(|&x| x > LAMBDA_ZERO_EPS)
63 .unwrap_or(LAMBDA_ZERO_EPS)
64}
65
66fn compute_tau_ns(fiedler: f64) -> u64 {
67 let raw = TAU_SCALE_NS / fiedler.max(LAMBDA_ZERO_EPS);
68 (raw as u64).clamp(TAU_FLOOR_NS, TAU_CEIL_NS)
69}
70
71fn compute_codel_eq_ns(eigenvalues: &[f64], n: usize, tau_ns: u64) -> u64 {
87 if n == 0 {
88 return TAU_FLOOR_NS;
89 }
90 let mut sum_inv_lambda = 0.0f64;
91 let mut sum_lambda = 0.0f64;
92 for &lambda in eigenvalues {
93 sum_lambda += lambda;
94 if lambda > LAMBDA_ZERO_EPS {
95 sum_inv_lambda += 1.0 / lambda;
96 }
97 }
98 let avg_reff = sum_inv_lambda / n as f64;
99 let two_m = sum_lambda;
100 let raw_ns = avg_reff * two_m * tau_ns as f64;
101 (raw_ns as u64).clamp(C_EQ_FLOOR_NS, C_EQ_CEIL_NS)
102}
103
104#[allow(dead_code)]
105pub struct CpuTopology {
106 pub nr_cpus: usize,
107 pub l2_domain: Vec<u32>, pub l2_groups: Vec<Vec<u32>>, pub socket_domain: Vec<u32>, pub nr_sockets: u32,
111}
112
113impl CpuTopology {
114 pub fn detect(nr_cpus: usize) -> Result<Self> {
115 let mut l2_domain = vec![0u32; nr_cpus];
116 let mut seen_groups: Vec<Vec<u32>> = Vec::new();
117
118 for cpu in 0..nr_cpus {
119 let path = format!(
120 "/sys/devices/system/cpu/cpu{}/cache/index2/shared_cpu_list",
121 cpu
122 );
123 let content = match std::fs::read_to_string(&path) {
124 Ok(s) => s,
125 Err(_) => {
126 l2_domain[cpu] = cpu as u32;
128 continue;
129 }
130 };
131
132 let members = parse_cpu_list(content.trim());
133
134 let group_id = match seen_groups.iter().position(|g| *g == members) {
136 Some(id) => id as u32,
137 None => {
138 let id = seen_groups.len() as u32;
139 seen_groups.push(members.clone());
140 id
141 }
142 };
143
144 l2_domain[cpu] = group_id;
145 }
146
147 let mut socket_domain = vec![0u32; nr_cpus];
149 let mut seen_sockets: Vec<u32> = Vec::new();
150
151 for cpu in 0..nr_cpus {
152 let path = format!(
153 "/sys/devices/system/cpu/cpu{}/topology/physical_package_id",
154 cpu
155 );
156 let pkg_id = match std::fs::read_to_string(&path) {
157 Ok(s) => s.trim().parse::<u32>().unwrap_or(0),
158 Err(_) => 0,
159 };
160 if !seen_sockets.contains(&pkg_id) {
161 seen_sockets.push(pkg_id);
162 }
163 let socket_idx = seen_sockets.iter().position(|&s| s == pkg_id).unwrap() as u32;
164 socket_domain[cpu] = socket_idx;
165 }
166
167 let nr_sockets = seen_sockets.len() as u32;
168
169 Ok(Self {
170 nr_cpus,
171 l2_domain,
172 l2_groups: seen_groups,
173 socket_domain,
174 nr_sockets,
175 })
176 }
177
178 pub fn populate_bpf_map(&self, sched: &Scheduler) -> Result<()> {
180 for cpu in 0..self.nr_cpus {
181 sched.write_cache_domain(cpu as u32, self.l2_domain[cpu])?;
182 }
183 Ok(())
184 }
185
186 pub fn populate_l2_siblings_map(&self, sched: &Scheduler) -> Result<()> {
189 const MAX_L2_SIBLINGS: usize = 8;
190 for (gid, members) in self.l2_groups.iter().enumerate() {
191 for (slot, &cpu) in members.iter().enumerate().take(MAX_L2_SIBLINGS) {
192 sched.write_l2_sibling(gid as u32, slot as u32, cpu)?;
193 }
194 if members.len() < MAX_L2_SIBLINGS {
195 sched.write_l2_sibling(gid as u32, members.len() as u32, u32::MAX)?;
196 }
197 }
198 Ok(())
199 }
200
201 const CONDUCTANCE_L2: f64 = 10.0;
223 const CONDUCTANCE_SOCKET: f64 = 1.0;
224 const CONDUCTANCE_CROSS: f64 = 0.3;
225
226 fn build_laplacian(&self) -> Vec<f64> {
228 let n = self.nr_cpus;
229 let mut l = vec![0.0f64; n * n];
230 for i in 0..n {
231 for j in (i + 1)..n {
232 let w = if self.l2_domain[i] == self.l2_domain[j] {
233 Self::CONDUCTANCE_L2
234 } else if self.socket_domain[i] == self.socket_domain[j] {
235 Self::CONDUCTANCE_SOCKET
236 } else {
237 Self::CONDUCTANCE_CROSS
238 };
239 l[i * n + j] = -w;
240 l[j * n + i] = -w;
241 l[i * n + i] += w;
242 l[j * n + j] += w;
243 }
244 }
245 l
246 }
247
248 fn symmetric_eigen(mat: &[f64], n: usize) -> (Vec<f64>, Vec<f64>) {
252 let mut a = mat.to_vec();
253 let mut v = vec![0.0f64; n * n];
255 for i in 0..n {
256 v[i * n + i] = 1.0;
257 }
258
259 let max_iter = 100 * n * n;
260 for _ in 0..max_iter {
261 let mut max_val = 0.0f64;
263 let mut p = 0;
264 let mut q = 1;
265 for i in 0..n {
266 for j in (i + 1)..n {
267 let val = a[i * n + j].abs();
268 if val > max_val {
269 max_val = val;
270 p = i;
271 q = j;
272 }
273 }
274 }
275 if max_val < 1e-12 {
276 break;
277 }
278
279 let app = a[p * n + p];
281 let aqq = a[q * n + q];
282 let apq = a[p * n + q];
283 let theta = if (app - aqq).abs() < 1e-15 {
284 std::f64::consts::FRAC_PI_4
285 } else {
286 0.5 * (2.0 * apq / (app - aqq)).atan()
287 };
288 let c = theta.cos();
289 let s = theta.sin();
290
291 for i in 0..n {
293 if i == p || i == q {
294 continue;
295 }
296 let aip = a[i * n + p];
297 let aiq = a[i * n + q];
298 a[i * n + p] = c * aip + s * aiq;
299 a[p * n + i] = a[i * n + p];
300 a[i * n + q] = -s * aip + c * aiq;
301 a[q * n + i] = a[i * n + q];
302 }
303 let new_pp = c * c * app + 2.0 * s * c * apq + s * s * aqq;
304 let new_qq = s * s * app - 2.0 * s * c * apq + c * c * aqq;
305 a[p * n + p] = new_pp;
306 a[q * n + q] = new_qq;
307 a[p * n + q] = 0.0;
308 a[q * n + p] = 0.0;
309
310 for i in 0..n {
312 let vip = v[i * n + p];
313 let viq = v[i * n + q];
314 v[i * n + p] = c * vip + s * viq;
315 v[i * n + q] = -s * vip + c * viq;
316 }
317 }
318
319 let eigenvalues: Vec<f64> = (0..n).map(|i| a[i * n + i]).collect();
320 (eigenvalues, v)
321 }
322
323 fn compute_pseudoinverse(eigenvalues: &[f64], eigenvectors: &[f64], n: usize) -> Vec<f64> {
325 let mut l_pinv = vec![0.0f64; n * n];
326 for k in 0..n {
327 if eigenvalues[k].abs() < 1e-8 {
328 continue; }
330 let inv_lambda = 1.0 / eigenvalues[k];
331 for i in 0..n {
332 for j in 0..n {
333 l_pinv[i * n + j] +=
334 inv_lambda * eigenvectors[i * n + k] * eigenvectors[j * n + k];
335 }
336 }
337 }
338 l_pinv
339 }
340
341 fn extract_reff(l_pinv: &[f64], n: usize) -> Vec<f64> {
344 let mut r = vec![0.0f64; n * n];
345 for i in 0..n {
346 for j in (i + 1)..n {
347 let val = l_pinv[i * n + i] + l_pinv[j * n + j] - 2.0 * l_pinv[i * n + j];
348 r[i * n + j] = val.max(0.0);
349 r[j * n + i] = r[i * n + j];
350 }
351 }
352 r
353 }
354
355 fn build_affinity_rank(reff: &[f64], n: usize) -> Vec<u32> {
358 let mut rank = vec![0u32; n * n];
359 for cpu in 0..n {
360 let mut others: Vec<(u64, u32)> = (0..n)
361 .filter(|&c| c != cpu)
362 .map(|c| {
363 let key = (reff[cpu * n + c] * 1_000_000.0) as u64;
365 (key, c as u32)
366 })
367 .collect();
368 others.sort();
369 for (slot, &(_, target)) in others.iter().enumerate() {
370 rank[cpu * n + slot] = target;
371 }
372 for slot in others.len()..n {
374 rank[cpu * n + slot] = u32::MAX;
375 }
376 }
377 rank
378 }
379
380 pub fn compute_resistance_affinity(&self) -> (Vec<f64>, Vec<u32>, TopologySpectrum) {
385 let n = self.nr_cpus;
386 let laplacian = self.build_laplacian();
387 let (eigenvalues, eigenvectors) = Self::symmetric_eigen(&laplacian, n);
388 let fiedler = extract_fiedler(&eigenvalues);
389 let tau_ns = compute_tau_ns(fiedler);
390 let l_pinv = Self::compute_pseudoinverse(&eigenvalues, &eigenvectors, n);
391 let reff = Self::extract_reff(&l_pinv, n);
392 let rank = Self::build_affinity_rank(&reff, n);
393 let codel_eq_ns = compute_codel_eq_ns(&eigenvalues, n, tau_ns);
394 (
395 reff,
396 rank,
397 TopologySpectrum {
398 fiedler,
399 tau_ns,
400 codel_eq_ns,
401 },
402 )
403 }
404
405 pub fn populate_affinity_rank_map(&self, sched: &Scheduler, rank: &[u32]) -> Result<()> {
414 let stride = crate::bpf_intf::MAX_AFFINITY_CANDIDATES as usize;
415 let valid = self.nr_cpus.saturating_sub(1).min(stride);
416 for cpu in 0..self.nr_cpus {
417 for slot in 0..valid {
418 let val = rank[cpu * self.nr_cpus + slot];
419 sched.write_affinity_rank(cpu as u32, slot as u32, val)?;
420 }
421 for slot in valid..stride {
422 sched.write_affinity_rank(cpu as u32, slot as u32, u32::MAX)?;
423 }
424 }
425 Ok(())
426 }
427
428 pub fn log_resistance_affinity(&self, reff: &[f64], rank: &[u32], spectrum: TopologySpectrum) {
429 log_info!(
430 "TOPOLOGY SPECTRUM: lambda2={:.4} tau={}ms codel_eq={}us",
431 spectrum.fiedler,
432 spectrum.tau_ns / 1_000_000,
433 spectrum.codel_eq_ns / 1_000
434 );
435 let n = self.nr_cpus;
436 let mut parts = Vec::new();
438 for slot in 0..3.min(n - 1) {
439 let target = rank[slot] as usize;
440 if target >= n {
441 break;
442 }
443 let r = reff[target];
444 parts.push(format!("CPU{}(R={:.3})", target, r));
445 }
446 log_info!("RESISTANCE AFFINITY: CPU 0 rank: {}", parts.join(", "));
447
448 if n >= 2 {
450 let l2_sib = rank[0] as usize;
451 let non_l2 = rank[1.min(n - 2)] as usize;
452 log_info!(
453 "RESISTANCE AFFINITY: R_eff L2={:.4} non-L2={:.4} ratio={:.1}x",
454 reff[l2_sib],
455 reff[non_l2],
456 if reff[l2_sib] > 0.0 {
457 reff[non_l2] / reff[l2_sib]
458 } else {
459 0.0
460 }
461 );
462 }
463 }
464
465 pub fn log_summary(&self) {
466 for (gid, members) in self.l2_groups.iter().enumerate() {
467 let cpus: Vec<String> = members.iter().map(|c| c.to_string()).collect();
468 log_info!("L2 GROUP {}: [{}]", gid, cpus.join(","));
469 }
470 log_info!(
471 "L2 GROUPS: {} across {} CPUs, {} SOCKETS",
472 self.l2_groups.len(),
473 self.nr_cpus,
474 self.nr_sockets
475 );
476 }
477}
478
479fn parse_cpu_list(s: &str) -> Vec<u32> {
481 let mut result = Vec::new();
482 for part in s.split(',') {
483 let part = part.trim();
484 if part.is_empty() {
485 continue;
486 }
487 if let Some((start, end)) = part.split_once('-') {
488 if let (Ok(s), Ok(e)) = (start.parse::<u32>(), end.parse::<u32>()) {
489 for cpu in s..=e {
490 result.push(cpu);
491 }
492 }
493 } else if let Ok(cpu) = part.parse::<u32>() {
494 result.push(cpu);
495 }
496 }
497 result.sort();
498 result.dedup();
499 result
500}
501
502#[cfg(test)]
503mod tests {
504 use super::*;
505
506 #[test]
507 fn parse_single() {
508 assert_eq!(parse_cpu_list("3"), vec![3]);
509 }
510
511 #[test]
512 fn parse_comma() {
513 assert_eq!(parse_cpu_list("0,6"), vec![0, 6]);
514 }
515
516 #[test]
517 fn parse_range() {
518 assert_eq!(parse_cpu_list("0-2,6-8"), vec![0, 1, 2, 6, 7, 8]);
519 }
520
521 #[test]
522 fn parse_mixed() {
523 assert_eq!(parse_cpu_list("0-2,5,9-11"), vec![0, 1, 2, 5, 9, 10, 11]);
524 }
525
526 #[test]
527 fn parse_empty() {
528 assert_eq!(parse_cpu_list(""), Vec::<u32>::new());
529 }
530
531 #[test]
532 fn detect_topology() {
533 let nr_cpus = std::fs::read_dir("/sys/devices/system/cpu")
535 .unwrap()
536 .filter(|e| {
537 e.as_ref()
538 .map(|e| {
539 e.file_name().to_string_lossy().starts_with("cpu")
540 && e.file_name().to_string_lossy()[3..].parse::<u32>().is_ok()
541 })
542 .unwrap_or(false)
543 })
544 .count();
545
546 if nr_cpus == 0 {
547 return; }
549
550 let topo = CpuTopology::detect(nr_cpus).unwrap();
551 assert_eq!(topo.nr_cpus, nr_cpus);
552 assert_eq!(topo.l2_domain.len(), nr_cpus);
553
554 let max_group = topo.l2_groups.len() as u32;
556 for cpu in 0..nr_cpus {
557 assert!(
558 topo.l2_domain[cpu] < max_group || topo.l2_domain[cpu] == cpu as u32,
559 "CPU {} has invalid l2 group {}",
560 cpu,
561 topo.l2_domain[cpu]
562 );
563 }
564
565 assert!(!topo.l2_groups.is_empty());
567
568 assert_eq!(topo.socket_domain.len(), nr_cpus);
570 assert!(topo.nr_sockets >= 1);
571 for cpu in 0..nr_cpus {
572 assert!(
573 topo.socket_domain[cpu] < topo.nr_sockets,
574 "CPU {} socket {} >= nr_sockets {}",
575 cpu,
576 topo.socket_domain[cpu],
577 topo.nr_sockets
578 );
579 }
580
581 let (reff, rank, spectrum) = topo.compute_resistance_affinity();
583 assert!(
584 spectrum.fiedler > 0.0,
585 "lambda_2 must be positive for connected graph"
586 );
587 assert!(spectrum.tau_ns >= 1_000_000, "tau must be >= 1ms floor");
588 assert!(spectrum.tau_ns <= 40_000_000, "tau must be <= 40ms ceiling");
589 assert_eq!(reff[0], 0.0);
591 if nr_cpus >= 2 {
593 let best = rank[0] as usize;
594 assert!(best < nr_cpus);
595 let r_best = reff[best];
596 assert!(r_best > 0.0);
597 for c in 1..nr_cpus {
599 let r_c = reff[c];
600 assert!(
601 r_c >= r_best - 1e-9,
602 "CPU {} R_eff {:.6} < best {:.6}",
603 c,
604 r_c,
605 r_best
606 );
607 }
608 }
609 }
610}