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. CAPACITY-AWARE ANCHOR: AT THE
36                                 // 12C REFERENCE (lambda_2=12, N=12)
37                                 // tau = 160ms / sqrt(144) = 13.3MS.
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    // Phi migration-potential distance->wait scale (Q16). The extra head-wait a
53    // steal must clear before crossing to a peer = (reff * this) >> 16 ns. Always
54    // computed now (T1): the continuous metric prices distance on every part, no
55    // binary topology gate -- on a monolithic part it calibrates to the L2 seam.
56    pub phi_dist_scale_q16: u64,
57}
58
59// T2: the emergent domain tree. Leaves are tightly-coupled CPU sets (an L2 group
60// / core -- nothing meaningful to partition below); internal nodes are a
61// min-conductance cut carrying its phi, the price to cross that seam. T3's
62// bounded-local steal climbs this tree: drain your leaf, then your subtree,
63// crossing a cut only when its phi says the imbalance pays. The discrete cache domain
64// enum is gone -- this structure emerges from the cache graph, per machine.
65#[derive(Debug, Clone)]
66pub enum DomainNode {
67    Leaf(Vec<usize>),
68    Cut {
69        phi: f64,
70        left: Box<DomainNode>,
71        right: Box<DomainNode>,
72    },
73}
74
75#[allow(dead_code)]
76impl DomainNode {
77    // Flatten to the leaf CPU sets -- each an emergent atomic domain.
78    pub fn leaves(&self) -> Vec<Vec<usize>> {
79        match self {
80            DomainNode::Leaf(cpus) => vec![cpus.clone()],
81            DomainNode::Cut { left, right, .. } => {
82                let mut v = left.leaves();
83                v.extend(right.leaves());
84                v
85            }
86        }
87    }
88
89    // Every cut's phi -- the crossing-price ladder (coarser seams cost less, so
90    // phi rises with depth as the steal climbs toward more tightly-coupled work).
91    pub fn cut_phis(&self) -> Vec<f64> {
92        match self {
93            DomainNode::Leaf(_) => Vec::new(),
94            DomainNode::Cut { phi, left, right } => {
95                let mut v = vec![*phi];
96                v.extend(left.cut_phis());
97                v.extend(right.cut_phis());
98                v
99            }
100        }
101    }
102}
103
104fn extract_fiedler(eigenvalues: &[f64]) -> f64 {
105    // Jacobi RETURNS EIGENVALUES UNSORTED. FOR A CONNECTED LAPLACIAN THE
106    // SMALLEST EIGENVALUE IS 0 (SKIPPED VIA LAMBDA_ZERO_EPS). FOR A
107    // DISCONNECTED GRAPH (HOTPLUG PARTITION) SEVERAL EIGENVALUES ARE ~0;
108    // lambda_2 IS THE SMALLEST STRICTLY POSITIVE ONE.
109    let mut v: Vec<f64> = eigenvalues.to_vec();
110    v.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
111    v.into_iter()
112        .find(|&x| x > LAMBDA_ZERO_EPS)
113        .unwrap_or(LAMBDA_ZERO_EPS)
114}
115
116fn compute_tau_ns(fiedler: f64, n: usize) -> u64 {
117    // CAPACITY-AWARE: tau = TAU_SCALE_NS / sqrt(lambda_2 * N) -- the geometric
118    // mean of connectivity (1/lambda_2) and capacity (1/sqrt(N)). The old
119    // pure-connectivity law gave a well-connected but capacity-starved
120    // topology (a 2-core L2 pair, lambda_2=20) a tiny tau (8ms) and thus tight
121    // tolerances exactly where scarce CPUs need loose ones -- which the
122    // apply_tau_scaling floors then patched back up. sqrt(N) penalizes small N,
123    // so 2C loosens to ~25ms with no floor needed. The 12C reference is
124    // preserved: lambda_2=12, N=12 -> sqrt(144)=12 -> 160ms/12 = 13.3ms.
125    let denom = (fiedler.max(LAMBDA_ZERO_EPS) * (n.max(1) as f64)).sqrt();
126    let raw = TAU_SCALE_NS / denom.max(LAMBDA_ZERO_EPS);
127    (raw as u64).clamp(TAU_FLOOR_NS, TAU_CEIL_NS)
128}
129
130// CoDel TARGET EQUILIBRIUM FROM THE LAPLACIAN SPECTRUM.
131// FORMULA:  c_eq = <R_eff> * 2m * tau
132// SPECTRAL FORM:
133//   <R_eff>  =  Tr(L+) / N  =  (1/N) * sum_{lambda > 0} 1 / lambda
134//   2m       =  Tr(L)      =  sum_{lambda} lambda
135//   tau      =  TAU_SCALE_NS / lambda_2  (already computed, in ns)
136//
137// PHYSICAL INTERPRETATION: c_eq is the natural commute-time scale of
138// the topology graph -- the average time it takes work to bounce
139// between two CPUs along the topology's slowest paths. The CoDel
140// target's mean-reverting equilibrium settles to this value in the
141// absence of disturbance, so the stall detector tightens around the
142// topology's intrinsic timescale instead of a hand-picked constant.
143//
144// CLAMPED TO [200us, 8ms] -- THE CoDel TARGET RANGE ITSELF.
145fn compute_codel_eq_ns(eigenvalues: &[f64], n: usize, tau_ns: u64) -> u64 {
146    if n == 0 {
147        return TAU_FLOOR_NS;
148    }
149    let mut sum_inv_lambda = 0.0f64;
150    let mut sum_lambda = 0.0f64;
151    for &lambda in eigenvalues {
152        sum_lambda += lambda;
153        if lambda > LAMBDA_ZERO_EPS {
154            sum_inv_lambda += 1.0 / lambda;
155        }
156    }
157    let avg_reff = sum_inv_lambda / n as f64;
158    let two_m = sum_lambda;
159    let raw_ns = avg_reff * two_m * tau_ns as f64;
160    (raw_ns as u64).clamp(C_EQ_FLOOR_NS, C_EQ_CEIL_NS)
161}
162
163#[allow(dead_code)]
164pub struct CpuTopology {
165    pub nr_cpus: usize,
166    pub l2_domain: Vec<u32>,      // l2_domain[cpu] = group_id
167    pub l2_groups: Vec<Vec<u32>>, // l2_groups[group_id] = [cpu, ...]
168    pub socket_domain: Vec<u32>,  // socket_domain[cpu] = socket_id
169    pub llc_domain: Vec<u32>,     // llc_domain[cpu] = L3 GROUP (== socket WHEN MONOLITHIC)
170    pub nr_sockets: u32,
171}
172
173impl CpuTopology {
174    pub fn detect(nr_cpus: usize) -> Result<Self> {
175        let mut l2_domain = vec![0u32; nr_cpus];
176        let mut seen_groups: Vec<Vec<u32>> = Vec::new();
177
178        for cpu in 0..nr_cpus {
179            let path = format!(
180                "/sys/devices/system/cpu/cpu{}/cache/index2/shared_cpu_list",
181                cpu
182            );
183            let content = match std::fs::read_to_string(&path) {
184                Ok(s) => s,
185                Err(_) => {
186                    // CPU MIGHT BE OFFLINE OR HAVE NO L2 INFO -- ASSIGN OWN GROUP
187                    l2_domain[cpu] = cpu as u32;
188                    continue;
189                }
190            };
191
192            let members = parse_cpu_list(content.trim());
193
194            // CHECK IF THIS GROUP ALREADY EXISTS
195            let group_id = match seen_groups.iter().position(|g| *g == members) {
196                Some(id) => id as u32,
197                None => {
198                    let id = seen_groups.len() as u32;
199                    seen_groups.push(members.clone());
200                    id
201                }
202            };
203
204            l2_domain[cpu] = group_id;
205        }
206
207        // DETECT SOCKET (PHYSICAL PACKAGE)
208        let mut socket_domain = vec![0u32; nr_cpus];
209        let mut seen_sockets: Vec<u32> = Vec::new();
210
211        for cpu in 0..nr_cpus {
212            let path = format!(
213                "/sys/devices/system/cpu/cpu{}/topology/physical_package_id",
214                cpu
215            );
216            let pkg_id = match std::fs::read_to_string(&path) {
217                Ok(s) => s.trim().parse::<u32>().unwrap_or(0),
218                Err(_) => 0,
219            };
220            if !seen_sockets.contains(&pkg_id) {
221                seen_sockets.push(pkg_id);
222            }
223            let socket_idx = seen_sockets.iter().position(|&s| s == pkg_id).unwrap() as u32;
224            socket_domain[cpu] = socket_idx;
225        }
226
227        let nr_sockets = seen_sockets.len() as u32;
228
229        // DETECT L3 / cache domain / cache domain DOMAIN (index3). ON AMD multi-domain PARTS index3
230        // SUBDIVIDES THE SOCKET INTO cache domain GROUPS; ON MONOLITHIC-L3 PARTS IT
231        // SPANS THE WHOLE SOCKET. USE THE TIER ONLY WHEN IT GENUINELY
232        // SUBDIVIDES A SOCKET (MORE L3 GROUPS THAN SOCKETS) AND index3 WAS
233        // PRESENT FOR EVERY CPU; OTHERWISE llc_domain == socket_domain SO THE
234        // CROSS-DOMAIN RUNG IN build_laplacian NEVER FIRES (EXACT NO-OP).
235        let mut llc_domain = vec![0u32; nr_cpus];
236        let mut seen_llc: Vec<Vec<u32>> = Vec::new();
237        let mut llc_ok = true;
238        for cpu in 0..nr_cpus {
239            let path = format!(
240                "/sys/devices/system/cpu/cpu{}/cache/index3/shared_cpu_list",
241                cpu
242            );
243            let content = match std::fs::read_to_string(&path) {
244                Ok(s) => s,
245                Err(_) => {
246                    llc_ok = false;
247                    break;
248                }
249            };
250            let members = parse_cpu_list(content.trim());
251            let group_id = match seen_llc.iter().position(|g| *g == members) {
252                Some(id) => id as u32,
253                None => {
254                    let id = seen_llc.len() as u32;
255                    seen_llc.push(members);
256                    id
257                }
258            };
259            llc_domain[cpu] = group_id;
260        }
261        // MAX_OVERFLOW_DOMAINS mirrors src/bpf/intf.h: the BPF side creates exactly
262        // this many per-domain overflow DSQs. If a (multi-socket, high-cache domain) box has
263        // more L3 groups than that, degrade to the socket domain rather than let
264        // a cache domain id index an uncreated DSQ -> dispatch failure -> ejection.
265        const MAX_OVERFLOW_DOMAINS: usize = 32;
266        let llc_subdivides = llc_ok
267            && seen_llc.len() > nr_sockets as usize
268            && seen_llc.len() <= MAX_OVERFLOW_DOMAINS;
269        if !llc_subdivides {
270            llc_domain = socket_domain.clone();
271        }
272
273        Ok(Self {
274            nr_cpus,
275            l2_domain,
276            l2_groups: seen_groups,
277            socket_domain,
278            llc_domain,
279            nr_sockets,
280        })
281    }
282
283    // WRITE L2 DOMAIN MAP TO BPF ARRAY VIA SCHEDULER
284    pub fn populate_bpf_map(&self, sched: &mut Scheduler) -> Result<()> {
285        for cpu in 0..self.nr_cpus {
286            sched.write_cache_domain(cpu as u32, self.l2_domain[cpu])?;
287        }
288        // nr_overflow_domains = number of distinct llc_domain values (the overflow-domain count)
289        let mut seen: Vec<u32> = Vec::new();
290        for &g in &self.llc_domain {
291            if !seen.contains(&g) {
292                seen.push(g);
293            }
294        }
295        sched.write_nr_overflow_domains(seen.len() as u32);
296        Ok(())
297    }
298
299    // WRITE L2 SIBLINGS FLAT ARRAY TO BPF MAP
300    // l2_siblings[group_id * 8 + slot] = cpu_id, SENTINEL u32::MAX MARKS END
301    pub fn populate_l2_siblings_map(&self, sched: &Scheduler) -> Result<()> {
302        const MAX_L2_SIBLINGS: usize = 8;
303        for (gid, members) in self.l2_groups.iter().enumerate() {
304            for (slot, &cpu) in members.iter().enumerate().take(MAX_L2_SIBLINGS) {
305                sched.write_l2_sibling(gid as u32, slot as u32, cpu)?;
306            }
307            if members.len() < MAX_L2_SIBLINGS {
308                sched.write_l2_sibling(gid as u32, members.len() as u32, u32::MAX)?;
309            }
310        }
311        Ok(())
312    }
313
314    // RESISTANCE AFFINITY (KYNG-DINIC ELECTRICAL FLOW MODEL)
315    //
316    // EFFECTIVE RESISTANCE R_eff(u,v) BETWEEN TWO CPUs CAPTURES THE TRUE
317    // MIGRATION COST THROUGH ALL TOPOLOGY PATHS. COMPUTED FROM THE LAPLACIAN
318    // PSEUDOINVERSE OF THE CPU TOPOLOGY GRAPH:
319    //   R_eff(i,j) = L+[i,i] + L+[j,j] - 2*L+[i,j]
320    //
321    // EDGE CONDUCTANCES (INVERSE RESISTANCE):
322    //   L2 SIBLINGS:      10.0  (SHARED L2, NEAR-ZERO MIGRATION COST)
323    //   SAME L3 / cache domain:     3.0  (SHARED LLC; ONLY WHEN A SOCKET HOLDS >1 cache domain)
324    //   CROSS-DOMAIN SOCKET:  1.0  (CROSS-DOMAIN INTERCONNECT HOP, ~8x CORE-TO-CORE LATENCY)
325    //   CROSS-SOCKET:      0.3  (NUMA HOP, HIGH COST)
326    // RAISING THE same-domain RUNG (NOT LOWERING THE CROSS-DOMAIN CUT) RANKS
327    // SAME-L3 PEERS AHEAD OF CROSS-L3 ONES WITHOUT MOVING lambda_2: THE CROSS-L3
328    // CUT STAYS 1.0, SO tau AND codel_eq ARE UNCHANGED. THE L3 RUNG IS ALWAYS ON;
329    // on a monolithic part llc_domain == socket_domain, so it coincides with the
330    // socket rung and the continuous R_eff metric calibrates to the L2 boundary.
331    //
332    // THE LAPLACIAN L = D - W WHERE D IS DEGREE MATRIX, W IS WEIGHTED ADJACENCY.
333    // L+ (MOORE-PENROSE PSEUDOINVERSE) COMPUTED VIA EIGENDECOMPOSITION:
334    //   L+ = sum_{i: lambda_i > 0} (1/lambda_i) * v_i * v_i^T
335    //
336    // FOR n CPUs THIS IS O(n^3) -- TRIVIAL AT SCHEDULER STARTUP (n <= 256).
337    //
338    // REFERENCE: Christiano-Kelner-Madry-Spielman-Teng (STOC 2011),
339    //            Chen-Kyng-Liu-Peng-Gutenberg-Sachdeva (FOCS 2022)
340
341    // Phi FIX A: SMT siblings SHARE L2, so a move between them costs ~0 cache.
342    // Make the edge very stiff -> R_eff(SMT-sib) ~ 0, which lands the Phi migration
343    // barrier exactly at the physical-core / L2 boundary (a real cold-L2 refill)
344    // instead of penalizing free intra-core moves. lambda_2 is the cross-domain Fiedler
345    // cut (independent of L2 stiffness) so tau is unchanged, and codel_eq is already
346    // clamped at its ceiling, so the oscillator timescales are invariant.
347    const CONDUCTANCE_L2: f64 = 1000.0; // L2 / SMT SIBLINGS
348    const CONDUCTANCE_LLC: f64 = 3.0; // SAME L3 (ABOVE SOCKET; always-on rung)
349    const CONDUCTANCE_SOCKET: f64 = 1.0; // SAME SOCKET, CROSS-DOMAIN (IF HOP) -- OR MONOLITHIC SAME-SOCKET
350    const CONDUCTANCE_CROSS: f64 = 0.3; // CROSS-SOCKET NUMA HOP
351
352    // BUILD WEIGHTED GRAPH LAPLACIAN FROM CPU TOPOLOGY
353    // Conductance edge weight between two CPUs, derived from the cache hierarchy.
354    // The single source of truth for BOTH the Laplacian (R_eff / tau) and the
355    // domain cut below, so the emergent locality boundary and the placement metric
356    // price the exact same graph -- the continuous metric drives everything.
357    fn conductance(&self, i: usize, j: usize) -> f64 {
358        if self.l2_domain[i] == self.l2_domain[j] {
359            Self::CONDUCTANCE_L2
360        } else if self.llc_domain[i] == self.llc_domain[j] {
361            Self::CONDUCTANCE_LLC
362        } else if self.socket_domain[i] == self.socket_domain[j] {
363            Self::CONDUCTANCE_SOCKET
364        } else {
365            Self::CONDUCTANCE_CROSS
366        }
367    }
368
369    fn build_laplacian(&self) -> Vec<f64> {
370        let n = self.nr_cpus;
371        let mut l = vec![0.0f64; n * n];
372        for i in 0..n {
373            for j in (i + 1)..n {
374                let w = self.conductance(i, j);
375                l[i * n + j] = -w;
376                l[j * n + i] = -w;
377                l[i * n + i] += w;
378                l[j * n + j] += w;
379            }
380        }
381        l
382    }
383
384    // ---- T2: emergent domain cut (SOSA min-conductance) --------------------
385    // The discrete cache domain layer is replaced by domains that EMERGE from the
386    // cache graph. The boundary is the min-conductance cut: phi = cut_weight /
387    // min(vol_a, vol_b). Low phi = a loosely-coupled seam = a real domain edge;
388    // the phi of the cut IS the cross-domain crossing price (THE FLAG: the price
389    // draws the boundary, no gate). Balance-free -- the seam falls where the
390    // silicon divides (asymmetric X3D / P+E included), not where volume balances.
391
392    // Conductance phi of a bipartition of `members` (in_side[c] = c is on side A).
393    // O(|members|^2); boot-time only. The random-walk variant (next) avoids the
394    // full scan for large N -- this exact form is the ground-truth + the price.
395    #[allow(dead_code)]
396    fn cut_conductance(&self, members: &[usize], in_side: &[bool]) -> f64 {
397        let mut cut = 0.0f64;
398        let (mut vol_a, mut vol_b) = (0.0f64, 0.0f64);
399        for &a in members {
400            for &b in members {
401                if a == b {
402                    continue;
403                }
404                let w = self.conductance(a, b);
405                if in_side[a] {
406                    vol_a += w;
407                } else {
408                    vol_b += w;
409                }
410                if in_side[a] != in_side[b] {
411                    cut += w; // each crossing edge counted twice (a,b and b,a)
412                }
413            }
414        }
415        cut /= 2.0;
416        let denom = vol_a.min(vol_b);
417        if denom <= 0.0 {
418            f64::INFINITY
419        } else {
420            cut / denom
421        }
422    }
423
424    // Fiedler vector (eigenvector of lambda_2) of the full graph -- the ground
425    // truth the scalable random-walk cut is cross-checked against. eigenvectors
426    // are column-major: component i of eigenvector k is eigenvectors[i*n + k].
427    #[allow(dead_code)]
428    fn fiedler_vector(eigenvalues: &[f64], eigenvectors: &[f64], n: usize) -> Vec<f64> {
429        let mut idx: Vec<usize> = (0..n).collect();
430        idx.sort_by(|&a, &b| {
431            eigenvalues[a]
432                .partial_cmp(&eigenvalues[b])
433                .unwrap_or(std::cmp::Ordering::Equal)
434        });
435        let k = if n >= 2 { idx[1] } else { idx[0] }; // 2nd smallest = lambda_2
436        (0..n).map(|i| eigenvectors[i * n + k]).collect()
437    }
438
439    // Single-level min-conductance cut of `members` via the Fiedler sweep: order
440    // the members by their Fiedler component, sweep every prefix as side A, keep
441    // the split with the lowest phi. Balance-free. Returns (side_a, side_b, phi),
442    // or None for a singleton. fvec is indexed by global CPU id.
443    #[allow(dead_code)]
444    fn fiedler_sweep_cut(
445        &self,
446        members: &[usize],
447        fvec: &[f64],
448    ) -> Option<(Vec<usize>, Vec<usize>, f64)> {
449        if members.len() < 2 {
450            return None;
451        }
452        let mut ordered = members.to_vec();
453        ordered.sort_by(|&a, &b| {
454            fvec[a]
455                .partial_cmp(&fvec[b])
456                .unwrap_or(std::cmp::Ordering::Equal)
457        });
458        let mut in_side = vec![false; self.nr_cpus];
459        let (mut best_phi, mut best_k) = (f64::INFINITY, 1usize);
460        for k in 1..ordered.len() {
461            in_side[ordered[k - 1]] = true; // grow the prefix one vertex
462            let phi = self.cut_conductance(members, &in_side);
463            if phi < best_phi {
464                best_phi = phi;
465                best_k = k;
466            }
467        }
468        Some((
469            ordered[..best_k].to_vec(),
470            ordered[best_k..].to_vec(),
471            best_phi,
472        ))
473    }
474
475    // True when every member shares one L2 group -- the atomic leaf. L2 siblings
476    // are maximally coupled; there is no meaningful seam to find below them.
477    #[allow(dead_code)]
478    fn all_same_l2(&self, members: &[usize]) -> bool {
479        members
480            .windows(2)
481            .all(|w| self.l2_domain[w[0]] == self.l2_domain[w[1]])
482    }
483
484    // Min-conductance cut of an ARBITRARY CPU subset (recursion-safe). The global
485    // Fiedler degrades inside a subtree, so this builds the INDUCED Laplacian on
486    // `members`, eigendecomposes it, and sweeps by the SUBSET's own Fiedler. (2d
487    // replaces these internals with a local random walk -- no eigensolve, so it
488    // scales; the tree-builder is agnostic to which produces the cut.) Returns
489    // global CPU ids.
490    #[allow(dead_code)]
491    fn domain_cut(&self, members: &[usize]) -> Option<(Vec<usize>, Vec<usize>, f64)> {
492        let k = members.len();
493        if k < 2 {
494            return None;
495        }
496        let mut lap = vec![0.0f64; k * k]; // induced Laplacian, local-indexed 0..k
497        for ia in 0..k {
498            for ib in (ia + 1)..k {
499                let w = self.conductance(members[ia], members[ib]);
500                lap[ia * k + ib] = -w;
501                lap[ib * k + ia] = -w;
502                lap[ia * k + ia] += w;
503                lap[ib * k + ib] += w;
504            }
505        }
506        let (ev, evec) = Self::symmetric_eigen(&lap, k);
507        let fsub = Self::fiedler_vector(&ev, &evec, k); // local-indexed
508        let mut order: Vec<usize> = (0..k).collect();
509        order.sort_by(|&a, &b| {
510            fsub[a]
511                .partial_cmp(&fsub[b])
512                .unwrap_or(std::cmp::Ordering::Equal)
513        });
514        let mut in_side = vec![false; self.nr_cpus]; // global-indexed for cut_conductance
515        let (mut best_phi, mut best_k) = (f64::INFINITY, 1usize);
516        for s in 1..k {
517            in_side[members[order[s - 1]]] = true; // grow the prefix (global id)
518            let phi = self.cut_conductance(members, &in_side);
519            if phi < best_phi {
520                best_phi = phi;
521                best_k = s;
522            }
523        }
524        let side_a: Vec<usize> = order[..best_k].iter().map(|&li| members[li]).collect();
525        let side_b: Vec<usize> = order[best_k..].iter().map(|&li| members[li]).collect();
526        Some((side_a, side_b, best_phi))
527    }
528
529    // Below this many CPUs the exact eigen cut is cheap, so use it; above it,
530    // the O(n^3) Jacobi is the wall and the random-walk cut takes over. This is an
531    // implementation dispatch on cost, not a placement gate (THE FLAG untouched).
532    const WALK_CUT_THRESHOLD: usize = 64;
533
534    // SCALABLE min-conductance cut: the SAME sweep as domain_cut, but the vertex
535    // ordering comes from a RANDOM WALK instead of an eigendecomposition -- no
536    // O(n^3) eigensolve, so it scales. Power iteration on the NORMALIZED operator
537    // M = 2I - L_sym, where L_sym = I - D^{-1/2} A D^{-1/2} is the symmetric
538    // normalized Laplacian (eigenvalues in [0,2] regardless of edge-weight scale,
539    // so stiff L2 edges don't dominate the shift the way a raw cI - L would). Its
540    // null eigenvector is D^{1/2}*1 (the walk's stationary distribution, deflated
541    // out each step); the next is the normalized Fiedler -- the iterate converges
542    // to it with a gap set by the CONDUCTANCE structure, not the weight magnitude.
543    // The sweep order is f[i] = y[i] / sqrt(deg[i]) (un-normalizing back to the
544    // walk eigenvector). Deterministic hash start (not a ramp -- ramps can align
545    // with a non-Fiedler mode) so a topology yields the same domains every boot.
546    // Cross-checked against domain_cut as ground truth (the T2 gate).
547    #[allow(dead_code)]
548    fn walk_cut(&self, members: &[usize]) -> Option<(Vec<usize>, Vec<usize>, f64)> {
549        let k = members.len();
550        if k < 2 {
551            return None;
552        }
553        let mut adj = vec![0.0f64; k * k];
554        let mut deg = vec![0.0f64; k];
555        for ia in 0..k {
556            for ib in 0..k {
557                if ia == ib {
558                    continue;
559                }
560                let w = self.conductance(members[ia], members[ib]);
561                adj[ia * k + ib] = w;
562                deg[ia] += w;
563            }
564        }
565        // D^{-1/2} and the stationary direction D^{1/2}*1 (L_sym's null vector).
566        let dis: Vec<f64> = deg
567            .iter()
568            .map(|&d| if d > 0.0 { 1.0 / d.sqrt() } else { 0.0 })
569            .collect();
570        let dsq: Vec<f64> = deg.iter().map(|&d| d.sqrt()).collect();
571        let dsq_sq: f64 = dsq.iter().map(|x| x * x).sum::<f64>().max(1e-30);
572        // Deflate out the stationary (null) component each step.
573        let deflate = |v: &mut [f64]| {
574            let dot: f64 = v.iter().zip(&dsq).map(|(a, b)| a * b).sum();
575            let coef = dot / dsq_sq;
576            for i in 0..k {
577                v[i] -= coef * dsq[i];
578            }
579        };
580        let mut y: Vec<f64> = (0..k)
581            .map(|i| {
582                let h = (i as u64).wrapping_mul(2654435761) & 0xffff;
583                h as f64 / 65535.0 - 0.5
584            })
585            .collect();
586        deflate(&mut y);
587        // A weak cut has a tiny normalized eigenvalue gap, so the per-iteration
588        // rate is near 1 and convergence can take thousands of steps -- iterate to
589        // CONVERGENCE (the direction stops moving), not a fixed count, capped high.
590        // Each step is O(k^2); a one-time boot cost, well under a millisecond.
591        const MAX_ITERS: usize = 20_000;
592        const TOL: f64 = 1e-10;
593        let mut w = vec![0.0f64; k];
594        for _ in 0..MAX_ITERS {
595            // (M y)[i] = y[i] + D^{-1/2}_i * sum_j A_ij * D^{-1/2}_j * y[j]
596            for i in 0..k {
597                let row = &adj[i * k..i * k + k];
598                let mut s = 0.0;
599                for j in 0..k {
600                    s += row[j] * dis[j] * y[j];
601                }
602                w[i] = y[i] + dis[i] * s;
603            }
604            deflate(&mut w);
605            let norm = w.iter().map(|x| x * x).sum::<f64>().sqrt();
606            if norm < 1e-12 {
607                break;
608            }
609            // cos angle between the new direction and the old unit vector y.
610            let dot: f64 = w.iter().zip(&y).map(|(a, b)| a * b).sum::<f64>() / norm;
611            for i in 0..k {
612                y[i] = w[i] / norm;
613            }
614            if 1.0 - dot.abs() < TOL {
615                break;
616            }
617        }
618        // Sweep order by the un-normalized walk eigenvector f[i] = y[i]/sqrt(deg).
619        let f: Vec<f64> = (0..k).map(|i| y[i] * dis[i]).collect();
620        let mut order: Vec<usize> = (0..k).collect();
621        order.sort_by(|&a, &b| f[a].partial_cmp(&f[b]).unwrap_or(std::cmp::Ordering::Equal));
622        let mut in_side = vec![false; self.nr_cpus];
623        let (mut best_phi, mut best_k) = (f64::INFINITY, 1usize);
624        for s in 1..k {
625            in_side[members[order[s - 1]]] = true;
626            let phi = self.cut_conductance(members, &in_side);
627            if phi < best_phi {
628                best_phi = phi;
629                best_k = s;
630            }
631        }
632        Some((
633            order[..best_k].iter().map(|&li| members[li]).collect(),
634            order[best_k..].iter().map(|&li| members[li]).collect(),
635            best_phi,
636        ))
637    }
638
639    // Dispatch: exact eigen cut below the threshold, scalable random-walk cut
640    // above. Both produce the same boundary on real cache graphs (gate-tested).
641    #[allow(dead_code)]
642    fn best_cut(&self, members: &[usize]) -> Option<(Vec<usize>, Vec<usize>, f64)> {
643        if members.len() <= Self::WALK_CUT_THRESHOLD {
644            self.domain_cut(members)
645        } else {
646            self.walk_cut(members)
647        }
648    }
649
650    // Recurse the cut into the emergent domain tree. Leaf when the members are a
651    // single L2 group (or one CPU) -- maximally coupled, no seam. Each Cut carries
652    // its phi (the crossing price). This IS de-facto NUMA: the boundary is drawn
653    // by the conductance landscape, not a hardcoded topology table.
654    #[allow(dead_code)]
655    pub fn build_domain_tree(&self, members: &[usize]) -> DomainNode {
656        if members.len() <= 1 || self.all_same_l2(members) {
657            return DomainNode::Leaf(members.to_vec());
658        }
659        match self.best_cut(members) {
660            Some((a, b, phi)) if !a.is_empty() && !b.is_empty() => DomainNode::Cut {
661                phi,
662                left: Box::new(self.build_domain_tree(&a)),
663                right: Box::new(self.build_domain_tree(&b)),
664            },
665            _ => DomainNode::Leaf(members.to_vec()),
666        }
667    }
668
669    // Compute the emergent domain tree over all online CPUs -- the de-facto-NUMA
670    // hierarchy from which T3's bounded-local steal reads its locality clusters
671    // and per-cut crossing prices. Public: the T2 -> T3 hand-off point.
672    pub fn compute_domain_tree(&self) -> DomainNode {
673        self.build_domain_tree(&(0..self.nr_cpus).collect::<Vec<_>>())
674    }
675
676    // Log the emergent domains at boot -- observability that the tree the steal
677    // will climb matches the silicon: atomic-domain count, cut depth, the
678    // crossing-price (phi) range, and the first leaves.
679    pub fn log_domains(&self, tree: &DomainNode) {
680        let leaves = tree.leaves();
681        let phis = tree.cut_phis();
682        let (pmin, pmax) = phis.iter().fold((f64::INFINITY, 0.0f64), |(lo, hi), &p| {
683            (lo.min(p), hi.max(p))
684        });
685        log_info!(
686            "EMERGENT DOMAINS: {} atomic, {} cuts, crossing phi {:.4}..{:.4}",
687            leaves.len(),
688            phis.len(),
689            if phis.is_empty() { 0.0 } else { pmin },
690            pmax
691        );
692        let preview: Vec<String> = leaves.iter().take(8).map(|l| format!("{:?}", l)).collect();
693        log_info!("EMERGENT DOMAINS: leaves {}", preview.join(" "));
694    }
695
696    // T3b.1: the per-CPU-pair crossing-price matrix the bounded steal reads.
697    // m[i*n + j] = (phi * 1e6) of the LCA cut separating CPU i and CPU j -- the
698    // price to steal across that emergent domain boundary. A LOW phi is a loose
699    // seam (a major boundary -- socket / cross-L3 -- far, needs more imbalance to
700    // cross); a HIGH phi is a tight seam (near). Same-leaf pairs share NO cut:
701    // sentinel u32::MAX = maximally local, the steal never has to "cross" for them.
702    // Replaces the discrete domain map's discrete same/different-cache domain test with a continuous,
703    // emergent boundary price (THE FLAG: priced, not gated).
704    pub fn domain_cross_phi_matrix(&self, tree: &DomainNode) -> Vec<u32> {
705        let n = self.nr_cpus;
706        let mut m = vec![u32::MAX; n * n]; // default: no boundary (same leaf)
707        Self::fill_cross_phi(tree, n, &mut m);
708        m
709    }
710
711    fn fill_cross_phi(node: &DomainNode, n: usize, m: &mut [u32]) {
712        if let DomainNode::Cut { phi, left, right } = node {
713            let lc: Vec<usize> = left.leaves().concat();
714            let rc: Vec<usize> = right.leaves().concat();
715            let p = (phi * 1_000_000.0).round().clamp(0.0, u32::MAX as f64) as u32;
716            for &a in &lc {
717                for &b in &rc {
718                    m[a * n + b] = p; // pairs whose lowest common ancestor IS this cut
719                    m[b * n + a] = p;
720                }
721            }
722            Self::fill_cross_phi(left, n, m);
723            Self::fill_cross_phi(right, n, m);
724        }
725    }
726
727    // Number of emergent OVERFLOW DOMAINS to target = distinct L3 groups (the old
728    // per-domain count), so re-keying the overflow DSQs preserves granularity.
729    pub fn overflow_domain_count(&self) -> usize {
730        let mut v = self.llc_domain.clone();
731        v.sort_unstable();
732        v.dedup();
733        v.len().max(1)
734    }
735
736    // T3b.2: partition CPUs into emergent OVERFLOW DOMAINS -- the the discrete domain map
737    // replacement. Descend the tree from the root, repeatedly splitting the
738    // frontier subtree whose cut has the LOWEST phi (the coarsest, most-separable
739    // seam) until `target` domains exist or no cut remains. Each resulting subtree
740    // is one overflow domain; dom[cpu] is its id. The granularity is the old L3
741    // count, but the boundary is now drawn by the emergent tree, not the discrete domain map.
742    pub fn domain_partition(&self, tree: &DomainNode, target: usize) -> Vec<u32> {
743        let mut frontier: Vec<&DomainNode> = vec![tree];
744        while frontier.len() < target.max(1) {
745            let mut best: Option<(usize, f64)> = None;
746            for (i, node) in frontier.iter().enumerate() {
747                if let DomainNode::Cut { phi, .. } = node {
748                    if best.map_or(true, |(_, bp)| *phi < bp) {
749                        best = Some((i, *phi));
750                    }
751                }
752            }
753            let Some((idx, _)) = best else { break }; // no cuts left to split
754            let node = frontier[idx];
755            if let DomainNode::Cut { left, right, .. } = node {
756                frontier.swap_remove(idx);
757                frontier.push(left.as_ref());
758                frontier.push(right.as_ref());
759            }
760        }
761        let n = self.nr_cpus;
762        let mut dom = vec![0u32; n];
763        for (id, node) in frontier.iter().enumerate() {
764            for leaf in node.leaves() {
765                for c in leaf {
766                    if c < n {
767                        dom[c] = id as u32;
768                    }
769                }
770            }
771        }
772        dom
773    }
774
775    // SYMMETRIC EIGENDECOMPOSITION VIA JACOBI ROTATIONS
776    // RETURNS (eigenvalues, eigenvectors_column_major)
777    // SUITABLE FOR n <= 256. NO EXTERNAL DEPENDENCIES.
778    fn symmetric_eigen(mat: &[f64], n: usize) -> (Vec<f64>, Vec<f64>) {
779        let mut a = mat.to_vec();
780        // EIGENVECTORS START AS IDENTITY
781        let mut v = vec![0.0f64; n * n];
782        for i in 0..n {
783            v[i * n + i] = 1.0;
784        }
785
786        let max_iter = 100 * n * n;
787        for _ in 0..max_iter {
788            // FIND LARGEST OFF-DIAGONAL ELEMENT
789            let mut max_val = 0.0f64;
790            let mut p = 0;
791            let mut q = 1;
792            for i in 0..n {
793                for j in (i + 1)..n {
794                    let val = a[i * n + j].abs();
795                    if val > max_val {
796                        max_val = val;
797                        p = i;
798                        q = j;
799                    }
800                }
801            }
802            if max_val < 1e-12 {
803                break;
804            }
805
806            // COMPUTE ROTATION
807            let app = a[p * n + p];
808            let aqq = a[q * n + q];
809            let apq = a[p * n + q];
810            let theta = if (app - aqq).abs() < 1e-15 {
811                std::f64::consts::FRAC_PI_4
812            } else {
813                0.5 * (2.0 * apq / (app - aqq)).atan()
814            };
815            let c = theta.cos();
816            let s = theta.sin();
817
818            // APPLY ROTATION TO A
819            for i in 0..n {
820                if i == p || i == q {
821                    continue;
822                }
823                let aip = a[i * n + p];
824                let aiq = a[i * n + q];
825                a[i * n + p] = c * aip + s * aiq;
826                a[p * n + i] = a[i * n + p];
827                a[i * n + q] = -s * aip + c * aiq;
828                a[q * n + i] = a[i * n + q];
829            }
830            let new_pp = c * c * app + 2.0 * s * c * apq + s * s * aqq;
831            let new_qq = s * s * app - 2.0 * s * c * apq + c * c * aqq;
832            a[p * n + p] = new_pp;
833            a[q * n + q] = new_qq;
834            a[p * n + q] = 0.0;
835            a[q * n + p] = 0.0;
836
837            // ACCUMULATE EIGENVECTORS
838            for i in 0..n {
839                let vip = v[i * n + p];
840                let viq = v[i * n + q];
841                v[i * n + p] = c * vip + s * viq;
842                v[i * n + q] = -s * vip + c * viq;
843            }
844        }
845
846        let eigenvalues: Vec<f64> = (0..n).map(|i| a[i * n + i]).collect();
847        (eigenvalues, v)
848    }
849
850    // COMPUTE LAPLACIAN PSEUDOINVERSE FROM EIGENDECOMPOSITION
851    fn compute_pseudoinverse(eigenvalues: &[f64], eigenvectors: &[f64], n: usize) -> Vec<f64> {
852        let mut l_pinv = vec![0.0f64; n * n];
853        for k in 0..n {
854            if eigenvalues[k].abs() < 1e-8 {
855                continue; // SKIP NULL EIGENVALUE (CONNECTED GRAPH HAS ONE)
856            }
857            let inv_lambda = 1.0 / eigenvalues[k];
858            for i in 0..n {
859                for j in 0..n {
860                    l_pinv[i * n + j] +=
861                        inv_lambda * eigenvectors[i * n + k] * eigenvectors[j * n + k];
862                }
863            }
864        }
865        l_pinv
866    }
867
868    // COMPUTE ALL-PAIRS EFFECTIVE RESISTANCE FROM PSEUDOINVERSE
869    // R_eff(i,j) = L+[i,i] + L+[j,j] - 2*L+[i,j]
870    fn extract_reff(l_pinv: &[f64], n: usize) -> Vec<f64> {
871        let mut r = vec![0.0f64; n * n];
872        for i in 0..n {
873            for j in (i + 1)..n {
874                let val = l_pinv[i * n + i] + l_pinv[j * n + j] - 2.0 * l_pinv[i * n + j];
875                r[i * n + j] = val.max(0.0);
876                r[j * n + i] = r[i * n + j];
877            }
878        }
879        r
880    }
881
882    // BUILD PER-CPU AFFINITY RANK: FOR EACH CPU, ALL OTHERS SORTED BY R_EFF
883    // Returns flat array: affinity_rank[cpu * nr_cpus + slot] = target_cpu
884    fn build_affinity_rank(reff: &[f64], n: usize) -> Vec<u32> {
885        let mut rank = vec![0u32; n * n];
886        for cpu in 0..n {
887            let mut others: Vec<(u64, u32)> = (0..n)
888                .filter(|&c| c != cpu)
889                .map(|c| {
890                    // SORT KEY: R_EFF AS FIXED-POINT TO AVOID FLOAT COMPARISON ISSUES
891                    let key = (reff[cpu * n + c] * 1_000_000.0) as u64;
892                    (key, c as u32)
893                })
894                .collect();
895            others.sort();
896            for (slot, &(_, target)) in others.iter().enumerate() {
897                rank[cpu * n + slot] = target;
898            }
899            // FILL REMAINING SLOTS WITH SENTINEL
900            for slot in others.len()..n {
901                rank[cpu * n + slot] = u32::MAX;
902            }
903        }
904        rank
905    }
906
907    // COMPUTE RESISTANCE AFFINITY: FULL PIPELINE
908    // Returns (reff_matrix, affinity_rank, spectrum) for use by BPF and scheduler.
909    // Spectrum carries lambda_2 (Fiedler value) and its derived tau_ns, used as
910    // the universal topology time constant for every core-scaled knob.
911    pub fn compute_resistance_affinity(&self) -> (Vec<f64>, Vec<u32>, TopologySpectrum) {
912        let n = self.nr_cpus;
913        let laplacian = self.build_laplacian();
914        let (eigenvalues, eigenvectors) = Self::symmetric_eigen(&laplacian, n);
915        let fiedler = extract_fiedler(&eigenvalues);
916        let tau_ns = compute_tau_ns(fiedler, n);
917        let l_pinv = Self::compute_pseudoinverse(&eigenvalues, &eigenvectors, n);
918        let reff = Self::extract_reff(&l_pinv, n);
919        let rank = Self::build_affinity_rank(&reff, n);
920        let codel_eq_ns = compute_codel_eq_ns(&eigenvalues, n, tau_ns);
921        // Phi FIX B: distance scale calibrated so the most distant pair (max R_eff)
922        // maps to ~tau of required steal-wait, an SMT sibling (R_eff ~ 0) to ~0. Only
923        // sustained backlog (~tau) justifies a far move; a single queued slice does
924        // not. reff_norm uses the same 1e6 scale the BPF reff_value map stores.
925        // ALWAYS computed: on a single-L3 part the most distant pair is the cross-L2
926        // (cross-core) max, so reff_norm auto-calibrates the brake to the L2 boundary
927        // instead of vanishing -- the continuous metric drives placement on EVERY
928        // processor, with no binary topology gate in front of it (THE FLAG).
929        let max_reff = reff.iter().cloned().fold(0.0f64, f64::max);
930        let reff_norm = ((max_reff * 1_000_000.0).round() as u64).max(1);
931        let phi_dist_scale_q16 = tau_ns.saturating_mul(65536) / reff_norm;
932        (
933            reff,
934            rank,
935            TopologySpectrum {
936                fiedler,
937                tau_ns,
938                codel_eq_ns,
939                phi_dist_scale_q16,
940            },
941        )
942    }
943
944    // WRITE AFFINITY RANK TO BPF MAP
945    // affinity_rank[cpu * MAX_AFFINITY_CANDIDATES + slot] = target_cpu
946    //
947    // Emit the full sorted R_eff peer list per CPU, capped at the BPF
948    // table width (MAX_AFFINITY_CANDIDATES). Slots beyond the actual
949    // topology end (nr_cpus - 1) are written as explicit u32::MAX
950    // sentinels so the BPF early-exit fires correctly -- map zero-init
951    // would otherwise alias to "CPU 0" and silently mis-route.
952    pub fn populate_affinity_rank_map(
953        &self,
954        sched: &Scheduler,
955        reff: &[f64],
956        rank: &[u32],
957        phi_dist_scale_q16: u64,
958        domain_phi: &[u32],
959    ) -> Result<()> {
960        let stride = crate::bpf_intf::MAX_AFFINITY_CANDIDATES as usize;
961        let valid = self.nr_cpus.saturating_sub(1).min(stride);
962        for cpu in 0..self.nr_cpus {
963            for slot in 0..valid {
964                let val = rank[cpu * self.nr_cpus + slot];
965                sched.write_affinity_rank(cpu as u32, slot as u32, val)?;
966                // T3b.1: the emergent-domain crossing price to this ranked peer,
967                // 1:1 with the rank slot (sentinel for an out-of-range peer id).
968                let dphi = domain_phi
969                    .get(cpu * self.nr_cpus + val as usize)
970                    .copied()
971                    .unwrap_or(u32::MAX);
972                sched.write_domain_phi(cpu as u32, slot as u32, dphi)?;
973                // FOLD THE PHI DISTANCE PENALTY AT INIT: reff_value stores the
974                // final steal extra-wait in ns, (R_eff * phi_dist_scale_q16) >> 16,
975                // so the BPF steal does one indexed read and no multiply. The 1e6
976                // scale matches build_affinity_rank's sort key. phi_dist_scale_q16
977                // is 0 on monolithic / --phi-scale 0 -> every penalty 0 -> flat
978                // codel_target (exact prior behavior).
979                let r_scaled = (reff[cpu * self.nr_cpus + val as usize] * 1_000_000.0)
980                    .round()
981                    .clamp(0.0, u32::MAX as f64) as u64;
982                let dist_extra =
983                    (r_scaled.saturating_mul(phi_dist_scale_q16) >> 16).min(u32::MAX as u64) as u32;
984                sched.write_reff_value(cpu as u32, slot as u32, dist_extra)?;
985            }
986            for slot in valid..stride {
987                sched.write_affinity_rank(cpu as u32, slot as u32, u32::MAX)?;
988                sched.write_reff_value(cpu as u32, slot as u32, u32::MAX)?;
989                sched.write_domain_phi(cpu as u32, slot as u32, u32::MAX)?;
990            }
991        }
992        Ok(())
993    }
994
995    pub fn log_resistance_affinity(&self, reff: &[f64], rank: &[u32], spectrum: TopologySpectrum) {
996        log_info!(
997            "TOPOLOGY SPECTRUM: lambda2={:.4} tau={}ms codel_eq={}us",
998            spectrum.fiedler,
999            spectrum.tau_ns / 1_000_000,
1000            spectrum.codel_eq_ns / 1_000
1001        );
1002        let n = self.nr_cpus;
1003        // LOG TOP 3 AFFINITIES FOR CPU 0
1004        let mut parts = Vec::new();
1005        for slot in 0..3.min(n - 1) {
1006            let target = rank[slot] as usize;
1007            if target >= n {
1008                break;
1009            }
1010            let r = reff[target];
1011            parts.push(format!("CPU{}(R={:.3})", target, r));
1012        }
1013        log_info!("RESISTANCE AFFINITY: CPU 0 rank: {}", parts.join(", "));
1014
1015        // LOG L2 VS NON-L2 R_EFF FOR FIRST CPU
1016        if n >= 2 {
1017            let l2_sib = rank[0] as usize;
1018            let non_l2 = rank[1.min(n - 2)] as usize;
1019            log_info!(
1020                "RESISTANCE AFFINITY: R_eff L2={:.4} non-L2={:.4} ratio={:.1}x",
1021                reff[l2_sib],
1022                reff[non_l2],
1023                if reff[l2_sib] > 0.0 {
1024                    reff[non_l2] / reff[l2_sib]
1025                } else {
1026                    0.0
1027                }
1028            );
1029        }
1030    }
1031
1032    pub fn log_summary(&self) {
1033        for (gid, members) in self.l2_groups.iter().enumerate() {
1034            let cpus: Vec<String> = members.iter().map(|c| c.to_string()).collect();
1035            log_info!("L2 GROUP {}: [{}]", gid, cpus.join(","));
1036        }
1037        log_info!(
1038            "L2 GROUPS: {} across {} CPUs, {} SOCKETS",
1039            self.l2_groups.len(),
1040            self.nr_cpus,
1041            self.nr_sockets
1042        );
1043        let mut llc = self.llc_domain.clone();
1044        llc.sort_unstable();
1045        llc.dedup();
1046        log_info!(
1047            "LLC DOMAINS: {} (L3 rung always-on, continuous Phi)",
1048            llc.len()
1049        );
1050    }
1051}
1052
1053// PARSE KERNEL CPU LIST FORMAT: "0,6" or "0-2,6-8" or "3"
1054fn parse_cpu_list(s: &str) -> Vec<u32> {
1055    let mut result = Vec::new();
1056    for part in s.split(',') {
1057        let part = part.trim();
1058        if part.is_empty() {
1059            continue;
1060        }
1061        if let Some((start, end)) = part.split_once('-') {
1062            if let (Ok(s), Ok(e)) = (start.parse::<u32>(), end.parse::<u32>()) {
1063                for cpu in s..=e {
1064                    result.push(cpu);
1065                }
1066            }
1067        } else if let Ok(cpu) = part.parse::<u32>() {
1068            result.push(cpu);
1069        }
1070    }
1071    result.sort();
1072    result.dedup();
1073    result
1074}
1075
1076#[cfg(test)]
1077mod t2_cut_tests {
1078    use super::*;
1079
1080    // 8 CPUs, no SMT (each its own L2), one socket, two L3 groups: {0..3},{4..7}.
1081    // Intra-L3 edges weigh CONDUCTANCE_LLC (3.0), inter-L3 same-socket weigh
1082    // CONDUCTANCE_SOCKET (1.0) -- two clusters joined by weak edges.
1083    fn synth_2domain() -> CpuTopology {
1084        CpuTopology {
1085            nr_cpus: 8,
1086            l2_domain: (0..8u32).collect(),
1087            l2_groups: Vec::new(),
1088            socket_domain: vec![0u32; 8],
1089            llc_domain: vec![0, 0, 0, 0, 1, 1, 1, 1],
1090            nr_sockets: 1,
1091        }
1092    }
1093
1094    #[test]
1095    fn min_conductance_cut_splits_on_llc() {
1096        let t = synth_2domain();
1097        let lap = t.build_laplacian();
1098        let (ev, evec) = CpuTopology::symmetric_eigen(&lap, 8);
1099        let fvec = CpuTopology::fiedler_vector(&ev, &evec, 8);
1100        let members: Vec<usize> = (0..8).collect();
1101        let (a, b, phi) = t.fiedler_sweep_cut(&members, &fvec).expect("cut");
1102        let (mut sa, mut sb) = (a.clone(), b.clone());
1103        sa.sort();
1104        sb.sort();
1105        let (llc0, llc1) = (vec![0usize, 1, 2, 3], vec![4usize, 5, 6, 7]);
1106        assert!(
1107            (sa == llc0 && sb == llc1) || (sa == llc1 && sb == llc0),
1108            "expected the L3 boundary, got {:?} | {:?}",
1109            sa,
1110            sb
1111        );
1112        assert!(phi.is_finite() && phi > 0.0 && phi < 1.0, "phi = {}", phi);
1113    }
1114
1115    #[test]
1116    fn cut_conductance_zero_weight_guard() {
1117        // A singleton member set has no valid bipartition -> None, not a panic.
1118        let t = synth_2domain();
1119        assert!(t.fiedler_sweep_cut(&[3usize], &vec![0.0; 8]).is_none());
1120    }
1121
1122    // 8 CPUs WITH SMT: 4 L2 pairs {0,1}{2,3}{4,5}{6,7}, two L3 groups {0..3},
1123    // {4..7}, one socket. L2-sib 1000, same-L3 cross-L2 3.0, cross-L3 same-socket
1124    // 1.0 -- a clean two-level hierarchy whose tree should be L3 over L2 pairs.
1125    fn synth_smt_2domain() -> CpuTopology {
1126        CpuTopology {
1127            nr_cpus: 8,
1128            l2_domain: vec![0, 0, 1, 1, 2, 2, 3, 3],
1129            l2_groups: Vec::new(),
1130            socket_domain: vec![0u32; 8],
1131            llc_domain: vec![0, 0, 0, 0, 1, 1, 1, 1],
1132            nr_sockets: 1,
1133        }
1134    }
1135
1136    #[test]
1137    fn top_cut_is_the_l3_seam() {
1138        let t = synth_smt_2domain();
1139        let (a, b, phi) = t.domain_cut(&(0..8).collect::<Vec<_>>()).expect("cut");
1140        let (mut sa, mut sb) = (a.clone(), b.clone());
1141        sa.sort();
1142        sb.sort();
1143        let (l3a, l3b) = (vec![0usize, 1, 2, 3], vec![4usize, 5, 6, 7]);
1144        assert!(
1145            (sa == l3a && sb == l3b) || (sa == l3b && sb == l3a),
1146            "top cut should be the L3 seam, got {:?} | {:?}",
1147            sa,
1148            sb
1149        );
1150        assert!(phi.is_finite() && phi > 0.0 && phi < 1.0, "phi = {}", phi);
1151    }
1152
1153    #[test]
1154    fn domain_tree_leaves_are_l2_groups() {
1155        let t = synth_smt_2domain();
1156        let tree = t.build_domain_tree(&(0..8).collect::<Vec<_>>());
1157        let mut leaves: Vec<Vec<usize>> = tree
1158            .leaves()
1159            .into_iter()
1160            .map(|mut l| {
1161                l.sort();
1162                l
1163            })
1164            .collect();
1165        leaves.sort();
1166        assert_eq!(
1167            leaves,
1168            vec![vec![0, 1], vec![2, 3], vec![4, 5], vec![6, 7]],
1169            "leaves should be the 4 L2 groups"
1170        );
1171        // The root cut (L3 seam) is the cheapest crossing: coarser seam, lower phi.
1172        let phis = tree.cut_phis();
1173        assert!(!phis.is_empty(), "tree should have cuts");
1174        let root_phi = phis[0];
1175        assert!(
1176            phis.iter().all(|&p| root_phi <= p + 1e-9),
1177            "root cut should be the lowest phi, got {:?}",
1178            phis
1179        );
1180    }
1181
1182    // Two cuts induce the same bipartition (ignoring which side is A vs B)?
1183    fn same_bipartition(
1184        a: &(Vec<usize>, Vec<usize>, f64),
1185        b: &(Vec<usize>, Vec<usize>, f64),
1186    ) -> bool {
1187        let norm = |c: &(Vec<usize>, Vec<usize>, f64)| {
1188            let (mut x, mut y) = (c.0.clone(), c.1.clone());
1189            x.sort();
1190            y.sort();
1191            if x < y {
1192                (x, y)
1193            } else {
1194                (y, x)
1195            }
1196        };
1197        norm(a) == norm(b)
1198    }
1199
1200    #[test]
1201    fn walk_cut_matches_eigen_cut_smt() {
1202        let t = synth_smt_2domain();
1203        let m: Vec<usize> = (0..8).collect();
1204        let eigen = t.domain_cut(&m).expect("eigen");
1205        let walk = t.walk_cut(&m).expect("walk");
1206        assert!(
1207            same_bipartition(&eigen, &walk),
1208            "walk {:?}|{:?} != eigen {:?}|{:?}",
1209            walk.0,
1210            walk.1,
1211            eigen.0,
1212            eigen.1
1213        );
1214    }
1215
1216    // 16 CPUs, 2 sockets {0..7}{8..15}, 4 L3 groups, 8 L2 pairs. The weakest seam
1217    // is cross-socket (CONDUCTANCE_CROSS 0.3) -- both cuts must land there,
1218    // exercising the random walk on a deeper graph than the 8-CPU case.
1219    fn synth_2socket() -> CpuTopology {
1220        CpuTopology {
1221            nr_cpus: 16,
1222            l2_domain: (0..16).map(|c| (c / 2) as u32).collect(),
1223            l2_groups: Vec::new(),
1224            socket_domain: (0..16).map(|c| (c / 8) as u32).collect(),
1225            llc_domain: (0..16).map(|c| (c / 4) as u32).collect(),
1226            nr_sockets: 2,
1227        }
1228    }
1229
1230    #[test]
1231    fn walk_cut_matches_eigen_cut_2socket() {
1232        let t = synth_2socket();
1233        let m: Vec<usize> = (0..16).collect();
1234        let eigen = t.domain_cut(&m).expect("eigen");
1235        let walk = t.walk_cut(&m).expect("walk");
1236        let (mut wa, mut wb) = (walk.0.clone(), walk.1.clone());
1237        wa.sort();
1238        wb.sort();
1239        let (s0, s1): (Vec<usize>, Vec<usize>) = ((0..8).collect(), (8..16).collect());
1240        assert!(
1241            (wa == s0 && wb == s1) || (wa == s1 && wb == s0),
1242            "walk top cut should be the socket seam, got {:?}|{:?}",
1243            wa,
1244            wb
1245        );
1246        assert!(same_bipartition(&eigen, &walk), "walk != eigen on 2-socket");
1247    }
1248
1249    #[test]
1250    fn compute_domain_tree_public_wrapper() {
1251        let t = synth_smt_2domain();
1252        let tree = t.compute_domain_tree();
1253        assert_eq!(tree.leaves().len(), 4, "smt 2-domain -> 4 L2-group leaves");
1254    }
1255
1256    #[test]
1257    fn single_cpu_is_one_leaf() {
1258        let t = CpuTopology {
1259            nr_cpus: 1,
1260            l2_domain: vec![0],
1261            l2_groups: Vec::new(),
1262            socket_domain: vec![0],
1263            llc_domain: vec![0],
1264            nr_sockets: 1,
1265        };
1266        let tree = t.compute_domain_tree();
1267        assert_eq!(tree.leaves(), vec![vec![0usize]]);
1268        assert!(tree.cut_phis().is_empty(), "a single CPU has no cuts");
1269    }
1270
1271    #[test]
1272    fn cross_phi_matrix_prices_the_boundaries() {
1273        let t = synth_smt_2domain();
1274        let tree = t.compute_domain_tree();
1275        let m = t.domain_cross_phi_matrix(&tree);
1276        let n = 8;
1277        // Same leaf {0,1}: no boundary -> sentinel.
1278        assert_eq!(m[0 * n + 1], u32::MAX, "same-leaf pair must be sentinel");
1279        // Cross-L2 same-L3 (0,2) and cross-L3 (0,4): real, priced boundaries.
1280        assert_ne!(m[0 * n + 2], u32::MAX);
1281        assert_ne!(m[0 * n + 4], u32::MAX);
1282        // Cross-L2 is the TIGHTER (nearer) seam -> higher phi than cross-L3.
1283        assert!(
1284            m[0 * n + 2] > m[0 * n + 4],
1285            "cross-L2 phi {} should exceed cross-L3 phi {}",
1286            m[0 * n + 2],
1287            m[0 * n + 4]
1288        );
1289        // CPUs 2 and 3 are the same sibling L2 pair: identical crossing price from 0.
1290        assert_eq!(m[0 * n + 2], m[0 * n + 3]);
1291        // Symmetric.
1292        assert_eq!(m[0 * n + 4], m[4 * n + 0]);
1293    }
1294
1295    #[test]
1296    fn overflow_partition_matches_l3_groups() {
1297        let t = synth_smt_2domain();
1298        assert_eq!(t.overflow_domain_count(), 2, "two L3 groups");
1299        let tree = t.compute_domain_tree();
1300        let dom = t.domain_partition(&tree, 2);
1301        assert!(
1302            dom[0] == dom[1] && dom[1] == dom[2] && dom[2] == dom[3],
1303            "L3 group 0 is one overflow domain: {:?}",
1304            dom
1305        );
1306        assert!(
1307            dom[4] == dom[5] && dom[5] == dom[6] && dom[6] == dom[7],
1308            "L3 group 1 is one overflow domain: {:?}",
1309            dom
1310        );
1311        assert_ne!(dom[0], dom[4], "the two L3 groups are distinct domains");
1312        // target 1 -> a single overflow domain (monolithic re-key).
1313        let mono = t.domain_partition(&tree, 1);
1314        assert!(
1315            mono.iter().all(|&d| d == 0),
1316            "target 1 = one domain: {:?}",
1317            mono
1318        );
1319    }
1320}