Skip to main content

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}