Skip to main content

scx_pandemonium/
topology.rs

1// PANDEMONIUM CPU CACHE TOPOLOGY
2// PARSES SYSFS AT STARTUP, POPULATES BPF MAP FOR CACHE-AWARE DISPATCH
3//
4// BPF dispatch() USES THE CACHE DOMAIN MAP TO PREFER TASKS THAT LAST
5// RAN ON THE SAME CPU OR AN L2 SIBLING. THIS PRESERVES CACHE WARMTH
6// AND REDUCES THE THROUGHPUT GAP CAUSED BY BLIND NODE-DSQ CONSUMPTION.
7
8use anyhow::Result;
9
10use crate::scheduler::Scheduler;
11
12// FIEDLER-VALUE / TOPOLOGY TIME CONSTANT
13// lambda_2 IS THE SECOND-SMALLEST EIGENVALUE OF THE WEIGHTED GRAPH LAPLACIAN
14// (THE "ALGEBRAIC CONNECTIVITY" OR "SPECTRAL GAP"). 1/lambda_2 IS THE MIXING
15// TIME OF A RANDOM WALK ACROSS THE CPU GRAPH -- A CANONICAL "TIME CONSTANT"
16// FOR HOW FAST WORK PROPAGATES ACROSS THE TOPOLOGY. EVERY TIMING/THRESHOLD
17// FORMULA IN THE SCHEDULER DERIVES FROM tau VIA scale_tau() (BPF) OR
18// scale_tau_u64() (RUST); ad-hoc nr_cpus FORMULAS ARE CRUDE APPROXIMATIONS
19// OF THIS AND ARE EXPLICITLY MIGRATED OUT.
20//
21// CARVE-OUT: ONLY ABSOLUTE-COUNT QUANTITIES KEEP nr_cpu_ids. GRAPH-SHAPE
22// QUANTITIES (including search budgets sized by spectral connectivity) ARE
23// EXPRESSED THROUGH tau VIA lambda_2 = TAU_SCALE_NS / tau. TWO SITES IN
24// main.bpf.c INTENTIONALLY KEEP nr_cpu_ids:
25//   - select_cpu()'s wake_wide() flips threshold (matches the kernel's
26//     wake_wide() convention; an external interface).
27//   - tick()'s rotating-scan budget switch (coverage over the active CPU
28//     range, not a graph-shape decision).
29// Everything else -- timing, oscillator dynamics, search budgets, depth
30// gates -- derives from tau in apply_tau_scaling().
31//
32// EXTRACTION IS O(n log n) ON TOP OF THE EXISTING O(n^3) Jacobi; NEGLIGIBLE.
33// REFERENCE: CHEEGER'S INEQUALITY BOUNDS lambda_2 AGAINST GRAPH BOTTLENECK.
34const LAMBDA_ZERO_EPS: f64 = 1e-8;
35const TAU_SCALE_NS: f64 = 1.6e8; // 160MS -- CALIBRATED SO FLAT K_4 WITH
36                                 // EDGE WEIGHT 10 (ALL L2 SIBLINGS)
37                                 // YIELDS tau = 4MS.
38const TAU_FLOOR_NS: u64 = 1_000_000; //  1MS
39const TAU_CEIL_NS: u64 = 40_000_000; // 40MS
40
41// CoDel TARGET EQUILIBRIUM CLAMP RANGE. THE CONTROLLER'S MEAN-REVERTING
42// TARGET IN ABSENCE OF DISTURBANCE. SAME ORDER OF MAGNITUDE AS THE
43// CoDel TARGET RANGE ITSELF (FLOOR ~200us, CEILING ~8MS).
44const C_EQ_FLOOR_NS: u64 = 200_000; // 200us
45const C_EQ_CEIL_NS: u64 = 8_000_000; // 8ms
46
47#[derive(Clone, Copy, Debug)]
48pub struct TopologySpectrum {
49    pub fiedler: f64,     // lambda_2
50    pub tau_ns: u64,      // clamped TAU_SCALE_NS / lambda_2
51    pub codel_eq_ns: u64, // <R_eff> * 2m * tau, clamped
52}
53
54fn extract_fiedler(eigenvalues: &[f64]) -> f64 {
55    // Jacobi RETURNS EIGENVALUES UNSORTED. FOR A CONNECTED LAPLACIAN THE
56    // SMALLEST EIGENVALUE IS 0 (SKIPPED VIA LAMBDA_ZERO_EPS). FOR A
57    // DISCONNECTED GRAPH (HOTPLUG PARTITION) SEVERAL EIGENVALUES ARE ~0;
58    // lambda_2 IS THE SMALLEST STRICTLY POSITIVE ONE.
59    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
71// CoDel TARGET EQUILIBRIUM FROM THE LAPLACIAN SPECTRUM.
72// FORMULA:  c_eq = <R_eff> * 2m * tau
73// SPECTRAL FORM:
74//   <R_eff>  =  Tr(L+) / N  =  (1/N) * sum_{lambda > 0} 1 / lambda
75//   2m       =  Tr(L)      =  sum_{lambda} lambda
76//   tau      =  TAU_SCALE_NS / lambda_2  (already computed, in ns)
77//
78// PHYSICAL INTERPRETATION: c_eq is the natural commute-time scale of
79// the topology graph -- the average time it takes work to bounce
80// between two CPUs along the topology's slowest paths. The CoDel
81// target's mean-reverting equilibrium settles to this value in the
82// absence of disturbance, so the stall detector tightens around the
83// topology's intrinsic timescale instead of a hand-picked constant.
84//
85// CLAMPED TO [200us, 8ms] -- THE CoDel TARGET RANGE ITSELF.
86fn 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>,      // l2_domain[cpu] = group_id
108    pub l2_groups: Vec<Vec<u32>>, // l2_groups[group_id] = [cpu, ...]
109    pub socket_domain: Vec<u32>,  // socket_domain[cpu] = socket_id
110    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                    // CPU MIGHT BE OFFLINE OR HAVE NO L2 INFO -- ASSIGN OWN GROUP
127                    l2_domain[cpu] = cpu as u32;
128                    continue;
129                }
130            };
131
132            let members = parse_cpu_list(content.trim());
133
134            // CHECK IF THIS GROUP ALREADY EXISTS
135            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        // DETECT SOCKET (PHYSICAL PACKAGE)
148        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    // WRITE L2 DOMAIN MAP TO BPF ARRAY VIA SCHEDULER
179    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    // WRITE L2 SIBLINGS FLAT ARRAY TO BPF MAP
187    // l2_siblings[group_id * 8 + slot] = cpu_id, SENTINEL u32::MAX MARKS END
188    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    // RESISTANCE AFFINITY (KYNG-DINIC ELECTRICAL FLOW MODEL)
202    //
203    // EFFECTIVE RESISTANCE R_eff(u,v) BETWEEN TWO CPUs CAPTURES THE TRUE
204    // MIGRATION COST THROUGH ALL TOPOLOGY PATHS. COMPUTED FROM THE LAPLACIAN
205    // PSEUDOINVERSE OF THE CPU TOPOLOGY GRAPH:
206    //   R_eff(i,j) = L+[i,i] + L+[j,j] - 2*L+[i,j]
207    //
208    // EDGE CONDUCTANCES (INVERSE RESISTANCE):
209    //   L2 SIBLINGS:     10.0  (SHARED CACHE, NEAR-ZERO MIGRATION COST)
210    //   SAME SOCKET:      1.0  (SHARED LLC, MODERATE COST)
211    //   CROSS-SOCKET:     0.3  (NUMA HOP, HIGH COST)
212    //
213    // THE LAPLACIAN L = D - W WHERE D IS DEGREE MATRIX, W IS WEIGHTED ADJACENCY.
214    // L+ (MOORE-PENROSE PSEUDOINVERSE) COMPUTED VIA EIGENDECOMPOSITION:
215    //   L+ = sum_{i: lambda_i > 0} (1/lambda_i) * v_i * v_i^T
216    //
217    // FOR n CPUs THIS IS O(n^3) -- TRIVIAL AT SCHEDULER STARTUP (n <= 256).
218    //
219    // REFERENCE: Christiano-Kelner-Madry-Spielman-Teng (STOC 2011),
220    //            Chen-Kyng-Liu-Peng-Gutenberg-Sachdeva (FOCS 2022)
221
222    const CONDUCTANCE_L2: f64 = 10.0;
223    const CONDUCTANCE_SOCKET: f64 = 1.0;
224    const CONDUCTANCE_CROSS: f64 = 0.3;
225
226    // BUILD WEIGHTED GRAPH LAPLACIAN FROM CPU TOPOLOGY
227    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    // SYMMETRIC EIGENDECOMPOSITION VIA JACOBI ROTATIONS
249    // RETURNS (eigenvalues, eigenvectors_column_major)
250    // SUITABLE FOR n <= 256. NO EXTERNAL DEPENDENCIES.
251    fn symmetric_eigen(mat: &[f64], n: usize) -> (Vec<f64>, Vec<f64>) {
252        let mut a = mat.to_vec();
253        // EIGENVECTORS START AS IDENTITY
254        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            // FIND LARGEST OFF-DIAGONAL ELEMENT
262            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            // COMPUTE ROTATION
280            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            // APPLY ROTATION TO A
292            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            // ACCUMULATE EIGENVECTORS
311            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    // COMPUTE LAPLACIAN PSEUDOINVERSE FROM EIGENDECOMPOSITION
324    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; // SKIP NULL EIGENVALUE (CONNECTED GRAPH HAS ONE)
329            }
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    // COMPUTE ALL-PAIRS EFFECTIVE RESISTANCE FROM PSEUDOINVERSE
342    // R_eff(i,j) = L+[i,i] + L+[j,j] - 2*L+[i,j]
343    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    // BUILD PER-CPU AFFINITY RANK: FOR EACH CPU, ALL OTHERS SORTED BY R_EFF
356    // Returns flat array: affinity_rank[cpu * nr_cpus + slot] = target_cpu
357    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                    // SORT KEY: R_EFF AS FIXED-POINT TO AVOID FLOAT COMPARISON ISSUES
364                    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            // FILL REMAINING SLOTS WITH SENTINEL
373            for slot in others.len()..n {
374                rank[cpu * n + slot] = u32::MAX;
375            }
376        }
377        rank
378    }
379
380    // COMPUTE RESISTANCE AFFINITY: FULL PIPELINE
381    // Returns (reff_matrix, affinity_rank, spectrum) for use by BPF and scheduler.
382    // Spectrum carries lambda_2 (Fiedler value) and its derived tau_ns, used as
383    // the universal topology time constant for every core-scaled knob.
384    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    // WRITE AFFINITY RANK TO BPF MAP
406    // affinity_rank[cpu * MAX_AFFINITY_CANDIDATES + slot] = target_cpu
407    //
408    // Emit the full sorted R_eff peer list per CPU, capped at the BPF
409    // table width (MAX_AFFINITY_CANDIDATES). Slots beyond the actual
410    // topology end (nr_cpus - 1) are written as explicit u32::MAX
411    // sentinels so the BPF early-exit fires correctly -- map zero-init
412    // would otherwise alias to "CPU 0" and silently mis-route.
413    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        // LOG TOP 3 AFFINITIES FOR CPU 0
437        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        // LOG L2 VS NON-L2 R_EFF FOR FIRST CPU
449        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
479// PARSE KERNEL CPU LIST FORMAT: "0,6" or "0-2,6-8" or "3"
480fn 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        // RUNS ON ANY MACHINE -- VERIFIES SANE OUTPUT
534        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; // NO CPUS VISIBLE (CONTAINER?)
548        }
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        // EVERY CPU MUST HAVE A VALID GROUP ID
555        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        // AT LEAST ONE GROUP MUST EXIST
566        assert!(!topo.l2_groups.is_empty());
567
568        // SOCKET DETECTION
569        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        // RESISTANCE AFFINITY: LAPLACIAN R_EFF
582        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        // SAME CPU = 0 (diagonal of R_eff matrix)
590        assert_eq!(reff[0], 0.0);
591        // L2 SIBLING SHOULD BE CHEAPEST (RANK SLOT 0)
592        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            // EVERY OTHER CPU SHOULD COST >= THE BEST
598            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}