Skip to content

Commit 59e55e6

Browse files
committed
Add CUDA Arrow device schema export
Signed-off-by: Alexander Droste <alexander.droste@protonmail.com>
1 parent bed0d4d commit 59e55e6

13 files changed

Lines changed: 317 additions & 277 deletions

File tree

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -270,7 +270,7 @@ jobs:
270270
run: |
271271
git ls-files vortex-cuda vortex-cxx vortex-duckdb vortex-ffi \
272272
| grep -E '\.(cpp|hpp|cu|cuh|h)$' \
273-
| grep -v 'arrow/reference/abi\.h$' \
273+
| grep -v 'arrow/reference/arrow_c_device\.h$' \
274274
| grep -v 'kernels/src/bit_unpack_.*\.cu$' \
275275
| grep -v 'kernels/src/bit_unpack_.*_lanes\.cuh$' \
276276
| xargs clang-format --dry-run --Werror --style=file

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

REUSE.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,8 @@ path = ["vortex-cuda/kernels/src/bit_unpack_*"]
4646
precedence = "override"
4747
SPDX-FileCopyrightText = "Copyright the Vortex contributors"
4848
SPDX-License-Identifier = "Apache-2.0"
49+
50+
[[annotations]]
51+
path = "vortex-cuda/src/arrow/reference/arrow_c_device.h"
52+
SPDX-FileCopyrightText = "2016-2025 Copyright The Apache Software Foundation"
53+
SPDX-License-Identifier = "Apache-2.0"

vortex-cuda/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ unstable_encodings = ["vortex/unstable_encodings"]
2424

2525
[dependencies]
2626
arc-swap = { workspace = true }
27+
arrow-schema = { workspace = true, features = ["ffi"] }
2728
async-trait = { workspace = true }
2829
bytes = { workspace = true }
2930
cudarc = { workspace = true, features = ["f16"] }

