Skip to main content

cubecl_runtime/memory_management/
handle.rs

1use crate::memory_management::MemoryHandle;
2use alloc::{sync::Arc, vec::Vec};
3use core::sync::atomic::{AtomicU64, Ordering};
4
5/// Managed Memory handle
6#[derive(Debug)]
7pub struct ManagedMemoryHandle {
8    descriptor: Arc<ManagedMemoryDescriptor>,
9    // Holds only the reference counts of the handle.
10    handle_count: Arc<()>,
11}
12
13/// Binding of a memory handle
14#[derive(Debug)]
15pub struct ManagedMemoryBinding {
16    descriptor: Arc<ManagedMemoryDescriptor>,
17}
18
19/// A list of bindings that are shared across multiple streams.
20#[derive(Debug, Default)]
21pub struct SharedMemoryBindings {
22    /// The bindings.
23    pub bindings: Vec<ManagedMemoryBinding>,
24}
25
26impl Clone for ManagedMemoryHandle {
27    fn clone(&self) -> Self {
28        Self {
29            descriptor: self.descriptor.clone(),
30            handle_count: self.handle_count.clone(),
31        }
32    }
33}
34
35/// Managed memory descriptor.
36///
37/// Multiple handles share the same descriptor via `Arc`, yet the memory
38/// management system needs to update the location after creation (e.g. during
39/// `reserve` / `bind`). The location is packed into an atomic for that.
40///
41/// All mutation happens on the device thread, so `Relaxed` is all the ordering
42/// it needs — and on the targets that matter that is a plain load and store.
43/// Being atomic rather than a `Cell` is what makes the descriptor `Sync` by
44/// construction: these methods are public so the pools in `cubecl-server` can
45/// reach them, and anything public can be called from any thread.
46#[doc(hidden)]
47pub struct ManagedMemoryDescriptor {
48    #[doc(hidden)]
49    pub id: ManagedMemoryId,
50    location: AtomicU64,
51}
52
53impl core::fmt::Debug for ManagedMemoryDescriptor {
54    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
55        f.debug_struct("ManagedMemoryDescriptor")
56            .field("id", &self.id)
57            .field("location", &self.location())
58            .finish()
59    }
60}
61
62#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Hash)]
63/// Managed memory unique identifier.
64pub struct ManagedMemoryId {
65    #[doc(hidden)]
66    pub value: usize,
67}
68
69impl PartialEq for ManagedMemoryDescriptor {
70    fn eq(&self, other: &Self) -> bool {
71        self.id == other.id
72    }
73}
74
75impl Eq for ManagedMemoryDescriptor {}
76
77#[derive(Clone, Copy, Debug)]
78/// Defines where the [`ManagedMemoryId`] is located.
79#[doc(hidden)]
80pub struct MemoryLocation {
81    /// The memory pool index in the global memory management.
82    pub pool: u8,
83    /// The memory page index in a memory pool.
84    pub page: u16,
85    /// The memory slice index in a memory page.
86    pub slice: u32,
87    /// Whether the memory location is known/initialized.
88    pub init: u8,
89}
90
91impl ManagedMemoryDescriptor {
92    /// Update the memory location for the given [`ManagedMemoryId`].
93    #[doc(hidden)]
94    pub fn update_location(&self, location: MemoryLocation) {
95        self.location.store(location.to_bits(), Ordering::Relaxed);
96    }
97
98    /// Update only the slice position for the given [`ManagedMemoryId`].
99    #[doc(hidden)]
100    pub fn update_slice(&self, slice: u32) {
101        self.modify(|location| MemoryLocation { slice, ..location });
102    }
103
104    /// Update only the memory page position for the given [`ManagedMemoryId`].
105    #[doc(hidden)]
106    pub fn update_page(&self, page: u16) {
107        self.modify(|location| MemoryLocation { page, ..location });
108    }
109
110    /// Retrieves the current location.
111    #[doc(hidden)]
112    pub fn location(&self) -> MemoryLocation {
113        MemoryLocation::from_bits(self.location.load(Ordering::Relaxed))
114    }
115
116    /// Whether a reservation ever gave this memory a location.
117    ///
118    /// A handle is minted before its memory exists, and the reservation that
119    /// follows can fail: a full device, a size no pool accepts.
120    #[doc(hidden)]
121    pub fn is_allocated(&self) -> bool {
122        self.location().init != 0
123    }
124
125    #[doc(hidden)]
126    pub fn slice(&self) -> usize {
127        self.location().slice as usize
128    }
129
130    #[doc(hidden)]
131    pub fn page(&self) -> usize {
132        self.location().page as usize
133    }
134
135    fn modify(&self, update: impl Fn(MemoryLocation) -> MemoryLocation) {
136        // Never `Err`: the closure always has an update to make.
137        let _ = self
138            .location
139            .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |bits| {
140                Some(update(MemoryLocation::from_bits(bits)).to_bits())
141            });
142    }
143}
144
145impl MemoryLocation {
146    /// The location packed into one word, so it can live in an atomic: pool
147    /// in the low byte, then page, then slice, then the init flag on top.
148    fn to_bits(self) -> u64 {
149        self.pool as u64
150            | (self.page as u64) << 8
151            | (self.slice as u64) << 24
152            | (self.init as u64) << 56
153    }
154
155    fn from_bits(bits: u64) -> Self {
156        Self {
157            pool: bits as u8,
158            page: (bits >> 8) as u16,
159            slice: (bits >> 24) as u32,
160            init: (bits >> 56) as u8,
161        }
162    }
163
164    /// Creates a new memory location.
165    #[doc(hidden)]
166    pub fn new(pool: u8, page: u16, slice: u32) -> Self {
167        Self {
168            pool,
169            page,
170            slice,
171            init: 1,
172        }
173    }
174
175    /// Creates a new uninitialized memory location.
176    #[doc(hidden)]
177    pub fn uninit() -> Self {
178        Self {
179            pool: 0,
180            page: 0,
181            slice: 0,
182            init: 0,
183        }
184    }
185}
186
187impl ManagedMemoryHandle {
188    /// Creates a new managed memory handle.
189    pub fn new() -> Self {
190        let value = Self::gen_id();
191
192        Self {
193            descriptor: Arc::new(ManagedMemoryDescriptor {
194                id: ManagedMemoryId { value },
195                location: AtomicU64::new(MemoryLocation::uninit().to_bits()),
196            }),
197            handle_count: Arc::new(()),
198        }
199    }
200
201    /// Retrieves the descriptor for the current handle.
202    #[doc(hidden)]
203    pub fn descriptor(&self) -> &ManagedMemoryDescriptor {
204        &self.descriptor
205    }
206
207    /// Return whether the current handle can be modified in-place.
208    pub fn can_mut(&self) -> bool {
209        Arc::strong_count(&self.handle_count) <= 2
210    }
211
212    /// Return whether the current handle is free.
213    pub fn is_free(&self) -> bool {
214        Arc::strong_count(&self.descriptor) <= 1
215    }
216
217    /// Returns the binding for the current handle.
218    pub fn binding(self) -> ManagedMemoryBinding {
219        ManagedMemoryBinding {
220            descriptor: self.descriptor.clone(),
221        }
222    }
223
224    fn gen_id() -> usize {
225        static COUNTER: core::sync::atomic::AtomicUsize = core::sync::atomic::AtomicUsize::new(0);
226        let value = COUNTER.fetch_add(1, core::sync::atomic::Ordering::Relaxed);
227        if value == usize::MAX {
228            core::panic!("Memory ID overflowed");
229        }
230        value
231    }
232}
233
234impl ManagedMemoryBinding {
235    /// Retrieves the descriptor for the current binding.
236    #[doc(hidden)]
237    pub fn descriptor(&self) -> &ManagedMemoryDescriptor {
238        &self.descriptor
239    }
240
241    /// The id of the memory this binding is bound to, stable for as long as the
242    /// allocation lives and never reused by a later one.
243    pub fn id(&self) -> ManagedMemoryId {
244        self.descriptor.id
245    }
246}
247
248impl Default for ManagedMemoryHandle {
249    fn default() -> Self {
250        Self::new()
251    }
252}
253
254impl Clone for ManagedMemoryBinding {
255    fn clone(&self) -> Self {
256        Self {
257            descriptor: self.descriptor.clone(),
258        }
259    }
260}
261
262impl MemoryHandle<ManagedMemoryBinding> for ManagedMemoryHandle {
263    fn can_mut(&self) -> bool {
264        self.can_mut()
265    }
266
267    fn binding(self) -> ManagedMemoryBinding {
268        self.binding()
269    }
270}
271
272impl SharedMemoryBindings {
273    /// Clears the shared bindings list.
274    pub fn clear(&mut self) {
275        self.bindings.clear();
276    }
277
278    /// Returns true if the shared bindings list is empty.
279    pub fn is_empty(&self) -> bool {
280        self.bindings.is_empty()
281    }
282
283    /// Push a memory binding to the list of shared bindings.
284    pub fn push(&mut self, binding: ManagedMemoryBinding) {
285        self.bindings.push(binding)
286    }
287}
288
289impl cubecl_common::pool::Reclaim for SharedMemoryBindings {
290    fn reclaim(&mut self) {
291        self.clear();
292    }
293}
294
295/// Calculates a best-effort heuristic for the alignment of row-aligned tensors.
296/// Prefers contiguous alignments for unit dimensions, 16-byte minimum alignment for non-unit,
297/// scaling with input size up to `buffer_align`.
298pub fn optimal_align(shape: usize, elem_size: usize, buffer_align: usize) -> usize {
299    if shape == 1 {
300        elem_size
301    } else {
302        (shape * elem_size)
303            .next_power_of_two()
304            .clamp(16, buffer_align)
305    }
306}
307
308#[cfg(test)]
309mod tests {
310    use super::*;
311
312    #[test]
313    fn test_memory_id_mutability() {
314        let handle1 = ManagedMemoryHandle::new();
315        handle1.descriptor().update_slice(4);
316        assert_eq!(handle1.descriptor().slice(), 4);
317
318        let handle2 = ManagedMemoryHandle::new();
319        handle2
320            .clone()
321            .descriptor()
322            .update_location(handle1.descriptor().location());
323        assert_eq!(handle2.descriptor().slice(), 4);
324    }
325
326    #[test]
327    fn test_location_visible_through_shared_arc() {
328        let handle = ManagedMemoryHandle::new();
329        let handle2 = handle.clone();
330
331        let location = MemoryLocation::new(1, 2, 3);
332        handle.descriptor().update_location(location);
333
334        assert_eq!(handle2.descriptor().location().pool, 1);
335        assert_eq!(handle2.descriptor().location().page, 2);
336        assert_eq!(handle2.descriptor().location().slice, 3);
337        assert_eq!(handle2.descriptor().location().init, 1);
338
339        handle.descriptor().update_slice(42);
340        assert_eq!(handle2.descriptor().slice(), 42);
341    }
342
343    /// Every field gets its own bits in the packed word, so none bleeds into
344    /// its neighbour even at its widest.
345    #[test]
346    fn a_location_survives_packing_at_every_extreme() {
347        let fields = |location: MemoryLocation| {
348            (location.pool, location.page, location.slice, location.init)
349        };
350
351        for location in [
352            MemoryLocation::uninit(),
353            MemoryLocation::new(1, 2, 3),
354            MemoryLocation::new(u8::MAX, u16::MAX, u32::MAX),
355            MemoryLocation {
356                init: u8::MAX,
357                ..MemoryLocation::uninit()
358            },
359        ] {
360            let packed = MemoryLocation::from_bits(location.to_bits());
361
362            assert_eq!(fields(packed), fields(location));
363        }
364    }
365}