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}