Skip to main content

hurray_core/descriptor/
shard.rs

1//! Shard descriptor — binary encode/decode.
2//!
3//! A shard descriptor identifies this tensor as a rectangular sub-region of a
4//! larger logical parent tensor. It is present in the wire format when the
5//! `HAS_SHARD` flag is set.
6//!
7//! Wire layout (spec § Shard Section):
8//! ```text
9//! parent_shape  uint64[rank]   (rank × 8 bytes)
10//! shard_offset  uint64[rank]   (rank × 8 bytes)
11//! ```
12//! Constraint: `shard_offset[k] + shape[k] <= parent_shape[k]` for all `k`.
13
14use crate::descriptor::cursor::{ByteCursor, ByteWriter};
15use crate::{Error, Result, Shape};
16
17/// Declares that this tensor is a rectangular shard of a larger parent tensor.
18///
19/// The shard descriptor carries the parent tensor's shape and the starting
20/// index of this shard within the parent along each dimension.
21///
22/// # Examples
23///
24/// ```
25/// use hurray_core::descriptor::ShardDescriptor;
26/// use hurray_core::Shape;
27///
28/// let parent = vec![10u64, 20];
29/// let offset = vec![0u64, 5];
30/// let shard  = ShardDescriptor::new(parent, offset).unwrap();
31/// assert_eq!(shard.parent_shape, [10, 20]);
32/// assert_eq!(shard.shard_offset, [0, 5]);
33/// ```
34#[derive(Debug, Clone, PartialEq, Eq)]
35pub struct ShardDescriptor {
36    /// Shape of the logical parent tensor. Same length as the tensor rank.
37    pub parent_shape: Vec<u64>,
38    /// Starting index of this shard within the parent along each dimension.
39    pub shard_offset: Vec<u64>,
40}
41
42impl ShardDescriptor {
43    /// Creates a new [`ShardDescriptor`], validating that `parent_shape` and
44    /// `shard_offset` have the same length.
45    ///
46    /// Shape-vs-offset bound checking (`offset[k] + shape[k] <= parent[k]`)
47    /// requires the tensor shape and is performed by
48    /// [`ShardDescriptor::validate_against_shape`].
49    ///
50    /// # Errors
51    ///
52    /// Returns [`Error::InvalidShape`] if `parent_shape.len() != shard_offset.len()`.
53    ///
54    /// # Examples
55    ///
56    /// ```
57    /// use hurray_core::descriptor::ShardDescriptor;
58    ///
59    /// let s = ShardDescriptor::new(vec![10, 20], vec![0, 5]).unwrap();
60    /// assert_eq!(s.parent_shape, [10, 20]);
61    ///
62    /// // Mismatched lengths are rejected.
63    /// assert!(ShardDescriptor::new(vec![10], vec![0, 5]).is_err());
64    /// ```
65    pub fn new(parent_shape: Vec<u64>, shard_offset: Vec<u64>) -> Result<Self> {
66        if parent_shape.len() != shard_offset.len() {
67            return Err(Error::InvalidShape(format!(
68                "shard: parent_shape.len() ({}) != shard_offset.len() ({})",
69                parent_shape.len(),
70                shard_offset.len()
71            )));
72        }
73        Ok(Self {
74            parent_shape,
75            shard_offset,
76        })
77    }
78
79    /// Checks that `shard_offset[k] + shape[k] <= parent_shape[k]` for every `k`.
80    ///
81    /// # Errors
82    ///
83    /// Returns [`Error::ShardOutOfBounds`] for the first dimension `k` that
84    /// violates the constraint.
85    ///
86    /// # Examples
87    ///
88    /// ```
89    /// use hurray_core::descriptor::ShardDescriptor;
90    /// use hurray_core::Shape;
91    ///
92    /// let shape  = Shape::new(vec![4, 4]).unwrap();
93    /// let shard  = ShardDescriptor::new(vec![10, 10], vec![0, 6]).unwrap();
94    ///
95    /// // offset[1]=6 + shape[1]=4 = 10 == parent[1]=10 → valid
96    /// assert!(shard.validate_against_shape(&shape).is_ok());
97    ///
98    /// let bad = ShardDescriptor::new(vec![10, 10], vec![0, 7]).unwrap();
99    /// // offset[1]=7 + shape[1]=4 = 11 > parent[1]=10 → rejected
100    /// assert!(bad.validate_against_shape(&shape).is_err());
101    /// ```
102    pub fn validate_against_shape(&self, shape: &Shape) -> Result<()> {
103        for k in 0..self.parent_shape.len() {
104            let dim = shape.dims()[k];
105            let offset = self.shard_offset[k];
106            let parent = self.parent_shape[k];
107            // Use saturating_add to avoid u64 overflow on pathological inputs.
108            if offset.saturating_add(dim) > parent {
109                return Err(Error::ShardOutOfBounds {
110                    dim: k,
111                    offset,
112                    size: dim,
113                    parent,
114                });
115            }
116        }
117        Ok(())
118    }
119
120    /// Encodes the shard section into `w` as `uint64[rank]` × 2.
121    pub(crate) fn encode_into(&self, w: &mut ByteWriter) {
122        for &v in &self.parent_shape {
123            w.write_u64_le(v);
124        }
125        for &v in &self.shard_offset {
126            w.write_u64_le(v);
127        }
128    }
129
130    /// Decodes a shard section from `cursor`, reading `2 × rank` u64 values.
131    pub(crate) fn decode_from(cursor: &mut ByteCursor<'_>, rank: u32) -> Result<Self> {
132        let rank = rank as usize;
133        let mut parent_shape = Vec::with_capacity(rank);
134        for _ in 0..rank {
135            parent_shape.push(cursor.read_u64_le()?);
136        }
137        let mut shard_offset = Vec::with_capacity(rank);
138        for _ in 0..rank {
139            shard_offset.push(cursor.read_u64_le()?);
140        }
141        Ok(Self {
142            parent_shape,
143            shard_offset,
144        })
145    }
146}
147
148// ── Tests ─────────────────────────────────────────────────────────────────────
149
150#[cfg(test)]
151mod tests {
152    use super::*;
153    use crate::descriptor::cursor::ByteWriter;
154    use crate::Shape;
155
156    fn round_trip(parent: Vec<u64>, offset: Vec<u64>) -> ShardDescriptor {
157        let rank = parent.len() as u32;
158        let shard = ShardDescriptor::new(parent, offset).unwrap();
159        let mut w = ByteWriter::new();
160        shard.encode_into(&mut w);
161        let bytes = w.into_vec();
162        let mut c = ByteCursor::new(&bytes, bytes.len());
163        ShardDescriptor::decode_from(&mut c, rank).unwrap()
164    }
165
166    #[test]
167    fn shard_round_trip() {
168        let parent = vec![100u64, 200, 300];
169        let offset = vec![10u64, 20, 30];
170        let decoded = round_trip(parent.clone(), offset.clone());
171        assert_eq!(decoded.parent_shape, parent);
172        assert_eq!(decoded.shard_offset, offset);
173    }
174
175    #[test]
176    fn shard_out_of_bounds_rejected() {
177        let shape = Shape::new(vec![3u64, 4]).unwrap();
178        // offset[1]=7 + shape[1]=4 = 11 > parent[1]=10
179        let shard = ShardDescriptor::new(vec![10, 10], vec![0, 7]).unwrap();
180        let err = shard.validate_against_shape(&shape).unwrap_err();
181        assert!(matches!(err, Error::ShardOutOfBounds { dim: 1, .. }));
182    }
183
184    #[test]
185    fn shard_length_mismatch_rejected() {
186        let err = ShardDescriptor::new(vec![10], vec![0, 5]).unwrap_err();
187        assert!(matches!(err, Error::InvalidShape(_)));
188    }
189
190    #[test]
191    fn shard_exact_bound_is_valid() {
192        // offset[k] + shape[k] == parent[k] is valid (not strictly less than).
193        let shape = Shape::new(vec![5u64]).unwrap();
194        let shard = ShardDescriptor::new(vec![10], vec![5]).unwrap();
195        assert!(shard.validate_against_shape(&shape).is_ok());
196    }
197}