[Parquet] ALP encoder/decoder support - #9372
Conversation
|
amaaazing |
|
@alamb I worked with codex on replicating the c++ implementation reviewing commit by commit. I'll do one final read myself, but this should be ready for initial review. |
|
Thank you -- I will put this on my short list to reivew I was out last week |
|
@devanbenz Sorry for taking so long to address your comments. I'll make sure to work on it this week! |
devanbenz
left a comment
There was a problem hiding this comment.
Comment about header regarding publication of spec.
|
Thanks @devanbenz, I've finally addressed your reviews! |
Please cargo fmt the code 🫡 |
|
|
|
I merged up from main to get the most recent changes, and I am now reviewing this one more time |
|
|
||
| const MAX_EXPONENT: u8 = ALP_MAX_EXPONENT_F32; | ||
| const MAGIC_NUMBER: Self = 12582912.0; // 2^22 + 2^23 | ||
| const ENCODING_UPPER_LIMIT: Self = 2147483520.0; |
There was a problem hiding this comment.
The new clippy lints claim this value is out of the precision range of f32 (and similarly for the others)
I did what it told me in 68010c9
--> parquet/src/encodings/alp.rs:496:40
|
496 | const ENCODING_UPPER_LIMIT: Self = 2147483520.0;
| ^^^^^^^^^^^^
|
= help: for further information visit https://rust-lang.github.io/rust-clippy/rust-1.97.0/index.html#lossy_float_literal
= note: requested on the command line with `-W clippy::lossy-float-literal`
help: consider changing the type or replacing it with
|
496 - const ENCODING_UPPER_LIMIT: Self = 2147483520.0;
496 + const ENCODING_UPPER_LIMIT: Self = 2_147_483_500.0;
|
alamb
left a comment
There was a problem hiding this comment.
Thank you @sdf-jkl and @devanbenz -- I went through this some more and I think it is really nice. THank you so much.
I will wait for @devanbenz 's review as well, but I think this is now ready to merge once the upstream parquet-testing PR has merged
👏 -- I think we will have the first open source ALP implementation delivered
In terms of next steps, I plan to focus on the blog post next
| /// floating-point columns. | ||
| /// | ||
| /// [`ParquetValueType`]: crate::data_type::private::ParquetValueType | ||
| pub trait GetEncoder { |
There was a problem hiding this comment.
I asked claude about this -- it says this trait is needed because
Since get_encoder<T: DataType> is generic, every arm of its match must type-check for every T it's monomorphized with. So this doesn't compile:
Encoding::ALP => match T::get_physical_type() {
Type::FLOAT | Type::DOUBLE => Box::new(AlpEncoder::new()), // error: `i32: AlpFloat` not satisfied
...
}Because we basically need to only invoke this for certain T types (f32 and f64)
|
Here are some follow ups suggested by claude
Since an all-null leading page is a realistic input, this is probably something we should fix (as a follow on PR)
|
|
Before we proceed to the benchmarks for the blog, should we work on some follow up Perf 🚀🚀🚀 PRs? I have a related PR for improving bit packing speed. I started working on it to bring it to ALP later. |
Well, clearly having better performnce for the blog would be good :) But at the moment I have no idea where we stand compared to ZSTD in the rust implementation so I would probably build the benchmark scripts first and then decide if we needed to optimize more before publishing It would also be interesting to show the effect of hardware (aka can this implementaton take advantage of SIMD instructions) |
I was trying to follow the ALP spirit as much as possible and make everything vectorizable. Bit packing should be the last thing not vectorized yet. |
The results would be dataset dependent. Not sure we can get a reliable benchmark using random generated ones. |
Yes for sure -- I think we can start with using data from the paper as a starting point (they list several datasets) |
Some of them are here - https://github.com/cwida/ALP/tree/main/benchmarks I'll work on the benchmarks PR |
I think there is two usecases:
|
devanbenz
left a comment
There was a problem hiding this comment.
Just the one comment, spent the morning looking over this PR and playing around with it locally. It is a very large body of work, looks good to me. My comment is non-blocking but maybe something to look in to.
| + self.streaming.estimated_memory_size() | ||
| } | ||
|
|
||
| fn flush_buffer(&mut self) -> Result<Bytes> { |
There was a problem hiding this comment.
Can this ever be called when an encoder has no values set or null value? For example, if you create a new encoder with AlpEncoder::<DoubleType>::new() and call flush_buffer all while later pages have well behaved data. Will it cause issues? Could you add a test for this if it's possible?
There was a problem hiding this comment.
Maybe something like:
#[test]
fn test_empty_first_page() {
let mut encoder = AlpEncoder::<DoubleType>::new();
// First page flushed with no values
let empty = encoder.flush_buffer().unwrap();
assert_eq!(empty.len(), ALP_HEADER_SIZE);
// Later pages carry well-behaved decimal data.
let values: Vec<f64> = (0..1500).map(|i| (i as f64) * 0.01).collect();
encoder.put(&values).unwrap();
let page = encoder.flush_buffer().unwrap();
assert!(page.len() < values.len() * 8);
}There was a problem hiding this comment.
I think it's the same issue @alamb mentioned above -
If the first data page of a column chunk has zero non-null values, build_preset(&[]) fixes the scale at 10^0, so every non-integer value in all later pages of that chunk becomes an exception
Since an all-null leading page is a realistic input, this is probably something we should fix (as a follow on PR)
We'll add this as a follow up
|
Added the parquet-rewrite wiring here - 5e9ad1c |
|
FWIW I plan to merge this once the upstream parquet-testing PR is merged. I will prod the upstream PR tomorrow at the parquet sync |
Which issue does this PR close?
Rationale for this change
check issue
What changes are included in this PR?
Are these changes tested?
Are there any user-facing changes?