Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: teach DeltaArray slice and scalar_at #927

Merged
merged 14 commits into from
Sep 30, 2024

Conversation

danking
Copy link
Member

@danking danking commented Sep 25, 2024

More subtle than I expected.

DeltaArray is a sequence of chunks. All chunks except the last must be "full", i.e. containing 1,024 values. The last chunk may contain as few as one value and is encoded differently from the rest.

In this PR, I introduced an "offset" and a "limit". Together they enable logical/lazy slicing while preserving full chunks for later decompression. The offset is a value, less than 1024, which offsets into the first chunk. The limit is either None or less than 1024. None represents no limit which allows callers to avoid computing the length of the last chunk [1]. Internally, the limit is converted to a "right-offset": trailing_garbage which is sliced away when decompression happens.

I also added support for signed values. I verify they're non-negative in delta_compress.

[1] Which is, a bit annoyingly, this:

match deltas.len() % 1024 {
    0 => 1024,
    n => n
}

@danking danking requested a review from robert3005 September 25, 2024 14:59
@danking danking marked this pull request as ready for review September 25, 2024 14:59
@danking
Copy link
Member Author

danking commented Sep 25, 2024

If you're curious how using DeltaEncoding affects things, this comment benchmarks the use of DeltaEncoding for the offsets of the varbin array inside an FSST array.

Copy link
Member

@robert3005 robert3005 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's couple places where we are not using compute functions the right way

Comment on lines 18 to 26
let array = if array.ptype().is_signed_int() {
match_each_signed_integer_ptype!(array.ptype(), |$T| {
assert!(array.statistics().compute_min::<$T>().map(|x| x >= 0).unwrap_or(false));
});
&array.reinterpret_cast(array.ptype().to_unsigned())
} else {
array
};

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generally can handle this in the compressor where you would say you will only delta compress unsigned types and then we FoR compressor should kick in to convert things into unsigned and you will only ever see unsinged integers

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The compressor actually already does this.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.


impl ScalarAtFn for DeltaArray {
fn scalar_at(&self, index: usize) -> VortexResult<Scalar> {
let decompressed = SliceFn::slice(self, index, index + 1)?.into_primitive()?;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just slice(self, index, index+1) is there a reason you want to do self.slice(index, index+1)?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't do self.slice because both ArrayCompute and SliceFn are in scope, but I could do slice(self, ...), the function. I'll do that.

I guess it does seem a bit silly (in terms of generated code) to do the whole rigamarole of getting an option, asserting its not None, just to call a function that I can statically resolve to SliceFn::slice. Thoughts on that?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hm fair, I haven't thought about arraycompute and individual functions colliding in scope. Fair to keep it as is

physical_start % 1024,
limit,
)?;
if arr.len() != stop - start {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a weird assert in this place...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I was developing it was helpful to ensure applying the offset & limit produced the correct length but I'll remove it now.

new_deltas,
new_validity,
physical_start % 1024,
limit,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should just be a number, and 0 should be perfectly fine value for it

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, done! The code does seem simpler now.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we explicitly specify length here instead of last chunk length?

})?;

let new_deltas = deltas.with_dyn(|a| {
a.slice()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you want slice(deltas ...)

let validity = self.validity();
let lanes = self.lanes();

let new_bases = bases.with_dyn(|a| {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you want slice(bases, ...)

array.validity()
)
});
Ok(decoded)
decoded
.slice(array.offset(), decoded.len() - array.trailing_garbage())?
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can use the original array length here that will be only the valid values, i.e. offset + len

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

@danking danking force-pushed the dk/teach-delta-array-scalar-at-and-slice branch 2 times, most recently from ac4d207 to a0fd137 Compare September 30, 2024 15:49
Copy link
Member Author

@danking danking left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All addressed.

array.validity()
)
});
Ok(decoded)
decoded
.slice(array.offset(), decoded.len() - array.trailing_garbage())?
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.


impl ScalarAtFn for DeltaArray {
fn scalar_at(&self, index: usize) -> VortexResult<Scalar> {
let decompressed = SliceFn::slice(self, index, index + 1)?.into_primitive()?;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't do self.slice because both ArrayCompute and SliceFn are in scope, but I could do slice(self, ...), the function. I'll do that.

I guess it does seem a bit silly (in terms of generated code) to do the whole rigamarole of getting an option, asserting its not None, just to call a function that I can statically resolve to SliceFn::slice. Thoughts on that?

physical_start % 1024,
limit,
)?;
if arr.len() != stop - start {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I was developing it was helpful to ensure applying the offset & limit produced the correct length but I'll remove it now.

new_deltas,
new_validity,
physical_start % 1024,
limit,
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, done! The code does seem simpler now.

Comment on lines 18 to 26
let array = if array.ptype().is_signed_int() {
match_each_signed_integer_ptype!(array.ptype(), |$T| {
assert!(array.statistics().compute_min::<$T>().map(|x| x >= 0).unwrap_or(false));
});
&array.reinterpret_cast(array.ptype().to_unsigned())
} else {
array
};

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

@danking danking requested a review from robert3005 September 30, 2024 16:17
@danking danking enabled auto-merge (squash) September 30, 2024 16:18
@@ -68,7 +162,7 @@ impl DeltaArray {
#[inline]
pub fn deltas(&self) -> Array {
self.as_ref()
.child(1, self.dtype(), self.len())
.child(1, self.dtype(), self.metadata().deltas_len)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you could use deltas_len() function here

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done.

@@ -19,11 +22,86 @@ impl_encoding!("fastlanes.delta", ids::FL_DELTA, Delta);
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DeltaMetadata {
validity: ValidityMetadata,
len: usize,
deltas_len: usize,
Copy link
Member

@robert3005 robert3005 Sep 30, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As a followup we could get rid of this one, it always have to be len rounded up to 1024 len + offset rounded up to 1024

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't true; in fact, this is what took me so long to wrap my head around this PR. Before this PR, if the logical length is not 0 mod 1024, then we encode the last chunk as one vector using scalar operations. After this PR, the above is still true except it depends on deltas.len() (which previously was always equal to the logical length).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh, I somehow assumed it was the same as fastlanes bitpacking

More subtle than I expected.

DeltaArray is a sequence of chunks. All chunks except the last must be "full", i.e. containing 1,024
values. The last chunk may contain as few as one value and is encoded differently from the rest.

In this PR, I introduced an "offset" and a "limit". Together they enable logical/lazy slicing while
preserving full chunks for later decompression. The offset is a value, less than 1024, which offsets
into the first chunk. The limit is either `None` or less than 1024. `None` represents no limit which
allows callers to avoid computing the length of the last chunk [1]. Internally, the limit is
converted to a "right-offset": `trailing_garbage` which is sliced away when decompression happens.

[1] Which is, a bit annoyingly, this:

```
match deltas.len() % 1024 {
    0 => 1024,
    n => n
}
```
1. logical_len is already stored as the array length.
2. trailing_garbage duplicates the information stored in the logical length.
@danking danking force-pushed the dk/teach-delta-array-scalar-at-and-slice branch from 611e124 to 1ed2261 Compare September 30, 2024 17:08
@danking danking merged commit 826cdf1 into develop Sep 30, 2024
5 checks passed
@danking danking deleted the dk/teach-delta-array-scalar-at-and-slice branch September 30, 2024 17:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants