Skip to content

Commit a3837e8

Browse files
: v1: value_mesh: rle: merge_value_runs (meta-pytorch#1480)
Summary: this change factors out RLE and merge logic into a new `rle` module and ports the merge algorithm from `RankedValues::merge_from` into a general `merge_value_runs` that operates over normalized `(Range, T)` lists. the function implements last-writer-wins semantics and handles equal-value coalescing, disjoint runs, and arbitrary overlap spans. `ValueMesh` now uses these primitives for compression (`rle_from_dense`, `rle_from_value_runs`) and sparse overlay updates via `merge_from_overlay,` with `materialized_runs()` added for inspection. comprehensive tests cover dense compression, value-run merging, and overlay application, including multidimensional regions. Differential Revision: D84285073
1 parent 8435a4c commit a3837e8

File tree

2 files changed

+649
-62
lines changed

2 files changed

+649
-62
lines changed

hyperactor_mesh/src/v1/value_mesh.rs

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

23+
mod rle;
2324
mod value_overlay;
2425
pub use value_overlay::BuildError;
2526
pub use value_overlay::ValueOverlay;
@@ -748,6 +749,95 @@ impl<T: PartialEq + Clone> ValueMesh<T> {
748749
pub fn compress_adjacent_in_place(&mut self) {
749750
self.compress_adjacent_in_place_by(|a, b| a == b)
750751
}
752+
753+
/// Materializes this mesh into a vector of `(Range<usize>, T)`
754+
/// runs.
755+
///
756+
/// For a dense representation, this walks the value vector and
757+
/// groups adjacent equal values into contiguous runs. The result
758+
/// is equivalent to what would be stored in a compressed
759+
/// representation, but the mesh itself is not mutated or
760+
/// re-encoded. This is purely a read-only view.
761+
///
762+
/// For a compressed representation, the stored runs are simply
763+
/// cloned.
764+
///
765+
/// This method is intended for inspection, testing, and
766+
/// diff/merge operations that need a uniform view of value runs
767+
/// without changing the underlying representation.
768+
fn materialized_runs(&self) -> Vec<(Range<usize>, T)> {
769+
match &self.rep {
770+
Rep::Dense { values } => {
771+
// Coalesce adjacent equals into runs.
772+
let mut out = Vec::new();
773+
if values.is_empty() {
774+
return out;
775+
}
776+
let mut start = 0usize;
777+
for i in 1..values.len() {
778+
if values[i] != values[i - 1] {
779+
out.push((start..i, values[i - 1].clone()));
780+
start = i;
781+
}
782+
}
783+
out.push((start..values.len(), values.last().unwrap().clone()));
784+
out
785+
}
786+
Rep::Compressed { table, runs } => runs
787+
.iter()
788+
.map(|r| {
789+
let id = r.id as usize;
790+
((r.start as usize..r.end as usize), table[id].clone())
791+
})
792+
.collect(),
793+
}
794+
}
795+
}
796+
797+
impl<T: Clone + Eq> ValueMesh<T> {
798+
/// Merge a sparse overlay into this mesh.
799+
///
800+
/// Overlay segments are applied with **last-writer-wins**
801+
/// precedence on overlap (identical to `RankedValues::merge_from`
802+
/// behavior). The result is stored compressed.
803+
pub fn merge_from_overlay(
804+
&mut self,
805+
region: &ndslice::view::Region,
806+
overlay: &ValueOverlay<T>,
807+
) -> Result<(), BuildError> {
808+
let n = region.num_ranks();
809+
810+
// Bounds validation (structure already validated by
811+
// ValueOverlay).
812+
for (r, _) in overlay.runs() {
813+
if r.end > n {
814+
return Err(BuildError::OutOfBounds {
815+
range: r.clone(),
816+
region_len: n,
817+
});
818+
}
819+
}
820+
821+
// Left: current mesh as normalized value-bearing runs.
822+
let left = self.materialized_runs();
823+
// Right: overlay runs (already sorted, non-overlapping,
824+
// coalesced).
825+
let right: Vec<(std::ops::Range<usize>, T)> = overlay.runs().cloned().collect();
826+
827+
// Merge with overlay precedence, reusing the same splitting
828+
// strategy as RankedValues::merge_from.
829+
let merged = rle::merge_value_runs(left, right);
830+
831+
// Re-encode to compressed representation:
832+
let (table, raw_runs) = rle::rle_from_value_runs(merged);
833+
let runs = raw_runs
834+
.into_iter()
835+
.map(|(r, id)| Run::new(r.start, r.end, id))
836+
.collect();
837+
self.rep = Rep::Compressed { table, runs };
838+
839+
Ok(())
840+
}
751841
}
752842

753843
impl<T: Clone> ValueMesh<T> {
@@ -782,72 +872,15 @@ impl<T: Clone> ValueMesh<T> {
782872
Rep::Dense { values } => std::mem::take(values),
783873
Rep::Compressed { .. } => return,
784874
};
785-
let (table, runs) = compress_adjacent_with(values, same);
875+
let (table, raw_runs) = rle::rle_from_dense(values, same);
876+
let runs = raw_runs
877+
.into_iter()
878+
.map(|(r, id)| Run::new(r.start, r.end, id))
879+
.collect();
786880
self.rep = Rep::Compressed { table, runs };
787881
}
788882
}
789883

