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 dry run, 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}