Skip to content

Commit 9f794ae

Browse files
committed
Use logical page indexes for scalar probes with operation-owned retained caches
Signed-off-by: Daniel King <dan@spiraldb.com>
1 parent 570951e commit 9f794ae

3 files changed

Lines changed: 104 additions & 131 deletions

File tree

‎encodings/pco/src/access.rs‎

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ use vortex_array::ArrayView;
1414
use vortex_array::ExecutionCtx;
1515
use vortex_array::arrays::PrimitiveArray;
1616
use vortex_array::dtype::half;
17+
use vortex_array::match_each_native_ptype;
18+
use vortex_array::scalar::Scalar;
1719
use vortex_error::VortexResult;
1820
use vortex_error::vortex_ensure;
1921
use vortex_error::vortex_ensure_eq;
@@ -30,6 +32,7 @@ const RANK_STRIDE: usize = 512;
3032
#[derive(Default)]
3133
pub(crate) struct AccessState<const RETAIN_PAGES: bool = true> {
3234
legacy: Option<RankMask>,
35+
validated_all_null: bool,
3336
decoder: Option<Box<dyn Any>>,
3437
pages: BTreeMap<usize, CachedPage>,
3538
last_page: Option<(usize, CachedPage)>,
@@ -113,6 +116,43 @@ impl RankMask {
113116
}
114117

115118
impl<const RETAIN_PAGES: bool> AccessState<RETAIN_PAGES> {
119+
/// Read the last fully validated decoded page directly. A miss preserves all
120+
/// operation-owned cache state and follows the ordinary fallible resolver.
121+
#[inline]
122+
pub fn cached_scalar(&self, array: ArrayView<'_, Pco>, row: usize) -> Option<Scalar> {
123+
if row >= array.unsliced_n_rows() {
124+
return None;
125+
}
126+
let (_, cached) = self.last_page.as_ref()?;
127+
let decoded = cached.decoded.as_ref()?;
128+
let value = if let Some(rows) = &cached.rows {
129+
if !rows.contains(&row) {
130+
return None;
131+
}
132+
cached.mask.as_ref()?.value_index::<true>(row - rows.start)
133+
} else {
134+
let value = self.legacy.as_ref()?.value_index::<true>(row);
135+
if let Some(value) = value {
136+
if !cached.values.contains(&value) {
137+
return None;
138+
}
139+
Some(value - cached.values.start)
140+
} else {
141+
None
142+
}
143+
};
144+
Some(match value {
145+
None => Scalar::null(array.dtype().clone()),
146+
Some(value) => match_each_native_ptype!(decoded.ptype(), |T| {
147+
Scalar::primitive(decoded.as_slice::<T>()[value], array.dtype().nullability())
148+
}),
149+
})
150+
}
151+
152+
pub fn validated_all_null(&self) -> bool {
153+
self.validated_all_null
154+
}
155+
116156
fn cached(&self, page: usize) -> Option<&CachedPage> {
117157
if let Some((last, cached)) = &self.last_page
118158
&& *last == page
@@ -232,6 +272,7 @@ impl<const RETAIN_PAGES: bool> AccessState<RETAIN_PAGES> {
232272
let mask = array
233273
.unsliced_validity()
234274
.execute_mask(array.unsliced_n_rows(), ctx)?;
275+
self.validated_all_null = index.pages.is_empty() && mask.all_false();
235276
self.legacy = Some(RankMask::new(mask));
236277
}
237278
let Some(value) = self
@@ -280,6 +321,7 @@ impl<const RETAIN_PAGES: bool> AccessState<RETAIN_PAGES> {
280321
total,
281322
"Pco validity count does not match stored values"
282323
);
324+
self.validated_all_null = index.pages.is_empty() && mask.all_false();
283325
self.legacy = Some(RankMask::new(mask));
284326
}
285327
let mask = self

‎encodings/pco/src/probe.rs‎

Lines changed: 40 additions & 131 deletions
Original file line numberDiff line numberDiff line change
@@ -3,51 +3,23 @@
33

44
//! PCO probes retain every decoded page and index the compacted, non-null value positions.
55
6-
use std::ops::Range;
7-
8-
use pco::data_types::Number;
9-
use pco::data_types::NumberType;
10-
use pco::match_number_enum;
11-
use pco::wrapped::FileDecompressor;
126
use vortex_array::ArrayView;
137
use vortex_array::ExecutionCtx;
14-
use vortex_array::IntoArray;
15-
use vortex_array::arrays::PrimitiveArray;
16-
use vortex_array::dtype::NativePType;
17-
use vortex_array::dtype::half;
188
use vortex_array::match_each_native_ptype;
199
use vortex_array::scalar::Scalar;
20-
use vortex_array::validity::Validity;
21-
use vortex_buffer::BufferMut;
22-
use vortex_error::VortexExpect;
2310
use vortex_error::VortexResult;
24-
use vortex_mask::Mask;
11+
use vortex_error::vortex_ensure;
2512

2613
use crate::Pco;
27-
use crate::PcoArrayExt;
28-
use crate::array::number_type_from_ptype;
29-
use crate::array::vortex_err_from_pco;
30-
31-
const RANK_STRIDE: usize = 512;
14+
use crate::access::AccessState;
3215

3316
/// State retained by repeated PCO probes. Construction performs no allocation.
3417
///
35-
/// The mask and rank index use unsliced logical rows; page boundaries use compacted value
36-
/// positions. Every page decoded so far is retained, so the state grows with the pages touched
37-
/// and never beyond the decompressed array.
18+
/// Decoded pages and their local validity are retained as touched, up to the full array.
19+
/// Legacy files additionally retain a full validity mask and its sparse rank index.
3820
#[derive(Default)]
3921
pub struct PcoProbeState {
40-
validity: Option<Mask>,
41-
rank: Vec<usize>,
42-
pages: Vec<Page>,
43-
/// The page the previous probe landed in, `None` until the first probe resolves one.
44-
last_page: Option<usize>,
45-
}
46-
47-
struct Page {
48-
values: Range<usize>,
49-
chunk: usize,
50-
decoded: Option<PrimitiveArray>,
22+
access: AccessState,
5123
}
5224

5325
pub(crate) fn scalar_at(
@@ -57,110 +29,47 @@ pub(crate) fn scalar_at(
5729
ctx: &mut ExecutionCtx,
5830
) -> VortexResult<Scalar> {
5931
let Some(state) = state else {
60-
let validity = array.unsliced_validity();
61-
return array
62-
._slice(index, index + 1)
63-
.decompress(&validity, ctx)?
64-
.into_array()
65-
.execute_scalar(0, ctx);
66-
};
67-
let mask = match &mut state.validity {
68-
Some(mask) => mask,
69-
slot @ None => slot.insert(
70-
array
71-
.unsliced_validity()
72-
.execute_mask(array.unsliced_n_rows(), ctx)?,
73-
),
74-
};
75-
let logical_index = array.slice_start() + index;
76-
77-
let value_index = match mask {
78-
Mask::AllTrue(_) => logical_index,
79-
Mask::AllFalse(_) => return Ok(Scalar::null(array.dtype().clone())),
80-
Mask::Values(values) => {
81-
let bits = values.bit_buffer();
82-
if !bits.value(logical_index) {
83-
return Ok(Scalar::null(array.dtype().clone()));
84-
}
85-
if state.rank.is_empty() {
86-
state.rank.reserve(bits.len().div_ceil(RANK_STRIDE));
87-
let mut count = 0;
88-
for start in (0..bits.len()).step_by(RANK_STRIDE) {
89-
state.rank.push(count);
90-
count += bits.count_range(start, (start + RANK_STRIDE).min(bits.len()));
91-
}
92-
}
93-
let block = logical_index / RANK_STRIDE;
94-
state.rank[block] + bits.count_range(block * RANK_STRIDE, logical_index)
95-
}
32+
return scalar_at_once(array, index, ctx);
9633
};
97-
98-
if state.pages.is_empty() {
99-
let mut start = 0;
100-
for (chunk, metadata) in array.metadata.chunks.iter().enumerate() {
101-
for page in &metadata.pages {
102-
let end = start + page.n_values as usize;
103-
state.pages.push(Page {
104-
values: start..end,
105-
chunk,
106-
decoded: None,
107-
});
108-
start = end;
109-
}
110-
}
34+
if state.access.validated_all_null() {
35+
vortex_ensure!(
36+
index < array.len(),
37+
"Pco scalar index exceeds visible array bounds"
38+
);
39+
return Ok(Scalar::null(array.dtype().clone()));
11140
}
112-
let page_index = match state.last_page {
113-
Some(last) if state.pages[last].values.contains(&value_index) => last,
114-
_ => state
115-
.pages
116-
.partition_point(|page| page.values.end <= value_index),
117-
};
118-
state.last_page = Some(page_index);
119-
let page = state
120-
.pages
121-
.get_mut(page_index)
122-
.vortex_expect("PCO pages cover every valid value index");
123-
let values = match &mut page.decoded {
124-
Some(decoded) => decoded,
125-
slot @ None => {
126-
let decoded = match_number_enum!(
127-
number_type_from_ptype(array.dtype().as_ptype()),
128-
NumberType<T> => {
129-
decode_page::<T>(array, page.chunk, page.values.len(), array.pages[page_index].as_slice(), ctx)?
130-
}
131-
);
132-
slot.insert(decoded)
133-
}
134-
};
135-
Ok(match_each_native_ptype!(values.ptype(), |T| {
136-
Scalar::primitive(
137-
values.as_slice::<T>()[value_index - page.values.start],
138-
array.dtype().nullability(),
139-
)
140-
}))
41+
let row = array.slice_start() + index;
42+
if let Some(value) = state.access.cached_scalar(array, row) {
43+
return Ok(value);
44+
}
45+
scalar_at_uncached(array, row, state, ctx)
14146
}
14247

143-
fn decode_page<T: Number + NativePType>(
48+
// Keep the large temporary cache out of the retained-probe hit frame. Stateless
49+
// probes and cache misses are ordinary workloads, so neither helper is cold.
50+
#[inline(never)]
51+
fn scalar_at_once(
14452
array: ArrayView<'_, Pco>,
145-
chunk: usize,
146-
n_values: usize,
147-
buffer: &[u8],
53+
index: usize,
14854
ctx: &mut ExecutionCtx,
149-
) -> VortexResult<PrimitiveArray> {
150-
let (file, _) =
151-
FileDecompressor::new(array.metadata.header.as_slice()).map_err(vortex_err_from_pco)?;
152-
let (mut chunk, _) = file
153-
.chunk_decompressor::<T, _>(array.chunk_metas[chunk].as_ref())
154-
.map_err(vortex_err_from_pco)?;
155-
let mut decoder = chunk
156-
.page_decompressor(buffer, n_values)
157-
.map_err(vortex_err_from_pco)?;
158-
let mut values = BufferMut::<T>::with_capacity_in(n_values, ctx.allocator().clone());
159-
// SAFETY: the buffer reserves `n_values` elements, and the page decompressor was built for
160-
// exactly that count, so `read` writes every element before anything observes it.
161-
unsafe { values.set_len(n_values) };
162-
decoder.read(&mut values).map_err(vortex_err_from_pco)?;
163-
Ok(PrimitiveArray::new(values.freeze(), Validity::NonNullable))
55+
) -> VortexResult<Scalar> {
56+
scalar_at(array, index, Some(&mut PcoProbeState::default()), ctx)
57+
}
58+
59+
#[inline(never)]
60+
fn scalar_at_uncached(
61+
array: ArrayView<'_, Pco>,
62+
row: usize,
63+
state: &mut PcoProbeState,
64+
ctx: &mut ExecutionCtx,
65+
) -> VortexResult<Scalar> {
66+
let Some((page, value)) = state.access.resolve(array, row, ctx)? else {
67+
return Ok(Scalar::null(array.dtype().clone()));
68+
};
69+
let values = state.access.values(array, page, ctx)?;
70+
Ok(match_each_native_ptype!(values.ptype(), |T| {
71+
Scalar::primitive(values.as_slice::<T>()[value], array.dtype().nullability())
72+
}))
16473
}
16574

16675
#[cfg(test)]

‎encodings/pco/src/probe/tests.rs‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,3 +110,25 @@ fn probe_outlives_source_and_moves() -> VortexResult<()> {
110110
assert!(moved.0.execute_scalar(4096, &mut ctx).is_err());
111111
Ok(())
112112
}
113+
114+
#[test]
115+
fn retained_all_null_probes_preserve_slice_bounds() -> VortexResult<()> {
116+
let mut ctx = SESSION.create_execution_ctx();
117+
let input = PrimitiveArray::new(vec![0i64; 100000], Validity::AllInvalid);
118+
let encoded = Pco::from_primitive(input.as_view(), 3, 1024, &mut ctx)?.into_array();
119+
let sliced = encoded.slice(17..145)?;
120+
let mut probe = sliced.repeated_probe();
121+
for index in [0, 127, 64, 0, 127] {
122+
assert_eq!(
123+
probe.execute_scalar(index, &mut ctx)?,
124+
input.execute_scalar(17 + index, &mut ctx)?
125+
);
126+
}
127+
assert!(probe.execute_scalar(128, &mut ctx).is_err());
128+
assert!(probe.execute_scalar(usize::MAX, &mut ctx).is_err());
129+
assert_eq!(
130+
probe.execute_scalar(0, &mut ctx)?,
131+
input.execute_scalar(17, &mut ctx)?
132+
);
133+
Ok(())
134+
}

0 commit comments

Comments
 (0)