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