hyperactor_mesh/v1/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 hyperactor::Named;
13use serde::Deserialize;
14use serde::Serialize;
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:
75/// - 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 /// Current number of runs.
101 pub fn len(&self) -> usize {
102 self.runs.len()
103 }
104
105 /// Returns `true` if the overlay contains no runs. This indicates
106 /// that no ranges have been added — i.e., the overlay represents
107 /// an empty or no-op patch.
108 pub fn is_empty(&self) -> bool {
109 self.runs.is_empty()
110 }
111}
112
113impl<T: PartialEq> ValueOverlay<T> {
114 /// Adds a `(range, value)` run while maintaining the overlay
115 /// invariants.
116 ///
117 /// Fast path:
118 /// - If `range` is **after** the last run (`last.end <=
119 /// range.start`), this is an O(1) append. If it **touches** the
120 /// last run and `value` is equal, the two runs are **coalesced**
121 /// by extending `end`.
122 ///
123 /// Slow path:
124 /// - If the input is **unsorted** or **overlaps** the last run,
125 /// the method falls back to a full **normalize** (sort +
126 /// coalesce + overlap check), making this call **O(n log n)**
127 /// in the number of runs.
128 ///
129 /// Errors:
130 /// - Returns `BuildError::EmptyRange` if `range.is_empty()`.
131 ///
132 /// Notes & guidance:
133 /// - Use this for **already-sorted, non-overlapping** appends to
134 /// get the cheap fast path.
135 /// - For **bulk or unsorted** inserts, prefer
136 /// `ValueOverlay::try_from_runs` (collect → sort → coalesce) to
137 /// avoid repeated re-normalization.
138 /// - Adjacent equal-value runs are coalesced automatically.
139 ///
140 /// # Examples
141 /// ```ignore
142 /// let mut ov = ValueOverlay::new();
143 /// ov.push_run(0..3, 1).unwrap(); // append
144 /// ov.push_run(3..5, 1).unwrap(); // coalesces to (0..5, 1)
145 ///
146 /// // Unsorted input triggers normalize (O(n log n)):
147 /// ov.push_run(1..2, 2).unwrap(); // re-sorts, checks overlaps, coalesces
148 /// ```
149 pub fn push_run(&mut self, range: Range<usize>, value: T) -> Result<(), BuildError> {
150 // Reject empty ranges.
151 if range.is_empty() {
152 return Err(BuildError::EmptyRange);
153 }
154
155 // Look at the last run.
156 match self.runs.last_mut() {
157 // The common case is appending in sorted order. Fast-path
158 // append if new run is after the last and
159 // non-overlapping.
160 Some((last_r, last_v)) if last_r.end <= range.start => {
161 if last_r.end == range.start && *last_v == value {
162 // Coalesce equal-adjacent.
163 last_r.end = range.end;
164 return Ok(());
165 }
166 self.runs.push((range, value));
167 Ok(())
168 }
169 // The overlay was previously empty or, the caller
170 // inserted out of order (unsorted input).
171 _ => {
172 // Slow path. Re-sort, merge and validate the full
173 // runs vector.
174 self.runs.push((range, value));
175 Self::normalize(&mut self.runs)
176 }
177 }
178 }
179
180 /// Sorts, checks for overlaps, and coalesces equal-adjacent runs
181 /// in-place.
182 fn normalize(v: &mut Vec<(Range<usize>, T)>) -> Result<(), BuildError> {
183 // Early exit for empty overlays.
184 if v.is_empty() {
185 return Ok(());
186 }
187
188 // After this, ever later range has start >= prev.start. If
189 // any later start < prev.end it's an overlap.
190 v.sort_by_key(|(r, _)| (r.start, r.end));
191
192 // Build a fresh vector to collect cleaned using drain(..) on
193 // the input avoiding clone().
194 let mut out: Vec<(Range<usize>, T)> = Vec::with_capacity(v.len());
195 for (r, val) in v.drain(..) {
196 if let Some((prev_r, prev_v)) = out.last_mut() {
197 // If the next run's start is before the previous run's
198 // end we have an overlapping interval.
199 if r.start < prev_r.end {
200 return Err(BuildError::OverlappingRanges {
201 prev: prev_r.clone(),
202 next: r,
203 });
204 }
205 // If the previous run touches the new run and has the
206 // same value, merge them by extending the end
207 // boundary.
208 if prev_r.end == r.start && *prev_v == val {
209 // Coalesce equal-adjacent.
210 prev_r.end = r.end;
211 continue;
212 }
213 }
214 // Otherwise, push as a new independent run.
215 out.push((r, val));
216 }
217
218 // Replace the old vector.
219 *v = out;
220
221 // Invariant: Runs is sorted, non-overlapping and coalesced.
222 Ok(())
223 }
224
225 /// Builds an overlay from arbitrary runs, validating structure
226 /// and coalescing equal-adjacent.
227 pub fn try_from_runs<I>(runs: I) -> Result<Self, BuildError>
228 where
229 I: IntoIterator<Item = (Range<usize>, T)>,
230 {
231 // We need a modifiable buffer to sort and normalize so we
232 // eagerly collect the iterator.
233 let mut v: Vec<(Range<usize>, T)> = runs.into_iter().collect();
234
235 // Reject empties up-front. Empty intervals are structurally
236 // invalid for an overlay. Fail fast.
237 for (r, _) in &v {
238 if r.is_empty() {
239 return Err(BuildError::EmptyRange);
240 }
241 }
242
243 // Sort by (start, end).
244 v.sort_by_key(|(r, _)| (r.start, r.end));
245
246 // Normalize (validate + coalesce).
247 Self::normalize(&mut v)?;
248
249 // Invariant: Runs is sorted, non-overlapping and coalesced.
250 Ok(Self { runs: v })
251 }
252}
253
254#[cfg(test)]
255mod tests {
256
257 use super::*;
258
259 #[test]
260 fn push_run_appends_and_coalesces() {
261 let mut ov = ValueOverlay::new();
262
263 // First insert.
264 ov.push_run(0..3, 1).unwrap();
265 assert_eq!(ov.runs, vec![(0..3, 1)]);
266
267 // Non-overlapping append.
268 ov.push_run(5..7, 2).unwrap();
269 assert_eq!(ov.runs, vec![(0..3, 1), (5..7, 2)]);
270
271 // Coalesce equal-adjacent (touching with same value).
272 ov.push_run(7..10, 2).unwrap();
273 assert_eq!(ov.runs, vec![(0..3, 1), (5..10, 2)]);
274 }
275
276 #[test]
277 fn push_run_detects_overlap() {
278 let mut ov = ValueOverlay::new();
279 ov.push_run(0..3, 1).unwrap();
280
281 // Overlaps 2..4 with existing 0..3.
282 let err = ov.push_run(2..4, 9).unwrap_err();
283 assert!(matches!(err, BuildError::OverlappingRanges { .. }));
284 }
285
286 #[test]
287 fn push_run_handles_unsorted_inserts() {
288 let mut ov = ValueOverlay::new();
289 // Insert out of order; normalize should sort and coalesce.
290 ov.push_run(10..12, 3).unwrap();
291 ov.push_run(5..8, 2).unwrap(); // Unsorted relative to last.
292 ov.push_run(8..10, 2).unwrap(); // Coalesce with previous.
293
294 assert_eq!(ov.runs, vec![(5..10, 2), (10..12, 3)]);
295 }
296
297 #[test]
298 fn try_from_runs_builds_and_coalesces() {
299 use super::ValueOverlay;
300
301 // Unsorted, with adjacent equal-value ranges that should
302 // coalesce.
303 let ov = ValueOverlay::try_from_runs(vec![(8..10, 2), (5..8, 2), (12..14, 3)]).unwrap();
304
305 assert_eq!(ov.runs, vec![(5..10, 2), (12..14, 3)]);
306 }
307
308 #[test]
309 fn try_from_runs_rejects_overlap_and_empty() {
310 // Overlap should error.
311 let err = ValueOverlay::try_from_runs(vec![(0..3, 1), (2..5, 2)]).unwrap_err();
312 assert!(matches!(err, BuildError::OverlappingRanges { .. }));
313
314 // Empty range should error.
315 let err = ValueOverlay::try_from_runs(vec![(0..0, 1)]).unwrap_err();
316 assert!(matches!(err, BuildError::EmptyRange));
317 }
318
319 #[test]
320 fn is_empty_reflects_state() {
321 let mut ov = ValueOverlay::<i32>::new();
322 assert!(ov.is_empty());
323
324 ov.push_run(0..1, 7).unwrap();
325 assert!(!ov.is_empty());
326 }
327
328 #[test]
329 fn normalize_sorts_coalesces_and_detects_overlap() {
330 // 1) Sort + coalesce equal-adjacent.
331 let mut v = vec![(5..7, 2), (3..5, 2), (7..9, 2)]; // unsorted, all value=2
332 ValueOverlay::<i32>::normalize(&mut v).unwrap();
333 assert_eq!(v, vec![(3..9, 2)]);
334
335 // 2) Overlap triggers error.
336 let mut v = vec![(3..6, 1), (5..8, 2)];
337 let err = ValueOverlay::<i32>::normalize(&mut v).unwrap_err();
338 assert!(matches!(err, BuildError::OverlappingRanges { .. }));
339 }
340}