#[cfg(feature = "gpu")]
mod adapter_probe;
mod backend;
#[cfg(all(test, feature = "gpu", target_os = "linux"))]
pub(crate) use backend::load_dynamic_library;
#[cfg(all(feature = "gpu", target_os = "linux"))]
pub(crate) use backend::probe_cuda_peer;
#[cfg(all(test, feature = "gpu"))]
pub(crate) use backend::with_test_resident_dispatch_failure;
pub use backend::GpuBackendAvailability;
#[cfg(feature = "gpu")]
pub(crate) use backend::{scan_gpu_literal_evidence_by_region_resident, GpuResidentLiteralSlot};
pub(crate) use backend::{GpuBackendAcquisitionFailure, GpuBackendPeers};
type RecoveryReceiptCounter = std::sync::Arc<std::sync::atomic::AtomicU64>;
thread_local! {
static RECOVERY_RECEIPT_COUNTER: std::cell::RefCell<Option<RecoveryReceiptCounter>> =
const { std::cell::RefCell::new(None) };
}
struct RecoveryReceiptCounterGuard {
previous: Option<RecoveryReceiptCounter>,
}
impl Drop for RecoveryReceiptCounterGuard {
fn drop(&mut self) {
let previous = self.previous.take();
RECOVERY_RECEIPT_COUNTER.with_borrow_mut(|counter| {
*counter = previous;
});
}
}
pub(crate) fn capture_recovery_receipts() -> Option<RecoveryReceiptCounter> {
RECOVERY_RECEIPT_COUNTER.with_borrow(|counter| counter.clone())
}
pub(crate) fn with_captured_recovery_receipts<T>(
counter: Option<&RecoveryReceiptCounter>,
operation: impl FnOnce() -> T,
) -> T {
let previous = RECOVERY_RECEIPT_COUNTER
.with_borrow_mut(|current| std::mem::replace(&mut *current, counter.cloned()));
let _guard = RecoveryReceiptCounterGuard { previous };
operation()
}
pub(crate) fn with_recovery_receipt_scope<T>(operation: impl FnOnce() -> T) -> (T, u64) {
let counter = RecoveryReceiptCounter::new(std::sync::atomic::AtomicU64::new(0));
let result = with_captured_recovery_receipts(Some(&counter), operation);
let receipts = counter.load(std::sync::atomic::Ordering::Relaxed);
(result, receipts)
}
pub(crate) fn record_recovery_receipt() {
RECOVERY_RECEIPT_COUNTER.with_borrow(|counter| {
if let Some(counter) = counter {
match counter.fetch_update(
std::sync::atomic::Ordering::Relaxed,
std::sync::atomic::Ordering::Relaxed,
|receipts| Some(receipts.saturating_add(1)),
) {
Ok(_) => {}
Err(_) => {
eprintln!(
"keyhog: recovery receipt counter rejected an unconditional saturating update"
);
tracing::error!(
target: "keyhog::gpu",
"recovery receipt counter rejected an unconditional saturating update"
);
}
}
}
});
}
#[cfg(feature = "gpu")]
pub(crate) mod gpu_shader;
mod policy;
pub use policy::*;
mod self_test;
pub use self_test::*;
#[cfg(feature = "gpu")]
pub(crate) use adapter_probe::{
gpu_adapter_device_identity, gpu_adapter_probe, is_software_adapter,
};
static MOE_FEATURE_NS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
static MOE_SCORE_NS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
#[cfg(feature = "ml")]
fn ml_split_prof_enabled() -> bool {
crate::scan_profile::enabled()
}
pub(crate) fn ml_split_profile_dump() {
use std::sync::atomic::Ordering::Relaxed;
let f = MOE_FEATURE_NS.swap(0, Relaxed) as f64 / 1e6;
let s = MOE_SCORE_NS.swap(0, Relaxed) as f64 / 1e6;
if f == 0.0 && s == 0.0 {
return;
}
eprintln!(
"=== ML split: feature_extract={f:.1}ms moe_score={s:.1}ms (score = {:.1}% of ML compute; \
only this fraction is GPU-offloadable) ===",
100.0 * s / (f + s).max(1e-9),
);
}
pub(crate) fn ml_split_profile_reset() {
use std::sync::atomic::Ordering::Relaxed;
MOE_FEATURE_NS.store(0, Relaxed);
MOE_SCORE_NS.store(0, Relaxed);
}
#[cfg(all(test, feature = "ml", feature = "multiline"))]
pub(crate) fn batch_ml_inference<T: crate::ml_scorer::MlScoreInput>(
candidates: &[T],
config: &crate::types::ScannerConfig,
) -> Vec<f64> {
match batch_ml_inference_with_timeout(
candidates,
config,
std::time::Duration::from_millis(
crate::scanner_config::ScannerTuningConfig::GPU_MOE_TIMEOUT_MS_DEFAULT,
),
) {
Ok(scores) => scores,
Err(error) => panic!("test GPU ML inference failed: {error}"),
}
}
#[cfg(feature = "ml")]
pub(crate) fn batch_ml_inference_with_timeout<T: crate::ml_scorer::MlScoreInput>(
candidates: &[T],
config: &crate::types::ScannerConfig,
gpu_moe_timeout: std::time::Duration,
) -> crate::error::Result<Vec<f64>> {
if candidates.is_empty() {
return Ok(Vec::new());
}
#[cfg(feature = "ml")]
{
use rayon::prelude::*;
#[cfg(not(feature = "gpu"))]
let _ = gpu_moe_timeout; let prof = ml_split_prof_enabled();
if candidates.len() < crate::ml_scorer::GPU_BATCH_THRESHOLD {
let t = prof.then(std::time::Instant::now);
let scores = crate::ml_scorer::score_input_batch_serial(candidates, config);
if let Some(t) = t {
MOE_SCORE_NS.fetch_add(
t.elapsed().as_nanos() as u64,
std::sync::atomic::Ordering::Relaxed,
);
}
return Ok(scores);
}
let t_feat = prof.then(std::time::Instant::now);
let features: Vec<[f32; crate::ml_scorer::NUM_FEATURES]> = candidates
.par_iter()
.map(|candidate| candidate.ml_features(config))
.collect();
if let Some(t) = t_feat {
MOE_FEATURE_NS.fetch_add(
t.elapsed().as_nanos() as u64,
std::sync::atomic::Ordering::Relaxed,
);
}
let t_score = prof.then(std::time::Instant::now);
let score_features_on_cpu =
|| crate::ml_scorer::score_precomputed_batch_on_cpu(candidates, &features);
let scores = {
#[cfg(feature = "gpu")]
{
match backend::batch_score_features(&features, gpu_moe_timeout) {
Ok(Some(mut scores)) if scores.len() == candidates.len() => {
crate::confidence::policy::apply_empty_candidate_score_policy(
candidates.iter().map(|candidate| candidate.ml_text()),
&mut scores,
);
scores
}
Ok(Some(scores)) => {
debug_assert_eq!(
scores.len(),
candidates.len(),
"backend::batch_score_features must return one score per input"
);
backend::moe_runtime_degrade(&format!(
"caller-side score count mismatch: backend returned {} scores for {} candidates",
scores.len(),
candidates.len()
))
.map_err(|error| crate::error::ScanError::Gpu(error.to_string()))?;
score_features_on_cpu()
}
Ok(None) => score_features_on_cpu(),
Err(error) => {
return Err(crate::error::ScanError::Gpu(error.to_string()));
}
}
}
#[cfg(not(feature = "gpu"))]
{
score_features_on_cpu()
}
};
if let Some(t) = t_score {
MOE_SCORE_NS.fetch_add(
t.elapsed().as_nanos() as u64,
std::sync::atomic::Ordering::Relaxed,
);
}
Ok(scores)
}
#[cfg(not(feature = "ml"))]
{
let _ = candidates; let _ = config; let _ = gpu_moe_timeout; Ok(Vec::new())
}
}
pub fn gpu_available() -> bool {
gpu_probe().available
}