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// CPU-BOUND DEMOTION THRESHOLDS
41// PER-REGIME: LENIENT IN LIGHT, AGGRESSIVE IN HEAVY
42
43pub const LIGHT_DEMOTION_NS: u64 = 3_500_000; // 3.5MS: LENIENT, FEW CONTEND
44pub const MIXED_DEMOTION_NS: u64 = 2_500_000; // 2.5MS: CURRENT CPU_BOUND_THRESH_NS
45pub const HEAVY_DEMOTION_NS: u64 = 2_000_000; // 2.0MS: AGGRESSIVE
46
47// CLASSIFIER THRESHOLDS
48// LAT_CRI SCORE BOUNDARIES FOR TIER CLASSIFICATION
49// EXPOSED AS TUNING KNOBS FOR RUNTIME ADJUSTMENT
50
51pub const DEFAULT_LAT_CRI_THRESH_HIGH: u64 = 32; // >= THIS: LAT_CRITICAL
52pub const DEFAULT_LAT_CRI_THRESH_LOW: u64 = 8; // >= THIS: INTERACTIVE, BELOW: BATCH
53
54// TUNING KNOBS
55// MATCHES struct tuning_knobs IN BPF (intf.h)
56
57// AFFINITY MODE: L2 PLACEMENT STRENGTH
58pub const AFFINITY_OFF: u64 = 0;
59pub const AFFINITY_WEAK: u64 = 1;
60pub const AFFINITY_STRONG: u64 = 2;
61
62#[repr(C)]
63#[derive(Clone, Copy)]
64pub struct TuningKnobs {
65    pub slice_ns: u64,
66    pub preempt_thresh_ns: u64,
67    pub lag_scale: u64,
68    pub batch_slice_ns: u64,
69    pub cpu_bound_thresh_ns: u64,
70    pub lat_cri_thresh_high: u64,
71    pub lat_cri_thresh_low: u64,
72    pub affinity_mode: u64,
73    pub sojourn_thresh_ns: u64,
74    pub burst_slice_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            cpu_bound_thresh_ns: MIXED_DEMOTION_NS,
85            lat_cri_thresh_high: DEFAULT_LAT_CRI_THRESH_HIGH,
86            lat_cri_thresh_low: DEFAULT_LAT_CRI_THRESH_LOW,
87            affinity_mode: AFFINITY_OFF,
88            sojourn_thresh_ns: 5_000_000,
89            burst_slice_ns: 1_000_000,
90        }
91    }
92}
93
94// REGIME
95
96#[repr(u8)]
97#[derive(Clone, Copy, PartialEq, Eq, Debug)]
98pub enum Regime {
99    Light = 0,
100    Mixed = 1,
101    Heavy = 2,
102}
103
104impl Regime {
105    pub fn label(self) -> &'static str {
106        match self {
107            Self::Light => "LIGHT",
108            Self::Mixed => "MIXED",
109            Self::Heavy => "HEAVY",
110        }
111    }
112
113    pub fn p99_ceiling(self) -> u64 {
114        match self {
115            Self::Light => LIGHT_P99_CEIL_NS,
116            Self::Mixed => MIXED_P99_CEIL_NS,
117            Self::Heavy => HEAVY_P99_CEIL_NS,
118        }
119    }
120}
121
122// REGIME KNOBS
123
124pub fn regime_knobs(r: Regime) -> TuningKnobs {
125    match r {
126        Regime::Light => TuningKnobs {
127            slice_ns: LIGHT_SLICE_NS,
128            preempt_thresh_ns: LIGHT_PREEMPT_NS,
129            lag_scale: LIGHT_LAG_SCALE,
130            batch_slice_ns: LIGHT_BATCH_NS,
131            cpu_bound_thresh_ns: LIGHT_DEMOTION_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        },
138        Regime::Mixed => TuningKnobs {
139            slice_ns: MIXED_SLICE_NS,
140            preempt_thresh_ns: MIXED_PREEMPT_NS,
141            lag_scale: MIXED_LAG_SCALE,
142            batch_slice_ns: MIXED_BATCH_NS,
143            cpu_bound_thresh_ns: MIXED_DEMOTION_NS,
144            lat_cri_thresh_high: DEFAULT_LAT_CRI_THRESH_HIGH,
145            lat_cri_thresh_low: DEFAULT_LAT_CRI_THRESH_LOW,
146            affinity_mode: AFFINITY_STRONG,
147            sojourn_thresh_ns: 5_000_000,
148            burst_slice_ns: 1_000_000,
149        },
150        Regime::Heavy => TuningKnobs {
151            slice_ns: HEAVY_SLICE_NS,
152            preempt_thresh_ns: HEAVY_PREEMPT_NS,
153            lag_scale: HEAVY_LAG_SCALE,
154            batch_slice_ns: HEAVY_BATCH_NS,
155            cpu_bound_thresh_ns: HEAVY_DEMOTION_NS,
156            lat_cri_thresh_high: DEFAULT_LAT_CRI_THRESH_HIGH,
157            lat_cri_thresh_low: DEFAULT_LAT_CRI_THRESH_LOW,
158            affinity_mode: AFFINITY_WEAK,
159            sojourn_thresh_ns: 5_000_000,
160            burst_slice_ns: 1_000_000,
161        },
162    }
163}
164
165// CORE-COUNT-AWARE REGIME KNOBS
166// AT LOW CORE COUNTS, TIME SLICES MUST BE SHORTER TO MAINTAIN ADEQUATE
167// DISPATCH FREQUENCY. HEAVY AT 2C WITH 4MS SLICES = 250 DISPATCHES/S/CORE,
168// TOO COARSE FOR DEADLINE AND IPC WORKLOADS.
169// SCALE: slice = min(BASE, nr_cpus * 500US), preempt = min(BASE, nr_cpus * 250US).
170// MIXED ALSO SCALES: BATCH_SLICE CAPS AT nr_cpus * 5MS (2C: 10MS VS 20MS BASE).
171
172pub fn scaled_regime_knobs(r: Regime, nr_cpus: u64) -> TuningKnobs {
173    let mut knobs = regime_knobs(r);
174    match r {
175        Regime::Heavy | Regime::Light => {
176            let slice_cap = nr_cpus * 500_000;
177            let preempt_cap = (nr_cpus * 250_000).max(1_000_000);
178            knobs.slice_ns = knobs.slice_ns.min(slice_cap);
179            knobs.preempt_thresh_ns = knobs.preempt_thresh_ns.min(preempt_cap);
180        }
181        Regime::Mixed => {
182            let slice_cap = nr_cpus * 500_000;
183            let preempt_cap = nr_cpus * 500_000;
184            knobs.slice_ns = knobs.slice_ns.min(slice_cap);
185            knobs.preempt_thresh_ns = knobs.preempt_thresh_ns.min(preempt_cap);
186            let batch_cap = nr_cpus * 5_000_000;
187            knobs.batch_slice_ns = knobs.batch_slice_ns.min(batch_cap);
188        }
189    }
190    // SOJOURN FLOOR: CPU-SCALED, SAME FORMULA THE OLD EWMA USED AS ITS FLOOR
191    knobs.sojourn_thresh_ns = (nr_cpus * 1_000_000).clamp(2_000_000, 6_000_000);
192    knobs
193}
194
195// REGIME DETECTION (SCHMITT TRIGGER)
196// DIRECTION-AWARE: CURRENT REGIME DETERMINES WHICH THRESHOLDS APPLY.
197// DEAD ZONES PREVENT OSCILLATION THAT SINGLE-BOUNDARY DETECTION CAUSED.
198
199pub fn detect_regime(current: Regime, idle_pct: u64) -> Regime {
200    match current {
201        Regime::Light => {
202            if idle_pct < LIGHT_EXIT_PCT {
203                Regime::Mixed
204            } else {
205                Regime::Light
206            }
207        }
208        Regime::Mixed => {
209            if idle_pct > LIGHT_ENTER_PCT {
210                Regime::Light
211            } else if idle_pct < HEAVY_ENTER_PCT {
212                Regime::Heavy
213            } else {
214                Regime::Mixed
215            }
216        }
217        Regime::Heavy => {
218            if idle_pct > HEAVY_EXIT_PCT {
219                Regime::Mixed
220            } else {
221                Regime::Heavy
222            }
223        }
224    }
225}
226
227// STABILITY MODE
228
229pub const STABILITY_THRESHOLD: u32 = 10; // CONSECUTIVE STABLE TICKS BEFORE HIBERNATE
230
231pub fn compute_stability_score(
232    prev_score: u32,
233    regime_changed: bool,
234    reflex_events_delta: u64,
235    p99_ns: u64,
236    p99_ceiling_ns: u64,
237) -> u32 {
238    if regime_changed || reflex_events_delta > 0 || p99_ns > p99_ceiling_ns / 2 {
239        return 0;
240    }
241    (prev_score + 1).min(STABILITY_THRESHOLD)
242}
243
244// TELEMETRY GATING
245
246pub fn should_print_telemetry(tick_counter: u64, stability_score: u32) -> bool {
247    if stability_score >= STABILITY_THRESHOLD {
248        tick_counter % 2 == 0
249    } else {
250        true
251    }
252}
253
254// P99 HISTOGRAM
255
256pub const HIST_BUCKETS: usize = 12;
257pub const HIST_EDGES_NS: [u64; HIST_BUCKETS] = [
258    10_000,     // 10us
259    25_000,     // 25us
260    50_000,     // 50us
261    100_000,    // 100us
262    250_000,    // 250us
263    500_000,    // 500us
264    1_000_000,  // 1ms
265    2_000_000,  // 2ms
266    5_000_000,  // 5ms
267    10_000_000, // 10ms
268    20_000_000, // 20ms
269    u64::MAX,   // +inf
270];
271
272// COMPUTE P99 FROM DRAINED HISTOGRAM COUNTS. PURE FUNCTION.
273// CAP AT 20MS (LAST REAL BUCKET) -- +INF WOULD POISON EVERY COMPARISON.
274pub fn compute_p99_from_histogram(counts: &[u64; HIST_BUCKETS]) -> u64 {
275    let total: u64 = counts.iter().sum();
276    if total == 0 {
277        return 0;
278    }
279    let threshold = (total * 99 + 99) / 100;
280    let mut cumulative = 0u64;
281    for i in 0..HIST_BUCKETS {
282        cumulative += counts[i];
283        if cumulative >= threshold {
284            return HIST_EDGES_NS[i].min(HIST_EDGES_NS[HIST_BUCKETS - 2]);
285        }
286    }
287    HIST_EDGES_NS[HIST_BUCKETS - 2]
288}
289
290// REFLEX TIGHTEN DECISION: USES BOTH AGGREGATE AND INTERACTIVE P99.
291// TIGHTEN IF EITHER EXCEEDS CEILING (INTERACTIVE STARVATION HIDDEN IN AGGREGATE).
292#[allow(dead_code)]
293pub fn should_reflex_tighten(aggregate_p99: u64, interactive_p99: u64, ceiling: u64) -> bool {
294    aggregate_p99 > ceiling || interactive_p99 > ceiling
295}
296
297// SLEEP-INFORMED BATCH TUNING
298// IO-HEAVY: EXTEND BATCH SLICES (+25%) -- IO-BOUND TASKS BATCH BETWEEN FREQUENT SHORT SLEEPS
299// IDLE-HEAVY: TIGHTEN BATCH SLICES (-25%) -- SPORADIC USER INPUT NEEDS FASTER PREEMPTION
300
301#[allow(dead_code)]
302pub const BATCH_MAX_NS: u64 = 25_000_000; // 25MS CEILING
303
304#[allow(dead_code)]
305pub fn sleep_adjust_batch_ns(base_batch_ns: u64, io_pct: u64) -> u64 {
306    if io_pct > 60 {
307        // IO-HEAVY: EXTEND BATCH SLICES (+25%)
308        (base_batch_ns * 5 / 4).min(BATCH_MAX_NS)
309    } else if io_pct < 15 {
310        // IDLE-HEAVY: TIGHTEN BATCH SLICES (-25%)
311        (base_batch_ns * 3 / 4).max(base_batch_ns / 2)
312    } else {
313        base_batch_ns
314    }
315}
316
317// MWU ORCHESTRATOR
318// SCHMITT-GATED MULTIPLICATIVE WEIGHT UPDATES ACROSS ALL 11 TUNING KNOBS.
319// 6 EXPERT PROFILES, EACH A SCALE FACTOR ON THE REGIME BASELINE.
320// CORRECTED SCALE FACTORS: sum(EQ[i] * SCALE[i]) = 1.0 FOR EACH CONTINUOUS KNOB.
321// DISCRETE KNOBS (LAG, AFFINITY, DEPTH) USE MAJORITY VOTE, NOT WEIGHTED AVERAGE.
322// 4 LOSS PATHWAYS: P99 SPIKE, RESCUE DELTA, IO DELTA, FORK STORM.
323// 1e-6 WEIGHT FLOOR PREVENTS UNDERFLOW (DEAD WEIGHTS CAN'T RECOVER).
324
325const N_EXPERTS: usize = 6;
326const ETA: f64 = 8.0;
327const RELAX_RATE: f64 = 0.80;
328const SPIKE_CONFIRM: u32 = 2;
329const RELAX_HOLD: u32 = 2;
330const RELAX_CEIL_PCT: f64 = 0.70;
331const EQUILIBRIUM: [f64; N_EXPERTS] = [0.08, 0.44, 0.12, 0.12, 0.12, 0.12];
332const WEIGHT_FLOOR: f64 = 1e-6;
333
334const EX_LATENCY: usize = 0;
335const EX_BALANCED: usize = 1;
336const EX_THROUGHPUT: usize = 2;
337const EX_IO_HEAVY: usize = 3;
338const EX_FORK_STORM: usize = 4;
339const EX_SATURATED: usize = 5;
340
341// CORRECTED CONTINUOUS SCALE FACTORS
342// PROPORTIONALLY ADJUSTED SO sum(EQ[i] * SCALE[i]) = 1.0 AT EQUILIBRIUM.
343// [LATENCY, BALANCED, THROUGHPUT, IO_HEAVY, FORK_STORM, SATURATED]
344const SC_SLICE: [f64; 6] = [0.74, 1.00, 1.23, 0.98, 0.49, 1.47];
345const SC_PREEMPT: [f64; 6] = [0.74, 1.00, 1.23, 0.98, 0.49, 1.47];
346const SC_BATCH: [f64; 6] = [0.78, 1.00, 1.30, 1.30, 0.52, 1.04];
347const SC_DEMOTE: [f64; 6] = [0.86, 1.00, 1.29, 1.08, 0.86, 0.86];
348const SC_LCRI_HI: [f64; 6] = [0.74, 1.00, 1.23, 0.98, 0.98, 0.98];
349const SC_LCRI_LO: [f64; 6] = [0.70, 1.00, 1.40, 0.93, 0.93, 0.93];
350const SC_SOJOURN: [f64; 6] = [0.80, 1.00, 1.60, 0.93, 0.53, 1.07];
351const SC_BURST: [f64; 6] = [0.74, 1.00, 1.47, 0.98, 0.49, 1.23];
352
353// DISCRETE KNOB VALUES (ABSOLUTE, NOT SCALE FACTORS)
354const DV_LAG: [u64; 6] = [6, 4, 3, 4, 4, 3];
355const DV_AFFINITY: [u64; 6] = [
356    AFFINITY_STRONG,
357    AFFINITY_STRONG,
358    AFFINITY_WEAK,
359    AFFINITY_WEAK,
360    AFFINITY_OFF,
361    AFFINITY_WEAK,
362];
363fn blend_continuous(base: u64, scales: &[f64; 6], w: &[f64; N_EXPERTS]) -> u64 {
364    let v: f64 = (0..N_EXPERTS).map(|i| w[i] * base as f64 * scales[i]).sum();
365    (v.round() as u64).max(1)
366}
367
368fn majority_discrete(values: &[u64; 6], w: &[f64; N_EXPERTS]) -> u64 {
369    // GROUP BY VALUE, SUM WEIGHTS, PICK HIGHEST GROUP
370    let mut best_val = values[0];
371    let mut best_w = 0.0f64;
372    for &v in values.iter() {
373        let total: f64 = (0..N_EXPERTS)
374            .filter(|&i| values[i] == v)
375            .map(|i| w[i])
376            .sum();
377        if total > best_w {
378            best_w = total;
379            best_val = v;
380        }
381    }
382    best_val
383}
384
385#[derive(Clone, Copy, PartialEq, Eq, Debug)]
386pub enum IoBucket {
387    Low,
388    Mid,
389    High,
390}
391
392pub fn io_bucket(io_pct: u64) -> IoBucket {
393    if io_pct > 60 {
394        IoBucket::High
395    } else if io_pct < 15 {
396        IoBucket::Low
397    } else {
398        IoBucket::Mid
399    }
400}
401
402pub struct MwuSignals {
403    pub p99_ns: u64,
404    pub interactive_p99_ns: u64,
405    pub io_pct: u64,
406    pub rescue_count: u64,
407    pub wakeup_rate: u64,
408}
409
410pub struct MwuController {
411    weights: [f64; N_EXPERTS],
412    baseline: TuningKnobs,
413    spike_streak: u32,
414    healthy_streak: u32,
415    fork_streak: u32,
416    prev_io_bucket: IoBucket,
417    prev_rescuing: bool,
418    losses_applied: bool,
419}
420
421impl MwuController {
422    pub fn new(baseline: TuningKnobs) -> Self {
423        Self {
424            weights: EQUILIBRIUM,
425            baseline,
426            spike_streak: 0,
427            healthy_streak: 0,
428            fork_streak: 0,
429            prev_io_bucket: IoBucket::Mid,
430            prev_rescuing: false,
431            losses_applied: false,
432        }
433    }
434
435    pub fn reset(&mut self) {
436        self.weights = EQUILIBRIUM;
437        self.spike_streak = 0;
438        self.healthy_streak = 0;
439        self.fork_streak = 0;
440        self.prev_io_bucket = IoBucket::Mid;
441        self.prev_rescuing = false;
442        self.losses_applied = false;
443    }
444
445    pub fn set_baseline(&mut self, baseline: TuningKnobs) {
446        self.baseline = baseline;
447    }
448
449    pub fn update(&mut self, sig: &MwuSignals, ceiling: u64, nr_cpus: u64) -> TuningKnobs {
450        let worst = sig.p99_ns.max(sig.interactive_p99_ns);
451        let above = worst > ceiling;
452        let below_relax = (worst as f64) < (ceiling as f64 * RELAX_CEIL_PCT);
453
454        let mut losses = [0.0f64; N_EXPERTS];
455        let mut has_loss = false;
456
457        // PATHWAY 1: P99 SPIKE (SCHMITT-GATED)
458        if above {
459            self.healthy_streak = 0;
460            self.spike_streak += 1;
461            if self.spike_streak >= SPIKE_CONFIRM {
462                let v = ((worst - ceiling) as f64 / ceiling as f64).min(3.0);
463                losses[EX_BALANCED] += v * 0.5;
464                losses[EX_THROUGHPUT] += v * 1.0;
465                losses[EX_IO_HEAVY] += v * 0.6;
466                losses[EX_FORK_STORM] += v * 0.3;
467                losses[EX_SATURATED] += v * 0.9;
468                has_loss = true;
469            }
470        } else {
471            self.spike_streak = 0;
472        }
473
474        // PATHWAY 2: RESCUE DELTA (0 -> NONZERO TRANSITION)
475        // PATHWAY 2: RESCUE DELTA (0 -> NONZERO TRANSITION)
476        // PENALIZE ALL EXPERTS EQUALLY: THE DAMPED OSCILLATION IN BPF
477        // HANDLES TIGHTENING VIA THE CODEL TARGET. MWU SHOULD HOLD STEADY,
478        // NOT COMPOUND BY ALSO TIGHTENING SLICES VIA LATENCY EXPERT.
479        let rescuing = sig.rescue_count > 0;
480        if rescuing && !self.prev_rescuing {
481            let v = (sig.rescue_count as f64 * 1.5).min(3.0);
482            losses[EX_LATENCY] += v * 0.4;
483            losses[EX_THROUGHPUT] += v * 0.6;
484            losses[EX_SATURATED] += v * 0.6;
485            losses[EX_IO_HEAVY] += v * 0.4;
486            losses[EX_BALANCED] += v * 0.2;
487            has_loss = true;
488        }
489        self.prev_rescuing = rescuing;
490
491        // PATHWAY 3: IO DELTA (BUCKET TRANSITION)
492        let cur_io = io_bucket(sig.io_pct);
493        if cur_io != self.prev_io_bucket {
494            match cur_io {
495                IoBucket::High => {
496                    let v = ((sig.io_pct as f64 - 60.0) / 40.0).min(1.0);
497                    for i in 0..N_EXPERTS {
498                        if i != EX_IO_HEAVY {
499                            losses[i] += v * 0.8;
500                        }
501                    }
502                    has_loss = true;
503                }
504                IoBucket::Low => {
505                    let v = ((15.0 - sig.io_pct as f64) / 15.0).clamp(0.0, 1.0);
506                    losses[EX_IO_HEAVY] += v * 1.0;
507                    has_loss = true;
508                }
509                IoBucket::Mid => {}
510            }
511        }
512        self.prev_io_bucket = cur_io;
513
514        // PATHWAY 4: FORK STORM (SCHMITT-GATED)
515        let fork_storm = sig.wakeup_rate > nr_cpus * 2;
516        if fork_storm {
517            self.fork_streak += 1;
518            if self.fork_streak >= SPIKE_CONFIRM {
519                losses[EX_LATENCY] += 0.05;
520                losses[EX_BALANCED] += 0.15;
521                losses[EX_THROUGHPUT] += 0.30;
522                losses[EX_IO_HEAVY] += 0.25;
523                losses[EX_SATURATED] += 0.20;
524                has_loss = true;
525            }
526        } else {
527            self.fork_streak = 0;
528        }
529
530        // APPLY LOSSES WITH WEIGHT FLOOR
531        if has_loss {
532            for i in 0..N_EXPERTS {
533                if losses[i] > 0.0 {
534                    self.weights[i] *= (-ETA * losses[i]).exp();
535                }
536                if self.weights[i] < WEIGHT_FLOOR {
537                    self.weights[i] = WEIGHT_FLOOR;
538                }
539            }
540            let sum: f64 = self.weights.iter().sum();
541            for w in self.weights.iter_mut() {
542                *w /= sum;
543            }
544        }
545
546        // RELAXATION
547        if !has_loss && below_relax {
548            self.healthy_streak += 1;
549            if self.healthy_streak >= RELAX_HOLD {
550                for i in 0..N_EXPERTS {
551                    self.weights[i] =
552                        (1.0 - RELAX_RATE) * self.weights[i] + RELAX_RATE * EQUILIBRIUM[i];
553                }
554            }
555        } else if !has_loss {
556            self.healthy_streak = 0;
557        }
558
559        self.losses_applied = has_loss;
560
561        // BLEND: CONTINUOUS KNOBS VIA CORRECTED SCALE FACTORS, DISCRETE VIA MAJORITY
562        let b = &self.baseline;
563        TuningKnobs {
564            slice_ns: blend_continuous(b.slice_ns, &SC_SLICE, &self.weights),
565            preempt_thresh_ns: blend_continuous(b.preempt_thresh_ns, &SC_PREEMPT, &self.weights),
566            lag_scale: majority_discrete(&DV_LAG, &self.weights),
567            batch_slice_ns: blend_continuous(b.batch_slice_ns, &SC_BATCH, &self.weights),
568            cpu_bound_thresh_ns: blend_continuous(b.cpu_bound_thresh_ns, &SC_DEMOTE, &self.weights),
569            lat_cri_thresh_high: blend_continuous(
570                b.lat_cri_thresh_high,
571                &SC_LCRI_HI,
572                &self.weights,
573            ),
574            lat_cri_thresh_low: blend_continuous(b.lat_cri_thresh_low, &SC_LCRI_LO, &self.weights),
575            affinity_mode: majority_discrete(&DV_AFFINITY, &self.weights),
576            sojourn_thresh_ns: blend_continuous(b.sojourn_thresh_ns, &SC_SOJOURN, &self.weights),
577            burst_slice_ns: blend_continuous(b.burst_slice_ns, &SC_BURST, &self.weights),
578        }
579    }
580
581    pub fn had_losses(&self) -> bool {
582        self.losses_applied
583    }
584
585    pub fn scale(&self) -> f64 {
586        let s: f64 = (0..N_EXPERTS).map(|i| self.weights[i] * SC_SLICE[i]).sum();
587        s
588    }
589}