use super::gpu_shader::moe_shader;
use bytemuck::{Pod, Zeroable};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::mpsc::TryRecvError;
use std::sync::OnceLock;
use std::time::{Duration, Instant};
use wgpu::util::DeviceExt;
use crate::ml_scorer::GPU_BATCH_THRESHOLD;
const INPUT_DIM: usize = crate::ml_scorer::NUM_FEATURES;
const GPU_READBACK_SPIN_LIMIT: u32 = 32;
const GPU_READBACK_YIELD_LIMIT: u32 = 64;
const GPU_READBACK_INITIAL_SLEEP_US: u64 = 2;
const GPU_READBACK_MAX_SLEEP_US: u64 = 256;
#[derive(Clone, Copy, Pod, Zeroable)]
#[repr(C)]
struct GpuParams {
batch_size: u32,
_pad: [u32; 3],
}
pub(crate) struct GpuContext {
device_queue: std::sync::Arc<(wgpu::Device, wgpu::Queue)>,
adapter_info: wgpu::AdapterInfo,
device_limits: wgpu::Limits,
pipeline: wgpu::ComputePipeline,
weights_buf: wgpu::Buffer,
bind_group_layout: wgpu::BindGroupLayout,
}
impl GpuContext {
pub(crate) fn vram_mb(&self) -> Option<u64> {
const SANE_CAP_MB: u64 = 256 * 1024;
Some((self.device_limits.max_buffer_size / (1024 * 1024)).min(SANE_CAP_MB))
}
pub(crate) fn gpu_name(&self) -> &str {
&self.adapter_info.name
}
#[inline]
fn device(&self) -> &wgpu::Device {
&self.device_queue.0
}
#[inline]
fn queue(&self) -> &wgpu::Queue {
&self.device_queue.1
}
}
static GPU: OnceLock<Option<GpuContext>> = OnceLock::new();
struct ReadbackWaitBackoff {
iterations: u32,
sleep_us: u64,
}
impl ReadbackWaitBackoff {
fn new() -> Self {
Self {
iterations: 0,
sleep_us: GPU_READBACK_INITIAL_SLEEP_US,
}
}
fn wait(&mut self, remaining: Duration) {
self.iterations = self.iterations.saturating_add(1);
if self.iterations <= GPU_READBACK_SPIN_LIMIT {
std::hint::spin_loop();
return;
}
if self.iterations <= GPU_READBACK_YIELD_LIMIT {
std::thread::yield_now();
return;
}
let sleep = Duration::from_micros(self.sleep_us).min(remaining);
if !sleep.is_zero() {
std::thread::sleep(sleep);
}
self.sleep_us = self
.sleep_us
.saturating_mul(2)
.min(GPU_READBACK_MAX_SLEEP_US);
}
}
struct GpuInitError {
adapter_present: bool,
detail: Box<dyn std::error::Error + Send + Sync>,
}
impl GpuInitError {
fn no_adapter(detail: impl Into<Box<dyn std::error::Error + Send + Sync>>) -> Self {
Self {
adapter_present: false,
detail: detail.into(),
}
}
fn adapter_unusable(detail: impl Into<Box<dyn std::error::Error + Send + Sync>>) -> Self {
Self {
adapter_present: true,
detail: detail.into(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum GpuInitFailureAction {
HardFail,
WarnCpuFallback,
Quiet,
}
fn classify_gpu_init_failure(
err: &GpuInitError,
disabled: bool,
required: bool,
) -> GpuInitFailureAction {
if required {
return GpuInitFailureAction::HardFail;
}
if !disabled && err.adapter_present {
return GpuInitFailureAction::WarnCpuFallback;
}
GpuInitFailureAction::Quiet
}
fn on_gpu_init_failed(err: &GpuInitError, disabled: bool, required: bool) -> Option<GpuContext> {
match classify_gpu_init_failure(err, disabled, required) {
GpuInitFailureAction::HardFail => {
crate::process_exit::require_gpu_unmet(format!(
"--require-gpu requested but GPU MoE init failed: {}",
err.detail
));
}
GpuInitFailureAction::WarnCpuFallback => {
eprintln!(
"keyhog: a GPU was detected but could not be initialized; using the \
CPU/SIMD scan path. Use --no-gpu to silence this, or --require-gpu to fail instead."
);
}
GpuInitFailureAction::Quiet => {}
}
tracing::debug!("GPU MoE init failed, using CPU fallback: {}", err.detail);
None
}
fn init_gpu() -> Result<GpuContext, GpuInitError> {
let vyre_backend = vyre_driver_wgpu::WgpuBackend::shared()
.map_err(|e| GpuInitError::no_adapter(format!("vyre WgpuBackend unavailable: {e}")))?;
let adapter_info = vyre_backend.adapter_info().clone();
if super::is_software_adapter(&adapter_info) {
return Err(GpuInitError::no_adapter(format!(
"GPU adapter is a software fallback ({} on {:?}); refusing to use",
adapter_info.name, adapter_info.backend
)));
}
let device_limits = vyre_backend.device_limits().clone();
let dq = vyre_backend.device_queue();
let all_weights = crate::ml_scorer::ml_weights::all_weights_slice();
let weights_bytes = std::mem::size_of_val(all_weights) as u64;
let max_storage_binding = u64::from(device_limits.max_storage_buffer_binding_size);
if weights_bytes > max_storage_binding {
return Err(GpuInitError::adapter_unusable(format!(
"GPU adapter {} exposes max_storage_buffer_binding_size={max_storage_binding} B, \
too small for the {weights_bytes} B MoE weights buffer",
adapter_info.name
)));
}
tracing::info!(
gpu = %adapter_info.name,
backend = ?adapter_info.backend,
device_type = ?adapter_info.device_type,
driver = %adapter_info.driver,
"GPU MoE: reusing vyre shared device"
);
let device = &dq.0;
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("moe_shader"),
source: wgpu::ShaderSource::Wgsl(moe_shader().into()),
});
let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("moe_bgl"),
entries: &[
bgl_entry(0, true),
bgl_entry(1, true),
bgl_entry(2, false),
wgpu::BindGroupLayoutEntry {
binding: 3,
visibility: wgpu::ShaderStages::COMPUTE,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
],
});
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("moe_pipeline_layout"),
bind_group_layouts: &[&bind_group_layout],
push_constant_ranges: &[],
});
let pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
label: Some("moe_pipeline"),
layout: Some(&pipeline_layout),
module: &shader,
entry_point: Some("moe_forward"),
compilation_options: Default::default(),
cache: None,
});
let weights_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("weights"),
contents: bytemuck::cast_slice(all_weights),
usage: wgpu::BufferUsages::STORAGE,
});
Ok(GpuContext {
device_queue: dq,
adapter_info,
device_limits,
pipeline,
weights_buf,
bind_group_layout,
})
}
fn bgl_entry(binding: u32, read_only: bool) -> wgpu::BindGroupLayoutEntry {
wgpu::BindGroupLayoutEntry {
binding,
visibility: wgpu::ShaderStages::COMPUTE,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Storage { read_only },
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
}
}
pub(crate) fn get_gpu() -> Option<&'static GpuContext> {
GPU.get_or_init(|| match init_gpu() {
Ok(ctx) => {
tracing::info!("GPU MoE inference initialized (shared device)");
Some(ctx)
}
Err(err) => on_gpu_init_failed(
&err,
super::gpu_disabled_by_policy(),
super::gpu_required_by_policy(),
),
})
.as_ref()
}
static MOE_RUNTIME_DEGRADE_WARNED: AtomicBool = AtomicBool::new(false);
static MOE_NONFINITE_WARNED: AtomicBool = AtomicBool::new(false);
static MOE_NUMERIC_TRUST: OnceLock<bool> = OnceLock::new();
static MOE_NUMERIC_FAULTED: AtomicBool = AtomicBool::new(false);
static MOE_NUMERIC_DIVERGENCE_WARNED: AtomicBool = AtomicBool::new(false);
pub(super) fn moe_runtime_degrade(reason: &str) {
let no_gpu = super::gpu_disabled_by_policy();
let require_gpu = super::gpu_required_by_policy();
if require_gpu {
crate::process_exit::require_gpu_unmet(format!(
"--require-gpu requested but the GPU MoE dispatch failed at runtime \
({reason}). Refusing to silently degrade to the CPU MoE."
));
}
if no_gpu {
return;
}
tracing::warn!(
reason,
"GPU MoE dispatch failed at runtime; affected batches are scored on the CPU MoE"
);
if !MOE_RUNTIME_DEGRADE_WARNED.swap(true, Ordering::Relaxed) {
eprintln!(
"keyhog: GPU MoE dispatch failed at runtime ({reason}); affected batches in \
this scan are scored on the CPU MoE (identical scores, lower throughput). Set \
--no-gpu to silence, or --require-gpu to hard-fail next time."
);
}
}
fn moe_nonfinite_degrade(nonfinite: usize, total: usize) {
let no_gpu = super::gpu_disabled_by_policy();
let require_gpu = super::gpu_required_by_policy();
if require_gpu {
crate::process_exit::require_gpu_unmet(format!(
"--require-gpu requested but the GPU MoE returned {nonfinite}/{total} \
non-finite (NaN/Inf) confidence score(s), a GPU driver/shader/weights malfunction. \
Refusing to continue with an untrusted GPU score."
));
}
if no_gpu {
return;
}
tracing::error!(
nonfinite,
total,
"GPU MoE produced non-finite confidence scores; affected batch is routed to CPU MoE"
);
if !MOE_NONFINITE_WARNED.swap(true, Ordering::Relaxed) {
eprintln!(
"keyhog: GPU MoE produced {nonfinite}/{total} non-finite (NaN/Inf) confidence \
score(s); the complete batch is rescored by the CPU MoE and GPU MoE scoring is disabled \
for this process. This indicates a GPU driver/shader/weights bug worth investigating. \
Use --no-gpu to select CPU scoring explicitly, or --require-gpu to hard-fail next time."
);
}
}
fn moe_numeric_divergence_degrade(reason: &str) {
let no_gpu = super::gpu_disabled_by_policy();
let require_gpu = super::gpu_required_by_policy();
if require_gpu {
crate::process_exit::require_gpu_unmet(format!(
"--require-gpu requested but the GPU MoE failed the CPU parity probe ({reason}). \
Refusing to silently score confidence on the CPU MoE.",
));
}
if no_gpu {
return;
}
tracing::error!(
reason,
"GPU MoE parity probe diverged from CPU MoE; scoring batches on CPU"
);
if !MOE_NUMERIC_DIVERGENCE_WARNED.swap(true, Ordering::Relaxed) {
eprintln!(
"keyhog: GPU MoE parity probe failed ({reason}); confidence batches are scored on \
the CPU MoE instead. Use --require-gpu to hard-fail until the GPU shader/driver/weights are fixed.",
);
}
}
pub(crate) fn batch_score_features(
features: &[[f32; INPUT_DIM]],
readback_timeout: Duration,
) -> Option<Vec<f64>> {
if features.len() < GPU_BATCH_THRESHOLD {
return None; }
if super::gpu_disabled_by_policy() {
return None;
}
if MOE_NUMERIC_FAULTED.load(Ordering::Acquire) {
return None;
}
if !gpu_moe_numerically_trustworthy(readback_timeout) {
return None;
}
dispatch_moe_batch(features, readback_timeout)
}
struct MoeBufferPool {
spare: Option<MoeBufferSet>,
}
struct MoeBufferSet {
input: wgpu::Buffer,
output: wgpu::Buffer,
staging: wgpu::Buffer,
params: wgpu::Buffer,
bind_group: wgpu::BindGroup,
alloc_batch_size: usize,
}
struct MoeDispatchLayout {
batch_size: u32,
input_bytes: u64,
output_bytes: u64,
workgroups: u32,
}
impl MoeDispatchLayout {
fn for_device(batch_size: usize, limits: &wgpu::Limits) -> Result<Self, &'static str> {
let batch_size_u32 = u32::try_from(batch_size)
.map_err(|_| "candidate count exceeds the GPU batch index width")?;
let input_bytes = batch_size
.checked_mul(INPUT_DIM)
.and_then(|values| values.checked_mul(std::mem::size_of::<f32>()))
.and_then(|bytes| u64::try_from(bytes).ok())
.ok_or("GPU MoE input-buffer size overflow")?;
let output_bytes = batch_size
.checked_mul(std::mem::size_of::<f32>())
.and_then(|bytes| u64::try_from(bytes).ok())
.ok_or("GPU MoE output-buffer size overflow")?;
let storage_limit = u64::from(limits.max_storage_buffer_binding_size);
if input_bytes > storage_limit || output_bytes > storage_limit {
return Err("GPU MoE batch exceeds the device storage-buffer binding limit");
}
if input_bytes > limits.max_buffer_size || output_bytes > limits.max_buffer_size {
return Err("GPU MoE batch exceeds the device buffer-size limit");
}
let workgroups =
batch_size_u32.div_ceil(crate::ml_scorer::model_arch::WORKGROUP_SIZE as u32);
if workgroups > limits.max_compute_workgroups_per_dimension {
return Err("GPU MoE batch exceeds the device compute-workgroup limit");
}
Ok(Self {
batch_size: batch_size_u32,
input_bytes,
output_bytes,
workgroups,
})
}
}
impl MoeBufferPool {
fn new() -> Self {
Self { spare: None }
}
fn take_spare(&mut self) -> Option<MoeBufferSet> {
self.spare.take()
}
fn checkin(&mut self, incoming: MoeBufferSet) -> Option<MoeBufferSet> {
match self.spare.take() {
None => {
self.spare = Some(incoming);
None
}
Some(existing) if existing.alloc_batch_size >= incoming.alloc_batch_size => {
self.spare = Some(existing);
Some(incoming)
}
Some(existing) => {
self.spare = Some(incoming);
Some(existing)
}
}
}
}
static MOE_BUFFER_POOL: std::sync::LazyLock<std::sync::Mutex<MoeBufferPool>> =
std::sync::LazyLock::new(|| std::sync::Mutex::new(MoeBufferPool::new()));
static MOE_BUFFER_POOL_POISON_WARNED: AtomicBool = AtomicBool::new(false);
fn lock_moe_buffer_pool() -> std::sync::MutexGuard<'static, MoeBufferPool> {
match MOE_BUFFER_POOL.lock() {
Ok(pool) => pool,
Err(poisoned) => {
if !MOE_BUFFER_POOL_POISON_WARNED.swap(true, Ordering::Relaxed) {
tracing::warn!(
"GPU MoE buffer pool lock was poisoned; recovering the reusable buffer state"
);
}
poisoned.into_inner()
}
}
}
fn return_moe_buffers(bufs: MoeBufferSet) {
let discarded = lock_moe_buffer_pool().checkin(bufs);
drop(discarded);
}
fn dispatch_moe_batch(
features: &[[f32; INPUT_DIM]],
readback_timeout: Duration,
) -> Option<Vec<f64>> {
let gpu = get_gpu()?;
let batch_size = features.len();
let device = gpu.device();
let queue = gpu.queue();
let layout = match MoeDispatchLayout::for_device(batch_size, &gpu.device_limits) {
Ok(layout) => layout,
Err(reason) => {
moe_runtime_degrade(reason);
return None;
}
};
let spare = lock_moe_buffer_pool().take_spare();
let bufs = match spare {
Some(set) if set.alloc_batch_size >= batch_size => Some(set),
Some(set) => {
drop(set);
None
}
None => None,
};
let bufs = match bufs {
Some(set) => set,
None => {
let input = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("moe_input_pooled"),
size: layout.input_bytes,
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let output = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("moe_output_pooled"),
size: layout.output_bytes,
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC,
mapped_at_creation: false,
});
let staging = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("moe_staging_pooled"),
size: layout.output_bytes,
usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let params = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("moe_params_pooled"),
size: std::mem::size_of::<GpuParams>() as u64,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("moe_bg_pooled"),
layout: &gpu.bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: gpu.weights_buf.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 1,
resource: input.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 2,
resource: output.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 3,
resource: params.as_entire_binding(),
},
],
});
MoeBufferSet {
input,
output,
staging,
params,
bind_group,
alloc_batch_size: batch_size,
}
}
};
let params = GpuParams {
batch_size: layout.batch_size,
_pad: [0; 3],
};
queue.write_buffer(&bufs.input, 0, bytemuck::cast_slice(features));
queue.write_buffer(&bufs.params, 0, bytemuck::bytes_of(¶ms));
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("moe_encoder"),
});
{
let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
label: Some("moe_pass"),
timestamp_writes: None,
});
pass.set_pipeline(&gpu.pipeline);
pass.set_bind_group(0, &bufs.bind_group, &[]);
pass.dispatch_workgroups(layout.workgroups, 1, 1);
}
encoder.copy_buffer_to_buffer(&bufs.output, 0, &bufs.staging, 0, layout.output_bytes);
encoder.clear_buffer(&bufs.input, 0, Some(layout.input_bytes));
encoder.clear_buffer(&bufs.params, 0, None);
queue.submit(std::iter::once(encoder.finish()));
let slice = bufs.staging.slice(..layout.output_bytes);
let (sender, receiver) = std::sync::mpsc::channel();
slice.map_async(wgpu::MapMode::Read, move |result| {
if sender.send(result).is_err() {
tracing::warn!(
"GPU MoE staging callback completed after its receiver closed; the caller already surfaced a readback failure"
);
}
});
let timeout = readback_timeout;
let deadline = Instant::now() + timeout;
let mut backoff = ReadbackWaitBackoff::new();
let map_recv = loop {
match receiver.try_recv() {
Ok(result) => break result,
Err(TryRecvError::Disconnected) => {
tracing::warn!(
"GPU MoE staging-buffer callback disconnected; GPU MoE disabled and scoring uses CPU MoE for this scan"
);
moe_runtime_degrade("staging-buffer callback disconnected");
return None;
}
Err(TryRecvError::Empty) => {}
}
if Instant::now() >= deadline {
tracing::warn!(
?timeout,
"GPU MoE staging-buffer readback timed out; GPU MoE disabled and scoring uses CPU MoE for this scan"
);
moe_runtime_degrade("staging-buffer readback timed out");
return None;
}
if let Err(error) = device.poll(wgpu::PollType::Poll) {
tracing::warn!(
?error,
"GPU MoE device.poll() failed; GPU MoE disabled and scoring uses CPU MoE for this scan"
);
moe_runtime_degrade("device.poll() failed");
return None;
}
match receiver.try_recv() {
Ok(result) => break result,
Err(TryRecvError::Disconnected) => {
tracing::warn!(
"GPU MoE staging-buffer callback disconnected after device polling; GPU MoE disabled and scoring uses CPU MoE for this scan"
);
moe_runtime_degrade("staging-buffer callback disconnected after device poll");
return None;
}
Err(TryRecvError::Empty) => {}
}
backoff.wait(deadline.saturating_duration_since(Instant::now()));
};
if let Err(error) = map_recv {
tracing::warn!(
?error,
"GPU MoE staging-buffer map_async failed; GPU MoE disabled and scoring uses CPU MoE for this scan"
);
moe_runtime_degrade("staging-buffer map_async failed");
return None;
}
let data = slice.get_mapped_range();
let scores: &[f32] = bytemuck::cast_slice(&data);
if scores.len() != batch_size {
tracing::warn!(
expected = batch_size,
actual = scores.len(),
"GPU MoE score count mismatch; routing batch to CPU MoE for this scan"
);
moe_runtime_degrade("score count mismatch");
drop(data);
bufs.staging.unmap();
return_moe_buffers(bufs);
return None;
}
let result = checked_moe_scores(scores);
if result.is_err() {
MOE_NUMERIC_FAULTED.store(true, Ordering::Release);
}
drop(data);
bufs.staging.unmap();
return_moe_buffers(bufs);
match result {
Ok(scores) => Some(scores),
Err(nonfinite) => {
moe_nonfinite_degrade(nonfinite, batch_size);
None
}
}
}
fn checked_moe_scores(scores: &[f32]) -> Result<Vec<f64>, usize> {
let mut result = Vec::with_capacity(scores.len());
let mut nonfinite = 0usize;
for &score in scores {
let score = f64::from(score);
if score.is_finite() {
result.push(score.clamp(0.0, 1.0));
} else {
nonfinite += 1;
}
}
if nonfinite == 0 {
Ok(result)
} else {
Err(nonfinite)
}
}
pub(crate) const GPU_MOE_PARITY_TOLERANCE: f64 = 0.01;
fn gpu_moe_parity_probe_features() -> Vec<[f32; INPUT_DIM]> {
const PROBES: &[(&str, &str)] = &[
(
"sk_live_4eC39HqLyjWDarjtT1zdp7dc",
"stripe_secret_key = \"sk_live_4eC39HqLyjWDarjtT1zdp7dc\"",
),
(
"AKIAQYLPMN5HFIQR7XYA",
"aws_access_key_id = \"AKIAQYLPMN5HFIQR7XYA\"",
),
(
"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",
"github_token = \"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\"",
),
(
"wJalrXUtnFEMIK7MDENGbPxRfiCYEXAMPLEKEY",
"aws_secret_access_key = \"wJalrXUtnFEMIK7MDENGbPxRfiCYEXAMPLEKEY\"",
),
(
"xoxb-1234567890-1234567890-AbCdEfGhIjKlMnOpQrStUvWx",
"slack_bot_token = \"xoxb-1234567890-1234567890-AbCdEfGhIjKlMnOpQrStUvWx\"",
),
("example", "display_name = \"example\""),
("localhost", "db_host = \"localhost\""),
("true", "feature_enabled = true"),
(
"Z9x8c7v6b5n4m3q2w1e0PkR",
"zendesk_api_token = \"Z9x8c7v6b5n4m3q2w1e0PkR\"",
),
];
let known_prefixes: Vec<String> = ["AKIA", "sk_live_", "ghp_", "xoxb-", "sk-"]
.iter()
.map(|s| s.to_string())
.collect();
let secret_keywords: Vec<String> = ["secret", "token", "key", "password"]
.iter()
.map(|s| s.to_string())
.collect();
let test_keywords: Vec<String> = ["test", "example"].iter().map(|s| s.to_string()).collect();
let placeholder_keywords: Vec<String> = ["example", "changeme"]
.iter()
.map(|s| s.to_string())
.collect();
(0..GPU_BATCH_THRESHOLD)
.map(|i| {
let (text, ctx) = PROBES[i % PROBES.len()];
crate::ml_scorer::compute_features_with_config(
text,
ctx,
&known_prefixes,
&secret_keywords,
&test_keywords,
&placeholder_keywords,
)
})
.collect()
}
pub(crate) fn gpu_moe_parity_max_divergence(readback_timeout: Duration) -> Result<f64, String> {
let probe = gpu_moe_parity_probe_features();
let gpu_scores = dispatch_moe_batch(&probe, readback_timeout)
.ok_or_else(|| "GPU MoE dispatch produced no result for the parity probe".to_string())?;
if gpu_scores.len() != probe.len() {
return Err(format!(
"GPU MoE parity probe returned {} scores for {} inputs",
gpu_scores.len(),
probe.len()
));
}
let mut max_abs = 0.0f64;
for (gpu, feat) in gpu_scores.iter().zip(probe.iter()) {
let cpu = crate::ml_scorer::score_features(feat);
max_abs = max_abs.max((gpu - cpu).abs());
}
Ok(max_abs)
}
fn gpu_moe_numerically_trustworthy(readback_timeout: Duration) -> bool {
*MOE_NUMERIC_TRUST.get_or_init(|| match gpu_moe_parity_max_divergence(readback_timeout) {
Ok(max_abs) if max_abs <= GPU_MOE_PARITY_TOLERANCE => {
tracing::info!(
target: "keyhog::gpu",
max_abs_diff = max_abs,
tolerance = GPU_MOE_PARITY_TOLERANCE,
"GPU MoE parity probe matched CPU MoE"
);
true
}
Ok(max_abs) => {
moe_numeric_divergence_degrade(&format!(
"max_abs_diff={max_abs:.6}, tolerance={GPU_MOE_PARITY_TOLERANCE:.6}"
));
false
}
Err(reason) => {
if !MOE_NUMERIC_FAULTED.load(Ordering::Acquire) {
moe_numeric_divergence_degrade(&reason);
}
false
}
})
}
#[cfg(test)]
#[path = "../../tests/unit/gpu_backend.rs"]
mod tests;