#[path = "gpu_shader.rs"]
mod gpu_shader;
#[path = "gpu_moe_backend.rs"]
mod backend;
pub fn batch_ml_inference(
candidates: &[(&str, &str)],
config: &crate::types::ScannerConfig,
) -> Vec<f64> {
if candidates.is_empty() {
return Vec::new();
}
#[cfg(feature = "ml")]
{
use rayon::prelude::*;
let features: Vec<[f32; crate::ml_scorer::NUM_FEATURES]> = candidates
.par_iter()
.map(|(text, ctx)| {
crate::ml_scorer::compute_features_with_config(
text,
ctx,
&config.known_prefixes,
&config.secret_keywords,
&config.test_keywords,
&config.placeholder_keywords,
)
})
.collect();
if let Some(scores) = backend::batch_score_features(&features) {
return scores;
}
candidates
.par_iter()
.map(|(text, ctx)| {
crate::ml_scorer::score_with_config(
text,
ctx,
&config.known_prefixes,
&config.secret_keywords,
&config.test_keywords,
&config.placeholder_keywords,
)
})
.collect()
}
#[cfg(not(feature = "ml"))]
{
let _ = candidates;
let _ = config;
Vec::new()
}
}
pub fn gpu_available() -> bool {
backend::get_gpu().is_some()
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GpuSelfTest {
pub adapter_name: String,
pub vram_mb: Option<u64>,
pub scores: usize,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct VyreGpuSelfTest {
pub direct_matches: usize,
pub coalesced_matches: usize,
}
static GPU_SELF_TEST_CACHE: std::sync::OnceLock<std::result::Result<GpuSelfTest, String>> =
std::sync::OnceLock::new();
pub fn gpu_self_test() -> Result<GpuSelfTest, String> {
GPU_SELF_TEST_CACHE
.get_or_init(|| {
const SELF_TEST_BATCH: usize = 64;
let gpu = backend::get_gpu().ok_or_else(|| {
"GPU adapter unavailable; install or enable a non-software GPU adapter and driver"
.to_string()
})?;
let features = [[0.0_f32; crate::ml_scorer::NUM_FEATURES]; SELF_TEST_BATCH];
let scores = backend::batch_score_features(&features)
.ok_or_else(|| "GPU dispatch produced no result".to_string())?;
if scores.len() != SELF_TEST_BATCH {
return Err(format!(
"GPU dispatch returned {} scores for {SELF_TEST_BATCH} inputs",
scores.len()
));
}
if let Some((index, score)) = scores
.iter()
.enumerate()
.find(|(_, score)| !score.is_finite() || !(0.0..=1.0).contains(*score))
{
return Err(format!(
"GPU dispatch returned invalid score {score} at index {index}"
));
}
Ok(GpuSelfTest {
adapter_name: gpu.gpu_name().to_string(),
vram_mb: gpu.vram_mb(),
scores: scores.len(),
})
})
.clone()
}
pub fn vyre_gpu_self_test() -> Result<VyreGpuSelfTest, String> {
use vyre_driver_wgpu::WgpuBackend;
use vyre_libs::scan::GpuLiteralSet;
let patterns: Vec<Vec<u8>> = vec![b"needle".to_vec()];
let pattern_refs: Vec<&[u8]> = patterns.iter().map(Vec::as_slice).collect();
let backend = WgpuBackend::shared().map_err(|e| format!("failed to init wgpu backend: {e}"))?;
let scanner = GpuLiteralSet::compile(&pattern_refs);
let direct = scanner
.scan(backend.as_ref(), b"needle", 100)
.map_err(|error| format!("vyre direct GPU scan failed: {error}"))?;
if direct.len() != 1 || direct[0].pattern_id != 0 || direct[0].start != 0 {
return Err(format!(
"vyre direct GPU scan returned unexpected matches: {direct:?}"
));
}
let items: Vec<Vec<u8>> = (0..100)
.map(|index| format!("id-{index:03}-needle").into_bytes())
.collect();
let mut buffer = Vec::with_capacity(items.iter().map(Vec::len).sum());
for item in &items {
buffer.extend_from_slice(item);
}
let coalesced = scanner
.scan(backend.as_ref(), &buffer, 10_000)
.map_err(|error| format!("vyre coalesced GPU scan failed: {error}"))?;
Ok(VyreGpuSelfTest {
direct_matches: direct.len(),
coalesced_matches: coalesced.len(),
})
}
pub struct VyreAcKernelSelfTest {
pub matches: usize,
pub backend_id: &'static str,
}
pub fn vyre_ac_kernel_self_test() -> Result<VyreAcKernelSelfTest, String> {
use crate::engine::{CompiledScanner, GpuPhase1Output};
use keyhog_core::{Chunk, ChunkMetadata, DetectorSpec, PatternSpec, Severity};
let detector = DetectorSpec {
id: "kh-gpu-self-test".into(),
name: "GPU self-test".into(),
service: "test".into(),
severity: Severity::Low,
patterns: vec![PatternSpec {
regex: "needle".into(),
description: None,
group: None,
client_safe: false,
}],
keywords: vec!["needle".into()],
..Default::default()
};
let scanner = CompiledScanner::compile(vec![detector])
.map_err(|e| format!("CompiledScanner::compile failed during self-test: {e}"))?;
let backend_id = scanner
.gpu_backend_label()
.ok_or_else(|| "no GPU backend acquired during self-test compile".to_string())?;
let chunk = Chunk {
data: "the quick brown needle jumps over the lazy fox".into(),
metadata: ChunkMetadata::default(),
};
match scanner.scan_coalesced_gpu_ac_phase1(&[chunk]) {
GpuPhase1Output::Hits(hits) => {
let total: usize = hits.iter().map(Vec::len).sum();
if total == 0 {
return Err(
"AC kernel ran on GPU but reported zero hits for the planted 'needle' \
literal. Indicates either a phase-1 lowering regression or a workgroup-size mismatch."
.to_string(),
);
}
Ok(VyreAcKernelSelfTest {
matches: total,
backend_id,
})
}
GpuPhase1Output::Done(_) => Err(
"AC phase 1 returned Done (CPU fallback) instead of Hits. The GPU AC \
program, the GPU matcher, or the GPU backend wasn't available at scan time even \
though compile said one was acquired."
.to_string(),
),
}
}
#[must_use]
pub fn gpu_probe() -> (bool, Option<String>, Option<u64>) {
if env_no_gpu() {
return (false, None, None);
}
if let Some(gpu) = backend::get_gpu() {
return (true, Some(gpu.gpu_name().to_string()), gpu.vram_mb());
}
(false, None, None)
}
pub fn env_no_gpu() -> bool {
if let Ok(v) = std::env::var("KEYHOG_NO_GPU") {
return !matches!(v.as_str(), "" | "0" | "false" | "FALSE" | "off" | "OFF");
}
is_ci_environment()
}
pub fn is_ci_environment() -> bool {
if let Ok(v) = std::env::var("CI") {
if !matches!(v.as_str(), "" | "0" | "false" | "FALSE" | "off" | "OFF") {
return true;
}
}
const CI_MARKERS: &[&str] = &[
"GITHUB_ACTIONS", "GITLAB_CI", "JENKINS_URL", "TF_BUILD", "TEAMCITY_VERSION", "BITBUCKET_BUILD_NUMBER", "BUILDKITE", "CIRCLECI", "DRONE", "TRAVIS", "APPVEYOR", "CODEBUILD_BUILD_ID", "WERCKER", "SEMAPHORE", ];
CI_MARKERS.iter().any(|k| std::env::var(k).is_ok())
}