scx_pandemonium/tuning.rs
1// PANDEMONIUM TUNING TYPES
2// PURE-RUST MODULE: ZERO BPF DEPENDENCIES
3// SHARED BETWEEN BINARY CRATE (scheduler.rs, adaptive.rs) AND LIB CRATE (tests)
4
5// REGIME THRESHOLDS (CHAOS-DRIVEN BANDS)
6// THESE TWO THRESHOLDS DEFINE THE HIGH/LOW BANDS OF mean_idle_pct USED BY THE CHAOS-DRIVEN
7// REGIME DETECTOR: BOTH ENTRY AND EXIT GO THROUGH THE SAME WINDOWED
8// MEAN, AND THE HVG/BP PRIMITIVES DECIDE WHEN ORDER IS SUFFICIENT TO
9// LATCH LIGHT OR HEAVY (OTHERWISE MIXED).
10
11pub const HEAVY_ENTER_PCT: u64 = 10; // mean_idle <= THIS BAND -> CANDIDATE HEAVY
12pub const LIGHT_ENTER_PCT: u64 = 50; // mean_idle >= THIS BAND -> CANDIDATE LIGHT
13
14// REGIME PROFILES
15// PREEMPT_THRESH CONTROLS WHEN TICK PREEMPTS BATCH TASKS (IF INTERACTIVE WAITING).
16// BATCH_SLICE_NS CONTROLS MAX UNINTERRUPTED BATCH RUN WHEN NO INTERACTIVE WAITING.
17// CPU_BOUND_THRESH_NS CONTROLS DEMOTION THRESHOLD PER REGIME (FEATURE 5).
18
19const MIXED_SLICE_NS: u64 = 1_000_000; // 1MS: TIGHT INTERACTIVE CONTROL
20const MIXED_PREEMPT_NS: u64 = 1_000_000; // 1MS: MATCH FOR CLEAN ENFORCEMENT
21const MIXED_BATCH_NS: u64 = 20_000_000; // 20MS: MATCHES LIGHT/HEAVY/BPF DEFAULT
22
23// P99 CEILINGS
24
25const LIGHT_P99_CEIL_NS: u64 = 3_000_000; // 3MS
26const MIXED_P99_CEIL_NS: u64 = 5_000_000; // 5MS: BELOW 16MS FRAME BUDGET
27const HEAVY_P99_CEIL_NS: u64 = 10_000_000; // 10MS: HEAVY LOAD, REALISTIC
28
29// CLASSIFIER THRESHOLDS
30// LAT_CRI SCORE BOUNDARIES FOR TIER CLASSIFICATION
31// EXPOSED AS TUNING KNOBS FOR RUNTIME ADJUSTMENT
32
33// TUNING KNOBS
34// MATCHES struct tuning_knobs IN BPF (intf.h)
35
36// AFFINITY MODE: L2 PLACEMENT STRENGTH
37pub const AFFINITY_OFF: u64 = 0;
38pub const AFFINITY_WEAK: u64 = 1;
39// AFFINITY_STRONG (2) IS IN THE BPF ABI BUT NOTHING SHIPS IT; THE BASE PROFILE
40// HOLDS WEAK.
41
42#[repr(C)]
43#[derive(Clone, Copy)]
44pub struct TuningKnobs {
45 pub slice_ns: u64,
46 pub preempt_thresh_ns: u64,
47 pub batch_slice_ns: u64,
48 pub affinity_mode: u64,
49 pub codel_thresh_ns: u64,
50 pub burst_slice_ns: u64,
51 // FIEDLER-DERIVED TOPOLOGY TIME CONSTANT, CAPACITY-AWARE IN N.
52 // ZERO MEANS RUST HAS NOT YET WRITTEN tau; BPF USES THE PRE-FIRST-TICK
53 // FALLBACK CONSTANTS UNTIL A NONZERO VALUE LANDS. WRITTEN BY RUST AT
54 // TOPOLOGY DETECT AND ON HOTPLUG; READ BY BPF AT THE FIRST CPU-0 TICK.
55 pub topology_tau_ns: u64,
56 // CODEL EQUILIBRIUM, A POSITION INSIDE THE TARGET BAND SET BY THE
57 // SPECTRAL-GAP DEFICIT. CO-LOCATED WITH topology_tau_ns; SAME ZERO/WRITE/
58 // CLAMP SEMANTICS.
59 pub codel_eq_ns: u64,
60}
61
62impl Default for TuningKnobs {
63 fn default() -> Self {
64 Self {
65 slice_ns: 1_000_000,
66 preempt_thresh_ns: 1_000_000,
67 batch_slice_ns: 20_000_000,
68 affinity_mode: AFFINITY_OFF,
69 codel_thresh_ns: 5_000_000,
70 burst_slice_ns: 1_000_000,
71 topology_tau_ns: 0,
72 codel_eq_ns: 0,
73 }
74 }
75}
76
77// REGIME
78
79#[repr(u8)]
80#[derive(Clone, Copy, PartialEq, Eq, Debug)]
81pub enum Regime {
82 Light = 0,
83 Mixed = 1,
84 Heavy = 2,
85}
86
87impl Regime {
88 pub fn label(self) -> &'static str {
89 match self {
90 Self::Light => "LIGHT",
91 Self::Mixed => "MIXED",
92 Self::Heavy => "HEAVY",
93 }
94 }
95
96 pub fn p99_ceiling(self) -> u64 {
97 match self {
98 Self::Light => LIGHT_P99_CEIL_NS,
99 Self::Mixed => MIXED_P99_CEIL_NS,
100 Self::Heavy => HEAVY_P99_CEIL_NS,
101 }
102 }
103}
104
105// REGIME KNOBS
106
107// THE BASE KNOB PROFILE.
108//
109// This was three profiles selected by a Regime enum. An audit of what actually
110// varied across Light/Mixed/Heavy found only slice, preempt and batch, all now
111// derived from measured queue depth, critical slowing and traffic shape.
112// codel_thresh_ns and burst_slice_ns were IDENTICAL in all three, so they were
113// never regime-dependent -- they were defaults wearing a selector.
114// affinity_mode carries its base value THROUGH: a coupling derivation was tried
115// and withdrawn after it regressed IPC 16x, and derive_percpu_knobs must not
116// reintroduce one.
117//
118// What remains is a starting point the derivations move from. It is not a
119// policy choice; it is the value a knob holds on a machine we have not measured
120// yet.
121pub fn base_knobs() -> TuningKnobs {
122 TuningKnobs {
123 slice_ns: MIXED_SLICE_NS,
124 preempt_thresh_ns: MIXED_PREEMPT_NS,
125 batch_slice_ns: MIXED_BATCH_NS,
126 affinity_mode: AFFINITY_WEAK,
127 codel_thresh_ns: 5_000_000,
128 burst_slice_ns: 1_000_000,
129 topology_tau_ns: 0,
130 codel_eq_ns: 0,
131 }
132}
133
134// TAU-SCALED REGIME KNOBS
135// CAPS DIMENSIONED AS Q16 FIXED-POINT MULTIPLIERS OF tau_ns. k_i CALIBRATED
136// AGAINST THE 12C REFERENCE TOPOLOGY, WHERE tau IS 13.3MS:
137// SLICE_CAP: 0.15 -> 2.0MS
138// PREEMPT_CAP: 0.075 -> 1.0MS
139// BATCH_CAP: 1.5 -> 20MS (Mixed ONLY)
140// SOJOURN: 0.15 -> 2.0MS, WHICH IS ITS CLAMP FLOOR
141// PER-CAP CLAMPS ARE SAFETY RAILS.
142const K_SLICE_CAP_Q16: u64 = 9830; // 0.15
143const K_PREEMPT_CAP_Q16: u64 = 4915; // 0.075
144const K_BATCH_CAP_Q16: u64 = 98304; // 1.5
145const K_SOJOURN_Q16: u64 = 9830; // 0.15
146
147#[inline]
148fn scale_tau_u64(tau_ns: u64, k_q16: u64) -> u64 {
149 ((tau_ns as u128 * k_q16 as u128) >> 16) as u64
150}
151
152pub fn scaled_regime_knobs(r: Regime, nr_cpus: u64, tau_ns: u64) -> TuningKnobs {
153 let mut knobs = base_knobs();
154
155 let slice_cap_tau = scale_tau_u64(tau_ns, K_SLICE_CAP_Q16).clamp(500_000, 8_000_000);
156 let preempt_cap_tau = scale_tau_u64(tau_ns, K_PREEMPT_CAP_Q16).clamp(250_000, 4_000_000);
157 let sojourn_tau = scale_tau_u64(tau_ns, K_SOJOURN_Q16).clamp(2_000_000, 6_000_000);
158
159 knobs.slice_ns = knobs.slice_ns.min(slice_cap_tau);
160 knobs.preempt_thresh_ns = knobs.preempt_thresh_ns.min(preempt_cap_tau);
161
162 // LOW-CORE SLICE CAP. tau IS LARGEST AT LOW CORE COUNT (lambda_2 SHRINKS
163 // AS CORES DROP), SO THE tau SLICE CAP ABOVE IS LOOSEST EXACTLY WHERE A
164 // WIDE BATCH SLICE DOES THE MOST DAMAGE: ON 2-4 CORES THE HEAVY PROFILE'S
165 // 4ms SLICE DENIES A LATENCY-SENSITIVE PROBE ACROSS MANY CONSECUTIVE
166 // SLICES, PRODUCING THE 50-200ms LONG-RUN/MIXED P99 TAIL AT LOW CORE.
167 // idle_pct READS LOW AT LOW CORE COUNT -- BECAUSE THE
168 // BPF WARM-STAY/PER-CPU LANDING DELIBERATELY BYPASSES THE IDLE FAST PATH
169 // -- SO THE REGIME FALSELY LATCHES HEAVY AND INHERITS ITS 4ms SLICE. THE
170 // BPF BASELINE RUNS 1ms HERE WITH NO SUCH TAIL, AND THE LONG-RUN WORK
171 // NUMBERS SHOW THE WIDE SLICE BUYS NO THROUGHPUT ON SO FEW CORES. CAP THE
172 // SLICE TO THE MIXED VALUE AT <=4 CORES; 8C/12C (WHERE ADAPTIVE WINS AND
173 // THE WIDER SLICE EARNS THROUGHPUT) ARE UNTOUCHED.
174 if nr_cpus <= 4 {
175 knobs.slice_ns = knobs.slice_ns.min(MIXED_SLICE_NS);
176 }
177 if matches!(r, Regime::Mixed) {
178 let batch_cap_tau = scale_tau_u64(tau_ns, K_BATCH_CAP_Q16).clamp(10_000_000, 80_000_000);
179 knobs.batch_slice_ns = knobs.batch_slice_ns.min(batch_cap_tau);
180 }
181 knobs.codel_thresh_ns = sojourn_tau;
182
183 knobs
184}
185
186// REGIME DETECTION (CHAOS-DRIVEN)
187// REGIME IS A FUNCTION OF:
188// - mean_idle_pct: WINDOWED MEAN OVER THE LAST N TICKS
189// - hvg_lambda: HVG MEAN DEGREE OF THE idle_pct WINDOW
190// - bp_h: BANDT-POMPE D=3 PERMUTATION ENTROPY OF THE WINDOW
191// HYSTERESIS IS BUILT INTO THE WINDOW: SAMPLES MUST FLOW IN BEFORE THE
192// MEAN MOVES. THE 2-TICK HOLD IN THE MONITOR LOOP STAYS AS ADDITIONAL
193// SMOOTHING.
194//
195// LIGHT := mean_idle HIGH AND CHAOS-LOW (PERIODIC / IDLE-DOMINATED)
196// HEAVY := mean_idle LOW AND CHAOS-LOW (PERIODIC / SATURATED)
197// MIXED := ANYTHING ELSE (REGIME IS UNSTABLE OR IN MID-BAND)
198//
199// THE chaos_low PREDICATE IS lambda < CHAOTIC_MIN OR bp_h < BP_H_HIGH.
200// EITHER PRIMITIVE INDICATING ORDER IS ENOUGH; THEY MEASURE DIFFERENT
201// THINGS (AMPLITUDE-AWARE VS AMPLITUDE-INVARIANT) AND THE FIRST TO
202// FIRE LATCHES THE REGIME TO LIGHT/HEAVY INSTEAD OF MIXED.
203
204pub fn detect_regime(mean_idle_pct: f64, hvg_lambda: f64, bp_h: f64) -> Regime {
205 let chaos_low =
206 hvg_lambda < crate::chaos::HVG_LAMBDA_CHAOTIC_MIN || bp_h < crate::chaos::BP_H_HIGH;
207
208 if chaos_low && mean_idle_pct >= LIGHT_ENTER_PCT as f64 {
209 Regime::Light
210 } else if chaos_low && mean_idle_pct <= HEAVY_ENTER_PCT as f64 {
211 Regime::Heavy
212 } else {
213 Regime::Mixed
214 }
215}
216
217// STABILITY MODE
218
219pub const STABILITY_THRESHOLD: u32 = 10; // CONSECUTIVE STABLE TICKS BEFORE HIBERNATE
220
221pub fn compute_stability_score(
222 prev_score: u32,
223 regime_changed: bool,
224 reflex_events_delta: u64,
225 p99_ns: u64,
226 p99_ceiling_ns: u64,
227) -> u32 {
228 if regime_changed || reflex_events_delta > 0 || p99_ns > p99_ceiling_ns / 2 {
229 return 0;
230 }
231 (prev_score + 1).min(STABILITY_THRESHOLD)
232}
233
234// TELEMETRY GATING
235
236pub fn should_print_telemetry(tick_counter: u64, stability_score: u32) -> bool {
237 if stability_score >= STABILITY_THRESHOLD {
238 tick_counter.is_multiple_of(2)
239 } else {
240 true
241 }
242}
243
244// P99 HISTOGRAM
245
246pub const HIST_BUCKETS: usize = 12;
247pub const HIST_EDGES_NS: [u64; HIST_BUCKETS] = [
248 10_000, // 10us
249 25_000, // 25us
250 50_000, // 50us
251 100_000, // 100us
252 250_000, // 250us
253 500_000, // 500us
254 1_000_000, // 1ms
255 2_000_000, // 2ms
256 5_000_000, // 5ms
257 10_000_000, // 10ms
258 20_000_000, // 20ms
259 u64::MAX, // +inf
260];
261
262// COMPUTE P99 FROM DRAINED HISTOGRAM COUNTS. PURE FUNCTION.
263// CAP AT 20MS (LAST REAL BUCKET) -- +INF WOULD POISON EVERY COMPARISON.
264pub fn compute_p99_from_histogram(counts: &[u64; HIST_BUCKETS]) -> u64 {
265 let total: u64 = counts.iter().sum();
266 if total == 0 {
267 return 0;
268 }
269 let threshold = (total * 99).div_ceil(100);
270 let mut cumulative = 0u64;
271 for i in 0..HIST_BUCKETS {
272 cumulative += counts[i];
273 if cumulative >= threshold {
274 return HIST_EDGES_NS[i].min(HIST_EDGES_NS[HIST_BUCKETS - 2]);
275 }
276 }
277 HIST_EDGES_NS[HIST_BUCKETS - 2]
278}
279
280// OSCILLATOR STATE, READ FROM BPF
281// The BPF damped-harmonic oscillator owns codel_target_ns. The adaptive layer
282// reads its position so nothing upstream re-derives a target the oscillator is
283// already moving.
284#[derive(Default, Clone, Copy, Debug)]
285pub struct OscillatorState {
286 pub codel_target_ns: u64,
287 pub codel_target_floor_ns: u64,
288 pub codel_target_max_ns: u64,
289 // HOME NEAREST-PEER PHI HOLD (reff_value SLOT 0, ns). THE BPF WARM-STAY
290 // AND STEP-1 R_eff STEAL RELEASE AT codel_target_ns + THIS, NOT AT THE
291 // BARE CODEL WINDOW. position() MEASURES THE BARE WINDOW; THE ABSOLUTE
292 // CHECKS BELOW ADD THIS TERM SO THE DEFER DECISION IS TAKEN AGAINST THE
293 // EFFECTIVE PHI RELEASE POINT THE BPF ACTUALLY USES. 0 = READBACK
294 // UNAVAILABLE -> TREATED AS NO EXTRA HOLD (NEUTRAL).
295 pub home_dist_extra_ns: u64,
296}
297
298impl OscillatorState {
299 // 0.0 = AT FLOOR (TIGHTENED), 1.0 = AT MAX (RELAXED).
300 // SENTINEL OR DEGENERATE RANGE -> 0.5 (CENTER, NEUTRAL).
301 pub fn position(&self) -> f64 {
302 if self.codel_target_max_ns == 0 || self.codel_target_floor_ns >= self.codel_target_max_ns {
303 return 0.5;
304 }
305 let range = (self.codel_target_max_ns - self.codel_target_floor_ns) as f64;
306 let pos = self
307 .codel_target_ns
308 .saturating_sub(self.codel_target_floor_ns) as f64;
309 (pos / range).clamp(0.0, 1.0)
310 }
311
312 // EFFECTIVE PHI RELEASE POINT: WHERE WARM-STAY / STEP-1 STEAL ACTUALLY LET
313 // A TASK LEAVE HOME (codel_target + NEAREST-PEER HOLD). THE DEFER GATE
314 // COMPARES THIS AGAINST codel_eq TO DECIDE WHETHER THE BPF HAS GENUINELY
315 // RESPONDED, RATHER THAN TRUSTING THE BARE-WINDOW position() ALONE.
316 pub fn effective_release_ns(&self) -> u64 {
317 self.codel_target_ns.saturating_add(self.home_dist_extra_ns)
318 }
319}
320
321// QUIESCENCE GATE
322// Latches a "frozen" flag telling the monitor loop the machine is steady. The
323// loop still ticks at 1 Hz; the chaos sensors are the exit condition.
324//
325// The third term used to be MWU weight-vector convergence. With no learner
326// there is nothing to converge, so the gate is the chaos band alone -- which
327// makes the saturation hole (a flat idle_pct window reads lambda ~2 and DET
328// 1.0, both in band) the only thing standing between a pegged box and a frozen
329// loop. Replacing this with an occupancy predicate over per-CPU queue depth is
330// the open item; the graph now supplies the depth series it needs.
331pub const QUIESCE_ENTER_TICKS: u32 = 4;
332
333pub struct QuiescenceState {
334 in_band_streak: u32,
335 frozen: bool,
336}
337
338impl Default for QuiescenceState {
339 fn default() -> Self {
340 Self::new()
341 }
342}
343
344impl QuiescenceState {
345 pub const fn new() -> Self {
346 Self {
347 in_band_streak: 0,
348 frozen: false,
349 }
350 }
351
352 // ADVANCE THE GATE ONE TICK. RETURNS THE LATCHED `frozen` FLAG.
353 // rqa_det IS None WHEN THE WINDOW IS NOT YET FULL -- THAT NEVER
354 // COUNTS AS IN-BAND (NEVER FREEZE ON INSUFFICIENT DATA).
355 pub fn update(&mut self, hvg_lambda: f64, rqa_det: Option<f64>, mwu_converged: bool) -> bool {
356 let in_band = hvg_lambda <= crate::chaos::HVG_LAMBDA_PERIODIC_MAX
357 && rqa_det.is_some_and(|d| d >= crate::chaos::RQA_DET_STEADY_MIN)
358 && mwu_converged;
359
360 if in_band {
361 self.in_band_streak = self.in_band_streak.saturating_add(1);
362 if self.in_band_streak >= QUIESCE_ENTER_TICKS {
363 self.frozen = true;
364 }
365 } else {
366 self.in_band_streak = 0;
367 self.frozen = false;
368 }
369 self.frozen
370 }
371}
372
373// ADAPTIVE-RARITY RETUNE INTERVAL
374// WHEN THE ORCHESTRATOR IS NOT FROZEN BUT A RETUNE PRODUCES ONLY A
375// SUB-THRESHOLD KNOB DELTA, STRETCH THE INTERVAL BETWEEN RETUNES x1.5
376// (UP TO A FORCED CEILING) SO THE LOOP CONVERGES TOWARD QUIESCENCE.
377// ANY DISTURBANCE SNAPS IT BACK TO THE BASE INTERVAL.
378
379pub const RETUNE_INTERVAL_BASE: u32 = 1;
380pub const RETUNE_INTERVAL_MAX: u32 = 8;
381
382pub fn next_retune_interval(cur: u32, sub_threshold: bool, disturbed: bool) -> u32 {
383 if disturbed {
384 RETUNE_INTERVAL_BASE
385 } else if sub_threshold {
386 // x1.5 STRETCH, BUT ALWAYS GROW BY AT LEAST 1 -- INTEGER x1.5
387 // OF THE BASE INTERVAL (1) WOULD OTHERWISE STALL AT 1.
388 let stretched = (cur.saturating_mul(3) / 2).max(cur + 1);
389 stretched.clamp(RETUNE_INTERVAL_BASE, RETUNE_INTERVAL_MAX)
390 } else {
391 cur
392 }
393}
394
395// COMMIT-ON-CHANGE: TRUE IFF THE TWO KNOB SETS DIFFER ON ANY
396// MWU-OWNED FIELD. topology_tau_ns / codel_eq_ns ARE EXCLUDED -- THEY
397// ARE OWNED BY THE TOPOLOGY LAYER AND WRITTEN INDEPENDENTLY VIA
398// write_topology_fields(); INCLUDING THEM WOULD SPURIOUSLY TRIP THE
399// DIFF EVERY TICK THE LOOP OVERLAYS THEM.
400pub fn knobs_differ(a: &TuningKnobs, b: &TuningKnobs) -> bool {
401 a.slice_ns != b.slice_ns
402 || a.preempt_thresh_ns != b.preempt_thresh_ns
403 || a.batch_slice_ns != b.batch_slice_ns
404 || a.affinity_mode != b.affinity_mode
405 || a.codel_thresh_ns != b.codel_thresh_ns
406 || a.burst_slice_ns != b.burst_slice_ns
407}