Skip to content

Commit 4369138

Browse files
committed
packed bitfield for PatchedMetadata
Signed-off-by: Andrew Duffy <andrew@a10y.dev>
1 parent ffadc1b commit 4369138

File tree

1 file changed

+225
-33
lines changed
  • vortex-array/src/arrays/patched/vtable

1 file changed

+225
-33
lines changed

vortex-array/src/arrays/patched/vtable/mod.rs

Lines changed: 225 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ use vortex_error::VortexExpect;
1414
use vortex_error::VortexResult;
1515
use vortex_error::vortex_ensure;
1616
use vortex_error::vortex_ensure_eq;
17-
use vortex_error::vortex_err;
1817
use vortex_error::vortex_panic;
1918
use vortex_session::VortexSession;
2019

@@ -65,22 +64,108 @@ impl ValidityChild<Patched> for Patched {
6564

6665
#[derive(Clone, prost::Message)]
6766
pub struct PatchedMetadata {
68-
/// An offset into the first chunk's patches that should be considered in-view.
67+
/// A bitfield packed into a single u64 containing all the metadata needed to decode a
68+
/// serialized `PatchedArray`.
6969
///
70-
/// This may become nonzero after slicing.
71-
#[prost(uint32, tag = "1")]
72-
pub(crate) offset: u32,
70+
/// See [`PatchedMetadataFields`].
71+
#[prost(uint64, tag = "1")]
72+
pub(crate) packed: u64,
73+
}
74+
75+
/// A bitfield implemented on top of a `u64` containing the necessary metadata for reading a
76+
/// serialized `PatchedArray`.
77+
///
78+
/// The bit fields are in the following order:
79+
///
80+
/// * `offset`: 10 bits (always < 1024). An offset into the first chunk's patches that should be
81+
/// considered in-view.
82+
/// * `n_lanes_exp`: 3 bits. The binary exponent of `n_lanes`, which must be a power of two.
83+
/// A stored value of 0b000 represents n_lanes=1, and 0b111 represents n_lanes=128.
84+
/// * `n_patches`: 23 bits. The number of total patches, and the length of the indices and values
85+
/// child arrays.
86+
///
87+
/// The remaining bits 36..64 are reserved for future use.
88+
pub(crate) struct PatchedMetadataFields(u64);
89+
90+
impl std::fmt::Debug for PatchedMetadataFields {
91+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
92+
f.debug_struct("PatchedMetadataFields")
93+
.field("offset", &self.offset())
94+
.field("n_lanes", &self.n_lanes())
95+
.field("n_patches", &self.n_patches())
96+
.finish()
97+
}
98+
}
7399

