Skip to content

Commit f19c5e7

Browse files
committed
Add CostModel::lower_bound; compressor prunes hopeless deferred work
CostModel gains lower_bound(scheme, data) -> Option<Cost>: a bound on the best cost any candidate from the scheme could achieve. Pass 2 of choose_best_scheme now skips a scheme's deferred sampling or callback outright when the bound is not strictly below the best cost so far. This is the hook that later lets decode-heavy models skip expensive FSST sampling when it cannot win. The default implementation returns None (never prune), which SizeCost keeps, so behavior is unchanged (zero golden snapshot churn). The plan sketched the signature as returning Cost with a never-prunes default, but Cost must be finite and its units are model-defined, so no sentinel 'never prunes' value exists; Option<Cost> expresses the default honestly. Unit tests cover pruning behind a best candidate and the no-best case where pruning must not fire. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Connor Tsui <connor.tsui20@gmail.com>
1 parent 83988a0 commit f19c5e7

2 files changed

Lines changed: 95 additions & 0 deletions

File tree

vortex-compressor/src/compressor.rs

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -485,6 +485,16 @@ impl CascadingCompressor {
485485
// short-circuit with `Skip` when they cannot beat it.
486486
for (scheme, deferred_estimate) in deferred {
487487
let _span = trace::scheme_eval_span(scheme.id()).entered();
488+
489+
// Model-side pruning: skip the deferred work outright when even this scheme's
490+
// best case cannot strictly beat the best candidate so far.
491+
if let Some((best_cost, _)) = best.as_ref()
492+
&& let Some(lower_bound) = self.cost_model.lower_bound(scheme.id(), data)
493+
&& lower_bound >= *best_cost
494+
{
495+
continue;
496+
}
497+
488498
let threshold: Option<EstimateScore> =
489499
best.as_ref().map(|(_, candidate)| candidate.score);
490500
match deferred_estimate {
@@ -1382,6 +1392,76 @@ mod tests {
13821392
Ok(())
13831393
}
13841394

1395+
/// Delegates pricing to [`SizeCost`] but bounds every scheme at a cost that can never
1396+
/// win, so all deferred work is pruned as soon as any best candidate exists.
1397+
#[derive(Debug)]
1398+
struct PruneAllDeferredModel;
1399+
1400+
impl CostModel for PruneAllDeferredModel {
1401+
fn cost(&self, candidate: &Candidate) -> Option<Cost> {
1402+
SizeCost.cost(candidate)
1403+
}
1404+
1405+
fn canonical_cost(&self, data: &ArrayAndStats, n_values: u64) -> Cost {
1406+
SizeCost.canonical_cost(data, n_values)
1407+
}
1408+
1409+
fn lower_bound(&self, _scheme: SchemeId, _data: &ArrayAndStats) -> Option<Cost> {
1410+
Some(Cost::new(f64::MAX))
1411+
}
1412+
}
1413+
1414+
#[test]
1415+
fn lower_bound_prunes_deferred_work_behind_a_best() -> VortexResult<()> {
1416+
// `CallbackRatioScheme`'s deferred `Ratio(3.0)` would beat `DirectRatioScheme`'s
1417+
// immediate `Ratio(2.0)` (see `callback_ratio_competes_numerically`), but the model
1418+
// bounds it above the running best, so its deferred work is pruned.
1419+
let compressor = CascadingCompressor::new(vec![&DirectRatioScheme, &CallbackRatioScheme])
1420+
.with_cost_model(Arc::new(PruneAllDeferredModel));
1421+
let schemes: [&'static dyn Scheme; 2] = [&DirectRatioScheme, &CallbackRatioScheme];
1422+
let data = estimate_test_data();
1423+
let mut exec_ctx = SESSION.create_execution_ctx();
1424+
1425+
let winner = compressor.choose_best_scheme(
1426+
&schemes,
1427+
&data,
1428+
CompressorContext::new(),
1429+
&mut exec_ctx,
1430+
)?;
1431+
1432+
assert!(matches!(
1433+
winner,
1434+
Some((scheme, WinnerEstimate::Score { score: EstimateScore::FiniteCompression(2.0), .. }))
1435+
if scheme.id() == DirectRatioScheme.id()
1436+
));
1437+
Ok(())
1438+
}
1439+
1440+
#[test]
1441+
fn lower_bound_never_prunes_without_a_best() -> VortexResult<()> {
1442+
// With no best candidate there is nothing to prune against, so the deferred
1443+
// callback still runs even under a prune-everything bound.
1444+
let compressor = CascadingCompressor::new(vec![&CallbackRatioScheme])
1445+
.with_cost_model(Arc::new(PruneAllDeferredModel));
1446+
let schemes: [&'static dyn Scheme; 1] = [&CallbackRatioScheme];
1447+
let data = estimate_test_data();
1448+
let mut exec_ctx = SESSION.create_execution_ctx();
1449+
1450+
let winner = compressor.choose_best_scheme(
1451+
&schemes,
1452+
&data,
1453+
CompressorContext::new(),
1454+
&mut exec_ctx,
1455+
)?;
1456+
1457+
assert!(matches!(
1458+
winner,
1459+
Some((scheme, WinnerEstimate::Score { score: EstimateScore::FiniteCompression(3.0), .. }))
1460+
if scheme.id() == CallbackRatioScheme.id()
1461+
));
1462+
Ok(())
1463+
}
1464+
13851465
#[test]
13861466
fn all_null_array_compresses_to_constant() -> VortexResult<()> {
13871467
let array = PrimitiveArray::new(

vortex-compressor/src/cost/mod.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ use std::fmt::Debug;
4242
pub use size::SizeCost;
4343

4444
pub use crate::candidate::Candidate;
45+
use crate::scheme::SchemeId;
4546
use crate::stats::ArrayAndStats;
4647

4748
/// An opaque, totally ordered cost. Lower is better.
@@ -95,4 +96,18 @@ pub trait CostModel: Debug + Send + Sync + 'static {
9596
/// Cost of leaving the array canonical — the baseline every candidate must strictly
9697
/// beat to be selected.
9798
fn canonical_cost(&self, data: &ArrayAndStats, n_values: u64) -> Cost;
99+
100+
/// A lower bound on the cost any candidate from `scheme` could possibly achieve on
101+
/// `data`, or `None` (the default) if the model offers no bound.
102+
///
103+
/// The compressor skips a scheme's deferred estimation work (sampling, callbacks)
104+
/// outright when the bound is not strictly below the best cost observed so far, since
105+
/// even the scheme's best case could not win selection. Models that can bound a
106+
/// scheme's payoff from cheap facts (e.g. minimum plausible output bytes at zero decode
107+
/// cost) can prune expensive sampling here; a sound bound never exceeds the cost of any
108+
/// candidate the scheme could actually produce.
109+
fn lower_bound(&self, scheme: SchemeId, data: &ArrayAndStats) -> Option<Cost> {
110+
let _ = (scheme, data);
111+
None
112+
}
98113
}

0 commit comments

Comments
 (0)