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