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), 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}