Skip to main content

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}