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, ManagedMemoryHandle, 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            // A buffer that was never allocated has no slice to carry a failure: the reason
126            // it was not allocated is recorded on the graph instead.
127            let failure = match handle.memory.descriptor().is_allocated() {
128                true => pool.try_get(&handle.stream)?.failure(handle)?,
129                false => failures.graph.unallocated_failure(handle.memory.id())?,
130            };
131            Some(ReadFailure {
132                failure,
133                needed: handle.memory.id(),
134                error: failures.graph.error(failure)?.clone(),
135            })
136        })
137    }
138
139    /// Taint every allocation in `written` with `error`: the work that was
140    /// going to write those buffers did not run, so a read of any of them
141    /// fails on this failure until something writes them again.
142    ///
143    /// Each binding is resolved to the manager of the stream it was created
144    /// on, which may not be the stream that failed — that is the point: the
145    /// fact lands on the memory, wherever it lives.
146    fn taint<'a>(&mut self, error: ServerError, written: impl Iterator<Item = &'a BufferBinding>) {
147        let (pool, failures) = self.split();
148        base::taint(pool, error, written, &mut failures.graph);
149    }
150
151    /// Release the failure on every allocation in `written`: work that writes
152    /// them has been enqueued, so a read of one is no longer reading bytes
153    /// nothing wrote.
154    fn written<'a>(&mut self, written: impl Iterator<Item = &'a BufferBinding>) {
155        let (pool, failures) = self.split();
156        base::written(pool, written, &mut failures.graph);
157    }
158
159    /// Record `error` as the reason `memory` was never allocated, so reading that memory
160    /// reports that error.
161    fn fail_unallocated(&mut self, memory: &ManagedMemoryHandle, error: ServerError) {
162        let (_, failures) = self.split();
163        failures.graph.fail_unallocated(memory, error);
164    }
165
166    /// A skipped launch's outputs take the failure that stopped it: nothing
167    /// wrote them, exactly as if the launch had failed, and the claim names
168    /// the root cause rather than minting a new one. The skip is recorded on
169    /// the failure, so a read of anything downstream can name the path back
170    /// to the root.
171    ///
172    /// Takes the write set by value and hands it back to the pool, the same
173    /// contract [`exit_write`](Self::exit_write) has, because a skip is the
174    /// other way a scope ends: a loop carrying a tainted buffer forward skips
175    /// on every iteration — the most frequent event in this whole design — and
176    /// a set the skip path dropped would allocate a fresh one every time.
177    fn propagate(
178        &mut self,
179        found: &ReadFailure,
180        kernel: KernelId,
181        mut written: Vec<BufferBinding>,
182    ) {
183        let (pool, failures) = self.split();
184        failures.graph.skipped(
185            found.failure,
186            Skipped {
187                kernel,
188                needed: found.needed,
189                produced: written.iter().map(|handle| handle.memory.id()).collect(),
190            },
191        );
192        base::taint_with(pool, found.failure, written.iter(), &mut failures.graph);
193        written.clear();
194        failures.scratch = written;
195    }
196
197    /// An empty write set, pooled here so a launch allocates nothing for it.
198    /// [`exit_write`](Self::exit_write) hands it back.
199    fn write_set(&mut self) -> Vec<BufferBinding> {
200        let (_, failures) = self.split();
201        core::mem::take(&mut failures.scratch)
202    }
203
204    /// Enter a write scope over `written`: taint every buffer the work is
205    /// going to write with a provisional failure, minted here because the real
206    /// one does not exist yet.
207    ///
208    /// The default this sets is tainted unless proven written — the opposite
209    /// of clearing on success and hoping every failure path remembered to
210    /// taint. A body that returns early, or panics before
211    /// [`exit_write`](Self::exit_write) runs, leaves the write set carrying
212    /// this failure, so a read of one of its buffers fails loudly instead of
213    /// returning bytes nothing wrote.
214    ///
215    /// An empty write set — a discarded launch, a launch writing nothing — claims
216    /// nothing and mints nothing.
217    fn enter_write(&mut self, written: &[BufferBinding]) -> Option<FailureId> {
218        if written.is_empty() {
219            return None;
220        }
221        let (pool, failures) = self.split();
222        // Payload-free on purpose: this node is minted and dropped again on
223        // every launch that succeeds, so it may not cost a formatted string
224        // or a stack walk. See [`ServerError::TornDown`].
225        let provisional = failures.graph.insert(ServerError::TornDown);
226        base::taint_with(pool, provisional, written.iter(), &mut failures.graph);
227        Some(provisional)
228    }
229
230    /// Settle the scope entered over `written`: release the provisional
231    /// failure when the work was enqueued, and swap the real error in for it
232    /// when the work was not. The taint is the whole answer — a read of one
233    /// of these buffers fails on it, whoever asks — and the error is logged
234    /// here, the backstop for the failure nobody ever reads. The staged
235    /// vector goes back to the pool either way.
236    fn exit_write(
237        &mut self,
238        provisional: Option<FailureId>,
239        mut written: Vec<BufferBinding>,
240        error: Option<&ServerError>,
241    ) {
242        match error {
243            None => self.written(written.iter()),
244            Some(error) => {
245                let (_, failures) = self.split();
246                failures.logger.log_failure(error);
247                if let Some(provisional) = provisional {
248                    failures.graph.replace(provisional, error.clone());
249                }
250            }
251        }
252        let (_, failures) = self.split();
253        // Covers the failure that tainted nothing — every binding resolving to
254        // a slot no stream ever initialized — and costs one lookup otherwise.
255        if let Some(provisional) = provisional {
256            failures.graph.prune(provisional);
257        }
258        written.clear();
259        failures.scratch = written;
260    }
261}