cubecl_server/stream/base.rs
1//! The pool of backend streams, and the primitives that record on their
2//! memory what a unit of work did or did not write.
3//!
4//! The pool answers one question — which stream sits in a slot — and the
5//! free functions below answer the other: which stream allocated a binding,
6//! so the claim lands on the memory rather than on whoever failed. They are
7//! free because the pool and the failure graph are held apart by every
8//! caller; the surface a driver actually uses is
9//! [`FailureStore`](super::FailureStore).
10
11use crate::memory_management::{ErrorGraph, FailureId, ManagedMemoryId};
12use crate::server::{BufferBinding, ServerError};
13use alloc::vec::Vec;
14use cubecl_environment::stream::StreamId;
15
16/// What a launch's read-set check found: the failure claiming an input, the
17/// input it claims, and the error — everything the skip needs to record and
18/// a capture needs to fail with.
19pub struct ReadFailure {
20 /// The failure claiming the input.
21 pub failure: FailureId,
22 /// The claimed input, which the skip record names as what the launch
23 /// needed.
24 pub needed: ManagedMemoryId,
25 /// The failure's error, cloned for the paths that report it directly.
26 pub error: ServerError,
27}
28
29/// Trait for creating streams, used by the stream pool to generate streams as needed.
30pub trait StreamFactory {
31 /// The type of stream produced by this factory.
32 type Stream;
33 /// Creates a new stream instance.
34 ///
35 /// # Errors
36 ///
37 /// Returns a [`ServerError`] when the backend cannot create a stream, e.g. on a device that
38 /// is poisoned.
39 fn create(&mut self) -> Result<Self::Stream, ServerError>;
40}
41
42/// The memory a stream's kernels see, for the taint bookkeeping.
43///
44/// Whether a buffer can be trusted lives on its allocation, inside one of the
45/// stream's memory managers. A backend supplies only which manager that is —
46/// the one whose allocations back [`BufferBinding`]s, never the auxiliary
47/// staging or uniform managers — and everything the drivers do with the
48/// answer lives on the shared wrappers.
49///
50/// The whole binding is passed rather than its memory handle because a
51/// binding names a byte range of its allocation ([`BufferBinding::range`]),
52/// and the claim is exactly that range: a launch that failed writing one
53/// region of a buffer says nothing about the rest of it.
54pub trait StreamMemory {
55 /// The failure claiming any byte the binding names, if one does.
56 fn failure(&self, binding: &BufferBinding) -> Option<FailureId>;
57
58 /// Point the bytes `binding` names at `failure`.
59 fn taint(&mut self, binding: &BufferBinding, failure: FailureId, failures: &mut ErrorGraph);
60
61 /// The bytes `binding` names have a writer again: release every claim on
62 /// them, and only on them.
63 fn written(&mut self, binding: &BufferBinding, failures: &mut ErrorGraph);
64}
65
66/// Represents a pool of streams, managing a collection of streams created by a factory.
67#[derive(Debug)]
68pub struct StreamPool<F: StreamFactory> {
69 /// Vector storing optional streams, where None indicates an uninitialized stream.
70 streams: Vec<Option<F::Stream>>,
71 /// The factory used to create new streams when needed.
72 factory: F,
73 /// Maximum number of regular streams (excludes special streams).
74 max_streams: usize,
75}
76
77impl<F: StreamFactory> StreamPool<F> {
78 /// Creates a new stream pool with the given backend factory and capacity constraints.
79 pub fn new(backend: F, max_streams: u8, num_special: u8) -> Self {
80 // Initialize a vector with capacity for regular and special streams.
81 let mut streams = Vec::with_capacity(max_streams as usize);
82 // Pre-populate the vector with None to reserve space for all streams.
83 for _ in 0..(max_streams.saturating_add(num_special)) {
84 streams.push(None);
85 }
86
87 Self {
88 streams,
89 factory: backend,
90 max_streams: max_streams as usize,
91 }
92 }
93
94 /// Read-only iterator over initialized streams (unlike [`Self::get_mut`], never creates one).
95 pub fn streams(&self) -> impl Iterator<Item = &F::Stream> {
96 self.streams.iter().flatten()
97 }
98
99 /// [`streams`](Self::streams), mutably.
100 pub fn streams_mut(&mut self) -> impl Iterator<Item = &mut F::Stream> {
101 self.streams.iter_mut().flatten()
102 }
103
104 /// Synthetic [`StreamId`]s, one per initialized regular pool slot.
105 ///
106 /// Each id round-trips through [`Self::get_mut`] to the same slot it
107 /// came from (slot `i` is reachable via `StreamId { value: i }` since
108 /// indexing is `value % max_streams`), so it's safe to feed these
109 /// ids back into per-stream APIs.
110 pub fn stream_ids(&self) -> impl Iterator<Item = StreamId> + '_ {
111 self.streams[..self.max_streams]
112 .iter()
113 .enumerate()
114 .filter_map(|(i, s)| s.as_ref().map(|_| StreamId { value: i as u64 }))
115 }
116
117 /// Retrieves a mutable reference to a stream for a given stream ID.
118 pub fn get_mut(&mut self, stream_id: &StreamId) -> &mut F::Stream {
119 // Calculate the index for the stream ID.
120 let index = self.stream_index(stream_id);
121
122 // Use unsafe method to retrieve the stream, assuming the index is valid.
123 //
124 // # Safety
125 //
126 // * The `stream_index` function ensures the index is within bounds.
127 unsafe { self.get_mut_index(index) }
128 }
129
130 /// Retrieves a mutable reference to a stream at the specified index, initializing it if needed.
131 ///
132 /// # Panics
133 ///
134 /// When a required stream creation fails.
135 ///
136 /// # Safety
137 ///
138 /// * Caller must ensure the index is valid (less than `max_streams + num_special`).
139 /// * Lifetimes still follow the Rust rules.
140 pub unsafe fn get_mut_index(&mut self, index: usize) -> &mut F::Stream {
141 unsafe { self.get_or_create_index(index) }
142 .unwrap_or_else(|err| panic!("a stream could not be created: {err}"))
143 }
144
145 /// The stream for `stream_id`, created if it does not exist yet.
146 ///
147 /// # Errors
148 ///
149 /// Returns a [`ServerError`] when a required stream creation fails.
150 pub fn get_or_create(&mut self, stream_id: &StreamId) -> Result<&mut F::Stream, ServerError> {
151 let index = self.stream_index(stream_id);
152 // SAFETY: `stream_index` keeps the index within the regular streams.
153 unsafe { self.get_or_create_index(index) }
154 }
155
156 /// The special stream at `index`, created if it does not exist yet.
157 ///
158 /// # Errors
159 ///
160 /// Same as [`get_or_create`](Self::get_or_create).
161 ///
162 /// # Safety
163 ///
164 /// * Caller must ensure the index corresponds to a valid special stream.
165 pub unsafe fn get_or_create_special(
166 &mut self,
167 index: u8,
168 ) -> Result<&mut F::Stream, ServerError> {
169 unsafe { self.get_or_create_index(self.max_streams + index as usize) }
170 }
171
172 /// The stream at `index`, created if it does not exist yet.
173 ///
174 /// # Safety
175 ///
176 /// * Caller must ensure the index is valid (less than `max_streams + num_special`).
177 pub(crate) unsafe fn get_or_create_index(
178 &mut self,
179 index: usize,
180 ) -> Result<&mut F::Stream, ServerError> {
181 unsafe {
182 // Access the stream entry without bounds checking for performance.
183 let entry = self.streams.get_unchecked_mut(index);
184 match entry {
185 // If the stream exists, return it.
186 Some(val) => Ok(val),
187 // If the stream is None, create a new one using the factory.
188 None => {
189 let stream = self.factory.create()?;
190 // Store the new stream in the vector.
191 *entry = Some(stream);
192
193 // Re-access the entry, which is now guaranteed to be Some.
194 match entry {
195 Some(val) => Ok(val),
196 // Unreachable because we just set it to Some.
197 None => unreachable!(),
198 }
199 }
200 }
201 }
202 }
203
204 /// Retrieves a mutable reference to a special stream at the given index.
205 ///
206 /// # Safety
207 ///
208 /// * Caller must ensure the index corresponds to a valid special stream.
209 /// * Lifetimes still follow the Rust rules.
210 pub unsafe fn get_special(&mut self, index: u8) -> &mut F::Stream {
211 // Calculate the index for the special stream (offset by max_streams).
212 unsafe { self.get_mut_index(self.max_streams + index as usize) }
213 }
214
215 /// Calculates the index for a given stream ID, mapping it to the pool's capacity.
216 pub fn stream_index(&mut self, id: &StreamId) -> usize {
217 stream_index(id, self.max_streams)
218 }
219
220 /// The stream on `id`'s slot, when that slot was ever initialized.
221 ///
222 /// Never creates one: resolving a buffer's owning slot must not bring a
223 /// backend stream into existence, which on CUDA and HIP would bind it to
224 /// whichever context happens to be current. A buffer's slot was
225 /// initialized by the allocation itself, so `None` here means the binding
226 /// is not this pool's to answer for.
227 pub fn try_get(&self, id: &StreamId) -> Option<&F::Stream> {
228 self.streams[stream_index(id, self.max_streams)].as_ref()
229 }
230
231 /// [`try_get`](Self::try_get), mutably.
232 pub fn try_get_mut(&mut self, id: &StreamId) -> Option<&mut F::Stream> {
233 self.streams[stream_index(id, self.max_streams)].as_mut()
234 }
235
236 /// Mutable access to the factory, e.g. to change the configuration new
237 /// streams are created with. Already-created streams are unaffected.
238 pub fn factory_mut(&mut self) -> &mut F {
239 &mut self.factory
240 }
241}
242
243/// Maps a stream ID to an index within the pool's capacity using modulo arithmetic.
244pub fn stream_index(stream_id: &StreamId, max_streams: usize) -> usize {
245 stream_id.value as usize % max_streams
246}
247
248/// Point the bytes every binding in `written` names at `failure`.
249///
250/// Each binding is resolved to the stream that allocated it, which may not be
251/// the stream that failed — that is the point: the fact lands on the memory,
252/// wherever it lives. A binding whose slot no stream ever initialized is
253/// skipped; it is not this pool's to answer for.
254///
255/// Free rather than a method because the pool and the graph are held apart by
256/// every caller: a driver owns both, a resolved borrow holds both mutably.
257pub fn taint_with<'a, F>(
258 pool: &mut StreamPool<F>,
259 failure: FailureId,
260 written: impl Iterator<Item = &'a BufferBinding>,
261 graph: &mut ErrorGraph,
262) where
263 F: StreamFactory<Stream: StreamMemory>,
264{
265 for handle in written {
266 if let Some(stream) = pool.try_get_mut(&handle.stream) {
267 stream.taint(handle, failure, graph);
268 }
269 }
270}
271
272/// [`taint_with`] under a failure minted for `error`, dropped again when it
273/// claimed nothing: a failure no buffer still holds has nothing to wait for.
274pub fn taint<'a, F>(
275 pool: &mut StreamPool<F>,
276 error: ServerError,
277 written: impl Iterator<Item = &'a BufferBinding>,
278 graph: &mut ErrorGraph,
279) where
280 F: StreamFactory<Stream: StreamMemory>,
281{
282 let failure = graph.insert(error);
283 taint_with(pool, failure, written, graph);
284 graph.prune(failure);
285}
286
287/// Release the failure on every allocation in `written`: work that writes
288/// them has been enqueued, so a read of one is no longer reading bytes
289/// nothing wrote.
290pub fn written<'a, F>(
291 pool: &mut StreamPool<F>,
292 written: impl Iterator<Item = &'a BufferBinding>,
293 graph: &mut ErrorGraph,
294) where
295 F: StreamFactory<Stream: StreamMemory>,
296{
297 for handle in written {
298 if let Some(stream) = pool.try_get_mut(&handle.stream) {
299 stream.written(handle, graph);
300 }
301 }
302}