Skip to content

Commit ef056d5

Browse files
feat(vortex-onpair): update to the onpair 0.1 API (#8675)
## Summary Updates the experimental `vortex-onpair` encoding to the public API introduced in `onpair` 0.1. - Migrates compression and decoding to the new configuration, raw column-parts, and validated compact-dictionary APIs. - Updates compressed-domain equality comparisons for the new dictionary representation. - Supports adaptive `u32`/`u64` code offsets. - Validates dictionaries loaded from serialized data, including rejecting dictionaries that exceed the `u16` token address space. - Adds regression coverage for empty and all-null inputs, large dictionaries, narrowed children, sliced arrays, and `u64` code offsets. ## Scope This PR intentionally does not include the new `LIKE`, `list_contains`, or `take` pushdowns. Those will be submitted in a follow-up PR. A separate follow-up will enable OnPair on the default read path without enabling it in the default compressor. ## Dependency note The dependency is temporarily pinned to an upstream OnPair commit containing the dictionary-validation fix. Before merging, that fix will be published in a new crates.io release and the Git dependency will be replaced with the released version. ## Testing - `cargo +nightly fmt --all -- --check` - `cargo test -p vortex-onpair` - `cargo test -p vortex-btrblocks --features unstable_encodings` - `cargo clippy -p vortex-onpair -p vortex-btrblocks --all-targets --all-features -- -D warnings` --------- Signed-off-by: Francesco Gargiulo <gargiulo.fr@gmail.com> Signed-off-by: Francesco Gargiulo <francesco@spiraldb.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 7cf53cb commit ef056d5

19 files changed

Lines changed: 758 additions & 299 deletions

File tree

Cargo.lock

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ num_enum = { version = "0.7.3", default-features = false }
192192
object_store = { version = "0.13.2", default-features = false }
193193
once_cell = "1.21"
194194
oneshot = { version = "0.2.0", features = ["async"] }
195-
onpair = { version = "0.0.4" }
195+
onpair = "0.1.1"
196196
opentelemetry = "0.32.0"
197197
opentelemetry-otlp = "0.32.0"
198198
opentelemetry_sdk = "0.32.0"

encodings/experimental/onpair/README.md

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,26 +10,25 @@ cascading-compressor support on every integer child.
1010

1111
## Compute
1212

13-
Like the FSST encoding, this crate provides `cast` and `filter`
14-
pushdown. Other operators fall back to ordinary decompression.
13+
Like the FSST encoding, this crate pushes down common operations over the
14+
encoded representation. It supports `cast`, `filter`, byte length, and
15+
constant equality / inequality. Unsupported operators fall back to ordinary
16+
decompression.
1517

1618
## Default Configuration
1719

18-
The default training preset is **dict-12**: 12 bits per token,
19-
dictionary capped at 4 096 entries. Token codes are stored as a
20-
`PrimitiveArray<u16>`; downstream `FastLanes::BitPacking` losslessly
21-
narrows the child to exactly `bits`-bit codes on disk.
20+
The default training configuration uses OnPair's default dictionary budget and
21+
a fixed seed. Vortex stores token codes as an integer child array; downstream
22+
integer compression may narrow or bit-pack that child independently.
2223

2324
## Layout
2425

2526
- Buffer 0 — `dict_bytes`: dictionary blob built by the OnPair trainer,
26-
padded with `MAX_TOKEN_SIZE` trailing zero bytes so the over-copy
27-
decoder can read 16 bytes past the last token.
28-
- Slot 0 — `dict_offsets`: `PrimitiveArray<u32>`, len `dict_size + 1`.
29-
- Slot 1 — `codes`: `PrimitiveArray<u16>`, length `total_tokens`.
30-
- Slot 2 — `codes_offsets`: `PrimitiveArray<u32>`, length `num_rows + 1`.
31-
- Slot 3 — `uncompressed_lengths`: integer `PrimitiveArray`, length
32-
`num_rows`.
27+
including the read padding required by the decoder.
28+
- Slot 0 — `dict_offsets`: integer child, len `dict_size + 1`.
29+
- Slot 1 — `codes`: integer child, length `total_tokens`.
30+
- Slot 2 — `codes_offsets`: integer child, length `num_rows + 1`.
31+
- Slot 3 — `uncompressed_lengths`: integer child, length `num_rows`.
3332
- Slot 4 — optional validity child.
3433

3534
All four integer slot children flow through the standard cascading

encodings/experimental/onpair/benches/decode.rs

Lines changed: 31 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
//
44
//! Decode-path microbenchmarks for the OnPair Vortex array.
55
//!
6-
//! * `decompress_into` — the upstream `onpair::decompress_into` decoder hot
6+
//! * `decode_into` — the upstream `onpair::decode_into` decoder hot
77
//! loop, fed by pre-materialised [`DecodeInputs`]. Measures the inner loop
88
//! only (no child `execute`, no allocation).
99
//! * `canonicalize_to_varbinview` — the full Vortex
@@ -28,7 +28,7 @@ use std::mem::MaybeUninit;
2828
use std::sync::LazyLock;
2929

3030
use divan::Bencher;
31-
use onpair::Parts;
31+
use onpair::CompactDictionaryView;
3232
use vortex_array::ArrayRef;
3333
use vortex_array::ExecutionCtx;
3434
use vortex_array::IntoArray;
@@ -38,43 +38,45 @@ use vortex_array::arrays::PrimitiveArray;
3838
use vortex_array::arrays::VarBinArray;
3939
use vortex_array::arrays::VarBinViewArray;
4040
use vortex_array::arrays::filter::FilterKernel;
41+
use vortex_array::buffer::BufferHandle;
4142
use vortex_array::builtins::ArrayBuiltins;
4243
use vortex_array::dtype::DType;
4344
use vortex_array::dtype::NativePType;
4445
use vortex_array::dtype::Nullability;
4546
use vortex_buffer::Buffer;
46-
use vortex_buffer::ByteBuffer;
4747
use vortex_mask::Mask;
4848
use vortex_onpair::DEFAULT_DICT12_CONFIG;
4949
use vortex_onpair::OnPair;
5050
use vortex_onpair::OnPairArray;
5151
use vortex_onpair::OnPairArraySlotsExt;
5252

5353
/// Host-resident decode inputs, materialised once so the decode-loop benchmark
54-
/// measures only `onpair::decompress_into` (not child `execute`/allocation).
54+
/// measures only `onpair::decode_into` (not child `execute`/allocation).
5555
struct DecodeInputs {
56-
dict_bytes: ByteBuffer,
56+
dict_bytes: BufferHandle,
5757
dict_offsets: Buffer<u32>,
5858
codes: Buffer<u16>,
59-
bits: u32,
6059
}
6160

6261
impl DecodeInputs {
63-
fn as_parts(&self) -> Parts<'_> {
64-
Parts {
65-
dict_bytes: self.dict_bytes.as_slice(),
66-
dict_offsets: self.dict_offsets.as_slice(),
67-
bits: self.bits,
68-
codes: self.codes.as_slice(),
62+
fn dict(&self) -> CompactDictionaryView<'_> {
63+
// SAFETY: `materialise` validates this borrowed dictionary once after
64+
// widening offsets. The benchmark then keeps both buffers immutable.
65+
unsafe {
66+
CompactDictionaryView::new_unchecked(
67+
self.dict_bytes.as_host().as_slice(),
68+
self.dict_offsets.as_slice(),
69+
)
6970
}
7071
}
7172

72-
fn decompressed_len(&self) -> usize {
73-
onpair::decompressed_len(self.as_parts())
73+
fn decoded_len(&self) -> usize {
74+
onpair::decoded_len(self.codes.as_slice(), self.dict())
7475
}
7576

76-
fn decompress_into(&self, out: &mut [MaybeUninit<u8>]) -> usize {
77-
onpair::decompress_into(self.as_parts(), out)
77+
fn decode_into(&self, out: &mut [MaybeUninit<u8>]) -> usize {
78+
// SAFETY: callers allocate `decoded_len + DECODE_PADDING` bytes.
79+
unsafe { onpair::decode_into(self.codes.as_slice(), self.dict(), out) }
7880
}
7981
}
8082
use vortex_onpair::onpair_compress;
@@ -162,6 +164,8 @@ fn compress(n: usize, shape: Shape, ctx: &mut ExecutionCtx) -> OnPairArray {
162164
);
163165
onpair_compress(varbin.as_array(), DEFAULT_DICT12_CONFIG, ctx)
164166
.unwrap_or_else(|e| panic!("onpair_compress failed: {e}"))
167+
.try_downcast::<OnPair>()
168+
.unwrap_or_else(|array| panic!("expected OnPair array, got {}", array.encoding_id()))
165169
}
166170

167171
/// Canonicalise a slot child to the decoder's native primitive width.
@@ -175,13 +179,16 @@ fn widen<T: NativePType>(arr: &ArrayRef, ctx: &mut ExecutionCtx) -> Buffer<T> {
175179

176180
fn materialise(arr: &OnPairArray, ctx: &mut ExecutionCtx) -> (DecodeInputs, usize) {
177181
let view = arr.as_view();
182+
let dict_offsets = widen::<u32>(view.dict_offsets(), ctx);
183+
let dict_bytes = view.dict_bytes_handle().clone();
184+
CompactDictionaryView::validate(dict_bytes.as_host().as_slice(), dict_offsets.as_slice())
185+
.expect("valid OnPair dictionary");
178186
let inputs = DecodeInputs {
179-
dict_bytes: view.dict_bytes().clone(),
180-
dict_offsets: widen::<u32>(view.dict_offsets(), ctx),
187+
dict_bytes,
188+
dict_offsets,
181189
codes: widen::<u16>(view.codes(), ctx),
182-
bits: view.bits(),
183190
};
184-
let total = inputs.decompressed_len();
191+
let total = inputs.decoded_len();
185192
(inputs, total)
186193
}
187194

@@ -194,16 +201,16 @@ const CASES: &[(Shape, usize)] = &[
194201
];
195202

196203
/// Raw decode loop time, excluding child `execute` and the output allocation.
197-
/// Hits `onpair::decompress_into` directly.
204+
/// Hits `onpair::decode_into` directly.
198205
#[divan::bench(args = CASES)]
199-
fn decompress_into_bench(bencher: Bencher, case: (Shape, usize)) {
206+
fn decode_into_bench(bencher: Bencher, case: (Shape, usize)) {
200207
let mut ctx = SESSION.create_execution_ctx();
201208
let (shape, n) = case;
202209
let arr = compress(n, shape, &mut ctx);
203210
let (inputs, total) = materialise(&arr, &mut ctx);
204211
bencher.bench_local(|| {
205-
let mut out: Vec<u8> = Vec::with_capacity(total);
206-
let written = inputs.decompress_into(out.spare_capacity_mut());
212+
let mut out: Vec<u8> = Vec::with_capacity(total + onpair::DECODE_PADDING);
213+
let written = inputs.decode_into(out.spare_capacity_mut());
207214
unsafe { out.set_len(written) };
208215
divan::black_box(out);
209216
});
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
 � ��(08
1+
� ��(08

0 commit comments

Comments
 (0)