Skip to main content

cubecl_server/stream/
event.rs

1use crate::{
2    config::streaming::StreamingLogLevel,
3    logging::ServerLogger,
4    memory_management::{
5        ErrorGraph, FailureId, ManagedMemoryId, SharedMemoryBindings, release_or_leak,
6    },
7    server::{BufferBinding, ServerError},
8    stream::{FailureStore, Failures, StreamFactory, StreamMemory, StreamPool, base},
9};
10use core::any::Any;
11use cubecl_environment::collections::HashMap;
12use cubecl_environment::stream::StreamId;
13use std::{
14    boxed::Box,
15    format,
16    sync::{Arc, mpsc::SyncSender},
17    vec::Vec,
18};
19
20/// Trait defining the backend operations for managing streams and events.
21///
22/// This trait provides the necessary methods for initializing streams, flushing them to create events,
23/// and waiting on events for synchronization purposes.
24pub trait EventStreamBackend: 'static {
25    /// The type representing a stream in this backend, which exposes the
26    /// memory its kernels see through [`StreamMemory`].
27    type Stream: core::fmt::Debug + StreamMemory;
28    /// The type representing an event in this backend.
29    type Event: Send + 'static;
30
31    /// Initializes and returns a new stream associated with the given stream ID.
32    ///
33    /// # Errors
34    ///
35    /// Returns a [`ServerError`] when the driver cannot create the stream, e.g. on a device that
36    /// is poisoned.
37    fn create_stream(&self) -> Result<Self::Stream, ServerError>;
38    /// Returns the cursor of the given handle on the given stream.
39    fn handle_cursor(stream: &Self::Stream, handle: &BufferBinding) -> u64;
40    /// Flushes the given stream, ensuring all pending operations are submitted, and returns an event
41    /// that can be used for synchronization.
42    ///
43    /// `failures` is the device's failure store, handed down because a flush
44    /// may drive the stream's periodic memory cleanup, and cleaning up is
45    /// where a pool sheds the failures its slices carry.
46    fn flush(stream: &mut Self::Stream, failures: &mut ErrorGraph) -> Self::Event;
47    /// Makes the stream wait for the specified event to complete before proceeding with further operations.
48    ///
49    /// # Errors
50    ///
51    /// Returns a [`ServerError`] when the dependency cannot be honoured, because the work the
52    /// event marks failed.
53    fn wait_event(stream: &mut Self::Stream, event: Self::Event) -> Result<(), ServerError>;
54    /// Wait for the given event synching the CPU.
55    fn wait_event_sync(event: Self::Event) -> Result<(), ServerError>;
56}
57
58/// Manages multiple streams with synchronization logic based on shared bindings.
59///
60/// This struct handles the creation and alignment of streams to ensure proper synchronization
61/// when bindings (e.g., buffers) are shared across different streams.
62#[derive(Debug)]
63pub struct MultiStream<B: EventStreamBackend> {
64    /// The map of stream IDs to their corresponding stream wrappers.
65    streams: StreamPool<EventStreamBackendWrapper<B>>,
66    /// Every failure the device is still holding, and the write scope's
67    /// scratch — see [`Failures`].
68    failures: Failures,
69    /// The logger used by the server.
70    pub logger: Arc<ServerLogger>,
71    max_streams: usize,
72    gc: GcThread<B>,
73    shared_bindings_pool: Vec<(ManagedMemoryId, StreamId, u64)>,
74}
75
76/// A wrapper around a backend stream that includes synchronization metadata.
77///
78/// This includes the stream itself, a map of last synchronized cursors from other streams,
79/// and the current cursor position for this stream.
80/// A backend stream plus the synchronization metadata the pool keeps beside
81/// it. Public only because it is the pool's stream type in
82/// [`FailureStore::Factory`]; backends reach the stream itself, not this.
83pub struct StreamWrapper<B: EventStreamBackend> {
84    /// The underlying backend stream.
85    stream: B::Stream,
86    /// The current cursor position, representing the logical progress or version of operations on this stream.
87    cursor: u64,
88    /// A map tracking the last synchronized cursor positions from other streams.
89    last_synced: HashMap<usize, u64>,
90}
91
92/// Streams that are synchronized correctly after a [`MultiStream::resolve`] is called.
93pub struct ResolvedStreams<'a, B: EventStreamBackend> {
94    /// The cursor on the current stream.
95    ///
96    /// This cursor should be use for new allocations happening on the current stream.
97    pub cursor: u64,
98    streams: &'a mut StreamPool<EventStreamBackendWrapper<B>>,
99    failures: &'a mut ErrorGraph,
100    analysis: SharedBindingAnalysis,
101    gc: &'a GcThread<B>,
102    /// The current stream where new tasks can be sent safely.
103    pub current: StreamId,
104}
105
106#[derive(Debug)]
107/// A task to be enqueue on the gc stream that will be clearned after an event is reached.
108pub struct GcTask<B: EventStreamBackend> {
109    to_drop: Box<dyn Any + Send + 'static>,
110    /// The event to sync making sure the bindings in the batch are ready to be reused by other streams.
111    event: B::Event,
112}
113
114impl<B: EventStreamBackend> GcTask<B> {
115    /// Creates a new task that will be clearned when the event is reached.
116    pub fn new<T: Send + 'static>(to_drop: T, event: B::Event) -> Self {
117        Self {
118            to_drop: Box::new(to_drop),
119            event,
120        }
121    }
122}
123
124#[derive(Debug)]
125/// The factory a [`MultiStream`]'s pool is built from. Public only because it
126/// names the pool in [`FailureStore::Factory`]; it has no surface of its own.
127pub struct EventStreamBackendWrapper<B: EventStreamBackend> {
128    backend: B,
129}
130
131impl<B: EventStreamBackend> StreamMemory for StreamWrapper<B> {
132    fn failure(&self, binding: &BufferBinding) -> Option<FailureId> {
133        self.stream.failure(binding)
134    }
135
136    fn taint(&mut self, binding: &BufferBinding, failure: FailureId, failures: &mut ErrorGraph) {
137        self.stream.taint(binding, failure, failures)
138    }
139
140    fn written(&mut self, binding: &BufferBinding, failures: &mut ErrorGraph) {
141        self.stream.written(binding, failures)
142    }
143}
144
145impl<B: EventStreamBackend> StreamFactory for EventStreamBackendWrapper<B> {
146    type Stream = StreamWrapper<B>;
147
148    fn create(&mut self) -> Result<Self::Stream, ServerError> {
149        Ok(StreamWrapper {
150            stream: self.backend.create_stream()?,
151            cursor: 0,
152            last_synced: Default::default(),
153        })
154    }
155}
156
157#[derive(Debug)]
158struct GcThread<B: EventStreamBackend> {
159    sender: SyncSender<GcTask<B>>,
160}
161
162impl<B: EventStreamBackend> GcThread<B> {
163    fn new() -> GcThread<B> {
164        let (sender, recv) = std::sync::mpsc::sync_channel::<GcTask<B>>(8);
165
166        cubecl_environment::thread::spawn(move || {
167            while let Ok(event) = recv.recv() {
168                match B::wait_event_sync(event.event) {
169                    Ok(()) => core::mem::drop(event.to_drop),
170                    Err(err) => release_or_leak(event.to_drop, &err),
171                }
172            }
173        });
174
175        GcThread { sender }
176    }
177    fn register(&self, task: GcTask<B>) {
178        self.sender.send(task).unwrap()
179    }
180}
181
182fn stream_index(stream_id: &StreamId, max_streams: usize) -> usize {
183    stream_id.value as usize % max_streams
184}
185
186impl<'a, B: EventStreamBackend> ResolvedStreams<'a, B> {
187    /// Get the stream associated to the given [`stream_id`](StreamId).
188    pub fn get(&mut self, stream_id: &StreamId) -> &mut B::Stream {
189        let stream = self.streams.get_mut(stream_id);
190        &mut stream.stream
191    }
192
193    /// The id of every initialized stream, each of which [`get`](Self::get)
194    /// resolves back to it.
195    pub fn stream_ids(&self) -> Vec<StreamId> {
196        self.streams.stream_ids().collect()
197    }
198
199    /// Every initialized stream; unlike [`get`](Self::get), never creates one.
200    pub fn all(&mut self) -> impl Iterator<Item = &mut B::Stream> {
201        self.streams.streams_mut().map(|stream| &mut stream.stream)
202    }
203
204    /// Get the stream associated to the [current `stream_id`](StreamId).
205    pub fn current(&mut self) -> &mut B::Stream {
206        let stream = self.streams.get_mut(&self.current);
207        &mut stream.stream
208    }
209
210    /// The current stream and the device's failure store together, for the
211    /// paths that hand the store to the stream's memory manager — every
212    /// reserve, bind and cleanup, since those are where slices shed the
213    /// failures they carry.
214    pub fn current_and_failures(&mut self) -> (&mut B::Stream, &mut ErrorGraph) {
215        let stream = self.streams.get_mut(&self.current);
216        (&mut stream.stream, self.failures)
217    }
218
219    /// [`current_and_failures`](Self::current_and_failures) for the stream at
220    /// `stream_id`.
221    pub fn get_and_failures(&mut self, stream_id: &StreamId) -> (&mut B::Stream, &mut ErrorGraph) {
222        let stream = self.streams.get_mut(stream_id);
223        (&mut stream.stream, self.failures)
224    }
225
226    /// Taint every allocation in `written` with `error` — see
227    /// [`base::taint`].
228    pub fn taint<'b>(
229        &mut self,
230        error: ServerError,
231        written: impl Iterator<Item = &'b BufferBinding>,
232    ) {
233        base::taint(self.streams, error, written, self.failures);
234    }
235
236    /// Release the failure on every allocation in `written` — see
237    /// [`base::written`].
238    pub fn written<'b>(&mut self, written: impl Iterator<Item = &'b BufferBinding>) {
239        base::written(self.streams, written, self.failures);
240    }
241
242    /// Enqueue a task to be cleaned.
243    pub fn gc(&mut self, gc: GcTask<B>) {
244        self.gc.sender.send(gc).unwrap();
245    }
246}
247
248impl<'a, B: EventStreamBackend> Drop for ResolvedStreams<'a, B> {
249    fn drop(&mut self) {
250        if self.analysis.pinned.is_empty() {
251            return;
252        }
253
254        let stream = self.streams.get_mut(&self.current);
255        let event_origin = B::flush(&mut stream.stream, self.failures);
256
257        let stream_gc = &mut unsafe { self.streams.get_special(0) }.stream;
258        let pinned = core::mem::take(&mut self.analysis.pinned);
259        // Without the dependency, the release stream's fence could be reached while this
260        // stream still reads the pinned memory.
261        if let Err(err) = B::wait_event(stream_gc, event_origin) {
262            release_or_leak(pinned, &err);
263            return;
264        }
265        let event = B::flush(stream_gc, self.failures);
266
267        self.gc.register(GcTask::new(pinned, event));
268    }
269}
270
271impl<B: EventStreamBackend> MultiStream<B> {
272    /// Mutable access to the stream-creation backend, e.g. to change the
273    /// configuration new streams are created with. Already-created streams are
274    /// unaffected.
275    pub fn backend_mut(&mut self) -> &mut B {
276        &mut self.streams.factory_mut().backend
277    }
278
279    /// Creates an empty multi-stream.
280    pub fn new(logger: Arc<ServerLogger>, backend: B, max_streams: u8) -> Self {
281        let wrapper = EventStreamBackendWrapper { backend };
282        Self {
283            streams: StreamPool::new(wrapper, max_streams, 1),
284            failures: Failures::new(logger.clone()),
285            logger,
286            max_streams: max_streams as usize,
287            gc: GcThread::new(),
288            shared_bindings_pool: Vec::new(),
289        }
290    }
291
292    /// Synthetic [`StreamId`]s, one per initialized stream (see [`StreamPool::stream_ids`]).
293    pub fn stream_ids(&self) -> impl Iterator<Item = StreamId> + '_ {
294        self.streams.stream_ids()
295    }
296
297    /// Enqueue a task to be cleaned.
298    pub fn gc(&mut self, gc: GcTask<B>) {
299        self.gc.sender.send(gc).unwrap();
300    }
301
302    /// The backend stream on `stream_id`'s slot when that slot was ever
303    /// initialized, mutably — a lookup that must not create a stream (see
304    /// [`StreamPool::try_get_mut`]).
305    pub fn try_stream_mut(&mut self, stream_id: &StreamId) -> Option<&mut B::Stream> {
306        self.streams
307            .try_get_mut(stream_id)
308            .map(|wrapper| &mut wrapper.stream)
309    }
310
311    /// Resolves and returns a mutable reference to the stream for the given ID, performing any necessary
312    /// alignment based on the provided bindings.
313    ///
314    /// This method ensures that the stream is synchronized with any shared bindings from other streams
315    /// before returning the stream reference.
316    ///
317    /// # Errors
318    ///
319    /// Returns a [`ServerError`] when the stream, or the stream that releases memory shared
320    /// between streams, has to be created and cannot be, e.g. on a poisoned device.
321    pub fn resolve<'a>(
322        &mut self,
323        stream_id: StreamId,
324        handles: impl Iterator<Item = &'a BufferBinding>,
325    ) -> Result<ResolvedStreams<'_, B>, ServerError> {
326        self.streams.get_or_create(&stream_id)?;
327        let analysis = self.align_streams(stream_id, handles)?;
328        if !analysis.pinned.is_empty() {
329            // The release of pinned memory goes through this stream when the resolved
330            // streams are dropped, where a failure could not be returned.
331            // SAFETY: the pool is created with one special stream.
332            unsafe { self.streams.get_or_create_special(0) }?;
333        }
334
335        let stream = self.streams.get_mut(&stream_id);
336        stream.cursor += 1;
337
338        Ok(ResolvedStreams {
339            cursor: stream.cursor,
340            streams: &mut self.streams,
341            failures: self.failures.graph_mut(),
342            current: stream_id,
343            analysis,
344            gc: &self.gc,
345        })
346    }
347
348    /// Aligns the target stream with other streams based on shared bindings.
349    ///
350    /// This initializes the stream if it doesn't exist, analyzes which originating streams need flushing
351    /// for synchronization, flushes them, and waits on the events in the target stream.
352    fn align_streams<'a>(
353        &mut self,
354        stream_id: StreamId,
355        handles: impl Iterator<Item = &'a BufferBinding>,
356    ) -> Result<SharedBindingAnalysis, ServerError> {
357        let analysis = self.update_shared_bindings(stream_id, handles)?;
358
359        self.apply_analysis(stream_id, analysis)
360    }
361
362    /// Updates and analyzes the bindings to determine which streams need alignment (flushing and waiting).
363    ///
364    /// This checks for shared bindings from other streams and determines if synchronization is needed
365    /// based on cursor positions.
366    pub(crate) fn update_shared_bindings<'a>(
367        &mut self,
368        stream_id: StreamId,
369        handles: impl Iterator<Item = &'a BufferBinding>,
370    ) -> Result<SharedBindingAnalysis, ServerError> {
371        // We reset the memory pool for the info.
372        self.shared_bindings_pool.clear();
373
374        let mut analysis = SharedBindingAnalysis::default();
375
376        // We only consider handles whose stream is different from the current stream.
377        for handle in handles.filter(|handle| handle.stream != stream_id) {
378            let index = stream_index(&handle.stream, self.max_streams);
379            // SAFETY: `stream_index` keeps the index within the regular streams.
380            let stream = unsafe { self.streams.get_or_create_index(index) }?;
381            let cursor_handle = B::handle_cursor(&stream.stream, handle);
382
383            self.shared_bindings_pool.push((
384                handle.memory.descriptor().id,
385                handle.stream,
386                cursor_handle,
387            ));
388            // Pinned unconditionally, even when the cursor check below decides
389            // no new wait is needed: the reverse-direction hazard (the origin
390            // stream freeing/reusing the memory under the in-flight consumer)
391            // exists either way.
392            analysis.pinned.push(handle.memory.clone());
393        }
394
395        let current = self.streams.get_mut(&stream_id);
396
397        for (handle_id, stream, cursor) in self.shared_bindings_pool.iter() {
398            let index = stream_index(stream, self.max_streams);
399
400            if let Some(last_synced) = current.last_synced.get(&index) {
401                if last_synced < cursor {
402                    self.logger.log_streaming(
403                        |level| matches!(level, StreamingLogLevel::Full),
404                        || {
405                            format!(
406                                "Binding on {} is shared on {} since it's not sync {} < {}",
407                                stream, stream_id, last_synced, cursor
408                            )
409                        },
410                    );
411                    analysis.shared(*handle_id, index);
412                }
413            } else {
414                self.logger.log_streaming(
415                    |level| matches!(level, StreamingLogLevel::Full),
416                    || {
417                        format!(
418                            "Binding on {} is shared on {} since it was never synced.",
419                            stream, stream_id,
420                        )
421                    },
422                );
423                analysis.shared(*handle_id, index);
424            }
425        }
426
427        Ok(analysis)
428    }
429
430    pub(crate) fn apply_analysis(
431        &mut self,
432        stream_id: StreamId,
433        analysis: SharedBindingAnalysis,
434    ) -> Result<SharedBindingAnalysis, ServerError> {
435        if analysis.slices.is_empty() {
436            return Ok(analysis);
437        }
438
439        let mut events = Vec::with_capacity(analysis.slices.len());
440
441        unsafe {
442            for origin in analysis.slices.keys() {
443                let stream = self.streams.get_mut_index(*origin);
444                let event = B::flush(&mut stream.stream, self.failures.graph_mut());
445
446                events.push(((origin, stream.cursor), event));
447            }
448        }
449
450        let stream = self.streams.get_mut(&stream_id);
451
452        for ((stream_origin, cursor_origin), event) in events {
453            self.logger.log_streaming(
454                |level| !matches!(level, StreamingLogLevel::Disabled),
455                || format!("Waiting on {stream_origin} from {stream_id}",),
456            );
457
458            B::wait_event(&mut stream.stream, event)?;
459            // Only once the wait is in place: a stream that did not wait has not synced.
460            stream.last_synced.insert(*stream_origin, cursor_origin);
461        }
462
463        Ok(analysis)
464    }
465}
466
467impl<B: EventStreamBackend> FailureStore for MultiStream<B> {
468    type Factory = EventStreamBackendWrapper<B>;
469
470    fn split(&mut self) -> (&mut StreamPool<Self::Factory>, &mut Failures) {
471        (&mut self.streams, &mut self.failures)
472    }
473
474    fn parts(&self) -> (&StreamPool<Self::Factory>, &Failures) {
475        (&self.streams, &self.failures)
476    }
477}
478
479impl<B: EventStreamBackend> core::fmt::Debug for StreamWrapper<B> {
480    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
481        f.debug_struct("StreamWrapper")
482            .field("stream", &self.stream)
483            .field("cursor", &self.cursor)
484            .field("last_synced", &self.last_synced)
485            .finish()
486    }
487}
488
489#[derive(Default, Debug)]
490pub(crate) struct SharedBindingAnalysis {
491    slices: HashMap<usize, Vec<ManagedMemoryId>>,
492    /// Every cross-stream binding of the task, kept alive until the consumer
493    /// stream's work completes (released by the GC thread after its event).
494    ///
495    /// The origin stream's pools consider a slice free once no handle/binding
496    /// references its descriptor, but the consumer's kernel may still be
497    /// running on the GPU after the CPU-side bindings were dropped at enqueue
498    /// time. Pinning the bindings here is what keeps the slice non-free until
499    /// the GC event fires, so `cleanup`/`try_reserve` on the origin stream
500    /// cannot dealloc or reuse memory that is still read by another stream.
501    pinned: SharedMemoryBindings,
502}
503
504/// Equality covers the sync analysis only; `pinned` is a lifetime mechanism,
505/// not part of the analysis result.
506impl PartialEq for SharedBindingAnalysis {
507    fn eq(&self, other: &Self) -> bool {
508        self.slices == other.slices
509    }
510}
511
512impl Eq for SharedBindingAnalysis {}
513
514impl SharedBindingAnalysis {
515    fn shared(&mut self, id: ManagedMemoryId, index: usize) {
516        match self.slices.get_mut(&index) {
517            Some(bindings) => bindings.push(id),
518            None => {
519                self.slices.insert(index, alloc::vec![id]);
520            }
521        }
522    }
523}
524
525#[cfg(test)]
526mod tests {
527    use crate::server::Handle;
528    use core::sync::atomic::{AtomicBool, Ordering};
529
530    use super::*;
531
532    /// A service for handles that never reach a device.
533    fn service() -> cubecl_common::device::ServiceId {
534        cubecl_common::device::ServiceId::of::<()>(cubecl_common::device::DeviceId::new(0, 0))
535    }
536
537    const MAX_STREAMS: u8 = 4;
538
539    #[test_log::test]
540    fn test_analysis_shared_bindings_1() {
541        let logger = Arc::new(ServerLogger::default());
542        let stream_1 = StreamId { value: 1 };
543        let stream_2 = StreamId { value: 2 };
544
545        let binding_1 = handle(stream_1);
546        let binding_2 = handle(stream_2);
547
548        let mut ms = MultiStream::new(logger, TestBackend, MAX_STREAMS);
549        ms.resolve(stream_1, [].into_iter()).unwrap();
550        ms.resolve(stream_2, [].into_iter()).unwrap();
551
552        let analysis = ms
553            .update_shared_bindings(stream_1, [&binding_1, &binding_2].into_iter())
554            .unwrap();
555
556        let mut expected = SharedBindingAnalysis::default();
557        expected.shared(
558            binding_2.memory.descriptor().id,
559            ms.streams.stream_index(&binding_2.stream),
560        );
561
562        assert_eq!(analysis, expected);
563    }
564
565    #[test_log::test]
566    fn test_analysis_shared_bindings_2() {
567        let logger = Arc::new(ServerLogger::default());
568        let stream_1 = StreamId { value: 1 };
569        let stream_2 = StreamId { value: 2 };
570
571        let binding_1 = handle(stream_1);
572        let binding_2 = handle(stream_2);
573        let binding_3 = handle(stream_1);
574
575        let mut ms = MultiStream::new(logger, TestBackend, 4);
576        ms.resolve(stream_1, [].into_iter()).unwrap();
577        ms.resolve(stream_2, [].into_iter()).unwrap();
578
579        let analysis = ms
580            .update_shared_bindings(stream_1, [&binding_1, &binding_2, &binding_3].into_iter())
581            .unwrap();
582
583        let mut expected = SharedBindingAnalysis::default();
584        expected.shared(
585            binding_2.memory.descriptor().id,
586            ms.streams.stream_index(&binding_2.stream),
587        );
588
589        assert_eq!(analysis, expected);
590    }
591
592    #[test_log::test]
593    fn test_analysis_no_shared() {
594        let logger = Arc::new(ServerLogger::default());
595        let stream_1 = StreamId { value: 1 };
596        let stream_2 = StreamId { value: 2 };
597
598        let binding_1 = handle(stream_1);
599        let binding_2 = handle(stream_1);
600        let binding_3 = handle(stream_1);
601
602        let mut ms = MultiStream::new(logger, TestBackend, MAX_STREAMS);
603        ms.resolve(stream_1, [].into_iter()).unwrap();
604        ms.resolve(stream_2, [].into_iter()).unwrap();
605
606        let analysis = ms
607            .update_shared_bindings(stream_1, [&binding_1, &binding_2, &binding_3].into_iter())
608            .unwrap();
609
610        let expected = SharedBindingAnalysis::default();
611
612        assert_eq!(analysis, expected);
613    }
614
615    #[test_log::test]
616    fn test_state() {
617        let logger = Arc::new(ServerLogger::default());
618        let stream_1 = StreamId { value: 1 };
619        let stream_2 = StreamId { value: 2 };
620
621        let binding_1 = handle(stream_1);
622        let binding_2 = handle(stream_2);
623        let binding_3 = handle(stream_1);
624
625        let mut ms = MultiStream::new(logger, TestBackend, MAX_STREAMS);
626        ms.resolve(stream_1, [].into_iter()).unwrap();
627        ms.resolve(stream_2, [].into_iter()).unwrap();
628
629        ms.resolve(stream_1, [&binding_1, &binding_2, &binding_3].into_iter())
630            .unwrap();
631
632        let stream1 = ms.streams.get_mut(&stream_1);
633        let index_2 = stream_index(&stream_2, MAX_STREAMS as usize);
634        assert_eq!(stream1.last_synced.get(&index_2), Some(&1));
635        assert_eq!(stream1.cursor, 2);
636
637        let stream2 = ms.streams.get_mut(&stream_2);
638        assert!(stream2.last_synced.is_empty());
639        assert_eq!(stream2.cursor, 1);
640    }
641
642    #[test_log::test]
643    fn test_cross_stream_binding_pinned_until_gc_event() {
644        let logger = Arc::new(ServerLogger::default());
645        let stream_1 = StreamId { value: 1 };
646        let stream_2 = StreamId { value: 2 };
647
648        let gate = Arc::new(AtomicBool::new(false));
649        let mut ms = MultiStream::new(logger, GatedBackend { gate: gate.clone() }, MAX_STREAMS);
650        ms.resolve(stream_1, [].into_iter()).unwrap();
651        ms.resolve(stream_2, [].into_iter()).unwrap();
652
653        let handle = Handle::new(service(), stream_1, 10);
654        let observer = handle.memory.clone();
655        let binding = handle.binding();
656
657        drop(ms.resolve(stream_2, [&binding].into_iter()).unwrap());
658        drop(binding);
659
660        // The GC thread is blocked on the (gated) consumer event, so the pinned
661        // binding must keep the memory non-free even though every user-side
662        // handle/binding is gone.
663        assert!(
664            !observer.is_free(),
665            "cross-stream binding must stay pinned while the consumer event is pending"
666        );
667
668        gate.store(true, Ordering::Release);
669        wait_until_free(&observer);
670    }
671
672    #[test_log::test]
673    fn test_already_synced_cross_stream_binding_still_pinned() {
674        let logger = Arc::new(ServerLogger::default());
675        let stream_1 = StreamId { value: 1 };
676        let stream_2 = StreamId { value: 2 };
677
678        let gate = Arc::new(AtomicBool::new(true));
679        let mut ms = MultiStream::new(logger, GatedBackend { gate: gate.clone() }, MAX_STREAMS);
680        ms.resolve(stream_1, [].into_iter()).unwrap();
681        ms.resolve(stream_2, [].into_iter()).unwrap();
682
683        // First resolve records stream_1 as synced on stream_2.
684        let handle_1 = Handle::new(service(), stream_1, 10);
685        let binding_1 = handle_1.binding();
686        drop(ms.resolve(stream_2, [&binding_1].into_iter()).unwrap());
687        drop(binding_1);
688
689        // Close the gate for the second round.
690        gate.store(false, Ordering::Release);
691
692        let handle_2 = Handle::new(service(), stream_1, 10);
693        let observer = handle_2.memory.clone();
694        let binding_2 = handle_2.binding();
695
696        // stream_2 already synced past this binding's cursor, so the sync
697        // analysis is empty — but the binding must still be pinned: the origin
698        // stream could otherwise free/reuse the memory under the in-flight
699        // consumer.
700        let resolved = ms.resolve(stream_2, [&binding_2].into_iter()).unwrap();
701        assert!(resolved.analysis.slices.is_empty());
702        drop(resolved);
703        drop(binding_2);
704
705        assert!(
706            !observer.is_free(),
707            "already-synced cross-stream binding must still be pinned"
708        );
709
710        gate.store(true, Ordering::Release);
711        wait_until_free(&observer);
712    }
713
714    fn wait_until_free(observer: &crate::memory_management::ManagedMemoryHandle) {
715        let start = std::time::Instant::now();
716        while !observer.is_free() {
717            assert!(
718                start.elapsed() < std::time::Duration::from_secs(10),
719                "pinned binding was never released"
720            );
721            std::thread::yield_now();
722        }
723    }
724
725    fn handle(stream: StreamId) -> BufferBinding {
726        Handle::new(service(), stream, 10).binding()
727    }
728
729    struct TestBackend;
730
731    #[derive(Debug, Default)]
732    struct TestStream;
733
734    #[derive(Debug)]
735    struct TestEvent {}
736
737    /// A backend whose events complete only once the shared `gate` opens,
738    /// emulating GPU work still in flight on the consumer stream.
739    struct GatedBackend {
740        gate: Arc<AtomicBool>,
741    }
742
743    #[derive(Debug, Default)]
744    struct GatedStream {
745        gate: Arc<AtomicBool>,
746    }
747
748    #[derive(Debug)]
749    struct GatedEvent {
750        gate: Arc<AtomicBool>,
751    }
752
753    /// The test streams manage no memory, so nothing carries a failure.
754    impl StreamMemory for GatedStream {
755        fn failure(&self, _binding: &BufferBinding) -> Option<FailureId> {
756            None
757        }
758
759        fn taint(
760            &mut self,
761            _binding: &BufferBinding,
762            _failure: FailureId,
763            _failures: &mut ErrorGraph,
764        ) {
765        }
766
767        fn written(&mut self, _binding: &BufferBinding, _failures: &mut ErrorGraph) {}
768    }
769
770    impl StreamMemory for TestStream {
771        fn failure(&self, _binding: &BufferBinding) -> Option<FailureId> {
772            None
773        }
774
775        fn taint(
776            &mut self,
777            _binding: &BufferBinding,
778            _failure: FailureId,
779            _failures: &mut ErrorGraph,
780        ) {
781        }
782
783        fn written(&mut self, _binding: &BufferBinding, _failures: &mut ErrorGraph) {}
784    }
785
786    impl EventStreamBackend for GatedBackend {
787        type Stream = GatedStream;
788        type Event = GatedEvent;
789
790        fn create_stream(&self) -> Result<Self::Stream, ServerError> {
791            Ok(GatedStream {
792                gate: self.gate.clone(),
793            })
794        }
795
796        fn flush(stream: &mut Self::Stream, _failures: &mut ErrorGraph) -> Self::Event {
797            GatedEvent {
798                gate: stream.gate.clone(),
799            }
800        }
801
802        fn wait_event(_stream: &mut Self::Stream, _event: Self::Event) -> Result<(), ServerError> {
803            Ok(())
804        }
805
806        fn wait_event_sync(event: Self::Event) -> Result<(), ServerError> {
807            while !event.gate.load(Ordering::Acquire) {
808                std::thread::yield_now();
809            }
810            Ok(())
811        }
812
813        fn handle_cursor(_stream: &Self::Stream, _handle: &BufferBinding) -> u64 {
814            0
815        }
816    }
817
818    impl EventStreamBackend for TestBackend {
819        type Stream = TestStream;
820        type Event = TestEvent;
821
822        fn create_stream(&self) -> Result<Self::Stream, ServerError> {
823            Ok(TestStream)
824        }
825
826        fn flush(_stream: &mut Self::Stream, _failures: &mut ErrorGraph) -> Self::Event {
827            TestEvent {}
828        }
829
830        fn wait_event(_stream: &mut Self::Stream, _event: Self::Event) -> Result<(), ServerError> {
831            Ok(())
832        }
833
834        fn wait_event_sync(_event: Self::Event) -> Result<(), ServerError> {
835            Ok(())
836        }
837
838        fn handle_cursor(_stream: &Self::Stream, _handle: &BufferBinding) -> u64 {
839            0
840        }
841    }
842
843    #[test]
844    fn memory_is_released_only_when_the_device_runs_nothing_anymore() {
845        let memory = Arc::new(());
846
847        let other = ServerError::Generic {
848            reason: "the wait failed".into(),
849            backtrace: Default::default(),
850        };
851        release_or_leak(memory.clone(), &other);
852        assert_eq!(Arc::strong_count(&memory), 2, "leaked, not released");
853
854        let poisoned: ServerError = crate::driver::DevicePoison::new("status 700").into();
855        release_or_leak(memory.clone(), &poisoned);
856        assert_eq!(Arc::strong_count(&memory), 2, "released, not leaked");
857    }
858}