Skip to content

Commit ba4acb6

Browse files
authored
Route CUDA pruning io through host memory (#8862)
## Rationale for this change For zonemap segments we should be using the host memory in cuda reads, but for the data segments we should be using the pinned buffer pool reader that ships segments to the gpu. If we read every segment with the cuda reader then we would get the zonemap segments shipped to the device by default, and to use them in pruning we have to get them back to the host ## What changes are included in this PR? Cuda reads now use a routing vortex read at implementation that picks zonemap segments and routes them to the host reader, whereas data segments route to the gpu reader Signed-off-by: Onur Satici <onur@spiraldb.com>
1 parent 6a79bcf commit ba4acb6

7 files changed

Lines changed: 292 additions & 20 deletions

File tree

vortex-cuda/ffi/src/lib.rs

Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,11 @@ use vortex::error::VortexResult;
1919
use vortex::error::vortex_ensure;
2020
use vortex::file::OpenOptionsSessionExt;
2121
use vortex::file::WriteStrategyBuilder;
22-
use vortex::io::VortexReadAt;
2322
use vortex::io::runtime::BlockingRuntime;
24-
use vortex::io::session::RuntimeSessionExt;
2523
use vortex::session::SessionExt;
2624
use vortex::session::VortexSession;
25+
use vortex_cuda::CudaOpenOptionsExt;
2726
use vortex_cuda::CudaSession;
28-
use vortex_cuda::PooledFileReadAt;
2927
use vortex_cuda::arrow::ArrowDeviceArray;
3028
use vortex_cuda::arrow::ArrowDeviceArrayStream;
3129
use vortex_cuda::arrow::DeviceArrayExt;
@@ -110,7 +108,10 @@ pub unsafe extern "C-unwind" fn vx_cuda_array_sink_open_file(
110108
})
111109
}
112110

