1use std::sync::{Arc, RwLock, TryLockError};
29
30use dlpk::sys::{DLDataType, DLDevice, DLPackVersion};
31use dlpk::{DLPackPointerCast, DLPackTensor, GetDLPackDataType};
32use ndarray::ArrayD;
33
34use crate::error::ParseError;
35
36pub trait Array: std::any::Any + Send + Sync {
43 fn as_any(&self) -> &dyn std::any::Any;
45
46 fn as_any_mut(&mut self) -> &mut dyn std::any::Any;
48
49 fn shape(&self) -> Vec<usize>;
51
52 fn dtype(&self) -> DLDataType;
54
55 fn device(&self) -> DLDevice;
57
58 fn as_dlpack(
66 &self,
67 device: DLDevice,
68 stream: Option<i64>,
69 max_version: DLPackVersion,
70 ) -> Result<DLPackTensor, ParseError>;
71
72 fn copy(&self) -> Box<dyn Array>;
76}
77
78impl<T> Array for Arc<RwLock<ArrayD<T>>>
83where
84 T: 'static + Send + Sync + Clone + Default + GetDLPackDataType + DLPackPointerCast,
85{
86 fn as_any(&self) -> &dyn std::any::Any {
87 self
88 }
89
90 fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
91 self
92 }
93
94 fn shape(&self) -> Vec<usize> {
95 match self.try_read() {
96 Ok(lock) => lock.shape().to_vec(),
97 Err(TryLockError::Poisoned(_)) => panic!("readcon-core array lock is poisoned"),
98 Err(TryLockError::WouldBlock) => panic!("readcon-core array is already locked"),
99 }
100 }
101
102 fn dtype(&self) -> DLDataType {
103 T::get_dlpack_data_type()
104 }
105
106 fn device(&self) -> DLDevice {
107 DLDevice::cpu()
108 }
109
110 fn as_dlpack(
111 &self,
112 device: DLDevice,
113 _stream: Option<i64>,
114 _max_version: DLPackVersion,
115 ) -> Result<DLPackTensor, ParseError> {
116 if device != DLDevice::cpu() {
117 return Err(ParseError::ValidationError(format!(
118 "Arc<RwLock<ArrayD>> is CPU-only; requested device {device:?} unsupported"
119 )));
120 }
121 let lock = match self.try_read() {
126 Ok(lock) => lock,
127 Err(TryLockError::Poisoned(_)) => {
128 return Err(ParseError::ValidationError(
129 "readcon-core array lock is poisoned".into(),
130 ));
131 }
132 Err(TryLockError::WouldBlock) => {
133 return Err(ParseError::ValidationError(
134 "readcon-core array is already locked".into(),
135 ));
136 }
137 };
138 let owned: ArrayD<T> = lock.to_owned();
145 DLPackTensor::try_from(owned).map_err(|e| {
146 ParseError::ValidationError(format!("dlpk ArrayD conversion failed: {e}"))
147 })
148 }
149
150 fn copy(&self) -> Box<dyn Array> {
151 Box::new(Arc::clone(self))
155 }
156}
157
158pub fn array_from_shape<T>(shape: &[usize]) -> Box<dyn Array>
160where
161 T: 'static + Send + Sync + Clone + Default + GetDLPackDataType + DLPackPointerCast,
162{
163 let arr: ArrayD<T> = ArrayD::default(ndarray::IxDyn(shape));
164 Box::new(Arc::new(RwLock::new(arr)))
165}
166
167pub fn allocate_array_on_device(
174 shape: &[usize],
175 device: DLDevice,
176) -> Result<Box<dyn Array>, ParseError> {
177 if device == DLDevice::cpu() {
178 return Ok(array_from_shape::<f64>(shape));
179 }
180 #[cfg(feature = "cuda")]
181 {
182 use dlpk::sys::DLDeviceType;
183 if device.device_type == DLDeviceType::kDLCUDA {
184 return crate::cuda_array::allocate_cuda_f64(shape, device.device_id);
185 }
186 }
187 Err(ParseError::ValidationError(format!(
188 "no device allocator in this build for {device:?}; use caller-supplied device buffers via from_dlpack / array_from_host_f64_on_device, or build with `--features cuda` for CUDA devices"
189 )))
190}
191
192pub struct DeviceTaggedF64Array {
201 shape: Vec<usize>,
202 device: DLDevice,
203 data: Arc<Vec<f64>>,
205}
206
207impl DeviceTaggedF64Array {
208 pub fn new(shape: &[usize], data: Vec<f64>, device: DLDevice) -> Result<Self, ParseError> {
210 let n: usize = shape.iter().product();
211 if data.len() != n {
212 return Err(ParseError::ValidationError(format!(
213 "device-tagged array: expected {n} f64 values for shape {shape:?}, got {}",
214 data.len()
215 )));
216 }
217 Ok(Self {
218 shape: shape.to_vec(),
219 device,
220 data: Arc::new(data),
221 })
222 }
223}
224
225pub fn array_from_host_f64_on_device(
227 shape: &[usize],
228 data: Vec<f64>,
229 device: DLDevice,
230) -> Result<Box<dyn Array>, ParseError> {
231 Ok(Box::new(DeviceTaggedF64Array::new(shape, data, device)?))
232}
233
234pub fn from_dlpack_f64(tensor: &DLPackTensor) -> Result<Box<dyn Array>, ParseError> {
240 let device = tensor.device();
241 let shape: Vec<usize> = tensor.shape().iter().map(|&d| d as usize).collect();
242 let n: usize = shape.iter().product();
243 let dtype = tensor.dtype();
244 if dtype.code != dlpk::sys::DLDataTypeCode::kDLFloat || dtype.bits != 64 {
245 return Err(ParseError::ValidationError(format!(
246 "from_dlpack_f64: expected f64, got dtype code={:?} bits={}",
247 dtype.code, dtype.bits
248 )));
249 }
250 let ptr = tensor
253 .data_ptr::<f64>()
254 .map_err(|e| ParseError::ValidationError(format!("from_dlpack_f64 data_ptr: {e}")))?;
255 let mut data = vec![0.0f64; n];
256 if n > 0 {
257 unsafe {
258 std::ptr::copy_nonoverlapping(ptr, data.as_mut_ptr(), n);
259 }
260 }
261 array_from_host_f64_on_device(&shape, data, device)
262}
263
264struct DeviceTaggedManager {
265 data: Arc<Vec<f64>>,
266 shape: Vec<i64>,
267}
268
269unsafe extern "C" fn device_tagged_deleter(managed: *mut dlpk::sys::DLManagedTensorVersioned) {
270 if managed.is_null() {
271 return;
272 }
273 unsafe {
277 let ctx = (*managed).manager_ctx;
278 if !ctx.is_null() {
279 let _ = Box::from_raw(ctx as *mut DeviceTaggedManager);
280 (*managed).manager_ctx = std::ptr::null_mut();
281 }
282 }
283}
284
285impl Array for DeviceTaggedF64Array {
286 fn as_any(&self) -> &dyn std::any::Any {
287 self
288 }
289
290 fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
291 self
292 }
293
294 fn shape(&self) -> Vec<usize> {
295 self.shape.clone()
296 }
297
298 fn dtype(&self) -> DLDataType {
299 f64::get_dlpack_data_type()
300 }
301
302 fn device(&self) -> DLDevice {
303 self.device
304 }
305
306 fn as_dlpack(
307 &self,
308 device: DLDevice,
309 _stream: Option<i64>,
310 _max_version: DLPackVersion,
311 ) -> Result<DLPackTensor, ParseError> {
312 if device != self.device {
313 return Err(ParseError::ValidationError(format!(
314 "device mismatch: array is on {:?}, requested {:?}",
315 self.device, device
316 )));
317 }
318 let manager = Box::new(DeviceTaggedManager {
322 data: Arc::clone(&self.data),
323 shape: self.shape.iter().map(|&d| d as i64).collect(),
324 });
325 let data_ptr = manager.data.as_ptr() as *mut std::ffi::c_void;
326 let shape_ptr = manager.shape.as_ptr() as *mut i64;
327 let mut managed = dlpk::sys::DLManagedTensorVersioned {
328 version: dlpk::sys::DLPackVersion {
329 major: dlpk::sys::DLPACK_MAJOR_VERSION,
330 minor: dlpk::sys::DLPACK_MINOR_VERSION,
331 },
332 manager_ctx: std::ptr::null_mut(),
333 deleter: Some(device_tagged_deleter),
334 dl_tensor: dlpk::sys::DLTensor {
335 data: data_ptr,
336 device: self.device,
337 ndim: self.shape.len() as i32,
338 dtype: f64::get_dlpack_data_type(),
339 shape: shape_ptr,
340 strides: std::ptr::null_mut(),
341 byte_offset: 0,
342 },
343 flags: 0,
344 };
345 managed.manager_ctx = Box::into_raw(manager) as *mut std::ffi::c_void;
346 Ok(unsafe { DLPackTensor::from_raw(managed) })
350 }
351
352 fn copy(&self) -> Box<dyn Array> {
353 Box::new(Self {
354 shape: self.shape.clone(),
355 device: self.device,
356 data: Arc::clone(&self.data),
357 })
358 }
359}
360
361#[cfg(test)]
362mod tests {
363 use super::*;
364
365 #[test]
366 fn array_from_shape_reports_shape_and_dtype() {
367 let a: Box<dyn Array> = array_from_shape::<f64>(&[5, 3]);
368 assert_eq!(a.shape(), vec![5, 3]);
369 let dt = a.dtype();
370 assert_eq!(dt.code, dlpk::sys::DLDataTypeCode::kDLFloat);
371 assert_eq!(dt.bits, 64);
372 assert_eq!(dt.lanes, 1);
373 assert_eq!(a.device(), DLDevice::cpu());
374 }
375
376 #[test]
377 fn array_copy_shares_storage_via_arc() {
378 let a = array_from_shape::<f64>(&[2, 3]);
379 let b = a.copy();
380 assert_eq!(a.shape(), b.shape());
382 }
383
384 #[test]
385 fn array_dlpack_export_round_trip() {
386 let a = array_from_shape::<f64>(&[4, 3]);
387 let tensor = a
388 .as_dlpack(DLDevice::cpu(), None, DLPackVersion::current())
389 .expect("DLPack export should succeed for CPU array");
390 assert_eq!(tensor.shape(), &[4, 3]);
391 }
392
393 #[cfg(not(feature = "cuda"))]
396 #[test]
397 fn allocate_non_cpu_fails_clearly() {
398 match allocate_array_on_device(&[2, 3], DLDevice::cuda(0)) {
399 Ok(_) => panic!("non-CPU allocate must fail without --features cuda"),
400 Err(err) => {
401 let msg = format!("{err:?}");
402 assert!(
403 msg.contains("no device allocator") || msg.contains("allocator"),
404 "{msg}"
405 );
406 }
407 }
408 let cpu = allocate_array_on_device(&[2, 3], DLDevice::cpu()).unwrap();
409 assert_eq!(cpu.device(), DLDevice::cpu());
410 }
411
412 #[cfg(feature = "cuda")]
415 #[test]
416 fn allocate_cuda_succeeds_with_feature() {
417 let a = allocate_array_on_device(&[2, 3], DLDevice::cuda(0))
418 .expect("CUDA allocate must succeed with --features cuda and a driver");
419 assert_eq!(a.device(), DLDevice::cuda(0));
420 assert_eq!(
421 a.device().device_type,
422 dlpk::sys::DLDeviceType::kDLCUDA
423 );
424 let t = a
425 .as_dlpack(DLDevice::cuda(0), None, DLPackVersion::current())
426 .expect("matching as_dlpack");
427 assert_eq!(t.device().device_type, dlpk::sys::DLDeviceType::kDLCUDA);
428 let cpu = allocate_array_on_device(&[2, 3], DLDevice::cpu()).unwrap();
429 assert_eq!(cpu.device(), DLDevice::cpu());
430 }
431
432 #[test]
433 fn cuda_tagged_preserves_device_and_matching_as_dlpack() {
434 let data: Vec<f64> = (0..6).map(|i| i as f64).collect();
435 let a = array_from_host_f64_on_device(&[2, 3], data.clone(), DLDevice::cuda(0)).unwrap();
436 assert_eq!(a.device(), DLDevice::cuda(0));
437 assert_eq!(a.shape(), vec![2, 3]);
438
439 let mismatch = a
440 .as_dlpack(DLDevice::cpu(), None, DLPackVersion::current())
441 .unwrap_err();
442 assert!(
443 format!("{mismatch:?}").contains("device mismatch"),
444 "{mismatch:?}"
445 );
446
447 let tensor = a
448 .as_dlpack(DLDevice::cuda(0), None, DLPackVersion::current())
449 .expect("matching CUDA device export");
450 assert_eq!(tensor.device(), DLDevice::cuda(0));
451 assert_eq!(tensor.shape(), &[2, 3]);
452
453 let back = from_dlpack_f64(&tensor).unwrap();
455 assert_eq!(back.device(), DLDevice::cuda(0));
456 assert_eq!(back.shape(), vec![2, 3]);
457 let again = back
458 .as_dlpack(DLDevice::cuda(0), None, DLPackVersion::current())
459 .unwrap();
460 assert_eq!(again.device(), DLDevice::cuda(0));
461 }
462
463 #[test]
464 fn cpu_tagged_path_unchanged() {
465 let a = array_from_host_f64_on_device(&[1, 3], vec![1.0, 2.0, 3.0], DLDevice::cpu()).unwrap();
466 assert_eq!(a.device(), DLDevice::cpu());
467 let t = a
468 .as_dlpack(DLDevice::cpu(), None, DLPackVersion::current())
469 .unwrap();
470 assert_eq!(t.device(), DLDevice::cpu());
471 let back = from_dlpack_f64(&t).unwrap();
472 assert_eq!(back.device(), DLDevice::cpu());
473 }
474}