Skip to main content

cubecl_server/memory_management/
taint.rs

1//! The tainted regions of one allocation.
2//!
3//! Whole-allocation taint answers whether a buffer can be trusted, but at the
4//! wrong grain: a host write covering one row of a tensor would end the claim
5//! on all of it, and a launch that failed writing one region would fail reads
6//! of the rest. Both matter more once failures start changing control flow —
7//! a partial write that clears a whole allocation un-skips every launch
8//! downstream, which is garbage that carries no failure to report.
9//!
10//! So the claim is a set of byte ranges, each pointing at the failure that
11//! made it. What a [`FailureId`] means still lives in the [`ErrorGraph`];
12//! this type owns the carrier side of the refcount — one tag per failure per
13//! allocation, held while that failure still claims at least one byte.
14//!
15//! # Why the storage is inline
16//!
17//! Every launch that writes anything runs the whole cycle on the success
18//! path: the write scope claims each buffer on the way in and releases it on
19//! the way out. So the case to price is not the clean slice, it is *one
20//! failure over one range, held for the length of a launch* — and behind a
21//! `Box<Vec<Vec<_>>>` that case cost three allocations and three frees per
22//! written buffer per launch. Inline capacity for one claim of one range
23//! makes it cost none, for about four words on a slice that already holds a
24//! storage handle, a memory handle, a cursor and its padding. A second
25//! failure, or a range a partial write split in two, spills to the heap as
26//! before.
27
28use super::{ErrorGraph, FailureId};
29use core::ops::Range;
30use smallvec::SmallVec;
31
32/// The claims one allocation carries before spilling to the heap. One: a
33/// launch's write scope claims the whole binding under a single failure, and
34/// that is what every launch of a working program does.
35type Claims = SmallVec<[Tainted; 1]>;
36
37/// The ranges one claim covers before spilling. One: a claim starts as the
38/// whole binding and only splits when a partial write lands inside it.
39type Ranges = SmallVec<[Range<u64>; 1]>;
40
41/// The tainted regions of one allocation, refcounted into the device's
42/// [`ErrorGraph`].
43#[derive(Debug, Default)]
44pub struct Taint {
45    entries: Claims,
46}
47
48/// One failure's claim on the allocation.
49#[derive(Debug)]
50struct Tainted {
51    failure: FailureId,
52    /// The bytes this failure left unwritten: disjoint, sorted, never empty.
53    /// More than one range because a partial write can split a claim in two.
54    ranges: Ranges,
55}
56
57impl Taint {
58    /// Point `range` at `failure`, releasing whatever claim other failures
59    /// held on those bytes: the work that failed is their last writer now,
60    /// and whatever the previous writer did or did not do stops mattering.
61    ///
62    /// Tainting is *set*, never *add*: re-tainting bytes this failure already
63    /// claims changes nothing, so a loop failing the same way every iteration
64    /// cannot grow the claim or pin the node harder. An empty range claims
65    /// nothing.
66    pub fn taint(&mut self, range: Range<u64>, failure: FailureId, failures: &mut ErrorGraph) {
67        if range.is_empty() {
68            return;
69        }
70        let entries = &mut self.entries;
71        entries.retain_mut(|entry| {
72            if entry.failure == failure {
73                return true;
74            }
75            subtract(&mut entry.ranges, &range);
76            match entry.ranges.is_empty() {
77                true => {
78                    failures.untag(Some(entry.failure));
79                    false
80                }
81                false => true,
82            }
83        });
84        match entries.iter_mut().find(|entry| entry.failure == failure) {
85            Some(entry) => add(&mut entry.ranges, range),
86            None => {
87                failures.tag(failure);
88                entries.push(Tainted {
89                    failure,
90                    ranges: Ranges::from_buf([range]),
91                });
92            }
93        }
94    }
95
96    /// The bytes in `range` have a writer again: release every claim on them,
97    /// and only on them — a write covering part of a buffer says nothing
98    /// about the rest, which keeps carrying the failure that left it stale.
99    pub fn written(&mut self, range: Range<u64>, failures: &mut ErrorGraph) {
100        if range.is_empty() {
101            return;
102        }
103        self.entries.retain_mut(|entry| {
104            subtract(&mut entry.ranges, &range);
105            match entry.ranges.is_empty() {
106                true => {
107                    failures.untag(Some(entry.failure));
108                    false
109                }
110                false => true,
111            }
112        });
113    }
114
115    /// The failure claiming any byte of `range`, if one does.
116    ///
117    /// A range overlapping several failures names one of them: the read fails
118    /// either way, and the caller dedupes by id across a whole read anyway.
119    pub fn failure(&self, range: &Range<u64>) -> Option<FailureId> {
120        self.entries
121            .iter()
122            .find(|entry| entry.ranges.iter().any(|held| overlaps(held, range)))
123            .map(|entry| entry.failure)
124    }
125
126    /// Release every claim, for an allocation that stops existing — the slice
127    /// is rebound, coalesced away, tombstoned or swept — since a tag must not
128    /// outlive its carrier.
129    pub fn clear(&mut self, failures: &mut ErrorGraph) {
130        for entry in core::mem::take(&mut self.entries) {
131            failures.untag(Some(entry.failure));
132        }
133    }
134
135    /// Whether nothing claims any byte.
136    pub fn is_clean(&self) -> bool {
137        self.entries.is_empty()
138    }
139}
140
141fn overlaps(a: &Range<u64>, b: &Range<u64>) -> bool {
142    // Their intersection is non-empty — which an empty range's never is, so a
143    // zero-sized binding neither trips a claim nor loses one.
144    a.start.max(b.start) < a.end.min(b.end)
145}
146
147/// Remove `cut` from `ranges`, splitting a range it lands inside.
148fn subtract(ranges: &mut Ranges, cut: &Range<u64>) {
149    let mut index = 0;
150    while index < ranges.len() {
151        let held = ranges[index].clone();
152        if !overlaps(&held, cut) {
153            index += 1;
154            continue;
155        }
156        let left = held.start..cut.start.min(held.end);
157        let right = cut.end.max(held.start)..held.end;
158        match (left.is_empty(), right.is_empty()) {
159            (true, true) => {
160                ranges.remove(index);
161            }
162            (false, true) => {
163                ranges[index] = left;
164                index += 1;
165            }
166            (true, false) => {
167                ranges[index] = right;
168                index += 1;
169            }
170            (false, false) => {
171                ranges[index] = left;
172                ranges.insert(index + 1, right);
173                index += 2;
174            }
175        }
176    }
177}
178
179/// Add `new` to `ranges`, fusing whatever it overlaps or touches so the list
180/// stays disjoint and sorted.
181fn add(ranges: &mut Ranges, mut new: Range<u64>) {
182    ranges.retain(|held| {
183        // Touching counts: [0, 10) and [10, 20) fuse into [0, 20).
184        let fuses = held.start <= new.end && new.start <= held.end;
185        if fuses {
186            new.start = new.start.min(held.start);
187            new.end = new.end.max(held.end);
188        }
189        !fuses
190    });
191    let at = ranges
192        .iter()
193        .position(|held| new.end < held.start)
194        .unwrap_or(ranges.len());
195    ranges.insert(at, new);
196}
197
198#[cfg(test)]
199mod tests {
200    use super::*;
201    use crate::server::ServerError;
202    use alloc::string::ToString;
203
204    fn error(reason: &str) -> ServerError {
205        ServerError::Generic {
206            reason: reason.to_string(),
207            backtrace: Default::default(),
208        }
209    }
210
211    /// A wider [`FailureId`] costs the carrier nothing: the id is followed by
212    /// padding out to the ranges' alignment either way.
213    ///
214    /// The id has to be wide, because ids are never reused and one is minted
215    /// per write scope — and this is what makes that free. A carrier that grew
216    /// would be a real cost on every slice in the pools, so the claim is
217    /// pinned rather than asserted in a doc.
218    #[test]
219    fn a_failure_id_is_free_in_the_carrier() {
220        struct Narrow {
221            _failure: core::num::NonZeroU32,
222            _ranges: Ranges,
223        }
224
225        assert_eq!(
226            core::mem::size_of::<Tainted>(),
227            core::mem::size_of::<Narrow>(),
228            "a 64-bit failure id must fit in the padding a 32-bit one leaves"
229        );
230    }
231
232    /// The precision this type exists for: a write covering part of a buffer
233    /// releases the claim on those bytes and only those bytes.
234    #[test]
235    fn a_partial_write_releases_only_the_bytes_it_covers() {
236        let mut graph = ErrorGraph::default();
237        let mut taint = Taint::default();
238        let failure = graph.insert(error("launch"));
239
240        taint.taint(0..100, failure, &mut graph);
241        taint.written(40..60, &mut graph);
242
243        assert_eq!(taint.failure(&(0..40)), Some(failure));
244        assert_eq!(taint.failure(&(40..60)), None, "these bytes were written");
245        assert_eq!(taint.failure(&(60..100)), Some(failure));
246        assert!(!graph.is_empty(), "the split claim still pins the node");
247
248        taint.written(0..40, &mut graph);
249        taint.written(60..100, &mut graph);
250        assert!(taint.is_clean());
251        assert!(graph.is_empty(), "the last byte released the node");
252    }
253
254    /// Two failures claiming disjoint regions coexist, and a read of each
255    /// region names its own.
256    #[test]
257    fn disjoint_claims_keep_their_own_failures() {
258        let mut graph = ErrorGraph::default();
259        let mut taint = Taint::default();
260        let first = graph.insert(error("first"));
261        let second = graph.insert(error("second"));
262
263        taint.taint(0..50, first, &mut graph);
264        taint.taint(50..100, second, &mut graph);
265
266        assert_eq!(taint.failure(&(10..20)), Some(first));
267        assert_eq!(taint.failure(&(60..70)), Some(second));
268        assert_eq!(graph.len(), 2);
269
270        taint.written(0..50, &mut graph);
271        assert!(graph.error(first).is_none(), "first has no carrier left");
272        assert_eq!(taint.failure(&(60..70)), Some(second));
273    }
274
275    /// A new failure claiming bytes an old one held takes them over — set,
276    /// never add — and the old node goes exactly when its last byte does.
277    #[test]
278    fn a_new_failure_takes_the_bytes_it_claims() {
279        let mut graph = ErrorGraph::default();
280        let mut taint = Taint::default();
281        let old = graph.insert(error("old"));
282        let new = graph.insert(error("new"));
283
284        taint.taint(0..100, old, &mut graph);
285        taint.taint(25..75, new, &mut graph);
286
287        assert_eq!(taint.failure(&(0..25)), Some(old));
288        assert_eq!(taint.failure(&(30..40)), Some(new));
289        assert_eq!(taint.failure(&(75..100)), Some(old));
290
291        taint.taint(0..100, new, &mut graph);
292        assert!(graph.error(old).is_none(), "old claims nothing any more");
293        assert_eq!(taint.failure(&(0..100)), Some(new));
294    }
295
296    /// The loop trap, range-flavored: failing the same way on the same bytes
297    /// every iteration keeps one entry, one tag, one node.
298    #[test]
299    fn retainting_the_same_bytes_counts_once() {
300        let mut graph = ErrorGraph::default();
301        let mut taint = Taint::default();
302        let failure = graph.insert(error("launch"));
303
304        for _ in 0..3 {
305            taint.taint(0..100, failure, &mut graph);
306        }
307        assert_eq!(graph.len(), 1);
308
309        taint.written(0..100, &mut graph);
310        assert!(graph.is_empty(), "one entry, one tag, one untag");
311    }
312
313    /// Adjacent claims of one failure fuse rather than accumulate, so a
314    /// kernel failing tile by tile does not grow the list without bound.
315    #[test]
316    fn adjacent_claims_of_one_failure_fuse() {
317        let mut graph = ErrorGraph::default();
318        let mut taint = Taint::default();
319        let failure = graph.insert(error("launch"));
320
321        taint.taint(0..10, failure, &mut graph);
322        taint.taint(20..30, failure, &mut graph);
323        taint.taint(10..20, failure, &mut graph);
324
325        assert_eq!(taint.entries.len(), 1);
326        assert_eq!(taint.entries[0].ranges.len(), 1);
327        assert_eq!(taint.entries[0].ranges[0], 0..30);
328    }
329
330    /// Clearing releases every claim at once, for the slice that stops
331    /// existing.
332    #[test]
333    fn clearing_releases_every_claim() {
334        let mut graph = ErrorGraph::default();
335        let mut taint = Taint::default();
336        let first = graph.insert(error("first"));
337        let second = graph.insert(error("second"));
338
339        taint.taint(0..50, first, &mut graph);
340        taint.taint(50..100, second, &mut graph);
341        taint.clear(&mut graph);
342
343        assert!(taint.is_clean());
344        assert!(graph.is_empty());
345    }
346
347    /// An empty range claims nothing and trips nothing: a zero-sized binding
348    /// has no bytes to distrust.
349    #[test]
350    fn an_empty_range_claims_nothing() {
351        let mut graph = ErrorGraph::default();
352        let mut taint = Taint::default();
353        let failure = graph.insert(error("launch"));
354
355        taint.taint(10..10, failure, &mut graph);
356        assert!(taint.is_clean());
357
358        taint.taint(0..100, failure, &mut graph);
359        assert_eq!(taint.failure(&(50..50)), None);
360    }
361}