Skip to content

Commit dd29b51

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 3d93593 commit dd29b51

File tree

2 files changed

+292
-2
lines changed

2 files changed

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

0 commit comments

Comments
 (0)