Skip to content

Commit ca59fc0

Browse files
: v1: value_mesh: new type ValueOverlay
Differential Revision: D84266477
1 parent 72ce048 commit ca59fc0

File tree

2 files changed

+270
-2
lines changed

2 files changed

+270
-2
lines changed

hyperactor_mesh/src/v1/value_mesh.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ use ndslice::view::Region;
2020
use serde::Deserialize;
2121
use serde::Serialize;
2222

23+
mod value_overlay;
24+
pub use value_overlay::BuildError;
25+
pub use value_overlay::ValueOverlay;
26+
2327
/// A mesh of values, one per rank in `region`.
2428
///
2529
/// The internal representation (`rep`) may be dense or compressed,
@@ -92,7 +96,7 @@ impl TryFrom<Run> for (Range<usize>, u32) {
9296
///
9397
/// Performs checked conversion of the 64-bit wire fields back
9498
/// into `usize` indices, returning an error if either bound
95-
/// exceeds the platforms addressable range. This ensures safe
99+
/// exceeds the platform's addressable range. This ensures safe
96100
/// round-tripping between the serialized wire format and native
97101
/// representation.
98102
fn try_from(r: Run) -> Result<Self, Self::Error> {
@@ -331,7 +335,7 @@ impl<T: Clone + PartialEq> ValueMesh<T> {
331335
/// store it efficiently.
332336
///
333337
/// # Parameters
334-
/// - `region`: The logical region describing the meshs shape and
338+
/// - `region`: The logical region describing the mesh's shape and
335339
/// rank order.
336340
/// - `values`: A dense vector of values, one per rank in
337341
/// `region`.
Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
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+
use std::ops::Range;
9+
10+
/// Builder error for overlays (structure only; region bounds are
11+
/// checked at merge time).
12+
#[derive(Debug, Clone, PartialEq, Eq)]
13+
pub enum BuildError {
14+
/// A run with an empty range (`start == end`) was provided.
15+
EmptyRange,
16+
17+
/// Two runs overlap or are unsorted: `prev.end > next.start`. The
18+
/// offending ranges are returned for debugging.
19+
OverlappingRanges { prev: Range<usize>, next: Range<usize> },
20+
}
21+
22+
/// A sparse overlay of rank ranges and values, used to assemble or
23+
/// patch a [`ValueMesh`] without materializing per-rank data.
24+
///
25+
/// Unlike `ValueMesh`, which always represents a complete, gap-free
26+
/// mapping over a [`Region`], a `ValueOverlay` is intentionally
27+
/// partial: it may describe only the ranks that have changed. This
28+
/// allows callers to build and merge small, incremental updates
29+
/// efficiently, while preserving the `ValueMesh` invariants after
30+
/// merge.
31+
///
32+
/// Invariants:
33+
/// - Runs are sorted by `(start, end)`.
34+
/// - Runs are non-empty and non-overlapping.
35+
/// - Adjacent runs with equal values are coalesced.
36+
/// - Region bounds are validated when the overlay is merged, not on
37+
/// insert.
38+
#[derive(Debug, Clone, PartialEq, Eq)]
39+
pub struct ValueOverlay<T> {
40+
runs: Vec<(Range<usize>, T)>,
41+
}
42+
43+
impl<T> ValueOverlay<T> {
44+
/// Creates an empty overlay.
45+
pub fn new() -> Self {
46+
Self { runs: Vec::new() }
47+
}
48+
49+
/// Returns an iterator over the internal runs.
50+
pub fn runs(&self) -> impl Iterator<Item = &(Range<usize>, T)> {
51+
self.runs.iter()
52+
}
53+
54+
/// Current number of runs.
55+
pub fn len(&self) -> usize {
56+
self.runs.len()
57+
}
58+
59+
/// Returns `true` if the overlay contains no runs. This indicates
60+
/// that no ranges have been added — i.e., the overlay represents
61+
/// an empty or no-op patch.
62+
pub fn is_empty(&self) -> bool {
63+
self.runs.is_empty()
64+
}
65+
}
66+
67+
impl<T: PartialEq> ValueOverlay<T> {
68+
/// Adds a run, maintaining invariants (sorted, non-overlapping,
69+
/// coalesced).
70+
///
71+
/// This does not check region bounds; that's validated by
72+
/// `merge_from_overlay`.
73+
pub fn push_run(&mut self, range: Range<usize>, value: T) -> Result<(), BuildError> {
74+
// Reject empty ranges.
75+
if range.is_empty() {
76+
return Err(BuildError::EmptyRange);
77+
}
78+
79+
// Look at the last run.
80+
match self.runs.last_mut() {
81+
// The common case is appending in sorted order. Fast-path
82+
// append if new run is after the last and
83+
// non-overlapping.
84+
Some((last_r, last_v)) if last_r.end <= range.start => {
85+
if last_r.end == range.start && *last_v == value {
86+
// Coalesce equal-adjacent.
87+
last_r.end = range.end;
88+
return Ok(());
89+
}
90+
self.runs.push((range, value));
91+
Ok(())
92+
}
93+
// The overlay was previously empty or, the caller
94+
// inserted out of order (unsorted input).
95+
_ => {
96+
// Slow path. Re-sort, merge and validate the full
97+
// runs vector.
98+
self.runs.push((range, value));
99+
Self::normalize(&mut self.runs)
100+
}
101+
}
102+
}
103+
104+
/// Sorts, checks for overlaps, and coalesces equal-adjacent runs
105+
/// in-place.
106+
fn normalize(v: &mut Vec<(Range<usize>, T)>) -> Result<(), BuildError> {
107+
// Early exit for empty overlays.
108+
if v.is_empty() {
109+
return Ok(());
110+
}
111+
112+
// After this, ever later range has start >= prev.start. If
113+
// any later start < prev.end it's an overlap.
114+
v.sort_by_key(|(r, _)| (r.start, r.end));
115+
116+
// Build a fresh vector to collect cleaned using drain(..) on
117+
// the input avoiding clone().
118+
let mut out: Vec<(Range<usize>, T)> = Vec::with_capacity(v.len());
119+
for (r, val) in v.drain(..) {
120+
if let Some((prev_r, prev_v)) = out.last_mut() {
121+
// If the next run's start is before the previous run's
122+
// end we have an overlapping interval.
123+
if r.start < prev_r.end {
124+
return Err(BuildError::OverlappingRanges {
125+
prev: prev_r.clone(),
126+
next: r,
127+
});
128+
}
129+
// If the previous run touches the new run and has the
130+
// same value, merge them by extending the end
131+
// boundary.
132+
if prev_r.end == r.start && *prev_v == val {
133+
// Coalesce equal-adjacent.
134+
prev_r.end = r.end;
135+
continue;
136+
}
137+
}
138+
// Otherwise, push as a new independent run.
139+
out.push((r, val));
140+
}
141+
142+
// Replace the old vector.
143+
*v = out;
144+
145+
// Invariant: Runs is sorted, non-overlapping and coalesced.
146+
Ok(())
147+
}
148+
149+
/// Builds an overlay from arbitrary runs, validating structure
150+
/// and coalescing equal-adjacent.
151+
pub fn try_from_runs<I>(runs: I) -> Result<Self, BuildError>
152+
where
153+
I: IntoIterator<Item = (Range<usize>, T)>,
154+
{
155+
// We need a modifiable buffer to sort and normalize so we
156+
// eagerly collect the iterator.
157+
let mut v: Vec<(Range<usize>, T)> = runs.into_iter().collect();
158+
159+
// Reject empties up-front. Empty intervals are structurally
160+
// invalid for an overlay. Fail fast.
161+
for (r, _) in &v {
162+
if r.is_empty() {
163+
return Err(BuildError::EmptyRange);
164+
}
165+
}
166+
167+
// Sort by (start, end).
168+
v.sort_by_key(|(r, _)| (r.start, r.end));
169+
170+
// Normalize (validate + coalesce).
171+
Self::normalize(&mut v)?;
172+
173+
// Invariant: Runs is sorted, non-overlapping and coalesced.
174+
Ok(Self { runs: v })
175+
}
176+
}
177+
178+
#[cfg(test)]
179+
mod tests {
180+
181+
use super::*;
182+
183+
#[test]
184+
fn push_run_appends_and_coalesces() {
185+
let mut ov = ValueOverlay::new();
186+
187+
// First insert.
188+
ov.push_run(0..3, 1).unwrap();
189+
assert_eq!(ov.runs, vec![(0..3, 1)]);
190+
191+
// Non-overlapping append.
192+
ov.push_run(5..7, 2).unwrap();
193+
assert_eq!(ov.runs, vec![(0..3, 1), (5..7, 2)]);
194+
195+
// Coalesce equal-adjacent (touching with same value).
196+
ov.push_run(7..10, 2).unwrap();
197+
assert_eq!(ov.runs, vec![(0..3, 1), (5..10, 2)]);
198+
}
199+
200+
#[test]
201+
fn push_run_detects_overlap() {
202+
let mut ov = ValueOverlay::new();
203+
ov.push_run(0..3, 1).unwrap();
204+
205+
// Overlaps 2..4 with existing 0..3.
206+
let err = ov.push_run(2..4, 9).unwrap_err();
207+
assert!(matches!(err, BuildError::OverlappingRanges { .. }));
208+
}
209+
210+
#[test]
211+
fn push_run_handles_unsorted_inserts() {
212+
let mut ov = ValueOverlay::new();
213+
// Insert out of order; normalize should sort and coalesce.
214+
ov.push_run(10..12, 3).unwrap();
215+
ov.push_run(5..8, 2).unwrap(); // Unsorted relative to last.
216+
ov.push_run(8..10, 2).unwrap(); // Coalesce with previous.
217+
218+
assert_eq!(ov.runs, vec![(5..10, 2),(10..12, 3)]);
219+
}
220+
221+
#[test]
222+
fn try_from_runs_builds_and_coalesces() {
223+
use super::ValueOverlay;
224+
225+
// Unsorted, with adjacent equal-value ranges that should
226+
// coalesce.
227+
let ov = ValueOverlay::try_from_runs(vec![(8..10, 2), (5..8, 2), (12..14, 3)]).unwrap();
228+
229+
assert_eq!(ov.runs, vec![(5..10, 2), (12..14, 3)]);
230+
}
231+
232+
#[test]
233+
fn try_from_runs_rejects_overlap_and_empty() {
234+
// Overlap should error.
235+
let err = ValueOverlay::try_from_runs(vec![(0..3, 1), (2..5, 2)]).unwrap_err();
236+
assert!(matches!(err, BuildError::OverlappingRanges { .. }));
237+
238+
// Empty range should error.
239+
let err = ValueOverlay::try_from_runs(vec![(0..0, 1)]).unwrap_err();
240+
assert!(matches!(err, BuildError::EmptyRange));
241+
}
242+
243+
#[test]
244+
fn is_empty_reflects_state() {
245+
let mut ov = ValueOverlay::<i32>::new();
246+
assert!(ov.is_empty());
247+
248+
ov.push_run(0..1, 7).unwrap();
249+
assert!(!ov.is_empty());
250+
}
251+
252+
#[test]
253+
fn normalize_sorts_coalesces_and_detects_overlap() {
254+
// 1) Sort + coalesce equal-adjacent.
255+
let mut v = vec![(5..7, 2), (3..5, 2), (7..9, 2)]; // unsorted, all value=2
256+
ValueOverlay::<i32>::normalize(&mut v).unwrap();
257+
assert_eq!(v, vec![(3..9, 2)]);
258+
259+
// 2) Overlap triggers error.
260+
let mut v = vec![(3..6, 1), (5..8, 2)];
261+
let err = ValueOverlay::<i32>::normalize(&mut v).unwrap_err();
262+
assert!(matches!(err, BuildError::OverlappingRanges { .. }));
263+
}
264+
}

0 commit comments

Comments
 (0)