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