Skip to content

Commit d45538e

Browse files
authored
fix[gpu]: handle sliced BP arrays in CUDA (#7912)
Signed-off-by: Alexander Droste <alexander.droste@protonmail.com>
1 parent 5d2ae8b commit d45538e

8 files changed

Lines changed: 205 additions & 93 deletions

File tree

vortex-cuda/kernels/src/patches.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ typedef enum { CO_U8 = 0, CO_U16 = 1, CO_U32 = 2, CO_U64 = 3 } ChunkOffsetType;
1414

1515
static const uint32_t PATCH_DERIVE_INDICES_BASE = UINT32_MAX;
1616

17-
/// GPU-resident patches for fused exception patching during bit-unpacking.
17+
/// GPU-resident patches for fused CUDA exception patching.
1818
///
1919
/// Patches are stored in sorted order within each chunk. The chunk_offsets
2020
/// array maps each chunk to the start of its range in the indices/values arrays.

vortex-cuda/src/dynamic_dispatch/mod.rs

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2680,11 +2680,6 @@ mod tests {
26802680
#[case] len: usize,
26812681
#[case] slice_range: Option<Range<usize>>,
26822682
) -> VortexResult<()> {
2683-
// TODO(#7839): BitPacked SliceReduce returns None when patches are present,
2684-
// producing SliceArray instead of BitPacked. CUDA cannot handle this yet.
2685-
if true {
2686-
return Ok(());
2687-
}
26882683
let bit_width: u8 = 4;
26892684
let max_val = (1u32 << bit_width) - 1;
26902685
let values: Vec<u32> = (0..len)
@@ -2766,12 +2761,6 @@ mod tests {
27662761

27672762
#[crate::test]
27682763
async fn test_for_bitpacked_with_patches_sliced() -> VortexResult<()> {
2769-
// TODO(#7839): BitPacked SliceReduce returns None when patches are present,
2770-
// producing SliceArray instead of BitPacked. CUDA cannot handle this yet.
2771-
if true {
2772-
return Ok(());
2773-
}
2774-
27752764
let len = 5000;
27762765
let bit_width: u8 = 6;
27772766
let reference = 42u32;

vortex-cuda/src/dynamic_dispatch/plan_builder.rs

Lines changed: 45 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ use super::tag_to_ptype;
5252
use crate::CudaBufferExt;
5353
use crate::CudaExecutionCtx;
5454
use crate::executor::CudaDispatchMode;
55+
use crate::kernel::bitpacked_slice_view;
5556
use crate::kernel::load_patches_to_gpu;
5657

5758
/// A plan whose source buffers have been copied to the device, ready for kernel launch.
@@ -154,12 +155,13 @@ pub fn has_standalone_kernel(array: &ArrayRef) -> bool {
154155

155156
/// Patch payload attached to the op that consumes it.
156157
///
157-
/// `slice` is a logical output range to apply when materializing the patch descriptor on the GPU.
158-
/// This lets the planner avoid calling `Patches::slice` when patch metadata may already be device-resident.
158+
/// `range` is the logical output range to apply when materializing the patch descriptor on the GPU.
159+
/// This lets the planner avoid calling `Patches::slice` when patch metadata may already be
160+
/// device-resident.
159161
#[derive(Clone)]
160162
struct PlanPatches {
161163
patches: Patches,
162-
slice: Option<Range<usize>>,
164+
range: Option<Range<usize>>,
163165
}
164166

165167
/// An unmaterialized stage: a source op, scalar ops, and optional source buffer reference.
@@ -191,6 +193,11 @@ impl Stage {
191193
source_ptype,
192194
}
193195
}
196+
197+
fn with_source_patches(mut self, source_patches: Option<PlanPatches>) -> Self {
198+
self.source_patches = source_patches;
199+
self
200+
}
194201
}
195202

196203
type SmemByteOffset = u32;
@@ -409,7 +416,7 @@ impl FusedPlan {
409416
// Upload source patches (e.g. BitPacked exceptions).
410417
if let Some(patches) = &stage.source_patches {
411418
let (ptr, bufs) =
412-
load_patches_to_gpu(&patches.patches, patches.slice.clone(), ctx).await?;
419+
load_patches_to_gpu(&patches.patches, patches.range.clone(), ctx).await?;
413420
source.params.bitunpack.patches_ptr = ptr;
414421
device_buffers.extend(bufs);
415422
}
@@ -419,7 +426,7 @@ impl FusedPlan {
419426
for (mut op, patches) in stage.scalar_ops.clone() {
420427
if let Some(patches) = &patches {
421428
let (ptr, bufs) =
422-
load_patches_to_gpu(&patches.patches, patches.slice.clone(), ctx).await?;
429+
load_patches_to_gpu(&patches.patches, patches.range.clone(), ctx).await?;
423430
op.params.alp.patches_ptr = ptr;
424431
device_buffers.extend(bufs);
425432
}
@@ -503,9 +510,8 @@ impl FusedPlan {
503510

504511
/// SliceArray → resolve the slice via reduce/execute rules.
505512
///
506-
/// When the plan builder encounters a `SliceArray`, it resolves the slice
507-
/// by invoking the child's `reduce_parent`. If that fails (e.g. ALP
508-
/// doesn't implement it), we manually slice the child's sub-arrays.
513+
/// When the plan builder encounters a `SliceArray`, it first asks the child to reduce the
514+
/// slice. If reduction fails, the planner falls back to encoding-specific handling.
509515
fn walk_slice(
510516
&mut self,
511517
array: ArrayRef,
@@ -518,6 +524,30 @@ impl FusedPlan {
518524
return self.walk(reduced, pending_subtrees);
519525
}
520526

527+
// BitPacked with patches does not reduce through Slice. Slice the
528+
// packed buffer here, and defer patch slicing to CUDA materialization.
529+
if child.encoding_id() == BitPacked.id() {
530+
let bp = child.as_::<BitPacked>();
531+
let offset = slice_arr.data().slice_range().start;
532+
let len = array.len();
533+
let (packed, bitpacked_offset, patch_range) = bitpacked_slice_view(bp, offset, len)?;
534+
535+
let source_ptype = ptype_to_tag(PType::try_from(bp.dtype()).map_err(|_| {
536+
vortex_err!("BitPacked must have primitive dtype, got {:?}", bp.dtype())
537+
})?);
538+
let buf_index = self.source_buffers.len();
539+
self.source_buffers.push(Some(packed));
540+
return Ok(Stage::new(
541+
SourceOp::bitunpack(bp.bit_width(), bitpacked_offset),
542+
Some(buf_index),
543+
source_ptype,
544+
)
545+
.with_source_patches(bp.patches().map(|patches| PlanPatches {
546+
patches,
547+
range: Some(patch_range),
548+
})));
549+
}
550+
521551
// ALP doesn't implement reduce_parent. Slice the encoded child here,
522552
// and defer patch slicing to CUDA materialization so device-resident
523553
// patch buffers stay on device.
@@ -530,7 +560,7 @@ impl FusedPlan {
530560
sliced_encoded,
531561
alp.patches().map(|patches| PlanPatches {
532562
patches,
533-
slice: Some(offset..offset + len),
563+
range: Some(offset..offset + len),
534564
}),
535565
alp.exponents(),
536566
pending_subtrees,
@@ -562,16 +592,15 @@ impl FusedPlan {
562592
})?);
563593
let buf_index = self.source_buffers.len();
564594
self.source_buffers.push(Some(bp.packed().clone()));
565-
let mut stage = Stage::new(
595+
Ok(Stage::new(
566596
SourceOp::bitunpack(bp.bit_width(), bp.offset()),
567597
Some(buf_index),
568598
source_ptype,
569-
);
570-
stage.source_patches = bp.patches().map(|patches| PlanPatches {
599+
)
600+
.with_source_patches(bp.patches().map(|patches| PlanPatches {
571601
patches,
572-
slice: None,
573-
});
574-
Ok(stage)
602+
range: None,
603+
})))
575604
}
576605

577606
fn walk_for(
@@ -629,7 +658,7 @@ impl FusedPlan {
629658
alp.encoded().clone(),
630659
alp.patches().map(|patches| PlanPatches {
631660
patches,
632-
slice: None,
661+
range: None,
633662
}),
634663
alp.exponents(),
635664
pending_subtrees,

vortex-cuda/src/kernel/encodings/bitpacked.rs

Lines changed: 119 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// SPDX-FileCopyrightText: Copyright the Vortex contributors
33

44
use std::fmt::Debug;
5+
use std::ops::Range;
56

67
use async_trait::async_trait;
78
use cudarc::driver::CudaFunction;
@@ -10,14 +11,20 @@ use cudarc::driver::LaunchConfig;
1011
use cudarc::driver::PushKernelArg;
1112
use tracing::instrument;
1213
use vortex::array::ArrayRef;
14+
use vortex::array::ArrayVTable;
15+
use vortex::array::ArrayView;
1316
use vortex::array::Canonical;
1417
use vortex::array::arrays::PrimitiveArray;
18+
use vortex::array::arrays::Slice;
19+
use vortex::array::arrays::slice::SliceArrayExt;
1520
use vortex::array::buffer::BufferHandle;
1621
use vortex::array::buffer::DeviceBufferExt;
1722
use vortex::array::match_each_integer_ptype;
23+
use vortex::array::patches::PATCH_CHUNK_SIZE;
1824
use vortex::dtype::NativePType;
1925
use vortex::encodings::fastlanes::BitPacked;
2026
use vortex::encodings::fastlanes::BitPackedArray;
27+
use vortex::encodings::fastlanes::BitPackedArrayExt;
2128
use vortex::encodings::fastlanes::BitPackedDataParts;
2229
use vortex::encodings::fastlanes::unpack_iter::BitPacked as BitPackedUnpack;
2330
use vortex::error::VortexResult;
@@ -30,14 +37,71 @@ use crate::executor::CudaExecute;
3037
use crate::executor::CudaExecutionCtx;
3138
use crate::kernel::patches::build_gpu_patches;
3239
use crate::kernel::patches::types::load_device_patches;
40+
use crate::kernel::patches::types::slice_device_patches;
3341

3442
/// CUDA decoder for bit-packed arrays.
3543
#[derive(Debug)]
3644
pub(crate) struct BitPackedExecutor;
3745

46+
/// Build the packed buffer view for decoding `Slice(BitPacked)`.
47+
///
48+
/// Bit-unpack kernels decode full FastLanes chunks, so the packed buffer is
49+
/// widened to chunk boundaries and `offset` is converted into the in-chunk
50+
/// starting position. The returned logical range is passed to patch
51+
/// materialization so exception metadata is sliced consistently.
52+
pub(crate) fn bitpacked_slice_view(
53+
bp: ArrayView<'_, BitPacked>,
54+
offset: usize,
55+
len: usize,
56+
) -> VortexResult<(BufferHandle, u16, Range<usize>)> {
57+
let patch_range = offset..offset + len;
58+
let offset_start = patch_range.start + bp.offset() as usize;
59+
let offset_stop = offset_start + len;
60+
let bitpacked_offset = offset_start % PATCH_CHUNK_SIZE;
61+
let block_start = offset_start - bitpacked_offset;
62+
let block_stop = offset_stop.div_ceil(PATCH_CHUNK_SIZE) * PATCH_CHUNK_SIZE;
63+
64+
let encoded_start = (block_start / 8) * bp.bit_width() as usize;
65+
let encoded_stop = (block_stop / 8) * bp.bit_width() as usize;
66+
67+
Ok((
68+
bp.packed().slice(encoded_start..encoded_stop),
69+
u16::try_from(bitpacked_offset)?,
70+
patch_range,
71+
))
72+
}
73+
3874
impl BitPackedExecutor {
39-
fn try_specialize(array: ArrayRef) -> Option<BitPackedArray> {
40-
array.try_downcast::<BitPacked>().ok()
75+
fn try_specialize(
76+
array: ArrayRef,
77+
) -> VortexResult<Option<(BitPackedArray, Option<Range<usize>>)>> {
78+
if let Ok(array) = array.clone().try_downcast::<BitPacked>() {
79+
return Ok(Some((array, None)));
80+
}
81+
82+
let Some(slice) = array.as_opt::<Slice>() else {
83+
return Ok(None);
84+
};
85+
let child = slice.child();
86+
if child.encoding_id() != BitPacked.id() {
87+
return Ok(None);
88+
}
89+
90+
let bp = child.as_::<BitPacked>();
91+
let offset = slice.data().slice_range().start;
92+
let len = array.len();
93+
let (packed, bitpacked_offset, patch_range) = bitpacked_slice_view(bp, offset, len)?;
94+
let sliced = BitPacked::try_new(
95+
packed,
96+
bp.ptype(bp.dtype()),
97+
child.validity()?.slice(patch_range.clone())?,
98+
bp.patches(),
99+
bp.bit_width(),
100+
len,
101+
bitpacked_offset,
102+
)?;
103+
104+
Ok(Some((sliced, Some(patch_range))))
41105
}
42106
}
43107

@@ -49,11 +113,12 @@ impl CudaExecute for BitPackedExecutor {
49113
array: ArrayRef,
50114
ctx: &mut CudaExecutionCtx,
51115
) -> VortexResult<Canonical> {
52-
let array =
53-
Self::try_specialize(array).ok_or_else(|| vortex_err!("Expected BitPackedArray"))?;
116+
let (array, patch_range) =
117+
Self::try_specialize(array)?.ok_or_else(|| vortex_err!("Expected BitPackedArray"))?;
118+
let ptype = array.ptype(array.dtype());
54119

55-
match_each_integer_ptype!(array.ptype(array.dtype()), |A| {
56-
decode_bitpacked::<A>(array, A::default(), ctx).await
120+
match_each_integer_ptype!(ptype, |A| {
121+
decode_bitpacked::<A>(array, A::default(), patch_range, ctx).await
57122
})
58123
}
59124
}
@@ -88,6 +153,7 @@ pub fn bitpacked_cuda_launch_config(output_width: usize, len: usize) -> VortexRe
88153
pub(crate) async fn decode_bitpacked<A>(
89154
array: BitPackedArray,
90155
reference: A,
156+
patch_range: Option<Range<usize>>,
91157
ctx: &mut CudaExecutionCtx,
92158
) -> VortexResult<Canonical>
93159
where
@@ -122,7 +188,11 @@ where
122188

123189
// We hold this here to keep the device buffers alive.
124190
let device_patches = if let Some(patches) = patches {
125-
Some(load_device_patches(&patches, ctx).await?)
191+
let mut device_patches = load_device_patches(&patches, ctx).await?;
192+
if let Some(range) = patch_range {
193+
slice_device_patches(&patches, range, &mut device_patches);
194+
}
195+
Some(device_patches)
126196
} else {
127197
None
128198
};
@@ -551,17 +621,53 @@ mod tests {
551621
Ok(())
552622
}
553623

624+
#[rstest]
625+
#[case::direct(None, 4096, None, 0, 4096 * 9 / 8)]
626+
#[case::mid_chunk(Some(67..3969), 3902, Some(67..3969), 67, 4096 * 9 / 8)]
627+
#[case::chunk_aligned(Some(1024..3072), 2048, Some(1024..3072), 0, 2048 * 9 / 8)]
628+
#[case::tail_chunk(Some(3000..4096), 1096, Some(3000..4096), 952, 2048 * 9 / 8)]
629+
#[crate::test]
630+
fn test_bitunpack_try_specialize_slices(
631+
#[case] range: Option<Range<usize>>,
632+
#[case] expected_len: usize,
633+
#[case] expected_patch_range: Option<Range<usize>>,
634+
#[case] expected_offset: u16,
635+
#[case] expected_packed_len: usize,
636+
) -> VortexResult<()> {
637+
let values = PrimitiveArray::new(
638+
(0u16..4096)
639+
.map(|i| if i % 1000 == 0 { 600 } else { i % 512 })
640+
.collect::<Buffer<_>>(),
641+
NonNullable,
642+
);
643+
let bitpacked = BitPacked::encode(
644+
&values.into_array(),
645+
9,
646+
&mut LEGACY_SESSION.create_execution_ctx(),
647+
)?;
648+
assert!(bitpacked.patches().is_some());
649+
let array = if let Some(range) = range {
650+
bitpacked.into_array().slice(range)?
651+
} else {
652+
bitpacked.into_array()
653+
};
654+
655+
let (specialized, patch_range) =
656+
BitPackedExecutor::try_specialize(array)?.vortex_expect("expected BitPacked input");
657+
658+
assert_eq!(specialized.len(), expected_len);
659+
assert_eq!(specialized.offset(), expected_offset);
660+
assert_eq!(specialized.packed().len(), expected_packed_len);
661+
assert_eq!(patch_range, expected_patch_range);
662+
663+
Ok(())
664+
}
665+
554666
/// Test slicing a bitpacked array with patches where the slice boundary
555667
/// falls in the middle of a chunk's patch range, creating a non-zero
556668
/// offset_within_chunk.
557669
#[crate::test]
558670
fn test_cuda_bitunpack_sliced_patches_offset_within_chunk() -> VortexResult<()> {
559-
// TODO(#7839): BitPacked SliceReduce returns None when patches are present,
560-
// producing SliceArray instead of BitPacked. CUDA cannot handle this yet.
561-
if true {
562-
return Ok(());
563-
}
564-
565671
let mut cuda_ctx = CudaSession::create_execution_ctx(&VortexSession::empty())
566672
.vortex_expect("failed to create execution context");
567673

@@ -604,12 +710,6 @@ mod tests {
604710
/// Test slicing a bitpacked array multiple times, accumulating offset_within_chunk.
605711
#[crate::test]
606712
fn test_cuda_bitunpack_double_sliced_patches() -> VortexResult<()> {
607-
// TODO(#7839): BitPacked SliceReduce returns None when patches are present,
608-
// producing SliceArray instead of BitPacked. CUDA cannot handle this yet.
609-
if true {
610-
return Ok(());
611-
}
612-
613713
let mut cuda_ctx = CudaSession::create_execution_ctx(&VortexSession::empty())
614714
.vortex_expect("failed to create execution context");
615715

@@ -664,12 +764,6 @@ mod tests {
664764
/// Test slicing to skip an entire chunk's worth of patches.
665765
#[crate::test]
666766
fn test_cuda_bitunpack_sliced_skip_first_chunk_patches() -> VortexResult<()> {
667-
// TODO(#7839): BitPacked SliceReduce returns None when patches are present,
668-
// producing SliceArray instead of BitPacked. CUDA cannot handle this yet.
669-
if true {
670-
return Ok(());
671-
}
672-
673767
let mut cuda_ctx = CudaSession::create_execution_ctx(&VortexSession::empty())
674768
.vortex_expect("failed to create execution context");
675769

0 commit comments

Comments
 (0)