use alloc::string::{String, ToString};
use alloc::vec::Vec;
use cubecl_environment::stream::StreamId;
#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct MemoryUsage {
pub number_allocs: u64,
pub bytes_in_use: u64,
pub bytes_padding: u64,
pub bytes_reserved: u64,
}
impl MemoryUsage {
pub fn combine(&self, other: MemoryUsage) -> MemoryUsage {
MemoryUsage {
number_allocs: self.number_allocs + other.number_allocs,
bytes_in_use: self.bytes_in_use + other.bytes_in_use,
bytes_padding: self.bytes_padding + other.bytes_padding,
bytes_reserved: self.bytes_reserved + other.bytes_reserved,
}
}
}
#[derive(new)]
#[doc(hidden)]
pub struct BytesFormat {
bytes: u64,
}
impl core::fmt::Display for BytesFormat {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
let unit = 1000;
if self.bytes < unit {
f.write_fmt(format_args!("{} B", self.bytes))
} else {
let size = self.bytes as f64;
let exp = match size.log(1000.0).floor() as usize {
0 => 1,
e => e,
};
let unit_prefix = "KMGTPEZY".as_bytes();
f.write_fmt(format_args!(
"{:.2} {}B",
(size / unit.pow(exp as u32) as f64),
unit_prefix[exp - 1] as char,
))
}
}
}
fn bytes_format(bytes: u64) -> String {
BytesFormat::new(bytes).to_string()
}
impl core::fmt::Display for MemoryUsage {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
let usage_percentage = (self.bytes_in_use as f32 / self.bytes_reserved as f32) * 100.0;
let padding_percentage = (self.bytes_padding as f32 / self.bytes_in_use as f32) * 100.0;
writeln!(f, "Memory Usage Report:")?;
writeln!(f, " Number of allocations: {}", self.number_allocs)?;
writeln!(f, " Bytes in use: {}", bytes_format(self.bytes_in_use))?;
writeln!(
f,
" Bytes used for padding: {}",
bytes_format(self.bytes_padding)
)?;
writeln!(
f,
" Total bytes reserved: {}",
bytes_format(self.bytes_reserved)
)?;
writeln!(f, " Usage efficiency: {usage_percentage:.2}%")?;
writeln!(f, " Padding overhead: {padding_percentage:.2}%")
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum MemoryPoolKind {
Sliced {
page_size: u64,
max_slice_size: u64,
},
Adaptive {
page_size: u64,
outdated_pages: u64,
},
Exclusive {
max_alloc_size: u64,
},
Direct,
Persistent,
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct MemoryPoolReport {
pub kind: MemoryPoolKind,
pub usage: MemoryUsage,
pub pages: u64,
pub pages_peak: u64,
pub pages_unmapped: u64,
pub largest_alloc: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MemoryScope {
Device,
CurrentStream,
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct MemoryReport {
pub streams: Vec<StreamMemoryReport>,
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct StreamMemoryReport {
pub stream: StreamId,
pub pools: MemoryPoolsReport,
pub auxiliary: Vec<AuxiliaryMemoryReport>,
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct AuxiliaryMemoryReport {
pub name: String,
pub pools: MemoryPoolsReport,
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct MemoryPoolsReport {
pub dynamic: Vec<MemoryPoolReport>,
pub persistent: MemoryPoolReport,
pub dedicated: MemoryPoolReport,
}
impl MemoryReport {
pub fn usage(&self) -> MemoryUsage {
self.streams
.iter()
.fold(MemoryUsage::default(), |usage, stream| {
usage.combine(stream.usage())
})
}
}
impl StreamMemoryReport {
pub fn usage(&self) -> MemoryUsage {
self.auxiliary
.iter()
.fold(self.pools.usage(), |usage, memory| {
usage.combine(memory.pools.usage())
})
}
}
impl MemoryPoolsReport {
pub fn usage(&self) -> MemoryUsage {
self.dynamic
.iter()
.chain([&self.persistent, &self.dedicated])
.fold(MemoryUsage::default(), |usage, pool| {
usage.combine(pool.usage.clone())
})
}
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct MemoryRecord {
pub label: alloc::string::String,
pub report: MemoryReport,
}
impl cubecl_environment::records::Record for MemoryRecord {
const KIND: &'static str = "memory";
}
pub trait MemoryHandle<Binding>: Clone + core::fmt::Debug {
fn can_mut(&self) -> bool;
fn binding(self) -> Binding;
}
#[cfg(test)]
mod tests {
use super::*;
use alloc::vec;
fn pool(bytes_in_use: u64) -> MemoryPoolReport {
MemoryPoolReport {
kind: MemoryPoolKind::Direct,
usage: MemoryUsage {
number_allocs: 1,
bytes_in_use,
bytes_padding: 0,
bytes_reserved: bytes_in_use,
},
pages: 1,
pages_peak: 1,
pages_unmapped: 0,
largest_alloc: bytes_in_use,
}
}
fn stream(value: u64, bytes: u64) -> StreamMemoryReport {
StreamMemoryReport {
stream: StreamId { value },
pools: MemoryPoolsReport {
dynamic: vec![pool(bytes)],
persistent: pool(bytes),
dedicated: pool(bytes),
},
auxiliary: Vec::new(),
}
}
#[test]
fn usage_sums_every_pool_of_every_stream() {
let report = MemoryReport {
streams: vec![stream(0, 1), stream(1, 10)],
};
assert_eq!(report.streams[0].usage().bytes_in_use, 3);
assert_eq!(report.usage().bytes_in_use, 33);
assert_eq!(report.usage().number_allocs, 6);
}
#[test]
fn usage_counts_auxiliary_memories() {
let mut report = stream(0, 1);
report.auxiliary.push(AuxiliaryMemoryReport {
name: "staging".to_string(),
pools: stream(0, 10).pools,
});
assert_eq!(report.usage().bytes_in_use, 33);
}
}