Skip to main content

cubecl_server/stream/
failures.rs

1//! The device's failure store, and the surface every multi-stream driver
2//! gives over it.
3//!
4//! Two drivers exist — the event-ordered [`MultiStream`](super::MultiStream)
5//! and the [`SchedulerMultiStream`](super::scheduler::SchedulerMultiStream) —
6//! and every rule about taint is the same on both. So the state lives in one
7//! type here, the operations are default methods on one trait, and a driver
8//! supplies only the thing that differs: where its pool and its store are.
9//! Written twice, the two would drift on the first rule either one gained.
10
11use crate::id::KernelId;
12use crate::logging::ServerLogger;
13use crate::memory_management::{Claim, ErrorGraph, FailureId, Skipped};
14use crate::server::{BufferBinding, ServerError};
15use crate::stream::{ReadFailure, StreamFactory, StreamMemory, StreamPool, base};
16use alloc::sync::Arc;
17use alloc::vec::Vec;
18
19/// Every failure a device is still holding, plus the little a write scope
20/// needs around them.
21///
22/// One store per device rather than per stream, because a launch failing on
23/// one stream can claim a slice owned by another and both have to point at
24/// the same thing.
25///
26/// There is no device-wide failure beside the graph. A failure belongs to the
27/// memory it left unwritten, and work that shares no buffer with it is
28/// unaffected — which is the whole point, and a slot that failed the next
29/// flush regardless of what it was flushing was the one thing that broke it.
30#[derive(Debug)]
31pub struct Failures {
32    graph: ErrorGraph,
33    /// The vector a write scope stages its write set in, pooled here so a
34    /// launch allocates nothing for it.
35    scratch: Vec<BufferBinding>,
36    logger: Arc<ServerLogger>,
37}
38
39impl Failures {
40    /// An empty store for a device that has failed at nothing yet.
41    pub fn new(logger: Arc<ServerLogger>) -> Self {
42        Self {
43            graph: ErrorGraph::default(),
44            scratch: Vec::new(),
45            logger,
46        }
47    }
48
49    /// What the failure ids carried by this device's allocations mean.
50    pub fn graph(&self) -> &ErrorGraph {
51        &self.graph
52    }
53
54    /// [`graph`](Self::graph) mutably, handed down to every reserve, bind and
55    /// cleanup — those are where slices shed the failures they carry.
56    pub fn graph_mut(&mut self) -> &mut ErrorGraph {
57        &mut self.graph
58    }
59}
60
61/// A multi-stream driver that owns a device's [`Failures`].
62///
63/// Implementing [`split`](Self::split) and [`parts`](Self::parts) buys the
64/// whole taint surface below, and the write scope's hooks with it. Whether a
65/// buffer can be trusted lives on its allocation, so every operation here is
66/// the same two steps: resolve each binding to the stream that allocated it,
67/// and tell that stream's memory what happened.
68pub trait FailureStore {
69    /// The factory the driver's [`StreamPool`] was built from. Its streams
70    /// expose the memory the taint is recorded on.
71    type Factory: StreamFactory<Stream: StreamMemory>;
72
73    /// The pool and the store, split-borrowed: nearly every operation here
74    /// reaches the allocations through the pool while mutating the store.
75    fn split(&mut self) -> (&mut StreamPool<Self::Factory>, &mut Failures);
76
77    /// [`split`](Self::split) for the read-only questions.
78    fn parts(&self) -> (&StreamPool<Self::Factory>, &Failures);
79
80    /// Fails when the buffers `handles` name carry a failure, with the errors
81    /// of the work that was supposed to write them.
82    ///
83    /// A read is only as good as the work that wrote the buffer: a launch that
84    /// failed never wrote it, so copying its bytes out hands back whatever was
85    /// in memory before. Whether that happened is a field on the allocation,
86    /// so the question is answered by the slice each binding resolves to — a
87    /// lookup the read was going to do anyway — and by nobody's queue.
88    ///
89    /// # Errors
90    ///
91    /// [`ServerError::Several`] naming every failure one of these buffers
92    /// carries, each failure once however many buffers carry it, and every
93    /// buffer that was never allocated. The caller has nothing to retry — the
94    /// bytes are gone — so the error is the answer to the read, not a hint to
95    /// try again.
96    fn ensure_written<'a>(
97        &self,
98        handles: impl Iterator<Item = &'a BufferBinding>,
99    ) -> Result<(), ServerError> {
100        let (pool, failures) = self.parts();
101        failures.graph.reports(handles.filter_map(|handle| {
102            let memory = handle.memory.id();
103            if !handle.memory.descriptor().is_allocated() {
104                return Some(Claim::Unallocated(memory));
105            }
106            let failure = pool.try_get(&handle.stream)?.failure(handle)?;
107            Some(Claim::Failed(failure, memory))
108        }))
109    }
110
111    /// The failure claiming bytes any of `reads` names, with its error — the
112    /// check a launch makes before it runs.
113    ///
114    /// A launch whose input cannot be trusted does not run: a buffer holding
115    /// garbage can be read as a dynamic cube count or as indices in a gather,
116    /// so running would risk dispatching an absurd grid or scattering into
117    /// memory that carried no failure at all. Skipping costs the same lookup,
118    /// because the inputs have to be read either way to decide anything.
119    fn read_failure<'a>(
120        &self,
121        mut reads: impl Iterator<Item = &'a BufferBinding>,
122    ) -> Option<ReadFailure> {
123        let (pool, failures) = self.parts();
124        reads.find_map(|handle| {
125            let failure = pool.try_get(&handle.stream)?.failure(handle)?;
126            Some(ReadFailure {
127                failure,
128                needed: handle.memory.id(),
129                error: failures.graph.error(failure)?.clone(),
130            })
131        })
132    }
133
134    /// Taint every allocation in `written` with `error`: the work that was
135    /// going to write those buffers did not run, so a read of any of them
136    /// fails on this failure until something writes them again.
137    ///
138    /// Each binding is resolved to the manager of the stream it was created
139    /// on, which may not be the stream that failed — that is the point: the
140    /// fact lands on the memory, wherever it lives.
141    fn taint<'a>(&mut self, error: ServerError, written: impl Iterator<Item = &'a BufferBinding>) {
142        let (pool, failures) = self.split();
143        base::taint(pool, error, written, &mut failures.graph);
144    }
145
146    /// Release the failure on every allocation in `written`: work that writes
147    /// them has been enqueued, so a read of one is no longer reading bytes
148    /// nothing wrote.
149    fn written<'a>(&mut self, written: impl Iterator<Item = &'a BufferBinding>) {
150        let (pool, failures) = self.split();
151        base::written(pool, written, &mut failures.graph);
152    }
153
154    /// A skipped launch's outputs take the failure that stopped it: nothing
155    /// wrote them, exactly as if the launch had failed, and the claim names
156    /// the root cause rather than minting a new one. The skip is recorded on
157    /// the failure, so a read of anything downstream can name the path back
158    /// to the root.
159    ///
160    /// Takes the write set by value and hands it back to the pool, the same
161    /// contract [`exit_write`](Self::exit_write) has, because a skip is the
162    /// other way a scope ends: a loop carrying a tainted buffer forward skips
163    /// on every iteration — the most frequent event in this whole design — and
164    /// a set the skip path dropped would allocate a fresh one every time.
165    fn propagate(
166        &mut self,
167        found: &ReadFailure,
168        kernel: KernelId,
169        mut written: Vec<BufferBinding>,
170    ) {
171        let (pool, failures) = self.split();
172        failures.graph.skipped(
173            found.failure,
174            Skipped {
175                kernel,
176                needed: found.needed,
177                produced: written.iter().map(|handle| handle.memory.id()).collect(),
178            },
179        );
180        base::taint_with(pool, found.failure, written.iter(), &mut failures.graph);
181        written.clear();
182        failures.scratch = written;
183    }
184
185    /// An empty write set, pooled here so a launch allocates nothing for it.
186    /// [`exit_write`](Self::exit_write) hands it back.
187    fn write_set(&mut self) -> Vec<BufferBinding> {
188        let (_, failures) = self.split();
189        core::mem::take(&mut failures.scratch)
190    }
191
192    /// Enter a write scope over `written`: taint every buffer the work is
193    /// going to write with a provisional failure, minted here because the real
194    /// one does not exist yet.
195    ///
196    /// The default this sets is tainted unless proven written — the opposite
197    /// of clearing on success and hoping every failure path remembered to
198    /// taint. A body that returns early, or panics before
199    /// [`exit_write`](Self::exit_write) runs, leaves the write set carrying
200    /// this failure, so a read of one of its buffers fails loudly instead of
201    /// returning bytes nothing wrote.
202    ///
203    /// An empty write set — a dry run, a launch writing nothing — claims
204    /// nothing and mints nothing.
205    fn enter_write(&mut self, written: &[BufferBinding]) -> Option<FailureId> {
206        if written.is_empty() {
207            return None;
208        }
209        let (pool, failures) = self.split();
210        // Payload-free on purpose: this node is minted and dropped again on
211        // every launch that succeeds, so it may not cost a formatted string
212        // or a stack walk. See [`ServerError::TornDown`].
213        let provisional = failures.graph.insert(ServerError::TornDown);
214        base::taint_with(pool, provisional, written.iter(), &mut failures.graph);
215        Some(provisional)
216    }
217
218    /// Settle the scope entered over `written`: release the provisional
219    /// failure when the work was enqueued, and swap the real error in for it
220    /// when the work was not. The taint is the whole answer — a read of one
221    /// of these buffers fails on it, whoever asks — and the error is logged
222    /// here, the backstop for the failure nobody ever reads. The staged
223    /// vector goes back to the pool either way.
224    fn exit_write(
225        &mut self,
226        provisional: Option<FailureId>,
227        mut written: Vec<BufferBinding>,
228        error: Option<&ServerError>,
229    ) {
230        match error {
231            None => self.written(written.iter()),
232            Some(error) => {
233                let (_, failures) = self.split();
234                failures.logger.log_failure(error);
235                if let Some(provisional) = provisional {
236                    failures.graph.replace(provisional, error.clone());
237                }
238            }
239        }
240        let (_, failures) = self.split();
241        // Covers the failure that tainted nothing — every binding resolving to
242        // a slot no stream ever initialized — and costs one lookup otherwise.
243        if let Some(provisional) = provisional {
244            failures.graph.prune(provisional);
245        }
246        written.clear();
247        failures.scratch = written;
248    }
249}