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 fn create(&mut self) -> Self::Stream;
35}
36
37/// The memory a stream's kernels see, for the taint bookkeeping.
38///
39/// Whether a buffer can be trusted lives on its allocation, inside one of the
40/// stream's memory managers. A backend supplies only which manager that is —
41/// the one whose allocations back [`BufferBinding`]s, never the auxiliary
42/// staging or uniform managers — and everything the drivers do with the
43/// answer lives on the shared wrappers.
44///
45/// The whole binding is passed rather than its memory handle because a
46/// binding names a byte range of its allocation ([`BufferBinding::range`]),
47/// and the claim is exactly that range: a launch that failed writing one
48/// region of a buffer says nothing about the rest of it.
49pub trait StreamMemory {
50 /// The failure claiming any byte the binding names, if one does.
51 fn failure(&self, binding: &BufferBinding) -> Option<FailureId>;
52
53 /// Point the bytes `binding` names at `failure`.
54 fn taint(&mut self, binding: &BufferBinding, failure: FailureId, failures: &mut ErrorGraph);
55
56 /// The bytes `binding` names have a writer again: release every claim on
57 /// them, and only on them.
58 fn written(&mut self, binding: &BufferBinding, failures: &mut ErrorGraph);
59}
60
61/// Represents a pool of streams, managing a collection of streams created by a factory.
62#[derive(Debug)]
63pub struct StreamPool<F: StreamFactory> {
64 /// Vector storing optional streams, where None indicates an uninitialized stream.
65 streams: Vec<Option<F::Stream>>,
66 /// The factory used to create new streams when needed.
67 factory: F,
68 /// Maximum number of regular streams (excludes special streams).
69 max_streams: usize,
70}
71
72impl<F: StreamFactory> StreamPool<F> {
73 /// Creates a new stream pool with the given backend factory and capacity constraints.
74 pub fn new(backend: F, max_streams: u8, num_special: u8) -> Self {
75 // Initialize a vector with capacity for regular and special streams.
76 let mut streams = Vec::with_capacity(max_streams as usize);
77 // Pre-populate the vector with None to reserve space for all streams.
78 for _ in 0..(max_streams.saturating_add(num_special)) {
79 streams.push(None);
80 }
81
82 Self {
83 streams,
84 factory: backend,
85 max_streams: max_streams as usize,
86 }
87 }
88
89 /// Read-only iterator over initialized streams (unlike [`Self::get_mut`], never creates one).
90 pub fn streams(&self) -> impl Iterator<Item = &F::Stream> {
91 self.streams.iter().flatten()
92 }
93
94 /// Synthetic [`StreamId`]s, one per initialized regular pool slot.
95 ///
96 /// Each id round-trips through [`Self::get_mut`] to the same slot it
97 /// came from (slot `i` is reachable via `StreamId { value: i }` since
98 /// indexing is `value % max_streams`), so it's safe to feed these
99 /// ids back into per-stream APIs.
100 pub fn stream_ids(&self) -> impl Iterator<Item = StreamId> + '_ {
101 self.streams[..self.max_streams]
102 .iter()
103 .enumerate()
104 .filter_map(|(i, s)| s.as_ref().map(|_| StreamId { value: i as u64 }))
105 }
106
107 /// Retrieves a mutable reference to a stream for a given stream ID.
108 pub fn get_mut(&mut self, stream_id: &StreamId) -> &mut F::Stream {
109 // Calculate the index for the stream ID.
110 let index = self.stream_index(stream_id);
111
112 // Use unsafe method to retrieve the stream, assuming the index is valid.
113 //
114 // # Safety
115 //
116 // * The `stream_index` function ensures the index is within bounds.
117 unsafe { self.get_mut_index(index) }
118 }
119
120 /// Retrieves a mutable reference to a stream at the specified index, initializing it if needed.
121 ///
122 /// # Safety
123 ///
124 /// * Caller must ensure the index is valid (less than `max_streams + num_special`).
125 /// * Lifetimes still follow the Rust rules.
126 pub unsafe fn get_mut_index(&mut self, index: usize) -> &mut F::Stream {
127 unsafe {
128 // Access the stream entry without bounds checking for performance.
129 let entry = self.streams.get_unchecked_mut(index);
130 match entry {
131 // If the stream exists, return it.
132 Some(val) => val,
133 // If the stream is None, create a new one using the factory.
134 None => {
135 let stream = self.factory.create();
136 // Store the new stream in the vector.
137 *entry = Some(stream);
138
139 // Re-access the entry, which is now guaranteed to be Some.
140 match entry {
141 Some(val) => val,
142 // Unreachable because we just set it to Some.
143 None => unreachable!(),
144 }
145 }
146 }
147 }
148 }
149
150 /// Retrieves a mutable reference to a special stream at the given index.
151 ///
152 /// # Safety
153 ///
154 /// * Caller must ensure the index corresponds to a valid special stream.
155 /// * Lifetimes still follow the Rust rules.
156 pub unsafe fn get_special(&mut self, index: u8) -> &mut F::Stream {
157 // Calculate the index for the special stream (offset by max_streams).
158 unsafe { self.get_mut_index(self.max_streams + index as usize) }
159 }
160
161 /// Calculates the index for a given stream ID, mapping it to the pool's capacity.
162 pub fn stream_index(&mut self, id: &StreamId) -> usize {
163 stream_index(id, self.max_streams)
164 }
165
166 /// The stream on `id`'s slot, when that slot was ever initialized.
167 ///
168 /// Never creates one: resolving a buffer's owning slot must not bring a
169 /// backend stream into existence, which on CUDA and HIP would bind it to
170 /// whichever context happens to be current. A buffer's slot was
171 /// initialized by the allocation itself, so `None` here means the binding
172 /// is not this pool's to answer for.
173 pub fn try_get(&self, id: &StreamId) -> Option<&F::Stream> {
174 self.streams[stream_index(id, self.max_streams)].as_ref()
175 }
176
177 /// [`try_get`](Self::try_get), mutably.
178 pub fn try_get_mut(&mut self, id: &StreamId) -> Option<&mut F::Stream> {
179 self.streams[stream_index(id, self.max_streams)].as_mut()
180 }
181
182 /// Mutable access to the factory, e.g. to change the configuration new
183 /// streams are created with. Already-created streams are unaffected.
184 pub fn factory_mut(&mut self) -> &mut F {
185 &mut self.factory
186 }
187}
188
189/// Maps a stream ID to an index within the pool's capacity using modulo arithmetic.
190pub fn stream_index(stream_id: &StreamId, max_streams: usize) -> usize {
191 stream_id.value as usize % max_streams
192}
193
194/// Point the bytes every binding in `written` names at `failure`.
195///
196/// Each binding is resolved to the stream that allocated it, which may not be
197/// the stream that failed — that is the point: the fact lands on the memory,
198/// wherever it lives. A binding whose slot no stream ever initialized is
199/// skipped; it is not this pool's to answer for.
200///
201/// Free rather than a method because the pool and the graph are held apart by
202/// every caller: a driver owns both, a resolved borrow holds both mutably.
203pub fn taint_with<'a, F>(
204 pool: &mut StreamPool<F>,
205 failure: FailureId,
206 written: impl Iterator<Item = &'a BufferBinding>,
207 graph: &mut ErrorGraph,
208) where
209 F: StreamFactory<Stream: StreamMemory>,
210{
211 for handle in written {
212 if let Some(stream) = pool.try_get_mut(&handle.stream) {
213 stream.taint(handle, failure, graph);
214 }
215 }
216}
217
218/// [`taint_with`] under a failure minted for `error`, dropped again when it
219/// claimed nothing: a failure no buffer still holds has nothing to wait for.
220pub fn taint<'a, F>(
221 pool: &mut StreamPool<F>,
222 error: ServerError,
223 written: impl Iterator<Item = &'a BufferBinding>,
224 graph: &mut ErrorGraph,
225) where
226 F: StreamFactory<Stream: StreamMemory>,
227{
228 let failure = graph.insert(error);
229 taint_with(pool, failure, written, graph);
230 graph.prune(failure);
231}
232
233/// Release the failure on every allocation in `written`: work that writes
234/// them has been enqueued, so a read of one is no longer reading bytes
235/// nothing wrote.
236pub fn written<'a, F>(
237 pool: &mut StreamPool<F>,
238 written: impl Iterator<Item = &'a BufferBinding>,
239 graph: &mut ErrorGraph,
240) where
241 F: StreamFactory<Stream: StreamMemory>,
242{
243 for handle in written {
244 if let Some(stream) = pool.try_get_mut(&handle.stream) {
245 stream.written(handle, graph);
246 }
247 }
248}