use std::{
collections::HashMap,
ffi::c_void,
panic::{AssertUnwindSafe, catch_unwind},
sync::{
Arc, Mutex, OnceLock,
atomic::{AtomicBool, AtomicU64, Ordering},
},
};
use super::usage::DeviceMemoryPoolUsage;
use crate::{
DeviceId,
core::error::{self, Result},
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DeviceMemoryFailureSource {
Physical,
ReservationLimit,
AllocationLimit,
Unknown(u32),
}
impl DeviceMemoryFailureSource {
fn from_sys(value: u32) -> Self {
match value {
value if value == cudf_sys::CUDF_MEMORY_FAILURE_SOURCE_PHYSICAL => Self::Physical,
value if value == cudf_sys::CUDF_MEMORY_FAILURE_SOURCE_RESERVATION_LIMIT => Self::ReservationLimit,
2 => Self::AllocationLimit,
other => Self::Unknown(other),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DeviceMemoryFailureAllocationDomain {
QueryMemoryDomain { domain_id: u64, member_id: u64 },
CacheAllocationScope { cache_domain_id: u64, scope_id: u64 },
Unknown { kind: u64, primary_id: u64, secondary_id: u64 },
}
impl DeviceMemoryFailureAllocationDomain {
fn from_sys(kind: u64, primary_id: u64, secondary_id: u64) -> Option<Self> {
match kind {
value if value == cudf_sys::CUDF_MEMORY_FAILURE_DOMAIN_NONE as u64 => None,
value if value == cudf_sys::CUDF_MEMORY_FAILURE_DOMAIN_QUERY_MEMORY_DOMAIN as u64 => {
Some(Self::QueryMemoryDomain { domain_id: primary_id, member_id: secondary_id })
},
4 => Some(Self::CacheAllocationScope { cache_domain_id: primary_id, scope_id: secondary_id }),
other => Some(Self::Unknown { kind: other, primary_id, secondary_id }),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DeviceMemoryFailureInfo {
pub device: DeviceId,
pub source: DeviceMemoryFailureSource,
pub allocation_domain: Option<DeviceMemoryFailureAllocationDomain>,
pub requested_bytes: u64,
pub callback_attempt: u32,
pub trim_retry_count: u32,
pub pool_usage: Option<DeviceMemoryPoolUsage>,
}
impl DeviceMemoryFailureInfo {
pub(super) fn from_sys(value: cudf_sys::cudf_memory_failure_info_t) -> Self {
let pool_usage =
(value.flags & cudf_sys::CUDF_MEMORY_FAILURE_INFO_HAS_POOL_USAGE != 0).then_some(DeviceMemoryPoolUsage {
reserved_current_bytes: value.pool_reserved_current_bytes,
reserved_high_bytes: value.pool_reserved_high_bytes,
used_current_bytes: value.pool_used_current_bytes,
used_high_bytes: value.pool_used_high_bytes,
});
let device = if value.device_ordinal >= 0 {
DeviceId::from_validated_ordinal_raw(value.device_ordinal)
} else {
DeviceId::DEFAULT
};
let allocation_domain = (value.flags & cudf_sys::CUDF_MEMORY_FAILURE_INFO_HAS_ALLOCATION_DOMAIN != 0)
.then(|| {
DeviceMemoryFailureAllocationDomain::from_sys(value.reserved[0], value.reserved[1], value.reserved[2])
})
.flatten();
Self {
device,
source: DeviceMemoryFailureSource::from_sys(value.failure_source),
allocation_domain,
requested_bytes: value.requested_bytes,
callback_attempt: value.callback_attempt,
trim_retry_count: value.trim_retry_count,
pool_usage,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AllocatorEventCompleteness {
pub timestamp: bool,
pub allocation_domain: bool,
pub pool_usage: bool,
pub action: bool,
}
impl AllocatorEventCompleteness {
fn from_sys(flags: u32) -> Self {
Self {
timestamp: flags & cudf_sys::CUDF_ALLOCATOR_EVENT_COMPLETENESS_HAS_TIMESTAMP != 0,
allocation_domain: flags & cudf_sys::CUDF_ALLOCATOR_EVENT_COMPLETENESS_HAS_DOMAIN_IDENTITY != 0,
pool_usage: flags & cudf_sys::CUDF_ALLOCATOR_EVENT_COMPLETENESS_HAS_POOL_USAGE != 0,
action: flags & cudf_sys::CUDF_ALLOCATOR_EVENT_COMPLETENESS_HAS_ACTION != 0,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AllocatorEventDomainIdentity {
pub domain: DeviceMemoryFailureAllocationDomain,
pub generation: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AllocatorEventAction {
Decline,
Retry,
TrimAndRetry,
CallbackPanicked,
Unknown(i32),
}
impl AllocatorEventAction {
fn from_sys(value: i32) -> Self {
match value {
value if value == cudf_sys::CUDF_MEMORY_FAILURE_ACTION_DECLINE as i32 => Self::Decline,
value if value == cudf_sys::CUDF_MEMORY_FAILURE_ACTION_RETRY as i32 => Self::Retry,
value if value == cudf_sys::CUDF_MEMORY_FAILURE_ACTION_TRIM_AND_RETRY as i32 => Self::TrimAndRetry,
value if value == cudf_sys::CUDF_MEMORY_FAILURE_ACTION_CALLBACK_PANICKED => Self::CallbackPanicked,
other => Self::Unknown(other),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AllocatorEvent {
pub sequence: u64,
pub monotonic_time_ns: Option<u64>,
pub device: Option<DeviceId>,
pub allocation_domain: Option<AllocatorEventDomainIdentity>,
pub requested_bytes: u64,
pub source: DeviceMemoryFailureSource,
pub action: Option<AllocatorEventAction>,
pub callback_attempt: u32,
pub trim_retry_count: u32,
pub pool_usage: Option<DeviceMemoryPoolUsage>,
pub completeness: AllocatorEventCompleteness,
}
impl AllocatorEvent {
pub(super) fn from_sys(value: cudf_sys::cudf_allocator_event_t) -> Self {
let completeness = AllocatorEventCompleteness::from_sys(value.completeness);
let device = (value.device_ordinal >= 0).then(|| DeviceId::from_validated_ordinal_raw(value.device_ordinal));
let allocation_domain = completeness
.allocation_domain
.then(|| {
DeviceMemoryFailureAllocationDomain::from_sys(
value.domain_kind as u64,
value.domain_id,
value.domain_member_id,
)
.map(|domain| AllocatorEventDomainIdentity { domain, generation: value.domain_generation })
})
.flatten();
let pool_usage = completeness.pool_usage.then_some(DeviceMemoryPoolUsage {
reserved_current_bytes: value.pool_reserved_current_bytes,
reserved_high_bytes: value.pool_reserved_high_bytes,
used_current_bytes: value.pool_used_current_bytes,
used_high_bytes: value.pool_used_high_bytes,
});
Self {
sequence: value.sequence,
monotonic_time_ns: completeness.timestamp.then_some(value.monotonic_time_ns),
device,
allocation_domain,
requested_bytes: value.requested_bytes,
source: DeviceMemoryFailureSource::from_sys(value.failure_source),
action: completeness.action.then(|| AllocatorEventAction::from_sys(value.action)),
callback_attempt: value.callback_attempt,
trim_retry_count: value.trim_retry_count,
pool_usage,
completeness,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AllocatorEventRingBounds {
pub capacity: u64,
pub first_available_sequence: Option<u64>,
pub next_sequence: u64,
}
impl From<cudf_sys::cudf_allocator_event_ring_bounds_t> for AllocatorEventRingBounds {
fn from(value: cudf_sys::cudf_allocator_event_ring_bounds_t) -> Self {
Self {
capacity: value.capacity,
first_available_sequence: (value.first_available_sequence != 0).then_some(value.first_available_sequence),
next_sequence: value.next_sequence,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AllocatorEventRead {
Available(AllocatorEvent),
Pending,
Overwritten,
NotYetRecorded,
}
pub(super) type FailureCallback = dyn Fn(DeviceMemoryFailureInfo) + Send + Sync + 'static;
pub(super) struct FailureCallbackState {
callback: Arc<FailureCallback>,
panicked: AtomicBool,
installation_generation: AtomicU64,
}
impl FailureCallbackState {
pub(super) fn new(callback: Arc<FailureCallback>) -> Self {
Self { callback, panicked: AtomicBool::new(false), installation_generation: AtomicU64::new(0) }
}
pub(super) fn installation_generation(&self) -> Option<u64> {
let generation = self.installation_generation.load(Ordering::Acquire);
(generation != 0).then_some(generation)
}
pub(super) fn bind_installation_generation(&self, generation: u64) -> Result<()> {
if generation == 0 {
return Err(error::Error::new_with(
error::ErrorCode::ForeignInvariantViolation,
"memory::device_memory_manager::install",
"native installation record returned a zero callback generation",
));
}
match self.installation_generation.compare_exchange(0, generation, Ordering::AcqRel, Ordering::Acquire) {
Ok(_) => Ok(()),
Err(existing) if existing == generation => Ok(()),
Err(existing) => Err(error::Error::new_with(
error::ErrorCode::InvariantViolation,
"memory::device_memory_manager::install",
"callback state was rebound to a different installation generation",
)
.with_detail("existing_generation", existing.to_string())
.with_detail("observed_generation", generation.to_string())),
}
}
}
pub(super) unsafe extern "C" fn failure_callback_trampoline(
info: *const cudf_sys::cudf_memory_failure_info_t,
callback_arg: *mut c_void,
) -> i32 {
if callback_arg.is_null() {
return 0;
}
let state = unsafe { &*(callback_arg as *const FailureCallbackState) };
if state.panicked.load(Ordering::Acquire) {
return cudf_sys::CUDF_MEMORY_FAILURE_ACTION_CALLBACK_PANICKED;
}
if info.is_null() {
return 0;
}
let info = DeviceMemoryFailureInfo::from_sys(unsafe { *info });
match catch_unwind(AssertUnwindSafe(|| (state.callback)(info))) {
Ok(()) => cudf_sys::CUDF_MEMORY_FAILURE_ACTION_DECLINE as i32,
Err(_) => {
state.panicked.store(true, Ordering::Release);
cudf_sys::CUDF_MEMORY_FAILURE_ACTION_CALLBACK_PANICKED
},
}
}
pub(super) fn wrap_simple_failure_callback(
callback: Arc<dyn Fn(usize) + Send + Sync + 'static>,
) -> Arc<FailureCallback> {
Arc::new(move |info| callback(info.requested_bytes as usize))
}
pub(super) fn device_manager_failure_callbacks() -> &'static Mutex<HashMap<i32, Arc<FailureCallbackState>>> {
static CALLBACKS: OnceLock<Mutex<HashMap<i32, Arc<FailureCallbackState>>>> = OnceLock::new();
CALLBACKS.get_or_init(|| Mutex::new(HashMap::new()))
}