Skip to main content

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}