cubecl_runtime/memory_management/base.rs
1use alloc::string::{String, ToString};
2use alloc::vec::Vec;
3use cubecl_environment::stream::StreamId;
4
5/// Amount of memory in use by this allocator
6/// and statistics on how much memory is reserved and
7/// wasted in total.
8#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
9pub struct MemoryUsage {
10 /// The number of allocations currently active.
11 ///
12 /// This is not the number of times an actual allocation happens to create a new memory page,
13 /// but really the number of active slices.
14 pub number_allocs: u64,
15 /// The number of bytes that are currently actually in use.
16 ///
17 /// This doesn't include any padding or other memory that needs to be
18 /// reserved, and is the minimum amount of memory that could possible
19 /// be allocated.
20 pub bytes_in_use: u64,
21 /// The amount of bytes used for padding memory in currently active allocations.
22 pub bytes_padding: u64,
23 /// The total amount of memory reserved on the device.
24 ///
25 /// This will be at least as much as `bytes_in_use` but in practice will
26 /// be higher, as allocations reserve memory for future allocations
27 /// and for padding.
28 pub bytes_reserved: u64,
29}
30
31impl MemoryUsage {
32 /// Calculate the combined memory usage of two reports (summing them).
33 pub fn combine(&self, other: MemoryUsage) -> MemoryUsage {
34 MemoryUsage {
35 number_allocs: self.number_allocs + other.number_allocs,
36 bytes_in_use: self.bytes_in_use + other.bytes_in_use,
37 bytes_padding: self.bytes_padding + other.bytes_padding,
38 bytes_reserved: self.bytes_reserved + other.bytes_reserved,
39 }
40 }
41}
42
43#[derive(new)]
44#[doc(hidden)]
45pub struct BytesFormat {
46 bytes: u64,
47}
48
49impl core::fmt::Display for BytesFormat {
50 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
51 let unit = 1000;
52
53 if self.bytes < unit {
54 f.write_fmt(format_args!("{} B", self.bytes))
55 } else {
56 let size = self.bytes as f64;
57 let exp = match size.log(1000.0).floor() as usize {
58 0 => 1,
59 e => e,
60 };
61 let unit_prefix = "KMGTPEZY".as_bytes();
62 f.write_fmt(format_args!(
63 "{:.2} {}B",
64 (size / unit.pow(exp as u32) as f64),
65 unit_prefix[exp - 1] as char,
66 ))
67 }
68 }
69}
70
71fn bytes_format(bytes: u64) -> String {
72 BytesFormat::new(bytes).to_string()
73}
74
75impl core::fmt::Display for MemoryUsage {
76 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
77 // In the future it'd be nice if MemoryUsage also held some stats about say,
78 // the 5 biggest allocations, to show when you an OOM.
79 let usage_percentage = (self.bytes_in_use as f32 / self.bytes_reserved as f32) * 100.0;
80 let padding_percentage = (self.bytes_padding as f32 / self.bytes_in_use as f32) * 100.0;
81 writeln!(f, "Memory Usage Report:")?;
82 writeln!(f, " Number of allocations: {}", self.number_allocs)?;
83 writeln!(f, " Bytes in use: {}", bytes_format(self.bytes_in_use))?;
84 writeln!(
85 f,
86 " Bytes used for padding: {}",
87 bytes_format(self.bytes_padding)
88 )?;
89 writeln!(
90 f,
91 " Total bytes reserved: {}",
92 bytes_format(self.bytes_reserved)
93 )?;
94 writeln!(f, " Usage efficiency: {usage_percentage:.2}%")?;
95 writeln!(f, " Padding overhead: {padding_percentage:.2}%")
96 }
97}
98
99/// The pool shape a [`MemoryPoolReport`] describes, carrying the pool's
100/// effective configuration (after alignment rounding and page-size shrinking).
101#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
102pub enum MemoryPoolKind {
103 /// Allocations are slices carved from shared pages.
104 Sliced {
105 /// The size of each device page.
106 page_size: u64,
107 /// The largest allocation the pool accepts.
108 max_slice_size: u64,
109 },
110 /// Slices carved from pages sized after the largest allocation served.
111 Adaptive {
112 /// The size new pages are allocated at.
113 page_size: u64,
114 /// Pages held at an older, smaller size, waiting on their last live
115 /// slice before they are returned to the driver.
116 outdated_pages: u64,
117 },
118 /// Every allocation is its own device page.
119 Exclusive {
120 /// The largest allocation the pool accepts.
121 max_alloc_size: u64,
122 },
123 /// One device allocation per reservation, sized to the request, reused by
124 /// exact size and returned to the driver only under memory pressure.
125 /// Wastes only alignment padding, and pays a driver allocation per
126 /// distinct size rather than per page.
127 Direct,
128 /// Exact-fit slices that are reused only by identical size.
129 Persistent,
130}
131
132/// A structured snapshot of one memory pool: its shape, its current usage, and
133/// the high-water marks a memory plan is derived from.
134#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
135pub struct MemoryPoolReport {
136 /// The pool's shape and effective configuration.
137 pub kind: MemoryPoolKind,
138 /// The pool's current usage.
139 pub usage: MemoryUsage,
140 /// Device allocations (pages) currently held.
141 pub pages: u64,
142 /// The most device allocations ever held at once: for a sliced pool, the
143 /// pages the workload needed at its peak.
144 pub pages_peak: u64,
145 /// How many of the current pages have no device backing yet — carved
146 /// under a dry run and never resolved into anything that executes. They
147 /// count toward `pages`/`pages_peak` (the plan is the *reserved* stream)
148 /// while costing no device memory; `pages - pages_unmapped` is the dry
149 /// run's actual footprint in this pool.
150 pub pages_unmapped: u64,
151 /// The largest single allocation this pool ever served, in requested
152 /// (pre-padding) bytes.
153 pub largest_alloc: u64,
154}
155
156/// Which memory a [`MemoryReport`] covers.
157#[derive(Debug, Clone, Copy, PartialEq, Eq)]
158pub enum MemoryScope {
159 /// Every stream's memory on the device.
160 Device,
161 /// The memory of the stream the client issues on.
162 CurrentStream,
163}
164
165/// Everything the memory of a [`MemoryScope`] holds, stream by stream: the
166/// single place memory is read from. A caller that wants the totals asks for
167/// the [`usage`](Self::usage).
168///
169/// A tuning pass allocates like anything else, so its scratch counts toward
170/// these marks; warming the tune caches in an earlier pass leaves the peaks of
171/// a measured one to the workload alone.
172#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
173pub struct MemoryReport {
174 /// One entry per stream the scope covers.
175 pub streams: Vec<StreamMemoryReport>,
176}
177
178/// What one stream's memory holds.
179#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
180pub struct StreamMemoryReport {
181 /// The stream the memory belongs to.
182 pub stream: StreamId,
183 /// The pools every allocation a user makes lives in.
184 pub pools: MemoryPoolsReport,
185 /// The memories a runtime keeps beside those pools for its own use, such
186 /// as staging buffers for reads or uniform buffers for launches. Empty
187 /// where the runtime keeps none.
188 pub auxiliary: Vec<AuxiliaryMemoryReport>,
189}
190
191/// A memory a runtime keeps for its own use, and what it holds.
192#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
193pub struct AuxiliaryMemoryReport {
194 /// What the memory is for.
195 pub name: String,
196 /// Its pools.
197 pub pools: MemoryPoolsReport,
198}
199
200/// What a memory holds, pool by pool.
201#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
202pub struct MemoryPoolsReport {
203 /// One entry per dynamic pool, in allocation-routing order, the pools a
204 /// growth left behind last.
205 pub dynamic: Vec<MemoryPoolReport>,
206 /// The persistent pool (weights, caches; explicit persistent windows).
207 pub persistent: MemoryPoolReport,
208 /// The dedicated allocations, each its own device allocation.
209 pub dedicated: MemoryPoolReport,
210}
211
212impl MemoryReport {
213 /// The usage of every pool of every stream together.
214 pub fn usage(&self) -> MemoryUsage {
215 self.streams
216 .iter()
217 .fold(MemoryUsage::default(), |usage, stream| {
218 usage.combine(stream.usage())
219 })
220 }
221}
222
223impl StreamMemoryReport {
224 /// The usage of this stream's pools together, the auxiliary memories'
225 /// included.
226 pub fn usage(&self) -> MemoryUsage {
227 self.auxiliary
228 .iter()
229 .fold(self.pools.usage(), |usage, memory| {
230 usage.combine(memory.pools.usage())
231 })
232 }
233}
234
235impl MemoryPoolsReport {
236 /// The usage of every pool together.
237 pub fn usage(&self) -> MemoryUsage {
238 self.dynamic
239 .iter()
240 .chain([&self.persistent, &self.dedicated])
241 .fold(MemoryUsage::default(), |usage, pool| {
242 usage.combine(pool.usage.clone())
243 })
244 }
245}
246
247/// A [`MemoryReport`] as the environment records it: a snapshot of every
248/// stream's memory on the device at a moment the caller named, written by
249/// [`Client::record_memory`](crate::client::Client::record_memory).
250#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
251pub struct MemoryRecord {
252 /// What the caller was doing: `model loaded`, `after the dry run`.
253 pub label: alloc::string::String,
254 /// The device's memory at that moment, stream by stream.
255 pub report: MemoryReport,
256}
257
258impl cubecl_environment::records::Record for MemoryRecord {
259 const KIND: &'static str = "memory";
260}
261
262/// The managed tensor buffer handle that points to some memory segment.
263/// It should not contain actual data.
264pub trait MemoryHandle<Binding>: Clone + core::fmt::Debug {
265 /// Checks if the underlying memory can be safely mutated.
266 fn can_mut(&self) -> bool;
267 /// Get the binding associated to the current handle.
268 fn binding(self) -> Binding;
269}
270
271#[cfg(test)]
272mod tests {
273 use super::*;
274 use alloc::vec;
275
276 fn pool(bytes_in_use: u64) -> MemoryPoolReport {
277 MemoryPoolReport {
278 kind: MemoryPoolKind::Direct,
279 usage: MemoryUsage {
280 number_allocs: 1,
281 bytes_in_use,
282 bytes_padding: 0,
283 bytes_reserved: bytes_in_use,
284 },
285 pages: 1,
286 pages_peak: 1,
287 pages_unmapped: 0,
288 largest_alloc: bytes_in_use,
289 }
290 }
291
292 fn stream(value: u64, bytes: u64) -> StreamMemoryReport {
293 StreamMemoryReport {
294 stream: StreamId { value },
295 pools: MemoryPoolsReport {
296 dynamic: vec![pool(bytes)],
297 persistent: pool(bytes),
298 dedicated: pool(bytes),
299 },
300 auxiliary: Vec::new(),
301 }
302 }
303
304 /// A device report is the sum of its streams, each the sum of its pools.
305 #[test]
306 fn usage_sums_every_pool_of_every_stream() {
307 let report = MemoryReport {
308 streams: vec![stream(0, 1), stream(1, 10)],
309 };
310 assert_eq!(report.streams[0].usage().bytes_in_use, 3);
311 assert_eq!(report.usage().bytes_in_use, 33);
312 assert_eq!(report.usage().number_allocs, 6);
313 }
314
315 /// The memories a runtime keeps for itself count toward the stream.
316 #[test]
317 fn usage_counts_auxiliary_memories() {
318 let mut report = stream(0, 1);
319 report.auxiliary.push(AuxiliaryMemoryReport {
320 name: "staging".to_string(),
321 pools: stream(0, 10).pools,
322 });
323 assert_eq!(report.usage().bytes_in_use, 33);
324 }
325}