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)
146 .map_err(|e| ParseError::ValidationError(format!("dlpk ArrayD conversion failed: {e}")))
147 }
148
149 fn copy(&self) -> Box<dyn Array> {
150 Box::new(Arc::clone(self))
154 }
155}
156
157pub fn array_from_shape<T>(shape: &[usize]) -> Box<dyn Array>
159where
160 T: 'static + Send + Sync + Clone + Default + GetDLPackDataType + DLPackPointerCast,
161{
162 let arr: ArrayD<T> = ArrayD::default(ndarray::IxDyn(shape));
163 Box::new(Arc::new(RwLock::new(arr)))
164}
165
166pub fn allocate_array_on_device(
173 shape: &[usize],
174 device: DLDevice,
175) -> Result<Box<dyn Array>, ParseError> {
176 if device == DLDevice::cpu() {
177 return Ok(array_from_shape::<f64>(shape));
178 }
179 #[cfg(feature = "cuda")]
180 {
181 use dlpk::sys::DLDeviceType;
182 if device.device_type == DLDeviceType::kDLCUDA {
183 return crate::cuda_array::allocate_cuda_f64(shape, device.device_id);
184 }
185 }
186 Err(ParseError::ValidationError(format!(
187 "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"
188 )))
189}
190
191pub struct DeviceTaggedF64Array {
200 shape: Vec<usize>,
201 device: DLDevice,
202 data: Arc<Vec<f64>>,
204}
205
206impl DeviceTaggedF64Array {
207 pub fn new(shape: &[usize], data: Vec<f64>, device: DLDevice) -> Result<Self, ParseError> {
209 let n: usize = shape.iter().product();
210 if data.len() != n {
211 return Err(ParseError::ValidationError(format!(
212 "device-tagged array: expected {n} f64 values for shape {shape:?}, got {}",
213 data.len()
214 )));
215 }
216 Ok(Self {
217 shape: shape.to_vec(),
218 device,
219 data: Arc::new(data),
220 })
221 }
222}
223
224pub fn array_from_host_f64_on_device(
226 shape: &[usize],
227 data: Vec<f64>,
228 device: DLDevice,
229) -> Result<Box<dyn Array>, ParseError> {
230 Ok(Box::new(DeviceTaggedF64Array::new(shape, data, device)?))
231}
232
233pub fn from_dlpack_f64(tensor: &DLPackTensor) -> Result<Box<dyn Array>, ParseError> {
239 let device = tensor.device();
240 let shape: Vec<usize> = tensor.shape().iter().map(|&d| d as usize).collect();
241 let n: usize = shape.iter().product();
242 let dtype = tensor.dtype();
243 if dtype.code != dlpk::sys::DLDataTypeCode::kDLFloat || dtype.bits != 64 {
244 return Err(ParseError::ValidationError(format!(
245 "from_dlpack_f64: expected f64, got dtype code={:?} bits={}",
246 dtype.code, dtype.bits
247 )));
248 }
249 let ptr = tensor
252 .data_ptr::<f64>()
253 .map_err(|e| ParseError::ValidationError(format!("from_dlpack_f64 data_ptr: {e}")))?;
254 let mut data = vec![0.0f64; n];
255 if n > 0 {
256 unsafe {
257 std::ptr::copy_nonoverlapping(ptr, data.as_mut_ptr(), n);
258 }
259 }
260 array_from_host_f64_on_device(&shape, data, device)
261}
262
263struct DeviceTaggedManager {
264 data: Arc<Vec<f64>>,
265 shape: Vec<i64>,
266}
267
268unsafe extern "C" fn device_tagged_deleter(managed: *mut dlpk::sys::DLManagedTensorVersioned) {
269 if managed.is_null() {
270 return;
271 }
272 unsafe {
276 let ctx = (*managed).manager_ctx;
277 if !ctx.is_null() {
278 let _ = Box::from_raw(ctx as *mut DeviceTaggedManager);
279 (*managed).manager_ctx = std::ptr::null_mut();
280 }
281 }
282}
283
284impl Array for DeviceTaggedF64Array {
285 fn as_any(&self) -> &dyn std::any::Any {
286 self
287 }
288
289 fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
290 self
291 }
292
293 fn shape(&self) -> Vec<usize> {
294 self.shape.clone()
295 }
296
297 fn dtype(&self) -> DLDataType {
298 f64::get_dlpack_data_type()
299 }
300
301 fn device(&self) -> DLDevice {
302 self.device
303 }
304
305 fn as_dlpack(
306 &self,
307 device: DLDevice,
308 _stream: Option<i64>,
309 _max_version: DLPackVersion,
310 ) -> Result<DLPackTensor, ParseError> {
311 if device != self.device {
312 return Err(ParseError::ValidationError(format!(
313 "device mismatch: array is on {:?}, requested {:?}",
314 self.device, device
315 )));
316 }
317 let manager = Box::new(DeviceTaggedManager {
321 data: Arc::clone(&self.data),
322 shape: self.shape.iter().map(|&d| d as i64).collect(),
323 });
324 let data_ptr = manager.data.as_ptr() as *mut std::ffi::c_void;
325 let shape_ptr = manager.shape.as_ptr() as *mut i64;
326 let mut managed = dlpk::sys::DLManagedTensorVersioned {
327 version: dlpk::sys::DLPackVersion {
328 major: dlpk::sys::DLPACK_MAJOR_VERSION,
329 minor: dlpk::sys::DLPACK_MINOR_VERSION,
330 },
331 manager_ctx: std::ptr::null_mut(),
332 deleter: Some(device_tagged_deleter),
333 dl_tensor: dlpk::sys::DLTensor {
334 data: data_ptr,
335 device: self.device,
336 ndim: self.shape.len() as i32,
337 dtype: f64::get_dlpack_data_type(),
338 shape: shape_ptr,
339 strides: std::ptr::null_mut(),
340 byte_offset: 0,
341 },
342 flags: dlpk::sys::DLPACK_FLAG_BITMASK_READ_ONLY,
343 };
344 managed.manager_ctx = Box::into_raw(manager) as *mut std::ffi::c_void;
345 Ok(unsafe { DLPackTensor::from_raw(managed) })
349 }
350
351 fn copy(&self) -> Box<dyn Array> {
352 Box::new(Self {
353 shape: self.shape.clone(),
354 device: self.device,
355 data: Arc::clone(&self.data),
356 })
357 }
358}
359
360#[cfg(test)]
361mod tests {
362 use super::*;
363
364 #[test]
365 fn array_from_shape_reports_shape_and_dtype() {
366 let a: Box<dyn Array> = array_from_shape::<f64>(&[5, 3]);
367 assert_eq!(a.shape(), vec![5, 3]);
368 let dt = a.dtype();
369 assert_eq!(dt.code, dlpk::sys::DLDataTypeCode::kDLFloat);
370 assert_eq!(dt.bits, 64);
371 assert_eq!(dt.lanes, 1);
372 assert_eq!(a.device(), DLDevice::cpu());
373 }
374
375 #[test]
376 fn array_copy_shares_storage_via_arc() {
377 let a = array_from_shape::<f64>(&[2, 3]);
378 let b = a.copy();
379 assert_eq!(a.shape(), b.shape());
381 }
382
383 #[test]
384 fn array_dlpack_export_round_trip() {
385 let a = array_from_shape::<f64>(&[4, 3]);
386 let tensor = a
387 .as_dlpack(DLDevice::cpu(), None, DLPackVersion::current())
388 .expect("DLPack export should succeed for CPU array");
389 assert_eq!(tensor.shape(), &[4, 3]);
390 }
391
392 #[cfg(not(feature = "cuda"))]
395 #[test]
396 fn allocate_non_cpu_fails_clearly() {
397 match allocate_array_on_device(&[2, 3], DLDevice::cuda(0)) {
398 Ok(_) => panic!("non-CPU allocate must fail without --features cuda"),
399 Err(err) => {
400 let msg = format!("{err:?}");
401 assert!(
402 msg.contains("no device allocator") || msg.contains("allocator"),
403 "{msg}"
404 );
405 }
406 }
407 let cpu = allocate_array_on_device(&[2, 3], DLDevice::cpu()).unwrap();
408 assert_eq!(cpu.device(), DLDevice::cpu());
409 }
410
411 #[cfg(feature = "cuda")]
414 #[test]
415 fn allocate_cuda_succeeds_with_feature() {
416 let a = allocate_array_on_device(&[2, 3], DLDevice::cuda(0))
417 .expect("CUDA allocate must succeed with --features cuda and a driver");
418 assert_eq!(a.device(), DLDevice::cuda(0));
419 assert_eq!(a.device().device_type, dlpk::sys::DLDeviceType::kDLCUDA);
420 let t = a
421 .as_dlpack(DLDevice::cuda(0), None, DLPackVersion::current())
422 .expect("matching as_dlpack");
423 assert_eq!(t.device().device_type, dlpk::sys::DLDeviceType::kDLCUDA);
424 let cpu = allocate_array_on_device(&[2, 3], DLDevice::cpu()).unwrap();
425 assert_eq!(cpu.device(), DLDevice::cpu());
426 }
427
428 #[test]
429 fn cuda_tagged_preserves_device_and_matching_as_dlpack() {
430 let data: Vec<f64> = (0..6).map(|i| i as f64).collect();
431 let a = array_from_host_f64_on_device(&[2, 3], data.clone(), DLDevice::cuda(0)).unwrap();
432 assert_eq!(a.device(), DLDevice::cuda(0));
433 assert_eq!(a.shape(), vec![2, 3]);
434
435 let mismatch = a
436 .as_dlpack(DLDevice::cpu(), None, DLPackVersion::current())
437 .unwrap_err();
438 assert!(
439 format!("{mismatch:?}").contains("device mismatch"),
440 "{mismatch:?}"
441 );
442
443 let tensor = a
444 .as_dlpack(DLDevice::cuda(0), None, DLPackVersion::current())
445 .expect("matching CUDA device export");
446 assert_eq!(tensor.device(), DLDevice::cuda(0));
447 assert_eq!(tensor.shape(), &[2, 3]);
448
449 let back = from_dlpack_f64(&tensor).unwrap();
451 assert_eq!(back.device(), DLDevice::cuda(0));
452 assert_eq!(back.shape(), vec![2, 3]);
453 let again = back
454 .as_dlpack(DLDevice::cuda(0), None, DLPackVersion::current())
455 .unwrap();
456 assert_eq!(again.device(), DLDevice::cuda(0));
457 }
458
459 #[test]
460 fn cpu_tagged_path_unchanged() {
461 let a =
462 array_from_host_f64_on_device(&[1, 3], vec![1.0, 2.0, 3.0], DLDevice::cpu()).unwrap();
463 assert_eq!(a.device(), DLDevice::cpu());
464 let t = a
465 .as_dlpack(DLDevice::cpu(), None, DLPackVersion::current())
466 .unwrap();
467 assert_eq!(t.device(), DLDevice::cpu());
468 let back = from_dlpack_f64(&t).unwrap();
469 assert_eq!(back.device(), DLDevice::cpu());
470 }
471}