1use std::ffi::c_void;
7use std::sync::Arc;
8
9use xlog_core::{Result, ScalarType, Schema, XlogError};
10
11use crate::device_runtime::Access;
12use crate::memory::{
13 validate_logical_row_count, with_memory_access, CudaBuffer, CudaColumn, DeviceRead,
14};
15use crate::provider::CudaKernelProvider;
16use crate::CudaDevice;
17
18pub type DLDeviceType = i32;
19
20pub const K_DLCPU: DLDeviceType = 1;
21pub const K_DLCUDA: DLDeviceType = 2;
22
23pub type DLDataTypeCode = u8;
24pub const K_DLINT: DLDataTypeCode = 0;
25pub const K_DLUINT: DLDataTypeCode = 1;
26pub const K_DLFLOAT: DLDataTypeCode = 2;
27pub const K_DLBOOL: DLDataTypeCode = 6;
28
29#[repr(C)]
30#[derive(Debug, Clone, Copy)]
31pub struct DLDevice {
32 pub device_type: DLDeviceType,
33 pub device_id: i32,
34}
35
36#[repr(C)]
37#[derive(Debug, Clone, Copy)]
38pub struct DLDataType {
39 pub code: DLDataTypeCode,
40 pub bits: u8,
41 pub lanes: u16,
42}
43
44#[repr(C)]
45#[derive(Debug)]
46pub struct DLTensor {
47 pub data: *mut c_void,
48 pub device: DLDevice,
49 pub ndim: i32,
50 pub dtype: DLDataType,
51 pub shape: *mut i64,
52 pub strides: *mut i64,
53 pub byte_offset: u64,
54}
55
56pub type DLDeleter = Option<unsafe extern "C" fn(*mut DLManagedTensor)>;
57
58#[repr(C)]
59#[derive(Debug)]
60pub struct DLManagedTensor {
61 pub dl_tensor: DLTensor,
62 pub manager_ctx: *mut c_void,
63 pub deleter: DLDeleter,
64}
65
66struct DlpackCtx {
67 _buffer: Arc<CudaBuffer>,
68 shape: Box<[i64]>,
69}
70
71unsafe extern "C" fn dlpack_deleter(ptr: *mut DLManagedTensor) {
72 if ptr.is_null() {
73 return;
74 }
75 let ctx_ptr = unsafe { (*ptr).manager_ctx as *mut DlpackCtx };
77 if !ctx_ptr.is_null() {
78 unsafe {
80 drop(Box::from_raw(ctx_ptr));
81 }
82 }
83 unsafe {
85 drop(Box::from_raw(ptr));
86 }
87}
88
89fn scalar_to_dl_dtype(ty: ScalarType) -> DLDataType {
90 match ty {
91 ScalarType::U32 | ScalarType::Symbol => DLDataType {
92 code: K_DLUINT,
93 bits: 32,
94 lanes: 1,
95 },
96 ScalarType::U64 => DLDataType {
97 code: K_DLUINT,
98 bits: 64,
99 lanes: 1,
100 },
101 ScalarType::I32 => DLDataType {
102 code: K_DLINT,
103 bits: 32,
104 lanes: 1,
105 },
106 ScalarType::I64 => DLDataType {
107 code: K_DLINT,
108 bits: 64,
109 lanes: 1,
110 },
111 ScalarType::F32 => DLDataType {
112 code: K_DLFLOAT,
113 bits: 32,
114 lanes: 1,
115 },
116 ScalarType::F64 => DLDataType {
117 code: K_DLFLOAT,
118 bits: 64,
119 lanes: 1,
120 },
121 ScalarType::Bool => DLDataType {
122 code: K_DLBOOL,
123 bits: 8,
124 lanes: 1,
125 },
126 }
127}
128
129fn dl_dtype_to_scalar(dtype: DLDataType) -> Result<ScalarType> {
130 if dtype.lanes != 1 {
131 return Err(XlogError::Kernel(format!(
132 "Unsupported DLPack dtype lanes {} (expected 1)",
133 dtype.lanes
134 )));
135 }
136 match (dtype.code, dtype.bits) {
137 (K_DLUINT, 32) => Ok(ScalarType::U32),
138 (K_DLUINT, 64) => Ok(ScalarType::U64),
139 (K_DLINT, 32) => Ok(ScalarType::I32),
140 (K_DLINT, 64) => Ok(ScalarType::I64),
141 (K_DLFLOAT, 32) => Ok(ScalarType::F32),
142 (K_DLFLOAT, 64) => Ok(ScalarType::F64),
143 (K_DLBOOL, 8) => Ok(ScalarType::Bool),
145 _ => Err(XlogError::Kernel(format!(
146 "Unsupported DLPack dtype code={} bits={} lanes={}",
147 dtype.code, dtype.bits, dtype.lanes
148 ))),
149 }
150}
151
152pub struct DlpackManagedTensor {
156 ptr: *mut DLManagedTensor,
157}
158
159unsafe impl Send for DlpackManagedTensor {}
164unsafe impl Sync for DlpackManagedTensor {}
165
166impl DlpackManagedTensor {
167 pub unsafe fn from_raw(ptr: *mut DLManagedTensor) -> Self {
176 Self { ptr }
177 }
178
179 pub fn as_ptr(&self) -> *mut DLManagedTensor {
180 self.ptr
181 }
182
183 pub fn into_raw(self) -> *mut DLManagedTensor {
184 let ptr = self.ptr;
185 std::mem::forget(self);
186 ptr
187 }
188}
189
190impl Drop for DlpackManagedTensor {
191 fn drop(&mut self) {
192 unsafe {
194 if !self.ptr.is_null() {
195 if let Some(deleter) = (*self.ptr).deleter {
196 deleter(self.ptr);
197 }
198 }
199 }
200 }
201}
202
203unsafe fn dlpack_tensor_info(
204 provider: &CudaKernelProvider,
205 tensor: &DlpackManagedTensor,
206) -> Result<(u64, ScalarType, cudarc::driver::sys::CUdeviceptr, usize)> {
207 let ptr = tensor.as_ptr();
208 if ptr.is_null() {
209 return Err(XlogError::Kernel(
210 "Null DLManagedTensor pointer".to_string(),
211 ));
212 }
213
214 let dl = unsafe { &(*ptr).dl_tensor };
216
217 unsafe { dlpack_tensor_metadata(provider.device().ordinal() as i32, dl) }
219}
220
221pub(crate) unsafe fn dlpack_tensor_metadata(
227 device_id: i32,
228 dl: &DLTensor,
229) -> Result<(u64, ScalarType, cudarc::driver::sys::CUdeviceptr, usize)> {
230 if dl.device.device_type != K_DLCUDA {
231 return Err(XlogError::Kernel(format!(
232 "Unsupported DLPack device type {} (expected CUDA)",
233 dl.device.device_type
234 )));
235 }
236 if dl.device.device_id != device_id {
237 return Err(XlogError::Kernel(format!(
238 "DLPack tensor device_id {} does not match provider device_id {}",
239 dl.device.device_id, device_id
240 )));
241 }
242
243 if dl.ndim != 1 {
244 return Err(XlogError::Kernel(format!(
245 "Unsupported DLPack ndim {} (expected 1)",
246 dl.ndim
247 )));
248 }
249 if dl.shape.is_null() {
250 return Err(XlogError::Kernel("DLPack tensor shape is null".to_string()));
251 }
252 if !dl.strides.is_null() {
253 let stride0 = unsafe { *dl.strides };
255 if stride0 != 1 {
256 return Err(XlogError::Kernel(format!(
257 "Non-contiguous DLPack tensor stride {} (expected 1)",
258 stride0
259 )));
260 }
261 }
262
263 let shape0 = unsafe { *dl.shape };
265 if shape0 < 0 {
266 return Err(XlogError::Kernel(format!(
267 "Negative DLPack tensor shape {}",
268 shape0
269 )));
270 }
271 let num_rows = shape0 as u64;
272
273 let scalar = dl_dtype_to_scalar(dl.dtype)?;
274 let elem_size = scalar.size_bytes();
275 if !dl.byte_offset.is_multiple_of(elem_size as u64) {
276 return Err(XlogError::Kernel(format!(
277 "DLPack byte_offset {} is not aligned to element size {}",
278 dl.byte_offset, elem_size
279 )));
280 }
281
282 if dl.data.is_null() && num_rows > 0 {
283 return Err(XlogError::Kernel(
284 "DLPack tensor data pointer is null".to_string(),
285 ));
286 }
287
288 let base = dl.data as usize;
289 let ptr_with_offset = base
290 .checked_add(dl.byte_offset as usize)
291 .ok_or_else(|| XlogError::Kernel("DLPack data pointer overflow".to_string()))?;
292
293 if ptr_with_offset % elem_size != 0 {
294 return Err(XlogError::Kernel(
295 "DLPack tensor data is not properly aligned".to_string(),
296 ));
297 }
298
299 let len_bytes = usize::try_from(num_rows)
300 .ok()
301 .and_then(|n| n.checked_mul(elem_size))
302 .ok_or_else(|| XlogError::Kernel("DLPack tensor length overflow".to_string()))?;
303
304 Ok((num_rows, scalar, ptr_with_offset as u64, len_bytes))
305}
306
307fn dlpack_logical_row_count(device: &Arc<CudaDevice>, buffer: &CudaBuffer) -> Result<usize> {
308 if let Some(cached_rows) = buffer.cached_row_count() {
309 return validate_logical_row_count(buffer.num_rows(), cached_rows as usize);
310 }
311
312 let mut host_rows = [0u32];
313 device
314 .inner()
315 .dtoh_sync_copy_into(buffer.num_rows_device(), &mut host_rows)
316 .map_err(|e| XlogError::Kernel(format!("Failed to read row count: {}", e)))?;
317 buffer.set_cached_row_count_if_unset(host_rows[0]);
318 validate_logical_row_count(buffer.num_rows(), host_rows[0] as usize)
319}
320
321pub struct DlpackTable {
325 buffer: Arc<CudaBuffer>,
326 cuda_device: Arc<CudaDevice>,
327 device: DLDevice,
328}
329
330impl DlpackTable {
331 pub fn column(&self, col_idx: usize) -> Result<DlpackManagedTensor> {
334 let logical_rows = dlpack_logical_row_count(&self.cuda_device, &self.buffer)?;
335 let dtype =
336 self.buffer.schema().column_type(col_idx).ok_or_else(|| {
337 XlogError::Kernel(format!("Column index {} out of bounds", col_idx))
338 })?;
339
340 let col = self
341 .buffer
342 .columns
343 .get(col_idx)
344 .ok_or_else(|| XlogError::Kernel(format!("Column {} not found", col_idx)))?;
345
346 let stream = col.stream();
347 let _ordinary = crate::cuda_graph::reserve_uncaptured_stream(stream).map_err(|error| {
348 XlogError::Kernel(format!("DLPack export stream admission: {error}"))
349 })?;
350 let view = col.device_view();
351 with_memory_access(
352 Arc::clone(stream),
353 vec![view
354 .access(Access::Read)
355 .map_err(|error| XlogError::Kernel(error.to_string()))?],
356 |_| Ok(stream.synchronize()?),
357 )
358 .map_err(|error| XlogError::Kernel(format!("DLPack producer readiness: {error}")))?;
359 let device_ptr = *col.device_ptr() as usize as *mut c_void;
360
361 let mut ctx = Box::new(DlpackCtx {
362 _buffer: self.buffer.clone(),
363 shape: vec![logical_rows as i64].into_boxed_slice(),
364 });
365 let shape_ptr = ctx.shape.as_mut_ptr();
366
367 let dl_tensor = DLTensor {
368 data: device_ptr,
369 device: self.device,
370 ndim: 1,
371 dtype: scalar_to_dl_dtype(dtype),
372 shape: shape_ptr,
373 strides: std::ptr::null_mut(),
374 byte_offset: 0,
375 };
376
377 let managed = Box::new(DLManagedTensor {
378 dl_tensor,
379 manager_ctx: Box::into_raw(ctx) as *mut c_void,
380 deleter: Some(dlpack_deleter),
381 });
382
383 Ok(DlpackManagedTensor {
384 ptr: Box::into_raw(managed),
385 })
386 }
387}
388
389impl CudaKernelProvider {
390 pub fn to_dlpack_table(&self, buffer: CudaBuffer) -> DlpackTable {
394 DlpackTable {
395 buffer: Arc::new(buffer),
396 cuda_device: Arc::clone(self.device()),
397 device: DLDevice {
398 device_type: K_DLCUDA,
399 device_id: self.device().ordinal() as i32,
400 },
401 }
402 }
403
404 pub fn from_dlpack_tensors(&self, tensors: Vec<DlpackManagedTensor>) -> Result<CudaBuffer> {
408 if tensors.is_empty() {
409 return self.create_empty_buffer(Schema::new(vec![]));
410 }
411
412 let mut columns = Vec::with_capacity(tensors.len());
413 let mut schema_cols = Vec::with_capacity(tensors.len());
414 let mut num_rows: Option<u64> = None;
415
416 for (i, tensor) in tensors.into_iter().enumerate() {
417 let (rows, ty, ptr, len_bytes) = unsafe { dlpack_tensor_info(self, &tensor)? };
419 if let Some(n) = num_rows {
420 if rows != n {
421 return Err(XlogError::Kernel(
422 "DLPack column row counts do not match".to_string(),
423 ));
424 }
425 } else {
426 num_rows = Some(rows);
427 }
428
429 schema_cols.push((format!("col_{}", i), ty));
430 columns.push(unsafe {
433 CudaColumn::dlpack(
434 ptr,
435 len_bytes,
436 self.device().inner().stream().clone(),
437 tensor,
438 )
439 });
440 }
441
442 let schema = xlog_core::Schema::new(schema_cols);
443 self.buffer_from_columns(columns, num_rows.unwrap_or(0), schema)
444 }
445
446 pub fn from_dlpack_tensors_with_schema(
448 &self,
449 schema: xlog_core::Schema,
450 tensors: Vec<DlpackManagedTensor>,
451 ) -> Result<CudaBuffer> {
452 if schema.arity() != tensors.len() {
453 return Err(XlogError::Kernel(format!(
454 "Schema arity {} does not match tensor count {}",
455 schema.arity(),
456 tensors.len()
457 )));
458 }
459
460 if tensors.is_empty() {
461 return self.create_empty_buffer(schema);
462 }
463
464 let mut columns = Vec::with_capacity(tensors.len());
465 let mut num_rows: Option<u64> = None;
466
467 for (i, tensor) in tensors.into_iter().enumerate() {
468 let (rows, ty, ptr, len_bytes) = unsafe { dlpack_tensor_info(self, &tensor)? };
470 let expected = schema.column_type(i).ok_or_else(|| {
471 XlogError::Kernel(format!("Missing schema type for column {}", i))
472 })?;
473 if !expected.dlpack_compatible(ty) {
474 return Err(XlogError::Kernel(format!(
475 "DLPack column {} dtype {:?} does not match schema {:?}",
476 i, ty, expected
477 )));
478 }
479
480 if let Some(n) = num_rows {
481 if rows != n {
482 return Err(XlogError::Kernel(
483 "DLPack column row counts do not match".to_string(),
484 ));
485 }
486 } else {
487 num_rows = Some(rows);
488 }
489
490 columns.push(unsafe {
493 CudaColumn::dlpack(
494 ptr,
495 len_bytes,
496 self.device().inner().stream().clone(),
497 tensor,
498 )
499 });
500 }
501
502 self.buffer_from_columns(columns, num_rows.unwrap_or(0), schema)
503 }
504}
505
506struct SliceExportCtx {
509 _slice: Arc<crate::memory::TrackedCudaSlice<u8>>,
510 shape: Box<[i64; 2]>,
511}
512
513unsafe extern "C" fn slice_export_deleter(ptr: *mut DLManagedTensor) {
514 if ptr.is_null() {
515 return;
516 }
517 let ctx_ptr = unsafe { (*ptr).manager_ctx as *mut SliceExportCtx };
519 if !ctx_ptr.is_null() {
520 unsafe {
522 drop(Box::from_raw(ctx_ptr));
523 }
524 }
525 unsafe {
527 drop(Box::from_raw(ptr));
528 }
529}
530
531pub fn export_slice_managed_tensor(
542 slice: Arc<crate::memory::TrackedCudaSlice<u8>>,
543 device_id: i32,
544 dtype: DLDataType,
545 rows: usize,
546 cols: usize,
547) -> Result<DlpackManagedTensor> {
548 let elem_bytes = (dtype.bits as usize / 8) * dtype.lanes as usize;
549 let needed = rows
550 .checked_mul(cols)
551 .and_then(|cells| cells.checked_mul(elem_bytes))
552 .ok_or_else(|| XlogError::Kernel("export shape overflows".to_string()))?;
553 if needed != slice.len() {
554 return Err(XlogError::Kernel(format!(
555 "export view {rows}x{cols}x{elem_bytes}B = {needed} bytes does not \
556 cover the {}-byte allocation exactly",
557 slice.len()
558 )));
559 }
560
561 let stream = slice.stream();
562 let _ordinary = crate::cuda_graph::reserve_uncaptured_stream(stream)
563 .map_err(|error| XlogError::Kernel(format!("DLPack export stream admission: {error}")))?;
564 let view = slice.view();
565 with_memory_access(
566 Arc::clone(stream),
567 vec![view
568 .access(Access::Read)
569 .map_err(|error| XlogError::Kernel(error.to_string()))?],
570 |_| Ok(stream.synchronize()?),
571 )
572 .map_err(|error| XlogError::Kernel(format!("DLPack producer readiness: {error}")))?;
573 let data = *slice.device_ptr() as usize as *mut c_void;
574 let mut ctx = Box::new(SliceExportCtx {
575 _slice: slice,
576 shape: Box::new([rows as i64, cols as i64]),
577 });
578 let shape_ptr = ctx.shape.as_mut_ptr();
579
580 let dl_tensor = DLTensor {
581 data,
582 device: DLDevice {
583 device_type: K_DLCUDA,
584 device_id,
585 },
586 ndim: 2,
587 dtype,
588 shape: shape_ptr,
589 strides: std::ptr::null_mut(),
590 byte_offset: 0,
591 };
592
593 let managed = Box::new(DLManagedTensor {
594 dl_tensor,
595 manager_ctx: Box::into_raw(ctx) as *mut c_void,
596 deleter: Some(slice_export_deleter),
597 });
598
599 Ok(DlpackManagedTensor {
600 ptr: Box::into_raw(managed),
601 })
602}