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