#[cfg(test)]
use self::batch::ZeroPhase2GpuDfaScratch;
use self::batch::{
build_packed_region_batch, build_packed_region_batch_refs, with_phase2_gpu_dfa_scratch,
Phase2GpuDfaScratch,
};
use self::candidates::{ascii_phase2_gpu_dfa_candidates, prefixless_always_active_candidates};
use self::lowering::build_shards_recursive;
#[cfg(test)]
use self::lowering::regex_dfa_source_for_pattern;
pub(super) use self::shard::match_region;
use self::shard::Phase2GpuDfaShard;
#[cfg(test)]
pub(super) use self::workload::build_phase2_gpu_admission_workload;
pub(super) use self::workload::Phase2GpuAdmissionWorkload;
pub(crate) use self::workload::Phase2GpuDfaAdmission;
pub(super) use self::workload::{
build_phase2_gpu_admission_workload_filtered, expand_phase2_gpu_admission,
validate_phase2_gpu_trigger_rows,
};
use super::*;
use std::sync::OnceLock;
mod batch;
mod candidates;
mod lowering;
mod resident;
mod shard;
mod workload;
const PHASE2_GPU_DFA_MAX_STATES: usize = 16_384;
fn report_phase2_gpu_catalog_loss(reason: impl std::fmt::Display) {
let reason = reason.to_string();
static PHASE2_GPU_CATALOG_LOSS_WARNED: OnceLock<()> = OnceLock::new();
if PHASE2_GPU_CATALOG_LOSS_WARNED.set(()).is_ok() {
eprintln!(
"keyhog: phase-2 GPU regex-DFA catalog incomplete ({reason}); CPU admission remains \
authoritative for uncovered patterns. GPU speed evidence is incomplete."
);
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum Phase2GpuDfaProgramKind {
CudaCompatible,
SubgroupCoalesced,
}
impl Phase2GpuDfaProgramKind {
fn for_backend_id(backend_id: Option<&str>) -> Self {
if backend_id == Some("cuda") {
Self::CudaCompatible
} else {
Self::SubgroupCoalesced
}
}
const fn use_subgroup_coalesce(self) -> bool {
matches!(self, Self::SubgroupCoalesced)
}
}
#[derive(Debug)]
pub(crate) struct Phase2GpuDfaCatalog {
shards: Vec<Phase2GpuDfaShard>,
uncovered_ascii_patterns: usize,
excluded_ascii_redundant_patterns: usize,
resident: resident::Phase2GpuDfaCatalogResident,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) struct Phase2GpuDfaCoverage {
pub(crate) covered_ascii_patterns: usize,
pub(crate) uncovered_ascii_patterns: usize,
pub(crate) excluded_ascii_redundant_patterns: usize,
pub(crate) shards: usize,
}
#[derive(Debug, Default)]
pub(crate) struct Phase2GpuDfaCatalogCache {
catalog: OnceLock<Option<Phase2GpuDfaCatalog>>,
preparation_ns: std::sync::atomic::AtomicU64,
}
impl Phase2GpuDfaCatalog {
pub(crate) fn coverage(&self) -> Phase2GpuDfaCoverage {
Phase2GpuDfaCoverage {
covered_ascii_patterns: self
.shards
.iter()
.map(|shard| shard.phase2_indices.len())
.sum(),
uncovered_ascii_patterns: self.uncovered_ascii_patterns,
excluded_ascii_redundant_patterns: self.excluded_ascii_redundant_patterns,
shards: self.shards.len(),
}
}
fn build(
phase2_patterns: &[(CompiledPattern, Vec<String>)],
always_active_indices: &[usize],
program_kind: Phase2GpuDfaProgramKind,
) -> Option<Self> {
let all_candidates =
prefixless_always_active_candidates(phase2_patterns, always_active_indices);
let candidates = ascii_phase2_gpu_dfa_candidates(phase2_patterns, &all_candidates);
Self::build_from_selected_candidates(
phase2_patterns,
candidates.len(),
all_candidates.len().saturating_sub(candidates.len()),
&candidates,
program_kind,
)
}
fn build_from_selected_candidates(
phase2_patterns: &[(CompiledPattern, Vec<String>)],
ascii_candidate_count: usize,
excluded_ascii_redundant_patterns: usize,
candidates: &[usize],
program_kind: Phase2GpuDfaProgramKind,
) -> Option<Self> {
if candidates.is_empty() {
return (ascii_candidate_count == 0).then_some(Self {
shards: Vec::new(),
uncovered_ascii_patterns: 0,
excluded_ascii_redundant_patterns,
resident: resident::Phase2GpuDfaCatalogResident::default(),
});
}
let mut shards = Vec::new();
let mut uncovered_ascii_patterns = ascii_candidate_count.saturating_sub(candidates.len());
build_shards_recursive(
phase2_patterns,
candidates,
program_kind.use_subgroup_coalesce(),
&mut shards,
&mut uncovered_ascii_patterns,
);
let covered_patterns: usize = shards.iter().map(|shard| shard.phase2_indices.len()).sum();
if shards.is_empty() {
tracing::warn!(
target: "keyhog::gpu",
candidates = ascii_candidate_count,
"phase-2 GPU regex-DFA admission has no lowerable ASCII prefixless always-active pattern; CPU admission remains authoritative"
);
report_phase2_gpu_catalog_loss(format!(
"no lowerable ASCII prefixless always-active pattern among {ascii_candidate_count} candidate(s)"
));
return None;
}
if uncovered_ascii_patterns > 0 {
tracing::warn!(
target: "keyhog::gpu",
covered = covered_patterns,
uncovered = uncovered_ascii_patterns,
"phase-2 GPU regex-DFA admission has uncovered ASCII prefixless pattern(s); GPU hits can admit chunks, misses still consult CPU admission"
);
report_phase2_gpu_catalog_loss(format!(
"{uncovered_ascii_patterns} ASCII prefixless always-active pattern(s) uncovered after lowering"
));
}
tracing::debug!(
target: "keyhog::gpu",
shards = shards.len(),
covered = covered_patterns,
uncovered_ascii = uncovered_ascii_patterns,
excluded_ascii_redundant = excluded_ascii_redundant_patterns,
program = "region-admission",
"phase-2 GPU regex-DFA ASCII admission catalog built"
);
Some(Self {
shards,
uncovered_ascii_patterns,
excluded_ascii_redundant_patterns,
resident: resident::Phase2GpuDfaCatalogResident::default(),
})
}
#[cfg(test)]
fn single_shard_catalogs_for_test(&self) -> Vec<Self> {
self.shards
.iter()
.cloned()
.map(|shard| Self {
shards: vec![shard],
uncovered_ascii_patterns: self.uncovered_ascii_patterns,
excluded_ascii_redundant_patterns: self.excluded_ascii_redundant_patterns,
resident: resident::Phase2GpuDfaCatalogResident::default(),
})
.collect()
}
pub(crate) fn scan_admission_refs(
&self,
backend: &std::sync::Arc<dyn vyre::VyreBackend>,
chunks: &[&keyhog_core::Chunk],
) -> std::result::Result<Phase2GpuDfaAdmission, String> {
self.scan_admission_with_builder(backend, chunks.len(), |scratch| {
build_packed_region_batch_refs(chunks, scratch)
})
}
pub(crate) fn scan_admission_chunks(
&self,
backend: &std::sync::Arc<dyn vyre::VyreBackend>,
chunks: &[keyhog_core::Chunk],
) -> std::result::Result<Phase2GpuDfaAdmission, String> {
self.scan_admission_with_builder(backend, chunks.len(), |scratch| {
build_packed_region_batch(chunks, scratch)
})
}
fn scan_admission_with_builder<F>(
&self,
backend: &std::sync::Arc<dyn vyre::VyreBackend>,
chunk_count: usize,
build_batch: F,
) -> std::result::Result<Phase2GpuDfaAdmission, String>
where
F: FnOnce(&mut Phase2GpuDfaScratch) -> std::result::Result<(), String>,
{
if chunk_count == 0 || self.shards.is_empty() {
return Ok(Phase2GpuDfaAdmission {
admitted: vec![false; chunk_count],
complete: vec![true; chunk_count],
matches_seen: 0,
});
}
with_phase2_gpu_dfa_scratch(|scratch| {
build_batch(scratch)?;
self.scan_admission_with_scratch(backend, scratch, chunk_count)
})
}
fn scan_admission_with_scratch(
&self,
backend: &std::sync::Arc<dyn vyre::VyreBackend>,
scratch: &mut Phase2GpuDfaScratch,
chunk_count: usize,
) -> std::result::Result<Phase2GpuDfaAdmission, String> {
use vyre_libs::scan::dispatch_io;
let haystack_len = u32::try_from(scratch.haystack_len).map_err(|error| {
format!(
"phase2_gpu_regex_dfa haystack is {} byte(s), above the u32 GPU ABI: {error}",
scratch.haystack_len
)
})?;
if haystack_len > dispatch_io::DEFAULT_MAX_SCAN_BYTES {
return Err(format!(
"phase2_gpu_regex_dfa scan-guard ceiling exceeded: {} byte(s) > {} byte(s). Fix: split the scan before dispatch.",
haystack_len,
dispatch_io::DEFAULT_MAX_SCAN_BYTES
));
}
let mut admitted = vec![false; chunk_count];
let complete = vec![self.uncovered_ascii_patterns == 0; chunk_count];
let evidence_seen = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
self.resident
.scan(&self.shards, backend, scratch, haystack_len, &mut admitted)
}))
.map_err(|panic| {
format!(
"phase-2 GPU resident admission panicked: {}. Fix: repair the selected GPU driver/runtime and recalibrate autoroute",
crate::error::panic_payload_detail(panic)
)
})??;
Ok(Phase2GpuDfaAdmission {
admitted,
complete,
matches_seen: evidence_seen,
})
}
}
impl Phase2GpuDfaCatalogCache {
pub(crate) fn catalog(
&self,
phase2_patterns: &[(CompiledPattern, Vec<String>)],
always_active_indices: &[usize],
backend_id: Option<&'static str>,
) -> Option<&Phase2GpuDfaCatalog> {
self.catalog
.get_or_init(|| {
let started = std::time::Instant::now();
let catalog = Phase2GpuDfaCatalog::build(
phase2_patterns,
always_active_indices,
Phase2GpuDfaProgramKind::for_backend_id(backend_id),
);
let elapsed_ns =
(started.elapsed().as_nanos().min(u128::from(u64::MAX)) as u64).max(1);
self.preparation_ns
.store(elapsed_ns, std::sync::atomic::Ordering::Release);
catalog
})
.as_ref()
}
pub(crate) fn preparation_ns(&self, _backend_id: Option<&'static str>) -> u128 {
self.preparation_ns
.load(std::sync::atomic::Ordering::Acquire) as u128
}
}
#[cfg(test)]
#[path = "../../tests/unit/engine_phase2_gpu_dfa.rs"]
mod tests;