use oxicuda_driver::error::{CudaError, CudaResult};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(u8)]
pub enum CompressionType {
None = 0,
Generic = 1,
}
impl CompressionType {
#[inline]
#[must_use]
pub fn is_compressed(self) -> bool {
matches!(self, Self::Generic)
}
}
impl std::fmt::Display for CompressionType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::None => write!(f, "none"),
Self::Generic => write!(f, "generic"),
}
}
}
pub const DEFAULT_COMPRESSION_GRANULARITY: usize = 2 * 1024 * 1024;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CompressionSupport {
supported: bool,
granularity: usize,
}
impl CompressionSupport {
#[must_use]
pub fn for_compute_capability(major: u32, minor: u32) -> Self {
let _ = minor;
Self {
supported: major >= 8,
granularity: DEFAULT_COMPRESSION_GRANULARITY,
}
}
#[must_use]
pub fn new(supported: bool, granularity: usize) -> Self {
Self {
supported,
granularity: granularity.max(1),
}
}
#[inline]
#[must_use]
pub fn is_supported(&self) -> bool {
self.supported
}
#[inline]
#[must_use]
pub fn granularity(&self) -> usize {
self.granularity
}
}
#[inline]
fn round_up(value: usize, granularity: usize) -> Option<usize> {
if value == 0 {
return Some(0);
}
let blocks = value.checked_add(granularity - 1)? / granularity;
blocks.checked_mul(granularity)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CompressionPlan {
requested_bytes: usize,
physical_bytes: usize,
granularity: usize,
}
impl CompressionPlan {
pub fn new(requested_bytes: usize, support: CompressionSupport) -> CudaResult<Self> {
if requested_bytes == 0 {
return Err(CudaError::InvalidValue);
}
if !support.is_supported() {
return Err(CudaError::NotSupported);
}
let physical_bytes =
round_up(requested_bytes, support.granularity()).ok_or(CudaError::InvalidValue)?;
Ok(Self {
requested_bytes,
physical_bytes,
granularity: support.granularity(),
})
}
#[inline]
#[must_use]
pub fn requested_bytes(&self) -> usize {
self.requested_bytes
}
#[inline]
#[must_use]
pub fn physical_bytes(&self) -> usize {
self.physical_bytes
}
#[inline]
#[must_use]
pub fn granularity(&self) -> usize {
self.granularity
}
#[inline]
#[must_use]
pub fn padding_bytes(&self) -> usize {
self.physical_bytes.saturating_sub(self.requested_bytes)
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct CompressedDeviceBuffer {
plan: CompressionPlan,
comp_type: CompressionType,
ratio: f64,
}
impl CompressedDeviceBuffer {
#[must_use]
pub fn new(plan: CompressionPlan) -> Self {
Self {
plan,
comp_type: CompressionType::Generic,
ratio: 1.0,
}
}
pub fn alloc(requested_bytes: usize, support: CompressionSupport) -> CudaResult<Self> {
Ok(Self::new(CompressionPlan::new(requested_bytes, support)?))
}
#[inline]
#[must_use]
pub fn plan(&self) -> CompressionPlan {
self.plan
}
#[inline]
#[must_use]
pub fn compression_type(&self) -> CompressionType {
self.comp_type
}
#[inline]
#[must_use]
pub fn logical_bytes(&self) -> usize {
self.plan.requested_bytes()
}
#[inline]
#[must_use]
pub fn physical_bytes(&self) -> usize {
self.plan.physical_bytes()
}
pub fn set_ratio(&mut self, ratio: f64) {
if ratio.is_finite() {
self.ratio = ratio.max(1.0);
}
}
#[inline]
#[must_use]
pub fn ratio(&self) -> f64 {
self.ratio
}
#[must_use]
pub fn effective_bus_bytes(&self) -> usize {
if self.ratio <= 1.0 {
return self.physical_bytes();
}
(self.physical_bytes() as f64 / self.ratio) as usize
}
#[must_use]
pub fn effective_bandwidth_gbps(&self, dram_gbps: f64) -> f64 {
if dram_gbps <= 0.0 {
return 0.0;
}
dram_gbps * self.ratio
}
#[inline]
#[must_use]
pub fn bytes_saved(&self) -> usize {
self.physical_bytes()
.saturating_sub(self.effective_bus_bytes())
}
}
impl std::fmt::Display for CompressedDeviceBuffer {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"CompressedDeviceBuffer(type={}, logical={} B, physical={} B, ratio={:.2})",
self.comp_type,
self.logical_bytes(),
self.physical_bytes(),
self.ratio,
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn compression_type_is_compressed() {
assert!(!CompressionType::None.is_compressed());
assert!(CompressionType::Generic.is_compressed());
}
#[test]
fn support_gated_on_compute_capability() {
assert!(CompressionSupport::for_compute_capability(8, 0).is_supported());
assert!(CompressionSupport::for_compute_capability(9, 0).is_supported());
assert!(!CompressionSupport::for_compute_capability(7, 5).is_supported());
assert!(!CompressionSupport::for_compute_capability(7, 0).is_supported());
assert!(!CompressionSupport::for_compute_capability(6, 1).is_supported());
}
#[test]
fn support_granularity_default() {
let s = CompressionSupport::for_compute_capability(8, 6);
assert_eq!(s.granularity(), 2 * 1024 * 1024);
}
#[test]
fn support_new_clamps_zero_granularity() {
let s = CompressionSupport::new(true, 0);
assert_eq!(s.granularity(), 1);
}
#[test]
fn round_up_exact_multiple_unchanged() {
let g = 2 * 1024 * 1024;
assert_eq!(round_up(g, g), Some(g));
assert_eq!(round_up(2 * g, g), Some(2 * g));
}
#[test]
fn round_up_partial_block_rounds_up() {
let g = 2 * 1024 * 1024;
assert_eq!(round_up(1, g), Some(g));
assert_eq!(round_up(g + 1, g), Some(2 * g));
}
#[test]
fn round_up_zero_is_zero() {
assert_eq!(round_up(0, 4096), Some(0));
}
#[test]
fn round_up_overflow_returns_none() {
assert_eq!(round_up(usize::MAX, 2 * 1024 * 1024), None);
}
#[test]
fn plan_rejects_zero() {
let s = CompressionSupport::for_compute_capability(8, 0);
assert_eq!(CompressionPlan::new(0, s), Err(CudaError::InvalidValue));
}
#[test]
fn plan_rejects_unsupported_device() {
let s = CompressionSupport::for_compute_capability(7, 5);
assert_eq!(CompressionPlan::new(4096, s), Err(CudaError::NotSupported));
}
#[test]
fn plan_aligns_to_granularity() {
let s = CompressionSupport::for_compute_capability(8, 0);
let g = s.granularity();
let plan = CompressionPlan::new(5 * 1024 * 1024, s).expect("plan");
assert_eq!(plan.requested_bytes(), 5 * 1024 * 1024);
assert_eq!(plan.physical_bytes(), 3 * g);
assert_eq!(plan.physical_bytes() % g, 0);
assert_eq!(plan.padding_bytes(), 1024 * 1024);
}
#[test]
fn plan_exact_multiple_has_no_padding() {
let s = CompressionSupport::for_compute_capability(8, 0);
let plan = CompressionPlan::new(s.granularity(), s).expect("plan");
assert_eq!(plan.padding_bytes(), 0);
assert_eq!(plan.physical_bytes(), s.granularity());
}
#[test]
fn buffer_alloc_defaults_to_generic_ratio_one() {
let s = CompressionSupport::for_compute_capability(8, 0);
let buf = CompressedDeviceBuffer::alloc(1024 * 1024, s).expect("alloc");
assert_eq!(buf.compression_type(), CompressionType::Generic);
assert!((buf.ratio() - 1.0).abs() < 1e-12);
assert_eq!(buf.effective_bus_bytes(), buf.physical_bytes());
assert_eq!(buf.bytes_saved(), 0);
}
#[test]
fn buffer_ratio_clamped_and_effective_bytes() {
let s = CompressionSupport::for_compute_capability(8, 0);
let mut buf = CompressedDeviceBuffer::alloc(2 * 1024 * 1024, s).expect("alloc");
assert_eq!(buf.physical_bytes(), 2 * 1024 * 1024);
buf.set_ratio(2.0);
assert!((buf.ratio() - 2.0).abs() < 1e-12);
assert_eq!(buf.effective_bus_bytes(), 1024 * 1024);
assert_eq!(buf.bytes_saved(), 1024 * 1024);
}
#[test]
fn buffer_ratio_below_one_clamped() {
let s = CompressionSupport::for_compute_capability(8, 0);
let mut buf = CompressedDeviceBuffer::alloc(2 * 1024 * 1024, s).expect("alloc");
buf.set_ratio(0.5);
assert!((buf.ratio() - 1.0).abs() < 1e-12);
}
#[test]
fn buffer_ignores_non_finite_ratio() {
let s = CompressionSupport::for_compute_capability(8, 0);
let mut buf = CompressedDeviceBuffer::alloc(2 * 1024 * 1024, s).expect("alloc");
buf.set_ratio(3.0);
buf.set_ratio(f64::NAN);
buf.set_ratio(f64::INFINITY);
assert!((buf.ratio() - 3.0).abs() < 1e-12);
}
#[test]
fn buffer_effective_bandwidth_scales_with_ratio() {
let s = CompressionSupport::for_compute_capability(8, 0);
let mut buf = CompressedDeviceBuffer::alloc(2 * 1024 * 1024, s).expect("alloc");
buf.set_ratio(2.5);
assert!((buf.effective_bandwidth_gbps(1000.0) - 2500.0).abs() < 1e-9);
assert_eq!(buf.effective_bandwidth_gbps(0.0), 0.0);
}
#[test]
fn buffer_display_contains_fields() {
let s = CompressionSupport::for_compute_capability(8, 0);
let buf = CompressedDeviceBuffer::alloc(1024 * 1024, s).expect("alloc");
let text = format!("{buf}");
assert!(text.contains("generic"));
assert!(text.contains("physical="));
}
}