Skip to main content

cubecl_runtime/stream/
event.rs

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