Skip to content

Commit 74d3edf

Browse files
: v1: value_mesh: new type ValueOverlay (#1476)
Summary: introduces `ValueOverlay`, a sparse representation of `(Range<usize>, T) `runs for assembling or patching `ValueMesh` instances without materializing per-rank data. includes structural validation (`EmptyRange`, `OverlappingRanges`), coalescing of equal-adjacent runs, and construction helpers (`push_run`, `try_from_runs`, `normalize`). adds unit tests covering append, coalescing, overlap detection, unsorted inserts, and empty overlays. no behavior change to `ValueMesh`; merge support will follow in the next diff. Differential Revision: D84266477
1 parent dd1114d commit 74d3edf

File tree

2 files changed

+274
-2
lines changed

2 files changed

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

0 commit comments

Comments
 (0)