Skip to main content

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}