Skip to main content

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}