cubecl_common/pool.rs
1//! A dynamically-growing pool of single-cell items handed out as exclusive, non-cloneable handles.
2
3use crate::stub::{AtomicBool, Ordering};
4use alloc::boxed::Box;
5use alloc::vec::Vec;
6use core::cell::UnsafeCell;
7
8/// Resets a pooled value so the pool can hand it out again.
9pub trait Reclaim {
10 /// Clear the value in place, keeping its allocation for the next acquire.
11 fn reclaim(&mut self);
12}
13
14/// A pooled value that stays owned by the [`LeasePool`] even while checked out.
15///
16/// `leased` is the free/used flag; the value lives in an `UnsafeCell` so a handout can mutate it in
17/// place while the pool still holds a shared reference to the item.
18struct LeaseSlot<T: Default + Reclaim> {
19 leased: AtomicBool,
20 item: UnsafeCell<T>,
21}
22
23impl<T: Default + Reclaim> Default for LeaseSlot<T> {
24 fn default() -> Self {
25 Self {
26 leased: AtomicBool::new(false),
27 item: UnsafeCell::new(T::default()),
28 }
29 }
30}
31
32// SAFETY: `leased` guarantees at most one live `LeaseHandle` references an item at a
33// time, so writes through the `UnsafeCell` are never aliased by another writer, and the pool only
34// ever forms shared references to the item (to read/write `leased`).
35unsafe impl<T: Default + Reclaim> Send for LeaseSlot<T> {}
36unsafe impl<T: Default + Reclaim> Sync for LeaseSlot<T> {}
37
38/// Pool of reusable values with single-cell items that are not cloneable.
39///
40/// Items stay in the pool while checked out; [`Self::acquire`] hands back a
41/// [`LeaseHandle`] pointing at one and flags it `leased`. Dropping the handle reclaims the
42/// value and frees the slot, so allocations are reused across acquires instead of allocating anew.
43#[derive(Default)]
44pub struct LeasePool<T: Default + Reclaim> {
45 /// Boxed so each item keeps a stable address when the `Vec` grows — handles hold raw pointers
46 /// into these allocations, which must stay valid across `push`.
47 items: Vec<Box<LeaseSlot<T>>>,
48}
49
50impl<T: Default + Reclaim> core::fmt::Debug for LeasePool<T> {
51 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
52 f.debug_struct("LeasePool")
53 .field("items", &self.items.len())
54 .finish()
55 }
56}
57
58impl<T: Default + Reclaim> LeasePool<T> {
59 /// Pre-reserve room for `capacity` pooled items (if exceeded, the pool still grows on demand).
60 pub fn with_capacity(capacity: usize) -> Self {
61 Self {
62 items: Vec::with_capacity(capacity),
63 }
64 }
65
66 /// Take an item, reusing a free pooled item when available.
67 pub fn acquire(&mut self) -> LeaseHandle<T> {
68 let item: &LeaseSlot<T> = match self.items.iter().find(|item| {
69 item.leased
70 .compare_exchange(false, true, Ordering::Acquire, Ordering::Relaxed)
71 .is_ok()
72 }) {
73 Some(item) => item,
74 None => {
75 let item = Box::new(LeaseSlot::default());
76 item.leased.store(true, Ordering::Release);
77 self.items.push(item);
78 self.items.last().unwrap()
79 }
80 };
81
82 LeaseHandle { item }
83 }
84}
85
86/// Handle to a pooled item. Reclaims the value and frees its slot when dropped.
87///
88/// The pointee is owned by the [`LeasePool`], which never removes items, so it outlives
89/// every handle. An atomic `leased` flag keeps the reference unique for the handle's lifetime.
90pub struct LeaseHandle<T: Default + Reclaim> {
91 item: *const LeaseSlot<T>,
92}
93
94// SAFETY: the handle may be moved across threads by its holder. The pointee is `Send + Sync` and,
95// per the `leased` flag invariant, uniquely owned by this handle.
96unsafe impl<T: Default + Reclaim> Send for LeaseHandle<T> {}
97
98impl<T: Default + Reclaim> core::ops::Deref for LeaseHandle<T> {
99 type Target = T;
100
101 fn deref(&self) -> &Self::Target {
102 // SAFETY: unique access is guaranteed by the `leased` flag; the pointee outlives the handle.
103 unsafe { &*(*self.item).item.get() }
104 }
105}
106
107impl<T: Default + Reclaim> core::ops::DerefMut for LeaseHandle<T> {
108 fn deref_mut(&mut self) -> &mut Self::Target {
109 // SAFETY: see `deref`; `&mut self` guarantees no other reference through this handle.
110 unsafe { &mut *(*self.item).item.get() }
111 }
112}
113
114impl<T: Default + Reclaim> core::fmt::Debug for LeaseHandle<T> {
115 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
116 f.debug_struct("LeaseHandle").finish_non_exhaustive()
117 }
118}
119
120impl<T: Default + Reclaim> Drop for LeaseHandle<T> {
121 fn drop(&mut self) {
122 // SAFETY: the pool outlives the handle, so the pointee is still valid.
123 let item = unsafe { &*self.item };
124 // Reclaim any value not drained by the holder (e.g. a caller that failed mid-use), keeping
125 // the allocation for the next acquire.
126 unsafe { &mut *item.item.get() }.reclaim();
127 item.leased.store(false, Ordering::Release);
128 }
129}
130
131#[cfg(test)]
132mod tests {
133 use super::*;
134 use alloc::{vec, vec::Vec};
135
136 /// Stand-in pooled value: a `Vec` so we can observe capacity reuse and reclaim clearing.
137 #[derive(Default)]
138 struct Probe(Vec<u32>);
139
140 impl Reclaim for Probe {
141 fn reclaim(&mut self) {
142 self.0.clear();
143 }
144 }
145
146 impl<T: Default + Reclaim> LeasePool<T> {
147 fn len(&self) -> usize {
148 self.items.len()
149 }
150 }
151
152 /// The key soundness case: acquiring more items grows `items`, which must not invalidate
153 /// handles already handed out (they hold raw pointers into the boxed slots). Run under Miri
154 /// to check the aliasing model, not just the observable values.
155 #[test]
156 fn growth_keeps_live_handles_valid() {
157 // Start empty so every acquire pushes a new item, reallocating `items` while earlier
158 // handles are still live.
159 let mut pool = LeasePool::<Probe>::with_capacity(0);
160
161 let mut a = pool.acquire();
162 a.0.push(1);
163 let mut b = pool.acquire();
164 b.0.push(2);
165 let mut c = pool.acquire();
166 c.0.push(3);
167
168 // Write through the earliest handle again, after two growth-triggering pushes.
169 a.0.push(10);
170
171 assert_eq!(a.0, vec![1, 10]);
172 assert_eq!(b.0, vec![2]);
173 assert_eq!(c.0, vec![3]);
174 }
175
176 /// Dropping a handle reclaims (clears) the value but keeps its allocation for the next acquire.
177 #[test]
178 fn drop_reclaims_and_reuses_allocation() {
179 let mut pool = LeasePool::<Probe>::with_capacity(0);
180
181 let mut a = pool.acquire();
182 a.0.extend([1, 2, 3, 4]);
183 let cap = a.0.capacity();
184 assert!(cap >= 4);
185 drop(a);
186
187 // The only free slot is the one just released; it comes back cleared but with its
188 // allocation intact.
189 let reused = pool.acquire();
190 assert!(reused.0.is_empty());
191 assert_eq!(reused.0.capacity(), cap);
192 }
193
194 /// A released slot is reused on the next acquire rather than growing the pool.
195 #[test]
196 fn frees_slot_for_later_acquire() {
197 let mut pool = LeasePool::<Probe>::with_capacity(0);
198 for _ in 0..8 {
199 let mut handle = pool.acquire();
200 handle.0.push(0);
201 }
202 // Only ever one live handle at a time, so the pool holds exactly one item.
203 assert_eq!(pool.len(), 1);
204 }
205}