Skip to main content

xlog_cuda/provider/
io.rs

1//! I/O operations: multi-column buffer creation, Arrow interop.
2
3use std::sync::Arc;
4
5use crate::{LaunchAsync, LaunchConfig};
6use xlog_core::{Result, ScalarType, Schema, XlogError};
7
8use super::{d4_kernels, pack_kernels, D4_MODULE, PACK_MODULE};
9use crate::{CudaBuffer, CudaColumn};
10
11fn host_columns_form_canonical_u32_set(schema: &Schema, columns: &[Vec<u8>], rows: usize) -> bool {
12    if rows <= 1 {
13        return true;
14    }
15    if schema.arity() == 0
16        || !(0..schema.arity()).all(|index| {
17            matches!(
18                schema.column_type(index),
19                Some(ScalarType::U32 | ScalarType::Symbol)
20            )
21        })
22    {
23        return false;
24    }
25
26    (1..rows).all(|row| {
27        columns
28            .iter()
29            .map(|column| {
30                let prior = (row - 1) * std::mem::size_of::<u32>();
31                let current = row * std::mem::size_of::<u32>();
32                let prior_value = u32::from_le_bytes(
33                    column[prior..prior + 4]
34                        .try_into()
35                        .expect("validated u32 column"),
36                );
37                let current_value = u32::from_le_bytes(
38                    column[current..current + 4]
39                        .try_into()
40                        .expect("validated u32 column"),
41                );
42                prior_value.cmp(&current_value)
43            })
44            .find(|ordering| *ordering != std::cmp::Ordering::Equal)
45            == Some(std::cmp::Ordering::Less)
46    })
47}
48
49impl super::CudaKernelProvider {
50    // ============== Buffer Helper Methods ==============
51
52    /// Create a CudaBuffer from multiple u32 column slices
53    ///
54    /// # Arguments
55    /// * `columns` - Slice of column data slices (each column as &[u32])
56    /// * `schema` - The schema for the buffer
57    ///
58    /// # Returns
59    /// A new CudaBuffer containing all columns
60    ///
61    /// # Errors
62    /// Returns `XlogError::Kernel` if upload fails or columns have mismatched lengths
63    pub fn create_buffer_from_u32_columns(
64        &self,
65        columns: &[&[u32]],
66        schema: Schema,
67    ) -> Result<CudaBuffer> {
68        if columns.is_empty() {
69            return self.create_empty_buffer(schema);
70        }
71
72        let num_rows = columns[0].len();
73        for (i, col) in columns.iter().enumerate() {
74            if col.len() != num_rows {
75                return Err(XlogError::Kernel(format!(
76                    "Column {} has {} rows but expected {}",
77                    i,
78                    col.len(),
79                    num_rows
80                )));
81            }
82        }
83
84        let mut cuda_columns = Vec::with_capacity(columns.len());
85        for col_data in columns {
86            let bytes: Vec<u8> = col_data.iter().flat_map(|v| v.to_le_bytes()).collect();
87            let mut col = self.memory.alloc::<u8>(bytes.len())?;
88            self.htod_sync_copy_into_tracked(&bytes, &mut col)
89                .map_err(|e| XlogError::Kernel(format!("Failed to upload column: {}", e)))?;
90            cuda_columns.push(col.into());
91        }
92
93        self.buffer_from_columns(cuda_columns, num_rows as u64, schema)
94    }
95
96    /// Create a buffer from multiple column slices (raw bytes)
97    ///
98    /// This is a generic version that works with any column type by accepting
99    /// raw byte slices. Each slice should contain the column data in little-endian
100    /// format with the correct size for the column's type.
101    ///
102    /// # Arguments
103    /// * `slices` - Slice of raw byte slices, one per column
104    /// * `schema` - The schema for the buffer
105    ///
106    /// # Returns
107    /// A new CudaBuffer containing all columns
108    ///
109    /// # Errors
110    /// Returns `XlogError::Kernel` if:
111    /// - Number of slices doesn't match schema arity
112    /// - Upload fails
113    pub fn create_buffer_from_slices(
114        &self,
115        slices: &[&[u8]],
116        schema: Schema,
117    ) -> Result<CudaBuffer> {
118        if slices.len() != schema.arity() {
119            return Err(XlogError::Kernel(format!(
120                "Slice count {} doesn't match schema arity {}",
121                slices.len(),
122                schema.arity()
123            )));
124        }
125
126        if slices.is_empty() {
127            return self.create_empty_buffer(schema);
128        }
129
130        let first_col_size = schema.column_type(0).map(|t| t.size_bytes()).unwrap_or(4);
131        let num_rows = slices[0].len() / first_col_size;
132
133        // Validate that all columns have consistent row counts
134        for (i, slice) in slices.iter().enumerate() {
135            let col_size = schema.column_type(i).map(|t| t.size_bytes()).unwrap_or(4);
136            let col_rows = slice.len() / col_size;
137            if col_rows != num_rows {
138                return Err(XlogError::Kernel(format!(
139                    "Column {} has {} rows but expected {} rows (based on first column)",
140                    i, col_rows, num_rows
141                )));
142            }
143            // Also verify the slice length is exactly divisible by the type size
144            if slice.len() % col_size != 0 {
145                return Err(XlogError::Kernel(format!(
146                    "Column {} slice length {} is not divisible by type size {}",
147                    i,
148                    slice.len(),
149                    col_size
150                )));
151            }
152        }
153
154        let mut columns = Vec::with_capacity(slices.len());
155
156        for (i, slice) in slices.iter().enumerate() {
157            let mut col = self.memory.alloc::<u8>(slice.len())?;
158            self.htod_sync_copy_into_tracked(slice, &mut col)
159                .map_err(|e| XlogError::Kernel(format!("Failed to upload column {}: {}", i, e)))?;
160            columns.push(col.into());
161        }
162
163        self.buffer_from_columns(columns, num_rows as u64, schema)
164    }
165
166    /// Upload a group of host-columnar relations with one payload transfer and
167    /// one row-count transfer. Each returned buffer owns disjoint views of the
168    /// shared device allocations, so dropping one relation cannot invalidate
169    /// another. Strictly sorted, unique U32/Symbol rows receive a canonical
170    /// full-row set proof after their host columns are checked.
171    pub fn create_buffers_from_host_columns(
172        &self,
173        relations: &[(Schema, Vec<Vec<u8>>, usize)],
174    ) -> Result<Vec<CudaBuffer>> {
175        if relations.is_empty() {
176            return Ok(Vec::new());
177        }
178
179        let count_capacity = relations
180            .len()
181            .checked_mul(std::mem::size_of::<u32>())
182            .ok_or_else(|| XlogError::Kernel("Host relation count size overflow".into()))?;
183        let mut payload_bytes = 0usize;
184        let mut spans = Vec::with_capacity(relations.len());
185        let mut count_bytes = Vec::with_capacity(count_capacity);
186        let mut canonical_sets = Vec::with_capacity(relations.len());
187        for (schema, columns, rows) in relations {
188            if columns.len() != schema.arity() {
189                return Err(XlogError::Kernel(format!(
190                    "Column count {} doesn't match schema arity {}",
191                    columns.len(),
192                    schema.arity()
193                )));
194            }
195            let rows_u32 = u32::try_from(*rows)
196                .map_err(|_| XlogError::Kernel(format!("Row count {} exceeds u32::MAX", rows)))?;
197            count_bytes.extend_from_slice(&rows_u32.to_le_bytes());
198
199            let mut column_spans = Vec::with_capacity(columns.len());
200            for (index, column) in columns.iter().enumerate() {
201                let width = schema
202                    .column_type(index)
203                    .map(|typ| typ.size_bytes())
204                    .unwrap_or(4);
205                let expected = rows
206                    .checked_mul(width)
207                    .ok_or_else(|| XlogError::Kernel("Host column byte length overflow".into()))?;
208                if column.len() != expected {
209                    return Err(XlogError::Kernel(format!(
210                        "Column {} has {} bytes but expected {} for {} rows",
211                        index,
212                        column.len(),
213                        expected,
214                        rows
215                    )));
216                }
217                // A later u64/f64 view may borrow this byte slice directly.
218                // Keep every column start aligned even after a shorter column.
219                let alignment = width.max(std::mem::align_of::<u64>());
220                let remainder = payload_bytes % alignment;
221                let padding = if remainder == 0 {
222                    0
223                } else {
224                    alignment - remainder
225                };
226                let start = payload_bytes.checked_add(padding).ok_or_else(|| {
227                    XlogError::Kernel("Host relation payload size overflow".into())
228                })?;
229                let end = start.checked_add(column.len()).ok_or_else(|| {
230                    XlogError::Kernel("Host relation payload size overflow".into())
231                })?;
232                column_spans.push(start..end);
233                payload_bytes = end;
234            }
235            canonical_sets.push(host_columns_form_canonical_u32_set(schema, columns, *rows));
236            spans.push(column_spans);
237        }
238
239        let mut payload = Vec::with_capacity(payload_bytes);
240        for ((_, columns, _), column_spans) in relations.iter().zip(&spans) {
241            for (column, span) in columns.iter().zip(column_spans) {
242                payload.resize(span.start, 0);
243                payload.extend_from_slice(column);
244            }
245        }
246        let payload_owner = if relations.iter().any(|(_, columns, _)| !columns.is_empty()) {
247            let mut allocation = self.memory.alloc::<u8>(payload.len())?;
248            if !payload.is_empty() {
249                self.htod_sync_copy_into_tracked(&payload, &mut allocation)?;
250            }
251            Some(allocation)
252        } else {
253            None
254        };
255        let mut counts = self.memory.alloc::<u8>(count_bytes.len())?;
256        self.htod_launch_metadata_sync_copy_into(&count_bytes, &mut counts)?;
257
258        let mut buffers = Vec::with_capacity(relations.len());
259        for (index, ((schema, _, rows), column_spans)) in relations.iter().zip(spans).enumerate() {
260            let mut columns = Vec::<CudaColumn>::with_capacity(column_spans.len());
261            for span in column_spans {
262                let column = payload_owner
263                    .as_ref()
264                    .expect("non-nullary relation has a payload owner")
265                    .try_owned_subslice::<u8>(span)
266                    .ok_or_else(|| XlogError::Kernel("Host column view is invalid".into()))?;
267                columns.push(column.into());
268            }
269            let start = index * std::mem::size_of::<u32>();
270            let count = counts
271                .try_owned_subslice::<u32>(start..start + std::mem::size_of::<u32>())
272                .ok_or_else(|| XlogError::Kernel("Host row count view is invalid".into()))?;
273            let mut buffer = CudaBuffer::from_columns_with_host_count(
274                columns,
275                *rows as u64,
276                count,
277                schema.clone(),
278                *rows as u32,
279            );
280            if canonical_sets[index] {
281                buffer.certify_canonical_full_row_set();
282            }
283            buffers.push(buffer);
284        }
285        Ok(buffers)
286    }
287
288    /// Export CudaBuffer to Arrow C Data Interface (device-resident).
289    ///
290    /// This is a zero-copy export: column buffers remain on device, and the
291    /// returned ArrowDeviceArray describes CUDA-resident memory.
292    ///
293    /// The export requires that the device row count matches the host row_cap
294    pub fn to_arrow_device_record_batch(
295        &self,
296        buffer: CudaBuffer,
297    ) -> Result<crate::arrow_device::ArrowDeviceArrayOwned> {
298        use crate::device_runtime::Access;
299        use crate::memory::{with_memory_access, DeviceRead};
300        use arrow::array::ArrayData;
301        use arrow::datatypes::{DataType, Field};
302        use arrow::ffi::to_ffi;
303
304        use crate::arrow_device::{ArrowDeviceArray, ARROW_DEVICE_CUDA};
305
306        let stream = self.device.inner().stream();
307        let _ordinary = crate::cuda_graph::reserve_uncaptured_stream(stream).map_err(|error| {
308            XlogError::Kernel(format!("Arrow export stream admission: {error}"))
309        })?;
310        let buffer = Arc::new(buffer);
311        let row_cap = buffer.num_rows();
312        let num_rows_u32 = u32::try_from(row_cap).map_err(|_| {
313            XlogError::Kernel(format!(
314                "Arrow device export supports at most {} rows, got {}",
315                u32::MAX,
316                row_cap
317            ))
318        })?;
319
320        // GPU-side assertion: device row count must match row_cap (no host reads).
321        let assert_fn = self
322            .device
323            .inner()
324            .get_func(D4_MODULE, d4_kernels::D4_ASSERT_U32_EQ)
325            .ok_or_else(|| XlogError::Kernel("d4_assert_u32_eq kernel not found".to_string()))?;
326        // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
327        unsafe {
328            assert_fn.clone().launch(
329                LaunchConfig {
330                    grid_dim: (1, 1, 1),
331                    block_dim: (1, 1, 1),
332                    shared_mem_bytes: 0,
333                },
334                (buffer.num_rows_device(), num_rows_u32),
335            )
336        }
337        .map_err(|e| XlogError::Kernel(format!("d4_assert_u32_eq failed: {}", e)))?;
338        // A null Arrow sync_event promises ready buffers, not merely a ready
339        // row count. Admit every exported column so nonblocking producer-stream
340        // dependencies are included in the wait without copying host data.
341        let accesses = buffer
342            .columns()
343            .iter()
344            .map(|column| column.device_view().access(Access::Read))
345            .collect::<crate::device_runtime::ResourceResult<Vec<_>>>()
346            .map_err(|error| XlogError::Kernel(error.to_string()))?;
347        with_memory_access(Arc::clone(stream), accesses, |_| Ok(stream.synchronize()?))
348            .map_err(|error| XlogError::Kernel(format!("Arrow producer readiness: {error}")))?;
349
350        let num_rows = usize::try_from(num_rows_u32)
351            .map_err(|_| XlogError::Kernel("Arrow device export row count overflow".to_string()))?;
352
353        let mut fields: Vec<Field> = Vec::with_capacity(buffer.arity());
354        let mut children: Vec<ArrayData> = Vec::with_capacity(buffer.arity());
355
356        for (col_idx, (name, scalar_type)) in buffer.schema().columns.iter().enumerate() {
357            let (field, child) =
358                self.build_arrow_device_child(&buffer, col_idx, name, *scalar_type, num_rows)?;
359            fields.push(field);
360            children.push(child);
361        }
362
363        let struct_type = DataType::Struct(fields.into());
364        let struct_data = ArrayData::builder(struct_type)
365            .len(num_rows)
366            .child_data(children)
367            .build()
368            .map_err(|e| XlogError::Kernel(format!("Arrow device export failed: {}", e)))?;
369
370        let (ffi_array, ffi_schema) =
371            to_ffi(&struct_data).map_err(|e| XlogError::Kernel(format!("{}", e)))?;
372        let array_ptr = Box::into_raw(Box::new(ffi_array));
373        let schema_ptr = Box::into_raw(Box::new(ffi_schema));
374
375        // SAFETY: these uniquely transferred Box allocations retain all GPU
376        // buffers through their thread-safe Arrow custom allocation owners.
377        Ok(unsafe {
378            ArrowDeviceArray::new(
379                ARROW_DEVICE_CUDA,
380                self.device.ordinal() as i32,
381                array_ptr,
382                schema_ptr,
383            )
384        })
385    }
386
387    /// Import Arrow C Data Interface (device-resident) into a CudaBuffer (zero-copy).
388    ///
389    /// This is an experimental API gated behind the `arrow-device-import` feature.
390    #[cfg(feature = "arrow-device-import")]
391    pub fn from_arrow_device_record_batch(
392        &self,
393        device_array: crate::arrow_device::ArrowDeviceArrayOwned,
394    ) -> Result<CudaBuffer> {
395        use arrow::array::ArrayData;
396        use arrow::datatypes::DataType;
397        use arrow::ffi::from_ffi;
398        use std::sync::Arc;
399
400        use crate::arrow_device::{ArrowDeviceImport, ARROW_DEVICE_CUDA};
401        use crate::memory::CudaColumn;
402
403        let (device_type, device_id, outer_owner, ffi_array, ffi_schema) =
404            // SAFETY: device_array is a valid ArrowDeviceArrayOwned; into_ffi_parts transfers ownership per Arrow C Data Interface
405            unsafe { device_array.into_ffi_parts() };
406
407        if device_type != ARROW_DEVICE_CUDA {
408            return Err(XlogError::Kernel(format!(
409                "Arrow device import requires CUDA device type={}, got {}",
410                ARROW_DEVICE_CUDA, device_type
411            )));
412        }
413        if device_id != self.device.ordinal() as i32 {
414            return Err(XlogError::Kernel(format!(
415                "Arrow device import device id mismatch: expected {}, got {}",
416                self.device.ordinal(),
417                device_id
418            )));
419        }
420
421        // SAFETY: ffi_array and ffi_schema conform to the Arrow C Data Interface; device and type checks passed above
422        let data: ArrayData = unsafe { from_ffi(ffi_array, &ffi_schema) }
423            .map_err(|e| XlogError::Kernel(format!("Arrow device import failed: {}", e)))?;
424        drop(ffi_schema);
425        let keepalive = Arc::new(ArrowDeviceImport::from_ffi(data, outer_owner));
426        let data = keepalive.data();
427
428        let (fields, children) = match data.data_type() {
429            DataType::Struct(fields) => (fields.clone(), data.child_data().to_vec()),
430            other => {
431                return Err(XlogError::Kernel(format!(
432                    "Arrow device import expects Struct, got {:?}",
433                    other
434                )))
435            }
436        };
437
438        if data.offset() != 0 {
439            return Err(XlogError::Kernel(
440                "Arrow device import does not support non-zero offsets".to_string(),
441            ));
442        }
443        if data.null_count() > 0 || data.nulls().is_some() {
444            return Err(XlogError::Kernel(
445                "Arrow device import does not support nulls".to_string(),
446            ));
447        }
448
449        let num_rows = data.len();
450        if fields.len() != children.len() {
451            return Err(XlogError::Kernel(
452                "Arrow device import field/child length mismatch".to_string(),
453            ));
454        }
455
456        let mut columns = Vec::with_capacity(children.len());
457        let mut schema_cols = Vec::with_capacity(children.len());
458
459        for (field, child) in fields.iter().zip(children.iter()) {
460            if child.len() != num_rows {
461                return Err(XlogError::Kernel(
462                    "Arrow device import child length mismatch".to_string(),
463                ));
464            }
465            if child.offset() != 0 {
466                return Err(XlogError::Kernel(
467                    "Arrow device import does not support child offsets".to_string(),
468                ));
469            }
470            if child.null_count() > 0 || child.nulls().is_some() {
471                return Err(XlogError::Kernel(
472                    "Arrow device import does not support child nulls".to_string(),
473                ));
474            }
475
476            let (scalar_type, elem_size) = Self::scalar_type_from_arrow_field(field)?;
477            let buffers = child.buffers();
478            let buf = buffers.first().ok_or_else(|| {
479                XlogError::Kernel("Arrow device import missing value buffer".to_string())
480            })?;
481            let len_bytes = buf.len();
482            let expected_bytes = num_rows.checked_mul(elem_size).ok_or_else(|| {
483                XlogError::Kernel("Arrow device import size overflow".to_string())
484            })?;
485            if len_bytes != expected_bytes {
486                return Err(XlogError::Kernel(format!(
487                    "Arrow device import buffer size mismatch: expected {}, got {}",
488                    expected_bytes, len_bytes
489                )));
490            }
491
492            let ptr = buf.as_ptr();
493            if ptr.is_null() && len_bytes > 0 {
494                return Err(XlogError::Kernel(
495                    "Arrow device import got null buffer pointer".to_string(),
496                ));
497            }
498            let device_ptr = ptr as usize as cudarc::driver::sys::CUdeviceptr;
499            // SAFETY: the validated FFI child is retained with its full outer
500            // producer owner; from_raw's contract supplies producer readiness.
501            columns.push(unsafe {
502                CudaColumn::arrow_device(
503                    device_ptr,
504                    len_bytes,
505                    self.device().inner().stream().clone(),
506                    keepalive.clone(),
507                )
508            });
509            schema_cols.push((field.name().to_string(), scalar_type));
510        }
511
512        let schema = Schema::new(schema_cols);
513        self.buffer_from_columns(columns, num_rows as u64, schema)
514    }
515
516    /// Export CudaBuffer to Arrow RecordBatch
517    ///
518    /// Downloads data from GPU and converts it to an Arrow RecordBatch for
519    /// interoperability with Arrow-based tools like cuDF, Polars, or DuckDB.
520    ///
521    /// # Arguments
522    /// * `buffer` - The CudaBuffer to export
523    ///
524    /// # Returns
525    /// An Arrow RecordBatch containing all columns from the buffer
526    ///
527    /// # Errors
528    /// Returns `XlogError::Kernel` if:
529    /// - Column download fails
530    /// - RecordBatch creation fails
531    pub fn to_arrow_record_batch(
532        &self,
533        buffer: &CudaBuffer,
534    ) -> Result<arrow::record_batch::RecordBatch> {
535        use arrow::array::*;
536        use arrow::datatypes::{Field, Schema as ArrowSchema};
537        use arrow::record_batch::{RecordBatch, RecordBatchOptions};
538
539        let num_rows = self.device_row_count(buffer)?;
540
541        let fields: Vec<Field> = buffer
542            .schema
543            .columns
544            .iter()
545            .map(|(name, scalar_type)| Field::new(name, scalar_type.to_arrow_type(), false))
546            .collect();
547        let arrow_schema = Arc::new(ArrowSchema::new(fields));
548
549        let mut arrays: Vec<Arc<dyn Array>> = Vec::with_capacity(buffer.arity());
550
551        for (col_idx, (_, scalar_type)) in buffer.schema.columns.iter().enumerate() {
552            let col = buffer
553                .column(col_idx)
554                .ok_or_else(|| XlogError::Kernel(format!("Column {} not found", col_idx)))?;
555
556            // Handle empty buffer case
557            if num_rows == 0 {
558                let array: Arc<dyn Array> = match scalar_type {
559                    ScalarType::Bool => Arc::new(BooleanArray::from(Vec::<bool>::new())),
560                    ScalarType::U32 => Arc::new(UInt32Array::from(Vec::<u32>::new())),
561                    ScalarType::Symbol => Arc::new(xlog_core::symbol::to_arrow(&[])),
562                    ScalarType::I32 => Arc::new(Int32Array::from(Vec::<i32>::new())),
563                    ScalarType::U64 => Arc::new(UInt64Array::from(Vec::<u64>::new())),
564                    ScalarType::I64 => Arc::new(Int64Array::from(Vec::<i64>::new())),
565                    ScalarType::F32 => Arc::new(Float32Array::from(Vec::<f32>::new())),
566                    ScalarType::F64 => Arc::new(Float64Array::from(Vec::<f64>::new())),
567                };
568                arrays.push(array);
569                continue;
570            }
571
572            let elem_size = scalar_type.size_bytes();
573            let num_bytes = num_rows
574                .checked_mul(elem_size)
575                .ok_or_else(|| XlogError::Kernel("Row byte size overflow".to_string()))?;
576            let mut bytes = vec![0u8; num_bytes];
577            let col_view = self.column_bytes_view(col, num_bytes)?;
578            self.device
579                .inner()
580                .dtoh_sync_copy_into(&col_view, &mut bytes)
581                .map_err(|e| XlogError::Kernel(format!("Failed to download column: {}", e)))?;
582
583            let array: Arc<dyn Array> = match scalar_type {
584                ScalarType::Bool => Arc::new(BooleanArray::from(
585                    bytes.iter().map(|&b| b != 0).collect::<Vec<_>>(),
586                )),
587                ScalarType::U32 => {
588                    let values: Vec<u32> = bytes
589                        .as_chunks::<4>()
590                        .0
591                        .iter()
592                        .map(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]))
593                        .collect();
594                    Arc::new(UInt32Array::from(values))
595                }
596                ScalarType::Symbol => {
597                    let values: Vec<u32> = bytes
598                        .as_chunks::<4>()
599                        .0
600                        .iter()
601                        .map(|c| u32::from_le_bytes([c[0], c[1], c[2], c[3]]))
602                        .collect();
603                    Arc::new(xlog_core::symbol::to_arrow(&values))
604                }
605                ScalarType::I32 => {
606                    let values: Vec<i32> = bytes
607                        .as_chunks::<4>()
608                        .0
609                        .iter()
610                        .map(|c| i32::from_le_bytes([c[0], c[1], c[2], c[3]]))
611                        .collect();
612                    Arc::new(Int32Array::from(values))
613                }
614                ScalarType::U64 => {
615                    let values: Vec<u64> = bytes
616                        .as_chunks::<8>()
617                        .0
618                        .iter()
619                        .map(|c| {
620                            u64::from_le_bytes([c[0], c[1], c[2], c[3], c[4], c[5], c[6], c[7]])
621                        })
622                        .collect();
623                    Arc::new(UInt64Array::from(values))
624                }
625                ScalarType::I64 => {
626                    let values: Vec<i64> = bytes
627                        .as_chunks::<8>()
628                        .0
629                        .iter()
630                        .map(|c| {
631                            i64::from_le_bytes([c[0], c[1], c[2], c[3], c[4], c[5], c[6], c[7]])
632                        })
633                        .collect();
634                    Arc::new(Int64Array::from(values))
635                }
636                ScalarType::F32 => {
637                    let values: Vec<f32> = bytes
638                        .as_chunks::<4>()
639                        .0
640                        .iter()
641                        .map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
642                        .collect();
643                    Arc::new(Float32Array::from(values))
644                }
645                ScalarType::F64 => {
646                    let values: Vec<f64> = bytes
647                        .as_chunks::<8>()
648                        .0
649                        .iter()
650                        .map(|c| {
651                            f64::from_le_bytes([c[0], c[1], c[2], c[3], c[4], c[5], c[6], c[7]])
652                        })
653                        .collect();
654                    Arc::new(Float64Array::from(values))
655                }
656            };
657
658            arrays.push(array);
659        }
660
661        let options = RecordBatchOptions::new().with_row_count(Some(num_rows));
662        RecordBatch::try_new_with_options(arrow_schema, arrays, &options)
663            .map_err(|e| XlogError::Kernel(format!("Failed to create RecordBatch: {}", e)))
664    }
665
666    fn build_arrow_device_child(
667        &self,
668        buffer: &Arc<CudaBuffer>,
669        col_idx: usize,
670        name: &str,
671        scalar_type: ScalarType,
672        num_rows: usize,
673    ) -> Result<(arrow::datatypes::Field, arrow::array::ArrayData)> {
674        use arrow::array::ArrayData;
675        use arrow::buffer::Buffer;
676        use arrow::datatypes::{DataType, Field};
677        use std::collections::HashMap;
678        use std::ptr::NonNull;
679
680        use crate::arrow_device::ArrowCudaAllocation;
681
682        let col = buffer
683            .column(col_idx)
684            .ok_or_else(|| XlogError::Kernel(format!("Column {} not found", col_idx)))?;
685
686        let (dtype, metadata) = if scalar_type == ScalarType::Symbol {
687            let mut meta = HashMap::new();
688            meta.insert("xlog.symbol".to_string(), "true".to_string());
689            meta.insert("xlog.symbol_encoding".to_string(), "u32".to_string());
690            (DataType::UInt32, Some(meta))
691        } else {
692            (scalar_type.to_arrow_type(), None)
693        };
694
695        let field = match metadata {
696            Some(meta) => Field::new(name, dtype.clone(), false).with_metadata(meta),
697            None => Field::new(name, dtype.clone(), false),
698        };
699
700        let elem_size = match scalar_type {
701            ScalarType::Symbol => 4usize,
702            _ => scalar_type.size_bytes(),
703        };
704
705        let len_bytes = num_rows
706            .checked_mul(elem_size)
707            .ok_or_else(|| XlogError::Kernel("Arrow device export size overflow".to_string()))?;
708
709        let mut extra = Vec::new();
710        let (ptr, len) = match scalar_type {
711            ScalarType::Bool => {
712                let packed_len = num_rows.div_ceil(8);
713                let mut packed = self.memory.alloc::<u8>(packed_len)?;
714                let pack_fn = self
715                    .device
716                    .inner()
717                    .get_func(PACK_MODULE, pack_kernels::PACK_BOOLS_TO_BITMAP)
718                    .ok_or_else(|| {
719                        XlogError::Kernel("pack_bools_to_bitmap kernel not found".to_string())
720                    })?;
721                let block_size = 256u32;
722                let grid_size = (packed_len as u32).div_ceil(block_size);
723                // SAFETY: kernel arguments match the PTX signature; device buffers were allocated with sufficient size
724                unsafe {
725                    pack_fn.clone().launch(
726                        LaunchConfig {
727                            grid_dim: (grid_size, 1, 1),
728                            block_dim: (block_size, 1, 1),
729                            shared_mem_bytes: 0,
730                        },
731                        (col, num_rows as u32, &mut packed),
732                    )
733                }
734                .map_err(|e| XlogError::Kernel(format!("pack_bools_to_bitmap failed: {}", e)))?;
735                self.device.synchronize()?;
736                let ptr = *packed.device_ptr() as usize as *mut u8;
737                extra.push(packed);
738                (ptr, packed_len)
739            }
740            _ => {
741                let ptr = *col.device_ptr() as usize as *mut u8;
742                (ptr, len_bytes)
743            }
744        };
745
746        let alloc = Arc::new(ArrowCudaAllocation::new(Arc::clone(buffer), extra));
747        let nn = if len == 0 {
748            NonNull::dangling()
749        } else {
750            NonNull::new(ptr).ok_or_else(|| {
751                XlogError::Kernel("Arrow device export got null device pointer".to_string())
752            })?
753        };
754        // SAFETY: nn points to CUDA device memory managed by ArrowCudaAllocation; len is the correct byte length
755        let buf = unsafe { Buffer::from_custom_allocation(nn, len, alloc) };
756
757        let data = ArrayData::builder(dtype)
758            .len(num_rows)
759            .add_buffer(buf)
760            .build()
761            .map_err(|e| XlogError::Kernel(format!("Arrow device export failed: {}", e)))?;
762
763        Ok((field, data))
764    }
765
766    #[cfg(feature = "arrow-device-import")]
767    fn scalar_type_from_arrow_field(
768        field: &arrow::datatypes::Field,
769    ) -> Result<(ScalarType, usize)> {
770        use arrow::datatypes::DataType;
771
772        // Arrow's Field metadata is a map (possibly empty), not an Option.
773        let is_symbol = field
774            .metadata()
775            .get("xlog.symbol")
776            .map(|v| v == "true")
777            .unwrap_or(false);
778
779        let scalar = match field.data_type() {
780            DataType::Boolean => {
781                return Err(XlogError::Kernel(
782                    "Arrow device import does not support bit-packed bool yet".to_string(),
783                ))
784            }
785            DataType::UInt32 if is_symbol => ScalarType::Symbol,
786            dt => ScalarType::from_arrow_type(dt).ok_or_else(|| {
787                XlogError::Kernel(format!("Arrow device import unsupported type {:?}", dt))
788            })?,
789        };
790
791        let elem_size = match scalar {
792            ScalarType::Symbol => 4usize,
793            _ => scalar.size_bytes(),
794        };
795
796        Ok((scalar, elem_size))
797    }
798
799    /// Import Arrow RecordBatch to CudaBuffer
800    ///
801    /// Uploads Arrow data to GPU memory.
802    ///
803    /// # Arguments
804    /// * `record_batch` - The Arrow RecordBatch to import
805    ///
806    /// # Returns
807    /// A new CudaBuffer with the data on GPU
808    ///
809    /// # Errors
810    /// Returns error if Arrow type is not supported or upload fails
811    pub fn from_arrow_record_batch(
812        &self,
813        record_batch: &arrow::record_batch::RecordBatch,
814    ) -> Result<CudaBuffer> {
815        use arrow::array::*;
816        use arrow::datatypes::DataType;
817
818        let num_rows = record_batch.num_rows() as u64;
819
820        if num_rows == 0 {
821            let columns: Vec<(String, ScalarType)> = record_batch
822                .schema()
823                .fields()
824                .iter()
825                .filter_map(|f| {
826                    ScalarType::from_arrow_type(f.data_type()).map(|st| (f.name().clone(), st))
827                })
828                .collect();
829            return self.create_empty_buffer(Schema::new(columns));
830        }
831
832        let mut columns = Vec::with_capacity(record_batch.num_columns());
833        let mut schema_cols = Vec::with_capacity(record_batch.num_columns());
834
835        for (col_idx, field) in record_batch.schema().fields().iter().enumerate() {
836            let array = record_batch.column(col_idx);
837            let scalar_type = ScalarType::from_arrow_type(field.data_type()).ok_or_else(|| {
838                XlogError::Kernel(format!("Unsupported Arrow type: {:?}", field.data_type()))
839            })?;
840
841            let bytes: Vec<u8> = match field.data_type() {
842                DataType::Boolean => {
843                    let arr = array.as_any().downcast_ref::<BooleanArray>().unwrap();
844                    arr.iter()
845                        .map(|v| if v.unwrap_or(false) { 1u8 } else { 0u8 })
846                        .collect()
847                }
848                DataType::UInt32 => {
849                    let arr = array.as_any().downcast_ref::<UInt32Array>().unwrap();
850                    arr.values().iter().flat_map(|v| v.to_le_bytes()).collect()
851                }
852                DataType::Int32 => {
853                    let arr = array.as_any().downcast_ref::<Int32Array>().unwrap();
854                    arr.values().iter().flat_map(|v| v.to_le_bytes()).collect()
855                }
856                DataType::UInt64 => {
857                    let arr = array.as_any().downcast_ref::<UInt64Array>().unwrap();
858                    arr.values().iter().flat_map(|v| v.to_le_bytes()).collect()
859                }
860                DataType::Int64 => {
861                    let arr = array.as_any().downcast_ref::<Int64Array>().unwrap();
862                    arr.values().iter().flat_map(|v| v.to_le_bytes()).collect()
863                }
864                DataType::Float32 => {
865                    let arr = array.as_any().downcast_ref::<Float32Array>().unwrap();
866                    arr.values().iter().flat_map(|v| v.to_le_bytes()).collect()
867                }
868                DataType::Float64 => {
869                    let arr = array.as_any().downcast_ref::<Float64Array>().unwrap();
870                    arr.values().iter().flat_map(|v| v.to_le_bytes()).collect()
871                }
872                _ => {
873                    return Err(XlogError::Kernel(format!(
874                        "Unsupported Arrow type: {:?}",
875                        field.data_type()
876                    )))
877                }
878            };
879
880            let mut d_col = self.memory.alloc::<u8>(bytes.len())?;
881            self.htod_sync_copy_into_tracked(&bytes, &mut d_col)
882                .map_err(|e| XlogError::Kernel(format!("Failed to upload column: {}", e)))?;
883
884            columns.push(d_col.into());
885            schema_cols.push((field.name().clone(), scalar_type));
886        }
887
888        self.buffer_from_columns(columns, num_rows, Schema::new(schema_cols))
889    }
890
891    /// Export a CudaBuffer to an Arrow IPC stream (RecordBatchStream) as bytes.
892    ///
893    /// This is a convenience wrapper around `to_arrow_record_batch` that enables
894    /// interoperability with tools like cuDF via standard Arrow IPC readers.
895    ///
896    /// Note: This is not zero-copy; data is downloaded from GPU to host memory.
897    pub fn to_arrow_ipc_stream(&self, buffer: &CudaBuffer) -> Result<Vec<u8>> {
898        use arrow::ipc::writer::StreamWriter;
899
900        let batch = self.to_arrow_record_batch(buffer)?;
901        let mut out = Vec::new();
902        let mut writer = StreamWriter::try_new(&mut out, &batch.schema())
903            .map_err(|e| XlogError::Kernel(format!("Failed to create Arrow IPC writer: {}", e)))?;
904        writer
905            .write(&batch)
906            .map_err(|e| XlogError::Kernel(format!("Failed to write Arrow RecordBatch: {}", e)))?;
907        writer
908            .finish()
909            .map_err(|e| XlogError::Kernel(format!("Failed to finish Arrow IPC stream: {}", e)))?;
910        Ok(out)
911    }
912
913    /// Import a single-batch Arrow IPC stream (RecordBatchStream) into a CudaBuffer.
914    ///
915    /// Note: This uploads Arrow data from host to GPU memory.
916    pub fn from_arrow_ipc_stream(&self, ipc: &[u8]) -> Result<CudaBuffer> {
917        use arrow::ipc::reader::StreamReader;
918        use std::io::Cursor;
919
920        let cursor = Cursor::new(ipc);
921        let mut reader = StreamReader::try_new(cursor, None)
922            .map_err(|e| XlogError::Kernel(format!("Failed to create Arrow IPC reader: {}", e)))?;
923
924        let batches = reader.by_ref();
925        let first = batches
926            .next()
927            .ok_or_else(|| {
928                XlogError::Kernel("Arrow IPC stream contained no record batches".to_string())
929            })?
930            .map_err(|e| XlogError::Kernel(format!("Failed to read Arrow RecordBatch: {}", e)))?;
931
932        if batches.next().is_some() {
933            return Err(XlogError::Kernel(
934                "Arrow IPC stream contains multiple record batches; this API expects exactly one"
935                    .to_string(),
936            ));
937        }
938
939        self.from_arrow_record_batch(&first)
940    }
941
942    /// Write a CudaBuffer to a file as an Arrow IPC stream (RecordBatchStream).
943    pub fn write_arrow_ipc_stream_file<P: AsRef<std::path::Path>>(
944        &self,
945        buffer: &CudaBuffer,
946        path: P,
947    ) -> Result<()> {
948        let bytes = self.to_arrow_ipc_stream(buffer)?;
949        std::fs::write(&path, bytes).map_err(|e| {
950            XlogError::Kernel(format!(
951                "Failed to write Arrow IPC stream to {}: {}",
952                path.as_ref().display(),
953                e
954            ))
955        })?;
956        Ok(())
957    }
958
959    /// Read a CudaBuffer from a file containing an Arrow IPC stream (RecordBatchStream).
960    pub fn read_arrow_ipc_stream_file<P: AsRef<std::path::Path>>(
961        &self,
962        path: P,
963    ) -> Result<CudaBuffer> {
964        let bytes = std::fs::read(&path).map_err(|e| {
965            XlogError::Kernel(format!(
966                "Failed to read Arrow IPC stream from {}: {}",
967                path.as_ref().display(),
968                e
969            ))
970        })?;
971        self.from_arrow_ipc_stream(&bytes)
972    }
973}