790-
/// Performs simple run-length encoding (RLE) compression over a dense
791-
/// sequence of values.
792-
///
793-
/// Adjacent "equal" elements are coalesced into contiguous runs,
794-
/// producing:
795-
///
796-
/// - a **table** of unique values (in first-occurrence order)
797-
/// - a **run list** of `(range, id)` pairs, where `range` is the
798-
/// half-open index range `[start, end)` in the original dense
799-
/// array, and `id` indexes into `table`.
800-
///
801-
/// # Example
802-
/// ```
803-
/// // Input: [A, A, B, B, B, A]
804-
/// // Output:
805-
/// // table = [A, B, A]
806-
/// // runs = [(0..2, 0), (2..5, 1), (5..6, 2)]
807-
/// ```
808-
///
809-
/// # Requirements
810-
/// - `T: Clone` is required to copy elements into the table.
811-
///
812-
/// # Returns
813-
/// A tuple `(table, runs)` that together form the compressed
814-
/// representation. Expanding the runs reproduces the original data.
815-
fn compress_adjacent_with<T: Clone, F>(values: Vec<T>, mut same: F) -> (Vec<T>, Vec<Run>)
816-
where
817-
F: FnMut(&T, &T) -> bool,
818-
{
819-
// Empty input; trivial empty compression.
820-
if values.is_empty() {
821-
return (Vec::new(), Vec::new());
822-
}
823-
824-
let mut table = Vec::new(); // unique values
825-
let mut runs = Vec::new(); // (range, table_id) pairs
826-
827-
let mut start = 0usize;
828-
table.push(values[0].clone());
829-
let mut cur_id: u32 = 0;
830-
831-
// Walk through all subsequent elements, closing and opening runs
832-
// whenever the value changes.
833-
for (i, _value) in values.iter().enumerate().skip(1) {
834-
if !same(&values[i], &table[cur_id as usize]) {
835-
// Close current run [start, i)
836-
runs.push(Run::new(start, i, cur_id));
837-
838-
// Start a new run
839-
start = i;
840-
table.push(values[i].clone());
841-
cur_id = (table.len() - 1) as u32;
842-
}
843-
}
844-
845-
// Close the final run
846-
runs.push(Run::new(start, values.len(), cur_id));
847-
848-
(table, runs)
849-
}
850-
851884
#[cfg(test)]
852885
mod tests {
853886
use std::convert::Infallible;
@@ -1681,4 +1714,79 @@ mod tests {
16811714
assert_eq!(mesh.get(3), Some(&2));
16821715
assert_eq!(mesh.get(5), Some(&3));
16831716
}
1717+
1718+
#[test]
1719+
fn merge_from_overlay_basic() {
1720+
// Base mesh with two contiguous runs.
1721+
let region: Region = extent!(n = 8).into();
1722+
let mut mesh = ValueMesh::from_dense(region.clone(), vec![1, 1, 1, 2, 2, 2, 3, 3]).unwrap();
1723+
1724+
// Overlay replaces middle segment [2..6) with 9s.
1725+
let overlay = ValueOverlay::try_from_runs(vec![(2..6, 9)]).unwrap();
1726+
1727+
mesh.merge_from_overlay(&region, &overlay).unwrap();
1728+
1729+
// Materialize back into ranges to inspect.
1730+
let out = mesh.materialized_runs();
1731+
1732+
// Expected: left prefix (0..2)=1, replaced middle (2..6)=9, tail (6..8)=3.
1733+
assert_eq!(out, vec![(0..2, 1), (2..6, 9), (6..8, 3)]);
1734+
}
1735+
1736+
#[test]
1737+
fn merge_from_overlay_multiple_spans() {
1738+
// Build mesh with alternating runs.
1739+
let region: Region = extent!(m = 12).into();
1740+
let mut mesh =
1741+
ValueMesh::from_dense(region.clone(), vec![1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4])
1742+
.unwrap();
1743+
1744+
// Overlay has a run that spans across the boundary of two
1745+
// left runs and another disjoint run later.
1746+
let overlay = ValueOverlay::try_from_runs(vec![(2..6, 9), (9..11, 8)]).unwrap();
1747+
1748+
mesh.merge_from_overlay(&region, &overlay).unwrap();
1749+
let out = mesh.materialized_runs();
1750+
1751+
// Expected after merge and re-compression:
1752+
// (0..2,1) untouched
1753+
// (2..6,9) overwrite of part of [1,2] runs
1754+
// (6..9,3) left tail survives
1755+
// (9..11,8) overwrite inside [4] run
1756+
// (11..12,4) leftover tail
1757+
assert_eq!(
1758+
out,
1759+
vec![(0..2, 1), (2..6, 9), (6..9, 3), (9..11, 8), (11..12, 4)]
1760+
);
1761+
}
1762+
1763+
#[test]
1764+
fn merge_from_overlay_crosses_row_boundary() {
1765+
// 2 x 5 region -> 10 linear ranks in row-major order.
1766+
let region: Region = extent!(rows = 2, cols = 5).into();
1767+
1768+
// Dense values laid out row-major:
1769+
// row 0: [1, 1, 1, 2, 2]
1770+
// row 1: [3, 3, 4, 4, 4]
1771+
let mut mesh =
1772+
ValueMesh::from_dense(region.clone(), vec![1, 1, 1, 2, 2, 3, 3, 4, 4, 4]).unwrap();
1773+
1774+
// Overlay that crosses the row boundary:
1775+
// linear ranks [3..7) -> 9
1776+
// - tail of row 0: indices 3,4 (the two 2s)
1777+
// - head of row 1: indices 5,6 (the two 3s)
1778+
let overlay = ValueOverlay::try_from_runs(vec![(3..7, 9)]).unwrap();
1779+
1780+
mesh.merge_from_overlay(&region, &overlay).unwrap();
1781+
1782+
// After merge, the dense view should be:
1783+
// [1,1,1, 9,9, 9,9, 4,4,4]
1784+
let flat: Vec<_> = mesh.values().collect();
1785+
assert_eq!(flat, vec![1, 1, 1, 9, 9, 9, 9, 4, 4, 4]);
1786+
1787+
// And the materialized runs should reflect that:
1788+
// (0..3,1) | (3..7,9) | (7..10,4)
1789+
let runs = mesh.materialized_runs();
1790+
assert_eq!(runs, vec![(0..3, 1), (3..7, 9), (7..10, 4)]);
1791+
}
16841792
}

0 commit comments

Comments
 (0)