-
Notifications
You must be signed in to change notification settings - Fork 32
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
Conversation
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. |
There was a problem hiding this 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
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 | ||
}; | ||
|
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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()?; |
There was a problem hiding this comment.
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)
?
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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...
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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() |
There was a problem hiding this comment.
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| { |
There was a problem hiding this comment.
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())? |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
ac4d207
to
a0fd137
Compare
There was a problem hiding this 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())? |
There was a problem hiding this comment.
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()?; |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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.
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 | ||
}; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
encodings/fastlanes/src/delta/mod.rs
Outdated
@@ -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) |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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.
fixup limit
611e124
to
1ed2261
Compare
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: