use crate::args::BackendArgs;
use anyhow::Result;
use keyhog_scanner::hw_probe::{
classify_gpu_tier, gpu_min_bytes_for_tier, gpu_solo_bytes_for_tier, probe_hardware,
select_backend, thresholds, GpuTier, ScanBackend,
};
use std::process::ExitCode;
const EXIT_SELF_TEST_FAILED: u8 = 4;
pub fn run(args: BackendArgs) -> Result<ExitCode> {
if args.self_test {
return run_self_test();
}
print_backend_report(&args)?;
Ok(ExitCode::SUCCESS)
}
fn print_backend_report(args: &BackendArgs) -> Result<()> {
let hw = probe_hardware();
println!("## hardware");
println!(" physical_cores: {}", hw.physical_cores);
println!(" logical_cores: {}", hw.logical_cores);
println!(
" simd: {}",
if hw.has_avx512 {
"AVX-512"
} else if hw.has_avx2 {
"AVX2"
} else if hw.has_neon {
"NEON"
} else {
"scalar"
}
);
println!(
" gpu: {} {}",
if hw.gpu_available {
hw.gpu_name.as_deref().unwrap_or("yes")
} else {
"not detected"
},
if hw.gpu_is_software {
"(software renderer: disabled)"
} else {
""
}
);
if let Some(buf) = hw.gpu_vram_mb {
if buf >= 1024 {
println!(" gpu_max_buffer: {} GB", buf / 1024);
} else {
println!(" gpu_max_buffer: {buf} MB");
}
}
if let Some(mem) = hw.total_memory_mb {
println!(" total_memory: {mem} MB");
}
println!(
" hyperscan: {}",
if hw.hyperscan_available {
"compiled-in"
} else {
"absent"
}
);
println!(
" io_uring: {}",
if hw.io_uring_available {
"available"
} else {
"n/a"
}
);
if let Ok(forced) = std::env::var("KEYHOG_BACKEND") {
println!();
println!("## env override");
println!(" KEYHOG_BACKEND={forced}");
}
let pat = args.patterns;
println!();
println!("## routing decision matrix (pattern_count = {pat})");
let active_tier = classify_gpu_tier(hw.gpu_name.as_deref());
let active_min = gpu_min_bytes_for_tier(active_tier);
let active_solo = gpu_solo_bytes_for_tier(active_tier);
let scenarios: &[(u64, &str)] = &[
(0, "idle (size=0)"),
(4 * 1024, "4 KiB single chunk"),
(1024 * 1024, "1 MiB chunk"),
(2 * 1024 * 1024, "2 MiB chunk (high-tier min)"),
(4 * 1024 * 1024, "4 MiB chunk"),
(
16 * 1024 * 1024,
"16 MiB chunk (high-tier solo / mid-tier min)",
),
(active_min.saturating_sub(1), "just under tier min_bytes"),
(active_min, "tier min_bytes exactly"),
(active_solo.saturating_sub(1), "just under tier solo cap"),
(active_solo, "tier solo cap exactly"),
(1024 * 1024 * 1024, "1 GiB single chunk"),
];
for (bytes, label) in scenarios {
let backend = select_backend(hw, *bytes, pat);
println!(" {:<42} → {}", label, backend.label());
}
if let Some(bytes) = args.probe_bytes {
println!();
let backend = select_backend(hw, bytes, pat);
println!("## --probe-bytes {bytes}");
println!(" → {}", backend.label());
}
println!();
println!("## gpu tier (heuristic from adapter name)");
let tier = classify_gpu_tier(hw.gpu_name.as_deref());
let tier_label = match tier {
GpuTier::High => "High (RTX 40/50, A100/H100, M-Max)",
GpuTier::Mid => "Mid (RTX 20/30, GTX 16, Arc, M-Pro/base)",
GpuTier::Low => "Low / unknown",
};
println!(" classified: {tier_label}");
println!(
" effective min bytes: {} (tier {:?})",
fmt_bytes(gpu_min_bytes_for_tier(tier)),
tier
);
println!(
" effective solo cap: {}",
fmt_bytes(gpu_solo_bytes_for_tier(tier))
);
println!();
println!("## thresholds (per-tier table)");
println!(
" high tier min/solo = {} / {}",
fmt_bytes(thresholds::GPU_MIN_BYTES_HIGH_TIER),
fmt_bytes(thresholds::GPU_BYTES_BREAKEVEN_SOLO_HIGH_TIER)
);
println!(
" mid tier min/solo = {} / {}",
fmt_bytes(thresholds::GPU_MIN_BYTES_MID_TIER),
fmt_bytes(thresholds::GPU_BYTES_BREAKEVEN_SOLO_MID_TIER)
);
println!(
" low tier min/solo = {} / {}",
fmt_bytes(thresholds::GPU_MIN_BYTES),
fmt_bytes(thresholds::GPU_BYTES_BREAKEVEN_SOLO)
);
println!(
" GPU_PATTERN_BREAKEVEN = {} patterns",
thresholds::GPU_PATTERN_BREAKEVEN
);
println!();
let cur = ScanBackend::Gpu.label();
println!(
"Force a backend with: KEYHOG_BACKEND={{gpu|simd|cpu}} (or `keyhog scan --backend ...`)"
);
let _ = cur;
Ok(())
}
fn run_self_test() -> Result<ExitCode> {
println!("## GPU self-test");
let hw = probe_hardware();
if !hw.gpu_available || hw.gpu_is_software {
let reason = if !hw.gpu_available {
"no GPU adapter detected"
} else {
"only software adapter (llvmpipe/lavapipe/swiftshader): won't be used for scans"
};
println!(" \x1b[33mSKIP\x1b[0m: {reason}");
return Ok(ExitCode::SUCCESS);
}
let mut all_ok = true;
print!(" moe_kernel ... ");
match keyhog_scanner::gpu::gpu_self_test() {
Ok(report) => println!(
"\x1b[32mPASS\x1b[0m ({}, scores={}, max_buffer={} MB)",
report.adapter_name,
report.scores,
report.vram_mb.unwrap_or(0)
),
Err(error) => {
println!("\x1b[31mFAIL\x1b[0m {error}");
all_ok = false;
}
}
print!(" vyre_literal_set ... ");
match keyhog_scanner::gpu::vyre_gpu_self_test() {
Ok(report) => println!(
"\x1b[32mPASS\x1b[0m (direct={}, coalesced={})",
report.direct_matches, report.coalesced_matches
),
Err(error) => {
let known_lowering_gap = error.contains("_vyre_match_leader")
|| error.contains("canonical pre-emit lowering")
|| error.contains("subgroup_ballot");
if known_lowering_gap {
println!(
"\x1b[33mKNOWN\x1b[0m vyre IR lowering rejects literal_set's subgroup form; \
scans use the AC kernel instead (works on this box)."
);
} else {
println!("\x1b[31mFAIL\x1b[0m {error}");
all_ok = false;
}
}
}
print!(" vyre_ac_kernel ... ");
match keyhog_scanner::gpu::vyre_ac_kernel_self_test() {
Ok(report) => println!(
"\x1b[32mPASS\x1b[0m (matches={}, backend={})",
report.matches, report.backend_id
),
Err(error) => {
println!("\x1b[31mFAIL\x1b[0m {error}");
all_ok = false;
}
}
println!();
if all_ok {
println!("\x1b[32m✓ GPU self-test passed\x1b[0m, scans on this box can route to GPU.");
Ok(ExitCode::SUCCESS)
} else {
eprintln!(
"\x1b[31m✗ GPU self-test failed\x1b[0m, keyhog will fall back to SIMD/CPU on this box."
);
Ok(ExitCode::from(EXIT_SELF_TEST_FAILED))
}
}
fn fmt_bytes(n: u64) -> String {
if n >= 1024 * 1024 * 1024 {
format!("{} GiB", n / (1024 * 1024 * 1024))
} else if n >= 1024 * 1024 {
format!("{} MiB", n / (1024 * 1024))
} else if n >= 1024 {
format!("{} KiB", n / 1024)
} else {
format!("{n} B")
}
}