74-
/// Number of patches. This is the length of the `indices` and `values` children.
75-
#[prost(uint32, tag = "2")]
76-
pub(crate) n_patches: u32,
100+
impl PatchedMetadataFields {
101+
const OFFSET_BITS: u32 = 10;
102+
const N_LANES_EXP_BITS: u32 = 3;
103+
const N_PATCHES_BITS: u32 = 23;
77104

78-
/// Number of lanes the patches get spread over.
105+
const OFFSET_MASK: u64 = (1 << Self::OFFSET_BITS) - 1;
106+
const N_LANES_EXP_MASK: u64 = (1 << Self::N_LANES_EXP_BITS) - 1;
107+
const N_PATCHES_MASK: u64 = (1 << Self::N_PATCHES_BITS) - 1;
108+
109+
const OFFSET_SHIFT: u32 = 0;
110+
const N_LANES_EXP_SHIFT: u32 = Self::OFFSET_BITS;
111+
const N_PATCHES_SHIFT: u32 = Self::OFFSET_BITS + Self::N_LANES_EXP_BITS;
112+
113+
/// Create a new `PatchedMetadataFields` from the component values.
79114
///
80-
/// By default, this is either 16 or 32 depending on the width of the type, but may change
81-
/// in the future, so we save it on write.
82-
#[prost(uint32, tag = "3")]
83-
pub(crate) n_lanes: u32,
115+
/// # Errors
116+
///
117+
/// Returns an error if any value exceeds its bit width:
118+
/// - `offset` must be < 1024 (10 bits)
119+
/// - `n_lanes` must be a power of two between 1 and 128 inclusive
120+
/// - `n_patches` must be < 8388608 (23 bits)
121+
pub fn new(offset: usize, n_lanes: usize, n_patches: usize) -> VortexResult<Self> {
122+
vortex_ensure!(
123+
offset < (1 << Self::OFFSET_BITS),
124+
"offset must be < 1024, got {offset}"
125+
);
126+
vortex_ensure!(
127+
n_lanes.is_power_of_two() && n_lanes <= 128,
128+
"n_lanes must be a power of two between 1 and 128, got {n_lanes}"
129+
);
130+
vortex_ensure!(
131+
n_patches < (1 << Self::N_PATCHES_BITS),
132+
"n_patches must be < 8388608, got {n_patches}"
133+
);
134+
135+
let n_lanes_exp = n_lanes.trailing_zeros() as u64;
136+
137+
let flags = (offset as u64)
138+
| (n_lanes_exp << Self::N_LANES_EXP_SHIFT)
139+
| ((n_patches as u64) << Self::N_PATCHES_SHIFT);
140+
Ok(Self(flags))
141+
}
142+
143+
/// Extract the offset field (bits 0..10).
144+
pub fn offset(&self) -> usize {
145+
((self.0 >> Self::OFFSET_SHIFT) & Self::OFFSET_MASK) as usize
146+
}
147+
148+
/// Extract the n_lanes field (bits 10..13), converted from the stored exponent.
149+
pub fn n_lanes(&self) -> usize {
150+
let exp = (self.0 >> Self::N_LANES_EXP_SHIFT) & Self::N_LANES_EXP_MASK;
151+
1 << exp
152+
}
153+
154+
/// Extract the n_patches field (bits 13..36).
155+
pub fn n_patches(&self) -> usize {
156+
((self.0 >> Self::N_PATCHES_SHIFT) & Self::N_PATCHES_MASK) as usize
157+
}
158+
159+
/// Return the underlying u64 representation.
160+
pub fn into_inner(self) -> u64 {
161+
self.0
162+
}
163+
}
164+
165+
impl From<u64> for PatchedMetadataFields {
166+
fn from(value: u64) -> Self {
167+
Self(value)
168+
}
84169
}
85170

86171
impl VTable for Patched {
@@ -166,24 +251,10 @@ impl VTable for Patched {
166251
}
167252

168253
fn metadata(array: &Self::Array) -> VortexResult<Self::Metadata> {
169-
let n_patches: u32 =
170-
array.indices.len().try_into().map_err(|_| {
171-
vortex_err!("Cannot serialize Patched array with > u32::MAX patches")
172-
})?;
173-
174-
#[expect(
175-
clippy::cast_possible_truncation,
176-
reason = "array offset always < 1024"
177-
)]
178-
let offset = array.offset as u32;
179-
180-
#[expect(clippy::cast_possible_truncation, reason = "n_lanes is always <= 64")]
181-
let n_lanes = array.n_lanes as u32;
254+
let fields = PatchedMetadataFields::new(array.offset, array.n_lanes, array.indices.len())?;
182255

183256
Ok(ProstMetadata(PatchedMetadata {
184-
offset,
185-
n_patches,
186-
n_lanes,
257+
packed: fields.into_inner(),
187258
}))
188259
}
189260

