Skip to main content

keyhog_scanner/hw_probe/
mod.rs

1//! Hardware capability probing with once-cached results.
2//!
3//! Detects CPU features (AVX-512, AVX2, NEON), GPU compute (wgpu/Vulkan),
4//! Hyperscan availability, io_uring support, memory, and core counts.
5//! All detection is done once at startup and cached for the process
6//! lifetime.
7//!
8//! Split into focused submodules by hardware-probe responsibility:
9//!
10//!   * `thresholds` - GPU routing crossover constants consumed through
11//!     the public tier lookup functions.
12//!   * [`tier`] - GPU adapter classification + tier threshold profiles.
13//!   * [`select`] - [`select_backend`] routing logic + env-override
14//!     parsing.
15//!   * [`banner`] - `startup_banner` formatter for the CLI header.
16//!   * [`platform`] - per-OS detection of physical cores, memory,
17//!     and io_uring availability.
18
19use std::sync::OnceLock;
20
21mod banner;
22pub(crate) mod platform;
23pub(crate) mod select;
24mod tier;
25
26pub(crate) mod thresholds;
27
28pub use banner::startup_banner;
29pub use select::{
30    gpu_could_engage, parse_backend_str, select_backend, select_backend_verdict,
31    BackendRoutingReason, BackendRoutingVerdict, BACKEND_OVERRIDE_VALUES,
32};
33pub use tier::{gpu_routing_profile, gpu_routing_profiles, GpuRoutingProfile};
34
35/// Scan execution backend selected for a given workload.
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
37#[non_exhaustive]
38pub enum ScanBackend {
39    /// GPU region-presence phase 1 through VYRE's CUDA driver.
40    GpuCuda,
41    /// GPU region-presence phase 1 through VYRE's WGPU driver.
42    GpuWgpu,
43    /// Hyperscan NFA multi-pattern matching + SIMD prefilter.
44    /// This is the primary high-throughput path on all platforms.
45    SimdCpu,
46    /// Pure CPU: Aho-Corasick literals plus Rust regex extraction. No
47    /// Hyperscan or GPU execution.
48    CpuFallback,
49}
50
51impl ScanBackend {
52    /// Stable label for logs and CLI startup banner.
53    #[must_use]
54    pub fn label(self) -> &'static str {
55        match self {
56            Self::GpuCuda => "gpu-cuda-region-presence",
57            Self::GpuWgpu => "gpu-wgpu-region-presence",
58            Self::SimdCpu => "simd-regex",
59            Self::CpuFallback => "cpu-fallback",
60        }
61    }
62
63    /// Whether this route executes on a physical GPU.
64    #[must_use]
65    pub const fn is_gpu(self) -> bool {
66        matches!(self, Self::GpuCuda | Self::GpuWgpu)
67    }
68}
69
70/// True when this build compiled a scan backend beyond the always-present scalar
71/// [`ScanBackend::CpuFallback`]. Hyperscan (`simd`) and/or the GPU stack (`gpu`).
72///
73/// When this is `false` there is no routing choice: every scan can only run
74/// `CpuFallback`, so autoroute calibration is neither needed nor possible and the
75/// caller resolves the lone backend directly instead of failing closed. This is a
76/// COMPILE-time fact, owned here in the scanner where the `simd`/`gpu` feature gates
77/// actually live: a consumer's own feature flags can diverge (e.g. the CLI's
78/// `ci-lean` enables `keyhog-scanner/simd` without the CLI's own `simd` feature), so
79/// consumers MUST ask the scanner rather than checking their own `cfg!`.
80#[must_use]
81pub const fn multiple_backends_compiled() -> bool {
82    simd_backend_compiled() || gpu_backend_compiled()
83}
84
85/// True when this scanner crate was compiled with the Hyperscan/SIMD backend.
86/// Consumers must query this owner instead of their own Cargo feature namespace:
87/// workspace feature unification can enable a dependency backend without
88/// enabling a same-named feature on the consuming crate.
89#[must_use]
90pub const fn simd_backend_compiled() -> bool {
91    cfg!(feature = "simd")
92}
93
94/// Runtime identity of the dynamically linked Hyperscan/Vectorscan library.
95///
96/// Autoroute persistence must bind SIMD measurements to the library that
97/// actually executed them, not only to the fact that SIMD support compiled.
98#[must_use]
99pub fn hyperscan_runtime_identity() -> Option<String> {
100    #[cfg(feature = "simd")]
101    {
102        Some(hyperscan::version().to_string())
103    }
104    #[cfg(not(feature = "simd"))]
105    {
106        None
107    }
108}
109
110/// True when this scanner crate was compiled with the GPU backend stack.
111/// Autoroute host identity and persisted build evidence use this dependency-owned
112/// fact so a GPU-selected calibration can never be stored without GPU identity.
113#[must_use]
114pub const fn gpu_backend_compiled() -> bool {
115    cfg!(feature = "gpu")
116}
117
118/// Single owner of the SIMD-tier label precedence chain.
119///
120/// The label reported by the startup banner, `keyhog backend`, `keyhog doctor`,
121/// and the backend store must always agree, so the `"AVX-512" > "AVX2" > "NEON"
122/// > "scalar"` precedence lives in exactly ONE place. Callers pass the three
123/// probed CPU-feature booleans (typically `caps.has_avx512`, `caps.has_avx2`,
124/// `caps.has_neon`) and receive the highest-priority label that is available.
125///
126/// Precedence is strict and independent of the lower bits: if `has_avx512` is
127/// true the result is `"AVX-512"` regardless of the other two, and so on down
128/// to `"scalar"` when none are present.
129#[must_use]
130pub const fn simd_label(has_avx512: bool, has_avx2: bool, has_neon: bool) -> &'static str {
131    if has_avx512 {
132        "AVX-512"
133    } else if has_avx2 {
134        "AVX2"
135    } else if has_neon {
136        "NEON"
137    } else {
138        "scalar"
139    }
140}
141
142/// Hardware capabilities detected at startup.
143#[derive(Debug, Clone)]
144pub struct HardwareCaps {
145    pub physical_cores: usize,
146    pub logical_cores: usize,
147    pub has_avx2: bool,
148    pub has_avx512: bool,
149    pub has_neon: bool,
150    pub gpu_available: bool,
151    pub gpu_name: Option<String>,
152    /// WGPU's portable `max_buffer_size`, expressed in MiB and capped by
153    /// KeyHog. The historical field name does not mean physical VRAM; WGPU can
154    /// expose a virtual buffer limit larger than installed device memory.
155    pub gpu_vram_mb: Option<u64>,
156    pub gpu_runtime_identity: Option<String>,
157    /// True when the GPU is a software renderer (llvmpipe/lavapipe) - always slower than CPU.
158    pub gpu_is_software: bool,
159    pub total_memory_mb: Option<u64>,
160    pub io_uring_available: bool,
161    /// True when the `simd` feature is compiled in AND Hyperscan initialized.
162    pub hyperscan_available: bool,
163    /// Runtime identity of the Hyperscan/Vectorscan library that produced these
164    /// capabilities. `None` when `hyperscan_available` is false or the identity
165    /// has not been probed yet.
166    pub hyperscan_runtime_identity: Option<String>,
167}
168
169static HW_PROBE: OnceLock<HardwareCaps> = OnceLock::new();
170
171/// Probe hardware once and cache the result.
172pub fn probe_hardware() -> &'static HardwareCaps {
173    HW_PROBE.get_or_init(|| {
174        let logical_cores = std::thread::available_parallelism()
175            .map(|n| n.get())
176            .unwrap_or(1); // LAW10: host/OS hardware probe parse failure => None/conservative default; perf-only, recall-irrelevant
177        let physical_cores = platform::physical_core_count().unwrap_or(logical_cores); // LAW10: host/OS hardware probe parse failure => None/conservative default; perf-only, recall-irrelevant
178
179        #[cfg(target_arch = "x86_64")]
180        let (has_avx2, has_avx512, has_neon) = (
181            std::arch::is_x86_feature_detected!("avx2"),
182            std::arch::is_x86_feature_detected!("avx512f"),
183            false,
184        );
185        #[cfg(target_arch = "aarch64")]
186        let (has_avx2, has_avx512, has_neon) = (false, false, true);
187        #[cfg(not(any(target_arch = "x86_64", target_arch = "aarch64")))]
188        let (has_avx2, has_avx512, has_neon) = (false, false, false);
189
190        let gpu_probe = crate::gpu::gpu_probe();
191        let gpu_available = gpu_probe.available;
192        let gpu_name = gpu_probe.name;
193        let gpu_vram_mb = gpu_probe.buffer_limit_mb;
194        let gpu_runtime_identity = gpu_probe.runtime_identity;
195        let gpu_is_software = gpu_probe.is_software;
196        if gpu_is_software {
197            tracing::warn!(
198                gpu = ?gpu_name,
199                "Software GPU detected: GPU scanning disabled (slower than CPU)"
200            );
201        }
202
203        let hyperscan_available = cfg!(feature = "simd");
204        let hyperscan_runtime_identity = hyperscan_available
205            .then(hyperscan_runtime_identity)
206            .flatten();
207        let total_memory_mb = platform::detect_total_memory_mb();
208        let io_uring_available = platform::detect_io_uring();
209
210        let caps = HardwareCaps {
211            physical_cores,
212            logical_cores,
213            has_avx2,
214            has_avx512,
215            has_neon,
216            gpu_available,
217            gpu_name: gpu_name.clone(),
218            gpu_vram_mb,
219            gpu_runtime_identity,
220            gpu_is_software,
221            total_memory_mb,
222            io_uring_available,
223            hyperscan_available,
224            hyperscan_runtime_identity,
225        };
226
227        tracing::info!(
228            physical_cores,
229            logical_cores,
230            gpu_available,
231            gpu_name = ?gpu_name,
232            has_avx512 = caps.has_avx512,
233            has_avx2 = caps.has_avx2,
234            has_neon = caps.has_neon,
235            hyperscan = hyperscan_available,
236            io_uring = io_uring_available,
237            "hardware probe complete"
238        );
239
240        caps
241    })
242}
243
244#[cfg(test)]
245#[doc(hidden)]
246pub mod testing {
247    pub use super::{
248        gpu_could_engage, parse_backend_str, probe_hardware, select_backend,
249        select_backend_verdict, startup_banner, BackendRoutingReason, BackendRoutingVerdict,
250        HardwareCaps, ScanBackend,
251    };
252
253    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
254    pub enum GpuTier {
255        High,
256        Mid,
257        Low,
258    }
259
260    fn from_inner(tier: super::tier::GpuTier) -> GpuTier {
261        match tier {
262            super::tier::GpuTier::High => GpuTier::High,
263            super::tier::GpuTier::Mid => GpuTier::Mid,
264            super::tier::GpuTier::Low => GpuTier::Low,
265        }
266    }
267
268    fn to_inner(tier: GpuTier) -> super::tier::GpuTier {
269        match tier {
270            GpuTier::High => super::tier::GpuTier::High,
271            GpuTier::Mid => super::tier::GpuTier::Mid,
272            GpuTier::Low => super::tier::GpuTier::Low,
273        }
274    }
275
276    /// Select the CPU scan backend (SIMD/scalar tier) for the given hardware
277    /// capabilities, ignoring any GPU. Delegates to [`super::select`].
278    pub fn cpu_tier_backend(caps: &HardwareCaps) -> ScanBackend {
279        super::select::cpu_tier_backend(caps)
280    }
281
282    /// Classify a GPU adapter name into a routing [`GpuTier`] (High/Mid/Low).
283    /// `None` (no adapter) classifies to the lowest tier.
284    pub fn classify_gpu_tier(adapter_name: Option<&str>) -> GpuTier {
285        from_inner(super::tier::classify_gpu_tier(adapter_name))
286    }
287
288    /// Minimum workload size (bytes) at which the GPU backend is allowed to
289    /// engage at all for this tier (below it, CPU always wins).
290    pub fn gpu_min_bytes_for_tier(tier: GpuTier) -> u64 {
291        super::tier::gpu_min_bytes_for_tier(to_inner(tier))
292    }
293
294    /// Minimum workload size (bytes) at which the GPU runs *solo* (no CPU
295    /// co-scan) for this tier.
296    pub fn gpu_solo_bytes_for_tier(tier: GpuTier) -> u64 {
297        super::tier::gpu_solo_bytes_for_tier(to_inner(tier))
298    }
299
300    /// Pattern-count break-even above which GPU scanning beats CPU at this tier.
301    pub fn gpu_pattern_breakeven_for_tier(tier: GpuTier) -> usize {
302        super::tier::gpu_pattern_breakeven_for_tier(to_inner(tier))
303    }
304
305    /// Choose the scan backend for a batch from hardware caps, total workload
306    /// size, pattern count, and the largest single-chunk size.
307    pub fn select_backend_for_batch(
308        caps: &HardwareCaps,
309        workload_bytes: u64,
310        pattern_count: usize,
311        large_chunk_bytes: u64,
312    ) -> ScanBackend {
313        super::select::select_backend_for_batch(
314            caps,
315            workload_bytes,
316            pattern_count,
317            large_chunk_bytes,
318        )
319    }
320
321    /// Like [`select_backend_for_batch`] but returns the full
322    /// [`BackendRoutingVerdict`] (the chosen backend plus the inputs and reason
323    /// behind the decision) for diagnostics/telemetry.
324    pub fn select_backend_for_batch_verdict(
325        caps: &HardwareCaps,
326        workload_bytes: u64,
327        pattern_count: usize,
328        large_chunk_bytes: u64,
329    ) -> BackendRoutingVerdict {
330        super::select::select_backend_for_batch_verdict(
331            caps,
332            workload_bytes,
333            pattern_count,
334            large_chunk_bytes,
335        )
336    }
337
338    /// Test-only forced backend override (from the `KEYHOG_*` routing env), or
339    /// `None` when routing is not overridden.
340    pub fn forced_backend_override_for_test() -> Option<ScanBackend> {
341        super::select::forced_backend_override_for_test()
342    }
343
344    /// Parse the physical CPU core count from `/proc/cpuinfo` contents (Linux).
345    #[cfg(target_os = "linux")]
346    pub fn linux_physical_cores_from_cpuinfo(content: &str) -> Option<usize> {
347        super::platform::linux_physical_cores_from_cpuinfo(content)
348    }
349
350    /// Parse total system memory (MiB) from `/proc/meminfo` contents (Linux).
351    #[cfg(target_os = "linux")]
352    pub fn linux_total_memory_mb_from_meminfo(content: &str) -> Option<u64> {
353        super::platform::linux_total_memory_mb_from_meminfo(content)
354    }
355}