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}