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 (SCHMITT TRIGGER)
6// DIRECTIONAL HYSTERESIS PREVENTS OSCILLATION AT REGIME BOUNDARIES.
7// WIDE DEAD ZONES: MUST CLEARLY ENTER A REGIME AND CLEARLY LEAVE IT.
8
9pub const HEAVY_ENTER_PCT: u64 = 10; // ENTER HEAVY: IDLE < 10%
10pub const HEAVY_EXIT_PCT: u64 = 25; // LEAVE HEAVY: IDLE > 25%
11pub const LIGHT_ENTER_PCT: u64 = 50; // ENTER LIGHT: IDLE > 50%
12pub const LIGHT_EXIT_PCT: u64 = 30; // LEAVE LIGHT: IDLE < 30%
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 LIGHT_SLICE_NS: u64 = 2_000_000; // 2MS
20const LIGHT_PREEMPT_NS: u64 = 1_000_000; // 1MS: AGGRESSIVE
21const LIGHT_LAG_SCALE: u64 = 6;
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_LAG_SCALE: u64 = 4;
27const MIXED_BATCH_NS: u64 = 20_000_000; // 20MS: MATCHES LIGHT/HEAVY/BPF DEFAULT
28
29const HEAVY_SLICE_NS: u64 = 4_000_000; // 4MS: WIDER FOR THROUGHPUT
30const HEAVY_PREEMPT_NS: u64 = 2_000_000; // 2MS: SLIGHTLY RELAXED
31const HEAVY_LAG_SCALE: u64 = 2;
32const HEAVY_BATCH_NS: u64 = 20_000_000; // 20MS: LET BATCH RIP
33
34// P99 CEILINGS
35
36const LIGHT_P99_CEIL_NS: u64 = 3_000_000; // 3MS
37const MIXED_P99_CEIL_NS: u64 = 5_000_000; // 5MS: BELOW 16MS FRAME BUDGET
38const HEAVY_P99_CEIL_NS: u64 = 10_000_000; // 10MS: HEAVY LOAD, REALISTIC
39
40// CLASSIFIER THRESHOLDS
41// LAT_CRI SCORE BOUNDARIES FOR TIER CLASSIFICATION
42// EXPOSED AS TUNING KNOBS FOR RUNTIME ADJUSTMENT
43
44pub const DEFAULT_LAT_CRI_THRESH_HIGH: u64 = 32; // >= THIS: LAT_CRITICAL
45pub const DEFAULT_LAT_CRI_THRESH_LOW: u64 = 8; // >= THIS: INTERACTIVE, BELOW: BATCH
46
47// TUNING KNOBS
48// MATCHES struct tuning_knobs IN BPF (intf.h)
49
50// AFFINITY MODE: L2 PLACEMENT STRENGTH
51pub const AFFINITY_OFF: u64 = 0;
52pub const AFFINITY_WEAK: u64 = 1;
53pub const AFFINITY_STRONG: u64 = 2;
54
55#[repr(C)]
56#[derive(Clone, Copy)]
57pub struct TuningKnobs {
58    pub slice_ns: u64,
59    pub preempt_thresh_ns: u64,
60    pub lag_scale: u64,
61    pub batch_slice_ns: u64,
62    pub lat_cri_thresh_high: u64,
63    pub lat_cri_thresh_low: u64,
64    pub affinity_mode: u64,
65    pub sojourn_thresh_ns: u64,
66    pub burst_slice_ns: u64,
67    // FIEDLER-DERIVED TOPOLOGY TIME CONSTANT (TAU_SCALE_NS / lambda_2).
68    // ZERO MEANS RUST HAS NOT YET WRITTEN tau; BPF USES THE PRE-FIRST-TICK
69    // FALLBACK CONSTANTS UNTIL A NONZERO VALUE LANDS. WRITTEN BY RUST AT
70    // TOPOLOGY DETECT AND ON HOTPLUG; READ BY BPF AT THE FIRST CPU-0 TICK.
71    pub topology_tau_ns: u64,
72    // R_eff-DERIVED CODEL EQUILIBRIUM TARGET (<R_eff> * 2m * tau).
73    // CO-LOCATED WITH topology_tau_ns; SAME ZERO/WRITE/CLAMP SEMANTICS.
74    pub codel_eq_ns: u64,
75}
76
77impl Default for TuningKnobs {
78    fn default() -> Self {
79        Self {
80            slice_ns: 1_000_000,
81            preempt_thresh_ns: 1_000_000,
82            lag_scale: 4,
83            batch_slice_ns: 20_000_000,
84            lat_cri_thresh_high: DEFAULT_LAT_CRI_THRESH_HIGH,
85            lat_cri_thresh_low: DEFAULT_LAT_CRI_THRESH_LOW,
86            affinity_mode: AFFINITY_OFF,
87            sojourn_thresh_ns: 5_000_000,
88            burst_slice_ns: 1_000_000,
89            topology_tau_ns: 0,
90            codel_eq_ns: 0,
91        }
92    }
93}
94
95// REGIME
96
97#[repr(u8)]
98#[derive(Clone, Copy, PartialEq, Eq, Debug)]
99pub enum Regime {
100    Light = 0,
101    Mixed = 1,
102    Heavy = 2,
103}
104
105impl Regime {
106    pub fn label(self) -> &'static str {
107        match self {
108            Self::Light => "LIGHT",
109            Self::Mixed => "MIXED",
110            Self::Heavy => "HEAVY",
111        }
112    }
113
114    pub fn p99_ceiling(self) -> u64 {
115        match self {
116            Self::Light => LIGHT_P99_CEIL_NS,
117            Self::Mixed => MIXED_P99_CEIL_NS,
118            Self::Heavy => HEAVY_P99_CEIL_NS,
119        }
120    }
121}
122
123// REGIME KNOBS
124
125pub fn regime_knobs(r: Regime) -> TuningKnobs {
126    match r {
127        Regime::Light => TuningKnobs {
128            slice_ns: LIGHT_SLICE_NS,
129            preempt_thresh_ns: LIGHT_PREEMPT_NS,
130            lag_scale: LIGHT_LAG_SCALE,
131            batch_slice_ns: LIGHT_BATCH_NS,
132            lat_cri_thresh_high: DEFAULT_LAT_CRI_THRESH_HIGH,
133            lat_cri_thresh_low: DEFAULT_LAT_CRI_THRESH_LOW,
134            affinity_mode: AFFINITY_WEAK,
135            sojourn_thresh_ns: 5_000_000,
136            burst_slice_ns: 1_000_000,
137            topology_tau_ns: 0,
138            codel_eq_ns: 0,
139        },
140        Regime::Mixed => TuningKnobs {
141            slice_ns: MIXED_SLICE_NS,
142            preempt_thresh_ns: MIXED_PREEMPT_NS,
143            lag_scale: MIXED_LAG_SCALE,
144            batch_slice_ns: MIXED_BATCH_NS,
145            lat_cri_thresh_high: DEFAULT_LAT_CRI_THRESH_HIGH,
146            lat_cri_thresh_low: DEFAULT_LAT_CRI_THRESH_LOW,
147            affinity_mode: AFFINITY_STRONG,
148            sojourn_thresh_ns: 5_000_000,
149            burst_slice_ns: 1_000_000,
150            topology_tau_ns: 0,
151            codel_eq_ns: 0,
152        },
153        Regime::Heavy => TuningKnobs {
154            slice_ns: HEAVY_SLICE_NS,
155            preempt_thresh_ns: HEAVY_PREEMPT_NS,
156            lag_scale: HEAVY_LAG_SCALE,
157            batch_slice_ns: HEAVY_BATCH_NS,
158            lat_cri_thresh_high: DEFAULT_LAT_CRI_THRESH_HIGH,
159            lat_cri_thresh_low: DEFAULT_LAT_CRI_THRESH_LOW,
160            affinity_mode: AFFINITY_WEAK,
161            sojourn_thresh_ns: 5_000_000,
162            burst_slice_ns: 1_000_000,
163            topology_tau_ns: 0,
164            codel_eq_ns: 0,
165        },
166    }
167}
168
169// TAU-SCALED REGIME KNOBS
170// CAPS DIMENSIONED AS Q16 FIXED-POINT MULTIPLIERS OF tau_ns. k_i CALIBRATED
171// AGAINST THE 12C REFERENCE TOPOLOGY (tau ~= 40MS):
172//   SLICE_CAP:   0.15 -> 6MS  AT tau=40MS
173//   PREEMPT_CAP: 0.075 -> 3MS AT tau=40MS
174//   BATCH_CAP:   1.5 -> 60MS  AT tau=40MS (Mixed ONLY)
175//   SOJOURN:     0.15 -> 6MS  AT tau=40MS
176// PER-CAP CLAMPS ARE SAFETY RAILS.
177const K_SLICE_CAP_Q16: u64 = 9830; // 0.15
178const K_PREEMPT_CAP_Q16: u64 = 4915; // 0.075
179const K_BATCH_CAP_Q16: u64 = 98304; // 1.5
180const K_SOJOURN_Q16: u64 = 9830; // 0.15
181
182// FORK-STORM RAW-WAKE-RATE THRESHOLD. Q16 RATIO INTERPRETED AS
183// "WAKES/SEC PER MS-OF-tau", SO scale_tau_u64(tau, K) PRODUCES
184// THE TOTAL-WAKE THRESHOLD (NOT PER-CPU). AT THE 12C REFERENCE
185// (tau=40MS) THE GATE FIRES AT ~8000 WAKE/S, TIGHTENING LINEARLY
186// AT LOWER tau (1200/S AT 4C, 400/S AT THE 2C FLOOR).
187const K_FORK_STORM_RATE_Q16: u64 = 13107; // 0.20
188const FORK_STORM_RATE_FLOOR: u64 = 200; // HZ; CLAMPS BELOW tau=1MS
189
190#[inline]
191fn scale_tau_u64(tau_ns: u64, k_q16: u64) -> u64 {
192    (tau_ns as u128 * k_q16 as u128 >> 16) as u64
193}
194
195pub fn scaled_regime_knobs(r: Regime, _nr_cpus: u64, tau_ns: u64) -> TuningKnobs {
196    let mut knobs = regime_knobs(r);
197
198    let slice_cap_tau = scale_tau_u64(tau_ns, K_SLICE_CAP_Q16).clamp(500_000, 8_000_000);
199    let preempt_cap_tau = scale_tau_u64(tau_ns, K_PREEMPT_CAP_Q16).clamp(250_000, 4_000_000);
200    let sojourn_tau = scale_tau_u64(tau_ns, K_SOJOURN_Q16).clamp(2_000_000, 6_000_000);
201
202    knobs.slice_ns = knobs.slice_ns.min(slice_cap_tau);
203    knobs.preempt_thresh_ns = knobs.preempt_thresh_ns.min(preempt_cap_tau);
204    if matches!(r, Regime::Mixed) {
205        let batch_cap_tau = scale_tau_u64(tau_ns, K_BATCH_CAP_Q16).clamp(10_000_000, 80_000_000);
206        knobs.batch_slice_ns = knobs.batch_slice_ns.min(batch_cap_tau);
207    }
208    knobs.sojourn_thresh_ns = sojourn_tau;
209
210    knobs
211}
212
213// REGIME DETECTION (SCHMITT TRIGGER)
214// DIRECTION-AWARE: CURRENT REGIME DETERMINES WHICH THRESHOLDS APPLY.
215// DEAD ZONES PREVENT OSCILLATION THAT SINGLE-BOUNDARY DETECTION CAUSED.
216
217pub fn detect_regime(current: Regime, idle_pct: u64) -> Regime {
218    match current {
219        Regime::Light => {
220            if idle_pct < LIGHT_EXIT_PCT {
221                Regime::Mixed
222            } else {
223                Regime::Light
224            }
225        }
226        Regime::Mixed => {
227            if idle_pct > LIGHT_ENTER_PCT {
228                Regime::Light
229            } else if idle_pct < HEAVY_ENTER_PCT {
230                Regime::Heavy
231            } else {
232                Regime::Mixed
233            }
234        }
235        Regime::Heavy => {
236            if idle_pct > HEAVY_EXIT_PCT {
237                Regime::Mixed
238            } else {
239                Regime::Heavy
240            }
241        }
242    }
243}
244
245// STABILITY MODE
246
247pub const STABILITY_THRESHOLD: u32 = 10; // CONSECUTIVE STABLE TICKS BEFORE HIBERNATE
248
249pub fn compute_stability_score(
250    prev_score: u32,
251    regime_changed: bool,
252    reflex_events_delta: u64,
253    p99_ns: u64,
254    p99_ceiling_ns: u64,
255) -> u32 {
256    if regime_changed || reflex_events_delta > 0 || p99_ns > p99_ceiling_ns / 2 {
257        return 0;
258    }
259    (prev_score + 1).min(STABILITY_THRESHOLD)
260}
261
262// TELEMETRY GATING
263
264pub fn should_print_telemetry(tick_counter: u64, stability_score: u32) -> bool {
265    if stability_score >= STABILITY_THRESHOLD {
266        tick_counter % 2 == 0
267    } else {
268        true
269    }
270}
271
272// P99 HISTOGRAM
273
274pub const HIST_BUCKETS: usize = 12;
275pub const HIST_EDGES_NS: [u64; HIST_BUCKETS] = [
276    10_000,     // 10us
277    25_000,     // 25us
278    50_000,     // 50us
279    100_000,    // 100us
280    250_000,    // 250us
281    500_000,    // 500us
282    1_000_000,  // 1ms
283    2_000_000,  // 2ms
284    5_000_000,  // 5ms
285    10_000_000, // 10ms
286    20_000_000, // 20ms
287    u64::MAX,   // +inf
288];
289
290// COMPUTE P99 FROM DRAINED HISTOGRAM COUNTS. PURE FUNCTION.
291// CAP AT 20MS (LAST REAL BUCKET) -- +INF WOULD POISON EVERY COMPARISON.
292pub fn compute_p99_from_histogram(counts: &[u64; HIST_BUCKETS]) -> u64 {
293    let total: u64 = counts.iter().sum();
294    if total == 0 {
295        return 0;
296    }
297    let threshold = (total * 99 + 99) / 100;
298    let mut cumulative = 0u64;
299    for i in 0..HIST_BUCKETS {
300        cumulative += counts[i];
301        if cumulative >= threshold {
302            return HIST_EDGES_NS[i].min(HIST_EDGES_NS[HIST_BUCKETS - 2]);
303        }
304    }
305    HIST_EDGES_NS[HIST_BUCKETS - 2]
306}
307
308// MWU ORCHESTRATOR
309// SCHMITT-GATED MULTIPLICATIVE WEIGHT UPDATES ACROSS ALL 11 TUNING KNOBS.
310// 6 EXPERT PROFILES, EACH A SCALE FACTOR ON THE REGIME BASELINE.
311// CORRECTED SCALE FACTORS: sum(EQ[i] * SCALE[i]) = 1.0 FOR EACH CONTINUOUS KNOB.
312// DISCRETE KNOBS (LAG, AFFINITY, DEPTH) USE MAJORITY VOTE, NOT WEIGHTED AVERAGE.
313// 4 LOSS PATHWAYS: P99 SPIKE, RESCUE DELTA, IO DELTA, FORK STORM.
314// 1e-6 WEIGHT FLOOR PREVENTS UNDERFLOW (DEAD WEIGHTS CAN'T RECOVER).
315
316const N_EXPERTS: usize = 6;
317const ETA: f64 = 8.0;
318const RELAX_RATE: f64 = 0.80;
319const SPIKE_CONFIRM: u32 = 2;
320const RELAX_HOLD: u32 = 2;
321const RELAX_CEIL_PCT: f64 = 0.70;
322const EQUILIBRIUM: [f64; N_EXPERTS] = [0.08, 0.44, 0.12, 0.12, 0.12, 0.12];
323const WEIGHT_FLOOR: f64 = 1e-6;
324
325const EX_LATENCY: usize = 0;
326const EX_BALANCED: usize = 1;
327const EX_THROUGHPUT: usize = 2;
328const EX_IO_HEAVY: usize = 3;
329const EX_FORK_STORM: usize = 4;
330const EX_SATURATED: usize = 5;
331
332// CORRECTED CONTINUOUS SCALE FACTORS
333// PROPORTIONALLY ADJUSTED SO sum(EQ[i] * SCALE[i]) = 1.0 AT EQUILIBRIUM.
334// [LATENCY, BALANCED, THROUGHPUT, IO_HEAVY, FORK_STORM, SATURATED]
335const SC_SLICE: [f64; 6] = [0.74, 1.00, 1.23, 0.98, 0.49, 1.47];
336const SC_PREEMPT: [f64; 6] = [0.74, 1.00, 1.23, 0.98, 0.49, 1.47];
337const SC_BATCH: [f64; 6] = [0.78, 1.00, 1.30, 1.30, 0.52, 1.04];
338const SC_LCRI_HI: [f64; 6] = [0.74, 1.00, 1.23, 0.98, 0.98, 0.98];
339const SC_LCRI_LO: [f64; 6] = [0.70, 1.00, 1.40, 0.93, 0.93, 0.93];
340const SC_SOJOURN: [f64; 6] = [0.80, 1.00, 1.60, 0.93, 0.53, 1.07];
341const SC_BURST: [f64; 6] = [0.74, 1.00, 1.47, 0.98, 0.49, 1.23];
342
343// DISCRETE KNOB VALUES (ABSOLUTE, NOT SCALE FACTORS)
344const DV_LAG: [u64; 6] = [6, 4, 3, 4, 4, 3];
345const DV_AFFINITY: [u64; 6] = [
346    AFFINITY_STRONG,
347    AFFINITY_STRONG,
348    AFFINITY_WEAK,
349    AFFINITY_WEAK,
350    AFFINITY_OFF,
351    AFFINITY_WEAK,
352];
353fn blend_continuous(base: u64, scales: &[f64; 6], w: &[f64; N_EXPERTS]) -> u64 {
354    let v: f64 = (0..N_EXPERTS).map(|i| w[i] * base as f64 * scales[i]).sum();
355    (v.round() as u64).max(1)
356}
357
358fn majority_discrete(values: &[u64; 6], w: &[f64; N_EXPERTS]) -> u64 {
359    // GROUP BY VALUE, SUM WEIGHTS, PICK HIGHEST GROUP
360    let mut best_val = values[0];
361    let mut best_w = 0.0f64;
362    for &v in values.iter() {
363        let total: f64 = (0..N_EXPERTS)
364            .filter(|&i| values[i] == v)
365            .map(|i| w[i])
366            .sum();
367        if total > best_w {
368            best_w = total;
369            best_val = v;
370        }
371    }
372    best_val
373}
374
375#[derive(Clone, Copy, PartialEq, Eq, Debug)]
376pub enum IoBucket {
377    Low,
378    Mid,
379    High,
380}
381
382pub fn io_bucket(io_pct: u64) -> IoBucket {
383    if io_pct > 60 {
384        IoBucket::High
385    } else if io_pct < 15 {
386        IoBucket::Low
387    } else {
388        IoBucket::Mid
389    }
390}
391
392pub struct MwuSignals {
393    pub p99_ns: u64,
394    pub interactive_p99_ns: u64,
395    pub io_pct: u64,
396    pub rescue_count: u64,
397    pub wakeup_rate: u64,
398}
399
400// SNAPSHOT OF THE BPF DAMPED-HARMONIC OSCILLATOR'S ADAPTIVE STATE.
401// MWU READS THIS BEFORE COMPUTING PATHWAY LOSSES SO IT CAN AVOID
402// DOUBLE-CORRECTING ON RESCUE PRESSURE: WHEN THE OSCILLATOR HAS
403// ALREADY TIGHTENED codel_target_ns TOWARD THE FLOOR, BPF HAS
404// RESPONDED -- MWU STAYS OUT. WHEN THE OSCILLATOR IS NEAR THE
405// CEILING, NO RESCUE PRESSURE EXISTS FOR MWU TO AMPLIFY.
406//
407// ALL FIELDS ZERO = SENTINEL ("READBACK UNAVAILABLE / INIT") -> NO GATING.
408#[derive(Clone, Copy, Debug, Default)]
409pub struct OscillatorState {
410    pub codel_target_ns: u64,
411    pub codel_target_floor_ns: u64,
412    pub codel_target_max_ns: u64,
413}
414
415impl OscillatorState {
416    // 0.0 = AT FLOOR (TIGHTENED), 1.0 = AT MAX (RELAXED).
417    // SENTINEL OR DEGENERATE RANGE -> 0.5 (CENTER, NEUTRAL).
418    pub fn position(&self) -> f64 {
419        if self.codel_target_max_ns == 0 || self.codel_target_floor_ns >= self.codel_target_max_ns {
420            return 0.5;
421        }
422        let range = (self.codel_target_max_ns - self.codel_target_floor_ns) as f64;
423        let pos = self
424            .codel_target_ns
425            .saturating_sub(self.codel_target_floor_ns) as f64;
426        (pos / range).clamp(0.0, 1.0)
427    }
428}
429
430pub struct MwuController {
431    weights: [f64; N_EXPERTS],
432    baseline: TuningKnobs,
433    spike_streak: u32,
434    healthy_streak: u32,
435    fork_streak: u32,
436    prev_io_bucket: IoBucket,
437    prev_rescuing: bool,
438    losses_applied: bool,
439}
440
441impl MwuController {
442    pub fn new(baseline: TuningKnobs) -> Self {
443        Self {
444            weights: EQUILIBRIUM,
445            baseline,
446            spike_streak: 0,
447            healthy_streak: 0,
448            fork_streak: 0,
449            prev_io_bucket: IoBucket::Mid,
450            prev_rescuing: false,
451            losses_applied: false,
452        }
453    }
454
455    pub fn reset(&mut self) {
456        self.weights = EQUILIBRIUM;
457        self.spike_streak = 0;
458        self.healthy_streak = 0;
459        self.fork_streak = 0;
460        self.prev_io_bucket = IoBucket::Mid;
461        self.prev_rescuing = false;
462        self.losses_applied = false;
463    }
464
465    pub fn set_baseline(&mut self, baseline: TuningKnobs) {
466        self.baseline = baseline;
467    }
468
469    pub fn update(
470        &mut self,
471        sig: &MwuSignals,
472        ceiling: u64,
473        _nr_cpus: u64,
474        tau_ns: u64,
475        osc: &OscillatorState,
476    ) -> TuningKnobs {
477        let worst = sig.p99_ns.max(sig.interactive_p99_ns);
478        let above = worst > ceiling;
479        let below_relax = (worst as f64) < (ceiling as f64 * RELAX_CEIL_PCT);
480
481        // OSCILLATOR-AWARE GATING. THE BPF DAMPED OSCILLATOR ALREADY
482        // CONSUMES global_rescue_count AND ADAPTS codel_target_ns ON
483        // EVERY TICK. PATHWAYS THAT TRIGGER ON THE SAME RESCUE SIGNAL
484        // (PATHWAY 2 RESCUE-DELTA, PATHWAY 4 FORK-STORM) SHOULD DEFER
485        // TO IT WHEN THE OSCILLATOR HAS ALREADY MOVED -- OTHERWISE BOTH
486        // CONTROLLERS PUSH IN THE SAME DIRECTION AND OVERSHOOT.
487        //
488        // POSITION 0.0 = TIGHTENED (FLOOR), 1.0 = RELAXED (MAX).
489        // < 0.40 -> BPF HAS RESPONDED HEAVILY; SKIP RESCUE-DRIVEN LOSSES.
490        // > 0.90 -> BPF SAYS QUIET; RESCUE BURSTS ARE STALE NOISE; SKIP.
491        let osc_pos = osc.position();
492        let osc_already_tight = osc_pos < 0.40;
493        let osc_already_loose = osc_pos > 0.90;
494        let defer_to_oscillator = osc_already_tight || osc_already_loose;
495
496        let mut losses = [0.0f64; N_EXPERTS];
497        let mut has_loss = false;
498
499        // PATHWAY 1: P99 SPIKE (SCHMITT-GATED)
500        if above {
501            self.healthy_streak = 0;
502            self.spike_streak += 1;
503            if self.spike_streak >= SPIKE_CONFIRM {
504                let v = ((worst - ceiling) as f64 / ceiling as f64).min(3.0);
505                losses[EX_BALANCED] += v * 0.5;
506                losses[EX_THROUGHPUT] += v * 1.0;
507                losses[EX_IO_HEAVY] += v * 0.6;
508                losses[EX_FORK_STORM] += v * 0.3;
509                losses[EX_SATURATED] += v * 0.9;
510                has_loss = true;
511            }
512        } else {
513            self.spike_streak = 0;
514        }
515
516        // PATHWAY 2: RESCUE DELTA (0 -> NONZERO TRANSITION)
517        // PATHWAY 2: RESCUE DELTA (0 -> NONZERO TRANSITION)
518        // PENALIZE ALL EXPERTS EQUALLY: THE DAMPED OSCILLATION IN BPF
519        // HANDLES TIGHTENING VIA THE CODEL TARGET. MWU SHOULD HOLD STEADY,
520        // NOT COMPOUND BY ALSO TIGHTENING SLICES VIA LATENCY EXPERT.
521        let rescuing = sig.rescue_count > 0;
522        if rescuing && !self.prev_rescuing && !defer_to_oscillator {
523            let v = (sig.rescue_count as f64 * 1.5).min(3.0);
524            losses[EX_LATENCY] += v * 0.4;
525            losses[EX_THROUGHPUT] += v * 0.6;
526            losses[EX_SATURATED] += v * 0.6;
527            losses[EX_IO_HEAVY] += v * 0.4;
528            losses[EX_BALANCED] += v * 0.2;
529            has_loss = true;
530        }
531        self.prev_rescuing = rescuing;
532
533        // PATHWAY 3: IO DELTA (BUCKET TRANSITION)
534        let cur_io = io_bucket(sig.io_pct);
535        if cur_io != self.prev_io_bucket {
536            match cur_io {
537                IoBucket::High => {
538                    let v = ((sig.io_pct as f64 - 60.0) / 40.0).min(1.0);
539                    for i in 0..N_EXPERTS {
540                        if i != EX_IO_HEAVY {
541                            losses[i] += v * 0.8;
542                        }
543                    }
544                    has_loss = true;
545                }
546                IoBucket::Low => {
547                    let v = ((15.0 - sig.io_pct as f64) / 15.0).clamp(0.0, 1.0);
548                    losses[EX_IO_HEAVY] += v * 1.0;
549                    has_loss = true;
550                }
551                IoBucket::Mid => {}
552            }
553        }
554        self.prev_io_bucket = cur_io;
555
556        // PATHWAY 4: FORK STORM (SCHMITT-GATED, PRESSURE-CONFIRMED).
557        // GATE THRESHOLD IS TAU-DERIVED. sig.wakeup_rate IS THE RAW TOTAL
558        // WAKES/SEC; scale_tau_u64(tau, K_FORK_STORM_RATE_Q16) PRODUCES
559        // THE COMPARISON THRESHOLD. AT THE 12C REFERENCE (tau=40MS) THE
560        // THRESHOLD IS ~8000/SEC; AT 4C IT TIGHTENS TO ~1200/SEC. A FORK
561        // STORM ALSO REQUIRES ACTIVE RESCUES (rescue_count > 0) AS GROUND-
562        // TRUTH PRESSURE.
563        //
564        // FORK_STORM EXPERT (SC_SLICE=0.49, SC_PREEMPT=0.49, SC_BATCH=0.52,
565        // SC_SOJOURN=0.53, SC_BURST=0.49) DOMINATES THE BLEND DURING A REAL
566        // STORM, DRIVING burst_slice_ns / preempt_thresh_ns / sojourn_thresh_ns
567        // / batch_slice_ns DOWN END-TO-END.
568        let fork_thresh = scale_tau_u64(tau_ns, K_FORK_STORM_RATE_Q16).max(FORK_STORM_RATE_FLOOR);
569        let fork_storm = sig.wakeup_rate > fork_thresh && sig.rescue_count > 0;
570        if fork_storm {
571            self.fork_streak += 1;
572            if self.fork_streak >= SPIKE_CONFIRM && !defer_to_oscillator {
573                let denom = fork_thresh.max(1) as f64;
574                let v = ((sig.wakeup_rate as f64 / denom) - 1.0).clamp(0.0, 3.0);
575                losses[EX_BALANCED] += v * 0.30;
576                losses[EX_THROUGHPUT] += v * 1.00;
577                losses[EX_IO_HEAVY] += v * 0.50;
578                losses[EX_SATURATED] += v * 0.80;
579                has_loss = true;
580            }
581        } else {
582            self.fork_streak = 0;
583        }
584
585        // APPLY LOSSES WITH WEIGHT FLOOR
586        if has_loss {
587            for i in 0..N_EXPERTS {
588                if losses[i] > 0.0 {
589                    self.weights[i] *= (-ETA * losses[i]).exp();
590                }
591                if self.weights[i] < WEIGHT_FLOOR {
592                    self.weights[i] = WEIGHT_FLOOR;
593                }
594            }
595            let sum: f64 = self.weights.iter().sum();
596            for w in self.weights.iter_mut() {
597                *w /= sum;
598            }
599        }
600
601        // RELAXATION
602        if !has_loss && below_relax {
603            self.healthy_streak += 1;
604            if self.healthy_streak >= RELAX_HOLD {
605                for i in 0..N_EXPERTS {
606                    self.weights[i] =
607                        (1.0 - RELAX_RATE) * self.weights[i] + RELAX_RATE * EQUILIBRIUM[i];
608                }
609            }
610        } else if !has_loss {
611            self.healthy_streak = 0;
612        }
613
614        self.losses_applied = has_loss;
615
616        // BLEND: CONTINUOUS KNOBS VIA CORRECTED SCALE FACTORS, DISCRETE VIA MAJORITY
617        let b = &self.baseline;
618        let blended_slice = blend_continuous(b.slice_ns, &SC_SLICE, &self.weights);
619        let blended_burst = blend_continuous(b.burst_slice_ns, &SC_BURST, &self.weights);
620        let mut blended_sojourn = blend_continuous(b.sojourn_thresh_ns, &SC_SOJOURN, &self.weights);
621
622        // SOJOURN FLOOR: dispatch waterfall services aged overflow at
623        // overflow_sojourn_rescue_ns (BPF-side, tau-clamped to [4ms, 10ms]).
624        // tick() also kicks per-CPU DSQs whose oldest task has aged past
625        // sojourn_thresh_ns. If MWU drives sojourn_thresh_ns below the
626        // dispatch service window + one slice, every tick generates kicks
627        // on per-CPU DSQs that the dispatcher will service on the next
628        // dispatch anyway -- a kick storm. Floor against the worst-case
629        // dispatch service window to prevent it.
630        let sojourn_floor = 4_000_000u64.saturating_add(blended_slice);
631        if blended_sojourn < sojourn_floor {
632            blended_sojourn = sojourn_floor;
633        }
634
635        TuningKnobs {
636            slice_ns: blended_slice,
637            preempt_thresh_ns: blend_continuous(b.preempt_thresh_ns, &SC_PREEMPT, &self.weights),
638            lag_scale: majority_discrete(&DV_LAG, &self.weights),
639            batch_slice_ns: blend_continuous(b.batch_slice_ns, &SC_BATCH, &self.weights),
640            lat_cri_thresh_high: blend_continuous(
641                b.lat_cri_thresh_high,
642                &SC_LCRI_HI,
643                &self.weights,
644            ),
645            lat_cri_thresh_low: blend_continuous(b.lat_cri_thresh_low, &SC_LCRI_LO, &self.weights),
646            affinity_mode: majority_discrete(&DV_AFFINITY, &self.weights),
647            sojourn_thresh_ns: blended_sojourn,
648            burst_slice_ns: blended_burst,
649            // topology_tau_ns AND codel_eq_ns ARE OWNED BY THE TOPOLOGY LAYER;
650            // MWU DOESN'T TOUCH THEM. THE MONITOR LOOP OVERLAYS THE LIVE BPF
651            // VALUES BACK ONTO MWU'S OUTPUT BEFORE WRITING -- PASSTHROUGH.
652            topology_tau_ns: 0,
653            codel_eq_ns: 0,
654        }
655    }
656
657    pub fn had_losses(&self) -> bool {
658        self.losses_applied
659    }
660
661    pub fn scale(&self) -> f64 {
662        let s: f64 = (0..N_EXPERTS).map(|i| self.weights[i] * SC_SLICE[i]).sum();
663        s
664    }
665}