Skip to content

Commit f7216f0

Browse files
committed
Route CUDA metadata reads through host memory
1 parent b7b01d3 commit f7216f0

7 files changed

Lines changed: 292 additions & 19 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 & 3 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;
@@ -161,8 +161,7 @@ async fn cmd_scan(path: PathBuf, gpu_file: bool, json_output: bool) -> VortexRes
161161
let handle = session.handle();
162162

163163
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?
164+
session.open_options().with_cuda().open_path(&path).await?
166165
} else {
167166
let (recompressed, footer) = recompress_for_gpu(&path, &session).await?;
168167
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)