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
75impl DomainNode {
76 // Flatten to the leaf CPU sets -- each an emergent atomic domain.
77 pub fn leaves(&self) -> Vec<Vec<usize>> {
78 match self {
79 DomainNode::Leaf(cpus) => vec![cpus.clone()],
80 DomainNode::Cut { left, right, .. } => {
81 let mut v = left.leaves();
82 v.extend(right.leaves());
83 v
84 }
85 }
86 }
87
88 // Every cut's phi -- the crossing-price ladder (coarser seams cost less, so
89 // phi rises with depth as the steal climbs toward more tightly-coupled work).
90 pub fn cut_phis(&self) -> Vec<f64> {
91 match self {
92 DomainNode::Leaf(_) => Vec::new(),
93 DomainNode::Cut { phi, left, right } => {
94 let mut v = vec![*phi];
95 v.extend(left.cut_phis());
96 v.extend(right.cut_phis());
97 v
98 }
99 }
100 }
101}
102
103fn extract_fiedler(eigenvalues: &[f64]) -> f64 {
104 // Jacobi RETURNS EIGENVALUES UNSORTED. FOR A CONNECTED LAPLACIAN THE
105 // SMALLEST EIGENVALUE IS 0 (SKIPPED VIA LAMBDA_ZERO_EPS). FOR A
106 // DISCONNECTED GRAPH (HOTPLUG PARTITION) SEVERAL EIGENVALUES ARE ~0;
107 // lambda_2 IS THE SMALLEST STRICTLY POSITIVE ONE.
108 let mut v: Vec<f64> = eigenvalues.to_vec();
109 v.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
110 v.into_iter()
111 .find(|&x| x > LAMBDA_ZERO_EPS)
112 .unwrap_or(LAMBDA_ZERO_EPS)
113}
114
115fn compute_tau_ns(fiedler: f64, n: usize) -> u64 {
116 // CAPACITY-AWARE: tau = TAU_SCALE_NS / sqrt(lambda_2 * N) -- the geometric
117 // mean of connectivity (1/lambda_2) and capacity (1/sqrt(N)). The old
118 // pure-connectivity law gave a well-connected but capacity-starved
119 // topology (a 2-core L2 pair, lambda_2=20) a tiny tau (8ms) and thus tight
120 // tolerances exactly where scarce CPUs need loose ones -- which the
121 // apply_tau_scaling floors then patched back up. sqrt(N) penalizes small N,
122 // so 2C loosens to ~25ms with no floor needed. The 12C reference is
123 // preserved: lambda_2=12, N=12 -> sqrt(144)=12 -> 160ms/12 = 13.3ms.
124 let denom = (fiedler.max(LAMBDA_ZERO_EPS) * (n.max(1) as f64)).sqrt();
125 let raw = TAU_SCALE_NS / denom.max(LAMBDA_ZERO_EPS);
126 (raw as u64).clamp(TAU_FLOOR_NS, TAU_CEIL_NS)
127}
128
129// CoDel TARGET EQUILIBRIUM FROM THE LAPLACIAN SPECTRUM.
130// FORMULA: c_eq = <R_eff> * 2m * tau
131// SPECTRAL FORM:
132// <R_eff> = Tr(L+) / N = (1/N) * sum_{lambda > 0} 1 / lambda
133// 2m = Tr(L) = sum_{lambda} lambda
134// tau = TAU_SCALE_NS / lambda_2 (already computed, in ns)
135//
136// PHYSICAL INTERPRETATION: c_eq is the natural commute-time scale of
137// the topology graph -- the average time it takes work to bounce
138// between two CPUs along the topology's slowest paths. The CoDel
139// target's mean-reverting equilibrium settles to this value in the
140// absence of disturbance, so the stall detector tightens around the
141// topology's intrinsic timescale instead of a hand-picked constant.
142//
143// CLAMPED TO [200us, 8ms] -- THE CoDel TARGET RANGE ITSELF.
144fn compute_codel_eq_ns(eigenvalues: &[f64], n: usize, tau_ns: u64) -> u64 {
145 if n == 0 {
146 return TAU_FLOOR_NS;
147 }
148 let mut sum_inv_lambda = 0.0f64;
149 let mut sum_lambda = 0.0f64;
150 for &lambda in eigenvalues {
151 sum_lambda += lambda;
152 if lambda > LAMBDA_ZERO_EPS {
153 sum_inv_lambda += 1.0 / lambda;
154 }
155 }
156 let avg_reff = sum_inv_lambda / n as f64;
157 let two_m = sum_lambda;
158 let raw_ns = avg_reff * two_m * tau_ns as f64;
159 (raw_ns as u64).clamp(C_EQ_FLOOR_NS, C_EQ_CEIL_NS)
160}
161
162pub struct CpuTopology {
163 pub nr_cpus: usize,
164 pub l2_domain: Vec<u32>, // l2_domain[cpu] = group_id
165 pub l2_groups: Vec<Vec<u32>>, // l2_groups[group_id] = [cpu, ...]
166 pub socket_domain: Vec<u32>, // socket_domain[cpu] = socket_id
167 pub llc_domain: Vec<u32>, // llc_domain[cpu] = L3 GROUP (== socket WHEN MONOLITHIC)
168 pub nr_sockets: u32,
169}
170
171impl CpuTopology {
172 pub fn detect(nr_cpus: usize) -> Result<Self> {
173 // TWO-PASS L2 DETECTION: A SINGLE PASS HAS TWO DEFECT CLASSES ON THE
174 // SYSFS-FAILURE PATH (THE NORMAL CASE FOR OFFLINE CPUs AND UNDER
175 // QEMU/KVM):
176 // 1. CPU-0 ALIASING: A FAILED READ THAT ASSIGNS l2_domain[cpu] = cpu
177 // AS A SYNTHETIC ID NEVER REGISTERED IN l2_groups CAN COLLIDE
178 // WITH A REAL GROUP ID; populate_l2_siblings_map NEVER WRITES
179 // THAT SLOT, SO THE BPF SIDE READS THE MAP'S ZERO-INIT AND
180 // find_idle_l2_sibling SILENTLY TREATS CPU 0 AS EVERYONE'S L2
181 // SIBLING.
182 // 2. ORDER DEPENDENCE: IF cpu A's READ TRANSIENTLY FAILS BUT cpu B's
183 // SUCCEEDS AND LISTS A AS A GENUINE SIBLING ("A,B"), THE TWO ARE
184 // NEVER UNIFIED -- AN ARTIFACT OF PROCESSING ORDER ALONE.
185 // PASS 1 BUILDS THE REAL GROUPS FROM EVERY SUCCESSFUL READ; PASS 2
186 // PLACES THE FAILURES -- JOINING A GROUP THAT ALREADY LISTS THEM, ELSE
187 // A REGISTERED SINGLETON GROUP WITH A COLLISION-FREE SEQUENTIAL ID.
188 let mut l2_domain = vec![u32::MAX; nr_cpus];
189 let mut seen_groups: Vec<Vec<u32>> = Vec::new();
190
191 for cpu in 0..nr_cpus {
192 let path = format!(
193 "/sys/devices/system/cpu/cpu{}/cache/index2/shared_cpu_list",
194 cpu
195 );
196 let content = match std::fs::read_to_string(&path) {
197 Ok(s) => s,
198 Err(_) => continue, // placed in pass 2
199 };
200
201 let members = parse_cpu_list(content.trim());
202
203 // CHECK IF THIS GROUP ALREADY EXISTS
204 let group_id = match seen_groups.iter().position(|g| *g == members) {
205 Some(id) => id as u32,
206 None => {
207 let id = seen_groups.len() as u32;
208 seen_groups.push(members.clone());
209 id
210 }
211 };
212
213 l2_domain[cpu] = group_id;
214 }
215 for cpu in 0..nr_cpus {
216 if l2_domain[cpu] != u32::MAX {
217 continue;
218 }
219 if let Some(id) = seen_groups.iter().position(|g| g.contains(&(cpu as u32))) {
220 l2_domain[cpu] = id as u32;
221 } else {
222 let id = seen_groups.len() as u32;
223 seen_groups.push(vec![cpu as u32]);
224 l2_domain[cpu] = id;
225 }
226 }
227
228 // DETECT SOCKET (PHYSICAL PACKAGE)
229 let mut socket_domain = vec![0u32; nr_cpus];
230 let mut seen_sockets: Vec<u32> = Vec::new();
231
232 for cpu in 0..nr_cpus {
233 let path = format!(
234 "/sys/devices/system/cpu/cpu{}/topology/physical_package_id",
235 cpu
236 );
237 let pkg_id = match std::fs::read_to_string(&path) {
238 Ok(s) => s.trim().parse::<u32>().unwrap_or(0),
239 Err(_) => 0,
240 };
241 if !seen_sockets.contains(&pkg_id) {
242 seen_sockets.push(pkg_id);
243 }
244 let socket_idx = seen_sockets.iter().position(|&s| s == pkg_id).unwrap() as u32;
245 socket_domain[cpu] = socket_idx;
246 }
247
248 let nr_sockets = seen_sockets.len() as u32;
249
250 // DETECT L3 / cache domain / cache domain DOMAIN (index3). ON AMD multi-domain PARTS index3
251 // SUBDIVIDES THE SOCKET INTO cache domain GROUPS; ON MONOLITHIC-L3 PARTS IT
252 // SPANS THE WHOLE SOCKET. USE THE TIER ONLY WHEN IT GENUINELY
253 // SUBDIVIDES A SOCKET (MORE L3 GROUPS THAN SOCKETS) AND index3 WAS
254 // PRESENT FOR EVERY CPU; OTHERWISE llc_domain == socket_domain SO THE
255 // CROSS-DOMAIN RUNG IN build_laplacian NEVER FIRES (EXACT NO-OP).
256 let mut llc_domain = vec![0u32; nr_cpus];
257 let mut seen_llc: Vec<Vec<u32>> = Vec::new();
258 let mut llc_ok = true;
259 for cpu in 0..nr_cpus {
260 let path = format!(
261 "/sys/devices/system/cpu/cpu{}/cache/index3/shared_cpu_list",
262 cpu
263 );
264 let content = match std::fs::read_to_string(&path) {
265 Ok(s) => s,
266 Err(_) => {
267 llc_ok = false;
268 break;
269 }
270 };
271 let members = parse_cpu_list(content.trim());
272 let group_id = match seen_llc.iter().position(|g| *g == members) {
273 Some(id) => id as u32,
274 None => {
275 let id = seen_llc.len() as u32;
276 seen_llc.push(members);
277 id
278 }
279 };
280 llc_domain[cpu] = group_id;
281 }
282 // MAX_OVERFLOW_DOMAINS mirrors src/bpf/intf.h: the BPF side creates exactly
283 // this many per-domain overflow DSQs. If a (multi-socket, high-cache domain) box has
284 // more L3 groups than that, degrade to the socket domain rather than let
285 // a cache domain id index an uncreated DSQ -> dispatch failure -> ejection.
286 const MAX_OVERFLOW_DOMAINS: usize = 32;
287 let llc_subdivides = llc_ok
288 && seen_llc.len() > nr_sockets as usize
289 && seen_llc.len() <= MAX_OVERFLOW_DOMAINS;
290 if !llc_subdivides {
291 llc_domain = socket_domain.clone();
292 }
293
294 Ok(Self {
295 nr_cpus,
296 l2_domain,
297 l2_groups: seen_groups,
298 socket_domain,
299 llc_domain,
300 nr_sockets,
301 })
302 }
303
304 // ONLINE CPU COUNT, UNPRIVILEGED READ. THE HOTPLUG POLL'S CHANGE SIGNAL.
305 pub fn online_cpu_count() -> usize {
306 std::fs::read_to_string("/sys/devices/system/cpu/online")
307 .map(|s| parse_cpu_list(s.trim()).len())
308 .unwrap_or(0)
309 }
310
311 // DETECT-AND-POPULATE, THE ONE STARTUP/HOTPLUG SEQUENCE. ALL RUST-SIDE
312 // COMPUTATION RUNS BEFORE ANY OF THE MAP WRITES: THERE IS NO CROSS-MAP
313 // TRANSACTION PRIMITIVE IN eBPF, SO THE EXPOSURE WHILE MAPS DISAGREE IS
314 // BOUNDED BY WRITE TIME ALONE -- INTERLEAVING THE O(n^3)
315 // EIGENDECOMPOSITION OR THE DOMAIN-TREE CUTS BETWEEN WRITES WOULD STRETCH
316 // THAT WINDOW FOR NOTHING. WRITE ORDER:
317 // cache_domain + l2_siblings (USED TOGETHER), THE AFFINITY-RANK FAMILY,
318 // cpu_domain, AND THE TUNING KNOBS LAST -- THE "GO" SIGNAL THAT TRIGGERS
319 // BPF'S OWN tau-SCALED RE-DERIVATION.
320 pub fn detect_and_populate(
321 sched: &mut Scheduler,
322 nr_cpus: usize,
323 phi_scale: Option<u64>,
324 ) -> Result<TopologySpectrum> {
325 let topo = Self::detect(nr_cpus)?;
326 topo.log_summary();
327 let (reff, rank, mut spectrum) = topo.compute_resistance_affinity();
328 if let Some(pv) = phi_scale {
329 log_info!(
330 "PHI OVERRIDE: phi_dist_scale_q16 {} -> {} (--phi-scale)",
331 spectrum.phi_dist_scale_q16,
332 pv
333 );
334 spectrum.phi_dist_scale_q16 = pv;
335 }
336 topo.log_resistance_affinity(&reff, &rank, spectrum);
337 let domains = topo.compute_domain_tree();
338 topo.log_domains(&domains);
339 let domain_phi = topo.domain_cross_phi_matrix(&domains);
340 let ov_domains = topo.overflow_domain_count();
341 let cpu_dom = topo.domain_partition(&domains, ov_domains);
342
343 if let Err(e) = topo.populate_bpf_map(sched) {
344 log_warn!("CACHE TOPOLOGY MAP WRITE FAILED: {}", e);
345 }
346 if let Err(e) = topo.populate_l2_siblings_map(sched) {
347 log_warn!("L2 SIBLINGS MAP WRITE FAILED: {}", e);
348 }
349 if let Err(e) = topo.populate_affinity_rank_map(
350 sched,
351 &reff,
352 &rank,
353 spectrum.phi_dist_scale_q16,
354 spectrum.codel_eq_ns,
355 &domain_phi,
356 ) {
357 log_warn!("AFFINITY RANK MAP WRITE FAILED: {}", e);
358 }
359 for (cpu, &d) in cpu_dom.iter().enumerate() {
360 if let Err(e) = sched.write_cpu_domain(cpu as u32, d) {
361 log_warn!("CPU DOMAIN MAP WRITE FAILED (cpu {}): {}", cpu, e);
362 break;
363 }
364 }
365 log_info!(
366 "OVERFLOW DOMAINS: {} (emergent, cpu_domain populated)",
367 ov_domains
368 );
369 if let Err(e) = sched.write_topology_fields(spectrum.tau_ns, spectrum.codel_eq_ns) {
370 log_warn!("TOPOLOGY KNOB WRITE FAILED: {}", e);
371 }
372 Ok(spectrum)
373 }
374
375 // HOTPLUG POLL, THE ONE IMPLEMENTATION: compare the online-CPU count
376 // against the last observed value; on change, log and re-run the full
377 // detect-and-populate so a CPU broken at boot (the L2 singleton fallback)
378 // self-corrects once real sysfs data exists, and every R_eff/phi/domain
379 // table tracks the live width. Called each tick by BOTH control loops.
380 // Returns true when it fired -- the adaptive loop keys its tau refresh on it.
381 pub fn poll_hotplug(
382 sched: &mut Scheduler,
383 nr_cpus: usize,
384 phi_scale: Option<u64>,
385 last_online: &mut usize,
386 ) -> bool {
387 let now = Self::online_cpu_count();
388 if now == 0 || now == *last_online {
389 return false;
390 }
391 log_info!(
392 "HOTPLUG: online CPUs {} -> {} -- re-deriving topology",
393 *last_online,
394 now
395 );
396 *last_online = now;
397 if let Err(e) = Self::detect_and_populate(sched, nr_cpus, phi_scale) {
398 log_warn!("HOTPLUG TOPOLOGY RE-DETECT FAILED: {}", e);
399 }
400 true
401 }
402
403 // WRITE L2 DOMAIN MAP TO BPF ARRAY VIA SCHEDULER
404 pub fn populate_bpf_map(&self, sched: &mut Scheduler) -> Result<()> {
405 for cpu in 0..self.nr_cpus {
406 sched.write_cache_domain(cpu as u32, self.l2_domain[cpu])?;
407 }
408 // nr_overflow_domains = number of distinct llc_domain values (the overflow-domain count)
409 let mut seen: Vec<u32> = Vec::new();
410 for &g in &self.llc_domain {
411 if !seen.contains(&g) {
412 seen.push(g);
413 }
414 }
415 sched.write_nr_overflow_domains(seen.len() as u32);
416 Ok(())
417 }
418
419 // WRITE L2 SIBLINGS FLAT ARRAY TO BPF MAP
420 // l2_siblings[group_id * 8 + slot] = cpu_id, SENTINEL u32::MAX MARKS END
421 pub fn populate_l2_siblings_map(&self, sched: &Scheduler) -> Result<()> {
422 const MAX_L2_SIBLINGS: usize = 8;
423 for (gid, members) in self.l2_groups.iter().enumerate() {
424 for (slot, &cpu) in members.iter().enumerate().take(MAX_L2_SIBLINGS) {
425 sched.write_l2_sibling(gid as u32, slot as u32, cpu)?;
426 }
427 if members.len() < MAX_L2_SIBLINGS {
428 sched.write_l2_sibling(gid as u32, members.len() as u32, u32::MAX)?;
429 }
430 }
431 Ok(())
432 }
433
434 // RESISTANCE AFFINITY (KYNG-DINIC ELECTRICAL FLOW MODEL)
435 //
436 // EFFECTIVE RESISTANCE R_eff(u,v) BETWEEN TWO CPUs CAPTURES THE TRUE
437 // MIGRATION COST THROUGH ALL TOPOLOGY PATHS. COMPUTED FROM THE LAPLACIAN
438 // PSEUDOINVERSE OF THE CPU TOPOLOGY GRAPH:
439 // R_eff(i,j) = L+[i,i] + L+[j,j] - 2*L+[i,j]
440 //
441 // EDGE CONDUCTANCES (INVERSE RESISTANCE):
442 // L2 SIBLINGS: 10.0 (SHARED L2, NEAR-ZERO MIGRATION COST)
443 // SAME L3 / cache domain: 3.0 (SHARED LLC; ONLY WHEN A SOCKET HOLDS >1 cache domain)
444 // CROSS-DOMAIN SOCKET: 1.0 (CROSS-DOMAIN INTERCONNECT HOP, ~8x CORE-TO-CORE LATENCY)
445 // CROSS-SOCKET: 0.3 (NUMA HOP, HIGH COST)
446 // RAISING THE same-domain RUNG (NOT LOWERING THE CROSS-DOMAIN CUT) RANKS
447 // SAME-L3 PEERS AHEAD OF CROSS-L3 ONES WITHOUT MOVING lambda_2: THE CROSS-L3
448 // CUT STAYS 1.0, SO tau AND codel_eq ARE UNCHANGED. THE L3 RUNG IS ALWAYS ON;
449 // on a monolithic part llc_domain == socket_domain, so it coincides with the
450 // socket rung and the continuous R_eff metric calibrates to the L2 boundary.
451 //
452 // THE LAPLACIAN L = D - W WHERE D IS DEGREE MATRIX, W IS WEIGHTED ADJACENCY.
453 // L+ (MOORE-PENROSE PSEUDOINVERSE) COMPUTED VIA EIGENDECOMPOSITION:
454 // L+ = sum_{i: lambda_i > 0} (1/lambda_i) * v_i * v_i^T
455 //
456 // FOR n CPUs THIS IS O(n^3) -- TRIVIAL AT SCHEDULER STARTUP (n <= 256).
457 //
458 // REFERENCE: Christiano-Kelner-Madry-Spielman-Teng (STOC 2011),
459 // Chen-Kyng-Liu-Peng-Gutenberg-Sachdeva (FOCS 2022)
460
461 // STIFF L2 RUNG: SMT siblings SHARE L2, so a move between them costs ~0 cache.
462 // Make the edge very stiff -> R_eff(SMT-sib) ~ 0, which lands the Phi migration
463 // barrier exactly at the physical-core / L2 boundary (a real cold-L2 refill)
464 // instead of penalizing free intra-core moves. lambda_2 is the cross-domain Fiedler
465 // cut (independent of L2 stiffness) so tau is unchanged, and codel_eq is already
466 // clamped at its ceiling, so the oscillator timescales are invariant.
467 const CONDUCTANCE_L2: f64 = 1000.0; // L2 / SMT SIBLINGS
468 const CONDUCTANCE_LLC: f64 = 3.0; // SAME L3 (ABOVE SOCKET; always-on rung)
469 const CONDUCTANCE_SOCKET: f64 = 1.0; // SAME SOCKET, CROSS-DOMAIN (IF HOP) -- OR MONOLITHIC SAME-SOCKET
470 const CONDUCTANCE_CROSS: f64 = 0.3; // CROSS-SOCKET NUMA HOP
471
472 // INTEGER MIRROR OF THE RUNGS ABOVE, x10 SO THE 0.3 CROSS-SOCKET CUT CLEARS
473 // TO AN INTEGER (SPILL-Phi). SAME RATIOS AS THE f64 TABLE --
474 // INCLUDING THE STIFF L2 RUNG -- SO THE EXACT AND FLOAT PATHS PRICE THE
475 // SAME GRAPH. THE STIFF L2 RUNG (10000) GROWS BAREISS INTERMEDIATES FAST:
476 // i128 HOLDS THE DETERMINANTS TO ROUGHLY 8 CPUs ON AN SMT PART AND THE
477 // checked_mul GUARD FALLS BACK TO THE FLOAT PSEUDOINVERSE ABOVE THAT --
478 // EXACT WHERE INTEGERS HOLD, FLOAT WHERE THEY DON'T, BY CONSTRUCTION.
479 const ICOND_L2: i128 = 10000;
480 const ICOND_LLC: i128 = 30;
481 const ICOND_SOCKET: i128 = 10;
482 const ICOND_CROSS: i128 = 3;
483 const ICOND_RECOVER: f64 = 10.0; // undo the x10 conductance scale
484 const REFF_EXACT_MAX_CPUS: usize = 24;
485
486 // SPILL-Phi PLACEMENT DEPTH THRESHOLDS. RUST FOLDS R_eff INTO A PER-PEER
487 // DSQ-DEPTH CAP THE BPF SPILL HELPER APPLIES: NEAR PEERS (R_eff~0) ACCEPT
488 // SPILLS UP TO SPILL_NEAR_DEPTH, THE MOST DISTANT ONLY WHEN NEAR-EMPTY
489 // (SPILL_FAR_DEPTH). MONOLITHIC (NO DISTANCE STRUCTURE TO PRICE) -> FLAT
490 // SPILL_MONO_DEPTH.
491 // FAR_DEPTH=1 IS THE SHIPPED SETTING: THE STRICT FORM -- A FAR PEER TAKES
492 // A SPILL ONLY WHEN EMPTY. MEASURED ON IT: LOCALITY 77.9% CACHE-LOCAL AT
493 // 86k MIGRATIONS/s, LONGRUN p99 0.58x VS THE ARCHIVED PEAK, ipc NEUTRAL;
494 // AN UNPROVEN BURST/MIXED DRIFT (~1.2x, p >= 0.67 AT N=2) IS TRACKED,
495 // WITH A FAR_DEPTH=2 A/B QUEUED IN ROADMAP.md. CHANGE THIS VALUE ONLY
496 // WITH THAT A/B IN HAND.
497 const SPILL_NEAR_DEPTH: u32 = 4;
498 const SPILL_FAR_DEPTH: u32 = 1;
499 const SPILL_MONO_DEPTH: u32 = 2;
500
501 // BUILD WEIGHTED GRAPH LAPLACIAN FROM CPU TOPOLOGY
502 // Conductance edge weight between two CPUs, derived from the cache hierarchy.
503 // The single source of truth for BOTH the Laplacian (R_eff / tau) and the
504 // domain cut below, so the emergent locality boundary and the placement metric
505 // price the exact same graph -- the continuous metric drives everything.
506 fn conductance(&self, i: usize, j: usize) -> f64 {
507 if self.l2_domain[i] == self.l2_domain[j] {
508 Self::CONDUCTANCE_L2
509 } else if self.llc_domain[i] == self.llc_domain[j] {
510 Self::CONDUCTANCE_LLC
511 } else if self.socket_domain[i] == self.socket_domain[j] {
512 Self::CONDUCTANCE_SOCKET
513 } else {
514 Self::CONDUCTANCE_CROSS
515 }
516 }
517
518 fn build_laplacian(&self) -> Vec<f64> {
519 let n = self.nr_cpus;
520 let mut l = vec![0.0f64; n * n];
521 for i in 0..n {
522 for j in (i + 1)..n {
523 let w = self.conductance(i, j);
524 l[i * n + j] = -w;
525 l[j * n + i] = -w;
526 l[i * n + i] += w;
527 l[j * n + j] += w;
528 }
529 }
530 l
531 }
532
533 // ---- T2: emergent domain cut (SOSA min-conductance) --------------------
534 // The discrete cache domain layer is replaced by domains that EMERGE from the
535 // cache graph. The boundary is the min-conductance cut: phi = cut_weight /
536 // min(vol_a, vol_b). Low phi = a loosely-coupled seam = a real domain edge;
537 // the phi of the cut IS the cross-domain crossing price (THE FLAG: the price
538 // draws the boundary, no gate). Balance-free -- the seam falls where the
539 // silicon divides (asymmetric X3D / P+E included), not where volume balances.
540
541 // Conductance phi of a bipartition of `members` (in_side[c] = c is on side A).
542 // O(|members|^2); boot-time only. The random-walk variant (next) avoids the
543 // full scan for large N -- this exact form is the ground-truth + the price.
544 fn cut_conductance(&self, members: &[usize], in_side: &[bool]) -> f64 {
545 let mut cut = 0.0f64;
546 let (mut vol_a, mut vol_b) = (0.0f64, 0.0f64);
547 for &a in members {
548 for &b in members {
549 if a == b {
550 continue;
551 }
552 let w = self.conductance(a, b);
553 if in_side[a] {
554 vol_a += w;
555 } else {
556 vol_b += w;
557 }
558 if in_side[a] != in_side[b] {
559 cut += w; // each crossing edge counted twice (a,b and b,a)
560 }
561 }
562 }
563 cut /= 2.0;
564 let denom = vol_a.min(vol_b);
565 if denom <= 0.0 {
566 f64::INFINITY
567 } else {
568 cut / denom
569 }
570 }
571
572 // Fiedler vector (eigenvector of lambda_2) of the full graph -- the ground
573 // truth the scalable random-walk cut is cross-checked against. eigenvectors
574 // are column-major: component i of eigenvector k is eigenvectors[i*n + k].
575 fn fiedler_vector(eigenvalues: &[f64], eigenvectors: &[f64], n: usize) -> Vec<f64> {
576 let mut idx: Vec<usize> = (0..n).collect();
577 idx.sort_by(|&a, &b| {
578 eigenvalues[a]
579 .partial_cmp(&eigenvalues[b])
580 .unwrap_or(std::cmp::Ordering::Equal)
581 });
582 let k = if n >= 2 { idx[1] } else { idx[0] }; // 2nd smallest = lambda_2
583 (0..n).map(|i| eigenvectors[i * n + k]).collect()
584 }
585
586 // True when every member shares one L2 group -- the atomic leaf. L2 siblings
587 // are maximally coupled; there is no meaningful seam to find below them.
588 fn all_same_l2(&self, members: &[usize]) -> bool {
589 members
590 .windows(2)
591 .all(|w| self.l2_domain[w[0]] == self.l2_domain[w[1]])
592 }
593
594 // Min-conductance cut of an ARBITRARY CPU subset (recursion-safe). The global
595 // Fiedler degrades inside a subtree, so this builds the INDUCED Laplacian on
596 // `members`, eigendecomposes it, and sweeps by the SUBSET's own Fiedler. (2d
597 // replaces these internals with a local random walk -- no eigensolve, so it
598 // scales; the tree-builder is agnostic to which produces the cut.) Returns
599 // global CPU ids.
600 fn domain_cut(&self, members: &[usize]) -> Option<(Vec<usize>, Vec<usize>, f64)> {
601 let k = members.len();
602 if k < 2 {
603 return None;
604 }
605 let mut lap = vec![0.0f64; k * k]; // induced Laplacian, local-indexed 0..k
606 for ia in 0..k {
607 for ib in (ia + 1)..k {
608 let w = self.conductance(members[ia], members[ib]);
609 lap[ia * k + ib] = -w;
610 lap[ib * k + ia] = -w;
611 lap[ia * k + ia] += w;
612 lap[ib * k + ib] += w;
613 }
614 }
615 let (ev, evec) = Self::symmetric_eigen(&lap, k);
616 let fsub = Self::fiedler_vector(&ev, &evec, k); // local-indexed
617 let mut order: Vec<usize> = (0..k).collect();
618 order.sort_by(|&a, &b| {
619 fsub[a]
620 .partial_cmp(&fsub[b])
621 .unwrap_or(std::cmp::Ordering::Equal)
622 });
623 let mut in_side = vec![false; self.nr_cpus]; // global-indexed for cut_conductance
624 let (mut best_phi, mut best_k) = (f64::INFINITY, 1usize);
625 for s in 1..k {
626 in_side[members[order[s - 1]]] = true; // grow the prefix (global id)
627 let phi = self.cut_conductance(members, &in_side);
628 if phi < best_phi {
629 best_phi = phi;
630 best_k = s;
631 }
632 }
633 let side_a: Vec<usize> = order[..best_k].iter().map(|&li| members[li]).collect();
634 let side_b: Vec<usize> = order[best_k..].iter().map(|&li| members[li]).collect();
635 Some((side_a, side_b, best_phi))
636 }
637
638 // Below this many CPUs the exact eigen cut is cheap, so use it; above it,
639 // the O(n^3) Jacobi is the wall and the random-walk cut takes over. This is an
640 // implementation dispatch on cost, not a placement gate (THE FLAG untouched).
641 const WALK_CUT_THRESHOLD: usize = 64;
642
643 // SCALABLE min-conductance cut: the SAME sweep as domain_cut, but the vertex
644 // ordering comes from a RANDOM WALK instead of an eigendecomposition -- no
645 // O(n^3) eigensolve, so it scales. Power iteration on the NORMALIZED operator
646 // M = 2I - L_sym, where L_sym = I - D^{-1/2} A D^{-1/2} is the symmetric
647 // normalized Laplacian (eigenvalues in [0,2] regardless of edge-weight scale,
648 // so stiff L2 edges don't dominate the shift the way a raw cI - L would). Its
649 // null eigenvector is D^{1/2}*1 (the walk's stationary distribution, deflated
650 // out each step); the next is the normalized Fiedler -- the iterate converges
651 // to it with a gap set by the CONDUCTANCE structure, not the weight magnitude.
652 // The sweep order is f[i] = y[i] / sqrt(deg[i]) (un-normalizing back to the
653 // walk eigenvector). Deterministic hash start (not a ramp -- ramps can align
654 // with a non-Fiedler mode) so a topology yields the same domains every boot.
655 // Cross-checked against domain_cut as ground truth (the T2 gate).
656 fn walk_cut(&self, members: &[usize]) -> Option<(Vec<usize>, Vec<usize>, f64)> {
657 let k = members.len();
658 if k < 2 {
659 return None;
660 }
661 let mut adj = vec![0.0f64; k * k];
662 let mut deg = vec![0.0f64; k];
663 for ia in 0..k {
664 for ib in 0..k {
665 if ia == ib {
666 continue;
667 }
668 let w = self.conductance(members[ia], members[ib]);
669 adj[ia * k + ib] = w;
670 deg[ia] += w;
671 }
672 }
673 // D^{-1/2} and the stationary direction D^{1/2}*1 (L_sym's null vector).
674 let dis: Vec<f64> = deg
675 .iter()
676 .map(|&d| if d > 0.0 { 1.0 / d.sqrt() } else { 0.0 })
677 .collect();
678 let dsq: Vec<f64> = deg.iter().map(|&d| d.sqrt()).collect();
679 let dsq_sq: f64 = dsq.iter().map(|x| x * x).sum::<f64>().max(1e-30);
680 // Deflate out the stationary (null) component each step.
681 let deflate = |v: &mut [f64]| {
682 let dot: f64 = v.iter().zip(&dsq).map(|(a, b)| a * b).sum();
683 let coef = dot / dsq_sq;
684 for i in 0..k {
685 v[i] -= coef * dsq[i];
686 }
687 };
688 let mut y: Vec<f64> = (0..k)
689 .map(|i| {
690 let h = (i as u64).wrapping_mul(2654435761) & 0xffff;
691 h as f64 / 65535.0 - 0.5
692 })
693 .collect();
694 deflate(&mut y);
695 // A weak cut has a tiny normalized eigenvalue gap, so the per-iteration
696 // rate is near 1 and convergence can take thousands of steps -- iterate to
697 // CONVERGENCE (the direction stops moving), not a fixed count, capped high.
698 // Each step is O(k^2); a one-time boot cost, well under a millisecond.
699 const MAX_ITERS: usize = 20_000;
700 const TOL: f64 = 1e-10;
701 let mut w = vec![0.0f64; k];
702 for _ in 0..MAX_ITERS {
703 // (M y)[i] = y[i] + D^{-1/2}_i * sum_j A_ij * D^{-1/2}_j * y[j]
704 for i in 0..k {
705 let row = &adj[i * k..i * k + k];
706 let mut s = 0.0;
707 for j in 0..k {
708 s += row[j] * dis[j] * y[j];
709 }
710 w[i] = y[i] + dis[i] * s;
711 }
712 deflate(&mut w);
713 let norm = w.iter().map(|x| x * x).sum::<f64>().sqrt();
714 if norm < 1e-12 {
715 break;
716 }
717 // cos angle between the new direction and the old unit vector y.
718 let dot: f64 = w.iter().zip(&y).map(|(a, b)| a * b).sum::<f64>() / norm;
719 for i in 0..k {
720 y[i] = w[i] / norm;
721 }
722 if 1.0 - dot.abs() < TOL {
723 break;
724 }
725 }
726 // Sweep order by the un-normalized walk eigenvector f[i] = y[i]/sqrt(deg).
727 let f: Vec<f64> = (0..k).map(|i| y[i] * dis[i]).collect();
728 let mut order: Vec<usize> = (0..k).collect();
729 order.sort_by(|&a, &b| f[a].partial_cmp(&f[b]).unwrap_or(std::cmp::Ordering::Equal));
730 let mut in_side = vec![false; self.nr_cpus];
731 let (mut best_phi, mut best_k) = (f64::INFINITY, 1usize);
732 for s in 1..k {
733 in_side[members[order[s - 1]]] = true;
734 let phi = self.cut_conductance(members, &in_side);
735 if phi < best_phi {
736 best_phi = phi;
737 best_k = s;
738 }
739 }
740 Some((
741 order[..best_k].iter().map(|&li| members[li]).collect(),
742 order[best_k..].iter().map(|&li| members[li]).collect(),
743 best_phi,
744 ))
745 }
746
747 // Dispatch: exact eigen cut below the threshold, scalable random-walk cut
748 // above. Both produce the same boundary on real cache graphs (gate-tested).
749 fn best_cut(&self, members: &[usize]) -> Option<(Vec<usize>, Vec<usize>, f64)> {
750 if members.len() <= Self::WALK_CUT_THRESHOLD {
751 self.domain_cut(members)
752 } else {
753 self.walk_cut(members)
754 }
755 }
756
757 // Recurse the cut into the emergent domain tree. Leaf when the members are a
758 // single L2 group (or one CPU) -- maximally coupled, no seam. Each Cut carries
759 // its phi (the crossing price). This IS de-facto NUMA: the boundary is drawn
760 // by the conductance landscape, not a hardcoded topology table.
761 pub fn build_domain_tree(&self, members: &[usize]) -> DomainNode {
762 if members.len() <= 1 || self.all_same_l2(members) {
763 return DomainNode::Leaf(members.to_vec());
764 }
765 match self.best_cut(members) {
766 Some((a, b, phi)) if !a.is_empty() && !b.is_empty() => DomainNode::Cut {
767 phi,
768 left: Box::new(self.build_domain_tree(&a)),
769 right: Box::new(self.build_domain_tree(&b)),
770 },
771 _ => DomainNode::Leaf(members.to_vec()),
772 }
773 }
774
775 // Compute the emergent domain tree over all online CPUs -- the de-facto-NUMA
776 // hierarchy from which T3's bounded-local steal reads its locality clusters
777 // and per-cut crossing prices. Public: the T2 -> T3 hand-off point.
778 pub fn compute_domain_tree(&self) -> DomainNode {
779 self.build_domain_tree(&(0..self.nr_cpus).collect::<Vec<_>>())
780 }
781
782 // Log the emergent domains at boot -- observability that the tree the steal
783 // will climb matches the silicon: atomic-domain count, cut depth, the
784 // crossing-price (phi) range, and the first leaves.
785 pub fn log_domains(&self, tree: &DomainNode) {
786 let leaves = tree.leaves();
787 let phis = tree.cut_phis();
788 let (pmin, pmax) = phis.iter().fold((f64::INFINITY, 0.0f64), |(lo, hi), &p| {
789 (lo.min(p), hi.max(p))
790 });
791 log_info!(
792 "EMERGENT DOMAINS: {} atomic, {} cuts, crossing phi {:.4}..{:.4}",
793 leaves.len(),
794 phis.len(),
795 if phis.is_empty() { 0.0 } else { pmin },
796 pmax
797 );
798 let preview: Vec<String> = leaves.iter().take(8).map(|l| format!("{:?}", l)).collect();
799 log_info!("EMERGENT DOMAINS: leaves {}", preview.join(" "));
800 }
801
802 // T3b.1: the per-CPU-pair crossing-price matrix the bounded steal reads.
803 // m[i*n + j] = (phi * 1e6) of the LCA cut separating CPU i and CPU j -- the
804 // price to steal across that emergent domain boundary. A LOW phi is a loose
805 // seam (a major boundary -- socket / cross-L3 -- far, needs more imbalance to
806 // cross); a HIGH phi is a tight seam (near). Same-leaf pairs share NO cut:
807 // sentinel u32::MAX = maximally local, the steal never has to "cross" for them.
808 // Replaces the discrete domain map's discrete same/different-cache domain test with a continuous,
809 // emergent boundary price (THE FLAG: priced, not gated).
810 pub fn domain_cross_phi_matrix(&self, tree: &DomainNode) -> Vec<u32> {
811 let n = self.nr_cpus;
812 let mut m = vec![u32::MAX; n * n]; // default: no boundary (same leaf)
813 Self::fill_cross_phi(tree, n, &mut m);
814 m
815 }
816
817 fn fill_cross_phi(node: &DomainNode, n: usize, m: &mut [u32]) {
818 if let DomainNode::Cut { phi, left, right } = node {
819 let lc: Vec<usize> = left.leaves().concat();
820 let rc: Vec<usize> = right.leaves().concat();
821 let p = (phi * 1_000_000.0).round().clamp(0.0, u32::MAX as f64) as u32;
822 for &a in &lc {
823 for &b in &rc {
824 m[a * n + b] = p; // pairs whose lowest common ancestor IS this cut
825 m[b * n + a] = p;
826 }
827 }
828 Self::fill_cross_phi(left, n, m);
829 Self::fill_cross_phi(right, n, m);
830 }
831 }
832
833 // Number of emergent OVERFLOW DOMAINS to target = distinct L3 groups, so
834 // re-keying the overflow DSQs preserves the L3 granularity.
835 pub fn overflow_domain_count(&self) -> usize {
836 let mut v = self.llc_domain.clone();
837 v.sort_unstable();
838 v.dedup();
839 v.len().max(1)
840 }
841
842 // T3b.2: partition CPUs into emergent OVERFLOW DOMAINS -- the the discrete domain map
843 // replacement. Descend the tree from the root, repeatedly splitting the
844 // frontier subtree whose cut has the LOWEST phi (the coarsest, most-separable
845 // seam) until `target` domains exist or no cut remains. Each resulting subtree
846 // is one overflow domain; dom[cpu] is its id. The granularity is the L3
847 // count; the boundary is drawn by the emergent tree, not the discrete domain map.
848 pub fn domain_partition(&self, tree: &DomainNode, target: usize) -> Vec<u32> {
849 let mut frontier: Vec<&DomainNode> = vec![tree];
850 while frontier.len() < target.max(1) {
851 let mut best: Option<(usize, f64)> = None;
852 for (i, node) in frontier.iter().enumerate() {
853 if let DomainNode::Cut { phi, .. } = node {
854 if best.map_or(true, |(_, bp)| *phi < bp) {
855 best = Some((i, *phi));
856 }
857 }
858 }
859 let Some((idx, _)) = best else { break }; // no cuts left to split
860 let node = frontier[idx];
861 if let DomainNode::Cut { left, right, .. } = node {
862 frontier.swap_remove(idx);
863 frontier.push(left.as_ref());
864 frontier.push(right.as_ref());
865 }
866 }
867 let n = self.nr_cpus;
868 let mut dom = vec![0u32; n];
869 for (id, node) in frontier.iter().enumerate() {
870 for leaf in node.leaves() {
871 for c in leaf {
872 if c < n {
873 dom[c] = id as u32;
874 }
875 }
876 }
877 }
878 dom
879 }
880
881 // SYMMETRIC EIGENDECOMPOSITION VIA JACOBI ROTATIONS
882 // RETURNS (eigenvalues, eigenvectors_column_major)
883 // SUITABLE FOR n <= 256. NO EXTERNAL DEPENDENCIES.
884 fn symmetric_eigen(mat: &[f64], n: usize) -> (Vec<f64>, Vec<f64>) {
885 let mut a = mat.to_vec();
886 // EIGENVECTORS START AS IDENTITY
887 let mut v = vec![0.0f64; n * n];
888 for i in 0..n {
889 v[i * n + i] = 1.0;
890 }
891
892 let max_iter = 100 * n * n;
893 for _ in 0..max_iter {
894 // FIND LARGEST OFF-DIAGONAL ELEMENT
895 let mut max_val = 0.0f64;
896 let mut p = 0;
897 let mut q = 1;
898 for i in 0..n {
899 for j in (i + 1)..n {
900 let val = a[i * n + j].abs();
901 if val > max_val {
902 max_val = val;
903 p = i;
904 q = j;
905 }
906 }
907 }
908 if max_val < 1e-12 {
909 break;
910 }
911
912 // COMPUTE ROTATION
913 let app = a[p * n + p];
914 let aqq = a[q * n + q];
915 let apq = a[p * n + q];
916 let theta = if (app - aqq).abs() < 1e-15 {
917 std::f64::consts::FRAC_PI_4
918 } else {
919 0.5 * (2.0 * apq / (app - aqq)).atan()
920 };
921 let c = theta.cos();
922 let s = theta.sin();
923
924 // APPLY ROTATION TO A
925 for i in 0..n {
926 if i == p || i == q {
927 continue;
928 }
929 let aip = a[i * n + p];
930 let aiq = a[i * n + q];
931 a[i * n + p] = c * aip + s * aiq;
932 a[p * n + i] = a[i * n + p];
933 a[i * n + q] = -s * aip + c * aiq;
934 a[q * n + i] = a[i * n + q];
935 }
936 let new_pp = c * c * app + 2.0 * s * c * apq + s * s * aqq;
937 let new_qq = s * s * app - 2.0 * s * c * apq + c * c * aqq;
938 a[p * n + p] = new_pp;
939 a[q * n + q] = new_qq;
940 a[p * n + q] = 0.0;
941 a[q * n + p] = 0.0;
942
943 // ACCUMULATE EIGENVECTORS
944 for i in 0..n {
945 let vip = v[i * n + p];
946 let viq = v[i * n + q];
947 v[i * n + p] = c * vip + s * viq;
948 v[i * n + q] = -s * vip + c * viq;
949 }
950 }
951
952 let eigenvalues: Vec<f64> = (0..n).map(|i| a[i * n + i]).collect();
953 (eigenvalues, v)
954 }
955
956 // COMPUTE LAPLACIAN PSEUDOINVERSE FROM EIGENDECOMPOSITION
957 fn compute_pseudoinverse(eigenvalues: &[f64], eigenvectors: &[f64], n: usize) -> Vec<f64> {
958 let mut l_pinv = vec![0.0f64; n * n];
959 for k in 0..n {
960 if eigenvalues[k].abs() < 1e-8 {
961 continue; // SKIP NULL EIGENVALUE (CONNECTED GRAPH HAS ONE)
962 }
963 let inv_lambda = 1.0 / eigenvalues[k];
964 for i in 0..n {
965 for j in 0..n {
966 l_pinv[i * n + j] +=
967 inv_lambda * eigenvectors[i * n + k] * eigenvectors[j * n + k];
968 }
969 }
970 }
971 l_pinv
972 }
973
974 // COMPUTE ALL-PAIRS EFFECTIVE RESISTANCE FROM PSEUDOINVERSE
975 // R_eff(i,j) = L+[i,i] + L+[j,j] - 2*L+[i,j]
976 fn extract_reff(l_pinv: &[f64], n: usize) -> Vec<f64> {
977 let mut r = vec![0.0f64; n * n];
978 for i in 0..n {
979 for j in (i + 1)..n {
980 let val = l_pinv[i * n + i] + l_pinv[j * n + j] - 2.0 * l_pinv[i * n + j];
981 r[i * n + j] = val.max(0.0);
982 r[j * n + i] = r[i * n + j];
983 }
984 }
985 r
986 }
987
988 // BUILD PER-CPU AFFINITY RANK: FOR EACH CPU, ALL OTHERS SORTED BY R_EFF
989 // Returns flat array: affinity_rank[cpu * nr_cpus + slot] = target_cpu
990 fn build_affinity_rank(reff: &[f64], n: usize) -> Vec<u32> {
991 let mut rank = vec![0u32; n * n];
992 for cpu in 0..n {
993 let mut others: Vec<(u64, u32)> = (0..n)
994 .filter(|&c| c != cpu)
995 .map(|c| {
996 // SORT KEY: R_EFF AS FIXED-POINT TO AVOID FLOAT COMPARISON ISSUES
997 let key = (reff[cpu * n + c] * 1_000_000.0) as u64;
998 (key, c as u32)
999 })
1000 .collect();
1001 others.sort();
1002 for (slot, &(_, target)) in others.iter().enumerate() {
1003 rank[cpu * n + slot] = target;
1004 }
1005 // FILL REMAINING SLOTS WITH SENTINEL
1006 for slot in others.len()..n {
1007 rank[cpu * n + slot] = u32::MAX;
1008 }
1009 }
1010 rank
1011 }
1012
1013 // INTEGER LAPLACIAN FROM THE ICOND_* RUNG TABLE (SPILL-Phi). SAME EDGE
1014 // STRUCTURE AS build_laplacian, INTEGER WEIGHTS.
1015 fn build_laplacian_int(&self) -> Vec<i128> {
1016 let n = self.nr_cpus;
1017 let mut l = vec![0i128; n * n];
1018 for i in 0..n {
1019 for j in (i + 1)..n {
1020 let w = if self.l2_domain[i] == self.l2_domain[j] {
1021 Self::ICOND_L2
1022 } else if self.llc_domain[i] == self.llc_domain[j] {
1023 Self::ICOND_LLC
1024 } else if self.socket_domain[i] == self.socket_domain[j] {
1025 Self::ICOND_SOCKET
1026 } else {
1027 Self::ICOND_CROSS
1028 };
1029 l[i * n + j] = -w;
1030 l[j * n + i] = -w;
1031 l[i * n + i] += w;
1032 l[j * n + j] += w;
1033 }
1034 }
1035 l
1036 }
1037
1038 // BAREISS FRACTION-FREE DETERMINANT of an integer matrix (row-major, m x m).
1039 // Every division is exact by the Bareiss invariant. None on i128 overflow or a
1040 // zero pivot -- a connected reduced Laplacian is positive-definite, so a zero
1041 // pivot only ever means fall-back-to-float here, never a real singular case.
1042 pub fn bareiss_det(mut a: Vec<i128>, m: usize) -> Option<i128> {
1043 let mut prev: i128 = 1;
1044 for k in 0..m.saturating_sub(1) {
1045 let pivot = a[k * m + k];
1046 if pivot == 0 {
1047 return None;
1048 }
1049 for i in (k + 1)..m {
1050 for j in (k + 1)..m {
1051 let t1 = a[i * m + j].checked_mul(pivot)?;
1052 let t2 = a[i * m + k].checked_mul(a[k * m + j])?;
1053 a[i * m + j] = t1.checked_sub(t2)? / prev;
1054 }
1055 }
1056 prev = pivot;
1057 }
1058 if m == 0 {
1059 Some(1)
1060 } else {
1061 Some(a[(m - 1) * m + (m - 1)])
1062 }
1063 }
1064
1065 // det of the integer Laplacian with the given index set's rows AND columns gone.
1066 fn minor_det(l_int: &[i128], n: usize, remove: &[usize]) -> Option<i128> {
1067 let keep: Vec<usize> = (0..n).filter(|x| !remove.contains(x)).collect();
1068 let m = keep.len();
1069 let mut sub = vec![0i128; m * m];
1070 for (a, &ri) in keep.iter().enumerate() {
1071 for (b, &cj) in keep.iter().enumerate() {
1072 sub[a * m + b] = l_int[ri * n + cj];
1073 }
1074 }
1075 Self::bareiss_det(sub, m)
1076 }
1077
1078 // EXACT R_eff(i,j) = det(L minus rows/cols i,j) / det(L minus row/col 0), the
1079 // spanning-2-forest / spanning-tree ratio (Barrett et al., Spanning 2-Forests
1080 // and Resistance Distance), in the integer Laplacian's units. None on overflow
1081 // so the caller falls back to the float path.
1082 pub fn reff_from_int_laplacian(l_int: &[i128], n: usize) -> Option<Vec<f64>> {
1083 if n < 2 {
1084 return Some(vec![0.0; n * n]);
1085 }
1086 let tau = Self::minor_det(l_int, n, &[0])?;
1087 if tau == 0 {
1088 return None;
1089 }
1090 let tau_f = tau as f64;
1091 let mut r = vec![0.0f64; n * n];
1092 for i in 0..n {
1093 for j in (i + 1)..n {
1094 let f = Self::minor_det(l_int, n, &[i, j])?;
1095 let v = ((f as f64) / tau_f).max(0.0);
1096 r[i * n + j] = v;
1097 r[j * n + i] = v;
1098 }
1099 }
1100 Some(r)
1101 }
1102
1103 // COMPUTE RESISTANCE AFFINITY: FULL PIPELINE
1104 // Returns (reff_matrix, affinity_rank, spectrum) for use by BPF and scheduler.
1105 // Spectrum carries lambda_2 (Fiedler value) and its derived tau_ns, used as
1106 // the universal topology time constant for every core-scaled knob.
1107 pub fn compute_resistance_affinity(&self) -> (Vec<f64>, Vec<u32>, TopologySpectrum) {
1108 let n = self.nr_cpus;
1109 let laplacian = self.build_laplacian();
1110 let (eigenvalues, eigenvectors) = Self::symmetric_eigen(&laplacian, n);
1111 let fiedler = extract_fiedler(&eigenvalues);
1112 let tau_ns = compute_tau_ns(fiedler, n);
1113 // EXACT INTEGER R_eff (Bareiss spanning-2-forest / spanning-tree determinant
1114 // ratio) where i128 holds it; else the float pseudoinverse. Same rung
1115 // ratios both paths, so downstream is unchanged either way.
1116 let reff: Vec<f64> = match (n <= Self::REFF_EXACT_MAX_CPUS)
1117 .then(|| Self::reff_from_int_laplacian(&self.build_laplacian_int(), n))
1118 .flatten()
1119 {
1120 Some(r) => r.into_iter().map(|x| x * Self::ICOND_RECOVER).collect(),
1121 None => {
1122 let l_pinv = Self::compute_pseudoinverse(&eigenvalues, &eigenvectors, n);
1123 Self::extract_reff(&l_pinv, n)
1124 }
1125 };
1126 let rank = Self::build_affinity_rank(&reff, n);
1127 let codel_eq_ns = compute_codel_eq_ns(&eigenvalues, n, tau_ns);
1128 // DISTANCE SCALE: calibrated so the most distant pair (max R_eff)
1129 // maps to ~tau of required steal-wait, an SMT sibling (R_eff ~ 0) to ~0. Only
1130 // sustained backlog (~tau) justifies a far move; a single queued slice does
1131 // not. reff_norm uses the same 1e6 scale the BPF reff_value map stores.
1132 // ALWAYS computed: on a single-L3 part the most distant pair is the cross-L2
1133 // (cross-core) max, so reff_norm auto-calibrates the brake to the L2 boundary
1134 // instead of vanishing -- the continuous metric drives placement on EVERY
1135 // processor, with no binary topology gate in front of it (THE FLAG).
1136 let max_reff = reff.iter().cloned().fold(0.0f64, f64::max);
1137 let reff_norm = ((max_reff * 1_000_000.0).round() as u64).max(1);
1138 let phi_dist_scale_q16 = tau_ns.saturating_mul(65536) / reff_norm;
1139 (
1140 reff,
1141 rank,
1142 TopologySpectrum {
1143 fiedler,
1144 tau_ns,
1145 codel_eq_ns,
1146 phi_dist_scale_q16,
1147 },
1148 )
1149 }
1150
1151 // WRITE AFFINITY RANK TO BPF MAP
1152 // affinity_rank[cpu * MAX_AFFINITY_CANDIDATES + slot] = target_cpu
1153 //
1154 // Emit the full sorted R_eff peer list per CPU, capped at the BPF
1155 // table width (MAX_AFFINITY_CANDIDATES). Slots beyond the actual
1156 // topology end (nr_cpus - 1) are written as explicit u32::MAX
1157 // sentinels so the BPF early-exit fires correctly -- map zero-init
1158 // would otherwise alias to "CPU 0" and silently mis-route.
1159 pub fn populate_affinity_rank_map(
1160 &self,
1161 sched: &Scheduler,
1162 reff: &[f64],
1163 rank: &[u32],
1164 phi_dist_scale_q16: u64,
1165 codel_eq_ns: u64,
1166 domain_phi: &[u32],
1167 ) -> Result<()> {
1168 let stride = crate::bpf_intf::MAX_AFFINITY_CANDIDATES as usize;
1169 let valid = self.nr_cpus.saturating_sub(1).min(stride);
1170 // SPILL-Phi: FOLD THE SAME R_eff INTO A PER-PEER DSQ-DEPTH CAP THE
1171 // BPF SPILL HELPER APPLIES (NEAR PEERS ACCEPT AT HIGHER DEPTH, FAR
1172 // PEERS NEAR-EMPTY ONLY). max_reff NORMALIZES DISTANCE TO [0,1];
1173 // MONOLITHIC (phi_dist_scale 0) -> FLAT SPILL_MONO_DEPTH, NO DISTANCE
1174 // TO PRICE.
1175 let max_reff = reff.iter().cloned().fold(0.0f64, f64::max).max(1e-9);
1176 for cpu in 0..self.nr_cpus {
1177 for slot in 0..valid {
1178 let val = rank[cpu * self.nr_cpus + slot];
1179 sched.write_affinity_rank(cpu as u32, slot as u32, val)?;
1180 // T3b.1: the emergent-domain crossing price to this ranked peer,
1181 // 1:1 with the rank slot (sentinel for an out-of-range peer id).
1182 // UNDER --phi-scale 0 (phi_dist_scale_q16 == 0) THE STEAL
1183 // DOES NO R_eff DISTANCE PRICING; MAKE THE CROSSING PRICE
1184 // SENTINEL TOO SO THE STEAL-SIDE PAIR-SPLIT HOLD IS A NO-OP,
1185 // MATCHING reff_value's FLAT codel_target BASELINE (THE STEAL
1186 // IS domain_phi's CONSUMER).
1187 let dphi = if phi_dist_scale_q16 == 0 {
1188 u32::MAX
1189 } else {
1190 domain_phi
1191 .get(cpu * self.nr_cpus + val as usize)
1192 .copied()
1193 .unwrap_or(u32::MAX)
1194 };
1195 sched.write_domain_phi(cpu as u32, slot as u32, dphi)?;
1196 // FOLD THE PHI DISTANCE PENALTY AT INIT: reff_value stores the
1197 // final steal extra-wait in ns, (R_eff * phi_dist_scale_q16) >> 16,
1198 // so the BPF steal does one indexed read and no multiply. The 1e6
1199 // scale matches build_affinity_rank's sort key. phi_dist_scale_q16
1200 // is 0 on monolithic / --phi-scale 0 -> every penalty 0 -> flat
1201 // codel_target (exact prior behavior).
1202 // CEILING-ONLY CLAMP (shipped with SPILL-Phi; deliberately no
1203 // floor): an unclamped toll scales toward tau itself (~40ms,
1204 // the magnitude behind three hard freezes in the postmortem
1205 // record); cap it at 2*codel_eq_ns. NO floor -- dist_extra is a
1206 // per-peer price that must reach ~0 for a genuinely near peer (an
1207 // SMT sibling stays freely relievable), and STEP 1's phi_thresh
1208 // already adds the uniform codel_target_ns base on top.
1209 let r_scaled = (reff[cpu * self.nr_cpus + val as usize] * 1_000_000.0)
1210 .round()
1211 .clamp(0.0, u32::MAX as f64) as u64;
1212 let raw_extra = r_scaled.saturating_mul(phi_dist_scale_q16) >> 16;
1213 let dist_extra = if phi_dist_scale_q16 == 0 {
1214 0
1215 } else {
1216 raw_extra
1217 .min(codel_eq_ns.saturating_mul(2))
1218 .min(u32::MAX as u64) as u32
1219 };
1220 sched.write_reff_value(cpu as u32, slot as u32, dist_extra)?;
1221 // PLACEMENT THRESHOLD: same R_eff, applied as a depth cap not a
1222 // delay. Caps by DEPTH, so it carries none of the steal side's
1223 // tau-scaling exposure the clamp above exists for.
1224 let spill_d = if phi_dist_scale_q16 == 0 {
1225 Self::SPILL_MONO_DEPTH
1226 } else {
1227 let frac = (reff[cpu * self.nr_cpus + val as usize] / max_reff).clamp(0.0, 1.0);
1228 (Self::SPILL_NEAR_DEPTH as f64
1229 - (Self::SPILL_NEAR_DEPTH - Self::SPILL_FAR_DEPTH) as f64 * frac)
1230 .round()
1231 .clamp(Self::SPILL_FAR_DEPTH as f64, Self::SPILL_NEAR_DEPTH as f64)
1232 as u32
1233 };
1234 sched.write_spill_depth(cpu as u32, slot as u32, spill_d)?;
1235 }
1236 for slot in valid..stride {
1237 sched.write_affinity_rank(cpu as u32, slot as u32, u32::MAX)?;
1238 sched.write_reff_value(cpu as u32, slot as u32, u32::MAX)?;
1239 sched.write_domain_phi(cpu as u32, slot as u32, u32::MAX)?;
1240 sched.write_spill_depth(cpu as u32, slot as u32, Self::SPILL_MONO_DEPTH)?;
1241 }
1242 }
1243 Ok(())
1244 }
1245
1246 pub fn log_resistance_affinity(&self, reff: &[f64], rank: &[u32], spectrum: TopologySpectrum) {
1247 log_info!(
1248 "TOPOLOGY SPECTRUM: lambda2={:.4} tau={}ms codel_eq={}us",
1249 spectrum.fiedler,
1250 spectrum.tau_ns / 1_000_000,
1251 spectrum.codel_eq_ns / 1_000
1252 );
1253 let n = self.nr_cpus;
1254 // LOG TOP 3 AFFINITIES FOR CPU 0
1255 let mut parts = Vec::new();
1256 for slot in 0..3.min(n - 1) {
1257 let target = rank[slot] as usize;
1258 if target >= n {
1259 break;
1260 }
1261 let r = reff[target];
1262 parts.push(format!("CPU{}(R={:.3})", target, r));
1263 }
1264 log_info!("RESISTANCE AFFINITY: CPU 0 rank: {}", parts.join(", "));
1265
1266 // LOG L2 VS NON-L2 R_EFF FOR FIRST CPU
1267 if n >= 2 {
1268 let l2_sib = rank[0] as usize;
1269 let non_l2 = rank[1.min(n - 2)] as usize;
1270 log_info!(
1271 "RESISTANCE AFFINITY: R_eff L2={:.4} non-L2={:.4} ratio={:.1}x",
1272 reff[l2_sib],
1273 reff[non_l2],
1274 if reff[l2_sib] > 0.0 {
1275 reff[non_l2] / reff[l2_sib]
1276 } else {
1277 0.0
1278 }
1279 );
1280 }
1281 }
1282
1283 pub fn log_summary(&self) {
1284 for (gid, members) in self.l2_groups.iter().enumerate() {
1285 let cpus: Vec<String> = members.iter().map(|c| c.to_string()).collect();
1286 log_info!("L2 GROUP {}: [{}]", gid, cpus.join(","));
1287 }
1288 log_info!(
1289 "L2 GROUPS: {} across {} CPUs, {} SOCKETS",
1290 self.l2_groups.len(),
1291 self.nr_cpus,
1292 self.nr_sockets
1293 );
1294 let mut llc = self.llc_domain.clone();
1295 llc.sort_unstable();
1296 llc.dedup();
1297 log_info!(
1298 "LLC DOMAINS: {} (L3 rung always-on, continuous Phi)",
1299 llc.len()
1300 );
1301 }
1302}
1303
1304// PARSE KERNEL CPU LIST FORMAT: "0,6" or "0-2,6-8" or "3"
1305fn parse_cpu_list(s: &str) -> Vec<u32> {
1306 let mut result = Vec::new();
1307 for part in s.split(',') {
1308 let part = part.trim();
1309 if part.is_empty() {
1310 continue;
1311 }
1312 if let Some((start, end)) = part.split_once('-') {
1313 if let (Ok(s), Ok(e)) = (start.parse::<u32>(), end.parse::<u32>()) {
1314 for cpu in s..=e {
1315 result.push(cpu);
1316 }
1317 }
1318 } else if let Ok(cpu) = part.parse::<u32>() {
1319 result.push(cpu);
1320 }
1321 }
1322 result.sort();
1323 result.dedup();
1324 result
1325}
1326
1327#[cfg(test)]
1328mod t2_cut_tests {
1329 use super::*;
1330
1331 // 8 CPUs, no SMT (each its own L2), one socket, two L3 groups: {0..3},{4..7}.
1332 // Intra-L3 edges weigh CONDUCTANCE_LLC (3.0), inter-L3 same-socket weigh
1333 // CONDUCTANCE_SOCKET (1.0) -- two clusters joined by weak edges.
1334 fn synth_2domain() -> CpuTopology {
1335 CpuTopology {
1336 nr_cpus: 8,
1337 l2_domain: (0..8u32).collect(),
1338 l2_groups: Vec::new(),
1339 socket_domain: vec![0u32; 8],
1340 llc_domain: vec![0, 0, 0, 0, 1, 1, 1, 1],
1341 nr_sockets: 1,
1342 }
1343 }
1344
1345 #[test]
1346 fn min_conductance_cut_splits_on_llc() {
1347 let t = synth_2domain();
1348 let members: Vec<usize> = (0..8).collect();
1349 let (a, b, phi) = t.best_cut(&members).expect("cut");
1350 let (mut sa, mut sb) = (a.clone(), b.clone());
1351 sa.sort();
1352 sb.sort();
1353 let (llc0, llc1) = (vec![0usize, 1, 2, 3], vec![4usize, 5, 6, 7]);
1354 assert!(
1355 (sa == llc0 && sb == llc1) || (sa == llc1 && sb == llc0),
1356 "expected the L3 boundary, got {:?} | {:?}",
1357 sa,
1358 sb
1359 );
1360 assert!(phi.is_finite() && phi > 0.0 && phi < 1.0, "phi = {}", phi);
1361 }
1362
1363 #[test]
1364 fn cut_conductance_zero_weight_guard() {
1365 // A singleton member set has no valid bipartition -> None, not a panic.
1366 let t = synth_2domain();
1367 assert!(t.best_cut(&[3usize]).is_none());
1368 }
1369
1370 // 8 CPUs WITH SMT: 4 L2 pairs {0,1}{2,3}{4,5}{6,7}, two L3 groups {0..3},
1371 // {4..7}, one socket. L2-sib 1000, same-L3 cross-L2 3.0, cross-L3 same-socket
1372 // 1.0 -- a clean two-level hierarchy whose tree should be L3 over L2 pairs.
1373 fn synth_smt_2domain() -> CpuTopology {
1374 CpuTopology {
1375 nr_cpus: 8,
1376 l2_domain: vec![0, 0, 1, 1, 2, 2, 3, 3],
1377 l2_groups: Vec::new(),
1378 socket_domain: vec![0u32; 8],
1379 llc_domain: vec![0, 0, 0, 0, 1, 1, 1, 1],
1380 nr_sockets: 1,
1381 }
1382 }
1383
1384 #[test]
1385 fn top_cut_is_the_l3_seam() {
1386 let t = synth_smt_2domain();
1387 let (a, b, phi) = t.domain_cut(&(0..8).collect::<Vec<_>>()).expect("cut");
1388 let (mut sa, mut sb) = (a.clone(), b.clone());
1389 sa.sort();
1390 sb.sort();
1391 let (l3a, l3b) = (vec![0usize, 1, 2, 3], vec![4usize, 5, 6, 7]);
1392 assert!(
1393 (sa == l3a && sb == l3b) || (sa == l3b && sb == l3a),
1394 "top cut should be the L3 seam, got {:?} | {:?}",
1395 sa,
1396 sb
1397 );
1398 assert!(phi.is_finite() && phi > 0.0 && phi < 1.0, "phi = {}", phi);
1399 }
1400
1401 #[test]
1402 fn domain_tree_leaves_are_l2_groups() {
1403 let t = synth_smt_2domain();
1404 let tree = t.build_domain_tree(&(0..8).collect::<Vec<_>>());
1405 let mut leaves: Vec<Vec<usize>> = tree
1406 .leaves()
1407 .into_iter()
1408 .map(|mut l| {
1409 l.sort();
1410 l
1411 })
1412 .collect();
1413 leaves.sort();
1414 assert_eq!(
1415 leaves,
1416 vec![vec![0, 1], vec![2, 3], vec![4, 5], vec![6, 7]],
1417 "leaves should be the 4 L2 groups"
1418 );
1419 // The root cut (L3 seam) is the cheapest crossing: coarser seam, lower phi.
1420 let phis = tree.cut_phis();
1421 assert!(!phis.is_empty(), "tree should have cuts");
1422 let root_phi = phis[0];
1423 assert!(
1424 phis.iter().all(|&p| root_phi <= p + 1e-9),
1425 "root cut should be the lowest phi, got {:?}",
1426 phis
1427 );
1428 }
1429
1430 // Two cuts induce the same bipartition (ignoring which side is A vs B)?
1431 fn same_bipartition(
1432 a: &(Vec<usize>, Vec<usize>, f64),
1433 b: &(Vec<usize>, Vec<usize>, f64),
1434 ) -> bool {
1435 let norm = |c: &(Vec<usize>, Vec<usize>, f64)| {
1436 let (mut x, mut y) = (c.0.clone(), c.1.clone());
1437 x.sort();
1438 y.sort();
1439 if x < y {
1440 (x, y)
1441 } else {
1442 (y, x)
1443 }
1444 };
1445 norm(a) == norm(b)
1446 }
1447
1448 #[test]
1449 fn walk_cut_matches_eigen_cut_smt() {
1450 let t = synth_smt_2domain();
1451 let m: Vec<usize> = (0..8).collect();
1452 let eigen = t.domain_cut(&m).expect("eigen");
1453 let walk = t.walk_cut(&m).expect("walk");
1454 assert!(
1455 same_bipartition(&eigen, &walk),
1456 "walk {:?}|{:?} != eigen {:?}|{:?}",
1457 walk.0,
1458 walk.1,
1459 eigen.0,
1460 eigen.1
1461 );
1462 }
1463
1464 // 16 CPUs, 2 sockets {0..7}{8..15}, 4 L3 groups, 8 L2 pairs. The weakest seam
1465 // is cross-socket (CONDUCTANCE_CROSS 0.3) -- both cuts must land there,
1466 // exercising the random walk on a deeper graph than the 8-CPU case.
1467 fn synth_2socket() -> CpuTopology {
1468 CpuTopology {
1469 nr_cpus: 16,
1470 l2_domain: (0..16).map(|c| (c / 2) as u32).collect(),
1471 l2_groups: Vec::new(),
1472 socket_domain: (0..16).map(|c| (c / 8) as u32).collect(),
1473 llc_domain: (0..16).map(|c| (c / 4) as u32).collect(),
1474 nr_sockets: 2,
1475 }
1476 }
1477
1478 #[test]
1479 fn walk_cut_matches_eigen_cut_2socket() {
1480 let t = synth_2socket();
1481 let m: Vec<usize> = (0..16).collect();
1482 let eigen = t.domain_cut(&m).expect("eigen");
1483 let walk = t.walk_cut(&m).expect("walk");
1484 let (mut wa, mut wb) = (walk.0.clone(), walk.1.clone());
1485 wa.sort();
1486 wb.sort();
1487 let (s0, s1): (Vec<usize>, Vec<usize>) = ((0..8).collect(), (8..16).collect());
1488 assert!(
1489 (wa == s0 && wb == s1) || (wa == s1 && wb == s0),
1490 "walk top cut should be the socket seam, got {:?}|{:?}",
1491 wa,
1492 wb
1493 );
1494 assert!(same_bipartition(&eigen, &walk), "walk != eigen on 2-socket");
1495 }
1496
1497 #[test]
1498 fn compute_domain_tree_public_wrapper() {
1499 let t = synth_smt_2domain();
1500 let tree = t.compute_domain_tree();
1501 assert_eq!(tree.leaves().len(), 4, "smt 2-domain -> 4 L2-group leaves");
1502 }
1503
1504 #[test]
1505 fn single_cpu_is_one_leaf() {
1506 let t = CpuTopology {
1507 nr_cpus: 1,
1508 l2_domain: vec![0],
1509 l2_groups: Vec::new(),
1510 socket_domain: vec![0],
1511 llc_domain: vec![0],
1512 nr_sockets: 1,
1513 };
1514 let tree = t.compute_domain_tree();
1515 assert_eq!(tree.leaves(), vec![vec![0usize]]);
1516 assert!(tree.cut_phis().is_empty(), "a single CPU has no cuts");
1517 }
1518
1519 #[test]
1520 fn cross_phi_matrix_prices_the_boundaries() {
1521 let t = synth_smt_2domain();
1522 let tree = t.compute_domain_tree();
1523 let m = t.domain_cross_phi_matrix(&tree);
1524 let n = 8;
1525 // Same leaf {0,1}: no boundary -> sentinel.
1526 assert_eq!(m[0 * n + 1], u32::MAX, "same-leaf pair must be sentinel");
1527 // Cross-L2 same-L3 (0,2) and cross-L3 (0,4): real, priced boundaries.
1528 assert_ne!(m[0 * n + 2], u32::MAX);
1529 assert_ne!(m[0 * n + 4], u32::MAX);
1530 // Cross-L2 is the TIGHTER (nearer) seam -> higher phi than cross-L3.
1531 assert!(
1532 m[0 * n + 2] > m[0 * n + 4],
1533 "cross-L2 phi {} should exceed cross-L3 phi {}",
1534 m[0 * n + 2],
1535 m[0 * n + 4]
1536 );
1537 // CPUs 2 and 3 are the same sibling L2 pair: identical crossing price from 0.
1538 assert_eq!(m[0 * n + 2], m[0 * n + 3]);
1539 // Symmetric.
1540 assert_eq!(m[0 * n + 4], m[4 * n + 0]);
1541 }
1542
1543 #[test]
1544 fn overflow_partition_matches_l3_groups() {
1545 let t = synth_smt_2domain();
1546 assert_eq!(t.overflow_domain_count(), 2, "two L3 groups");
1547 let tree = t.compute_domain_tree();
1548 let dom = t.domain_partition(&tree, 2);
1549 assert!(
1550 dom[0] == dom[1] && dom[1] == dom[2] && dom[2] == dom[3],
1551 "L3 group 0 is one overflow domain: {:?}",
1552 dom
1553 );
1554 assert!(
1555 dom[4] == dom[5] && dom[5] == dom[6] && dom[6] == dom[7],
1556 "L3 group 1 is one overflow domain: {:?}",
1557 dom
1558 );
1559 assert_ne!(dom[0], dom[4], "the two L3 groups are distinct domains");
1560 // target 1 -> a single overflow domain (monolithic re-key).
1561 let mono = t.domain_partition(&tree, 1);
1562 assert!(
1563 mono.iter().all(|&d| d == 0),
1564 "target 1 = one domain: {:?}",
1565 mono
1566 );
1567 }
1568}