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