Skip to main content

cubecl_runtime/stream/
scheduler.rs

1use crate::{
2    config::streaming::StreamingLogLevel,
3    logging::ServerLogger,
4    stream::{StreamFactory, StreamPool},
5};
6use alloc::{format, sync::Arc, vec, vec::Vec};
7use cubecl_common::stream_id::StreamId;
8
9/// Defines a trait for a scheduler stream backend, specifying the types and behavior for task scheduling.
10pub trait SchedulerStreamBackend {
11    /// Type representing a task.
12    type Task: core::fmt::Debug;
13    /// Type representing a stream.
14    type Stream: core::fmt::Debug;
15    /// Type for the stream factory, which creates streams of type `Self::Stream`.
16    type Factory: StreamFactory<Stream = Self::Stream>;
17
18    /// Enqueues a task onto a given stream for execution.
19    fn enqueue(task: Self::Task, stream: &mut Self::Stream);
20    /// Flush the inner stream queue to ensure ordering between different streams.
21    fn flush(stream: &mut Self::Stream);
22    /// Returns a mutable reference to the stream factory.
23    fn factory(&mut self) -> &mut Self::Factory;
24}
25
26/// Represents a multi-stream scheduler that manages task execution across multiple streams.
27#[derive(Debug)]
28pub struct SchedulerMultiStream<B: SchedulerStreamBackend> {
29    /// Pool of streams managed by the scheduler.
30    pool: StreamPool<SchedulerPoolMarker<B>>,
31    /// Strategy for scheduling tasks (e.g., Interleave or Sequential).
32    strategy: SchedulerStrategy,
33    /// Maximum number of tasks allowed per stream before execution is triggered.
34    max_tasks: usize,
35    /// Server logger.
36    pub logger: Arc<ServerLogger>,
37}
38
39/// Defines the scheduling strategy for task execution.
40#[derive(Debug)]
41pub enum SchedulerStrategy {
42    /// Tasks from different streams are interleaved during execution.
43    Interleave,
44    /// Tasks from each stream are executed sequentially.
45    Sequential,
46}
47
48/// Represents a single stream that holds tasks and a backend stream.
49#[derive(Debug)]
50pub struct Stream<B: SchedulerStreamBackend> {
51    /// List of tasks queued for execution in this stream.
52    tasks: Vec<B::Task>,
53    /// The backend stream used for task execution.
54    stream: B::Stream,
55}
56
57impl<B: SchedulerStreamBackend> Stream<B> {
58    /// Flushes all tasks from the stream, returning them and clearing the internal task list.
59    fn flush(&mut self) -> Vec<B::Task> {
60        let mut returned = Vec::with_capacity(self.tasks.capacity());
61        core::mem::swap(&mut returned, &mut self.tasks);
62        returned
63    }
64}
65
66#[derive(Debug)]
67struct SchedulerPoolMarker<B: SchedulerStreamBackend> {
68    backend: B,
69}
70
71impl<B: SchedulerStreamBackend> StreamFactory for SchedulerPoolMarker<B> {
72    // The type of stream produced by this factory.
73    type Stream = Stream<B>;
74
75    // Creates a new stream with an empty task list and a backend stream.
76    fn create(&mut self) -> Self::Stream {
77        Stream {
78            tasks: Vec::new(),
79            // Uses the backend's factory to create a new stream.
80            stream: self.backend.factory().create(),
81        }
82    }
83}
84
85/// Options for configuring a `SchedulerMultiStream`.
86#[derive(Debug)]
87pub struct SchedulerMultiStreamOptions {
88    /// Maximum number of streams allowed in the pool.
89    pub max_streams: u8,
90    /// Maximum number of tasks per stream before execution is triggered.
91    pub max_tasks: usize,
92    /// The scheduling strategy to use.
93    pub strategy: SchedulerStrategy,
94}
95
96impl<B: SchedulerStreamBackend> SchedulerMultiStream<B> {
97    /// Creates a new `SchedulerMultiStream` with the given backend and options.
98    pub fn new(
99        logger: Arc<ServerLogger>,
100        backend: B,
101        options: SchedulerMultiStreamOptions,
102    ) -> Self {
103        Self {
104            pool: StreamPool::new(SchedulerPoolMarker { backend }, options.max_streams, 0),
105            max_tasks: options.max_tasks,
106            strategy: options.strategy,
107            logger,
108        }
109    }
110
111    /// Returns a mutable reference to the backend stream for a given stream ID.
112    pub fn stream(&mut self, stream_id: &StreamId) -> &mut B::Stream {
113        let stream = self.pool.get_mut(stream_id);
114        &mut stream.stream
115    }
116
117    /// Mutable access to the scheduling backend, e.g. to change the
118    /// configuration new streams are created with. Already-created streams are
119    /// unaffected.
120    pub fn backend_mut(&mut self) -> &mut B {
121        &mut self.pool.factory_mut().backend
122    }
123
124    /// Read-only iterator over initialized backend streams.
125    pub fn streams(&self) -> impl Iterator<Item = &B::Stream> {
126        self.pool.streams().map(|s| &s.stream)
127    }
128
129    /// Synthetic [`StreamId`]s, one per initialized stream (see [`StreamPool::stream_ids`]).
130    pub fn stream_ids(&self) -> impl Iterator<Item = StreamId> + '_ {
131        self.pool.stream_ids()
132    }
133
134    /// Registers a task for execution on a specific stream, ensuring stream alignment.
135    pub fn register(&mut self, stream_id: StreamId, task: B::Task, args_streams: &[StreamId]) {
136        // Align streams to ensure dependencies are handled correctly.
137        self.align_streams(stream_id, args_streams);
138
139        // Get the stream for the given stream ID and add the task to its queue.
140        let current = self.pool.get_mut(&stream_id);
141        current.tasks.push(task);
142
143        // If the task queue exceeds the maximum, execute the stream.
144        if current.tasks.len() >= self.max_tasks {
145            self.execute_streams(vec![stream_id]);
146        }
147    }
148
149    /// Aligns streams by flushing tasks from streams that conflict with the given bindings.
150    pub(crate) fn align_streams(&mut self, stream_id: StreamId, args_streams: &[StreamId]) {
151        let mut to_flush = Vec::new();
152        // Get the index of the target stream.
153        let index = self.pool.stream_index(&stream_id);
154
155        // Identify streams that need to be flushed due to conflicting bindings.
156        for arg_stream in args_streams {
157            let index_stream = self.pool.stream_index(arg_stream);
158            if index != index_stream {
159                to_flush.push(*arg_stream);
160
161                self.logger.log_streaming(
162                    |level| matches!(level, StreamingLogLevel::Full),
163                    || format!("Binding on {} is shared on {}", arg_stream, stream_id),
164                );
165            }
166        }
167
168        // If no streams need flushing, return early.
169        if to_flush.is_empty() {
170            return;
171        }
172
173        self.logger.log_streaming(
174            |level| !matches!(level, StreamingLogLevel::Disabled),
175            || {
176                format!(
177                    "Flushing streams {to_flush:?} before registering more tasks on {stream_id}"
178                )
179            },
180        );
181        // Execute the streams that need to be flushed.
182        self.execute_streams(to_flush);
183    }
184
185    /// Executes tasks from the specified streams based on the scheduling strategy.
186    pub fn execute_streams(&mut self, stream_ids: Vec<StreamId>) {
187        let mut indices = Vec::with_capacity(stream_ids.len());
188
189        // Collect unique stream indices to avoid redundant processing.
190        for id in stream_ids {
191            let index = self.pool.stream_index(&id);
192            if !indices.contains(&index) {
193                indices.push(index);
194            }
195        }
196
197        // Create schedules for each stream to be executed.
198        let mut schedules = Vec::new();
199        for index in indices {
200            let stream = unsafe { self.pool.get_mut_index(index) }; // Note: `unsafe` usage assumes valid index.
201            let tasks = stream.flush();
202            let num_tasks = tasks.len();
203
204            schedules.push(Schedule {
205                tasks: tasks.into_iter(),
206                num_tasks,
207                stream_index: index,
208            });
209        }
210
211        // If no schedules were created, return early.
212        if schedules.is_empty() {
213            return;
214        }
215
216        // Execute schedules based on the configured strategy.
217        match self.strategy {
218            SchedulerStrategy::Interleave => self.execute_schedules_interleave(schedules),
219            SchedulerStrategy::Sequential => self.execute_schedules_sequence(schedules),
220        }
221    }
222
223    /// Executes schedules sequentially, processing each stream's tasks in order.
224    fn execute_schedules_sequence(&mut self, schedules: Vec<Schedule<B>>) {
225        for schedule in schedules {
226            let stream = unsafe { self.pool.get_mut_index(schedule.stream_index) }; // Note: `unsafe` usage assumes valid index.
227            for task in schedule.tasks {
228                // Enqueue each task on the stream.
229                B::enqueue(task, &mut stream.stream);
230            }
231
232            // Makes sure the tasks are ordered on the compute queue.
233            B::flush(&mut stream.stream);
234        }
235    }
236
237    //// Executes schedules in an interleaved manner, alternating tasks from different streams.
238    ///
239    /// We chose the first stream as the one executing the tasks, ensuring proper ordering by
240    /// flushing all other streams first and flushing the execution stream at the end.
241    /// This way, we ensure that most tasks are actually interleaved on the real compute queue
242    /// shared across all streams.
243    fn execute_schedules_interleave(&mut self, mut schedules: Vec<Schedule<B>>) {
244        // Makes sure the tasks are ordered on the compute queue.
245        for schedule in schedules.iter_mut().skip(1) {
246            let stream = unsafe { self.pool.get_mut_index(schedule.stream_index) };
247            B::flush(&mut stream.stream);
248        }
249
250        let execution_index = schedules.first().expect("At least one stream").stream_index;
251        let stream = unsafe { self.pool.get_mut_index(execution_index) };
252
253        // Find the maximum number of tasks across all schedules.
254        let num_tasks_max = schedules
255            .iter()
256            .map(|s| s.num_tasks)
257            .max()
258            .expect("At least one schedule");
259
260        // Iterate through tasks, interleaving them across streams.
261        for _ in 0..num_tasks_max {
262            for schedule in schedules.iter_mut() {
263                // If there are tasks remaining in the schedule, enqueue the next one.
264                if let Some(task) = schedule.tasks.next() {
265                    B::enqueue(task, &mut stream.stream);
266                }
267            }
268        }
269
270        // Making sure all tasks are registered to the queue.
271        B::flush(&mut stream.stream);
272    }
273}
274
275// Represents a schedule for executing tasks on a specific stream.
276struct Schedule<B: SchedulerStreamBackend> {
277    // Iterator over the tasks to be executed.
278    tasks: alloc::vec::IntoIter<B::Task>,
279    // Number of tasks in the schedule.
280    num_tasks: usize,
281    // Index of the stream in the pool.
282    stream_index: usize,
283}