vortex-cuda/build.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ fn main() {
5656
generate_unpack::<u64>(&kernels_src, 16).expect("Failed to generate unpack for u64");
5757

5858
let out_dir = PathBuf::from(env::var("OUT_DIR").expect("OUT_DIR not set"));
59-
generate_arrow_c_abi_bindings(Path::new(&manifest_dir), &out_dir);
59+
generate_arrow_device_array_bindings(Path::new(&manifest_dir), &out_dir);
6060
generate_dynamic_dispatch_bindings(&kernels_src, &out_dir);
6161
generate_patches_bindings(&kernels_src, &out_dir);
6262

@@ -197,9 +197,9 @@ fn nvcc_compile_ptx(
197197
Ok(())
198198
}
199199

200-
/// Generate bindings for the vendored Arrow C ABI header.
201-
fn generate_arrow_c_abi_bindings(manifest_dir: &Path, out_dir: &Path) {
202-
let header = manifest_dir.join("src/arrow/reference/abi.h");
200+
/// Generate bindings for the vendored Arrow C Device ABI header.
201+
fn generate_arrow_device_array_bindings(manifest_dir: &Path, out_dir: &Path) {
202+
let header = manifest_dir.join("src/arrow/reference/arrow_c_device.h");
203203
println!("cargo:rerun-if-changed={}", header.display());
204204

205205
let bindings = bindgen::Builder::default()
@@ -212,7 +212,7 @@ fn generate_arrow_c_abi_bindings(manifest_dir: &Path, out_dir: &Path) {
212212
.derive_debug(true)
213213
.layout_tests(false)
214214
.generate()
215-
.expect("Failed to generate Arrow C ABI bindings");
215+
.expect("Failed to generate Arrow C Device bindings");
216216

217217
bindings
218218
.write_to_file(out_dir.join("arrow_c_abi.rs"))

vortex-cuda/kernels/src/varbinview_compute_offsets.cu

Lines changed: 0 additions & 30 deletions
This file was deleted.

vortex-cuda/kernels/src/varbinview_copy_strings.cu

Lines changed: 0 additions & 39 deletions
This file was deleted.

vortex-cuda/src/arrow/canonical.rs

Lines changed: 111 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ use vortex::array::arrays::decimal::DecimalDataParts;
1515
use vortex::array::arrays::extension::ExtensionArrayExt;
1616
use vortex::array::arrays::primitive::PrimitiveDataParts;
1717
use vortex::array::arrays::struct_::StructDataParts;
18+
use vortex::array::arrays::varbinview::VarBinViewDataParts;
1819
use vortex::array::buffer::BufferHandle;
1920
use vortex::dtype::DecimalType;
2021
use vortex::error::VortexResult;
@@ -30,8 +31,6 @@ use crate::arrow::ExportDeviceArray;
3031
use crate::arrow::PrivateData;
3132
use crate::arrow::SyncEvent;
3233
use crate::arrow::check_validity_empty;
33-
use crate::arrow::varbinview::BinaryParts;
34-
use crate::arrow::varbinview::copy_varbinview_to_varbin;
3534
use crate::executor::CudaArrayExt;
3635

3736
/// An implementation of `ExportDeviceArray` that exports Vortex arrays to `ArrowDeviceArray` by
@@ -49,13 +48,13 @@ impl ExportDeviceArray for CanonicalDeviceArrayExport {
4948
) -> VortexResult<ArrowDeviceArray> {
5049
let cuda_array = array.execute_cuda(ctx).await?;
5150

52-
let (arrow_array, _) = export_canonical(cuda_array, ctx).await?;
51+
let (arrow_array, sync_event) = export_canonical(cuda_array, ctx).await?;
5352

5453
Ok(ArrowDeviceArray {
5554
array: arrow_array,
5655
device_id: ctx.stream().context().ordinal() as i64,
5756
device_type: ARROW_DEVICE_CUDA,
58-
sync_event: ptr::null_mut(),
57+
sync_event,
5958
reserved: Default::default(),
6059
})
6160
}
@@ -148,24 +147,32 @@ fn export_canonical(
148147
}
149148
Canonical::VarBinView(varbinview) => {
150149
let len = varbinview.len();
151-
check_validity_empty(&varbinview.validity()?)?;
150+
let VarBinViewDataParts {
151+
views,
152+
buffers: data_buffers,
153+
validity,
154+
..
155+
} = varbinview.into_data_parts();
152156

153-
let BinaryParts { offsets, bytes } =
154-
copy_varbinview_to_varbin(varbinview, ctx).await?;
157+
check_validity_empty(&validity)?;
155158

156-
let offsets = ctx.ensure_on_device(offsets).await?;
157-
let bytes = ctx.ensure_on_device(bytes).await?;
159+
let views = ctx.ensure_on_device(views).await?;
160+
let mut buffers = Vec::with_capacity(data_buffers.len() + 2);
161+
buffers.push(None);
162+
buffers.push(Some(views));
163+
for buffer in data_buffers.iter() {
164+
buffers.push(Some(ctx.ensure_on_device(buffer.clone()).await?));
165+
}
158166

159-
let buffers = vec![None, Some(offsets), Some(bytes)];
167+
let n_buffers = i64::try_from(buffers.len())?;
160168
let mut private_data = PrivateData::new(buffers, vec![], ctx)?;
161169
let sync_event = private_data.sync_event();
162-
//
163170
let arrow_array = ArrowArray {
164171
length: len as i64,
165172
null_count: 0,
166173
offset: 0,
167-
// Arrow Utf8/Binary layout: optional null bitmap, offsets, and data bytes.
168-
n_buffers: 3,
174+
// Arrow Utf8View/BinaryView layout: optional null bitmap, views, and data buffers.
175+
n_buffers,
169176
buffers: private_data.buffer_ptrs.as_mut_ptr(),
170177
n_children: 0,
171178
children: ptr::null_mut(),
@@ -232,8 +239,8 @@ fn export_fixed_size(
232239
"buffer must already be copied to device before calling"
233240
);
234241

235-
// TODO(aduffy): currently the null buffer is always None, in the future we will need
236-
// to pass it.
242+
// Non-trivial validity is rejected before fixed-size export, so the Arrow null bitmap slot is
243+
// always null for now. Future nullable export support should pass the validity bitmap here.
237244
let mut private_data = PrivateData::new(vec![None, Some(buffer)], vec![], ctx)?;
238245
let sync_event: SyncEvent = private_data.sync_event();
239246

@@ -271,7 +278,9 @@ unsafe extern "C" fn release_array(array: *mut ArrowArray) {
271278
let children = mem::take(&mut private_data.children);
272279
for child in children {
273280
if !child.is_null() {
274-
release_array(child);
281+
if let Some(release) = (*child).release {
282+
release(child);
283+
}
275284
// Children are allocated with Box::into_raw in PrivateData::new, so the
276285
// release callback must also reclaim the ArrowArray allocation itself.
277286
drop(Box::from_raw(child));
@@ -286,6 +295,9 @@ unsafe extern "C" fn release_array(array: *mut ArrowArray) {
286295

287296
#[cfg(test)]
288297
mod tests {
298+
use arrow_schema::DataType;
299+
use arrow_schema::Field;
300+
use arrow_schema::Schema;
289301
use rstest::rstest;
290302
use vortex::array::ArrayRef;
291303
use vortex::array::IntoArray;
@@ -432,14 +444,10 @@ mod tests {
432444

433445
assert_eq!(device_array.array.length, 3);
434446
assert_eq!(device_array.array.null_count, 0);
435-
// VarBin export: null buffer + offsets + data
447+
// VarBinView export: null buffer + views + data buffers
436448
assert_eq!(device_array.array.n_buffers, 3);
437-
let buffers = unsafe {
438-
std::slice::from_raw_parts(
439-
device_array.array.buffers,
440-
device_array.array.n_buffers as usize,
441-
)
442-
};
449+
let n_buffers = usize::try_from(device_array.array.n_buffers)?;
450+
let buffers = unsafe { std::slice::from_raw_parts(device_array.array.buffers, n_buffers) };
443451
assert!(buffers[0].is_null());
444452
assert!(!buffers[1].is_null());
445453
assert!(!buffers[2].is_null());
@@ -479,4 +487,84 @@ mod tests {
479487
unsafe { release_array(&raw mut device_array.array) };
480488
Ok(())
481489
}
490+
491+
#[crate::test]
492+
async fn test_export_struct_with_schema() -> VortexResult<()> {
493+
let mut ctx = CudaSession::create_execution_ctx(&VortexSession::empty())
494+
.vortex_expect("failed to create execution context");
495+
496+
let array = StructArray::new(
497+
FieldNames::from_iter(["a", "b", "c"]),
498+
vec![
499+
PrimitiveArray::from_iter(0u32..5).into_array(),
500+
PrimitiveArray::from_iter(0i64..5).into_array(),
501+
VarBinViewArray::from_iter_str(["one", "two", "three", "four", "five"])
502+
.into_array(),
503+
],
504+
5,
505+
Validity::NonNullable,
506+
)
507+
.into_array();
508+
let mut exported = array.export_device_array_with_schema(&mut ctx).await?;
509+
510+
let schema = Schema::try_from(&exported.schema)?;
511+
assert_eq!(
512+
schema,
513+
Schema::new(vec![
514+
Field::new("a", DataType::UInt32, false),
515+
Field::new("b", DataType::Int64, false),
516+
Field::new("c", DataType::Utf8View, false),
517+
])
518+
);
519+
assert_eq!(exported.array.array.length, 5);
520+
assert_eq!(exported.array.array.n_buffers, 1);
521+
assert_eq!(exported.array.array.n_children, 3);
522+
assert_eq!(exported.array.device_type, ARROW_DEVICE_CUDA);
523+
524+
unsafe { release_array(&raw mut exported.array.array) };
525+
Ok(())
526+
}
527+
528+
#[crate::test]
529+
async fn test_export_primitive_with_schema_is_column_shaped() -> VortexResult<()> {
530+
let mut ctx = CudaSession::create_execution_ctx(&VortexSession::empty())
531+
.vortex_expect("failed to create execution context");
532+
533+
let array = PrimitiveArray::from_iter(0u32..5).into_array();
534+
let mut exported = array.export_device_array_with_schema(&mut ctx).await?;
535+
536+
let field = Field::try_from(&exported.schema)?;
537+
assert_eq!(field, Field::new("", DataType::UInt32, false));
538+
assert_eq!(exported.array.array.length, 5);
539+
assert_eq!(exported.array.array.n_buffers, 2);
540+
assert_eq!(exported.array.array.n_children, 0);
541+
assert_eq!(exported.array.device_type, ARROW_DEVICE_CUDA);
542+
543+
unsafe { release_array(&raw mut exported.array.array) };
544+
Ok(())
545+
}
546+
547+
#[crate::test]
548+
async fn test_export_varbinview_with_schema_uses_utf8_view_layout() -> VortexResult<()> {
549+
let mut ctx = CudaSession::create_execution_ctx(&VortexSession::empty())
550+
.vortex_expect("failed to create execution context");
551+
552+
let array = VarBinViewArray::from_iter_str([
553+
"one",
554+
"two",
555+
"this is a longer string for out-of-line storage",
556+
])
557+
.into_array();
558+
let mut exported = array.export_device_array_with_schema(&mut ctx).await?;
559+
560+
let field = Field::try_from(&exported.schema)?;
561+
assert_eq!(field, Field::new("", DataType::Utf8View, false));
562+
assert_eq!(exported.array.array.length, 3);
563+
assert_eq!(exported.array.array.n_buffers, 3);
564+
assert_eq!(exported.array.array.n_children, 0);
565+
assert_eq!(exported.array.device_type, ARROW_DEVICE_CUDA);
566+
567+
unsafe { release_array(&raw mut exported.array.array) };
568+
Ok(())
569+
}
482570
}

0 commit comments

Comments
 (0)