@@ -10,11 +10,13 @@ use tracing::instrument;
1010use vortex:: array:: ArrayRef ;
1111use vortex:: array:: Canonical ;
1212use vortex:: array:: IntoArray ;
13+ use vortex:: array:: arrays:: BoolArray ;
1314use vortex:: array:: arrays:: DecimalArray ;
1415use vortex:: array:: arrays:: Dict ;
1516use vortex:: array:: arrays:: DictArray ;
1617use vortex:: array:: arrays:: PrimitiveArray ;
1718use vortex:: array:: arrays:: VarBinViewArray ;
19+ use vortex:: array:: arrays:: bool:: BoolDataParts ;
1820use vortex:: array:: arrays:: decimal:: DecimalDataParts ;
1921use vortex:: array:: arrays:: dict:: DictArraySlotsExt ;
2022use vortex:: array:: arrays:: primitive:: PrimitiveDataParts ;
@@ -29,6 +31,7 @@ use vortex::dtype::NativePType;
2931use vortex:: error:: VortexExpect ;
3032use vortex:: error:: VortexResult ;
3133use vortex:: error:: vortex_bail;
34+ use vortex:: error:: vortex_ensure;
3235
3336use crate :: CudaBufferExt ;
3437use crate :: CudaDeviceBuffer ;
@@ -55,6 +58,8 @@ impl CudaExecute for DictExecutor {
5558
5659 let values_dtype = dict_array. values ( ) . dtype ( ) . clone ( ) ;
5760 match & values_dtype {
61+ // Nullable dictionary values expose their logical validity as a lazy `Dict<bool>`.
62+ DType :: Bool ( ..) => execute_dict_bool ( dict_array, ctx) . await ,
5863 DType :: Decimal ( ..) => execute_dict_decimal ( dict_array, ctx) . await ,
5964 DType :: Primitive ( ..) => execute_dict_prim ( dict_array, ctx) . await ,
6065 DType :: Utf8 ( ..) | DType :: Binary ( ..) => execute_dict_varbinview ( dict_array, ctx) . await ,
@@ -86,6 +91,80 @@ async fn execute_dict_prim(dict: DictArray, ctx: &mut CudaExecutionCtx) -> Vorte
8691 } )
8792}
8893
94+ /// Gather bit-packed boolean values through dictionary codes.
95+ ///
96+ /// This path is especially important for dictionary validity. When dictionary values are
97+ /// nullable, `Dict::validity()` represents `take(values_validity, codes)` lazily as a `Dict<bool>`.
98+ /// Materializing that validity on the GPU therefore requires a boolean dictionary gather even
99+ /// when the user-visible array contains strings or another non-boolean type.
100+ async fn execute_dict_bool ( dict : DictArray , ctx : & mut CudaExecutionCtx ) -> VortexResult < Canonical > {
101+ let values = dict. values ( ) . clone ( ) . execute_cuda ( ctx) . await ?. into_bool ( ) ;
102+ let codes = dict
103+ . codes ( )
104+ . clone ( )
105+ . execute_cuda ( ctx)
106+ . await ?
107+ . into_primitive ( ) ;
108+ let codes_ptype = codes. ptype ( ) ;
109+
110+ match_each_integer_ptype ! ( codes_ptype, |I | {
111+ execute_dict_bool_typed:: <I >( values, codes, ctx) . await
112+ } )
113+ }
114+
115+ async fn execute_dict_bool_typed < I : DeviceRepr + NativePType > (
116+ values : BoolArray ,
117+ codes : PrimitiveArray ,
118+ ctx : & mut CudaExecutionCtx ,
119+ ) -> VortexResult < Canonical > {
120+ vortex_ensure ! ( !codes. is_empty( ) , "cannot CUDA-decode an empty dictionary" ) ;
121+ let codes_len = codes. len ( ) ;
122+
123+ let values_len = values. len ( ) ;
124+ let values_validity = values. validity ( ) ?;
125+ let BoolDataParts {
126+ bits : values_buffer,
127+ meta : values_meta,
128+ } = values. into_data ( ) . into_parts ( values_len) ;
129+ let output_validity = values_validity. take ( & codes. clone ( ) . into_array ( ) ) ?;
130+ let PrimitiveDataParts {
131+ buffer : codes_buffer,
132+ ..
133+ } = codes. into_data_parts ( ) ;
134+
135+ let values_device = ctx. ensure_on_device ( values_buffer) . await ?;
136+ let codes_device = ctx. ensure_on_device ( codes_buffer) . await ?;
137+
138+ // Each CUDA thread owns complete output bytes, avoiding races between threads writing
139+ // different bits in the same byte. The kernel handles the final partial byte explicitly.
140+ let output_bytes = codes_len. div_ceil ( 8 ) ;
141+ let output_slice = ctx. device_alloc :: < u8 > ( output_bytes) ?;
142+ let output_device = CudaDeviceBuffer :: new ( output_slice) ;
143+
144+ let values_view = values_device. cuda_view :: < u8 > ( ) ?;
145+ let codes_view = codes_device. cuda_view :: < I > ( ) ?;
146+ let output_view = output_device. as_view :: < u8 > ( ) ;
147+ let codes_len_u64 = codes_len as u64 ;
148+ let values_offset_u64 = values_meta. offset ( ) as u64 ;
149+
150+ let codes_ptype = I :: PTYPE . to_string ( ) ;
151+ let kernel_function = ctx. load_function_with_suffixes ( "dict" , & [ "bool" , & codes_ptype] ) ?;
152+ ctx. launch_kernel ( & kernel_function, output_bytes, |args| {
153+ args. arg ( & codes_view)
154+ . arg ( & codes_len_u64)
155+ . arg ( & values_view)
156+ . arg ( & values_offset_u64)
157+ . arg ( & output_view) ;
158+ } ) ?;
159+
160+ Ok ( Canonical :: Bool ( BoolArray :: new_handle (
161+ BufferHandle :: new_device ( Arc :: new ( output_device) ) ,
162+ 0 ,
163+ codes_len,
164+ output_validity,
165+ ) ) )
166+ }
167+
89168async fn execute_dict_prim_typed < V : DeviceRepr + NativePType , I : DeviceRepr + NativePType > (
90169 values : PrimitiveArray ,
91170 codes : PrimitiveArray ,
@@ -298,6 +377,7 @@ async fn execute_dict_varbinview(
298377#[ cfg( test) ]
299378mod tests {
300379 use vortex:: array:: IntoArray ;
380+ use vortex:: array:: arrays:: BoolArray ;
301381 use vortex:: array:: arrays:: DecimalArray ;
302382 use vortex:: array:: arrays:: DictArray ;
303383 use vortex:: array:: arrays:: PrimitiveArray ;
@@ -323,6 +403,43 @@ mod tests {
323403 ) )
324404 }
325405
406+ #[ crate :: test]
407+ async fn test_cuda_dict_bool_gathers_packed_validity_bits ( ) -> VortexResult < ( ) > {
408+ let mut ctx = vortex_array:: array_session ( ) . create_execution_ctx ( ) ;
409+ let mut cuda_ctx = CudaSession :: create_execution_ctx ( & crate :: cuda_session ( ) )
410+ . vortex_expect ( "failed to create execution context" ) ;
411+
412+ // Slicing leaves the dictionary values at a non-zero bit offset. Thirteen codes also
413+ // exercise a final partial output byte.
414+ let values = BoolArray :: from_iter ( [
415+ false , true , false , true , false , true , true , false , true , false ,
416+ ] )
417+ . into_array ( )
418+ . slice ( 3 ..8 ) ?;
419+ let codes = PrimitiveArray :: new (
420+ Buffer :: from ( vec ! [ 0u8 , 1 , 2 , 3 , 4 , 3 , 2 , 1 , 0 , 4 , 1 , 3 , 0 ] ) ,
421+ NonNullable ,
422+ ) ;
423+ let expected = DictArray :: try_new ( codes. clone ( ) . into_array ( ) , values. clone ( ) ) ?. into_array ( ) ;
424+
425+ let codes_handle = cuda_ctx
426+ . ensure_on_device ( codes. buffer_handle ( ) . clone ( ) )
427+ . await ?;
428+ let device_codes =
429+ PrimitiveArray :: from_buffer_handle ( codes_handle, codes. ptype ( ) , codes. validity ( ) ?) ;
430+ let dict = DictArray :: try_new ( device_codes. into_array ( ) , values) ?. into_array ( ) ;
431+
432+ let actual = DictExecutor
433+ . execute ( dict, & mut cuda_ctx)
434+ . await ?
435+ . into_host ( )
436+ . await ?
437+ . into_bool ( ) ;
438+
439+ assert_arrays_eq ! ( actual. into_array( ) , expected, & mut ctx) ;
440+ Ok ( ( ) )
441+ }
442+
326443 #[ crate :: test]
327444 async fn test_cuda_dict_u32_values_u8_codes ( ) -> VortexResult < ( ) > {
328445 let mut ctx = vortex_array:: array_session ( ) . create_execution_ctx ( ) ;
0 commit comments