@@ -263,17 +334,19 @@ impl VTable for Patched {
263334
_buffers: &[BufferHandle],
264335
children: &dyn ArrayChildren,
265336
) -> VortexResult<PatchedArray> {
266-
let offset = metadata.offset as usize;
337+
let fields = PatchedMetadataFields::from(metadata.packed);
338+
let offset = fields.offset();
339+
let n_lanes = fields.n_lanes();
340+
let n_patches = fields.n_patches();
267341

268342
// n_chunks should correspond to the chunk in the `inner`.
269343
// After slicing when offset > 0, there may be additional chunks.
270344
let n_chunks = (len + offset).div_ceil(1024);
271-
let n_lanes = metadata.n_lanes as usize;
272345

273346
let inner = children.get(0, dtype, len)?;
274347
let lane_offsets = children.get(1, PType::U32.into(), n_chunks * n_lanes + 1)?;
275-
let indices = children.get(2, PType::U16.into(), metadata.n_patches as usize)?;
276-
let values = children.get(3, dtype, metadata.n_patches as usize)?;
348+
let indices = children.get(2, PType::U16.into(), n_patches)?;
349+
let values = children.get(3, dtype, n_patches)?;
277350

278351
Ok(PatchedArray {
279352
inner,
@@ -744,4 +817,123 @@ mod tests {
744817

745818
Ok(())
746819
}
820+
821+
mod metadata_fields_tests {
822+
use vortex_error::VortexResult;
823+
824+
use super::super::PatchedMetadataFields;
825+
826+
#[test]
827+
fn test_roundtrip_min_values() -> VortexResult<()> {
828+
let fields = PatchedMetadataFields::new(0, 1, 0)?;
829+
assert_eq!(fields.offset(), 0);
830+
assert_eq!(fields.n_lanes(), 1);
831+
assert_eq!(fields.n_patches(), 0);
832+
assert_eq!(fields.into_inner(), 0);
833+
Ok(())
834+
}
835+
836+
#[test]
837+
fn test_roundtrip_typical_values() -> VortexResult<()> {
838+
let fields = PatchedMetadataFields::new(512, 16, 1000)?;
839+
assert_eq!(fields.offset(), 512);
840+
assert_eq!(fields.n_lanes(), 16);
841+
assert_eq!(fields.n_patches(), 1000);
842+
Ok(())
843+
}
844+
845+
#[test]
846+
fn test_roundtrip_max_values() -> VortexResult<()> {
847+
let max_offset = (1 << 10) - 1; // 1023
848+
let max_n_lanes = 128; // 2^7
849+
let max_n_patches = (1 << 23) - 1; // 8388607
850+
851+
let fields = PatchedMetadataFields::new(max_offset, max_n_lanes, max_n_patches)?;
852+
assert_eq!(fields.offset(), max_offset);
853+
assert_eq!(fields.n_lanes(), max_n_lanes);
854+
assert_eq!(fields.n_patches(), max_n_patches);
855+
Ok(())
856+
}
857+
858+
#[test]
859+
fn test_all_valid_n_lanes() -> VortexResult<()> {
860+
for exp in 0..=7 {
861+
let n_lanes = 1 << exp;
862+
let fields = PatchedMetadataFields::new(0, n_lanes, 0)?;
863+
assert_eq!(fields.n_lanes(), n_lanes);
864+
}
865+
Ok(())
866+
}
867+
868+
#[test]
869+
fn test_from_u64() {
870+
// n_lanes=16 means exp=4, stored in bits 10..13
871+
let n_lanes_exp = 4u64; // log2(16)
872+
let raw: u64 = 512 | (n_lanes_exp << 10) | (1000 << 13);
873+
let fields = PatchedMetadataFields::from(raw);
874+
assert_eq!(fields.offset(), 512);
875+
assert_eq!(fields.n_lanes(), 16);
876+
assert_eq!(fields.n_patches(), 1000);
877+
}
878+
879+
#[test]
880+
fn test_offset_overflow() {
881+
let result = PatchedMetadataFields::new(1024, 1, 0);
882+
assert!(result.is_err());
883+
assert!(
884+
result
885+
.unwrap_err()
886+
.to_string()
887+
.contains("offset must be < 1024")
888+
);
889+
}
890+
891+
#[test]
892+
fn test_n_lanes_not_power_of_two() {
893+
let result = PatchedMetadataFields::new(0, 3, 0);
894+
assert!(result.is_err());
895+
assert!(
896+
result
897+
.unwrap_err()
898+
.to_string()
899+
.contains("n_lanes must be a power of two")
900+
);
901+
}
902+
903+
#[test]
904+
fn test_n_lanes_overflow() {
905+
let result = PatchedMetadataFields::new(0, 256, 0);
906+
assert!(result.is_err());
907+
assert!(
908+
result
909+
.unwrap_err()
910+
.to_string()
911+
.contains("n_lanes must be a power of two between 1 and 128")
912+
);
913+
}
914+
915+
#[test]
916+
fn test_n_lanes_zero() {
917+
let result = PatchedMetadataFields::new(0, 0, 0);
918+
assert!(result.is_err());
919+
assert!(
920+
result
921+
.unwrap_err()
922+
.to_string()
923+
.contains("n_lanes must be a power of two")
924+
);
925+
}
926+
927+
#[test]
928+
fn test_n_patches_overflow() {
929+
let result = PatchedMetadataFields::new(0, 1, 1 << 23);
930+
assert!(result.is_err());
931+
assert!(
932+
result
933+
.unwrap_err()
934+
.to_string()
935+
.contains("n_patches must be < 8388608")
936+
);
937+
}
938+
}
747939
}

0 commit comments

Comments
 (0)