113-
/// Scan a local Vortex file through pinned host buffers and export an Arrow C Device stream.
111+
/// Scan a local Vortex file and export an Arrow C Device stream.
112+
///
113+
/// Footer and zone-map reads remain on the host. Data segments are staged through pinned host
114+
/// buffers and transferred directly to the GPU.
114115
///
115116
/// The file must use encodings and layouts supported by the CUDA execution path, such as files
116117
/// written by [`vx_cuda_array_sink_open_file`]. Pinned staging buffers are reused across scans made
@@ -140,19 +141,8 @@ pub unsafe extern "C-unwind" fn vx_cuda_scan_path_arrow_device_stream(
140141

141142
let path = unsafe { path.as_str() }?.to_owned();
142143
let session = session_with_cuda(unsafe { vx_session_ref(session) }?)?;
143-
let cuda_session = session.get::<CudaSession>();
144-
let stream = cuda_session.stream()?;
145-
let pool = Arc::clone(cuda_session.pinned_buffer_pool());
146-
drop(cuda_session);
147-
148-
let reader: Arc<dyn VortexReadAt> = Arc::new(PooledFileReadAt::open(
149-
path,
150-
session.handle(),
151-
pool,
152-
stream,
153-
)?);
154144
let array_stream = ffi_runtime().block_on(async {
155-
let file = session.open_options().open(reader).await?;
145+
let file = session.open_options().with_cuda().open_path(path).await?;
156146
Ok::<_, vortex::error::VortexError>(file.scan()?.into_array_stream()?.boxed())
157147
})?;
158148
let device_stream = array_stream.export_device_array_stream(&session, ffi_runtime())?;

vortex-cuda/gpu-scan-cli/src/main.rs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,9 @@ use vortex::file::WriteStrategyBuilder;
3131
use vortex::io::session::RuntimeSessionExt;
3232
use vortex::session::SessionExt;
3333
use vortex::session::VortexSession;
34+
use vortex_cuda::CudaOpenOptionsExt;
3435
use vortex_cuda::CudaSession;
3536
use vortex_cuda::PooledByteBufferReadAt;
36-
use vortex_cuda::PooledFileReadAt;
3737
use vortex_cuda::TracingLaunchStrategy;
3838
use vortex_cuda::executor::CudaArrayExt;
3939
use vortex_cuda::layout::CudaFlatLayoutStrategy;
@@ -158,11 +158,9 @@ async fn cmd_scan(path: PathBuf, gpu_file: bool, json_output: bool) -> VortexRes
158158
let pool = Arc::clone(cuda_session.pinned_buffer_pool());
159159
let cuda_stream = cuda_session.stream()?;
160160
drop(cuda_session);
161-
let handle = session.handle();
162161

163162
let gpu_file_handle = if gpu_file {
164-
let reader = PooledFileReadAt::open(&path, handle, Arc::clone(&pool), cuda_stream)?;
165-
session.open_options().open(Arc::new(reader)).await?
163+
session.open_options().with_cuda().open_path(&path).await?
166164
} else {
167165
let (recompressed, footer) = recompress_for_gpu(&path, &session).await?;
168166
let reader = PooledByteBufferReadAt::new(recompressed, Arc::clone(&pool), cuda_stream);

vortex-cuda/src/file.rs

Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// SPDX-FileCopyrightText: Copyright the Vortex contributors
3+
4+
//! CUDA-aware Vortex file opening.
5+
6+
use std::path::Path;
7+
use std::sync::Arc;
8+
9+
use vortex::error::VortexResult;
10+
use vortex::error::vortex_bail;
11+
use vortex::file::VortexFile;
12+
use vortex::file::VortexOpenOptions;
13+
use vortex::io::session::RuntimeSessionExt;
14+
use vortex::layout::Layout;
15+
use vortex::layout::layouts::zoned::LegacyStats;
16+
use vortex::layout::layouts::zoned::Zoned;
17+
use vortex::layout::segments::SegmentFuture;
18+
use vortex::layout::segments::SegmentId;
19+
use vortex::layout::segments::SegmentSource;
20+
21+
use crate::CudaSessionExt;
22+
use crate::PooledFileReadAt;
23+
use crate::layout::register_cuda_layout;
24+
25+
/// Extension trait for opening CUDA-readable files from [`VortexOpenOptions`].
26+
pub trait CudaOpenOptionsExt {
27+
/// Configure the opener to keep control-plane segments on the host and read data-plane
28+
/// segments into CUDA device memory.
29+
fn with_cuda(self) -> CudaOpenOptions;
30+
}
31+
32+
impl CudaOpenOptionsExt for VortexOpenOptions {
33+
fn with_cuda(self) -> CudaOpenOptions {
34+
CudaOpenOptions { inner: self }
35+
}
36+
}
37+
38+
/// CUDA-aware Vortex file open options.
39+
///
40+
/// Construct this adapter with [`CudaOpenOptionsExt::with_cuda`]. Standard file options should be
41+
/// configured before calling `with_cuda`.
42+
pub struct CudaOpenOptions {
43+
inner: VortexOpenOptions,
44+
}
45+
46+
impl CudaOpenOptions {
47+
/// Open a local Vortex file for CUDA execution.
48+
///
49+
/// The footer and zone-map segments are read through the ordinary host path. All other file
50+
/// segments are staged through the session-scoped pinned buffer pool and transferred to the
51+
/// device.
52+
pub async fn open_path(self, path: impl AsRef<Path>) -> VortexResult<VortexFile> {
53+
let path = path.as_ref().to_owned();
54+
register_cuda_layout(self.inner.session());
55+
56+
// A host cache cannot front the data source: cached host buffers would violate the CUDA
57+
// source's device-placement contract. It remains enabled on the control-plane source.
58+
let data_options = self.inner.clone().without_segment_cache();
59+
let control_file = self.inner.open_path(&path).await?;
60+
let footer = control_file.footer().clone();
61+
62+
let session = control_file.session().clone();
63+
let cuda_session = session.cuda_session();
64+
let stream = cuda_session.stream()?;
65+
let pool = Arc::clone(cuda_session.pinned_buffer_pool());
66+
drop(cuda_session);
67+
68+
let reader = Arc::new(PooledFileReadAt::open(
69+
&path,
70+
session.handle(),
71+
pool,
72+
stream,
73+
)?);
74+
let data_file = data_options
75+
.with_footer(footer.clone())
76+
.open(reader)
77+
.await?;
78+
79+
let control_segments =
80+
control_plane_segments(footer.layout().as_ref(), footer.segment_map().len())?;
81+
let routed = Arc::new(RoutingSegmentSource {
82+
control_segments: control_segments.into(),
83+
control: control_file.segment_source(),
84+
data: data_file.segment_source(),
85+
});
86+
87+
Ok(data_file.with_segment_source(routed))
88+
}
89+
}
90+
91+
struct RoutingSegmentSource {
92+
control_segments: Arc<[bool]>,
93+
control: Arc<dyn SegmentSource>,
94+
data: Arc<dyn SegmentSource>,
95+
}
96+
97+
impl SegmentSource for RoutingSegmentSource {
98+
fn request(&self, id: SegmentId) -> SegmentFuture {
99+
let Ok(idx) = usize::try_from(*id) else {
100+
return self.data.request(id);
101+
};
102+
if self.control_segments.get(idx).copied().unwrap_or(false) {
103+
self.control.request(id)
104+
} else {
105+
self.data.request(id)
106+
}
107+
}
108+
}
109+
110+
fn control_plane_segments(layout: &dyn Layout, segment_count: usize) -> VortexResult<Vec<bool>> {
111+
let mut control_segments = vec![false; segment_count];
112+
mark_zone_map_segments(layout, &mut control_segments)?;
113+
Ok(control_segments)
114+
}
115+
116+
fn mark_zone_map_segments(layout: &dyn Layout, control_segments: &mut [bool]) -> VortexResult<()> {
117+
if layout.is::<Zoned>() || layout.is::<LegacyStats>() {
118+
mark_layout_segments(layout.child(1)?.as_ref(), control_segments)?;
119+
mark_zone_map_segments(layout.child(0)?.as_ref(), control_segments)?;
120+
return Ok(());
121+
}
122+
123+
for child in layout.children()? {
124+
mark_zone_map_segments(child.as_ref(), control_segments)?;
125+
}
126+
Ok(())
127+
}
128+
129+
fn mark_layout_segments(layout: &dyn Layout, control_segments: &mut [bool]) -> VortexResult<()> {
130+
for id in layout.segment_ids() {
131+
let idx = usize::try_from(*id)?;
132+
let Some(is_control) = control_segments.get_mut(idx) else {
133+
vortex_bail!("layout references out-of-range segment {id}");
134+
};
135+
*is_control = true;
136+
}
137+
138+
for child in layout.children()? {
139+
mark_layout_segments(child.as_ref(), control_segments)?;
140+
}
141+
Ok(())
142+
}
143+
144+
#[cfg(test)]
145+
mod tests {
146+
use std::num::NonZeroUsize;
147+
148+
use futures::FutureExt;
149+
use parking_lot::Mutex;
150+
use vortex::array::buffer::BufferHandle;
151+
use vortex::array::dtype::DType;
152+
use vortex::array::dtype::Nullability::NonNullable;
153+
use vortex::array::dtype::PType;
154+
use vortex::array::dtype::StructFields;
155+
use vortex::buffer::ByteBuffer;
156+
use vortex::layout::IntoLayout;
157+
use vortex::layout::layouts::flat::FlatLayout;
158+
use vortex::layout::layouts::zoned::ZonedLayout;
159+
use vortex::session::registry::ReadContext;
160+
161+
use super::*;
162+
163+
#[derive(Clone)]
164+
struct RecordingSource {
165+
requests: Arc<Mutex<Vec<SegmentId>>>,
166+
value: u8,
167+
}
168+
169+
impl RecordingSource {
170+
fn new(value: u8) -> Self {
171+
Self {
172+
requests: Arc::new(Mutex::new(Vec::new())),
173+
value,
174+
}
175+
}
176+
}
177+
178+
impl SegmentSource for RecordingSource {
179+
fn request(&self, id: SegmentId) -> SegmentFuture {
180+
self.requests.lock().push(id);
181+
let value = self.value;
182+
async move { Ok(BufferHandle::new_host(ByteBuffer::from(vec![value]))) }.boxed()
183+
}
184+
}
185+
186+
#[tokio::test]
187+
async fn routes_control_and_data_segments() {
188+
let control = RecordingSource::new(1);
189+
let data = RecordingSource::new(2);
190+
let source = RoutingSegmentSource {
191+
control_segments: Arc::from([false, true, false]),
192+
control: Arc::new(control.clone()),
193+
data: Arc::new(data.clone()),
194+
};
195+
196+
let control_buffer = source
197+
.request(SegmentId::from(1))
198+
.await
199+
.unwrap()
200+
.unwrap_host();
201+
let data_buffer = source
202+
.request(SegmentId::from(2))
203+
.await
204+
.unwrap()
205+
.unwrap_host();
206+
207+
assert_eq!(control_buffer.as_ref(), &[1]);
208+
assert_eq!(data_buffer.as_ref(), &[2]);
209+
assert_eq!(*control.requests.lock(), [SegmentId::from(1)]);
210+
assert_eq!(*data.requests.lock(), [SegmentId::from(2)]);
211+
}
212+
213+
#[test]
214+
fn marks_only_zone_map_subtree_segments_as_control_plane() -> VortexResult<()> {
215+
let read_ctx = ReadContext::new([]);
216+
let data_dtype = DType::Primitive(PType::I32, NonNullable);
217+
let data =
218+
FlatLayout::new(16, data_dtype, SegmentId::from(0), read_ctx.clone()).into_layout();
219+
let zones = FlatLayout::new(
220+
1,
221+
DType::Struct(StructFields::empty(), NonNullable),
222+
SegmentId::from(1),
223+
read_ctx,
224+
)
225+
.into_layout();
226+
let layout =
227+
ZonedLayout::try_new(data, zones, NonZeroUsize::new(16).unwrap(), Arc::new([]))?
228+
.into_layout();
229+
230+
let control_segments = control_plane_segments(layout.as_ref(), 2)?;
231+
232+
assert_eq!(control_segments, [false, true]);
233+
Ok(())
234+
}
235+
}

vortex-cuda/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ mod device_buffer;
1414
mod device_read_at;
1515
pub mod dynamic_dispatch;
1616
pub mod executor;
17+
mod file;
1718
pub mod hybrid_dispatch;
1819
mod kernel;
1920
pub mod layout;
@@ -34,6 +35,8 @@ pub use device_read_at::CopyDeviceReadAt;
3435
pub use executor::CudaDispatchMode;
3536
pub use executor::CudaExecutionCtx;
3637
pub use executor::CudaKernelEvents;
38+
pub use file::CudaOpenOptions;
39+
pub use file::CudaOpenOptionsExt;
3740
use kernel::ALPExecutor;
3841
use kernel::BitPackedExecutor;
3942
use kernel::ConstantNumericExecutor;

vortex-cuda/src/pooled_read_at.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ pub const DEFAULT_OBJECT_STORE_CONCURRENCY: usize = 192;
4040
///
4141
/// Reads into a pooled pinned (page-locked) buffer, then submits a non-blocking
4242
/// H2D DMA transfer and returns a device `BufferHandle`.
43+
///
44+
/// This is a data-plane reader. To open a complete local Vortex file, prefer
45+
/// [`crate::CudaOpenOptionsExt::with_cuda`], which keeps the footer and zone maps on the host.
4346
#[derive(Clone)]
4447
pub struct PooledFileReadAt {
4548
uri: Arc<str>,

vortex-file/src/file.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,18 @@ impl VortexFile {
125125
Arc::clone(&self.segment_source)
126126
}
127127

128+
/// Replace the segment source used by this file.
129+
///
130+
/// Any cached layout reader is cleared so that subsequent scans construct readers over the
131+
/// replacement source.
132+
pub fn with_segment_source(mut self, segment_source: Arc<dyn SegmentSource>) -> Self {
133+
self.segment_source = segment_source;
134+
if self.layout_reader_cache.is_some() {
135+
self.layout_reader_cache = Some(OnceLock::new());
136+
}
137+
self
138+
}
139+
128140
/// Returns a reference to the Vortex session used to open this file.
129141
pub fn session(&self) -> &VortexSession {
130142
&self.session

0 commit comments

Comments
 (0)