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}