1use std::sync::atomic::{AtomicU64, Ordering};
12use std::sync::Arc;
13
14use dashmap::DashMap;
15
16#[derive(Debug, Clone, PartialEq, Eq)]
18pub enum DeoptReason {
19 CudaError(String),
21 NumericalDivergence,
23 AssemblyFailed(String),
25}
26
27impl DeoptReason {
28 pub fn name(&self) -> &'static str {
30 match self {
31 DeoptReason::CudaError(_) => "cuda_error",
32 DeoptReason::NumericalDivergence => "numerical_divergence",
33 DeoptReason::AssemblyFailed(_) => "assembly_failed",
34 }
35 }
36}
37
38pub struct DeoptGuard {
40 deopted: DashMap<u64, DeoptReason>,
41 success_total: AtomicU64,
42 fallback_total: AtomicU64,
43 metrics: Option<tensor_wasm_core::metrics::TensorWasmMetrics>,
44}
45
46impl DeoptGuard {
47 pub fn new() -> Self {
49 Self {
50 deopted: DashMap::new(),
51 success_total: AtomicU64::new(0),
52 fallback_total: AtomicU64::new(0),
53 metrics: None,
54 }
55 }
56
57 pub fn with_metrics(metrics: tensor_wasm_core::metrics::TensorWasmMetrics) -> Self {
64 Self {
65 deopted: DashMap::new(),
66 success_total: AtomicU64::new(0),
67 fallback_total: AtomicU64::new(0),
68 metrics: Some(metrics),
69 }
70 }
71
72 pub fn is_deopted(&self, fingerprint: u64) -> bool {
74 self.deopted.contains_key(&fingerprint)
75 }
76
77 pub fn record_deopt(&self, fingerprint: u64, reason: DeoptReason) {
80 self.fallback_total.fetch_add(1, Ordering::Relaxed);
81 if let Some(m) = &self.metrics {
82 m.offload_fallback_total().inc();
83 }
84 self.deopted.insert(fingerprint, reason);
85 }
86
87 pub fn record_success(&self) {
91 self.success_total.fetch_add(1, Ordering::Relaxed);
92 if let Some(m) = &self.metrics {
93 m.offload_success_total().inc();
94 }
95 }
96
97 pub fn clear_deopt(&self, fingerprint: u64) -> Option<DeoptReason> {
100 self.deopted.remove(&fingerprint).map(|(_, v)| v)
101 }
102
103 pub fn reason(&self, fingerprint: u64) -> Option<DeoptReason> {
105 self.deopted.get(&fingerprint).map(|r| r.value().clone())
106 }
107
108 pub fn success_total(&self) -> u64 {
110 self.success_total.load(Ordering::Relaxed)
111 }
112
113 pub fn fallback_total(&self) -> u64 {
115 self.fallback_total.load(Ordering::Relaxed)
116 }
117
118 pub fn success_rate(&self) -> f64 {
120 let s = self.success_total() as f64;
121 let f = self.fallback_total() as f64;
122 let total = s + f;
123 if total == 0.0 {
124 0.0
125 } else {
126 s / total
127 }
128 }
129}
130
131impl Default for DeoptGuard {
132 fn default() -> Self {
133 Self::new()
134 }
135}
136
137pub type SharedDeoptGuard = Arc<DeoptGuard>;
139
140#[cfg(test)]
141mod tests {
142 use super::*;
143
144 #[test]
145 fn fresh_guard_is_empty() {
146 let g = DeoptGuard::new();
147 assert!(!g.is_deopted(42));
148 assert_eq!(g.success_total(), 0);
149 assert_eq!(g.fallback_total(), 0);
150 assert_eq!(g.success_rate(), 0.0);
151 }
152
153 #[test]
154 fn record_deopt_marks_kernel() {
155 let g = DeoptGuard::new();
156 g.record_deopt(42, DeoptReason::CudaError("ctx not current".into()));
157 assert!(g.is_deopted(42));
158 assert_eq!(g.fallback_total(), 1);
159 let reason = g.reason(42).unwrap();
160 assert!(matches!(reason, DeoptReason::CudaError(_)));
161 }
162
163 #[test]
164 fn clear_deopt_reverts() {
165 let g = DeoptGuard::new();
166 g.record_deopt(42, DeoptReason::NumericalDivergence);
167 let popped = g.clear_deopt(42).unwrap();
168 assert_eq!(popped, DeoptReason::NumericalDivergence);
169 assert!(!g.is_deopted(42));
170 }
171
172 #[test]
173 fn success_rate_accumulates() {
174 let g = DeoptGuard::new();
175 for _ in 0..7 {
176 g.record_success();
177 }
178 for _ in 0..3 {
179 g.record_deopt(0, DeoptReason::NumericalDivergence);
180 }
181 assert!((g.success_rate() - 0.7).abs() < 1e-9);
182 }
183
184 #[test]
185 fn deopt_reason_names() {
186 assert_eq!(
187 DeoptReason::NumericalDivergence.name(),
188 "numerical_divergence"
189 );
190 assert_eq!(DeoptReason::CudaError("x".into()).name(), "cuda_error");
191 assert_eq!(
192 DeoptReason::AssemblyFailed("x".into()).name(),
193 "assembly_failed"
194 );
195 }
196
197 #[test]
198 fn with_metrics_increments_offload_success_total() {
199 use tensor_wasm_core::metrics::TensorWasmMetrics;
200 let metrics = TensorWasmMetrics::new();
201 let g = DeoptGuard::with_metrics(metrics.clone());
202 g.record_success();
203 g.record_success();
204 g.record_success();
205 assert_eq!(g.success_total(), 3);
207 let text = metrics.encode_text();
209 assert!(
210 text.contains("tensor_wasm_offload_success_total 3"),
211 "got:\n{text}"
212 );
213 }
214
215 #[test]
216 fn with_metrics_increments_offload_fallback_total() {
217 use tensor_wasm_core::metrics::TensorWasmMetrics;
218 let metrics = TensorWasmMetrics::new();
219 let g = DeoptGuard::with_metrics(metrics.clone());
220 g.record_deopt(1, DeoptReason::NumericalDivergence);
221 g.record_deopt(2, DeoptReason::CudaError("x".into()));
222 assert_eq!(g.fallback_total(), 2);
223 let text = metrics.encode_text();
224 assert!(
225 text.contains("tensor_wasm_offload_fallback_total 2"),
226 "got:\n{text}"
227 );
228 }
229
230 #[test]
231 fn without_metrics_still_works() {
232 let g = DeoptGuard::new();
234 g.record_success();
235 g.record_deopt(42, DeoptReason::NumericalDivergence);
236 assert_eq!(g.success_total(), 1);
237 assert_eq!(g.fallback_total(), 1);
238 }
239}