Repository navigation
Conversation
|
Hi @rapour, thanks for the contribution! Could you let us know how this is different from other Rust implementations (the one that comes to my mind is the |
Merging this PR will not alter performance1 warning
Comparing Footnotes
|
|
Hi @connortsui20, Thanks a lot for looking into this PR I have consulted two reference implementations:
Here is my initial reasoning:
The design follows rise-rs (the sampling scheme and its parameters). Even if some of these could be worked around, I think keeping it in-house is simpler than maintaining a wrapper plus an external dependency. That being said, I'll follow your lead. |
|
I unfortunately will not be able to review this until next week, but at a high level the implementation seems fine to me. Will have to spend a bit more time in the details though since this is a non-trivial PR |
|
I totally understand, Thanks a lot! I'll try to focus more on the vortex API part of the work in the meantime. |
|
Ok I unfortunately cannot review this this week but I promise I will next Monday 🙏 |
1ed4d94 to
037a0d3
Compare
|
Done, this PR now carries the latest codec, slightly different now that we have switched to a kernel for slicing. The #9935 is the follow up work stacked on top of these changes. |
|
Ok so I've been thinking about this and I think it would be more ideal to have an Elias Fano encoding that has 2 children arrays, where the low bits are fastlanes bitpacked (we still get constant time random access which is all the EF encoding needs, plus every other benefit of fastlanes bitpacking), and the high bits are This means we should add a I think this works nicely, and it should make the implementation a bit easier to understand and review as well. The only slightly more complex thing is that we need to match on the array children to make sure they have the correct encodings (unary + bitpacked) to get the fast asm output for the special EF operations. But if you look at FoR + BitPacked we already do this and I think it's quite easy. So I think the first step is to create a And it would be great if you could create an Epic and Tracking Issue for this (epic about adding EF, tracking issues about unary, fused kernels encoding, serialization, etc). Just follow the templates (and note that we have an loose rule of not LLM-generating tracking issues and epics). I'm also happy to write a tracking issue if you're not comfortable. Also, please reach out on slack via #ext-vortex or messaging me! Also @joseph-isaacs I'm happy to take this over. |
Signed-off-by: rapour <reza.apour@gmail.com>
037a0d3 to
1216793
Compare
connortsui20
left a comment
There was a problem hiding this comment.
overall the structure looks good. I left some review comments
| let mut upper = BufferMut::<u64>::zeroed(usize::try_from(max >> lower_bits)? + 1); | ||
| let mut lower = BufferMut::<T>::with_capacity(values.len()); | ||
| for &value in values { | ||
| let value: u64 = value.as_(); | ||
| upper[AsPrimitive::<usize>::as_(value >> lower_bits)] += 1; | ||
| lower.push((value & mask).as_()); | ||
| } |
There was a problem hiding this comment.
shouldn't this be reusing the unary encode logic?
There was a problem hiding this comment.
After a follow up discussion on Slack, elias_fano_encode keeps both children Primitive, as FoR::encode does, and I've documented that the compressor encodes them. The new EliasFanoScheme in vortex-btrblocks encodes upper with unary_encode and bit-packs lower. It isn't registered yet, so builders made with unrestricted() don't start choosing it. Happy to add it if you would prefer.
| /// The dtype of the `upper` child. | ||
| const UPPER_DTYPE: DType = DType::Primitive(PType::U64, NonNullable); |
There was a problem hiding this comment.
Is there a reason that this is hardcoded? I'm not against it (especially since we just merged the Widen encoding) but we should have some justification.
There was a problem hiding this comment.
One bucket can hold every value, so a count can reach the array's length. The EF scheme stores upper as Unary, whose bits are the same whatever the dtype. If you think using Widen is in order, please let me know and I'll try to make the switch.
There was a problem hiding this comment.
I should clarify: if we hardcode the dtype to u64 then whenever we canonicalize this array it will blow up to 64 bits per value, even if most of the values are small. And while I think there is no theoretical guarantee of the size of the buckets, I'm guessing that mostly uniform data is going to have smaller-sized buckets (especially if we remove duplicates and runs with encodings above this one).
I just added a Widen encoding that allows us to store a smaller width dtype underneath a larger logical dtype, which means we definitely want to use that if we hardcode this as u64.
This is also a more general question for Vortex in general: with the addition of the Widen encoding, should we start enforcing larger widths for integer child arrays that can be compressed into smaller widths rather than forcing a different dtype?
There was a problem hiding this comment.
Ok we talked about this offline, I think we all agree that can fix this to u64 and then use Widen. Doing this everywhere in Vortex is beyond the scope of this PR
Signed-off-by: rapour <reza.apour@gmail.com>
Summary
This adds a naive
EliasFanoencoding with two children:upper, the number of values in each bucket of high bits, andlower, the low bits of each value. There are no kernels, no reduce rules and no slicing, and it is in no file edition. It also adds anEliasFanoSchemetovortex-btrblocks. Fused kernels, including unpacking aBitPackedlowerin the same pass, are left to the later steps.Changes
elias_fano_encodesplits a sorted, non-nullable unsigned array into the two children and leaves both to the compressor, asFoR::encodedoes.upperhas a fixedu64dtype, and the encoder holds its counts in aWidenover the narrowest type that fits them.Widenhas no wire form yet, so the encoder's output can be written only onceupperis compressed.lower_bits, which isfloor(log2(max / len))or 0 whenmax < len, and the length ofupper.validatechecks the dtype and the children's shapes. Decoding returns an error when the counts do not add up to the length, or a low value does not fit inlower_bits.executerebuilds each value as(bucket << lower_bits) | low. WhenupperisUnary, it reads the buckets straight from the bits, as FoR does with aBitPackedchild, so decoding never builds theu64counts. Any otherupper, including the encoder'sWiden, is executed to a Primitive first.scalar_atcombines a scalar from each child: the bucket is aselecton the bits whenupperisUnary, and a scan of the counts otherwise.EliasFanoSchemewraps the values in FoR, splits FoR's child withelias_fano_encode, encodesupperwithunary_encodeand bit-packslower, asFoRSchemedoes. Its estimate comes from the min and max stats, and it checksis_sortedonly when EF could beat the best scheme so far. It is not registered in the defaultCompressionSession, so builders made withunrestricted()don't start choosing it.Widenupper and with theUnaryupper the scheme writes;Unaryupper is decoded without being executed first;Unaryupper;test_array_consistency, and a metadata golden file.