1use core::fmt;
25use core::time::Duration;
26
27use crate::admission::StopReason;
28
29#[derive(Clone, Copy, Debug, PartialEq, Eq)]
35pub enum Seam {
36 TalkerLogits,
38 MicrodecoderLogits,
40 CodecOutput,
42 Pcm,
44}
45
46impl Seam {
47 #[must_use]
49 pub const fn as_str(self) -> &'static str {
50 match self {
51 Self::TalkerLogits => "talker_logits",
52 Self::MicrodecoderLogits => "microdecoder_logits",
53 Self::CodecOutput => "codec_output",
54 Self::Pcm => "pcm",
55 }
56 }
57}
58
59#[derive(Clone, Copy, Debug, PartialEq, Eq)]
61pub enum SeamPolicy {
62 Off,
65 Sampled { every: u32 },
68 All,
70}
71
72impl SeamPolicy {
73 #[must_use]
77 pub const fn is_checking(self) -> bool {
78 !matches!(self, Self::Off)
79 }
80}
81
82#[derive(Clone, Copy, Debug, PartialEq, Eq)]
87pub enum HealthViolation {
88 NonFinite {
90 seam: Seam,
91 index: usize,
93 is_nan: bool,
95 },
96 NoProgress {
98 frames_emitted: u64,
99 stalled_millis: u64,
100 },
101 StopInconsistent {
103 claimed: StopReason,
104 frames_emitted: u64,
105 frame_cap: u64,
106 },
107 RepetitionRunaway { token: u32, repeats: u32 },
109 OutputSilent { silent_millis: u64 },
111 KernelDemoted { from: KernelTier, to: KernelTier },
113 ThermalDegraded {
115 percent_below_baseline: u32,
117 },
118}
119
120impl HealthViolation {
121 #[must_use]
123 pub const fn as_str(self) -> &'static str {
124 match self {
125 Self::NonFinite { .. } => "non_finite",
126 Self::NoProgress { .. } => "no_progress",
127 Self::StopInconsistent { .. } => "stop_inconsistent",
128 Self::RepetitionRunaway { .. } => "repetition_runaway",
129 Self::OutputSilent { .. } => "output_silent",
130 Self::KernelDemoted { .. } => "kernel_demoted",
131 Self::ThermalDegraded { .. } => "thermal_degraded",
132 }
133 }
134
135 #[must_use]
140 pub const fn invalidates_output(self) -> bool {
141 !matches!(
142 self,
143 Self::KernelDemoted { .. } | Self::ThermalDegraded { .. }
144 )
145 }
146
147 #[must_use]
149 pub const fn remedy(self) -> &'static str {
150 match self {
151 Self::NonFinite { .. } => {
152 "a non-finite value reached this seam: rerun with FTTS_MATH_MODE=strict; if it \
153 persists there, the fault is in the kernel rather than a fast-math approximation"
154 }
155 Self::NoProgress { .. } => {
156 "generation stopped advancing: cancel and retry; if reproducible, capture the \
157 prompt — a stalled decode loop is a bug, not a capacity problem"
158 }
159 Self::StopInconsistent { .. } => {
160 "the stop reason disagrees with the frame counters; treat this result as \
161 untrusted and report it — one of the two is lying about whether audio was cut off"
162 }
163 Self::RepetitionRunaway { .. } => {
164 "the model entered a repetition loop: raise the repetition penalty or shorten the \
165 input; the audio to this point is usable, everything after the loop began is not"
166 }
167 Self::OutputSilent { .. } => {
168 "output was silent past the allowed window: check the voice pack and reference \
169 audio; a silent result is a failure even though it produced bytes"
170 }
171 Self::KernelDemoted { .. } => {
172 "an optimised kernel failed its selftest and the certified scalar path took over: \
173 results stay correct and slower; report the ISA and CPU"
174 }
175 Self::ThermalDegraded { .. } => {
176 "sustained throughput fell below the opening window: expected under thermal load; \
177 do not quote this run's rate as a steady-state number"
178 }
179 }
180 }
181}
182
183impl fmt::Display for HealthViolation {
184 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
185 match self {
186 Self::NonFinite {
187 seam,
188 index,
189 is_nan,
190 } => write!(
191 formatter,
192 "{} at {} index {index}",
193 if *is_nan { "NaN" } else { "infinity" },
194 seam.as_str()
195 ),
196 Self::NoProgress {
197 frames_emitted,
198 stalled_millis,
199 } => write!(
200 formatter,
201 "no frame progress for {stalled_millis} ms after {frames_emitted} frame(s)"
202 ),
203 Self::StopInconsistent {
204 claimed,
205 frames_emitted,
206 frame_cap,
207 } => write!(
208 formatter,
209 "stop reason {} contradicts {frames_emitted} frame(s) against a cap of {frame_cap}",
210 claimed.as_str()
211 ),
212 Self::RepetitionRunaway { token, repeats } => {
213 write!(formatter, "token {token} repeated {repeats} times")
214 }
215 Self::OutputSilent { silent_millis } => {
216 write!(formatter, "output silent for {silent_millis} ms")
217 }
218 Self::KernelDemoted { from, to } => write!(
219 formatter,
220 "kernel demoted from {} to {}",
221 from.as_str(),
222 to.as_str()
223 ),
224 Self::ThermalDegraded {
225 percent_below_baseline,
226 } => write!(
227 formatter,
228 "throughput {percent_below_baseline}% below the opening window"
229 ),
230 }
231 }
232}
233
234#[derive(Clone, Debug)]
240pub struct NumericGuard {
241 policy: SeamPolicy,
242 calls: u32,
243}
244
245impl NumericGuard {
246 #[must_use]
247 pub const fn new(policy: SeamPolicy) -> Self {
248 Self { policy, calls: 0 }
249 }
250
251 #[must_use]
252 pub const fn policy(&self) -> SeamPolicy {
253 self.policy
254 }
255
256 pub fn check(&mut self, seam: Seam, values: &[f32]) -> Result<(), HealthViolation> {
261 if !self.should_check() {
262 return Ok(());
263 }
264 for (index, value) in values.iter().enumerate() {
265 if !value.is_finite() {
266 return Err(HealthViolation::NonFinite {
267 seam,
268 index,
269 is_nan: value.is_nan(),
270 });
271 }
272 }
273 Ok(())
274 }
275
276 fn should_check(&mut self) -> bool {
277 match self.policy {
278 SeamPolicy::Off => false,
279 SeamPolicy::All => true,
280 SeamPolicy::Sampled { every } => {
281 if every <= 1 {
285 return true;
286 }
287 let due = self.calls.is_multiple_of(every);
288 self.calls = self.calls.wrapping_add(1);
289 due
290 }
291 }
292 }
293}
294
295#[derive(Clone, Debug)]
304pub struct ProgressWatchdog<T> {
305 timeout: Duration,
306 last_progress: T,
307 frames_emitted: u64,
308}
309
310impl<T: Copy + core::ops::Sub<T, Output = Duration>> ProgressWatchdog<T> {
311 #[must_use]
312 pub const fn new(timeout: Duration, started: T) -> Self {
313 Self {
314 timeout,
315 last_progress: started,
316 frames_emitted: 0,
317 }
318 }
319
320 pub fn record_frame(&mut self, now: T) {
322 self.frames_emitted += 1;
323 self.last_progress = now;
324 }
325
326 #[must_use]
327 pub const fn frames_emitted(&self) -> u64 {
328 self.frames_emitted
329 }
330
331 pub fn check(&self, now: T) -> Result<(), HealthViolation> {
333 let stalled = now - self.last_progress;
334 if stalled > self.timeout {
335 return Err(HealthViolation::NoProgress {
336 frames_emitted: self.frames_emitted,
337 stalled_millis: u64::try_from(stalled.as_millis()).unwrap_or(u64::MAX),
338 });
339 }
340 Ok(())
341 }
342}
343
344pub fn check_stop_consistency(
359 claimed: StopReason,
360 frames_emitted: u64,
361 frame_cap: u64,
362) -> Result<(), HealthViolation> {
363 let inconsistent = match claimed {
364 StopReason::EndOfSpeech => frames_emitted >= frame_cap,
365 StopReason::FrameCapReached => frames_emitted < frame_cap,
366 StopReason::DurationLimitReached | StopReason::Cancelled => false,
368 };
369 if inconsistent {
370 return Err(HealthViolation::StopInconsistent {
371 claimed,
372 frames_emitted,
373 frame_cap,
374 });
375 }
376 Ok(())
377}
378
379#[derive(Clone, Debug)]
388pub struct RunawayDetector {
389 max_consecutive: u32,
390 max_cycle_repeats: u32,
391 last: Option<u32>,
392 consecutive: u32,
393 recent: [u32; Self::CYCLE_WINDOW],
394 filled: usize,
395 cycle_repeats: u32,
396}
397
398impl RunawayDetector {
399 const CYCLE_WINDOW: usize = 8;
401
402 #[must_use]
403 pub const fn new(max_consecutive: u32, max_cycle_repeats: u32) -> Self {
404 Self {
405 max_consecutive,
406 max_cycle_repeats,
407 last: None,
408 consecutive: 0,
409 recent: [u32::MAX; Self::CYCLE_WINDOW],
410 filled: 0,
411 cycle_repeats: 0,
412 }
413 }
414
415 pub fn observe(&mut self, token: u32) -> Result<(), HealthViolation> {
417 if self.last == Some(token) {
418 self.consecutive += 1;
419 } else {
420 self.consecutive = 1;
421 self.last = Some(token);
422 }
423 if self.consecutive > self.max_consecutive {
424 return Err(HealthViolation::RepetitionRunaway {
425 token,
426 repeats: self.consecutive,
427 });
428 }
429
430 if self.filled >= 2 && self.recent[(self.filled - 2) % Self::CYCLE_WINDOW] == token {
432 self.cycle_repeats += 1;
433 if self.cycle_repeats > self.max_cycle_repeats {
434 return Err(HealthViolation::RepetitionRunaway {
435 token,
436 repeats: self.cycle_repeats,
437 });
438 }
439 } else {
440 self.cycle_repeats = 0;
441 }
442 self.recent[self.filled % Self::CYCLE_WINDOW] = token;
443 self.filled += 1;
444 Ok(())
445 }
446}
447
448#[derive(Clone, Debug)]
457pub struct SilenceDetector {
458 floor: i16,
459 max_silent_samples: u64,
460 sample_rate: u32,
461 silent_samples: u64,
462}
463
464impl SilenceDetector {
465 #[must_use]
472 pub const fn new(floor: i16, max_silent: Duration, sample_rate: u32) -> Self {
473 Self {
474 floor,
475 max_silent_samples: (max_silent.as_millis() as u64) * (sample_rate as u64) / 1000,
476 sample_rate,
477 silent_samples: 0,
478 }
479 }
480
481 pub fn observe(&mut self, samples: &[i16]) -> Result<(), HealthViolation> {
483 for sample in samples {
484 if sample.saturating_abs() > self.floor {
485 self.silent_samples = 0;
486 } else {
487 self.silent_samples += 1;
488 }
489 }
490 if self.sample_rate == 0 {
491 return Ok(());
492 }
493 if self.silent_samples > self.max_silent_samples {
494 return Err(HealthViolation::OutputSilent {
495 silent_millis: self.silent_millis(),
496 });
497 }
498 Ok(())
499 }
500
501 #[must_use]
503 pub const fn silent_millis(&self) -> u64 {
504 if self.sample_rate == 0 {
505 return 0;
506 }
507 self.silent_samples * 1000 / self.sample_rate as u64
508 }
509}
510
511#[derive(Clone, Copy, Debug, PartialEq, Eq)]
517pub enum KernelTier {
518 Optimized(&'static str),
520 Scalar,
522}
523
524impl KernelTier {
525 #[must_use]
526 pub const fn as_str(self) -> &'static str {
527 match self {
528 Self::Optimized(name) => name,
529 Self::Scalar => "scalar",
530 }
531 }
532}
533
534#[derive(Clone, Debug)]
541pub struct KernelSelector {
542 preferred: KernelTier,
543 active: KernelTier,
544 demoted: bool,
545}
546
547impl KernelSelector {
548 #[must_use]
549 pub const fn new(preferred: KernelTier) -> Self {
550 Self {
551 preferred,
552 active: preferred,
553 demoted: false,
554 }
555 }
556
557 #[must_use]
558 pub const fn active(&self) -> KernelTier {
559 self.active
560 }
561
562 #[must_use]
563 pub const fn demoted(&self) -> bool {
564 self.demoted
565 }
566
567 pub fn on_selftest_failure(&mut self) -> Option<HealthViolation> {
569 if self.demoted || matches!(self.active, KernelTier::Scalar) {
570 self.demoted = true;
571 self.active = KernelTier::Scalar;
572 return None;
573 }
574 let from = self.active;
575 self.active = KernelTier::Scalar;
576 self.demoted = true;
577 Some(HealthViolation::KernelDemoted {
578 from,
579 to: KernelTier::Scalar,
580 })
581 }
582
583 #[must_use]
585 pub const fn preferred(&self) -> KernelTier {
586 self.preferred
587 }
588}
589
590#[derive(Clone, Debug)]
601pub struct ThermalReporter {
602 baseline: Option<f64>,
603 latest: Option<f64>,
604 report_below_percent: u32,
605}
606
607impl ThermalReporter {
608 #[must_use]
609 pub const fn new(report_below_percent: u32) -> Self {
610 Self {
611 baseline: None,
612 latest: None,
613 report_below_percent,
614 }
615 }
616
617 pub fn observe(&mut self, throughput: f64) -> Option<HealthViolation> {
621 if !throughput.is_finite() || throughput <= 0.0 {
622 return None;
623 }
624 self.latest = Some(throughput);
625 let baseline = *self.baseline.get_or_insert(throughput);
626 if throughput >= baseline {
627 return None;
628 }
629 let percent = ((baseline - throughput) / baseline * 100.0).floor();
630 let percent = percent.clamp(0.0, f64::from(u32::MAX)) as u32;
631 if percent >= self.report_below_percent {
632 return Some(HealthViolation::ThermalDegraded {
633 percent_below_baseline: percent,
634 });
635 }
636 None
637 }
638
639 #[must_use]
640 pub const fn baseline(&self) -> Option<f64> {
641 self.baseline
642 }
643
644 #[must_use]
645 pub const fn latest(&self) -> Option<f64> {
646 self.latest
647 }
648}
649
650#[cfg(test)]
651mod tests {
652 use super::*;
653 use std::time::Instant;
654
655 #[test]
656 fn a_nan_is_located_at_its_first_index() {
657 let mut guard = NumericGuard::new(SeamPolicy::All);
658 let values = [1.0, 2.0, f32::NAN, f32::NAN];
659 let violation = guard
660 .check(Seam::TalkerLogits, &values)
661 .expect_err("NaN must be caught");
662 assert_eq!(
663 violation,
664 HealthViolation::NonFinite {
665 seam: Seam::TalkerLogits,
666 index: 2,
667 is_nan: true,
668 }
669 );
670 assert!(violation.invalidates_output());
671 }
672
673 #[test]
674 fn an_infinity_is_distinguished_from_a_nan() {
675 let mut guard = NumericGuard::new(SeamPolicy::All);
676 let violation = guard
677 .check(Seam::CodecOutput, &[f32::INFINITY])
678 .expect_err("infinity must be caught");
679 assert!(matches!(
680 violation,
681 HealthViolation::NonFinite { is_nan: false, .. }
682 ));
683 }
684
685 #[test]
686 fn policy_off_never_looks_and_says_so() {
687 let mut guard = NumericGuard::new(SeamPolicy::Off);
688 assert!(guard.check(Seam::Pcm, &[f32::NAN]).is_ok());
689 assert!(!guard.policy().is_checking());
691 }
692
693 #[test]
694 fn a_zero_sampling_interval_checks_rather_than_disables() {
695 let mut guard = NumericGuard::new(SeamPolicy::Sampled { every: 0 });
697 assert!(guard.check(Seam::Pcm, &[f32::NAN]).is_err());
698 }
699
700 #[test]
701 fn sampling_checks_periodically() {
702 let mut guard = NumericGuard::new(SeamPolicy::Sampled { every: 3 });
703 let bad = [f32::NAN];
704 assert!(guard.check(Seam::Pcm, &bad).is_err(), "first call checks");
705 assert!(guard.check(Seam::Pcm, &bad).is_ok(), "second is skipped");
706 assert!(guard.check(Seam::Pcm, &bad).is_ok(), "third is skipped");
707 assert!(guard.check(Seam::Pcm, &bad).is_err(), "fourth checks again");
708 }
709
710 #[test]
711 fn the_watchdog_fires_only_after_the_timeout() {
712 let start = Instant::now();
713 let watchdog = ProgressWatchdog::new(Duration::from_millis(500), start);
714 assert!(watchdog.check(start + Duration::from_millis(499)).is_ok());
715 let violation = watchdog
716 .check(start + Duration::from_millis(501))
717 .expect_err("a stall past the timeout must fire");
718 assert!(matches!(
719 violation,
720 HealthViolation::NoProgress {
721 frames_emitted: 0,
722 ..
723 }
724 ));
725 }
726
727 #[test]
728 fn recording_a_frame_resets_the_stall_timer() {
729 let start = Instant::now();
730 let mut watchdog = ProgressWatchdog::new(Duration::from_millis(100), start);
731 let later = start + Duration::from_millis(90);
732 watchdog.record_frame(later);
733 assert!(watchdog.check(later + Duration::from_millis(90)).is_ok());
734 assert_eq!(watchdog.frames_emitted(), 1);
735 }
736
737 #[test]
738 fn end_of_speech_on_the_cap_is_reported_as_inconsistent() {
739 let violation = check_stop_consistency(StopReason::EndOfSpeech, 2048, 2048)
741 .expect_err("EOS exactly on the cap must be challenged");
742 assert!(matches!(
743 violation,
744 HealthViolation::StopInconsistent { .. }
745 ));
746 assert!(violation.invalidates_output());
747 }
748
749 #[test]
750 fn a_cap_stop_short_of_the_cap_is_inconsistent() {
751 assert!(check_stop_consistency(StopReason::FrameCapReached, 100, 2048).is_err());
752 }
753
754 #[test]
755 fn consistent_outcomes_pass() {
756 assert!(check_stop_consistency(StopReason::EndOfSpeech, 100, 2048).is_ok());
757 assert!(check_stop_consistency(StopReason::FrameCapReached, 2048, 2048).is_ok());
758 assert!(check_stop_consistency(StopReason::Cancelled, 7, 2048).is_ok());
760 assert!(check_stop_consistency(StopReason::DurationLimitReached, 7, 2048).is_ok());
761 }
762
763 #[test]
764 fn a_stuck_token_trips_the_runaway_detector() {
765 let mut detector = RunawayDetector::new(4, 8);
766 for _ in 0..4 {
767 detector.observe(42).expect("within threshold");
768 }
769 let violation = detector
770 .observe(42)
771 .expect_err("the fifth repeat must trip");
772 assert!(matches!(
773 violation,
774 HealthViolation::RepetitionRunaway {
775 token: 42,
776 repeats: 5
777 }
778 ));
779 }
780
781 #[test]
782 fn a_two_token_cycle_trips_even_though_nothing_repeats_consecutively() {
783 let mut detector = RunawayDetector::new(100, 3);
785 let mut result = Ok(());
786 for index in 0..12 {
787 result = detector.observe(if index % 2 == 0 { 7 } else { 9 });
788 if result.is_err() {
789 break;
790 }
791 }
792 assert!(result.is_err(), "a ping-pong cycle must be detected");
793 }
794
795 #[test]
796 fn ordinary_variety_does_not_trip_the_detector() {
797 let mut detector = RunawayDetector::new(4, 3);
798 for token in 0..64u32 {
799 detector.observe(token).expect("varied tokens are healthy");
800 }
801 }
802
803 #[test]
804 fn silence_past_the_window_is_a_violation() {
805 let mut detector = SilenceDetector::new(4, Duration::from_millis(100), 24_000);
806 let silent = vec![0i16; 2_401];
808 let violation = detector
809 .observe(&silent)
810 .expect_err("silence past the window must fire");
811 assert!(matches!(violation, HealthViolation::OutputSilent { .. }));
812 }
813
814 #[test]
815 fn any_audible_sample_resets_the_silence_run() {
816 let mut detector = SilenceDetector::new(4, Duration::from_millis(100), 24_000);
817 detector.observe(&vec![0i16; 2_000]).expect("under window");
818 detector.observe(&[9_000]).expect("audible sample resets");
819 assert_eq!(detector.silent_millis(), 0);
820 detector
821 .observe(&vec![0i16; 2_000])
822 .expect("run restarted, so still under the window");
823 }
824
825 #[test]
826 fn demotion_is_one_way_and_reported_once() {
827 let mut selector = KernelSelector::new(KernelTier::Optimized("i8mm"));
828 assert_eq!(selector.active(), KernelTier::Optimized("i8mm"));
829
830 let violation = selector
831 .on_selftest_failure()
832 .expect("the first demotion is reported");
833 assert_eq!(
834 violation,
835 HealthViolation::KernelDemoted {
836 from: KernelTier::Optimized("i8mm"),
837 to: KernelTier::Scalar,
838 }
839 );
840 assert!(!violation.invalidates_output());
842 assert_eq!(selector.active(), KernelTier::Scalar);
843
844 assert!(selector.on_selftest_failure().is_none());
847 assert_eq!(selector.active(), KernelTier::Scalar);
848 assert!(selector.demoted());
849 assert_eq!(selector.preferred(), KernelTier::Optimized("i8mm"));
850 }
851
852 #[test]
853 fn thermal_decline_is_reported_against_the_opening_window() {
854 let mut reporter = ThermalReporter::new(10);
855 assert!(
856 reporter.observe(20.0).is_none(),
857 "the first sample is the baseline"
858 );
859 assert!(
860 reporter.observe(19.0).is_none(),
861 "5% is under the threshold"
862 );
863 let violation = reporter
864 .observe(17.0)
865 .expect("15% below baseline must be reported");
866 assert_eq!(
867 violation,
868 HealthViolation::ThermalDegraded {
869 percent_below_baseline: 15
870 }
871 );
872 assert!(!violation.invalidates_output());
874 assert_eq!(reporter.baseline(), Some(20.0));
875 assert_eq!(reporter.latest(), Some(17.0));
876 }
877
878 #[test]
879 fn a_nonsense_throughput_sample_is_ignored_rather_than_becoming_the_baseline() {
880 let mut reporter = ThermalReporter::new(10);
881 assert!(reporter.observe(0.0).is_none());
882 assert!(reporter.observe(f64::NAN).is_none());
883 assert_eq!(reporter.baseline(), None, "no baseline was established");
884 assert!(reporter.observe(10.0).is_none());
885 assert_eq!(reporter.baseline(), Some(10.0));
886 }
887
888 #[test]
889 fn every_violation_carries_a_remedy_and_a_wire_name() {
890 let violations = [
891 HealthViolation::NonFinite {
892 seam: Seam::Pcm,
893 index: 0,
894 is_nan: true,
895 },
896 HealthViolation::NoProgress {
897 frames_emitted: 1,
898 stalled_millis: 2,
899 },
900 HealthViolation::StopInconsistent {
901 claimed: StopReason::EndOfSpeech,
902 frames_emitted: 1,
903 frame_cap: 1,
904 },
905 HealthViolation::RepetitionRunaway {
906 token: 1,
907 repeats: 2,
908 },
909 HealthViolation::OutputSilent { silent_millis: 1 },
910 HealthViolation::KernelDemoted {
911 from: KernelTier::Optimized("i8mm"),
912 to: KernelTier::Scalar,
913 },
914 HealthViolation::ThermalDegraded {
915 percent_below_baseline: 11,
916 },
917 ];
918 for violation in violations {
919 assert!(!violation.as_str().is_empty());
920 assert!(
921 violation.remedy().len() > 40,
922 "{}: a remedy must tell the caller what to do",
923 violation.as_str()
924 );
925 assert!(!violation.to_string().is_empty());
926 }
927 }
928}