-
-
Notifications
You must be signed in to change notification settings - Fork 985
fix(columnar): estimate BlockwiseLinear's residuals per block, not per column #3050
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
base: main
Are you sure you want to change the base?
Changes from 2 commits
e710a93
a952b6c
ec83eba
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,4 +1,5 @@ | ||
| use std::io::Write; | ||
| use std::num::NonZeroU64; | ||
| use std::sync::Arc; | ||
| use std::{io, iter}; | ||
|
|
||
|
|
@@ -8,6 +9,7 @@ use tantivy_bitpacker::{BitPacker, BitUnpacker, compute_num_bits}; | |
|
|
||
| use crate::MonotonicallyMappableToU64; | ||
| use crate::column_values::u64_based::line::Line; | ||
| use crate::column_values::u64_based::stats_collector::compute_gcd; | ||
| use crate::column_values::u64_based::{ColumnCodec, ColumnCodecEstimator, ColumnStats}; | ||
| use crate::column_values::{ColumnValues, VecColumn}; | ||
|
|
||
|
|
@@ -42,27 +44,70 @@ fn compute_num_blocks(num_vals: u32) -> u32 { | |
| num_vals.div_ceil(BLOCK_SIZE) | ||
| } | ||
|
|
||
| struct GcdBlock { | ||
| max_residual: u64, | ||
| endpoint_delta: u64, | ||
| gcd: u64, | ||
| num_rows: u32, | ||
| } | ||
|
|
||
| pub struct BlockwiseLinearEstimator { | ||
| block: Vec<u64>, | ||
| values_num_bytes: u64, | ||
| values_num_bits: u64, | ||
| gcd_blocks: Vec<GcdBlock>, | ||
| meta_num_bytes: u64, | ||
| } | ||
|
|
||
| impl Default for BlockwiseLinearEstimator { | ||
| fn default() -> Self { | ||
| Self { | ||
| block: Vec::with_capacity(BLOCK_SIZE as usize), | ||
| values_num_bytes: 0u64, | ||
| values_num_bits: 0u64, | ||
| gcd_blocks: Vec::new(), | ||
| meta_num_bytes: 0u64, | ||
| } | ||
| } | ||
| } | ||
|
|
||
| impl BlockwiseLinearEstimator { | ||
| fn block_min_and_gcd(&self) -> (u64, NonZeroU64) { | ||
| let Some((&first_val, rest)) = self.block.split_first() else { | ||
| return (0u64, NonZeroU64::MIN); | ||
| }; | ||
| let mut block_min = first_val; | ||
| let mut block_gcd: Option<NonZeroU64> = None; | ||
| for &buffer_val in rest { | ||
| block_min = block_min.min(buffer_val); | ||
| if block_gcd.map(NonZeroU64::get) == Some(1) { | ||
| continue; | ||
| } | ||
| let Some(non_zero_diff) = NonZeroU64::new(buffer_val.abs_diff(first_val)) else { | ||
| continue; | ||
| }; | ||
| block_gcd = Some(match block_gcd { | ||
| Some(gcd) => compute_gcd(non_zero_diff, gcd), | ||
| None => non_zero_diff, | ||
| }); | ||
| } | ||
| (block_min, block_gcd.unwrap_or(NonZeroU64::MIN)) | ||
| } | ||
|
|
||
| fn flush_block_estimate(&mut self) { | ||
| if self.block.is_empty() { | ||
| return; | ||
| } | ||
| let (block_min, block_gcd) = self.block_min_and_gcd(); | ||
| if block_gcd.get() > 1 { | ||
| let divider = DividerU64::divide_by(block_gcd.get()); | ||
| for buffer_val in self.block.iter_mut() { | ||
| *buffer_val = divider.divide(*buffer_val - block_min); | ||
| } | ||
| } else { | ||
| for buffer_val in self.block.iter_mut() { | ||
| *buffer_val -= block_min; | ||
| } | ||
| } | ||
|
|
||
| let column = VecColumn::from(std::mem::take(&mut self.block)); | ||
| let line = Line::train(&column); | ||
| self.block = column.into(); | ||
|
|
@@ -73,8 +118,19 @@ impl BlockwiseLinearEstimator { | |
| let val = buffer_val.wrapping_sub(interpolated_val); | ||
| max_value = val.max(max_value); | ||
| } | ||
| let bit_width = compute_num_bits(max_value) as usize; | ||
| self.values_num_bytes += (bit_width * self.block.len() + 7) as u64 / 8; | ||
| let num_rows = self.block.len() as u32; | ||
| if block_gcd.get() > 1 { | ||
| let first_val = self.block[0]; | ||
| let last_val = self.block[self.block.len() - 1]; | ||
| self.gcd_blocks.push(GcdBlock { | ||
| max_residual: max_value, | ||
| endpoint_delta: last_val.abs_diff(first_val), | ||
| gcd: block_gcd.get(), | ||
| num_rows, | ||
| }); | ||
| } else { | ||
| self.values_num_bits += compute_num_bits(max_value) as u64 * u64::from(num_rows); | ||
| } | ||
| self.meta_num_bytes += 1 + line.num_bytes(); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When a block has a large offset from the column minimum, this counts the VInt sizes of the locally normalized line rather than the line that Useful? React with 👍 / 👎. |
||
| } | ||
| } | ||
|
|
@@ -88,13 +144,24 @@ impl ColumnCodecEstimator for BlockwiseLinearEstimator { | |
| } | ||
| } | ||
| fn estimate(&self, stats: &ColumnStats) -> Option<u64> { | ||
| let mut estimate = 4 + stats.num_bytes() + self.meta_num_bytes + self.values_num_bytes; | ||
| if stats.gcd.get() > 1 { | ||
| let estimate_gain_from_gcd = | ||
| (stats.gcd.get() as f32).log2().floor() * stats.num_rows as f32 / 8.0f32; | ||
| estimate = estimate.saturating_sub(estimate_gain_from_gcd as u64); | ||
| } | ||
| Some(estimate) | ||
| let gcd = stats.gcd.get(); | ||
| let values_num_bits: u64 = self.values_num_bits | ||
| + self | ||
| .gcd_blocks | ||
| .iter() | ||
| .map(|block| { | ||
| let scale = (block.gcd / gcd).max(1); | ||
| let bit_width = if block.endpoint_delta < 1 << 31 | ||
| && block.endpoint_delta.saturating_mul(scale) >= 1 << 31 | ||
| { | ||
| 64 | ||
|
marcbachmann marked this conversation as resolved.
Outdated
|
||
| } else { | ||
| compute_num_bits(block.max_residual.saturating_mul(scale)) as u64 | ||
|
marcbachmann marked this conversation as resolved.
Outdated
|
||
| }; | ||
| bit_width * u64::from(block.num_rows) | ||
| }) | ||
| .sum::<u64>(); | ||
| Some(4 + stats.num_bytes() + self.meta_num_bytes + values_num_bits.div_ceil(8)) | ||
| } | ||
|
|
||
| fn finalize(&mut self) { | ||
|
|
||
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.
When most blocks have a local gcd greater than one, this retains a
GcdBlockfor every 512 input rows, changing the estimation pass from a roughly 4 KiB block buffer to memory linear in the column length. Each entry contains twou128s plus fiveu64s and a row count (roughly 80 bytes after alignment), so a 100-million-row timestamp or counter column can add about 15 MiB of estimator state, and billion-row columns about 150 MiB, even though values are otherwise streamed. This can materially increase peak indexing memory or cause OOMs on large segments; the summaries should be folded or represented without one allocation-sized record per block.Useful? React with 👍 / 👎.