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