hyperactor/value_mesh/value_overlay.rs
1/*
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 * All rights reserved.
4 *
5 * This source code is licensed under the BSD-style license found in the
6 * LICENSE file in the root directory of this source tree.
7 */
8
9use std::fmt;
10use std::ops::Range;
11
12use serde::Deserialize;
13use serde::Serialize;
14use typeuri::Named;
15
16/// Builder error for overlays (structure only; region bounds are
17/// checked at merge time).
18///
19/// Note: serialization assumes identical pointer width between sender
20/// and receiver, as `Range<usize>` is not portable across
21/// architectures. TODO: introduce a wire‐stable run type.
22#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
23pub enum BuildError {
24 /// A run with an empty range (`start == end`) was provided.
25 EmptyRange,
26
27 /// Two runs overlap or are unsorted: `prev.end > next.start`. The
28 /// offending ranges are returned for debugging.
29 OverlappingRanges {
30 /// Previous run seen while validating normalized order.
31 prev: Range<usize>,
32 /// Current run that overlaps or sorts before `prev`.
33 next: Range<usize>,
34 },
35
36 /// A run exceeds the region bounds when applying an overlay
37 /// merge.
38 OutOfBounds {
39 /// Range that exceeded the target region.
40 range: Range<usize>,
41 /// Number of ranks in the target region.
42 region_len: usize,
43 },
44}
45
46impl fmt::Display for BuildError {
47 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
48 match self {
49 BuildError::EmptyRange => {
50 write!(f, "a run with an empty range (start == end) was provided")
51 }
52 BuildError::OverlappingRanges { prev, next } => write!(
53 f,
54 "overlapping or unsorted runs: prev={:?}, next={:?}",
55 prev, next
56 ),
57 BuildError::OutOfBounds { range, region_len } => write!(
58 f,
59 "range {:?} exceeds region bounds (len={})",
60 range, region_len
61 ),
62 }
63 }
64}
65
66impl std::error::Error for BuildError {}
67
68/// A sparse overlay of rank ranges and values, used to assemble or
69/// patch a [`ValueMesh`] without materializing per-rank data.
70///
71/// Unlike `ValueMesh`, which always represents a complete, gap-free
72/// mapping over a [`Region`], a `ValueOverlay` is intentionally
73/// partial: it may describe only the ranks that have changed. This
74/// allows callers to build and merge small, incremental updates
75/// efficiently, while preserving the `ValueMesh` invariants after
76/// merge.
77///
78/// Invariants (VO-1):
79/// - **VO-1 (sorted-runs):** Runs are sorted by `(start, end)`.
80/// - Runs are non-empty and non-overlapping.
81/// - Adjacent runs with equal values are coalesced.
82/// - Region bounds are validated when the overlay is merged, not on
83/// insert.
84///
85/// Note: serialization assumes identical pointer width between sender
86/// and receiver, as `Range<usize>` is not portable across
87/// architectures. TODO: introduce a wire‐stable run type.
88#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Named, Default)]
89pub struct ValueOverlay<T> {
90 runs: Vec<(Range<usize>, T)>,
91}
92
93impl<T> ValueOverlay<T> {
94 /// Creates an empty overlay.
95 pub fn new() -> Self {
96 Self { runs: Vec::new() }
97 }
98
99 /// Returns an iterator over the internal runs.
100 pub fn runs(&self) -> impl Iterator<Item = &(Range<usize>, T)> {
101 self.runs.iter()
102 }
103
104 /// Consumes the overlay and returns the internal runs.
105 pub fn into_runs(self) -> Vec<(Range<usize>, T)> {
106 self.runs
107 }
108
109 /// Current number of runs.
110 pub fn len(&self) -> usize {
111 self.runs.len()
112 }
113
114 /// Returns `true` if the overlay contains no runs. This indicates
115 /// that no ranges have been added — i.e., the overlay represents
116 /// an empty or no-op patch.
117 pub fn is_empty(&self) -> bool {
118 self.runs.is_empty()
119 }
120}
121
122impl<T: PartialEq> ValueOverlay<T> {
123 /// Adds a `(range, value)` run while maintaining the overlay
124 /// invariants.
125 ///
126 /// Fast path:
127 /// - If `range` is **after** the last run (`last.end <=
128 /// range.start`), this is an O(1) append. If it **touches** the
129 /// last run and `value` is equal, the two runs are **coalesced**
130 /// by extending `end`.
131 ///
132 /// Slow path:
133 /// - If the input is **unsorted** or **overlaps** the last run,
134 /// the method falls back to a full **normalize** (sort +
135 /// coalesce + overlap check), making this call **O(n log n)**
136 /// in the number of runs.
137 ///
138 /// Errors:
139 /// - Returns `BuildError::EmptyRange` if `range.is_empty()`.
140 ///
141 /// Notes & guidance:
142 /// - Use this for **already-sorted, non-overlapping** appends to
143 /// get the cheap fast path.
144 /// - For **bulk or unsorted** inserts, prefer
145 /// `ValueOverlay::try_from_runs` (collect → sort → coalesce) to
146 /// avoid repeated re-normalization.
147 /// - Adjacent equal-value runs are coalesced automatically.
148 ///
149 /// # Examples
150 /// ```ignore
151 /// let mut ov = ValueOverlay::new();
152 /// ov.push_run(0..3, 1).unwrap(); // append
153 /// ov.push_run(3..5, 1).unwrap(); // coalesces to (0..5, 1)
154 ///
155 /// // Unsorted input triggers normalize (O(n log n)):
156 /// ov.push_run(1..2, 2).unwrap(); // re-sorts, checks overlaps, coalesces
157 /// ```
158 pub fn push_run(&mut self, range: Range<usize>, value: T) -> Result<(), BuildError> {
159 // Reject empty ranges.
160 if range.is_empty() {
161 return Err(BuildError::EmptyRange);
162 }
163
164 // Look at the last run.
165 match self.runs.last_mut() {
166 // The common case is appending in sorted order. Fast-path
167 // append if new run is after the last and
168 // non-overlapping.
169 Some((last_r, last_v)) if last_r.end <= range.start => {
170 if last_r.end == range.start && *last_v == value {
171 // Coalesce equal-adjacent.
172 last_r.end = range.end;
173 return Ok(());
174 }
175 self.runs.push((range, value));
176 Ok(())
177 }
178 // The overlay was previously empty or, the caller
179 // inserted out of order (unsorted input).
180 _ => {
181 // Slow path. Re-sort, merge and validate the full
182 // runs vector.
183 self.runs.push((range, value));
184 Self::normalize(&mut self.runs)
185 }
186 }
187 }
188
189 /// Sorts, checks for overlaps, and coalesces equal-adjacent runs
190 /// in-place.
191 fn normalize(v: &mut Vec<(Range<usize>, T)>) -> Result<(), BuildError> {
192 // Early exit for empty overlays.
193 if v.is_empty() {
194 return Ok(());
195 }
196
197 // After this, ever later range has start >= prev.start. If
198 // any later start < prev.end it's an overlap.
199 v.sort_by_key(|(r, _)| (r.start, r.end));
200
201 // Build a fresh vector to collect cleaned using drain(..) on
202 // the input avoiding clone().
203 let mut out: Vec<(Range<usize>, T)> = Vec::with_capacity(v.len());
204 for (r, val) in v.drain(..) {
205 if let Some((prev_r, prev_v)) = out.last_mut() {
206 // If the next run's start is before the previous run's
207 // end we have an overlapping interval.
208 if r.start < prev_r.end {
209 return Err(BuildError::OverlappingRanges {
210 prev: prev_r.clone(),
211 next: r,
212 });
213 }
214 // If the previous run touches the new run and has the
215 // same value, merge them by extending the end
216 // boundary.
217 if prev_r.end == r.start && *prev_v == val {
218 // Coalesce equal-adjacent.
219 prev_r.end = r.end;
220 continue;
221 }
222 }
223 // Otherwise, push as a new independent run.
224 out.push((r, val));
225 }
226
227 // Replace the old vector.
228 *v = out;
229
230 // VO-1: runs are sorted, non-overlapping and coalesced.
231 Ok(())
232 }
233
234 /// Builds an overlay from arbitrary runs, validating structure
235 /// and coalescing equal-adjacent.
236 pub fn try_from_runs<I>(runs: I) -> Result<Self, BuildError>
237 where
238 I: IntoIterator<Item = (Range<usize>, T)>,
239 {
240 // We need a modifiable buffer to sort and normalize so we
241 // eagerly collect the iterator.
242 let mut v: Vec<(Range<usize>, T)> = runs.into_iter().collect();
243
244 // Reject empties up-front. Empty intervals are structurally
245 // invalid for an overlay. Fail fast.
246 for (r, _) in &v {
247 if r.is_empty() {
248 return Err(BuildError::EmptyRange);
249 }
250 }
251
252 // Sort by (start, end).
253 v.sort_by_key(|(r, _)| (r.start, r.end));
254
255 // Normalize (validate + coalesce).
256 Self::normalize(&mut v)?;
257
258 // VO-1: runs are sorted, non-overlapping and coalesced.
259 Ok(Self { runs: v })
260 }
261}
262
263#[cfg(test)]
264mod tests {
265
266 use super::*;
267
268 #[test]
269 fn push_run_appends_and_coalesces() {
270 let mut ov = ValueOverlay::new();
271
272 // First insert.
273 ov.push_run(0..3, 1).unwrap();
274 assert_eq!(ov.runs, vec![(0..3, 1)]);
275
276 // Non-overlapping append.
277 ov.push_run(5..7, 2).unwrap();
278 assert_eq!(ov.runs, vec![(0..3, 1), (5..7, 2)]);
279
280 // Coalesce equal-adjacent (touching with same value).
281 ov.push_run(7..10, 2).unwrap();
282 assert_eq!(ov.runs, vec![(0..3, 1), (5..10, 2)]);
283 }
284
285 #[test]
286 fn push_run_detects_overlap() {
287 let mut ov = ValueOverlay::new();
288 ov.push_run(0..3, 1).unwrap();
289
290 // Overlaps 2..4 with existing 0..3.
291 let err = ov.push_run(2..4, 9).unwrap_err();
292 assert!(matches!(err, BuildError::OverlappingRanges { .. }));
293 }
294
295 #[test]
296 fn push_run_handles_unsorted_inserts() {
297 let mut ov = ValueOverlay::new();
298 // Insert out of order; normalize should sort and coalesce.
299 ov.push_run(10..12, 3).unwrap();
300 ov.push_run(5..8, 2).unwrap(); // Unsorted relative to last.
301 ov.push_run(8..10, 2).unwrap(); // Coalesce with previous.
302
303 assert_eq!(ov.runs, vec![(5..10, 2), (10..12, 3)]);
304 }
305
306 #[test]
307 fn try_from_runs_builds_and_coalesces() {
308 use super::ValueOverlay;
309
310 // Unsorted, with adjacent equal-value ranges that should
311 // coalesce.
312 let ov = ValueOverlay::try_from_runs(vec![(8..10, 2), (5..8, 2), (12..14, 3)]).unwrap();
313
314 assert_eq!(ov.runs, vec![(5..10, 2), (12..14, 3)]);
315 }
316
317 #[test]
318 fn try_from_runs_rejects_overlap_and_empty() {
319 // Overlap should error.
320 let err = ValueOverlay::try_from_runs(vec![(0..3, 1), (2..5, 2)]).unwrap_err();
321 assert!(matches!(err, BuildError::OverlappingRanges { .. }));
322
323 // Empty range should error.
324 let err = ValueOverlay::try_from_runs(vec![(0..0, 1)]).unwrap_err();
325 assert!(matches!(err, BuildError::EmptyRange));
326 }
327
328 #[test]
329 fn is_empty_reflects_state() {
330 let mut ov = ValueOverlay::<i32>::new();
331 assert!(ov.is_empty());
332
333 ov.push_run(0..1, 7).unwrap();
334 assert!(!ov.is_empty());
335 }
336
337 #[test]
338 fn normalize_sorts_coalesces_and_detects_overlap() {
339 // 1) Sort + coalesce equal-adjacent.
340 let mut v = vec![(5..7, 2), (3..5, 2), (7..9, 2)]; // unsorted, all value=2
341 ValueOverlay::<i32>::normalize(&mut v).unwrap();
342 assert_eq!(v, vec![(3..9, 2)]);
343
344 // 2) Overlap triggers error.
345 let mut v = vec![(3..6, 1), (5..8, 2)];
346 let err = ValueOverlay::<i32>::normalize(&mut v).unwrap_err();
347 assert!(matches!(err, BuildError::OverlappingRanges { .. }));
348 }
349}