Skip to main content

cubecl_server/stream/
scheduler.rs

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