1#[cfg(feature = "gpu")]
31mod adapter_probe;
32mod backend;
33#[cfg(all(test, feature = "gpu", target_os = "linux"))]
34pub(crate) use backend::load_dynamic_library;
35#[cfg(all(feature = "gpu", target_os = "linux"))]
36pub(crate) use backend::probe_cuda_peer;
37#[cfg(all(test, feature = "gpu"))]
38pub(crate) use backend::with_test_resident_dispatch_failure;
39pub use backend::GpuBackendAvailability;
40#[cfg(feature = "gpu")]
41pub(crate) use backend::{scan_gpu_literal_evidence_by_region_resident, GpuResidentLiteralSlot};
42pub(crate) use backend::{GpuBackendAcquisitionFailure, GpuBackendPeers};
43type RecoveryReceiptCounter = std::sync::Arc<std::sync::atomic::AtomicU64>;
44
45thread_local! {
46 static RECOVERY_RECEIPT_COUNTER: std::cell::RefCell<Option<RecoveryReceiptCounter>> =
47 const { std::cell::RefCell::new(None) };
48}
49
50struct RecoveryReceiptCounterGuard {
51 previous: Option<RecoveryReceiptCounter>,
52}
53
54impl Drop for RecoveryReceiptCounterGuard {
55 fn drop(&mut self) {
56 let previous = self.previous.take();
57 RECOVERY_RECEIPT_COUNTER.with_borrow_mut(|counter| {
58 *counter = previous;
59 });
60 }
61}
62
63pub(crate) fn capture_recovery_receipts() -> Option<RecoveryReceiptCounter> {
64 RECOVERY_RECEIPT_COUNTER.with_borrow(|counter| counter.clone())
65}
66
67pub(crate) fn with_captured_recovery_receipts<T>(
68 counter: Option<&RecoveryReceiptCounter>,
69 operation: impl FnOnce() -> T,
70) -> T {
71 let previous = RECOVERY_RECEIPT_COUNTER
72 .with_borrow_mut(|current| std::mem::replace(&mut *current, counter.cloned()));
73 let _guard = RecoveryReceiptCounterGuard { previous };
74 operation()
75}
76
77pub(crate) fn with_recovery_receipt_scope<T>(operation: impl FnOnce() -> T) -> (T, u64) {
78 let counter = RecoveryReceiptCounter::new(std::sync::atomic::AtomicU64::new(0));
79 let result = with_captured_recovery_receipts(Some(&counter), operation);
80 let receipts = counter.load(std::sync::atomic::Ordering::Relaxed);
81 (result, receipts)
82}
83
84pub(crate) fn record_recovery_receipt() {
85 RECOVERY_RECEIPT_COUNTER.with_borrow(|counter| {
86 if let Some(counter) = counter {
87 match counter.fetch_update(
88 std::sync::atomic::Ordering::Relaxed,
89 std::sync::atomic::Ordering::Relaxed,
90 |receipts| Some(receipts.saturating_add(1)),
91 ) {
92 Ok(_) => {}
93 Err(_) => {
96 eprintln!(
97 "keyhog: recovery receipt counter rejected an unconditional saturating update"
98 );
99 tracing::error!(
100 target: "keyhog::gpu",
101 "recovery receipt counter rejected an unconditional saturating update"
102 );
103 }
104 }
105 }
106 });
107}
108#[cfg(feature = "gpu")]
109pub(crate) mod gpu_shader;
110
111mod policy;
112pub use policy::*;
113mod self_test;
114pub use self_test::*;
115
116#[cfg(feature = "gpu")]
117pub(crate) use adapter_probe::{
118 gpu_adapter_device_identity, gpu_adapter_probe, is_software_adapter,
119};
120
121static MOE_FEATURE_NS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
127static MOE_SCORE_NS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
128
129#[cfg(feature = "ml")]
132fn ml_split_prof_enabled() -> bool {
133 crate::scan_profile::enabled()
134}
135
136pub(crate) fn ml_split_profile_dump() {
139 use std::sync::atomic::Ordering::Relaxed;
140 let f = MOE_FEATURE_NS.swap(0, Relaxed) as f64 / 1e6;
141 let s = MOE_SCORE_NS.swap(0, Relaxed) as f64 / 1e6;
142 if f == 0.0 && s == 0.0 {
143 return;
144 }
145 eprintln!(
146 "=== ML split: feature_extract={f:.1}ms moe_score={s:.1}ms (score = {:.1}% of ML compute; \
147only this fraction is GPU-offloadable) ===",
148 100.0 * s / (f + s).max(1e-9),
149 );
150}
151
152pub(crate) fn ml_split_profile_reset() {
153 use std::sync::atomic::Ordering::Relaxed;
154 MOE_FEATURE_NS.store(0, Relaxed);
155 MOE_SCORE_NS.store(0, Relaxed);
156}
157
158#[cfg(all(test, feature = "ml", feature = "multiline"))]
159pub(crate) fn batch_ml_inference<T: crate::ml_scorer::MlScoreInput>(
160 candidates: &[T],
161 config: &crate::types::ScannerConfig,
162) -> Vec<f64> {
163 match batch_ml_inference_with_timeout(
164 candidates,
165 config,
166 std::time::Duration::from_millis(
167 crate::scanner_config::ScannerTuningConfig::GPU_MOE_TIMEOUT_MS_DEFAULT,
168 ),
169 ) {
170 Ok(scores) => scores,
171 Err(error) => panic!("test GPU ML inference failed: {error}"),
172 }
173}
174
175#[cfg(feature = "ml")]
176pub(crate) fn batch_ml_inference_with_timeout<T: crate::ml_scorer::MlScoreInput>(
177 candidates: &[T],
178 config: &crate::types::ScannerConfig,
179 gpu_moe_timeout: std::time::Duration,
180) -> crate::error::Result<Vec<f64>> {
181 if candidates.is_empty() {
182 return Ok(Vec::new());
183 }
184
185 #[cfg(feature = "ml")]
186 {
187 use rayon::prelude::*;
188 #[cfg(not(feature = "gpu"))]
189 let _ = gpu_moe_timeout; let prof = ml_split_prof_enabled();
191
192 if candidates.len() < crate::ml_scorer::GPU_BATCH_THRESHOLD {
198 let t = prof.then(std::time::Instant::now);
200 let scores = crate::ml_scorer::score_input_batch_serial(candidates, config);
201 if let Some(t) = t {
202 MOE_SCORE_NS.fetch_add(
205 t.elapsed().as_nanos() as u64,
206 std::sync::atomic::Ordering::Relaxed,
207 );
208 }
209 return Ok(scores);
210 }
211
212 let t_feat = prof.then(std::time::Instant::now);
214 let features: Vec<[f32; crate::ml_scorer::NUM_FEATURES]> = candidates
215 .par_iter()
216 .map(|candidate| candidate.ml_features(config))
217 .collect();
218 if let Some(t) = t_feat {
219 MOE_FEATURE_NS.fetch_add(
220 t.elapsed().as_nanos() as u64,
221 std::sync::atomic::Ordering::Relaxed,
222 );
223 }
224
225 let t_score = prof.then(std::time::Instant::now);
226 let score_features_on_cpu =
227 || crate::ml_scorer::score_precomputed_batch_on_cpu(candidates, &features);
228 let scores = {
229 #[cfg(feature = "gpu")]
230 {
231 match backend::batch_score_features(&features, gpu_moe_timeout) {
232 Ok(Some(mut scores)) if scores.len() == candidates.len() => {
233 crate::confidence::policy::apply_empty_candidate_score_policy(
234 candidates.iter().map(|candidate| candidate.ml_text()),
235 &mut scores,
236 );
237 scores
238 }
239 Ok(Some(scores)) => {
240 debug_assert_eq!(
241 scores.len(),
242 candidates.len(),
243 "backend::batch_score_features must return one score per input"
244 );
245 backend::moe_runtime_degrade(&format!(
246 "caller-side score count mismatch: backend returned {} scores for {} candidates",
247 scores.len(),
248 candidates.len()
249 ))
250 .map_err(|error| crate::error::ScanError::Gpu(error.to_string()))?;
251 score_features_on_cpu()
252 }
253 Ok(None) => score_features_on_cpu(),
254 Err(error) => {
255 return Err(crate::error::ScanError::Gpu(error.to_string()));
256 }
257 }
258 }
259 #[cfg(not(feature = "gpu"))]
260 {
261 score_features_on_cpu()
262 }
263 };
264 if let Some(t) = t_score {
265 MOE_SCORE_NS.fetch_add(
266 t.elapsed().as_nanos() as u64,
267 std::sync::atomic::Ordering::Relaxed,
268 );
269 }
270 Ok(scores)
271 }
272
273 #[cfg(not(feature = "ml"))]
274 {
275 let _ = candidates; let _ = config; let _ = gpu_moe_timeout; Ok(Vec::new())
279 }
280}
281
282pub fn gpu_available() -> bool {
295 gpu_probe().available
296}