Skip to content

Commit cc4afad

Browse files
feat(vortex-onpair): port to onpair 0.1.0 API
Port the OnPair encoding to the 0.1.0 public API, including raw column parts, validated compact dictionaries, and MaxDictBits configuration. Pin the latest upstream validation fix, add compressed-domain equality pushdown, and strengthen file-borne dictionary and code validation. Signed-off-by: Francesco Gargiulo <gargiulo.fr@gmail.com> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 204dacc commit cc4afad

18 files changed

Lines changed: 539 additions & 229 deletions

File tree

Cargo.lock

Lines changed: 2 additions & 3 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 = { git = "https://github.com/spiraldb/onpair.git", rev = "28df115c799674c663adedc8f81620ac82c7a890" }
196196
opentelemetry = "0.32.0"
197197
opentelemetry-otlp = "0.32.0"
198198
opentelemetry_sdk = "0.32.0"

encodings/experimental/onpair/README.md

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,23 +10,26 @@ 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 preset is **dict-12**: the OnPair trainer may build a
21+
dictionary with up to 4 096 tokens. After compression, the runtime code width is
22+
derived from the actual dictionary size. Vortex stores token codes as an integer
23+
child array; downstream integer compression may narrow or bit-pack that child
24+
independently of the OnPair metadata.
2225

2326
## Layout
2427

2528
- Buffer 0 — `dict_bytes`: dictionary blob built by the OnPair trainer,
2629
padded with `MAX_TOKEN_SIZE` trailing zero bytes so the over-copy
2730
decoder can read 16 bytes past the last token.
2831
- Slot 0 — `dict_offsets`: `PrimitiveArray<u32>`, len `dict_size + 1`.
29-
- Slot 1 — `codes`: `PrimitiveArray<u16>`, length `total_tokens`.
32+
- Slot 1 — `codes`: integer `PrimitiveArray`, length `total_tokens`.
3033
- Slot 2 — `codes_offsets`: `PrimitiveArray<u32>`, length `num_rows + 1`.
3134
- Slot 3 — `uncompressed_lengths`: integer `PrimitiveArray`, length
3235
`num_rows`.

encodings/experimental/onpair/benches/decode.rs

Lines changed: 29 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;
@@ -175,13 +177,16 @@ fn widen<T: NativePType>(arr: &ArrayRef, ctx: &mut ExecutionCtx) -> Buffer<T> {
175177

176178
fn materialise(arr: &OnPairArray, ctx: &mut ExecutionCtx) -> (DecodeInputs, usize) {
177179
let view = arr.as_view();
180+
let dict_offsets = widen::<u32>(view.dict_offsets(), ctx);
181+
let dict_bytes = view.dict_bytes_handle().clone();
182+
CompactDictionaryView::validate(dict_bytes.as_host().as_slice(), dict_offsets.as_slice())
183+
.expect("valid OnPair dictionary");
178184
let inputs = DecodeInputs {
179-
dict_bytes: view.dict_bytes().clone(),
180-
dict_offsets: widen::<u32>(view.dict_offsets(), ctx),
185+
dict_bytes,
186+
dict_offsets,
181187
codes: widen::<u16>(view.codes(), ctx),
182-
bits: view.bits(),
183188
};
184-
let total = inputs.decompressed_len();
189+
let total = inputs.decoded_len();
185190
(inputs, total)
186191
}
187192

@@ -194,16 +199,16 @@ const CASES: &[(Shape, usize)] = &[
194199
];
195200

196201
/// Raw decode loop time, excluding child `execute` and the output allocation.
197-
/// Hits `onpair::decompress_into` directly.
202+
/// Hits `onpair::decode_into` directly.
198203
#[divan::bench(args = CASES)]
199-
fn decompress_into_bench(bencher: Bencher, case: (Shape, usize)) {
204+
fn decode_into_bench(bencher: Bencher, case: (Shape, usize)) {
200205
let mut ctx = SESSION.create_execution_ctx();
201206
let (shape, n) = case;
202207
let arr = compress(n, shape, &mut ctx);
203208
let (inputs, total) = materialise(&arr, &mut ctx);
204209
bencher.bench_local(|| {
205-
let mut out: Vec<u8> = Vec::with_capacity(total);
206-
let written = inputs.decompress_into(out.spare_capacity_mut());
210+
let mut out: Vec<u8> = Vec::with_capacity(total + onpair::DECODE_PADDING);
211+
let written = inputs.decode_into(out.spare_capacity_mut());
207212
unsafe { out.set_len(written) };
208213
divan::black_box(out);
209214
});
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)