use std::collections::BTreeMap;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex};
use cudarc::driver::sys::CUdeviceptr;
use onnx_runtime_ep_api::{EpError, Result};
use crate::runtime::CudaRuntime;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct CsaCursors {
pub seq_cursor: u64,
pub compressed_len: u64,
pub compression_carry_len: u64,
pub index_len: u64,
pub index_carry_len: u64,
}
impl CsaCursors {
pub fn from_sequence(seq_cursor: u64, ratio: u64) -> Self {
let ratio = ratio.max(1);
let compressed_len = seq_cursor / ratio;
let carry_len = seq_cursor % ratio;
Self {
seq_cursor,
compressed_len,
compression_carry_len: carry_len,
index_len: compressed_len,
index_carry_len: carry_len,
}
}
}
#[derive(Clone, Copy, Debug)]
pub struct CsaCheckpoint {
cursors: CsaCursors,
generation: u64,
main_carry_bytes: usize,
index_carry_bytes: usize,
}
impl CsaCheckpoint {
pub fn cursors(&self) -> CsaCursors {
self.cursors
}
pub fn generation(&self) -> u64 {
self.generation
}
}
pub struct CsaCheckpointJournal {
runtime: Arc<CudaRuntime>,
ratio: u64,
main_snapshot: CUdeviceptr,
index_snapshot: CUdeviceptr,
main_capacity: usize,
index_capacity: usize,
metrics: Arc<CsaMetrics>,
}
impl CsaCheckpointJournal {
pub fn new(
runtime: Arc<CudaRuntime>,
ratio: u64,
main_carry_bytes: usize,
index_carry_bytes: usize,
metrics: Arc<CsaMetrics>,
) -> Result<Self> {
let main_snapshot = runtime.alloc_raw(main_carry_bytes.max(1))?;
let index_snapshot = match runtime.alloc_raw(index_carry_bytes.max(1)) {
Ok(ptr) => ptr,
Err(error) => {
let _ = unsafe { runtime.free_raw(main_snapshot) };
return Err(error);
}
};
Ok(Self {
runtime,
ratio: ratio.max(1),
main_snapshot,
index_snapshot,
main_capacity: main_carry_bytes.max(1),
index_capacity: index_carry_bytes.max(1),
metrics,
})
}
pub unsafe fn checkpoint(
&self,
main_carry: CUdeviceptr,
index_carry: CUdeviceptr,
main_carry_bytes: usize,
index_carry_bytes: usize,
seq_cursor: u64,
generation: u64,
) -> Result<CsaCheckpoint> {
if main_carry_bytes > self.main_capacity || index_carry_bytes > self.index_capacity {
return Err(EpError::KernelFailed(format!(
"CSA checkpoint: carry ({main_carry_bytes},{index_carry_bytes}) exceeds reserved \
snapshot capacity ({},{})",
self.main_capacity, self.index_capacity
)));
}
if main_carry_bytes > 0 {
unsafe {
self.runtime
.dtod(main_carry, self.main_snapshot, main_carry_bytes)?;
}
}
if index_carry_bytes > 0 {
unsafe {
self.runtime
.dtod(index_carry, self.index_snapshot, index_carry_bytes)?;
}
}
Ok(CsaCheckpoint {
cursors: CsaCursors::from_sequence(seq_cursor, self.ratio),
generation,
main_carry_bytes,
index_carry_bytes,
})
}
pub unsafe fn restore_prefix(
&self,
checkpoint: &CsaCheckpoint,
accepted: u64,
generation: u64,
main_carry: CUdeviceptr,
index_carry: CUdeviceptr,
seq_scalar: Option<CUdeviceptr>,
) -> Result<CsaCursors> {
if generation != checkpoint.generation {
return Err(EpError::KernelFailed(format!(
"CSA restore: generation {generation} does not match checkpoint {}",
checkpoint.generation
)));
}
if accepted > checkpoint.cursors.seq_cursor {
return Err(EpError::KernelFailed(format!(
"CSA restore: accepted prefix {accepted} exceeds committed checkpoint {}",
checkpoint.cursors.seq_cursor
)));
}
if checkpoint.main_carry_bytes > 0 {
unsafe {
self.runtime
.dtod(self.main_snapshot, main_carry, checkpoint.main_carry_bytes)?;
}
}
if checkpoint.index_carry_bytes > 0 {
unsafe {
self.runtime.dtod(
self.index_snapshot,
index_carry,
checkpoint.index_carry_bytes,
)?;
}
}
if let Some(scalar) = seq_scalar {
let bytes = (accepted as i64).to_ne_bytes();
unsafe {
self.runtime.htod(&bytes, scalar)?;
}
}
self.metrics.record_rollback();
Ok(CsaCursors::from_sequence(accepted, self.ratio))
}
pub fn metrics(&self) -> &Arc<CsaMetrics> {
&self.metrics
}
}
impl Drop for CsaCheckpointJournal {
fn drop(&mut self) {
let _ = unsafe { self.runtime.free_raw(self.index_snapshot) };
let _ = unsafe { self.runtime.free_raw(self.main_snapshot) };
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum CsaAttentionMode {
#[default]
Host,
Device,
}
#[derive(Clone, Copy, Debug, Default)]
pub struct CsaLayerMetrics {
pub mode: CsaAttentionMode,
pub cursors: CsaCursors,
pub bytes_avoided: u64,
pub host_bytes: u64,
pub device_bytes: u64,
pub sink_mass: f32,
pub stage_timings_us: [u32; 8],
pub decode_count: u64,
}
#[derive(Debug, Default)]
pub struct CsaMetrics {
rollback_count: AtomicU64,
device_bytes_total: AtomicU64,
host_bytes_total: AtomicU64,
bytes_avoided_total: AtomicU64,
layers: Mutex<BTreeMap<u64, CsaLayerMetrics>>,
}
impl CsaMetrics {
pub fn record_layer(&self, layer_id: u64, sample: CsaLayerMetrics) {
self.device_bytes_total
.fetch_add(sample.device_bytes, Ordering::Relaxed);
self.host_bytes_total
.fetch_add(sample.host_bytes, Ordering::Relaxed);
self.bytes_avoided_total
.fetch_add(sample.bytes_avoided, Ordering::Relaxed);
let mut layers = self.layers.lock().expect("CSA metrics mutex poisoned");
let entry = layers.entry(layer_id).or_default();
let decode_count = entry.decode_count + 1;
*entry = sample;
entry.decode_count = decode_count;
}
pub fn record_rollback(&self) {
self.rollback_count.fetch_add(1, Ordering::Relaxed);
}
pub fn rollback_count(&self) -> u64 {
self.rollback_count.load(Ordering::Relaxed)
}
pub fn device_bytes_total(&self) -> u64 {
self.device_bytes_total.load(Ordering::Relaxed)
}
pub fn host_bytes_total(&self) -> u64 {
self.host_bytes_total.load(Ordering::Relaxed)
}
pub fn bytes_avoided_total(&self) -> u64 {
self.bytes_avoided_total.load(Ordering::Relaxed)
}
pub fn layer(&self, layer_id: u64) -> Option<CsaLayerMetrics> {
self.layers
.lock()
.expect("CSA metrics mutex poisoned")
.get(&layer_id)
.copied()
}
pub fn layer_count(&self) -> usize {
self.layers
.lock()
.expect("CSA metrics mutex poisoned")
.len()
}
}