cubecl_server/stream/scheduler.rs
1use crate::{
2 config::streaming::StreamingLogLevel,
3 logging::ServerLogger,
4 memory_management::{ErrorGraph, FailureId},
5 server::BufferBinding,
6 stream::{FailureStore, Failures, StreamFactory, StreamMemory, StreamPool},
7};
8use alloc::{format, sync::Arc, vec, vec::Vec};
9use cubecl_environment::stream::StreamId;
10
11/// Defines a trait for a scheduler stream backend, specifying the types and behavior for task scheduling.
12pub trait SchedulerStreamBackend {
13 /// Type representing a task.
14 type Task: core::fmt::Debug;
15 /// Type representing a stream, which exposes the memory its kernels see
16 /// through [`StreamMemory`].
17 type Stream: core::fmt::Debug + StreamMemory;
18 /// Type for the stream factory, which creates streams of type `Self::Stream`.
19 type Factory: StreamFactory<Stream = Self::Stream>;
20
21 /// Enqueues a task onto a given stream for execution.
22 ///
23 /// `failures` is the device's failure store, handed down because running
24 /// a task reserves memory (staging, uniforms), and reserving is where a
25 /// pool sheds the failures its slices carry.
26 fn enqueue(task: Self::Task, stream: &mut Self::Stream, failures: &mut ErrorGraph);
27 /// Flush the inner stream queue to ensure ordering between different streams.
28 fn flush(stream: &mut Self::Stream, failures: &mut ErrorGraph);
29 /// Returns a mutable reference to the stream factory.
30 fn factory(&mut self) -> &mut Self::Factory;
31 /// Whether this stream currently requires its own tasks to execute on
32 /// itself — no interleaving of its tasks onto another stream, and no other
33 /// stream's tasks onto it. While any stream involved in an execution
34 /// requires isolation, the scheduler falls back to the sequential path.
35 /// A graph capture engages this for its whole prepare → record window.
36 /// Defaults to `false`.
37 fn requires_isolation(_stream: &Self::Stream) -> bool {
38 false
39 }
40}
41
42/// Represents a multi-stream scheduler that manages task execution across multiple streams.
43#[derive(Debug)]
44pub struct SchedulerMultiStream<B: SchedulerStreamBackend> {
45 /// Pool of streams managed by the scheduler.
46 pool: StreamPool<SchedulerPoolMarker<B>>,
47 /// Every failure the device is still holding, and the write scope's
48 /// scratch — see [`Failures`].
49 failures: Failures,
50 /// Strategy for scheduling tasks (e.g., Interleave or Sequential).
51 strategy: SchedulerStrategy,
52 /// Maximum number of tasks allowed per stream before execution is triggered.
53 max_tasks: usize,
54 /// Server logger.
55 pub logger: Arc<ServerLogger>,
56}
57
58/// Defines the scheduling strategy for task execution.
59#[derive(Debug)]
60pub enum SchedulerStrategy {
61 /// Tasks from different streams are interleaved during execution.
62 Interleave,
63 /// Tasks from each stream are executed sequentially.
64 Sequential,
65}
66
67/// Represents a single stream that holds tasks and a backend stream.
68#[derive(Debug)]
69pub struct Stream<B: SchedulerStreamBackend> {
70 /// List of tasks queued for execution in this stream.
71 tasks: Vec<B::Task>,
72 /// The backend stream used for task execution.
73 stream: B::Stream,
74}
75
76impl<B: SchedulerStreamBackend> Stream<B> {
77 /// Flushes all tasks from the stream, returning them and clearing the internal task list.
78 fn flush(&mut self) -> Vec<B::Task> {
79 let mut returned = Vec::with_capacity(self.tasks.capacity());
80 core::mem::swap(&mut returned, &mut self.tasks);
81 returned
82 }
83}
84
85#[derive(Debug)]
86/// The factory a [`SchedulerMultiStream`]'s pool is built from. Public only
87/// because it names the pool in [`FailureStore::Factory`]; it has no surface
88/// of its own.
89pub struct SchedulerPoolMarker<B: SchedulerStreamBackend> {
90 backend: B,
91}
92
93impl<B: SchedulerStreamBackend> StreamMemory for Stream<B> {
94 fn failure(&self, binding: &BufferBinding) -> Option<FailureId> {
95 self.stream.failure(binding)
96 }
97
98 fn taint(&mut self, binding: &BufferBinding, failure: FailureId, failures: &mut ErrorGraph) {
99 self.stream.taint(binding, failure, failures)
100 }
101
102 fn written(&mut self, binding: &BufferBinding, failures: &mut ErrorGraph) {
103 self.stream.written(binding, failures)
104 }
105}
106
107impl<B: SchedulerStreamBackend> FailureStore for SchedulerMultiStream<B> {
108 type Factory = SchedulerPoolMarker<B>;
109
110 fn split(&mut self) -> (&mut StreamPool<Self::Factory>, &mut Failures) {
111 (&mut self.pool, &mut self.failures)
112 }
113
114 fn parts(&self) -> (&StreamPool<Self::Factory>, &Failures) {
115 (&self.pool, &self.failures)
116 }
117}
118
119impl<B: SchedulerStreamBackend> StreamFactory for SchedulerPoolMarker<B> {
120 // The type of stream produced by this factory.
121 type Stream = Stream<B>;
122
123 // Creates a new stream with an empty task list and a backend stream.
124 fn create(&mut self) -> Self::Stream {
125 Stream {
126 tasks: Vec::new(),
127 // Uses the backend's factory to create a new stream.
128 stream: self.backend.factory().create(),
129 }
130 }
131}
132
133/// Options for configuring a `SchedulerMultiStream`.
134#[derive(Debug)]
135pub struct SchedulerMultiStreamOptions {
136 /// Maximum number of streams allowed in the pool.
137 pub max_streams: u8,
138 /// Maximum number of tasks per stream before execution is triggered.
139 pub max_tasks: usize,
140 /// The scheduling strategy to use.
141 pub strategy: SchedulerStrategy,
142}
143
144impl<B: SchedulerStreamBackend> SchedulerMultiStream<B> {
145 /// Creates a new `SchedulerMultiStream` with the given backend and options.
146 pub fn new(
147 logger: Arc<ServerLogger>,
148 backend: B,
149 options: SchedulerMultiStreamOptions,
150 ) -> Self {
151 Self {
152 pool: StreamPool::new(SchedulerPoolMarker { backend }, options.max_streams, 0),
153 failures: Failures::new(logger.clone()),
154 max_tasks: options.max_tasks,
155 strategy: options.strategy,
156 logger,
157 }
158 }
159
160 /// Returns a mutable reference to the backend stream for a given stream ID.
161 pub fn stream(&mut self, stream_id: &StreamId) -> &mut B::Stream {
162 let stream = self.pool.get_mut(stream_id);
163 &mut stream.stream
164 }
165
166 /// The stream at `stream_id` and the device's failure store together, for
167 /// the paths that hand the store to the stream's memory manager — every
168 /// reserve, bind and cleanup, since those are where slices shed the
169 /// failures they carry.
170 pub fn stream_and_failures(
171 &mut self,
172 stream_id: &StreamId,
173 ) -> (&mut B::Stream, &mut ErrorGraph) {
174 let stream = self.pool.get_mut(stream_id);
175 (&mut stream.stream, self.failures.graph_mut())
176 }
177
178 /// Mutable access to the scheduling backend, e.g. to change the
179 /// configuration new streams are created with. Already-created streams are
180 /// unaffected.
181 pub fn backend_mut(&mut self) -> &mut B {
182 &mut self.pool.factory_mut().backend
183 }
184
185 /// Read-only iterator over initialized backend streams.
186 pub fn streams(&self) -> impl Iterator<Item = &B::Stream> {
187 self.pool.streams().map(|s| &s.stream)
188 }
189
190 /// Synthetic [`StreamId`]s, one per initialized stream (see [`StreamPool::stream_ids`]).
191 pub fn stream_ids(&self) -> impl Iterator<Item = StreamId> + '_ {
192 self.pool.stream_ids()
193 }
194
195 /// Registers a task for execution on a specific stream, ensuring stream alignment.
196 pub fn register(&mut self, stream_id: StreamId, task: B::Task, args_streams: &[StreamId]) {
197 // Align streams to ensure dependencies are handled correctly.
198 self.align_streams(stream_id, args_streams);
199
200 // Get the stream for the given stream ID and add the task to its queue.
201 let current = self.pool.get_mut(&stream_id);
202 current.tasks.push(task);
203
204 // If the task queue exceeds the maximum, execute the stream.
205 if current.tasks.len() >= self.max_tasks {
206 self.execute_streams(vec![stream_id]);
207 }
208 }
209
210 /// Aligns streams by flushing tasks from streams that conflict with the given bindings.
211 pub(crate) fn align_streams(&mut self, stream_id: StreamId, args_streams: &[StreamId]) {
212 let mut to_flush = Vec::new();
213 // Get the index of the target stream.
214 let index = self.pool.stream_index(&stream_id);
215
216 // Identify streams that need to be flushed due to conflicting bindings.
217 for arg_stream in args_streams {
218 let index_stream = self.pool.stream_index(arg_stream);
219 if index != index_stream {
220 to_flush.push(*arg_stream);
221
222 self.logger.log_streaming(
223 |level| matches!(level, StreamingLogLevel::Full),
224 || format!("Binding on {} is shared on {}", arg_stream, stream_id),
225 );
226 }
227 }
228
229 // If no streams need flushing, return early.
230 if to_flush.is_empty() {
231 return;
232 }
233
234 self.logger.log_streaming(
235 |level| !matches!(level, StreamingLogLevel::Disabled),
236 || {
237 format!(
238 "Flushing streams {to_flush:?} before registering more tasks on {stream_id}"
239 )
240 },
241 );
242 // Execute the streams that need to be flushed.
243 self.execute_streams(to_flush);
244 }
245
246 /// Executes tasks from the specified streams based on the scheduling strategy.
247 pub fn execute_streams(&mut self, stream_ids: Vec<StreamId>) {
248 let mut indices = Vec::with_capacity(stream_ids.len());
249
250 // Collect unique stream indices to avoid redundant processing.
251 for id in stream_ids {
252 let index = self.pool.stream_index(&id);
253 if !indices.contains(&index) {
254 indices.push(index);
255 }
256 }
257
258 // Create schedules for each stream to be executed, noting on the way
259 // whether any of them refuses to have its tasks interleaved (see
260 // [`SchedulerStreamBackend::requires_isolation`]).
261 let mut schedules = Vec::new();
262 let mut isolation = false;
263 for index in indices {
264 let stream = unsafe { self.pool.get_mut_index(index) }; // Note: `unsafe` usage assumes valid index.
265 isolation |= B::requires_isolation(&stream.stream);
266 let tasks = stream.flush();
267 let num_tasks = tasks.len();
268
269 schedules.push(Schedule {
270 tasks: tasks.into_iter(),
271 num_tasks,
272 stream_index: index,
273 });
274 }
275
276 // If no schedules were created, return early.
277 if schedules.is_empty() {
278 return;
279 }
280
281 // Execute schedules based on the configured strategy. Interleaving is
282 // suspended while any involved stream requires isolation; the
283 // sequential path keeps every task on the stream that owns it.
284 match self.strategy {
285 SchedulerStrategy::Interleave if !isolation => {
286 self.execute_schedules_interleave(schedules)
287 }
288 _ => self.execute_schedules_sequence(schedules),
289 }
290 }
291
292 /// Executes schedules sequentially, processing each stream's tasks in order.
293 fn execute_schedules_sequence(&mut self, schedules: Vec<Schedule<B>>) {
294 for schedule in schedules {
295 let stream = unsafe { self.pool.get_mut_index(schedule.stream_index) }; // Note: `unsafe` usage assumes valid index.
296 for task in schedule.tasks {
297 // Enqueue each task on the stream.
298 B::enqueue(task, &mut stream.stream, self.failures.graph_mut());
299 }
300
301 // Makes sure the tasks are ordered on the compute queue.
302 B::flush(&mut stream.stream, self.failures.graph_mut());
303 }
304 }
305
306 //// Executes schedules in an interleaved manner, alternating tasks from different streams.
307 ///
308 /// We chose the first stream as the one executing the tasks, ensuring proper ordering by
309 /// flushing all other streams first and flushing the execution stream at the end.
310 /// This way, we ensure that most tasks are actually interleaved on the real compute queue
311 /// shared across all streams.
312 fn execute_schedules_interleave(&mut self, mut schedules: Vec<Schedule<B>>) {
313 // Makes sure the tasks are ordered on the compute queue.
314 for schedule in schedules.iter_mut().skip(1) {
315 let stream = unsafe { self.pool.get_mut_index(schedule.stream_index) };
316 B::flush(&mut stream.stream, self.failures.graph_mut());
317 }
318
319 let execution_index = schedules.first().expect("At least one stream").stream_index;
320 let stream = unsafe { self.pool.get_mut_index(execution_index) };
321
322 // Find the maximum number of tasks across all schedules.
323 let num_tasks_max = schedules
324 .iter()
325 .map(|s| s.num_tasks)
326 .max()
327 .expect("At least one schedule");
328
329 // Iterate through tasks, interleaving them across streams.
330 for _ in 0..num_tasks_max {
331 for schedule in schedules.iter_mut() {
332 // If there are tasks remaining in the schedule, enqueue the next one.
333 if let Some(task) = schedule.tasks.next() {
334 B::enqueue(task, &mut stream.stream, self.failures.graph_mut());
335 }
336 }
337 }
338
339 // Making sure all tasks are registered to the queue.
340 B::flush(&mut stream.stream, self.failures.graph_mut());
341 }
342}
343
344// Represents a schedule for executing tasks on a specific stream.
345struct Schedule<B: SchedulerStreamBackend> {
346 // Iterator over the tasks to be executed.
347 tasks: alloc::vec::IntoIter<B::Task>,
348 // Number of tasks in the schedule.
349 num_tasks: usize,
350 // Index of the stream in the pool.
351 stream_index: usize,
352}