Skip to content

Commit 0bb4edb

Browse files
committed
Improve V2 scan partition scheduling
Signed-off-by: Nicholas Gates <nick@nickgates.com>
1 parent bbf029c commit 0bb4edb

5 files changed

Lines changed: 395 additions & 162 deletions

File tree

benchmarks/datafusion-bench/src/lib.rs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ fn vortex_session_from_env() -> anyhow::Result<VortexSession> {
145145
}
146146

147147
fn scan_scheduler_config_from_env() -> anyhow::Result<ScanSchedulerConfig> {
148-
Ok(std::env::var("VORTEX_SCAN_MAX_MORSEL_SLOTS")
148+
let config = std::env::var("VORTEX_SCAN_MAX_MORSEL_SLOTS")
149149
.ok()
150150
.map(|value| {
151151
value
@@ -154,7 +154,19 @@ fn scan_scheduler_config_from_env() -> anyhow::Result<ScanSchedulerConfig> {
154154
.map_err(|e| anyhow::anyhow!("invalid scan scheduler slot count {value}: {e}"))
155155
})
156156
.transpose()?
157-
.unwrap_or_else(ScanSchedulerConfig::default_morsel_slots))
157+
.unwrap_or_else(ScanSchedulerConfig::default_morsel_slots);
158+
159+
Ok(std::env::var("VORTEX_SCAN_MAX_READ_BYTES")
160+
.ok()
161+
.map(|value| {
162+
value
163+
.parse::<u64>()
164+
.map_err(|e| anyhow::anyhow!("invalid scan scheduler byte budget {value}: {e}"))
165+
})
166+
.transpose()?
167+
.map_or(config.clone(), |bytes| {
168+
config.with_read_byte_budget(Some(bytes))
169+
}))
158170
}
159171

160172
fn vortex_table_options() -> VortexTableOptions {

vortex-datafusion/src/persistent/opener.rs

Lines changed: 83 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -370,20 +370,15 @@ impl FileOpener for VortexOpener {
370370
if byte_range.start == 0 && byte_range.end == file.object_meta.size {
371371
None
372372
} else {
373-
// Distribute V2 natural split ranges across DataFusion's byte-range
374-
// file_groups. V2 prepares morsels from these same split ranges, so each
375-
// DataFusion partition owns whole V2 morsels instead of slicing through
376-
// chunk boundaries and forcing duplicate decode work.
377-
let split_ranges = scan_plan_split_ranges_for_file(
378-
natural_split_ranges.as_ref(),
379-
&file.object_meta.location,
380-
&vxf,
381-
)?;
382-
383-
let Some(row_range) = split_aligned_row_range(
373+
// DataFusion partitions a single file by byte ranges. V2 may expose only
374+
// coarse top-level split hints, so assigning whole natural splits here can
375+
// collapse many byte ranges into a few row ranges. Slice proportionally by
376+
// row count; the V2 scan plan will still split the resulting row range into
377+
// layout-aware morsels during preparation.
378+
let Some(row_range) = byte_range_to_row_range(
384379
byte_range,
385380
file.object_meta.size,
386-
split_ranges.as_ref(),
381+
vxf.row_count(),
387382
) else {
388383
return Ok(stream::empty().boxed());
389384
};
@@ -662,43 +657,34 @@ fn compute_natural_split_ranges(layout_reader: &dyn LayoutReader) -> DFResult<Ar
662657
Ok(split_points.into())
663658
}
664659

665-
/// Get or create V2 natural split ranges for a file. These ranges are produced by the file's V2
666-
/// ScanPlan root and cached per path so every byte-range partition sees the same row boundaries.
667-
fn scan_plan_split_ranges_for_file(
668-
split_ranges_cache: &DashMap<Path, Arc<[Range<u64>]>>,
669-
path: &Path,
670-
file: &VortexFile,
671-
) -> DFResult<Arc<[Range<u64>]>> {
672-
if let Some(ranges) = split_ranges_cache.get(path) {
673-
return Ok(Arc::clone(ranges.value()));
660+
fn byte_range_to_row_range(
661+
byte_range: Range<u64>,
662+
total_size: u64,
663+
row_count: u64,
664+
) -> Option<Range<u64>> {
665+
if byte_range.start >= byte_range.end || total_size == 0 || row_count == 0 {
666+
return None;
674667
}
675668

676-
let ranges: Arc<[Range<u64>]> = file
677-
.scan_plan_splits()
678-
.map_err(|e| exec_datafusion_err!("Failed to compute Vortex scan2 splits: {e}"))?
679-
.into();
680-
tracing::debug!(
681-
target: "vortex_datafusion::persistent::opener",
682-
path = %path,
683-
split_count = ranges.len(),
684-
first_split = ?ranges.first(),
685-
last_split = ?ranges.last(),
686-
"scan2 file split ranges"
687-
);
688-
tracing::trace!(
689-
target: "vortex_datafusion::persistent::opener",
690-
path = %path,
691-
?ranges,
692-
"scan2 file split range detail"
693-
);
694-
695-
match split_ranges_cache.entry(path.clone()) {
696-
Entry::Occupied(entry) => Ok(Arc::clone(entry.get())),
697-
Entry::Vacant(entry) => {
698-
entry.insert(Arc::clone(&ranges));
699-
Ok(ranges)
700-
}
669+
let start_byte = byte_range.start.min(total_size);
670+
let end_byte = byte_range.end.min(total_size);
671+
if start_byte >= end_byte {
672+
return None;
701673
}
674+
675+
let start = byte_to_row(start_byte, total_size, row_count);
676+
let end = if end_byte == total_size {
677+
row_count
678+
} else {
679+
byte_to_row(end_byte, total_size, row_count)
680+
};
681+
682+
(start < end).then_some(start..end)
683+
}
684+
685+
fn byte_to_row(byte: u64, total_size: u64, row_count: u64) -> u64 {
686+
let row = (u128::from(byte) * u128::from(row_count)) / u128::from(total_size);
687+
u64::try_from(row).vortex_expect("byte-to-row projection should fit into u64")
702688
}
703689

704690
/// Translate a DataFusion byte range to the contiguous natural split ranges it owns.
@@ -807,6 +793,57 @@ mod tests {
807793

808794
static SESSION: LazyLock<VortexSession> = LazyLock::new(VortexSession::default);
809795

796+
#[rstest]
797+
#[case(0..10, 100, 50, Some(0..5))]
798+
#[case(10..20, 100, 50, Some(5..10))]
799+
#[case(90..100, 100, 50, Some(45..50))]
800+
#[case(100..110, 100, 50, None)]
801+
#[case(0..1, 100, 50, None)]
802+
fn test_byte_range_to_row_range(
803+
#[case] byte_range: Range<u64>,
804+
#[case] total_size: u64,
805+
#[case] row_count: u64,
806+
#[case] expected: Option<Range<u64>>,
807+
) {
808+
assert_eq!(
809+
byte_range_to_row_range(byte_range, total_size, row_count),
810+
expected
811+
);
812+
}
813+
814+
#[test]
815+
fn test_byte_ranges_cover_rows_exactly_once() {
816+
let total_size = 179_114_706;
817+
let row_count = 6_001_215;
818+
let partitions = 18;
819+
let byte_ranges = (0..partitions)
820+
.map(|idx| {
821+
let start = idx * total_size / partitions;
822+
let end = (idx + 1) * total_size / partitions;
823+
start..end
824+
})
825+
.collect::<Vec<_>>();
826+
827+
let row_ranges = byte_ranges
828+
.into_iter()
829+
.filter_map(|byte_range| byte_range_to_row_range(byte_range, total_size, row_count))
830+
.collect::<Vec<_>>();
831+
832+
assert_eq!(row_ranges.len(), partitions as usize);
833+
assert_eq!(row_ranges.first().map(|range| range.start), Some(0));
834+
assert_eq!(row_ranges.last().map(|range| range.end), Some(row_count));
835+
assert_eq!(
836+
row_ranges
837+
.iter()
838+
.map(|range| range.end - range.start)
839+
.sum::<u64>(),
840+
row_count
841+
);
842+
for (left, right) in row_ranges.iter().tuple_windows() {
843+
assert_eq!(left.end, right.start);
844+
}
845+
}
846+
810847
#[rstest]
811848
#[case(0..3, 10, vec![0..2, 2..5, 5..10], Some(0..2))]
812849
#[case(3..7, 10, vec![0..2, 2..5, 5..10], Some(2..5))]

vortex-file/src/file.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ use vortex_session::VortexSession;
3636
use crate::FileStatistics;
3737
use crate::footer::Footer;
3838
use crate::multi::scan_v2;
39+
use crate::multi::scan_v2::PreparedScanPlanCache;
3940
use crate::pruning::can_prune_file_stats;
4041
use crate::v2::FileStatsLayoutReader;
4142

@@ -60,6 +61,8 @@ pub struct VortexFile {
6061
scan_plan_state_cache: PreparedStateCacheRef,
6162
/// Shared cache for v2 in-flight segment futures across row-range scans of this file.
6263
scan_plan_segment_future_cache: Arc<SegmentFutureCache>,
64+
/// Shared cache for v2 request-level prepared plans across row-range scans of this file.
65+
scan_plan_prepared_cache: Arc<PreparedScanPlanCache>,
6366
}
6467

6568
fn layout_reader(
@@ -98,6 +101,7 @@ impl VortexFile {
98101
scan_plan_root_cache: Arc::new(OnceLock::new()),
99102
scan_plan_state_cache: Arc::new(PreparedStateCache::default()),
100103
scan_plan_segment_future_cache: Arc::new(SegmentFutureCache::new()),
104+
scan_plan_prepared_cache: Arc::new(PreparedScanPlanCache::default()),
101105
}
102106
}
103107

@@ -111,6 +115,7 @@ impl VortexFile {
111115
scan_plan_root_cache: self.scan_plan_root_cache,
112116
scan_plan_state_cache: self.scan_plan_state_cache,
113117
scan_plan_segment_future_cache: self.scan_plan_segment_future_cache,
118+
scan_plan_prepared_cache: self.scan_plan_prepared_cache,
114119
}
115120
}
116121

@@ -201,6 +206,10 @@ impl VortexFile {
201206
Arc::clone(&self.scan_plan_segment_future_cache)
202207
}
203208

209+
pub(crate) fn scan_plan_prepared_cache(&self) -> Arc<PreparedScanPlanCache> {
210+
Arc::clone(&self.scan_plan_prepared_cache)
211+
}
212+
204213
/// Create a [`DataSource`](vortex_scan::DataSource) from this file for scanning.
205214
///
206215
/// Wraps the file's layout reader with [`FileStatsLayoutReader`] (when file-level

0 commit comments

Comments
 (0)