= match_each_integer_ptype!(lens.ptype(), |P| {
- let mut out = Vec::with_capacity(lens.len() + 1);
- let mut acc: u64 = 0;
- out.push(0u64);
- #[allow(clippy::unnecessary_cast)]
- for &l in lens.as_slice::() {
- acc += l as u64;
- out.push(acc);
- }
- out
- });
+ if exact_nonnegative_length_sum(&fsst).is_some() || can_build_i32_offsets(&fsst) {
+ decode_fsst_varbinview(fsst, ctx).await
+ } else {
+ decode_fsst_host_varbinview(fsst, ctx).await
+ }
+ }
+}
- // Dispatch on the unsigned width; signed and unsigned offsets of the
- // same width share an identical byte representation.
- match_each_unsigned_integer_ptype!(codes_offsets.ptype().to_unsigned(), |U| {
- decode_fsst::(fsst, codes_offsets, lens, output_offsets, ctx).await
- })
+/// Decode FSST directly into a device-resident canonical `VarBinView` array.
+async fn decode_fsst_varbinview(
+ fsst: FSSTArray,
+ ctx: &mut CudaExecutionCtx,
+) -> VortexResult {
+ let dtype = fsst.dtype().clone();
+ let validity = fsst.codes().validity()?;
+ let len = fsst.len();
+ let lens = fsst
+ .uncompressed_lengths()
+ .clone()
+ .execute_cuda(ctx)
+ .await?
+ .into_primitive();
+ let codes_offsets = fsst
+ .codes()
+ .offsets()
+ .clone()
+ .execute_cuda(ctx)
+ .await?
+ .into_primitive();
+ let I32Offsets {
+ buffer: output_offsets,
+ total: total_size,
+ } = fsst_i32_offsets(&fsst, lens, ctx).await?;
+
+ if total_size == 0 {
+ let views = ctx.copy_to_device(vec![0i128; len])?.await?;
+ return Ok(Canonical::VarBinView(unsafe {
+ VarBinViewArray::new_handle_unchecked(views, Arc::from([]), dtype, validity)
+ }));
}
+
+ match_each_unsigned_integer_ptype!(codes_offsets.ptype().to_unsigned(), |U| {
+ decode_fsst_varbinview_typed::(fsst, codes_offsets, output_offsets, total_size, ctx)
+ .await
+ })
+}
+
+async fn decode_fsst_varbinview_typed(
+ fsst: FSSTArray,
+ codes_offsets: PrimitiveArray,
+ output_offsets: BufferHandle,
+ total_size: usize,
+ ctx: &mut CudaExecutionCtx,
+) -> VortexResult
+where
+ U: NativePType + DeviceRepr + Send + Sync + 'static,
+{
+ let dtype = fsst.dtype().clone();
+ let validity = fsst.codes().validity()?;
+ let num_strings = fsst.len();
+ let num_strings_u64 = u64::try_from(num_strings)?;
+ let symbols_u64 = fsst
+ .symbols()
+ .iter()
+ .map(|symbol| symbol.to_u64())
+ .collect::>();
+ let symbol_lengths = fsst.padded_symbol_lengths().slice(0..fsst.n_symbols());
+ let codes_bytes_handle = fsst.codes_bytes_handle().clone();
+ let PrimitiveDataParts {
+ buffer: codes_offsets_buffer,
+ ..
+ } = codes_offsets.into_data_parts();
+ let (validity_bit_offset, validity_bits) = cuda_validity(&validity, num_strings, ctx).await?;
+
+ let symbols = ctx.stream().copy_to_device_sync(&symbols_u64)?;
+ let symbol_lengths = ctx.stream().copy_to_device_sync(symbol_lengths.as_ref())?;
+ let validity_device = ctx.ensure_on_device_sync(validity_bits)?;
+ let (codes_bytes, codes_offsets) = futures::try_join!(
+ ctx.ensure_on_device(codes_bytes_handle),
+ ctx.ensure_on_device(codes_offsets_buffer),
+ )?;
+
+ let mut output = ctx.device_alloc::(total_size)?;
+ let mut views = ctx.device_alloc::(num_strings)?;
+ let codes_bytes_view = codes_bytes.cuda_view::()?;
+ let codes_offsets_view = codes_offsets.cuda_view::()?;
+ let symbols_view = symbols.cuda_view::()?;
+ let symbol_lengths_view = symbol_lengths.cuda_view::()?;
+ let output_offsets_view = output_offsets.cuda_view::()?;
+ let validity_view = validity_device.cuda_view::()?;
+ let ptype = U::PTYPE.to_string();
+ let cuda_function = ctx.load_function_with_suffixes("fsst", &["varbinview", &ptype])?;
+
+ ctx.launch_kernel(&cuda_function, num_strings, |args| {
+ args.arg(&codes_bytes_view)
+ .arg(&codes_offsets_view)
+ .arg(&symbols_view)
+ .arg(&symbol_lengths_view)
+ .arg(&output_offsets_view)
+ .arg(&validity_view)
+ .arg(&validity_bit_offset)
+ .arg(&mut output)
+ .arg(&mut views)
+ .arg(&num_strings_u64);
+ })?;
+
+ let views = BufferHandle::new_device(Arc::new(CudaDeviceBuffer::new(views)));
+ let values = BufferHandle::new_device(Arc::new(CudaDeviceBuffer::new(output)));
+ Ok(Canonical::VarBinView(unsafe {
+ VarBinViewArray::new_handle_unchecked(views, Arc::from([values]), dtype, validity)
+ }))
}
/// Decode FSST directly into Arrow-compatible i32 offsets and contiguous values on device.
@@ -183,7 +263,7 @@ pub(crate) async fn decode_fsst_varbin(
let I32Offsets {
buffer: output_offsets,
total: total_size,
- } = i32_offsets_from_lengths(lens, ctx).await?;
+ } = fsst_i32_offsets(&fsst, lens, ctx).await?;
if total_size == 0 {
let allocation = CudaDeviceBuffer::new(ctx.device_alloc::(1)?);
@@ -229,10 +309,10 @@ where
} = codes_offsets.into_data_parts();
let (validity_bit_offset, validity_bits) = cuda_validity(&validity, len, ctx).await?;
- let (symbols, symbol_lengths, validity_device, codes_bytes, codes_offsets) = futures::try_join!(
- ctx.copy_to_device(symbols_u64)?,
- ctx.copy_to_device(symbol_lengths)?,
- ctx.ensure_on_device(validity_bits),
+ let symbols = ctx.stream().copy_to_device_sync(&symbols_u64)?;
+ let symbol_lengths = ctx.stream().copy_to_device_sync(symbol_lengths.as_ref())?;
+ let validity_device = ctx.ensure_on_device_sync(validity_bits)?;
+ let (codes_bytes, codes_offsets) = futures::try_join!(
ctx.ensure_on_device(codes_bytes_handle),
ctx.ensure_on_device(codes_offsets_buffer),
)?;
@@ -277,6 +357,84 @@ where
})
}
+async fn fsst_i32_offsets(
+ fsst: &FSSTArray,
+ lengths: PrimitiveArray,
+ ctx: &mut CudaExecutionCtx,
+) -> VortexResult {
+ if let Some(total) = exact_nonnegative_length_sum(fsst) {
+ return Ok(I32Offsets {
+ buffer: i32_offsets_from_known_lengths(lengths, ctx).await?,
+ total,
+ });
+ }
+
+ i32_offsets_from_lengths(lengths, ctx).await
+}
+
+fn exact_nonnegative_length_sum(fsst: &FSSTArray) -> Option {
+ let stats = fsst.uncompressed_lengths().statistics();
+ let Precision::Exact(min) = stats.get(Stat::Min) else {
+ return None;
+ };
+ let Precision::Exact(sum) = stats.get(Stat::Sum) else {
+ return None;
+ };
+ let min = i64::try_from(&min).ok()?;
+ let total = usize::try_from(&sum).ok()?;
+ (min >= 0 && total <= i32::MAX as usize).then_some(total)
+}
+
+fn can_build_i32_offsets(fsst: &FSSTArray) -> bool {
+ let max_length = match fsst.uncompressed_lengths().dtype().as_ptype() {
+ PType::U8 => u8::MAX as usize,
+ PType::U16 => u16::MAX as usize,
+ PType::U32 => u32::MAX as usize,
+ PType::U64 => usize::MAX,
+ _ => return false,
+ };
+ fsst.len()
+ .checked_mul(max_length)
+ .is_some_and(|max_total| max_total <= i32::MAX as usize)
+}
+
+async fn decode_fsst_host_varbinview(
+ fsst: FSSTArray,
+ ctx: &mut CudaExecutionCtx,
+) -> VortexResult {
+ let lens = fsst
+ .uncompressed_lengths()
+ .clone()
+ .execute_cuda(ctx)
+ .await?
+ .into_host()
+ .await?
+ .into_primitive();
+ let codes_offsets = fsst
+ .codes()
+ .offsets()
+ .clone()
+ .execute_cuda(ctx)
+ .await?
+ .into_primitive();
+
+ let output_offsets: Vec = match_each_integer_ptype!(lens.ptype(), |P| {
+ let mut out = Vec::with_capacity(lens.len() + 1);
+ let mut acc: u64 = 0;
+ out.push(0u64);
+ #[allow(clippy::unnecessary_cast)]
+ for &length in lens.as_slice::() {
+ acc += length as u64;
+ out.push(acc);
+ }
+ out
+ });
+
+ match_each_unsigned_integer_ptype!(codes_offsets.ptype().to_unsigned(), |U| {
+ decode_fsst::(fsst, codes_offsets, lens, output_offsets, ctx).await
+ })
+}
+
async fn decode_fsst(
fsst: FSSTArray,
codes_offsets: PrimitiveArray,
diff --git a/vortex-cuda/src/kernel/encodings/runend.rs b/vortex-cuda/src/kernel/encodings/runend.rs
index 36ceb8c7b8b..bf5c295c54b 100644
--- a/vortex-cuda/src/kernel/encodings/runend.rs
+++ b/vortex-cuda/src/kernel/encodings/runend.rs
@@ -10,8 +10,10 @@ use tracing::instrument;
use vortex::array::ArrayRef;
use vortex::array::Canonical;
use vortex::array::IntoArray;
+use vortex::array::arrays::BoolArray;
use vortex::array::arrays::ConstantArray;
use vortex::array::arrays::PrimitiveArray;
+use vortex::array::arrays::bool::BoolDataParts;
use vortex::array::arrays::primitive::PrimitiveDataParts;
use vortex::array::buffer::BufferHandle;
use vortex::array::match_each_native_ptype;
@@ -149,10 +151,36 @@ async fn decode_runend_typed {
unreachable!("AllInvalid should be handled by RunEndExecutor::execute")
}
- Validity::Array(_) => {
- vortex_bail!(
- "RunEnd GPU decoding does not yet support per-element validity in values; falling back to CPU"
- );
+ Validity::Array(array) => {
+ let values_validity = array.execute_cuda(ctx).await?.into_bool();
+ let BoolDataParts { bits, meta } = values_validity.into_data().into_parts(num_runs);
+ let values_validity_device = ctx.ensure_on_device(bits).await?;
+ let values_validity_view = values_validity_device.cuda_view::()?;
+ let output_bytes = output_len.div_ceil(8);
+ let mut output_validity = ctx.device_alloc::(output_bytes)?;
+ let validity_kernel =
+ ctx.load_function_with_suffixes("runend", &["validity", &E::PTYPE.to_string()])?;
+ let values_validity_offset = u64::try_from(meta.offset())?;
+
+ ctx.launch_kernel(&validity_kernel, output_bytes, |args| {
+ args.arg(&ends_view)
+ .arg(&num_runs_u64)
+ .arg(&values_validity_view)
+ .arg(&values_validity_offset)
+ .arg(&offset_u64)
+ .arg(&output_len_u64)
+ .arg(&mut output_validity);
+ })?;
+
+ Validity::Array(
+ BoolArray::new_handle(
+ BufferHandle::new_device(Arc::new(CudaDeviceBuffer::new(output_validity))),
+ 0,
+ output_len,
+ Validity::NonNullable,
+ )
+ .into_array(),
+ )
}
};
@@ -182,7 +210,6 @@ mod tests {
use super::*;
use crate::CanonicalCudaExt;
- use crate::executor::CudaArrayExt;
use crate::session::CudaSession;
fn make_runend_array(ends: Vec, values: Vec, ctx: &mut ExecutionCtx) -> RunEndArray
@@ -303,7 +330,7 @@ mod tests {
}
#[crate::test]
- async fn test_cuda_runend_nullable_values_falls_back_to_cpu() -> VortexResult<()> {
+ async fn test_cuda_runend_nullable_values() -> VortexResult<()> {
let mut ctx = vortex_array::array_session().create_execution_ctx();
let mut cuda_ctx = CudaSession::create_execution_ctx(&crate::cuda_session())
.vortex_expect("failed to create execution context");
@@ -312,19 +339,26 @@ mod tests {
let ends_array =
PrimitiveArray::new(Buffer::from(vec![3u32, 6, 10]), Validity::NonNullable)
.into_array();
- let validity =
- Validity::Array(BoolArray::from_iter([true, false, true].into_iter()).into_array());
- let values_array =
- PrimitiveArray::new(Buffer::from(vec![10i32, 0, 30]), validity).into_array();
- let runend_array = RunEnd::new(ends_array, values_array, cuda_ctx.execution_ctx());
-
- // execute_cuda should fall back to CPU and still produce the correct result.
- let gpu_result = runend_array
- .clone()
- .into_array()
- .execute_cuda(&mut cuda_ctx)
+ // Slice the validity to exercise a non-zero input bit offset, then slice the RunEnd
+ // array to exercise a non-zero logical offset and a partial final output byte.
+ let values_validity = BoolArray::from_iter([
+ false, false, false, true, false, true, false, false, false, false,
+ ])
+ .into_array()
+ .slice(3..6)?;
+ let values_array = PrimitiveArray::new(
+ Buffer::from(vec![10i32, 0, 30]),
+ Validity::Array(values_validity),
+ )
+ .into_array();
+ // SAFETY: ends are increasing, ends/values have equal length, and [offset, offset +
+ // length) = [1, 10) is covered by the final run end.
+ let runend_array = unsafe { RunEnd::new_unchecked(ends_array, values_array, 1, 9) };
+
+ let gpu_result = RunEndExecutor
+ .execute(runend_array.clone().into_array(), &mut cuda_ctx)
.await
- .vortex_expect("GPU/CPU fallback should succeed")
+ .vortex_expect("GPU decompression failed")
.into_host()
.await?
.into_array();
diff --git a/vortex-cuda/src/kernel/mod.rs b/vortex-cuda/src/kernel/mod.rs
index 36735024c7f..b9b01714b2f 100644
--- a/vortex-cuda/src/kernel/mod.rs
+++ b/vortex-cuda/src/kernel/mod.rs
@@ -31,6 +31,7 @@ mod slice;
pub(crate) use arrays::ConstantNumericExecutor;
pub(crate) use arrays::DictExecutor;
+pub(crate) use arrays::MaskedExecutor;
pub(crate) use arrays::SharedExecutor;
pub use encodings::ZstdKernelPrep;
pub use encodings::zstd_kernel_prepare;
diff --git a/vortex-cuda/src/kernel/patches/mod.rs b/vortex-cuda/src/kernel/patches/mod.rs
index 7c651b7e507..727ef4b7611 100644
--- a/vortex-cuda/src/kernel/patches/mod.rs
+++ b/vortex-cuda/src/kernel/patches/mod.rs
@@ -27,13 +27,13 @@ use crate::CudaBufferExt;
use crate::CudaDeviceBuffer;
use crate::CudaExecutionCtx;
use crate::executor::CudaArrayExt;
-use crate::kernel::patches::gpu::ChunkOffsetType;
-use crate::kernel::patches::gpu::ChunkOffsetType_CO_U8;
-use crate::kernel::patches::gpu::ChunkOffsetType_CO_U16;
-use crate::kernel::patches::gpu::ChunkOffsetType_CO_U32;
-use crate::kernel::patches::gpu::ChunkOffsetType_CO_U64;
use crate::kernel::patches::gpu::GPUPatches;
use crate::kernel::patches::gpu::PATCH_DERIVE_INDICES_BASE;
+use crate::kernel::patches::gpu::UnsignedType;
+use crate::kernel::patches::gpu::UnsignedType_UNSIGNED_U8;
+use crate::kernel::patches::gpu::UnsignedType_UNSIGNED_U16;
+use crate::kernel::patches::gpu::UnsignedType_UNSIGNED_U32;
+use crate::kernel::patches::gpu::UnsignedType_UNSIGNED_U64;
use crate::kernel::patches::types::DevicePatches;
// Safe because `GPUPatches` contains only raw pointers, POD integers, and an enum.
@@ -44,7 +44,8 @@ impl GPUPatches {
/// `chunk_offsets` pointer is the signal `PatchesCursor` checks for.
pub(crate) const NULL_PATCHES: Self = Self {
chunk_offsets: std::ptr::null_mut(),
- chunk_offset_type: ChunkOffsetType_CO_U32,
+ chunk_offset_type: UnsignedType_UNSIGNED_U32,
+ indices_type: UnsignedType_UNSIGNED_U32,
indices: std::ptr::null_mut(),
values: std::ptr::null_mut(),
offset: 0,
@@ -55,14 +56,14 @@ impl GPUPatches {
};
}
-/// Convert a [`PType`] to the corresponding [`ChunkOffsetType`] for GPU patches.
-pub(crate) fn ptype_to_chunk_offset_type(ptype: PType) -> VortexResult {
+/// Convert a [`PType`] to the corresponding [`UnsignedType`] for GPU patches.
+pub(crate) fn ptype_to_unsigned_type(ptype: PType) -> VortexResult {
match ptype {
- PType::U8 => Ok(ChunkOffsetType_CO_U8),
- PType::U16 => Ok(ChunkOffsetType_CO_U16),
- PType::U32 => Ok(ChunkOffsetType_CO_U32),
- PType::U64 => Ok(ChunkOffsetType_CO_U64),
- _ => vortex_bail!("Invalid PType for chunk_offsets: {:?}", ptype),
+ PType::U8 => Ok(UnsignedType_UNSIGNED_U8),
+ PType::U16 => Ok(UnsignedType_UNSIGNED_U16),
+ PType::U32 => Ok(UnsignedType_UNSIGNED_U32),
+ PType::U64 => Ok(UnsignedType_UNSIGNED_U64),
+ _ => vortex_bail!("Invalid unsigned PType: {:?}", ptype),
}
}
@@ -77,7 +78,8 @@ pub(crate) fn build_gpu_patches(
match device_patches {
Some(p) => Ok(GPUPatches {
chunk_offsets: p.chunk_offsets.cuda_device_ptr()? as _,
- chunk_offset_type: ptype_to_chunk_offset_type(p.chunk_offset_ptype)?,
+ chunk_offset_type: ptype_to_unsigned_type(p.chunk_offset_ptype)?,
+ indices_type: ptype_to_unsigned_type(p.indices_ptype)?,
indices: p.indices.cuda_device_ptr()? as _,
values: p.values.cuda_device_ptr()? as _,
offset: p.offset as u32,
diff --git a/vortex-cuda/src/kernel/patches/types.rs b/vortex-cuda/src/kernel/patches/types.rs
index 3bfe2270b66..da0058a63fc 100644
--- a/vortex-cuda/src/kernel/patches/types.rs
+++ b/vortex-cuda/src/kernel/patches/types.rs
@@ -6,31 +6,29 @@
use std::mem::size_of;
use std::ops::Range;
-use num_traits::ToPrimitive;
use vortex::array::buffer::BufferHandle;
use vortex::buffer::Alignment;
-use vortex::buffer::Buffer;
-use vortex::buffer::BufferMut;
use vortex::buffer::ByteBufferMut;
use vortex::dtype::PType;
-use vortex_array::match_each_unsigned_integer_ptype;
use vortex_array::patches::PATCH_CHUNK_SIZE;
use vortex_array::patches::Patches;
use vortex_error::VortexResult;
use vortex_error::vortex_bail;
+use vortex_error::vortex_ensure;
use crate::CudaBufferExt;
use crate::CudaExecutionCtx;
use crate::executor::CudaArrayExt;
use crate::kernel::patches::gpu::GPUPatches;
use crate::kernel::patches::gpu::PATCH_DERIVE_INDICES_BASE;
-use crate::kernel::patches::ptype_to_chunk_offset_type;
+use crate::kernel::patches::ptype_to_unsigned_type;
/// A set of device-resident patches.
pub struct DevicePatches {
pub(crate) chunk_offsets: BufferHandle,
pub(crate) chunk_offset_ptype: PType,
pub(crate) indices: BufferHandle,
+ pub(crate) indices_ptype: PType,
pub(crate) values: BufferHandle,
pub(crate) offset: usize,
pub(crate) offset_within_chunk: usize,
@@ -55,6 +53,12 @@ pub(crate) async fn load_device_patches(
ctx: &mut CudaExecutionCtx,
) -> VortexResult {
let offset = patches.offset();
+ vortex_ensure!(
+ offset
+ .checked_add(patches.array_len())
+ .is_some_and(|end| end <= u32::MAX as usize),
+ "CUDA patches require offset + array length to fit in u32"
+ );
let offset_within_chunk = patches.offset_within_chunk().unwrap_or_default();
// Get or compute chunk_offsets
let Some(co) = patches.chunk_offsets() else {
@@ -68,7 +72,7 @@ pub(crate) async fn load_device_patches(
(co_canonical.buffer_handle().clone(), ptype, len)
};
- // Load indices - must be converted to u32 for GPU use
+ // Load indices at their native width.
let indices = patches
.indices()
.clone()
@@ -76,23 +80,7 @@ pub(crate) async fn load_device_patches(
.await?
.into_primitive();
let indices_ptype = indices.ptype();
- #[expect(clippy::expect_used)]
- let indices = if indices_ptype == PType::U32 {
- indices.buffer_handle().clone()
- } else {
- // Convert indices to u32
- let indices_buf = indices.buffer_handle().to_host().await;
- let indices_u32 = match_each_unsigned_integer_ptype!(indices_ptype, |I| {
- let src: Buffer = Buffer::from_byte_buffer(indices_buf);
- let mut dst: BufferMut = BufferMut::with_capacity(src.len());
- for &idx in src.as_slice() {
- // Indices are limited to u32 range for GPU
- dst.push(idx.to_u32().expect("index should fit in u32"));
- }
- dst.freeze()
- });
- BufferHandle::new_host(indices_u32.into_byte_buffer())
- };
+ let indices = indices.buffer_handle().clone();
// Load values
let values = patches
@@ -113,6 +101,7 @@ pub(crate) async fn load_device_patches(
chunk_offsets,
chunk_offset_ptype,
indices,
+ indices_ptype,
values,
offset,
offset_within_chunk,
@@ -134,7 +123,8 @@ fn build_gpu_patches(
// chunk_offset_type and indices) which would be UB when serialized.
let mut gpu_patches: GPUPatches = unsafe { std::mem::zeroed() };
gpu_patches.chunk_offsets = dp.chunk_offsets.cuda_device_ptr()? as _;
- gpu_patches.chunk_offset_type = ptype_to_chunk_offset_type(dp.chunk_offset_ptype)?;
+ gpu_patches.chunk_offset_type = ptype_to_unsigned_type(dp.chunk_offset_ptype)?;
+ gpu_patches.indices_type = ptype_to_unsigned_type(dp.indices_ptype)?;
gpu_patches.indices = dp.indices.cuda_device_ptr()? as _;
gpu_patches.values = dp.values.cuda_device_ptr()? as _;
gpu_patches.offset = dp.offset as u32;
diff --git a/vortex-cuda/src/lib.rs b/vortex-cuda/src/lib.rs
index 3c712d20fb8..b0f31863214 100644
--- a/vortex-cuda/src/lib.rs
+++ b/vortex-cuda/src/lib.rs
@@ -48,6 +48,7 @@ use kernel::FSSTExecutor;
use kernel::FilterExecutor;
use kernel::FoRExecutor;
pub use kernel::LaunchStrategy;
+use kernel::MaskedExecutor;
use kernel::RunEndExecutor;
use kernel::SharedExecutor;
pub use kernel::TracingLaunchStrategy;
@@ -73,6 +74,7 @@ use vortex::array::ArrayVTable;
use vortex::array::arrays::Constant;
use vortex::array::arrays::Dict;
use vortex::array::arrays::Filter;
+use vortex::array::arrays::Masked;
use vortex::array::arrays::Shared;
use vortex::array::arrays::Slice;
use vortex::encodings::alp::ALP;
@@ -118,6 +120,7 @@ pub fn initialize_cuda(session: &CudaSession) {
session.register_kernel(Shared.id(), &SharedExecutor);
session.register_kernel(FoR.id(), &FoRExecutor);
session.register_kernel(FSST.id(), &FSSTExecutor);
+ session.register_kernel(Masked.id(), &MaskedExecutor);
session.register_kernel(RunEnd.id(), &RunEndExecutor);
session.register_kernel(Sequence.id(), &SequenceExecutor);
session.register_kernel(ZigZag.id(), &ZigZagExecutor);
diff --git a/vortex-cuda/src/session.rs b/vortex-cuda/src/session.rs
index a5410db0c99..06edbdcce92 100644
--- a/vortex-cuda/src/session.rs
+++ b/vortex-cuda/src/session.rs
@@ -114,6 +114,30 @@ impl CudaSession {
}
}
+ /// Creates a single-stream CUDA session using device 0, with event tracking disabled.
+ ///
+ /// Every execution context created from this session shares the same stream. This avoids
+ /// cudarc's per-buffer events, which are only needed to synchronize buffer use across streams.
+ pub fn try_single_stream() -> VortexResult {
+ // cudarc panics rather than returning an error when the CUDA driver library cannot be
+ // loaded, so catch any unwind here to uphold this constructor's no-panic contract.
+ match catch_unwind(AssertUnwindSafe(|| -> VortexResult {
+ let context = CudaContext::new(0)
+ .map_err(|err| vortex_err!("failed to initialize CUDA device 0: {err}"))?;
+ // SAFETY: this context is private to a session whose pool contains exactly one stream,
+ // and event tracking is disabled before any device buffers can be allocated.
+ unsafe { context.disable_event_tracking() };
+ let this = Self::with_stream_pool_capacity(context, 1);
+ initialize_cuda(&this);
+ Ok(this)
+ })) {
+ Ok(result) => result,
+ Err(_) => Err(vortex_err!(
+ "failed to initialize CUDA: the driver library is unavailable"
+ )),
+ }
+ }
+
/// Creates a new CUDA execution context.
pub fn create_execution_ctx(
vortex_session: &vortex::session::VortexSession,