cubecl_server/memory_management/drop_queue/queue.rs
1use crate::server::ServerError;
2use alloc::vec::Vec;
3use cubecl_common::bytes::Bytes;
4
5use crate::memory_management::{
6 drop_queue::FlushingPolicy, drop_queue::policy::FlushingPolicyState,
7};
8
9/// A synchronization primitive that blocks until the device has finished
10/// processing all commands submitted before the fence was created.
11pub trait Fence: Sized {
12 /// Block the current thread until the device signals this fence.
13 ///
14 /// # Errors
15 ///
16 /// The fault the wait reveals, when the stream itself failed.
17 fn wait(self) -> Result<(), ServerError>;
18
19 /// [`wait`](Self::wait), ignoring a fault.
20 ///
21 /// What the drop queue needs: it only has to know the device is done
22 /// reading the memory it is about to free, and a stream that faulted is
23 /// done either way. The fault reaches the caller through whatever it
24 /// touches next.
25 fn sync(self) {
26 let _ = self.wait();
27 }
28}
29
30/// Defers the drop of CPU-side [`Bytes`] allocations until the device has
31/// finished reading them.
32///
33/// # How it works
34///
35/// The device uploads are asynchronous: after you copy bytes into a staging buffer
36/// and enqueue an upload command, the CPU memory must remain valid until the
37/// device is done. `PendingDropQueue` manages this lifetime with a two-phase
38/// approach:
39///
40/// 1. **Stage** – call [`push`](Self::push) to hand over bytes that are
41/// in-flight. They land in the `staged` list.
42/// 2. **Flush** – call [`flush`](Self::flush) to rotate the lists. The
43/// previously staged bytes move to `pending`, a new [`Fence`] is created
44/// to mark the end of the current upload batch, and any bytes that were
45/// *already* pending (i.e. the batch before that) are freed after syncing
46/// the previous fence.
47///
48/// This double-buffer scheme means CPU memory is held for at most two flush
49/// cycles, while avoiding any unnecessary stalls on the hot path.
50///
51/// # Flushing policy
52///
53/// Call [`should_flush`](Self::should_flush) to check whether enough bytes
54/// have accumulated to warrant a flush. You may also flush unconditionally
55/// (e.g. at the end of a frame).
56pub struct PendingDropQueue<E: Fence> {
57 /// Fence signalling that the device has consumed everything in `pending`.
58 fence: Option<E>,
59 /// Bytes from the *previous* flush cycle, kept alive until `event` fires.
60 pending: Vec<Bytes>,
61 /// Bytes queued in the *current* cycle, not yet associated with a fence.
62 staged: Vec<Bytes>,
63 /// The configuration of the queue.
64 policy: FlushingPolicy,
65 /// The current state of the policy.
66 policy_state: FlushingPolicyState,
67}
68
69impl<E: Fence> core::fmt::Debug for PendingDropQueue<E> {
70 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
71 f.debug_struct("PendingDropQueue")
72 .field("pending", &self.pending)
73 .field("staged", &self.staged)
74 .field("policy", &self.policy)
75 .field("policy_state", &self.policy_state)
76 .finish()
77 }
78}
79
80impl<E: Fence> Default for PendingDropQueue<E> {
81 fn default() -> Self {
82 Self::new(Default::default())
83 }
84}
85
86impl<F: Fence> PendingDropQueue<F> {
87 /// Creates a new `PendingDropQueue`.
88 pub fn new(policy: FlushingPolicy) -> Self {
89 Self {
90 fence: None,
91 pending: Vec::new(),
92 staged: Vec::new(),
93 policy,
94 policy_state: Default::default(),
95 }
96 }
97 /// Enqueue `bytes` to be dropped once the device has finished reading them.
98 ///
99 /// The bytes are added to the current staged batch and will be freed on
100 /// the flush cycle *after* the next call to [`flush`](Self::flush).
101 ///
102 /// Crate-visible, like every mutation of the queue: the only paths allowed
103 /// to move it are the shared [`Command`](crate::command::Command) and
104 /// [`Window`](crate::command::Window), which carry the capture-deferral
105 /// rule a backend touching the queue directly would have to remember.
106 pub(crate) fn push(&mut self, bytes: Bytes) {
107 self.policy_state.register(&bytes);
108 self.staged.push(bytes);
109 }
110
111 /// Returns `true` when the staged batch is large enough to justify a
112 /// flush.
113 pub(crate) fn should_flush(&self) -> bool {
114 self.policy_state.should_flush(&self.policy)
115 }
116
117 /// Flush until nothing is held back.
118 ///
119 /// One [`flush`](Self::flush) frees the batch staged two cycles ago and
120 /// rotates the current one into pending, so what was just dropped is still
121 /// held when it returns. A caller that needs the memory *now* — an
122 /// explicit cleanup, a capture window about to open onto pools that may
123 /// not allocate — wants both rotations.
124 pub(crate) fn drain<Factory: Fn() -> F>(&mut self, factory: Factory) {
125 self.flush(&factory);
126 self.flush(&factory);
127 }
128
129 /// Rotate the double-buffer and free any memory the device is done with.
130 ///
131 /// `factory` is called to produce a [`Fence`]. It should submit (or
132 /// record) a device signal command so that syncing the fence guarantees all
133 /// preceding device work is complete.
134 pub(crate) fn flush<Factory: Fn() -> F>(&mut self, factory: Factory) {
135 // An idle queue mints no fence. Nothing is held, so there is nothing a
136 // fence would protect — and the fence is not free: it is recorded on
137 // the stream, which an open capture window cannot tolerate.
138 if self.pending.is_empty() && self.staged.is_empty() {
139 return;
140 }
141
142 // Sync the fence from the previous flush and free the bytes it was
143 // protecting.
144 if let Some(event) = self.fence.take() {
145 event.sync();
146 self.pending.clear();
147 }
148
149 // Safety net: if pending is somehow still populated (no prior fence),
150 // stall immediately rather than freeing memory the GPU might still
151 // be reading.
152 if !self.pending.is_empty() {
153 let event = factory();
154 event.sync();
155 self.pending.clear();
156 }
157
158 // The current staged batch becomes the new pending batch.
159 core::mem::swap(&mut self.pending, &mut self.staged);
160
161 // Record a fence so the *next* flush knows when this batch is safe to
162 // free.
163 self.fence = Some(factory());
164 self.policy_state.reset();
165 }
166}
167
168#[cfg(test)]
169mod tests {
170 use super::*;
171 use alloc::vec;
172 use core::cell::Cell;
173
174 // ---------------------------------------------------------------------------
175 // Test helpers
176 // ---------------------------------------------------------------------------
177
178 #[derive(Clone)]
179 struct MockFence<'a> {
180 sync_count: &'a Cell<u32>,
181 }
182
183 impl Fence for MockFence<'_> {
184 fn wait(self) -> Result<(), ServerError> {
185 self.sync_count.set(self.sync_count.get() + 1);
186 Ok(())
187 }
188 }
189
190 fn make_queue<'a>(
191 sync_count: &'a Cell<u32>,
192 ) -> (
193 PendingDropQueue<MockFence<'a>>,
194 impl Fn() -> MockFence<'a> + 'a,
195 ) {
196 let queue = PendingDropQueue::new(test_policy());
197 let factory = move || MockFence { sync_count };
198 (queue, factory)
199 }
200
201 fn sample_bytes() -> Bytes {
202 Bytes::from_elems(vec![1u8, 2, 3])
203 }
204
205 fn test_policy() -> FlushingPolicy {
206 FlushingPolicy {
207 max_bytes_count: 2048,
208 max_bytes_size: 8,
209 }
210 }
211
212 // ---------------------------------------------------------------------------
213 // push / should_flush
214 // ---------------------------------------------------------------------------
215
216 #[test]
217 fn push_at_count_threshold_triggers_flush_hint() {
218 let sync_count = Cell::new(0u32);
219 let (mut queue, _factory) = make_queue(&sync_count);
220
221 for _ in 0..test_policy().max_bytes_count {
222 queue.push(sample_bytes());
223 }
224
225 assert!(queue.should_flush());
226 }
227
228 #[test]
229 fn push_large_allocation_triggers_flush_via_size_threshold() {
230 let sync_count = Cell::new(0u32);
231 let (mut queue, _factory) = make_queue(&sync_count);
232 let big = Bytes::from_elems(vec![0u8; test_policy().max_bytes_size as usize + 1]);
233
234 queue.push(big);
235
236 assert!(queue.should_flush());
237 }
238
239 // ---------------------------------------------------------------------------
240 // flush – fence / sync behaviour
241 // ---------------------------------------------------------------------------
242
243 #[test]
244 fn first_flush_creates_fence_without_syncing() {
245 let sync_count = Cell::new(0u32);
246 let (mut queue, factory) = make_queue(&sync_count);
247
248 queue.push(sample_bytes());
249 queue.flush(&factory);
250
251 // The fence is created but must not be synced yet — that happens on
252 // the next flush.
253 assert_eq!(
254 sync_count.get(),
255 0,
256 "fence should not be synced on first flush"
257 );
258 }
259
260 #[test]
261 fn second_flush_syncs_fence_from_first_flush() {
262 let sync_count = Cell::new(0u32);
263 let (mut queue, factory) = make_queue(&sync_count);
264
265 queue.push(sample_bytes());
266 queue.flush(&factory); // flush 1 – creates fence A
267
268 queue.push(sample_bytes());
269 queue.flush(&factory); // flush 2 – syncs fence A, creates fence B
270
271 assert_eq!(sync_count.get(), 1, "exactly one sync after two flushes");
272 }
273
274 #[test]
275 fn each_subsequent_flush_syncs_the_previous_fence() {
276 let sync_count = Cell::new(0u32);
277 let (mut queue, factory) = make_queue(&sync_count);
278
279 for _ in 0..10 {
280 queue.push(sample_bytes());
281 queue.flush(&factory);
282 }
283
284 // Each flush except the first syncs the fence from the previous one.
285 assert_eq!(sync_count.get(), 9);
286 }
287
288 // ---------------------------------------------------------------------------
289 // flush – buffer rotation
290 // ---------------------------------------------------------------------------
291
292 #[test]
293 fn staged_is_empty_after_flush() {
294 let sync_count = Cell::new(0u32);
295 let (mut queue, factory) = make_queue(&sync_count);
296
297 for _ in 0..5 {
298 queue.push(sample_bytes());
299 }
300 queue.flush(&factory);
301
302 assert!(queue.staged.is_empty());
303 }
304
305 #[test]
306 fn pending_holds_previously_staged_bytes_after_flush() {
307 let sync_count = Cell::new(0u32);
308 let (mut queue, factory) = make_queue(&sync_count);
309
310 for _ in 0..5 {
311 queue.push(sample_bytes());
312 }
313 queue.flush(&factory);
314
315 assert_eq!(queue.pending.len(), 5);
316 }
317
318 #[test]
319 fn pending_is_replaced_on_second_flush() {
320 let sync_count = Cell::new(0u32);
321 let (mut queue, factory) = make_queue(&sync_count);
322
323 for _ in 0..5 {
324 queue.push(sample_bytes());
325 }
326 queue.flush(&factory); // pending = 5 items
327
328 queue.push(sample_bytes());
329 queue.flush(&factory); // syncs fence → pending cleared, rotated
330
331 // Only the one item staged between the two flushes should be pending.
332 assert_eq!(queue.pending.len(), 1);
333 }
334
335 // ---------------------------------------------------------------------------
336 // flush – policy state reset
337 // ---------------------------------------------------------------------------
338
339 #[test]
340 fn should_flush_resets_after_flush() {
341 let sync_count = Cell::new(0u32);
342 let (mut queue, factory) = make_queue(&sync_count);
343
344 for _ in 0..test_policy().max_bytes_count {
345 queue.push(sample_bytes());
346 }
347 assert!(queue.should_flush());
348
349 queue.flush(&factory);
350
351 assert!(
352 !queue.should_flush(),
353 "policy state should be reset after flush"
354 );
355 }
356
357 // ---------------------------------------------------------------------------
358 // Edge cases
359 // ---------------------------------------------------------------------------
360
361 #[test]
362 fn flush_on_empty_queue_is_safe() {
363 let sync_count = Cell::new(0u32);
364 let (mut queue, factory) = make_queue(&sync_count);
365
366 // Should not panic regardless of how many times it is called.
367 queue.flush(&factory);
368 queue.flush(&factory);
369 queue.flush(&factory);
370 }
371}