1#[cfg(feature = "simdsieve")]
2use super::compile_helpers::build_hot_pattern_slots;
3#[cfg(all(target_os = "linux", feature = "gpu"))]
4use super::compile_helpers::surface_cuda_acquisition_failure;
5use super::*;
6
7impl CompiledScanner {
8 pub fn compile(detectors: Vec<DetectorSpec>) -> Result<Self> {
11 Self::compile_with_gpu_policy(detectors, GpuInitPolicy::FromRuntimePolicy)
12 }
13
14 pub fn compile_with_gpu_policy(
17 detectors: Vec<DetectorSpec>,
18 gpu_policy: GpuInitPolicy,
19 ) -> Result<Self> {
20 Self::compile_with_gpu_policy_and_tuning(
21 detectors,
22 gpu_policy,
23 &ScannerTuningConfig::default(),
24 )
25 }
26
27 pub fn compile_with_gpu_policy_and_tuning(
30 detectors: Vec<DetectorSpec>,
31 gpu_policy: GpuInitPolicy,
32 tuning_config: &ScannerTuningConfig,
33 ) -> Result<Self> {
34 super::validation::validate_detector_corpus(&detectors)
35 .map_err(crate::error::ScanError::Config)?;
36 crate::entropy::policy::validate_feature_compatibility(&detectors)
37 .map_err(crate::error::ScanError::Config)?;
38 let decoder_plan = Arc::new(crate::decode::CompiledDecoderPlan::snapshot().map_err(
39 |error| crate::error::ScanError::Config(format!("invalid decoder registry: {error}")),
40 )?);
41 #[cfg(not(feature = "simd"))]
43 let _tuning_config = tuning_config;
44 let mut state = build_compile_state(&detectors)?;
45 let static_intern_strings: Vec<&str> = detectors
49 .iter()
50 .flat_map(|detector| {
51 [
52 detector.id.as_str(),
53 detector.name.as_str(),
54 detector.service.as_str(),
55 ]
56 .into_iter()
57 .chain(
58 detector
59 .entropy_fallback
60 .as_ref()
61 .into_iter()
62 .flat_map(|metadata| {
63 [
64 metadata.id.as_str(),
65 metadata.name.as_str(),
66 metadata.service.as_str(),
67 ]
68 }),
69 )
70 .chain(
71 detector
72 .companions
73 .iter()
74 .map(|companion| companion.name.as_str()),
75 )
76 })
77 .collect();
78 let static_intern = Arc::new(crate::static_intern::StaticInterner::from_detector_strings(
79 static_intern_strings,
80 ));
81 for companions in &mut state.companions {
82 for companion in companions {
83 companion.name =
84 static_intern
85 .lookup(companion.name.as_ref())
86 .ok_or_else(|| {
87 crate::error::ScanError::Config(format!(
88 "compiled companion name missing from static interner: {}",
89 companion.name
90 ))
91 })?;
92 }
93 }
94 let detector_digest = super::detector_digest::from_execution_plan(
95 keyhog_core::compute_spec_hash(&detectors),
96 decoder_plan.identity(),
97 );
98 let detector_plans =
99 crate::detector_plan::CompiledDetectorPlans::compile_with_decoder_plan(
100 &detectors,
101 static_intern.as_ref(),
102 state.companions,
103 decoder_plan,
104 )
105 .map_err(crate::error::ScanError::Config)?;
106 validate_compiled_pattern_detector_indices(
107 &state.ac_map,
108 &state.phase2_patterns,
109 detectors.len(),
110 )?;
111 let ac = build_ac_pattern_set(&state.ac_literals)?;
112 let gpu_disabled = match gpu_policy {
125 GpuInitPolicy::FromRuntimePolicy => crate::gpu::gpu_disabled_by_policy(),
126 GpuInitPolicy::ForceEnabled => false,
127 GpuInitPolicy::ForceDisabled => true,
128 };
129 if gpu_disabled {
130 let disabled_by_policy = matches!(gpu_policy, GpuInitPolicy::ForceDisabled);
131 if disabled_by_policy {
132 tracing::info!(
133 target: "keyhog::routing",
134 "GPU init bypassed by caller policy; scanner will use CPU/SIMD paths"
135 );
136 } else {
137 tracing::info!(
138 target: "keyhog::routing",
139 "GPU init bypassed by resolved scanner policy; routing every chunk through the CPU/SIMD path"
140 );
141 }
142 }
143 #[cfg(feature = "gpu")]
144 let (gpu_backends, gpu_acquisition_failures) = if !gpu_disabled {
145 let mut peers = GpuBackendPeers::default();
146 let mut failures = Vec::new();
147 {
148 #[cfg(target_os = "linux")]
149 {
150 match super::types::probe_cuda_peer() {
151 Ok(caps) => {
152 peers.cuda_available = true;
153 peers.cuda_device_identity = Some(format!(
154 "{}:ordinal={}:cc={}.{}:vram={}",
155 caps.name,
156 caps.ordinal,
157 caps.compute_capability.0,
158 caps.compute_capability.1,
159 caps.total_memory
160 ));
161 match linux_cuda_runtime_identity() {
162 Ok(identity) => peers.cuda_runtime_identity = Some(identity),
163 Err(diagnostic) => {
164 tracing::warn!(
165 target: "keyhog::routing",
166 %diagnostic,
167 "CUDA peer acquired without reproducible runtime identity"
168 );
169 }
170 }
171 tracing::debug!(target: "keyhog::routing", "CUDA peer identity probed");
172 }
173 Err(error) => {
174 surface_cuda_acquisition_failure(&error);
175 failures.push(GpuBackendAcquisitionFailure {
176 backend: "cuda",
177 diagnostic: error.to_string(),
178 });
179 }
180 }
181 }
182 }
183 if let Some(probe) = crate::gpu::gpu_adapter_probe() {
184 peers.wgpu_available = true;
185 peers.wgpu_device_identity = Some(probe.device_identity.clone());
186 peers.wgpu_runtime_identity = Some(probe.runtime_identity.clone());
187 peers.wgpu_is_software = probe.is_software;
188 #[cfg(target_os = "macos")]
189 {
190 peers.metal_available = true;
191 peers.metal_device_identity = Some(probe.device_identity.clone());
192 peers.metal_runtime_identity = Some(format!(
193 "vyre-metal={};{}",
194 env!("KEYHOG_VYRE_METAL_VERSION"),
195 probe.runtime_identity
196 ));
197 tracing::debug!(target: "keyhog::routing", "native Metal peer identity probed");
198 }
199 tracing::debug!(target: "keyhog::routing", "WGPU peer identity probed");
200 } else {
201 failures.push(GpuBackendAcquisitionFailure {
202 backend: "wgpu",
203 diagnostic: "WGPU adapter census found no adapters".to_string(),
204 });
205 }
206 (peers, failures)
207 } else {
208 (GpuBackendPeers::default(), Vec::new())
209 };
210
211 #[cfg(not(feature = "gpu"))]
217 let (gpu_backends, gpu_acquisition_failures) = {
218 let _ = gpu_disabled; (GpuBackendPeers::default(), Vec::new())
220 };
221 let prefix_propagation = CsrU32::from(build_prefix_propagation(&state.ac_literals));
222 let same_prefix_patterns = CsrU32::from(build_same_prefix_patterns(&state.ac_literals));
223
224 #[cfg(feature = "simd")]
228 let simd_compile_plan =
229 build_simd_compile_plan(&state.ac_map, &state.ac_literals, tuning_config);
230 #[cfg(feature = "simd")]
231 let simd_candidate_available = simd_compile_plan.is_some();
232
233 let (phase2_keyword_ac, phase2_keyword_to_patterns, phase2_keywords) =
234 build_phase2_keyword_ac(&state.phase2_patterns);
235 let phase2_keyword_count = phase2_keywords.len();
236 let phase2_keyword_to_patterns = CsrU32::from(phase2_keyword_to_patterns);
237 let phase2_always_active_indices = phase2_always_active_indices(&state.phase2_patterns);
240
241 let (phase2_anchor_index, ((suffix_gate_ac, ac_suffix_gate), confirmed_anchor_index)) =
256 rayon::join(
257 || Phase2AnchorIndex::build(&state.phase2_patterns, &phase2_always_active_indices),
258 || {
259 rayon::join(
260 || build_confirmed_suffix_gate(&state.ac_map),
261 || ConfirmedAnchorIndex::build(&state.ac_map),
262 )
263 },
264 );
265 let phase2_always_anchor_literal_count = phase2_anchor_index
266 .as_ref()
267 .map_or(0, |index| index.always_anchor_literals().len());
268 #[cfg(feature = "gpu")]
269 let confirmed_anchor_literals = confirmed_anchor_index
270 .as_ref()
271 .map_or(&[] as &[String], |index| index.anchor_literals());
272 #[cfg(feature = "gpu")]
273 let confirmed_anchor_literal_count = confirmed_anchor_literals.len();
274 #[cfg(feature = "gpu")]
275 let generic_keyword_literals = detector_plans
276 .generic_assignment()
277 .map(|plan| plan.stem_literals().map(str::to_owned).collect::<Vec<_>>())
278 .unwrap_or_default();
280 #[cfg(feature = "gpu")]
281 let generic_keyword_literal_count = generic_keyword_literals.len();
282 let gated = ac_suffix_gate.iter().filter(|g| !g.is_empty()).count();
283 #[cfg(feature = "gpu")]
284 let gpu_literals = if gpu_backends.availability().any() {
285 let phase2_always_anchor_literals = phase2_anchor_index
286 .as_ref()
287 .map_or(&[] as &[String], |index| index.always_anchor_literals());
288 build_gpu_literals(
289 &state.ac_literals,
290 &phase2_keywords,
291 phase2_always_anchor_literals,
292 confirmed_anchor_literals,
293 &generic_keyword_literals,
294 )
295 } else {
296 None
297 };
298 #[cfg(not(feature = "gpu"))]
299 let gpu_literals: Option<Arc<Vec<Vec<u8>>>> = None;
300 #[cfg(feature = "gpu")]
301 let gpu_max_literal_len = gpu_literals.as_ref().map_or(0, |literals| {
302 literals
303 .iter()
304 .fold(0, |longest, literal| longest.max(literal.len()))
305 });
306
307 let phase2_always_active_prefilter = phase2::Phase2AlwaysActivePrefilter::build(
312 &state.phase2_patterns,
313 &phase2_always_active_indices,
314 phase2_anchor_index.as_ref(),
315 );
316 tracing::debug!(
317 eligible = phase2_anchor_index
318 .as_ref()
319 .map_or(0, |i| i.eligible_count()),
320 total = state.phase2_patterns.len(),
321 always_active = phase2_always_active_indices.len(),
322 "phase-2 prefilter built with homoglyph ASCII-folded fast path"
323 );
324
325 tracing::debug!(
326 gated,
327 anchored = confirmed_anchor_index
328 .as_ref()
329 .map_or(0, |index| index.eligible_count()),
330 total = state.ac_map.len(),
331 "confirmed suffix/anchor gates built"
332 );
333
334 log_quality_warnings(&state.quality_warnings);
335
336 let mut alphabet_targets = state.ac_literals.clone();
337 let extra_keyword_count: usize = state
343 .phase2_patterns
344 .iter()
345 .map(|(_, keywords)| keywords.len())
346 .sum();
347 alphabet_targets.reserve(extra_keyword_count);
348 for (_, keywords) in &state.phase2_patterns {
349 alphabet_targets.extend(keywords.iter().cloned());
350 }
351 let alphabet_screen = if alphabet_targets.is_empty() {
352 None
353 } else {
354 Some(crate::alphabet_filter::AlphabetScreen::new(
355 &alphabet_targets,
356 ))
357 };
358
359 let bigram_bloom =
363 crate::bigram_bloom::BigramBloom::from_literal_prefixes(&state.ac_literals);
364 tracing::debug!(
365 popcount = bigram_bloom.popcount(),
366 "selective literal-anchor bloom built (65536 slots / 8 KB)"
367 );
368
369 let missing_weak_anchor_floors = detectors
373 .iter()
374 .enumerate()
375 .filter_map(|(index, detector)| {
376 let has_weak_pattern = match detector_plans.get(index).weak_anchor_base {
377 crate::suppression::WeakAnchorBase::Always => true,
378 crate::suppression::WeakAnchorBase::PerPattern => {
379 detector.patterns.iter().any(|pattern| pattern.weak_anchor)
380 }
381 crate::suppression::WeakAnchorBase::Never => false,
382 };
383 (has_weak_pattern && detector_plans.get(index).entropy_floor.is_none())
384 .then_some(detector.id.as_str())
385 })
386 .collect::<Vec<_>>();
387 if !missing_weak_anchor_floors.is_empty() {
388 return Err(crate::error::ScanError::Config(format!(
389 "weak-anchor detectors omit detector-local entropy_high/entropy_floor policy: {}",
390 missing_weak_anchor_floors.join(", ")
391 )));
392 }
393 #[cfg(feature = "simdsieve")]
397 let hot_pattern_slots = build_hot_pattern_slots(&detectors, &state.ac_map)?;
398 #[cfg(feature = "simdsieve")]
399 let hot_confirmed_by_pattern = {
400 let mut hot = vec![false; state.ac_map.len()];
401 for slot in &hot_pattern_slots {
402 hot[slot.ac_map_index] = true;
403 }
404 hot
405 };
406 #[cfg(not(feature = "simdsieve"))]
407 let hot_confirmed_by_pattern = vec![false; state.ac_map.len()];
408
409 let pattern_boundary_context = derive_pattern_boundary_context(
410 state
411 .ac_map
412 .iter()
413 .chain(state.phase2_patterns.iter().map(|(pattern, _)| pattern)),
414 );
415 #[cfg(feature = "gpu")]
416 let ac_match_upper_bounds: Vec<Option<usize>> = state
417 .ac_map
418 .iter()
419 .map(|pattern| regex_match_byte_upper_bound(pattern.regex.as_str()))
420 .collect();
421
422 let mut structural_confirmed_patterns = vec![Vec::new(); detectors.len()];
423 for (pattern_index, pattern) in state.ac_map.iter().enumerate() {
424 if pattern.structural_password_slot {
425 structural_confirmed_patterns[pattern.detector_index].push(pattern_index);
426 }
427 }
428 let mut structural_phase2_patterns = vec![Vec::new(); detectors.len()];
429 for (pattern_index, (pattern, _)) in state.phase2_patterns.iter().enumerate() {
430 if pattern.structural_password_slot {
431 structural_phase2_patterns[pattern.detector_index].push(pattern_index);
432 }
433 }
434 let scanner = Self {
435 detector_digest,
436 ac,
437 gpu_backends,
438 gpu_acquisition_failures,
439 gpu_literals,
440 #[cfg(feature = "gpu")]
441 gpu_max_literal_len,
442 gpu_matcher: OnceLock::new(),
443 #[cfg(feature = "gpu")]
444 gpu_resident_literal_cuda: std::sync::Mutex::new(GpuResidentLiteralSlot::Empty),
445 #[cfg(feature = "gpu")]
446 gpu_resident_literal_metal: std::sync::Mutex::new(GpuResidentLiteralSlot::Empty),
447 #[cfg(feature = "gpu")]
448 gpu_resident_literal_wgpu: std::sync::Mutex::new(GpuResidentLiteralSlot::Empty),
449 gpu_last_degrade_reason: std::sync::Mutex::new(None),
450 gpu_degrade_count: std::sync::atomic::AtomicU64::new(0),
451 autoroute_gpu_shared_cold_ns: std::sync::atomic::AtomicU64::new(0),
452 static_intern,
453 detector_plans,
454 assignment_keyword_matcher: std::sync::Mutex::new(
455 crate::assignment_keyword_matcher::AssignmentKeywordMatcherCache::default(),
456 ),
457 #[cfg(feature = "gpu")]
458 ac_match_upper_bounds,
459 suffix_gate_ac,
460 ac_suffix_gate,
461 hot_confirmed_by_pattern,
462 confirmed_anchor_index,
463 ac_map: state.ac_map,
464 pattern_boundary_context,
465 prefix_propagation,
466 phase2_patterns: state.phase2_patterns,
467 structural_confirmed_patterns,
468 structural_phase2_patterns,
469 same_prefix_patterns,
470 phase2_keyword_ac,
471 phase2_keyword_to_patterns,
472 phase2_keyword_count,
473 phase2_always_anchor_literal_count,
474 #[cfg(feature = "gpu")]
475 confirmed_anchor_literal_count,
476 #[cfg(feature = "gpu")]
477 generic_keyword_literal_count,
478 phase2_always_active_indices,
479 phase2_always_active_prefilter,
480 phase2_anchor_index,
481 #[cfg(feature = "gpu")]
482 phase2_gpu_dfa: Phase2GpuDfaCatalogCache::default(),
483 tuning: phase2::ScannerTuning::from_defaults(),
484 #[cfg(feature = "simd")]
485 simd_candidate_available,
486 #[cfg(feature = "simd")]
487 simd_compile_plan: std::sync::Mutex::new(simd_compile_plan),
488 #[cfg(feature = "simd")]
489 simd_prefilter: std::sync::OnceLock::new(),
490 #[cfg(feature = "simd")]
491 simd_initialization_ns: std::sync::atomic::AtomicU64::new(0),
492 #[cfg(feature = "simdsieve")]
493 hot_pattern_slots,
494 config: ScannerConfig::default(),
495 alphabet_screen,
496 bigram_bloom,
497 fragment_cache: crate::fragment_cache::FragmentCache::new(1000),
498 };
499
500 Ok(scanner)
501 }
502
503 pub fn with_config(mut self, config: ScannerConfig) -> Self {
505 profile::set_profile_enabled(config.profile);
506 profile::set_perf_trace_enabled(config.perf_trace);
507 self.config = config;
508 self
509 }
510
511 pub fn with_tuning_config(self, config: ScannerTuningConfig) -> Self {
513 self.tuning.apply_config(&config);
514 self
515 }
516}
517
518#[cfg(all(target_os = "linux", feature = "gpu"))]
519fn linux_cuda_runtime_identity() -> std::result::Result<String, String> {
520 let version = std::fs::read_to_string("/proc/driver/nvidia/version")
521 .map_err(|error| format!("cannot read /proc/driver/nvidia/version: {error}"))?;
522 let version = version.split_whitespace().collect::<Vec<_>>().join(" ");
523 if version.is_empty() {
524 Err("/proc/driver/nvidia/version contains no runtime identity".to_owned())
525 } else {
526 Ok(format!("nvidia-kernel:{version}"))
527 }
528}