use keyhog_profile::{AnnotationId, CounterId, EventId, GaugeId, MetricId};
use std::collections::BTreeSet;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Mutex;
pub(crate) const BACKEND_CUDA: u64 = 1;
pub(crate) const BACKEND_METAL: u64 = 2;
pub(crate) const BACKEND_WGPU: u64 = 3;
pub(crate) fn backend_code(backend_id: &str) -> u64 {
match backend_id {
"cuda" => BACKEND_CUDA,
"metal" => BACKEND_METAL,
_ => BACKEND_WGPU,
}
}
pub(crate) mod fault {
pub(crate) const DISPATCH: u64 = 1;
}
pub(crate) mod capability {
pub(crate) const KERNEL_TIMESTAMPS: u64 = 1;
pub(crate) const OCCUPANCY: u64 = 2;
pub(crate) const UTILIZATION: u64 = 3;
pub(crate) const STALL_COUNTERS: u64 = 4;
}
const MAX_RECORDED_CONTEXTS: usize = 1024;
struct ContextClaimSet {
seen: BTreeSet<(u64, u16)>,
lost: u64,
}
impl ContextClaimSet {
const fn new() -> Self {
Self {
seen: BTreeSet::new(),
lost: 0,
}
}
fn claim(&mut self, context: u64, slot: u16) -> bool {
if self.seen.contains(&(context, slot)) {
return false;
}
if self.seen.len() >= MAX_RECORDED_CONTEXTS {
self.lost = self.lost.saturating_add(1);
tracing::warn!(
target: "keyhog::gpu",
context,
slot,
lost = self.lost,
capacity = MAX_RECORDED_CONTEXTS,
"accelerator evidence dedup set is full; this once-per-runtime record is dropped"
);
return false;
}
self.seen.insert((context, slot));
true
}
}
static CONTEXT_CLAIMS: Mutex<ContextClaimSet> = Mutex::new(ContextClaimSet::new());
fn claim_once(context: u64, slot: u16) -> bool {
let mut claims = CONTEXT_CLAIMS
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
claims.claim(context, slot)
}
const IDENTITY_SLOT_BASE: u16 = 0;
const CAPABILITY_SLOT_BASE: u16 = 0x100;
pub(crate) struct AdapterIdentity<'a> {
pub(crate) backend_code: u64,
pub(crate) vendor: u32,
pub(crate) device: u32,
pub(crate) is_software: bool,
pub(crate) name: &'a str,
pub(crate) driver: &'a str,
pub(crate) driver_info: &'a str,
}
pub(crate) fn record_adapter_identity(identity: &AdapterIdentity<'_>) {
let Some(runtime) = keyhog_profile::current_runtime() else {
return;
};
let slot = IDENTITY_SLOT_BASE + identity.backend_code as u16;
if !claim_once(runtime.context_id(), slot) {
return;
}
keyhog_profile::record_event(EventId::GpuAdapterAcquired, identity.backend_code);
keyhog_profile::record_annotation(AnnotationId::GpuBackendKind, identity.backend_code);
keyhog_profile::record_annotation(AnnotationId::GpuAdapterVendor, u64::from(identity.vendor));
keyhog_profile::record_annotation(AnnotationId::GpuAdapterDevice, u64::from(identity.device));
tracing::info!(
target: "keyhog::gpu",
adapter = identity.name,
backend = identity.backend_code,
vendor = format_args!("{:#06x}", identity.vendor),
device = format_args!("{:#06x}", identity.device),
driver = identity.driver,
driver_info = identity.driver_info,
is_software = identity.is_software,
"GPU adapter identity recorded for the active profile"
);
}
pub(crate) fn report_capability_unsupported(backend_code: u64, capability: u64) {
let Some(runtime) = keyhog_profile::current_runtime() else {
return;
};
let slot = CAPABILITY_SLOT_BASE + capability as u16;
if !claim_once(runtime.context_id(), slot) {
return;
}
keyhog_profile::record_event(EventId::GpuCapabilityUnsupported, capability);
tracing::debug!(
target: "keyhog::gpu",
backend = backend_code,
capability,
"accelerator capability is unsupported on this backend; the profile records an explicit gap"
);
}
pub(crate) fn report_counter_caps_unsupported(backend_code: u64) {
for capability in [
capability::OCCUPANCY,
capability::UTILIZATION,
capability::STALL_COUNTERS,
] {
report_capability_unsupported(backend_code, capability);
}
}
pub(crate) fn record_upload(bytes: u64, ns: Option<u64>) {
keyhog_profile::add_counter(CounterId::GpuUploadBytes, bytes);
if let Some(ns) = ns {
keyhog_profile::add_counter(CounterId::GpuUploadNs, ns);
keyhog_profile::record_distribution(MetricId::GpuUploadNs, ns);
}
}
pub(crate) fn record_readback(bytes: u64, ns: Option<u64>) {
keyhog_profile::add_counter(CounterId::GpuReadbackBytes, bytes);
if let Some(ns) = ns {
keyhog_profile::add_counter(CounterId::GpuReadbackNs, ns);
keyhog_profile::record_distribution(MetricId::GpuReadbackNs, ns);
}
}
pub(crate) fn record_submit_to_complete(ns: u64) {
keyhog_profile::add_counter(CounterId::GpuSubmitToCompleteNs, ns);
keyhog_profile::record_distribution(MetricId::GpuSubmitToCompleteNs, ns);
}
pub(crate) fn record_kernel(ns: u64) {
keyhog_profile::add_counter(CounterId::GpuKernelNs, ns);
keyhog_profile::record_distribution(MetricId::GpuKernelNs, ns);
}
pub(crate) fn record_queue_wait(ns: u64) {
keyhog_profile::add_counter(CounterId::GpuQueueWaitNs, ns);
keyhog_profile::record_distribution(MetricId::GpuQueueWaitNs, ns);
}
pub(crate) fn record_dispatch_submitted() {
keyhog_profile::add_counter(CounterId::GpuDispatchCalls, 1);
}
pub(crate) fn record_fault(_backend_code: u64, kind: u64) {
keyhog_profile::add_counter(CounterId::GpuFaults, 1);
keyhog_profile::record_event(EventId::GpuFault, kind);
}
pub(crate) fn record_retry(attempt: u64) {
keyhog_profile::add_counter(CounterId::GpuRetries, 1);
keyhog_profile::record_annotation(AnnotationId::RetryAttempt, attempt);
}
pub(crate) fn record_recovery(backend_code: u64) {
keyhog_profile::add_counter(CounterId::GpuRecoveries, 1);
keyhog_profile::record_event(EventId::BackendRecovered, backend_code);
}
pub(crate) fn record_residual_batch() {
keyhog_profile::add_counter(CounterId::GpuResidualBatches, 1);
}
static DEVICE_RESIDENT_BYTES: AtomicU64 = AtomicU64::new(0);
static DEVICE_PEAK_RESIDENT_BYTES: AtomicU64 = AtomicU64::new(0);
pub(crate) fn note_device_alloc(bytes: u64) {
if bytes == 0 {
return;
}
keyhog_profile::add_counter(CounterId::GpuAllocCalls, 1);
keyhog_profile::add_counter(CounterId::GpuAllocBytes, bytes);
let current = DEVICE_RESIDENT_BYTES.fetch_add(bytes, Ordering::Relaxed) + bytes;
DEVICE_PEAK_RESIDENT_BYTES.fetch_max(current, Ordering::Relaxed);
let peak = DEVICE_PEAK_RESIDENT_BYTES.load(Ordering::Relaxed);
keyhog_profile::set_gauge(GaugeId::GpuResidentBytes, current);
keyhog_profile::set_gauge(GaugeId::GpuPeakResidentBytes, peak);
}
pub(crate) fn note_device_free(bytes: u64) {
if bytes == 0 {
return;
}
keyhog_profile::add_counter(CounterId::GpuFreeBytes, bytes);
let current = DEVICE_RESIDENT_BYTES
.fetch_update(Ordering::Relaxed, Ordering::Relaxed, |value| {
Some(value.saturating_sub(bytes))
})
.map_or(0, |previous| previous.saturating_sub(bytes));
keyhog_profile::set_gauge(GaugeId::GpuResidentBytes, current);
}
#[cfg(test)]
pub(crate) fn resident_bytes_snapshot() -> (u64, u64) {
(
DEVICE_RESIDENT_BYTES.load(Ordering::Relaxed),
DEVICE_PEAK_RESIDENT_BYTES.load(Ordering::Relaxed),
)
}
#[cfg(all(test, feature = "gpu"))]
#[path = "../../tests/unit/gpu_evidence.rs"]
mod tests;
#[cfg(test)]
#[path = "../../tests/unit/gpu_evidence_bounded_dedup.rs"]
mod bounded_dedup_tests;