#[cfg(not(target_os = "macos"))]
use crate::error::DriverLoadError;
#[cfg(not(target_os = "macos"))]
use libloading::Library;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(u32)]
pub enum CuptiActivityKind {
Memcpy = 1,
Memset = 2,
Kernel = 3,
Driver = 4,
Runtime = 5,
ConcurrentKernel = 10,
Name = 11,
Marker = 12,
MemoryPool = 50,
}
impl CuptiActivityKind {
#[must_use]
pub fn raw(self) -> u32 {
self as u32
}
}
#[cfg(not(target_os = "macos"))]
const CUPTI_LIBRARY_NAMES: &[&str] = &[
"libcupti.so",
"libcupti.so.12",
"libcupti.so.11",
"cupti64_2025.1.0.dll",
"cupti64_2024.1.0.dll",
];
#[cfg(not(target_os = "macos"))]
pub struct CuptiLibrary {
_lib: Library,
has_activity_enable: bool,
has_register_callbacks: bool,
has_flush_all: bool,
}
#[cfg(not(target_os = "macos"))]
impl CuptiLibrary {
pub fn load() -> Result<Self, DriverLoadError> {
let mut last_error = String::new();
let lib = 'open: {
for name in CUPTI_LIBRARY_NAMES {
match unsafe { Library::new(*name) } {
Ok(lib) => break 'open lib,
Err(e) => last_error = e.to_string(),
}
}
return Err(DriverLoadError::LibraryNotFound {
candidates: CUPTI_LIBRARY_NAMES
.iter()
.map(|s| (*s).to_string())
.collect(),
last_error,
});
};
let has_activity_enable = Self::has_symbol(&lib, b"cuptiActivityEnable");
let has_register_callbacks = Self::has_symbol(&lib, b"cuptiActivityRegisterCallbacks");
let has_flush_all = Self::has_symbol(&lib, b"cuptiActivityFlushAll");
if !has_activity_enable {
return Err(DriverLoadError::SymbolNotFound {
symbol: "cuptiActivityEnable",
reason: "symbol absent from loaded CUPTI library".to_string(),
});
}
Ok(Self {
_lib: lib,
has_activity_enable,
has_register_callbacks,
has_flush_all,
})
}
fn has_symbol(lib: &Library, name: &[u8]) -> bool {
unsafe { lib.get::<unsafe extern "C" fn()>(name) }.is_ok()
}
#[must_use]
pub fn supports_activity_enable(&self) -> bool {
self.has_activity_enable
}
#[must_use]
pub fn supports_register_callbacks(&self) -> bool {
self.has_register_callbacks
}
#[must_use]
pub fn supports_flush_all(&self) -> bool {
self.has_flush_all
}
}
#[must_use]
pub fn cupti_available() -> bool {
#[cfg(not(target_os = "macos"))]
{
CuptiLibrary::load().is_ok()
}
#[cfg(target_os = "macos")]
{
false
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ActivityRecord {
pub kind: CuptiActivityKind,
pub start_ns: u64,
pub end_ns: u64,
pub correlation_id: u32,
}
impl ActivityRecord {
#[must_use]
pub fn duration_ns(&self) -> u64 {
self.end_ns.saturating_sub(self.start_ns)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
pub enum CuptiActivityError {
#[error("activity buffer callbacks not registered")]
CallbacksNotRegistered,
#[error("activity kind {kind:?} is not enabled")]
KindNotEnabled {
kind: CuptiActivityKind,
},
#[error("activity buffer full ({capacity} records); flush required")]
BufferFull {
capacity: usize,
},
}
#[derive(Debug)]
pub struct ActivitySession {
enabled: Vec<CuptiActivityKind>,
callbacks_registered: bool,
buffer_capacity: usize,
pending: Vec<ActivityRecord>,
completed: Vec<ActivityRecord>,
dropped: u64,
}
impl ActivitySession {
pub const DEFAULT_BUFFER_CAPACITY: usize = 256;
#[must_use]
pub fn new() -> Self {
Self::with_buffer_capacity(Self::DEFAULT_BUFFER_CAPACITY)
}
#[must_use]
pub fn with_buffer_capacity(buffer_capacity: usize) -> Self {
Self {
enabled: Vec::new(),
callbacks_registered: false,
buffer_capacity: buffer_capacity.max(1),
pending: Vec::new(),
completed: Vec::new(),
dropped: 0,
}
}
pub fn register_callbacks(&mut self) {
self.callbacks_registered = true;
}
#[must_use]
pub fn callbacks_registered(&self) -> bool {
self.callbacks_registered
}
pub fn enable(&mut self, kind: CuptiActivityKind) -> Result<(), CuptiActivityError> {
if !self.callbacks_registered {
return Err(CuptiActivityError::CallbacksNotRegistered);
}
if !self.enabled.contains(&kind) {
self.enabled.push(kind);
}
Ok(())
}
pub fn disable(&mut self, kind: CuptiActivityKind) {
self.enabled.retain(|k| *k != kind);
}
#[must_use]
pub fn is_enabled(&self, kind: CuptiActivityKind) -> bool {
self.enabled.contains(&kind)
}
#[must_use]
pub fn enabled_kinds(&self) -> &[CuptiActivityKind] {
&self.enabled
}
pub fn record(&mut self, record: ActivityRecord) -> Result<(), CuptiActivityError> {
if !self.callbacks_registered {
return Err(CuptiActivityError::CallbacksNotRegistered);
}
if !self.enabled.contains(&record.kind) {
self.dropped = self.dropped.saturating_add(1);
return Ok(());
}
if self.pending.len() >= self.buffer_capacity {
return Err(CuptiActivityError::BufferFull {
capacity: self.buffer_capacity,
});
}
self.pending.push(record);
Ok(())
}
pub fn flush(&mut self) -> Result<usize, CuptiActivityError> {
if !self.callbacks_registered {
return Err(CuptiActivityError::CallbacksNotRegistered);
}
let n = self.pending.len();
self.completed.append(&mut self.pending);
Ok(n)
}
pub fn drain(&mut self) -> Vec<ActivityRecord> {
std::mem::take(&mut self.completed)
}
#[must_use]
pub fn pending_len(&self) -> usize {
self.pending.len()
}
#[must_use]
pub fn completed_len(&self) -> usize {
self.completed.len()
}
#[must_use]
pub fn dropped_records(&self) -> u64 {
self.dropped
}
}
impl Default for ActivitySession {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn rec(kind: CuptiActivityKind, start: u64, end: u64, cid: u32) -> ActivityRecord {
ActivityRecord {
kind,
start_ns: start,
end_ns: end,
correlation_id: cid,
}
}
#[test]
fn activity_kind_raw_discriminants() {
assert_eq!(CuptiActivityKind::Memcpy.raw(), 1);
assert_eq!(CuptiActivityKind::Kernel.raw(), 3);
assert_eq!(CuptiActivityKind::ConcurrentKernel.raw(), 10);
assert_eq!(CuptiActivityKind::Name.raw(), 11);
assert_eq!(CuptiActivityKind::Marker.raw(), 12);
assert_eq!(CuptiActivityKind::MemoryPool.raw(), 50);
}
#[test]
fn cupti_available_does_not_panic() {
let _ = cupti_available();
}
#[cfg(not(target_os = "macos"))]
#[test]
fn load_without_cupti_yields_library_not_found() {
match CuptiLibrary::load() {
Err(DriverLoadError::LibraryNotFound { candidates, .. }) => {
assert!(candidates.iter().any(|c| c.contains("cupti")));
}
Err(DriverLoadError::SymbolNotFound { .. }) => {
}
Ok(lib) => {
assert!(lib.supports_activity_enable());
}
Err(other) => panic!("unexpected error variant: {other:?}"),
}
}
#[test]
fn record_requires_registered_callbacks() {
let mut s = ActivitySession::new();
let r = rec(CuptiActivityKind::Kernel, 0, 100, 1);
assert_eq!(s.record(r), Err(CuptiActivityError::CallbacksNotRegistered));
}
#[test]
fn enable_requires_registered_callbacks() {
let mut s = ActivitySession::new();
assert_eq!(
s.enable(CuptiActivityKind::Kernel),
Err(CuptiActivityError::CallbacksNotRegistered)
);
}
#[test]
fn flush_requires_registered_callbacks() {
let mut s = ActivitySession::new();
assert_eq!(s.flush(), Err(CuptiActivityError::CallbacksNotRegistered));
}
#[test]
fn full_enable_record_flush_drain_cycle() {
let mut s = ActivitySession::new();
s.register_callbacks();
s.enable(CuptiActivityKind::Kernel).unwrap();
s.enable(CuptiActivityKind::Memcpy).unwrap();
s.record(rec(CuptiActivityKind::Kernel, 0, 500, 1)).unwrap();
s.record(rec(CuptiActivityKind::Memcpy, 500, 700, 2))
.unwrap();
assert_eq!(s.pending_len(), 2);
assert_eq!(s.completed_len(), 0);
let flushed = s.flush().unwrap();
assert_eq!(flushed, 2);
assert_eq!(s.pending_len(), 0);
assert_eq!(s.completed_len(), 2);
let records = s.drain();
assert_eq!(records.len(), 2);
assert_eq!(records[0].duration_ns(), 500);
assert_eq!(records[1].duration_ns(), 200);
assert_eq!(s.completed_len(), 0);
}
#[test]
fn disabled_kind_is_dropped_and_counted() {
let mut s = ActivitySession::new();
s.register_callbacks();
s.enable(CuptiActivityKind::Kernel).unwrap();
s.record(rec(CuptiActivityKind::Memcpy, 0, 10, 1)).unwrap();
s.record(rec(CuptiActivityKind::Kernel, 0, 10, 2)).unwrap();
assert_eq!(s.pending_len(), 1);
assert_eq!(s.dropped_records(), 1);
}
#[test]
fn disable_stops_recording() {
let mut s = ActivitySession::new();
s.register_callbacks();
s.enable(CuptiActivityKind::Kernel).unwrap();
assert!(s.is_enabled(CuptiActivityKind::Kernel));
s.disable(CuptiActivityKind::Kernel);
assert!(!s.is_enabled(CuptiActivityKind::Kernel));
s.record(rec(CuptiActivityKind::Kernel, 0, 10, 1)).unwrap();
assert_eq!(s.pending_len(), 0);
assert_eq!(s.dropped_records(), 1);
}
#[test]
fn buffer_full_requires_flush() {
let mut s = ActivitySession::with_buffer_capacity(2);
s.register_callbacks();
s.enable(CuptiActivityKind::Kernel).unwrap();
s.record(rec(CuptiActivityKind::Kernel, 0, 1, 1)).unwrap();
s.record(rec(CuptiActivityKind::Kernel, 1, 2, 2)).unwrap();
assert_eq!(
s.record(rec(CuptiActivityKind::Kernel, 2, 3, 3)),
Err(CuptiActivityError::BufferFull { capacity: 2 })
);
s.flush().unwrap();
s.record(rec(CuptiActivityKind::Kernel, 2, 3, 3)).unwrap();
assert_eq!(s.pending_len(), 1);
}
#[test]
fn enable_is_idempotent() {
let mut s = ActivitySession::new();
s.register_callbacks();
s.enable(CuptiActivityKind::Kernel).unwrap();
s.enable(CuptiActivityKind::Kernel).unwrap();
assert_eq!(s.enabled_kinds().len(), 1);
}
#[test]
fn register_callbacks_is_idempotent() {
let mut s = ActivitySession::new();
s.register_callbacks();
s.register_callbacks();
assert!(s.callbacks_registered());
}
#[test]
fn buffer_capacity_floors_at_one() {
let s = ActivitySession::with_buffer_capacity(0);
assert_eq!(s.pending_len(), 0);
let mut s = s;
s.register_callbacks();
s.enable(CuptiActivityKind::Kernel).unwrap();
s.record(rec(CuptiActivityKind::Kernel, 0, 1, 1)).unwrap();
assert_eq!(
s.record(rec(CuptiActivityKind::Kernel, 1, 2, 2)),
Err(CuptiActivityError::BufferFull { capacity: 1 })
);
}
#[test]
fn duration_saturates_on_inverted_timestamps() {
let r = rec(CuptiActivityKind::Kernel, 100, 50, 1);
assert_eq!(r.duration_ns(), 0);
}
}