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// THE SCHMITT-TRIGGER ENTER/EXIT PAIR IS GONE. THESE TWO THRESHOLDS
7// DEFINE THE HIGH/LOW BANDS OF mean_idle_pct USED BY THE CHAOS-DRIVEN
8// REGIME DETECTOR: BOTH ENTRY AND EXIT GO THROUGH THE SAME WINDOWED
9// MEAN, AND THE HVG/BP PRIMITIVES DECIDE WHEN ORDER IS SUFFICIENT TO
10// LATCH LIGHT OR HEAVY (OTHERWISE MIXED).
11
12pub const HEAVY_ENTER_PCT: u64 = 10; // mean_idle <= THIS BAND -> CANDIDATE HEAVY
13pub const LIGHT_ENTER_PCT: u64 = 50; // mean_idle >= THIS BAND -> CANDIDATE LIGHT
14
15// REGIME PROFILES
16// PREEMPT_THRESH CONTROLS WHEN TICK PREEMPTS BATCH TASKS (IF INTERACTIVE WAITING).
17// BATCH_SLICE_NS CONTROLS MAX UNINTERRUPTED BATCH RUN WHEN NO INTERACTIVE WAITING.
18// CPU_BOUND_THRESH_NS CONTROLS DEMOTION THRESHOLD PER REGIME (FEATURE 5).
19
20const LIGHT_SLICE_NS: u64 = 2_000_000; // 2MS
21const LIGHT_PREEMPT_NS: u64 = 1_000_000; // 1MS: AGGRESSIVE
22const LIGHT_BATCH_NS: u64 = 20_000_000; // 20MS: NO CONTENTION, LET BATCH RIP
23
24const MIXED_SLICE_NS: u64 = 1_000_000; // 1MS: TIGHT INTERACTIVE CONTROL
25const MIXED_PREEMPT_NS: u64 = 1_000_000; // 1MS: MATCH FOR CLEAN ENFORCEMENT
26const MIXED_BATCH_NS: u64 = 20_000_000; // 20MS: MATCHES LIGHT/HEAVY/BPF DEFAULT
27
28const HEAVY_SLICE_NS: u64 = 4_000_000; // 4MS: WIDER FOR THROUGHPUT
29const HEAVY_PREEMPT_NS: u64 = 2_000_000; // 2MS: SLIGHTLY RELAXED
30const HEAVY_BATCH_NS: u64 = 20_000_000; // 20MS: LET BATCH RIP
31
32// P99 CEILINGS
33
34const LIGHT_P99_CEIL_NS: u64 = 3_000_000; // 3MS
35const MIXED_P99_CEIL_NS: u64 = 5_000_000; // 5MS: BELOW 16MS FRAME BUDGET
36const HEAVY_P99_CEIL_NS: u64 = 10_000_000; // 10MS: HEAVY LOAD, REALISTIC
37
38// CLASSIFIER THRESHOLDS
39// LAT_CRI SCORE BOUNDARIES FOR TIER CLASSIFICATION
40// EXPOSED AS TUNING KNOBS FOR RUNTIME ADJUSTMENT
41
42pub const DEFAULT_LAT_CRI_THRESH_HIGH: u64 = 32; // >= THIS: LAT_CRITICAL
43pub const DEFAULT_LAT_CRI_THRESH_LOW: u64 = 8; // >= THIS: INTERACTIVE, BELOW: BATCH
44
45// TUNING KNOBS
46// MATCHES struct tuning_knobs IN BPF (intf.h)
47
48// AFFINITY MODE: L2 PLACEMENT STRENGTH
49pub const AFFINITY_OFF: u64 = 0;
50pub const AFFINITY_WEAK: u64 = 1;
51pub const AFFINITY_STRONG: u64 = 2;
52
53#[repr(C)]
54#[derive(Clone, Copy)]
55pub struct TuningKnobs {
56    pub slice_ns: u64,
57    pub preempt_thresh_ns: u64,
58    pub batch_slice_ns: u64,
59    pub lat_cri_thresh_high: u64,
60    pub lat_cri_thresh_low: u64,
61    pub affinity_mode: u64,
62    pub sojourn_thresh_ns: u64,
63    pub burst_slice_ns: u64,
64    // FIEDLER-DERIVED TOPOLOGY TIME CONSTANT (TAU_SCALE_NS / lambda_2).
65    // ZERO MEANS RUST HAS NOT YET WRITTEN tau; BPF USES THE PRE-FIRST-TICK
66    // FALLBACK CONSTANTS UNTIL A NONZERO VALUE LANDS. WRITTEN BY RUST AT
67    // TOPOLOGY DETECT AND ON HOTPLUG; READ BY BPF AT THE FIRST CPU-0 TICK.
68    pub topology_tau_ns: u64,
69    // R_eff-DERIVED CODEL EQUILIBRIUM TARGET (<R_eff> * 2m * tau).
70    // CO-LOCATED WITH topology_tau_ns; SAME ZERO/WRITE/CLAMP SEMANTICS.
71    pub codel_eq_ns: u64,
72}
73
74impl Default for TuningKnobs {
75    fn default() -> Self {
76        Self {
77            slice_ns: 1_000_000,
78            preempt_thresh_ns: 1_000_000,
79            batch_slice_ns: 20_000_000,
80            lat_cri_thresh_high: DEFAULT_LAT_CRI_THRESH_HIGH,
81            lat_cri_thresh_low: DEFAULT_LAT_CRI_THRESH_LOW,
82            affinity_mode: AFFINITY_OFF,
83            sojourn_thresh_ns: 5_000_000,
84            burst_slice_ns: 1_000_000,
85            topology_tau_ns: 0,
86            codel_eq_ns: 0,
87        }
88    }
89}
90
91// REGIME
92
93#[repr(u8)]
94#[derive(Clone, Copy, PartialEq, Eq, Debug)]
95pub enum Regime {
96    Light = 0,
97    Mixed = 1,
98    Heavy = 2,
99}
100
101impl Regime {
102    pub fn label(self) -> &'static str {
103        match self {
104            Self::Light => "LIGHT",
105            Self::Mixed => "MIXED",
106            Self::Heavy => "HEAVY",
107        }
108    }
109
110    pub fn p99_ceiling(self) -> u64 {
111        match self {
112            Self::Light => LIGHT_P99_CEIL_NS,
113            Self::Mixed => MIXED_P99_CEIL_NS,
114            Self::Heavy => HEAVY_P99_CEIL_NS,
115        }
116    }
117}
118
119// REGIME KNOBS
120
121pub fn regime_knobs(r: Regime) -> TuningKnobs {
122    match r {
123        Regime::Light => TuningKnobs {
124            slice_ns: LIGHT_SLICE_NS,
125            preempt_thresh_ns: LIGHT_PREEMPT_NS,
126            batch_slice_ns: LIGHT_BATCH_NS,
127            lat_cri_thresh_high: DEFAULT_LAT_CRI_THRESH_HIGH,
128            lat_cri_thresh_low: DEFAULT_LAT_CRI_THRESH_LOW,
129            affinity_mode: AFFINITY_WEAK,
130            sojourn_thresh_ns: 5_000_000,
131            burst_slice_ns: 1_000_000,
132            topology_tau_ns: 0,
133            codel_eq_ns: 0,
134        },
135        Regime::Mixed => TuningKnobs {
136            slice_ns: MIXED_SLICE_NS,
137            preempt_thresh_ns: MIXED_PREEMPT_NS,
138            batch_slice_ns: MIXED_BATCH_NS,
139            lat_cri_thresh_high: DEFAULT_LAT_CRI_THRESH_HIGH,
140            lat_cri_thresh_low: DEFAULT_LAT_CRI_THRESH_LOW,
141            affinity_mode: AFFINITY_STRONG,
142            sojourn_thresh_ns: 5_000_000,
143            burst_slice_ns: 1_000_000,
144            topology_tau_ns: 0,
145            codel_eq_ns: 0,
146        },
147        Regime::Heavy => TuningKnobs {
148            slice_ns: HEAVY_SLICE_NS,
149            preempt_thresh_ns: HEAVY_PREEMPT_NS,
150            batch_slice_ns: HEAVY_BATCH_NS,
151            lat_cri_thresh_high: DEFAULT_LAT_CRI_THRESH_HIGH,
152            lat_cri_thresh_low: DEFAULT_LAT_CRI_THRESH_LOW,
153            affinity_mode: AFFINITY_WEAK,
154            sojourn_thresh_ns: 5_000_000,
155            burst_slice_ns: 1_000_000,
156            topology_tau_ns: 0,
157            codel_eq_ns: 0,
158        },
159    }
160}
161
162// TAU-SCALED REGIME KNOBS
163// CAPS DIMENSIONED AS Q16 FIXED-POINT MULTIPLIERS OF tau_ns. k_i CALIBRATED
164// AGAINST THE 12C REFERENCE TOPOLOGY (tau ~= 40MS):
165//   SLICE_CAP:   0.15 -> 6MS  AT tau=40MS
166//   PREEMPT_CAP: 0.075 -> 3MS AT tau=40MS
167//   BATCH_CAP:   1.5 -> 60MS  AT tau=40MS (Mixed ONLY)
168//   SOJOURN:     0.15 -> 6MS  AT tau=40MS
169// PER-CAP CLAMPS ARE SAFETY RAILS.
170const K_SLICE_CAP_Q16: u64 = 9830; // 0.15
171const K_PREEMPT_CAP_Q16: u64 = 4915; // 0.075
172const K_BATCH_CAP_Q16: u64 = 98304; // 1.5
173const K_SOJOURN_Q16: u64 = 9830; // 0.15
174
175// FORK-STORM RAW-WAKE-RATE THRESHOLD. Q16 RATIO INTERPRETED AS
176// "WAKES/SEC PER MS-OF-tau", SO scale_tau_u64(tau, K) PRODUCES
177// THE TOTAL-WAKE THRESHOLD (NOT PER-CPU). AT THE 12C REFERENCE
178// (tau=40MS) THE GATE FIRES AT ~8000 WAKE/S, TIGHTENING LINEARLY
179// AT LOWER tau (1200/S AT 4C, 400/S AT THE 2C FLOOR).
180const K_FORK_STORM_RATE_Q16: u64 = 13107; // 0.20
181const FORK_STORM_RATE_FLOOR: u64 = 200; // HZ; CLAMPS BELOW tau=1MS
182
183#[inline]
184fn scale_tau_u64(tau_ns: u64, k_q16: u64) -> u64 {
185    (tau_ns as u128 * k_q16 as u128 >> 16) as u64
186}
187
188pub fn scaled_regime_knobs(r: Regime, nr_cpus: u64, tau_ns: u64) -> TuningKnobs {
189    let mut knobs = regime_knobs(r);
190
191    let slice_cap_tau = scale_tau_u64(tau_ns, K_SLICE_CAP_Q16).clamp(500_000, 8_000_000);
192    let preempt_cap_tau = scale_tau_u64(tau_ns, K_PREEMPT_CAP_Q16).clamp(250_000, 4_000_000);
193    let sojourn_tau = scale_tau_u64(tau_ns, K_SOJOURN_Q16).clamp(2_000_000, 6_000_000);
194
195    knobs.slice_ns = knobs.slice_ns.min(slice_cap_tau);
196    knobs.preempt_thresh_ns = knobs.preempt_thresh_ns.min(preempt_cap_tau);
197
198    // LOW-CORE SLICE CAP. tau IS LARGEST AT LOW CORE COUNT (lambda_2 SHRINKS
199    // AS CORES DROP), SO THE tau SLICE CAP ABOVE IS LOOSEST EXACTLY WHERE A
200    // WIDE BATCH SLICE DOES THE MOST DAMAGE: ON 2-4 CORES THE HEAVY PROFILE'S
201    // 4ms SLICE DENIES A LATENCY-SENSITIVE PROBE ACROSS MANY CONSECUTIVE
202    // SLICES, PRODUCING THE 50-200ms LONG-RUN/MIXED P99 TAIL AT LOW CORE.
203    // idle_pct READS LOW AT LOW CORE COUNT -- BECAUSE THE
204    // BPF WARM-STAY/PER-CPU LANDING DELIBERATELY BYPASSES THE IDLE FAST PATH
205    // -- SO THE REGIME FALSELY LATCHES HEAVY AND INHERITS ITS 4ms SLICE. THE
206    // BPF BASELINE RUNS 1ms HERE WITH NO SUCH TAIL, AND THE LONG-RUN WORK
207    // NUMBERS SHOW THE WIDE SLICE BUYS NO THROUGHPUT ON SO FEW CORES. CAP THE
208    // SLICE TO THE MIXED VALUE AT <=4 CORES; 8C/12C (WHERE ADAPTIVE WINS AND
209    // THE WIDER SLICE EARNS THROUGHPUT) ARE UNTOUCHED.
210    if nr_cpus <= 4 {
211        knobs.slice_ns = knobs.slice_ns.min(MIXED_SLICE_NS);
212    }
213    if matches!(r, Regime::Mixed) {
214        let batch_cap_tau = scale_tau_u64(tau_ns, K_BATCH_CAP_Q16).clamp(10_000_000, 80_000_000);
215        knobs.batch_slice_ns = knobs.batch_slice_ns.min(batch_cap_tau);
216    }
217    knobs.sojourn_thresh_ns = sojourn_tau;
218
219    knobs
220}
221
222// REGIME DETECTION (CHAOS-DRIVEN)
223// THE SCHMITT TRIGGER IS GONE. REGIME IS A FUNCTION OF:
224//   - mean_idle_pct: WINDOWED MEAN OVER THE LAST N TICKS
225//   - hvg_lambda:    HVG MEAN DEGREE OF THE idle_pct WINDOW
226//   - bp_h:          BANDT-POMPE D=3 PERMUTATION ENTROPY OF THE WINDOW
227// HYSTERESIS IS BUILT INTO THE WINDOW: SAMPLES MUST FLOW IN BEFORE THE
228// MEAN MOVES. THE 2-TICK HOLD IN THE MONITOR LOOP STAYS AS ADDITIONAL
229// SMOOTHING.
230//
231// LIGHT  := mean_idle HIGH  AND CHAOS-LOW (PERIODIC / IDLE-DOMINATED)
232// HEAVY  := mean_idle LOW   AND CHAOS-LOW (PERIODIC / SATURATED)
233// MIXED  := ANYTHING ELSE (REGIME IS UNSTABLE OR IN MID-BAND)
234//
235// THE chaos_low PREDICATE IS lambda < CHAOTIC_MIN OR bp_h < BP_H_HIGH.
236// EITHER PRIMITIVE INDICATING ORDER IS ENOUGH; THEY MEASURE DIFFERENT
237// THINGS (AMPLITUDE-AWARE VS AMPLITUDE-INVARIANT) AND THE FIRST TO
238// FIRE LATCHES THE REGIME TO LIGHT/HEAVY INSTEAD OF MIXED.
239
240pub fn detect_regime(mean_idle_pct: f64, hvg_lambda: f64, bp_h: f64) -> Regime {
241    let chaos_low =
242        hvg_lambda < crate::chaos::HVG_LAMBDA_CHAOTIC_MIN || bp_h < crate::chaos::BP_H_HIGH;
243
244    if chaos_low && mean_idle_pct >= LIGHT_ENTER_PCT as f64 {
245        Regime::Light
246    } else if chaos_low && mean_idle_pct <= HEAVY_ENTER_PCT as f64 {
247        Regime::Heavy
248    } else {
249        Regime::Mixed
250    }
251}
252
253// STABILITY MODE
254
255pub const STABILITY_THRESHOLD: u32 = 10; // CONSECUTIVE STABLE TICKS BEFORE HIBERNATE
256
257pub fn compute_stability_score(
258    prev_score: u32,
259    regime_changed: bool,
260    reflex_events_delta: u64,
261    p99_ns: u64,
262    p99_ceiling_ns: u64,
263) -> u32 {
264    if regime_changed || reflex_events_delta > 0 || p99_ns > p99_ceiling_ns / 2 {
265        return 0;
266    }
267    (prev_score + 1).min(STABILITY_THRESHOLD)
268}
269
270// TELEMETRY GATING
271
272pub fn should_print_telemetry(tick_counter: u64, stability_score: u32) -> bool {
273    if stability_score >= STABILITY_THRESHOLD {
274        tick_counter % 2 == 0
275    } else {
276        true
277    }
278}
279
280// P99 HISTOGRAM
281
282pub const HIST_BUCKETS: usize = 12;
283pub const HIST_EDGES_NS: [u64; HIST_BUCKETS] = [
284    10_000,     // 10us
285    25_000,     // 25us
286    50_000,     // 50us
287    100_000,    // 100us
288    250_000,    // 250us
289    500_000,    // 500us
290    1_000_000,  // 1ms
291    2_000_000,  // 2ms
292    5_000_000,  // 5ms
293    10_000_000, // 10ms
294    20_000_000, // 20ms
295    u64::MAX,   // +inf
296];
297
298// COMPUTE P99 FROM DRAINED HISTOGRAM COUNTS. PURE FUNCTION.
299// CAP AT 20MS (LAST REAL BUCKET) -- +INF WOULD POISON EVERY COMPARISON.
300pub fn compute_p99_from_histogram(counts: &[u64; HIST_BUCKETS]) -> u64 {
301    let total: u64 = counts.iter().sum();
302    if total == 0 {
303        return 0;
304    }
305    let threshold = (total * 99 + 99) / 100;
306    let mut cumulative = 0u64;
307    for i in 0..HIST_BUCKETS {
308        cumulative += counts[i];
309        if cumulative >= threshold {
310            return HIST_EDGES_NS[i].min(HIST_EDGES_NS[HIST_BUCKETS - 2]);
311        }
312    }
313    HIST_EDGES_NS[HIST_BUCKETS - 2]
314}
315
316// MWU ORCHESTRATOR
317// MULTIPLICATIVE WEIGHT UPDATES ACROSS ALL 11 TUNING KNOBS.
318// 6 EXPERT PROFILES, EACH A SCALE FACTOR ON THE REGIME BASELINE.
319// DISCRETE KNOBS (LAG, AFFINITY, DEPTH) USE MAJORITY VOTE, NOT WEIGHTED AVERAGE.
320// 5 LOSS PATHWAYS: P99 SPIKE, RESCUE DELTA, IO DELTA, FORK STORM, CHAOS TRANSITION.
321// 1e-6 WEIGHT FLOOR PREVENTS UNDERFLOW (DEAD WEIGHTS CAN'T RECOVER).
322// PATHWAYS FIRE IMMEDIATELY -- NO SCHMITT-STYLE STREAK CONFIRMATION.
323//
324// v5.11.0: WEIGHTS ARE PER-REGIME (ONE VECTOR PER Light/Mixed/Heavy).
325// ON A STEADY WORKLOAD THE REGIME IS CONSTANT, SO THE ACTIVE VECTOR
326// SEES A STATIONARY LOSS STREAM AND CONVERGES HARD INSTEAD OF BEING
327// RE-LITIGATED EVERY TICK. BUTTERWORTH-DAMPED WEIGHT UPDATE, ANCHORED
328// LEARNING RATE, NO-OP-SKEWED INIT + WARM-UP GATE, ADAPTIVE STEP SIZE,
329// COARSE DIRTY-TRACKING, AND A WEIGHT-VARIANCE CONVERGENCE DETECTOR
330// THAT FEEDS THE QUIESCENCE GATE.
331
332const N_EXPERTS: usize = 6;
333
334// ANCHORED LEARNING RATE. ETA = ETA_CONST * sqrt(ln(N_EXPERTS) / T),
335// ETA_CONST = 1.0, T = 16 (CHAOS_WIN horizon). sqrt(ln 6 / 16) =
336// sqrt(1.79176 / 16) = 0.33465. THE OLD FIXED 8.0 OVERSHOT; THIS IS
337// THE THEORY-OPTIMAL HEDGE/EXP3 RATE FOR THE WINDOW HORIZON. IF
338// LATENCY RESPONSE REGRESSES, ETA_CONST IS THE PRIMARY TUNING SURFACE
339// (RAISE TOWARD 2-3 BEFORE TOUCHING THE QUIESCENCE GATE).
340const ETA: f64 = 0.33465;
341
342const RELAX_RATE: f64 = 0.80;
343const RELAX_HOLD: u32 = 2;
344const RELAX_CEIL_PCT: f64 = 0.70;
345
346// NO-OP-SKEWED INIT: DOMINANT MASS ON THE ANCHOR (EX_BALANCED, THE
347// "LEAVE KNOBS AT REGIME BASELINE" EXPERT WITH THE ALL-1.00 SCALE
348// COLUMN). THIS IS BOTH THE INITIAL VECTOR AND THE RELAX TARGET -- A
349// HEALTHY WORKLOAD RELAXES BACK TO "DO NOTHING", NOT TO A CONTESTED
350// MIDDLE. NOTE: THE FULLY-RELAXED BLEND SITS AT ~0.996x BASELINE (THE
351// THIN AGGRESSOR SPREAD), NOT EXACTLY BASELINE -- SUB-1%, WITHIN
352// RUN-TO-RUN NOISE, AND THE COARSE DIRTY-TRACKING MEANS A STEADY
353// WORKLOAD RETURNS THE EXACT CACHED KNOBS ANYWAY.
354const EQUILIBRIUM: [f64; N_EXPERTS] = [0.04, 0.80, 0.04, 0.04, 0.04, 0.04];
355
356const WEIGHT_FLOOR: f64 = 1e-6;
357
358// CROSS-DOMAIN SCATTER THRESHOLD (PATHWAY 6). PLACEMENT-SIDE CROSS-DOMAIN MIGRATION
359// FRACTION ABOVE THIS (PERCENT OF DISPATCHES) IS TREATED AS STORM PRESSURE.
360// EEVDF'S MEASURED BASELINE IS ~14%; 20 LEAVES HEADROOM SO NORMAL CROSS-DOMAIN
361// WORK-CONSERVATION DOES NOT TRIP THE PATHWAY, ONLY A GENUINE SCATTER CLIMB.
362const SCATTER_THRESH_PCT: u64 = 20;
363
364// WARM-UP GATE: FOR THE FIRST WARMUP_TICKS CALLS PER REGIME, SCORE THE
365// LOSS PATHWAYS (KEEP prev_* EDGE STATE CURRENT) BUT SKIP THE WEIGHT
366// UPDATE AND RETURN BASELINE KNOBS. PREVENTS THE FIRST NOISY TICKS
367// AFTER START / REGIME CHANGE FROM YANKING KNOBS.
368const WARMUP_TICKS: u32 = 3;
369
370// DYNAMIC BUTTERWORTH DAMPING ON THE WEIGHT UPDATE. THE POST-LOSS /
371// POST-RELAX VECTOR IS LOW-PASS BLENDED TOWARD ITS ONE-TICK-AGO SELF
372// SO IT APPROACHES THE TARGET WITHOUT RINGING. THE BLEND COEFFICIENT
373// IS DRIVEN BY SIGNAL TRUST -- RQA-DET + HVG-LAMBDA -- NOT CPU COUNT.
374//
375// PRINCIPLE: BUTTERWORTH = MAXIMALLY-FLAT (FAITHFUL) TRACKING.
376// FAITHFUL TRACKING IS CORRECT ONLY WHEN THE SIGNAL IS TRUSTWORTHY;
377// WHEN THE SIGNAL IS ITSELF CHAOTIC, FAITHFUL TRACKING IS THE THING
378// THAT CAUSES THE CONTROLLER TO RING. SO DAMPING IS A FUNCTION OF
379// HOW STEADY THE WORKLOAD APPEARS:
380//   HIGH RQA-DET + lambda <= PERIODIC_MAX -> TRUST -> LIGHT DAMP (~0.85)
381//   LOW RQA-DET OR lambda >= CHAOTIC_MIN  -> NO TRUST -> HEAVY DAMP (0.50)
382//
383// REPLACED THE v5.10.0 CPU-COUNT CLIFF (0.707 BELOW 11 CPUS, 0.5 AT
384// OR ABOVE) WHICH DAMPED HARDER AT EXACTLY THE CORE COUNTS WHERE THE
385// CONTROLLER NEEDS THE MOST RESPONSIVENESS, PRODUCING THE 12C
386// REGRESSION CLUSTER (IPC BPF 12.9x, MIXED BPF 3.4x, LONGRUN
387// ADAPTIVE 2.2x). NR_CPUS NO LONGER PARTICIPATES IN DAMPING.
388const DAMP_LO: f64 = 0.50;
389const DAMP_HI: f64 = 0.85;
390// PENALTY APPLIED TO TRUST WHEN hvg_lambda CROSSES INTO THE
391// TRANSITION BAND (PERIODIC_MAX, CHAOTIC_MIN). LINEAR-RAMPED ACROSS
392// THE BAND; SATURATES AT 1.0 ONCE lambda >= CHAOTIC_MIN.
393const DAMP_LAMBDA_PENALTY: f64 = 0.20;
394// NEUTRAL TRUST WHEN RQA-DET IS NONE (WINDOW NOT FULL: NO RECURRENCE
395// EVIDENCE YET, NEITHER STEADY NOR CHAOTIC -- MIDDLE OF THE RANGE).
396const DAMP_TRUST_NEUTRAL: f64 = 0.5;
397
398// SIGNAL-TRUST -> DAMP COEFFICIENT. PURE FUNCTION OF THE TWO RAW
399// CHAOS SIGNALS ALREADY COMPUTED EVERY TICK FOR THE QUIESCENCE GATE.
400pub fn compute_damp(rqa_det: Option<f64>, hvg_lambda: f64) -> f64 {
401    let rqa_trust = rqa_det.unwrap_or(DAMP_TRUST_NEUTRAL);
402    let band = crate::chaos::HVG_LAMBDA_CHAOTIC_MIN - crate::chaos::HVG_LAMBDA_PERIODIC_MAX;
403    let lambda_penalty = if hvg_lambda > crate::chaos::HVG_LAMBDA_PERIODIC_MAX {
404        ((hvg_lambda - crate::chaos::HVG_LAMBDA_PERIODIC_MAX) / band).clamp(0.0, 1.0)
405    } else {
406        0.0
407    };
408    let trust = (rqa_trust - DAMP_LAMBDA_PENALTY * lambda_penalty).clamp(0.0, 1.0);
409    DAMP_LO + (DAMP_HI - DAMP_LO) * trust
410}
411
412// ADAPTIVE STEP SIZE: step = STEP_BASE / (1 + residual), residual =
413// aggregate loss this tick. BIG DISTURBANCE -> SMALLER CORRECTIVE
414// STEP; STEADY -> step ~= STEP_BASE. FOLDED INTO THE DAMPING COEFF.
415const STEP_BASE: f64 = 1.0;
416
417// WEIGHT-VARIANCE CONVERGENCE DETECTOR. PER-REGIME RING OF THE LAST
418// VAR_HIST L1 WEIGHT-MOVEMENT SAMPLES. CONVERGED WHEN BOTH HALVES'
419// MEAN MOVEMENT ARE BELOW CONVERGE_MOVE_EPS AND THEIR RATIO IS WITHIN
420// [1 - DELTA, 1 + DELTA]. FEEDS THE QUIESCENCE GATE.
421const VAR_HIST: usize = 8;
422const CONVERGE_MOVE_EPS: f64 = 1e-3;
423const CONVERGE_RATIO_DELTA: f64 = 0.25;
424
425// COARSE DIRTY-TRACKING THRESHOLD: WHEN THE ACTIVE WEIGHT VECTOR MOVED
426// LESS THAN THIS (L1) THIS TICK, update() RETURNS THE CACHED BLEND
427// OUTPUT (last_knobs) AND SKIPS THE BLEND ENTIRELY.
428const WEIGHTS_MOVED_EPS: f64 = 1e-9;
429
430const EX_LATENCY: usize = 0;
431const EX_BALANCED: usize = 1;
432const EX_THROUGHPUT: usize = 2;
433const EX_IO_HEAVY: usize = 3;
434const EX_FORK_STORM: usize = 4;
435const EX_SATURATED: usize = 5;
436
437// CORRECTED CONTINUOUS SCALE FACTORS
438// PROPORTIONALLY ADJUSTED SO sum(EQ[i] * SCALE[i]) = 1.0 AT EQUILIBRIUM.
439// [LATENCY, BALANCED, THROUGHPUT, IO_HEAVY, FORK_STORM, SATURATED]
440const SC_SLICE: [f64; 6] = [0.74, 1.00, 1.23, 0.98, 0.49, 1.47];
441const SC_PREEMPT: [f64; 6] = [0.74, 1.00, 1.23, 0.98, 0.49, 1.47];
442const SC_BATCH: [f64; 6] = [0.78, 1.00, 1.30, 1.30, 0.52, 1.04];
443const SC_LCRI_HI: [f64; 6] = [0.74, 1.00, 1.23, 0.98, 0.98, 0.98];
444const SC_LCRI_LO: [f64; 6] = [0.70, 1.00, 1.40, 0.93, 0.93, 0.93];
445const SC_SOJOURN: [f64; 6] = [0.80, 1.00, 1.60, 0.93, 0.53, 1.07];
446const SC_BURST: [f64; 6] = [0.74, 1.00, 1.47, 0.98, 0.49, 1.23];
447
448// DISCRETE KNOB VALUES (ABSOLUTE, NOT SCALE FACTORS)
449// FORK_STORM (INDEX 4) IS WEAK, NOT OFF: THE BPF READS affinity_mode ONLY AS
450// A BINARY GATE (main.bpf.c:1619, > 0) ON find_idle_l2_sibling. OFF DISABLES
451// THE L2 SIBLING PRE-FILTER, SO DURING A STORM WAKEES SEAT TOPOLOGY-BLIND AND
452// SCATTER CROSS-DOMAIN -- FIGHTING THE PHI/WARM-STAY PLACEMENT INSTEAD OF HELPING.
453// WEAK KEEPS THE L2 PRE-FILTER ON. (STRONG VS WEAK IS A NO-OP TO THE BPF; ONLY
454// OFF DIFFERS.)
455const DV_AFFINITY: [u64; 6] = [
456    AFFINITY_STRONG,
457    AFFINITY_STRONG,
458    AFFINITY_WEAK,
459    AFFINITY_WEAK,
460    AFFINITY_WEAK,
461    AFFINITY_WEAK,
462];
463fn blend_continuous(base: u64, scales: &[f64; 6], w: &[f64; N_EXPERTS]) -> u64 {
464    let v: f64 = (0..N_EXPERTS).map(|i| w[i] * base as f64 * scales[i]).sum();
465    (v.round() as u64).max(1)
466}
467
468fn majority_discrete(values: &[u64; 6], w: &[f64; N_EXPERTS]) -> u64 {
469    // GROUP BY VALUE, SUM WEIGHTS, PICK HIGHEST GROUP
470    let mut best_val = values[0];
471    let mut best_w = 0.0f64;
472    for &v in values.iter() {
473        let total: f64 = (0..N_EXPERTS)
474            .filter(|&i| values[i] == v)
475            .map(|i| w[i])
476            .sum();
477        if total > best_w {
478            best_w = total;
479            best_val = v;
480        }
481    }
482    best_val
483}
484
485#[derive(Clone, Copy, PartialEq, Eq, Debug)]
486pub enum IoBucket {
487    Low,
488    Mid,
489    High,
490}
491
492pub fn io_bucket(io_pct: u64) -> IoBucket {
493    if io_pct > 60 {
494        IoBucket::High
495    } else if io_pct < 15 {
496        IoBucket::Low
497    } else {
498        IoBucket::Mid
499    }
500}
501
502pub struct MwuSignals {
503    pub p99_ns: u64,
504    pub interactive_p99_ns: u64,
505    pub io_pct: u64,
506    pub rescue_count: u64,
507    pub wakeup_rate: u64,
508    // CROSS-DOMAIN SCATTER FRACTION THIS TICK: PLACEMENT-SIDE CROSS-DOMAIN LANDINGS
509    // (XDOM_SEL_* + XDOM_ENQ_T1, EXCLUDING THE PHI-CORRECT STEAL/STEP5 WORK-
510    // CONSERVATION PATHS) AS A PERCENTAGE OF DISPATCHES. EEVDF BASELINE ~14%;
511    // PANDEMONIUM'S STORM REGRESSION RAN ~48%. PATHWAY 6 KEYS ON THIS SO MWU
512    // CAN SEE THE MIGRATION STORM IT MIGHT BE INDUCING AND STEER AWAY FROM IT.
513    pub scatter_pct: u64,
514    // CHAOS PRIMITIVES (RAW-WINDOW DERIVED, NO EWMA, NO SCHMITT).
515    // hvg_lambda     := HVG MEAN DEGREE OF THE idle_pct WINDOW
516    // bp_h_delta     := bp_h(THIS TICK) - bp_h(PREVIOUS TICK), WHERE
517    //                   bp_h IS BANDT-POMPE D=3 PERMUTATION ENTROPY ON
518    //                   THE wakeup_rate WINDOW, NORMALIZED TO [0, 1].
519    //                   POSITIVE = WORKLOAD ENTERED A MORE-DISORDERED
520    //                   ORDINAL REGIME WITHIN THE LAST SECOND.
521    pub hvg_lambda: f64,
522    pub bp_h_delta: f64,
523    // RQA DETERMINISM OVER THE SAME idle_pct WINDOW HVG SEES.
524    // 1.0 = PERFECTLY RECURRENT, 0.0 = NO RECURRENCE, None = WINDOW
525    // NOT YET FULL. CONSUMED BY compute_damp() AS THE PRIMARY TRUST
526    // SIGNAL FOR THE DYNAMIC BUTTERWORTH BLEND.
527    pub rqa_det: Option<f64>,
528}
529
530// SNAPSHOT OF THE BPF DAMPED-HARMONIC OSCILLATOR'S ADAPTIVE STATE.
531// MWU READS THIS BEFORE COMPUTING PATHWAY LOSSES SO IT CAN AVOID
532// DOUBLE-CORRECTING ON RESCUE PRESSURE: WHEN THE OSCILLATOR HAS
533// ALREADY TIGHTENED codel_target_ns TOWARD THE FLOOR, BPF HAS
534// RESPONDED -- MWU STAYS OUT. WHEN THE OSCILLATOR IS NEAR THE
535// CEILING, NO RESCUE PRESSURE EXISTS FOR MWU TO AMPLIFY.
536//
537// ALL FIELDS ZERO = SENTINEL ("READBACK UNAVAILABLE / INIT") -> NO GATING.
538#[derive(Clone, Copy, Debug, Default)]
539pub struct OscillatorState {
540    pub codel_target_ns: u64,
541    pub codel_target_floor_ns: u64,
542    pub codel_target_max_ns: u64,
543    // HOME NEAREST-PEER PHI HOLD (reff_value SLOT 0, ns). THE BPF WARM-STAY
544    // AND STEP-1 R_eff STEAL RELEASE AT codel_target_ns + THIS, NOT AT THE
545    // BARE CODEL WINDOW. position() MEASURES THE BARE WINDOW; THE ABSOLUTE
546    // CHECKS BELOW ADD THIS TERM SO THE DEFER DECISION IS TAKEN AGAINST THE
547    // EFFECTIVE PHI RELEASE POINT THE BPF ACTUALLY USES. 0 = READBACK
548    // UNAVAILABLE -> TREATED AS NO EXTRA HOLD (NEUTRAL).
549    pub home_dist_extra_ns: u64,
550}
551
552impl OscillatorState {
553    // 0.0 = AT FLOOR (TIGHTENED), 1.0 = AT MAX (RELAXED).
554    // SENTINEL OR DEGENERATE RANGE -> 0.5 (CENTER, NEUTRAL).
555    pub fn position(&self) -> f64 {
556        if self.codel_target_max_ns == 0 || self.codel_target_floor_ns >= self.codel_target_max_ns {
557            return 0.5;
558        }
559        let range = (self.codel_target_max_ns - self.codel_target_floor_ns) as f64;
560        let pos = self
561            .codel_target_ns
562            .saturating_sub(self.codel_target_floor_ns) as f64;
563        (pos / range).clamp(0.0, 1.0)
564    }
565
566    // EFFECTIVE PHI RELEASE POINT: WHERE WARM-STAY / STEP-1 STEAL ACTUALLY LET
567    // A TASK LEAVE HOME (codel_target + NEAREST-PEER HOLD). THE DEFER GATE
568    // COMPARES THIS AGAINST codel_eq TO DECIDE WHETHER THE BPF HAS GENUINELY
569    // RESPONDED, RATHER THAN TRUSTING THE BARE-WINDOW position() ALONE.
570    pub fn effective_release_ns(&self) -> u64 {
571        self.codel_target_ns.saturating_add(self.home_dist_extra_ns)
572    }
573}
574
575pub struct MwuController {
576    // PER-REGIME WEIGHT VECTORS, INDEXED BY `regime as usize`
577    // (Light=0, Mixed=1, Heavy=2). EACH REGIME KEEPS ITS OWN LEARNED
578    // VECTOR INSTEAD OF BEING RESET ON EVERY TRANSITION.
579    weights: [[f64; N_EXPERTS]; 3],
580    // ONE-TICK-AGO SNAPSHOT PER REGIME -- THE BUTTERWORTH DAMPING
581    // TARGET AND THE L1-MOVEMENT REFERENCE.
582    prev_weights: [[f64; N_EXPERTS]; 3],
583    // PER-REGIME RING OF L1 WEIGHT-MOVEMENT SAMPLES (CONVERGENCE).
584    weight_move_hist: [[f64; VAR_HIST]; 3],
585    move_hist_head: [usize; 3],
586    move_hist_filled: [usize; 3],
587    baseline: TuningKnobs,
588    // CACHED BLEND OUTPUT -- RETURNED DIRECTLY WHEN THE ACTIVE VECTOR
589    // DID NOT MOVE THIS TICK (COARSE DIRTY-TRACKING).
590    last_knobs: TuningKnobs,
591    healthy_streak: u32,
592    // WARM-UP GATE COUNTER, RESET PER REGIME CHANGE.
593    warmup_ticks: u32,
594    prev_io_bucket: IoBucket,
595    prev_rescuing: bool,
596    prev_lambda_chaotic: bool,
597    // LAST TICK'S CROSS-DOMAIN SCATTER %. PATHWAY 6 IS RISING-EDGE GATED ON THIS
598    // SO A STABLE HIGH-SCATTER REGIME (E.G. 8C+ THROUGHPUT WHERE IT WINS) IS
599    // NOT PENALIZED -- ONLY A CLIMBING STORM IS.
600    prev_scatter_pct: u64,
601    losses_applied: bool,
602    // TRUE WHEN THE ACTIVE WEIGHT VECTOR MOVED THIS TICK.
603    weights_changed: bool,
604}
605
606impl MwuController {
607    pub fn new(baseline: TuningKnobs) -> Self {
608        Self {
609            weights: [EQUILIBRIUM; 3],
610            prev_weights: [EQUILIBRIUM; 3],
611            weight_move_hist: [[0.0; VAR_HIST]; 3],
612            move_hist_head: [0; 3],
613            move_hist_filled: [0; 3],
614            baseline,
615            last_knobs: baseline,
616            healthy_streak: 0,
617            warmup_ticks: 0,
618            prev_io_bucket: IoBucket::Mid,
619            prev_rescuing: false,
620            prev_lambda_chaotic: false,
621            prev_scatter_pct: 0,
622            losses_applied: false,
623            weights_changed: false,
624        }
625    }
626
627    // RESET ONE REGIME'S WEIGHT VECTOR + THE CROSS-PATHWAY EDGE STATE.
628    // CALLED BY THE MONITOR LOOP ON A REGIME TRANSITION: a regime change
629    // IS A DISTURBANCE; STALE EDGE STATE SHOULD NOT CARRY OVER, AND THE
630    // WARM-UP GATE RE-ARMS.
631    pub fn reset_regime(&mut self, r: Regime) {
632        let ri = r as usize;
633        self.weights[ri] = EQUILIBRIUM;
634        self.prev_weights[ri] = EQUILIBRIUM;
635        self.weight_move_hist[ri] = [0.0; VAR_HIST];
636        self.move_hist_head[ri] = 0;
637        self.move_hist_filled[ri] = 0;
638        self.healthy_streak = 0;
639        self.warmup_ticks = 0;
640        self.prev_io_bucket = IoBucket::Mid;
641        self.prev_rescuing = false;
642        self.prev_lambda_chaotic = false;
643        self.prev_scatter_pct = 0;
644        self.losses_applied = false;
645        self.weights_changed = false;
646        self.last_knobs = self.baseline;
647    }
648
649    pub fn set_baseline(&mut self, baseline: TuningKnobs) {
650        self.baseline = baseline;
651    }
652
653    // WEIGHT-VARIANCE CONVERGENCE DETECTOR. TRUE WHEN THE ACTIVE
654    // REGIME'S WEIGHT VECTOR HAS STOPPED MOVING -- BOTH HALVES OF THE
655    // MOVEMENT RING BELOW CONVERGE_MOVE_EPS AND THEIR RATIO NEAR 1.
656    // RETURNS FALSE UNTIL THE RING HAS FILLED (NO FREEZING ON A FRESH
657    // OR JUST-RESET CONTROLLER).
658    pub fn converged(&self, r: Regime) -> bool {
659        let ri = r as usize;
660        if self.move_hist_filled[ri] < VAR_HIST {
661            return false;
662        }
663        let hist = &self.weight_move_hist[ri];
664        let head = self.move_hist_head[ri];
665        let half = VAR_HIST / 2;
666        let mut recent = 0.0;
667        let mut older = 0.0;
668        for k in 0..half {
669            recent += hist[(head + VAR_HIST - 1 - k) % VAR_HIST];
670            older += hist[(head + k) % VAR_HIST];
671        }
672        let recent_mean = recent / half as f64;
673        let older_mean = older / half as f64;
674        if recent_mean >= CONVERGE_MOVE_EPS || older_mean >= CONVERGE_MOVE_EPS {
675            return false;
676        }
677        // BOTH HALVES BELOW EPS. IF THE OLDER HALF IS ESSENTIALLY ZERO
678        // BOTH ARE ZERO -> CONVERGED. OTHERWISE REQUIRE THE RATIO NEAR 1
679        // (NOT STILL DECAYING).
680        if older_mean < 1e-12 {
681            return true;
682        }
683        ((recent_mean / older_mean) - 1.0).abs() <= CONVERGE_RATIO_DELTA
684    }
685
686    pub fn update(
687        &mut self,
688        sig: &MwuSignals,
689        ceiling: u64,
690        _nr_cpus: u64,
691        tau_ns: u64,
692        osc: &OscillatorState,
693        regime: Regime,
694    ) -> TuningKnobs {
695        let ri = regime as usize;
696        let worst = sig.p99_ns.max(sig.interactive_p99_ns);
697        let above = worst > ceiling;
698        let below_relax = (worst as f64) < (ceiling as f64 * RELAX_CEIL_PCT);
699
700        // OSCILLATOR-AWARE GATING. THE BPF DAMPED OSCILLATOR ALREADY
701        // CONSUMES global_rescue_count AND ADAPTS codel_target_ns ON
702        // EVERY TICK. PATHWAYS THAT TRIGGER ON THE SAME RESCUE SIGNAL
703        // (PATHWAY 2 RESCUE-DELTA, PATHWAY 4 FORK-STORM) SHOULD DEFER
704        // TO IT WHEN THE OSCILLATOR HAS ALREADY MOVED -- OTHERWISE BOTH
705        // CONTROLLERS PUSH IN THE SAME DIRECTION AND OVERSHOOT.
706        //
707        // POSITION 0.0 = TIGHTENED (FLOOR), 1.0 = RELAXED (MAX).
708        // < 0.40 -> BPF HAS RESPONDED HEAVILY; SKIP RESCUE-DRIVEN LOSSES.
709        // > 0.90 -> BPF SAYS QUIET; RESCUE BURSTS ARE STALE NOISE; SKIP.
710        //
711        // position() MEASURES THE BARE codel WINDOW, BUT THE BPF ACTUALLY
712        // RELEASES WARM-STAY / STEP-1 STEAL AT codel_target + home_dist_extra
713        // (THE NEAREST-PEER PHI HOLD). WITH A LARGE HOLD THE BARE POSITION CAN
714        // READ "LOOSE" WHILE THE EFFECTIVE RELEASE IS STILL HARD, AND MWU WOULD
715        // WRONGLY STAY OUT OF GENUINE RESCUE PRESSURE. CONFIRM EACH SIDE WITH
716        // THE EFFECTIVE RELEASE VS THE PHI EQUILIBRIUM (codel_eq, FROM THE
717        // BASELINE): ONLY TREAT THE OSCILLATOR AS TIGHT WHEN THE EFFECTIVE
718        // RELEASE IS BELOW EQUILIBRIUM, AND AS LOOSE WHEN IT IS ABOVE. WITH
719        // home_dist_extra_ns = 0 (READBACK UNAVAILABLE) THIS REDUCES TO THE
720        // BARE-POSITION BEHAVIOR. codel_eq = 0 (UNSEEDED) DISABLES THE ABSOLUTE
721        // QUALIFIER (THE BARE POSITION GOVERNS), NEVER GATING SPURIOUSLY.
722        let osc_pos = osc.position();
723        let eff_release = osc.effective_release_ns();
724        let codel_eq = self.baseline.codel_eq_ns;
725        let osc_already_tight = osc_pos < 0.40 && (codel_eq == 0 || eff_release < codel_eq);
726        let osc_already_loose = osc_pos > 0.90 && (codel_eq == 0 || eff_release > codel_eq);
727        let defer_to_oscillator = osc_already_tight || osc_already_loose;
728
729        let mut losses = [0.0f64; N_EXPERTS];
730        let mut has_loss = false;
731
732        // PATHWAY 1: P99 SPIKE (FIRES IMMEDIATELY)
733        if above {
734            self.healthy_streak = 0;
735            let v = ((worst - ceiling) as f64 / ceiling as f64).min(3.0);
736            losses[EX_BALANCED] += v * 0.5;
737            losses[EX_THROUGHPUT] += v * 1.0;
738            losses[EX_IO_HEAVY] += v * 0.6;
739            losses[EX_FORK_STORM] += v * 0.3;
740            losses[EX_SATURATED] += v * 0.9;
741            has_loss = true;
742        }
743
744        // PATHWAY 2: RESCUE DELTA (0 -> NONZERO TRANSITION)
745        // PATHWAY 2: RESCUE DELTA (0 -> NONZERO TRANSITION)
746        // PENALIZE ALL EXPERTS EQUALLY: THE DAMPED OSCILLATION IN BPF
747        // HANDLES TIGHTENING VIA THE CODEL TARGET. MWU SHOULD HOLD STEADY,
748        // NOT COMPOUND BY ALSO TIGHTENING SLICES VIA LATENCY EXPERT.
749        let rescuing = sig.rescue_count > 0;
750        if rescuing && !self.prev_rescuing && !defer_to_oscillator {
751            let v = (sig.rescue_count as f64 * 1.5).min(3.0);
752            losses[EX_LATENCY] += v * 0.4;
753            losses[EX_THROUGHPUT] += v * 0.6;
754            losses[EX_SATURATED] += v * 0.6;
755            losses[EX_IO_HEAVY] += v * 0.4;
756            losses[EX_BALANCED] += v * 0.2;
757            has_loss = true;
758        }
759        self.prev_rescuing = rescuing;
760
761        // PATHWAY 3: IO DELTA (BUCKET TRANSITION)
762        let cur_io = io_bucket(sig.io_pct);
763        if cur_io != self.prev_io_bucket {
764            match cur_io {
765                IoBucket::High => {
766                    let v = ((sig.io_pct as f64 - 60.0) / 40.0).min(1.0);
767                    for i in 0..N_EXPERTS {
768                        if i != EX_IO_HEAVY {
769                            losses[i] += v * 0.8;
770                        }
771                    }
772                    has_loss = true;
773                }
774                IoBucket::Low => {
775                    let v = ((15.0 - sig.io_pct as f64) / 15.0).clamp(0.0, 1.0);
776                    losses[EX_IO_HEAVY] += v * 1.0;
777                    has_loss = true;
778                }
779                IoBucket::Mid => {}
780            }
781        }
782        self.prev_io_bucket = cur_io;
783
784        // PATHWAY 4: FORK STORM (PRESSURE-CONFIRMED, FIRES IMMEDIATELY).
785        // GATE THRESHOLD IS TAU-DERIVED. sig.wakeup_rate IS THE RAW TOTAL
786        // WAKES/SEC; scale_tau_u64(tau, K_FORK_STORM_RATE_Q16) PRODUCES
787        // THE COMPARISON THRESHOLD. AT THE 12C REFERENCE (tau=40MS) THE
788        // THRESHOLD IS ~8000/SEC; AT 4C IT TIGHTENS TO ~1200/SEC. A FORK
789        // STORM ALSO REQUIRES ACTIVE RESCUES (rescue_count > 0) AS GROUND-
790        // TRUTH PRESSURE -- THE PRESSURE PREDICATE IS THE CONFIRMATION;
791        // NO ADDITIONAL STREAK COUNTER.
792        //
793        // FORK_STORM EXPERT (SC_SLICE=0.49, SC_PREEMPT=0.49, SC_BATCH=0.52,
794        // SC_SOJOURN=0.53, SC_BURST=0.49) DOMINATES THE BLEND DURING A REAL
795        // STORM, DRIVING burst_slice_ns / preempt_thresh_ns / sojourn_thresh_ns
796        // / batch_slice_ns DOWN END-TO-END.
797        let fork_thresh = scale_tau_u64(tau_ns, K_FORK_STORM_RATE_Q16).max(FORK_STORM_RATE_FLOOR);
798        let fork_storm = sig.wakeup_rate > fork_thresh && sig.rescue_count > 0;
799        if fork_storm && !defer_to_oscillator {
800            let denom = fork_thresh.max(1) as f64;
801            let v = ((sig.wakeup_rate as f64 / denom) - 1.0).clamp(0.0, 3.0);
802            losses[EX_BALANCED] += v * 0.30;
803            losses[EX_THROUGHPUT] += v * 1.00;
804            losses[EX_IO_HEAVY] += v * 0.50;
805            losses[EX_SATURATED] += v * 0.80;
806            has_loss = true;
807        }
808
809        // PATHWAY 5: CHAOS TRANSITION (RAW-WINDOW, NO EWMA, NO SCHMITT)
810        // FIRES WHEN THE WORKLOAD'S ORDINAL OR AMPLITUDE-DEGREE STRUCTURE
811        // SHIFTS WITHIN THE LAST WINDOW. THE FAILED-PREDICTION INTERPRETATION
812        // IS: WHICHEVER EXPERT IS CURRENTLY DOMINANT WAS PRICED FOR THE
813        // PREVIOUS WINDOW; PENALIZE IT PROPORTIONAL TO THE TRANSITION SIZE
814        // SO THE BLEND RAPIDLY RE-WEIGHTS TOWARD BALANCED / SATURATED.
815        // EX_BALANCED IS EXEMPT (IT IS THE REGIME-AGNOSTIC BASELINE).
816        //
817        // GATE CONDITIONS (EITHER FIRES):
818        //   - bp_h_delta > 0.10   (>10% NORMALIZED PERMUTATION-ENTROPY JUMP)
819        //   - hvg_lambda CROSSED CHAOTIC_MIN UPWARD
820        let bp_jump = sig.bp_h_delta > 0.10;
821        let lambda_cross =
822            sig.hvg_lambda >= crate::chaos::HVG_LAMBDA_CHAOTIC_MIN && !self.prev_lambda_chaotic;
823        let chaos_transition = bp_jump || lambda_cross;
824        if chaos_transition {
825            // SIZE OF THE LOSS: SCALES WITH WHICHEVER GATE TRIGGERED HARDER.
826            let v_bp = (sig.bp_h_delta / 0.10).clamp(0.0, 3.0);
827            let v_l = if lambda_cross {
828                ((sig.hvg_lambda - crate::chaos::HVG_LAMBDA_CHAOTIC_MIN)
829                    / (4.0 - crate::chaos::HVG_LAMBDA_CHAOTIC_MIN))
830                    .clamp(0.0, 1.5)
831            } else {
832                0.0
833            };
834            let v = v_bp.max(v_l);
835            // FIND THE DOMINANT EXPERT (HIGHEST WEIGHT) AND PENALIZE IT.
836            // EX_BALANCED IS EXEMPT (THE ANCHOR IS NEVER PENALIZED).
837            let mut dom_idx = EX_BALANCED;
838            let mut dom_w = 0.0f64;
839            for i in 0..N_EXPERTS {
840                if i == EX_BALANCED {
841                    continue;
842                }
843                if self.weights[ri][i] > dom_w {
844                    dom_w = self.weights[ri][i];
845                    dom_idx = i;
846                }
847            }
848            if dom_idx != EX_BALANCED && dom_w > 0.0 {
849                losses[dom_idx] += v * 0.8;
850                has_loss = true;
851            }
852        }
853        self.prev_lambda_chaotic = sig.hvg_lambda >= crate::chaos::HVG_LAMBDA_CHAOTIC_MIN;
854
855        // PATHWAY 6: CROSS-DOMAIN SCATTER (PHI-AWARE, RISING-EDGE GATED).
856        // sig.scatter_pct IS THE PLACEMENT-SIDE CROSS-DOMAIN MIGRATION FRACTION
857        // (XDOM_SEL_* + XDOM_ENQ_T1, EXCLUDING THE PHI-CORRECT STEAL/STEP5
858        // WORK-CONSERVATION PATHS -- COMPUTED IN THE ADAPTIVE LOOP). WHEN IT
859        // CLIMBS PAST THE EEVDF-BASELINE HEADROOM THE BLEND HAS DRIFTED TOWARD
860        // EXPERTS THAT SCATTER WAKEES THE BPF WARM-STAY/PER-CPU LANDING TRIED
861        // TO KEEP HOME. PENALIZE THE EXPERTS THAT FAVOR WIDE SLICES + WEAK/OFF
862        // AFFINITY (THROUGHPUT, SATURATED, FORK_STORM) SO THE BLEND RE-WEIGHTS
863        // TOWARD LATENCY/BALANCED AND AFFINITY STRONG, ALIGNING THE ADAPTIVE
864        // LAYER WITH PHI PLACEMENT INSTEAD OF FIGHTING IT.
865        //
866        // RISING-EDGE: ONLY FIRES WHILE SCATTER IS HIGH AND INCREASING. A
867        // STABLE HIGH-SCATTER REGIME (8C+ THROUGHPUT, WHERE WIDE-SLICE SCATTER
868        // IS A WINNING TRADE) PLATEAUS AND IS NOT PENALIZED -- ONLY A CLIMBING
869        // STORM IS. EX_BALANCED/EX_LATENCY ARE NEVER PENALIZED (THEY ARE THE
870        // DIRECTION WE WANT THE BLEND TO MOVE TOWARD).
871        let scatter_high = sig.scatter_pct > SCATTER_THRESH_PCT;
872        let scatter_rising = scatter_high && sig.scatter_pct > self.prev_scatter_pct;
873        if scatter_rising {
874            let v = ((sig.scatter_pct as f64 - SCATTER_THRESH_PCT as f64)
875                / SCATTER_THRESH_PCT as f64)
876                .clamp(0.0, 3.0);
877            losses[EX_THROUGHPUT] += v * 1.00;
878            losses[EX_SATURATED] += v * 0.80;
879            losses[EX_FORK_STORM] += v * 0.60;
880            has_loss = true;
881        }
882        self.prev_scatter_pct = sig.scatter_pct;
883
884        // WARM-UP GATE. THE PATHWAYS ABOVE HAVE SCORED `losses` AND
885        // UPDATED THE prev_* EDGE STATE. FOR THE FIRST WARMUP_TICKS
886        // CALLS PER REGIME, SKIP THE WEIGHT UPDATE ENTIRELY AND RETURN
887        // BASELINE -- THE FIRST NOISY TICKS AFTER START / REGIME CHANGE
888        // MUST NOT YANK KNOBS.
889        if self.warmup_ticks < WARMUP_TICKS {
890            self.warmup_ticks += 1;
891            self.losses_applied = has_loss;
892            self.weights_changed = false;
893            let mut k = self.baseline;
894            k.topology_tau_ns = 0;
895            k.codel_eq_ns = 0;
896            self.last_knobs = k;
897            return k;
898        }
899
900        // ACTIVE WEIGHT VECTOR -- LOCAL COPY, MUTATED THROUGH THE
901        // LOSS / RELAX / DAMP STAGES THEN COMMITTED BACK.
902        let mut w = self.weights[ri];
903
904        // APPLY LOSSES WITH WEIGHT FLOOR.
905        if has_loss {
906            for i in 0..N_EXPERTS {
907                if losses[i] > 0.0 {
908                    w[i] *= (-ETA * losses[i]).exp();
909                }
910                if w[i] < WEIGHT_FLOOR {
911                    w[i] = WEIGHT_FLOOR;
912                }
913            }
914            let sum: f64 = w.iter().sum();
915            for x in w.iter_mut() {
916                *x /= sum;
917            }
918        }
919
920        // RELAXATION -- TOWARD THE NO-OP-SKEWED EQUILIBRIUM.
921        if !has_loss && below_relax {
922            self.healthy_streak += 1;
923            if self.healthy_streak >= RELAX_HOLD {
924                for i in 0..N_EXPERTS {
925                    w[i] = (1.0 - RELAX_RATE) * w[i] + RELAX_RATE * EQUILIBRIUM[i];
926                }
927            }
928        } else if !has_loss {
929            self.healthy_streak = 0;
930        }
931
932        // DYNAMIC BUTTERWORTH DAMPING + ADAPTIVE STEP. `w` IS NOW THE
933        // RAW POST-LOSS / POST-RELAX TARGET; LOW-PASS BLEND IT TOWARD
934        // THE ONE-TICK-AGO VECTOR. DAMP IS DRIVEN BY SIGNAL TRUST
935        // (RQA-DET + HVG-LAMBDA), NOT CPU COUNT -- SEE compute_damp.
936        // residual = aggregate loss this tick: a big disturbance takes
937        // a smaller corrective step (anti-overshoot governor).
938        let residual: f64 = losses.iter().sum();
939        let step = STEP_BASE / (1.0 + residual);
940        let damp = compute_damp(sig.rqa_det, sig.hvg_lambda);
941        let eff = (damp * step).clamp(0.0, 1.0);
942        let prev = self.prev_weights[ri];
943        for i in 0..N_EXPERTS {
944            w[i] = eff * w[i] + (1.0 - eff) * prev[i];
945            if w[i] < WEIGHT_FLOOR {
946                w[i] = WEIGHT_FLOOR;
947            }
948        }
949        let sum: f64 = w.iter().sum();
950        for x in w.iter_mut() {
951            *x /= sum;
952        }
953
954        // L1 MOVEMENT FROM LAST TICK -- FEEDS THE CONVERGENCE DETECTOR.
955        let mut move_l1 = 0.0;
956        for i in 0..N_EXPERTS {
957            move_l1 += (w[i] - prev[i]).abs();
958        }
959        let h = self.move_hist_head[ri];
960        self.weight_move_hist[ri][h] = move_l1;
961        self.move_hist_head[ri] = (h + 1) % VAR_HIST;
962        if self.move_hist_filled[ri] < VAR_HIST {
963            self.move_hist_filled[ri] += 1;
964        }
965
966        // COMMIT: w_new IS BOTH THE ACTIVE VECTOR AND NEXT TICK'S
967        // prev SNAPSHOT.
968        self.weights[ri] = w;
969        self.prev_weights[ri] = w;
970        self.weights_changed = move_l1 > WEIGHTS_MOVED_EPS;
971        self.losses_applied = has_loss;
972
973        // COARSE DIRTY-TRACKING: IF THE VECTOR DID NOT MOVE, THE BLEND
974        // OUTPUT IS IDENTICAL TO LAST TICK -- RETURN THE CACHE AND SKIP
975        // THE BLEND ENTIRELY.
976        if !self.weights_changed {
977            return self.last_knobs;
978        }
979
980        // BLEND: CONTINUOUS KNOBS VIA SCALE FACTORS, DISCRETE VIA MAJORITY.
981        let b = &self.baseline;
982        let blended_slice = blend_continuous(b.slice_ns, &SC_SLICE, &w);
983        let blended_burst = blend_continuous(b.burst_slice_ns, &SC_BURST, &w);
984        let mut blended_sojourn = blend_continuous(b.sojourn_thresh_ns, &SC_SOJOURN, &w);
985
986        // SOJOURN FLOOR: tick() KICKS PER-CPU DSQs WHOSE OLDEST TASK HAS AGED
987        // PAST sojourn_thresh_ns (BPF, main.bpf.c:2374). THAT EVICTION DEADLINE
988        // MUST NOT SIT ABOVE THE THRESHOLD WARM-STAY / STEP-1 R_eff STEAL
989        // ACTUALLY RELEASE AT, OR THE ADAPTIVE LAYER HOLDS TASKS LONG PAST THE
990        // POINT THE PHI GATES ASSUMED RELIEF HAD ARRIVED (THE 2C/4C LONG-RUN /
991        // LAT P99 TAIL). THE BPF OVERFLOW RESCUE IS NO LONGER "[4ms,10ms]" --
992        // IT WAS REWIRED TO THE PHI EQUILIBRIUM (overflow_sojourn_rescue_ns =
993        // codel_target_equilibrium_ns, main.bpf.c:995), SUB-MS AT LOW CORE.
994        // ANCHOR THE FLOOR TO THAT SAME PHI-DERIVED EQUILIBRIUM SO USERSPACE
995        // EVICTION AND THE BPF PHI RELEASE AGREE. THE +blended_slice TERM IS
996        // THE GENUINE DISPATCH-SERVICE-WINDOW GUARD (KEPT). codel_eq_ns CAN BE
997        // SEEDED 0 BY MwuController::new BEFORE THE TOPOLOGY OVERLAY LANDS --
998        // GUARD AGAINST COLLAPSING THE FLOOR AND REINTRODUCING THE KICK STORM.
999        let floor_base = if b.codel_eq_ns >= 200_000 {
1000            b.codel_eq_ns
1001        } else {
1002            4_000_000
1003        };
1004        let sojourn_floor = floor_base.saturating_add(blended_slice);
1005        if blended_sojourn < sojourn_floor {
1006            blended_sojourn = sojourn_floor;
1007        }
1008
1009        let k = TuningKnobs {
1010            slice_ns: blended_slice,
1011            // FLOOR AT REGIME BASELINE: MWU may loosen preempt (raise it) but
1012            // never tighten below baseline -- sub-ms preempt at 2C thrashed the
1013            // longrun (the ADAPTIVE 2C tail). No-op where MWU stays above it.
1014            preempt_thresh_ns: blend_continuous(b.preempt_thresh_ns, &SC_PREEMPT, &w)
1015                .max(b.preempt_thresh_ns),
1016            batch_slice_ns: blend_continuous(b.batch_slice_ns, &SC_BATCH, &w),
1017            lat_cri_thresh_high: blend_continuous(b.lat_cri_thresh_high, &SC_LCRI_HI, &w),
1018            lat_cri_thresh_low: blend_continuous(b.lat_cri_thresh_low, &SC_LCRI_LO, &w),
1019            affinity_mode: majority_discrete(&DV_AFFINITY, &w),
1020            sojourn_thresh_ns: blended_sojourn,
1021            burst_slice_ns: blended_burst,
1022            // topology_tau_ns AND codel_eq_ns ARE OWNED BY THE TOPOLOGY LAYER;
1023            // MWU DOESN'T TOUCH THEM. THE MONITOR LOOP OVERLAYS THE LIVE BPF
1024            // VALUES BACK ONTO MWU'S OUTPUT BEFORE WRITING -- PASSTHROUGH.
1025            topology_tau_ns: 0,
1026            codel_eq_ns: 0,
1027        };
1028        self.last_knobs = k;
1029        k
1030    }
1031
1032    pub fn had_losses(&self) -> bool {
1033        self.losses_applied
1034    }
1035
1036    pub fn scale(&self, r: Regime) -> f64 {
1037        let w = &self.weights[r as usize];
1038        (0..N_EXPERTS).map(|i| w[i] * SC_SLICE[i]).sum()
1039    }
1040}
1041
1042// QUIESCENCE GATE
1043// DETECTS STEADY STATE CHEAPLY AND LATCHES A "FROZEN" FLAG THAT TELLS
1044// THE MONITOR LOOP TO SKIP THE EXPENSIVE MWU RETUNE + KNOB WRITE. THE
1045// LOOP STILL TICKS AT 1 HZ -- THE CHAOS SENSORS ARE THE EXIT CONDITION
1046// FOR FROZEN MODE -- BUT THE ORCHESTRATOR MACHINERY STOPS.
1047//
1048// STEADY STATE IS THE CONJUNCTION OF THREE SIGNALS, HELD FOR
1049// QUIESCE_ENTER_TICKS CONSECUTIVE TICKS:
1050//   - HVG MEAN DEGREE IN THE PERIODIC BAND (lambda <= PERIODIC_MAX);
1051//     NOTE THE Engine A/B SHORTHAND "lambda ~= ln(3/2)" REFERS TO THE
1052//     IID *CHARACTERISTIC EXPONENT*, NOT A lambda VALUE -- THE GATE
1053//     USES THE PERIODIC-BAND THRESHOLD.
1054//   - RQA DETERMINISM AT OR ABOVE RQA_DET_STEADY_MIN.
1055//   - THE ACTIVE-REGIME MWU WEIGHT VECTOR HAS CONVERGED.
1056// EXIT IS IMMEDIATE WHEN THE SIGNAL LEAVES THE BAND -- THE STREAK
1057// COUNTER PROVIDES ENTRY HYSTERESIS, NO EXIT HYSTERESIS NEEDED.
1058
1059// CONSECUTIVE IN-BAND TICKS BEFORE THE GATE LATCHES (~1/4 OF THE
1060// 16-TICK CHAOS WINDOW).
1061pub const QUIESCE_ENTER_TICKS: u32 = 4;
1062
1063pub struct QuiescenceState {
1064    in_band_streak: u32,
1065    frozen: bool,
1066}
1067
1068impl Default for QuiescenceState {
1069    fn default() -> Self {
1070        Self::new()
1071    }
1072}
1073
1074impl QuiescenceState {
1075    pub const fn new() -> Self {
1076        Self {
1077            in_band_streak: 0,
1078            frozen: false,
1079        }
1080    }
1081
1082    // ADVANCE THE GATE ONE TICK. RETURNS THE LATCHED `frozen` FLAG.
1083    // rqa_det IS None WHEN THE WINDOW IS NOT YET FULL -- THAT NEVER
1084    // COUNTS AS IN-BAND (NEVER FREEZE ON INSUFFICIENT DATA).
1085    pub fn update(&mut self, hvg_lambda: f64, rqa_det: Option<f64>, mwu_converged: bool) -> bool {
1086        let in_band = hvg_lambda <= crate::chaos::HVG_LAMBDA_PERIODIC_MAX
1087            && rqa_det.map_or(false, |d| d >= crate::chaos::RQA_DET_STEADY_MIN)
1088            && mwu_converged;
1089
1090        if in_band {
1091            self.in_band_streak = self.in_band_streak.saturating_add(1);
1092            if self.in_band_streak >= QUIESCE_ENTER_TICKS {
1093                self.frozen = true;
1094            }
1095        } else {
1096            self.in_band_streak = 0;
1097            self.frozen = false;
1098        }
1099        self.frozen
1100    }
1101}
1102
1103// ADAPTIVE-RARITY RETUNE INTERVAL
1104// WHEN THE ORCHESTRATOR IS NOT FROZEN BUT A RETUNE PRODUCES ONLY A
1105// SUB-THRESHOLD KNOB DELTA, STRETCH THE INTERVAL BETWEEN RETUNES x1.5
1106// (UP TO A FORCED CEILING) SO THE LOOP CONVERGES TOWARD QUIESCENCE.
1107// ANY DISTURBANCE SNAPS IT BACK TO THE BASE INTERVAL.
1108
1109pub const RETUNE_INTERVAL_BASE: u32 = 1;
1110pub const RETUNE_INTERVAL_MAX: u32 = 8;
1111
1112pub fn next_retune_interval(cur: u32, sub_threshold: bool, disturbed: bool) -> u32 {
1113    if disturbed {
1114        RETUNE_INTERVAL_BASE
1115    } else if sub_threshold {
1116        // x1.5 STRETCH, BUT ALWAYS GROW BY AT LEAST 1 -- INTEGER x1.5
1117        // OF THE BASE INTERVAL (1) WOULD OTHERWISE STALL AT 1.
1118        let stretched = (cur.saturating_mul(3) / 2).max(cur + 1);
1119        stretched.clamp(RETUNE_INTERVAL_BASE, RETUNE_INTERVAL_MAX)
1120    } else {
1121        cur
1122    }
1123}
1124
1125// COMMIT-ON-CHANGE: TRUE IFF THE TWO KNOB SETS DIFFER ON ANY
1126// MWU-OWNED FIELD. topology_tau_ns / codel_eq_ns ARE EXCLUDED -- THEY
1127// ARE OWNED BY THE TOPOLOGY LAYER AND WRITTEN INDEPENDENTLY VIA
1128// write_topology_fields(); INCLUDING THEM WOULD SPURIOUSLY TRIP THE
1129// DIFF EVERY TICK THE LOOP OVERLAYS THEM.
1130pub fn knobs_differ(a: &TuningKnobs, b: &TuningKnobs) -> bool {
1131    a.slice_ns != b.slice_ns
1132        || a.preempt_thresh_ns != b.preempt_thresh_ns
1133        || a.batch_slice_ns != b.batch_slice_ns
1134        || a.lat_cri_thresh_high != b.lat_cri_thresh_high
1135        || a.lat_cri_thresh_low != b.lat_cri_thresh_low
1136        || a.affinity_mode != b.affinity_mode
1137        || a.sojourn_thresh_ns != b.sojourn_thresh_ns
1138        || a.burst_slice_ns != b.burst_slice_ns
1139}