Skip to content

Commit 1d4133f

Browse files
committed
Properly handle scalar tensors
1 parent aface2f commit 1d4133f

8 files changed

Lines changed: 256 additions & 21 deletions

File tree

src/data_types.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,7 @@ mod tests {
174174
}
175175

176176
#[test]
177+
#[cfg_attr(miri, ignore)]
177178
fn test_dlpack_pointer_cast() {
178179
let value: u32 = 42;
179180
let mut mock_data = value;

src/lib.rs

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -475,9 +475,13 @@ impl<'a> DLPackTensorRef<'a> {
475475

476476
/// Get the shape of this tensor
477477
pub fn shape(&self) -> &[i64] {
478-
assert!(!self.raw.shape.is_null());
479-
unsafe {
480-
return std::slice::from_raw_parts(self.raw.shape, self.n_dims());
478+
if self.raw.shape.is_null() {
479+
assert!(self.raw.ndim == 0, "Shape pointer is null but ndim is not 0");
480+
return &[];
481+
} else {
482+
unsafe {
483+
return std::slice::from_raw_parts(self.raw.shape, self.n_dims());
484+
}
481485
}
482486
}
483487

src/ndarray/mod.rs

Lines changed: 83 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -423,6 +423,7 @@ where
423423
ctx.array.as_ptr()
424424
};
425425

426+
let ndim = ctx.shape.len() as i32;
426427
let dl_tensor = sys::DLTensor {
427428
// Casting to a mut pointer is not necessarily safe, but is required
428429
// by DLPack. The data can be mutated through this pointer, we
@@ -433,10 +434,10 @@ where
433434
device_type: sys::DLDeviceType::kDLCPU,
434435
device_id: 0,
435436
},
436-
ndim: ctx.shape.len() as i32,
437+
ndim: ndim,
437438
dtype: T::get_dlpack_data_type(),
438-
shape: ctx.shape.as_mut_ptr(),
439-
strides: ctx.strides.as_mut_ptr(),
439+
shape: if ndim == 0 { std::ptr::null_mut() } else { ctx.shape.as_mut_ptr() },
440+
strides: if ndim == 0 { std::ptr::null_mut() } else { ctx.strides.as_mut_ptr() },
440441
byte_offset: 0,
441442
};
442443

@@ -489,8 +490,8 @@ where
489490
},
490491
ndim,
491492
dtype: T::get_dlpack_data_type(),
492-
shape: ctx.shape.as_mut_ptr(),
493-
strides: ctx.strides.as_mut_ptr(),
493+
shape: if ndim == 0 { std::ptr::null_mut() } else { ctx.shape.as_mut_ptr() },
494+
strides: if ndim == 0 { std::ptr::null_mut() } else { ctx.strides.as_mut_ptr() },
494495
byte_offset: 0,
495496
};
496497

@@ -786,7 +787,7 @@ mod tests {
786787
assert_eq!(array_view.shape(), &[0, 0, 0]);
787788
}
788789

789-
unsafe extern "C" fn empty_deleter(tensor: *mut sys::DLManagedTensorVersioned) {
790+
unsafe extern "C" fn box_deleter(tensor: *mut sys::DLManagedTensorVersioned) {
790791
let _ = Box::from_raw(tensor);
791792
}
792793

@@ -811,7 +812,7 @@ mod tests {
811812
let managed = Box::new(crate::sys::DLManagedTensorVersioned {
812813
version: crate::sys::DLPackVersion::current(),
813814
manager_ctx: std::ptr::null_mut(),
814-
deleter: Some(empty_deleter),
815+
deleter: Some(box_deleter),
815816
flags: 0,
816817
dl_tensor,
817818
});
@@ -820,4 +821,79 @@ mod tests {
820821
let array: Array3<f32> = tensor.try_into().unwrap();
821822
assert_eq!(array.shape(), &[0, 0, 0]);
822823
}
824+
825+
#[test]
826+
fn scalar_ndarray_to_dlpack() {
827+
let array = arr0(42.0f64);
828+
let tensor: DLPackTensor = array.try_into().unwrap();
829+
assert_eq!(tensor.n_dims(), 0);
830+
assert!(tensor.as_dltensor().shape.is_null());
831+
assert!(tensor.shape().is_empty());
832+
assert!(tensor.as_dltensor().strides.is_null());
833+
assert!(tensor.strides().is_none());
834+
}
835+
836+
#[test]
837+
fn scalar_arc_array_to_dlpack() {
838+
let array = ndarray::ArcArray::<f64, ndarray::Ix0>::from_elem((), 42.0f64);
839+
let tensor: DLPackTensor = array.try_into().unwrap();
840+
assert_eq!(tensor.n_dims(), 0);
841+
assert!(tensor.as_dltensor().shape.is_null());
842+
assert!(tensor.shape().is_empty());
843+
assert!(tensor.as_dltensor().strides.is_null());
844+
assert!(tensor.strides().is_none());
845+
}
846+
847+
#[test]
848+
fn scalar_dlpack_to_ndarray_view() {
849+
let mut value = 3.41f32;
850+
851+
let dl_tensor = DLTensor {
852+
data: (&mut value as *mut f32).cast(),
853+
device: DLDevice {
854+
device_type: DLDeviceType::kDLCPU,
855+
device_id: 0,
856+
},
857+
ndim: 0,
858+
dtype: f32::get_dlpack_data_type(),
859+
shape: std::ptr::null_mut(),
860+
strides: std::ptr::null_mut(),
861+
byte_offset: 0,
862+
};
863+
864+
let dlpack_ref = unsafe { DLPackTensorRef::from_raw(dl_tensor) };
865+
let array_view = ArrayView0::<f32>::try_from(dlpack_ref).unwrap();
866+
assert!(array_view.shape().is_empty());
867+
assert_eq!(array_view[()], 3.41);
868+
}
869+
870+
#[test]
871+
fn scalar_dlpack_to_ndarray_owned() {
872+
let mut value = 2.72f64;
873+
874+
let dl_tensor = DLTensor {
875+
data: (&mut value as *mut f64).cast(),
876+
device: DLDevice {
877+
device_type: DLDeviceType::kDLCPU,
878+
device_id: 0,
879+
},
880+
ndim: 0,
881+
dtype: f64::get_dlpack_data_type(),
882+
shape: std::ptr::null_mut(),
883+
strides: std::ptr::null_mut(),
884+
byte_offset: 0,
885+
};
886+
887+
let managed = Box::new(crate::sys::DLManagedTensorVersioned {
888+
version: crate::sys::DLPackVersion::current(),
889+
manager_ctx: std::ptr::null_mut(),
890+
deleter: Some(box_deleter),
891+
flags: 0,
892+
dl_tensor,
893+
});
894+
895+
let tensor = unsafe { DLPackTensor::from_ptr(Box::into_raw(managed)) };
896+
let array: Array0<f64> = tensor.try_into().unwrap();
897+
assert_eq!(array[()], 2.72);
898+
}
823899
}

src/ndarray/sync.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -412,4 +412,37 @@ mod tests {
412412
assert_eq!(tensor.shape(), &[0, 0, 0]);
413413
assert!(tensor.as_dltensor().data.is_null());
414414
}
415+
416+
#[test]
417+
fn scalar_mutex_to_dlpack() {
418+
let array = Arc::new(Mutex::new(ndarray::arr0(42.0f64)));
419+
let tensor: DLPackTensor = Arc::clone(&array).try_into().unwrap();
420+
assert_eq!(tensor.n_dims(), 0);
421+
assert!(tensor.shape().is_empty());
422+
unsafe {
423+
assert_eq!(std::ptr::read(tensor.data_ptr::<f64>().unwrap()), 42.0);
424+
}
425+
}
426+
427+
#[test]
428+
fn scalar_rwlock_write_to_dlpack() {
429+
let array = Arc::new(RwLock::new(ndarray::arr0(3.41f64)));
430+
let tensor: DLPackTensor = ReadWrite(Arc::clone(&array)).try_into().unwrap();
431+
assert_eq!(tensor.n_dims(), 0);
432+
assert!(tensor.shape().is_empty());
433+
unsafe {
434+
assert_eq!(std::ptr::read(tensor.data_ptr::<f64>().unwrap()), 3.41);
435+
}
436+
}
437+
438+
#[test]
439+
fn scalar_rwlock_read_to_dlpack() {
440+
let array = Arc::new(RwLock::new(ndarray::arr0(2.72f64)));
441+
let tensor: DLPackTensor = ReadOnly(Arc::clone(&array)).try_into().unwrap();
442+
assert_eq!(tensor.n_dims(), 0);
443+
assert!(tensor.shape().is_empty());
444+
unsafe {
445+
assert_eq!(std::ptr::read(tensor.data_ptr::<f64>().unwrap()), 2.72);
446+
}
447+
}
415448
}

src/pyo3.rs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -522,6 +522,58 @@ result_capsule = array.__dlpack__()
522522
})
523523
}
524524

525+
#[test]
526+
#[cfg_attr(miri, ignore)]
527+
fn test_scalar_ndarray_to_numpy() -> PyResult<()> {
528+
Python::initialize();
529+
Python::attach(|py| {
530+
let rust_array = ndarray::arr0(3.41f64);
531+
let dl_tensor = DLPackTensor::try_from(rust_array).unwrap();
532+
let tensor = PyDLPack::try_from(dl_tensor).unwrap();
533+
534+
let locals = PyDict::new(py);
535+
locals.set_item("np", py.import("numpy")?)?;
536+
locals.set_item("tensor", tensor)?;
537+
538+
let code = c_str!(
539+
"
540+
array = np.from_dlpack(tensor)
541+
assert array.ndim == 0, f\"got ndim {array.ndim}\"
542+
assert array.size == 1
543+
assert np.allclose(array, 3.41)
544+
"
545+
);
546+
py.run(code, None, Some(&locals))?;
547+
Ok(())
548+
})
549+
}
550+
551+
#[test]
552+
#[cfg_attr(miri, ignore)]
553+
fn test_scalar_numpy_to_ndarray() -> PyResult<()> {
554+
Python::initialize();
555+
Python::attach(|py| {
556+
let locals = PyDict::new(py);
557+
locals.set_item("np", py.import("numpy")?)?;
558+
559+
let code = c_str!(
560+
"
561+
array = np.array(42.0)
562+
result_capsule = array.__dlpack__()
563+
"
564+
);
565+
py.run(code, None, Some(&locals))?;
566+
567+
let result = locals.get_item("result_capsule")?.unwrap();
568+
let capsule: Bound<PyCapsule> = result.extract()?;
569+
570+
let dlpack_ref = DLPackTensorRef::try_from(capsule)?;
571+
let array = ndarray::ArrayView0::<f64>::try_from(dlpack_ref).unwrap();
572+
assert_eq!(array[()], 42.0);
573+
Ok(())
574+
})
575+
}
576+
525577
#[test]
526578
#[cfg_attr(miri, ignore)]
527579
fn test_v1_0_null_strides_allowed() -> PyResult<()> {

src/sync.rs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -395,4 +395,35 @@ mod tests {
395395
assert_eq!(tensor.shape(), &[0]);
396396
assert!(tensor.as_dltensor().data.is_null());
397397
}
398+
399+
#[test]
400+
fn scalar_mutex_to_dlpack() {
401+
let data = Arc::new(Mutex::new(vec![42i32]));
402+
let tensor: DLPackTensor = Arc::clone(&data).try_into().unwrap();
403+
assert_eq!(tensor.shape(), &[1]);
404+
405+
unsafe {
406+
assert_eq!(std::ptr::read(tensor.data_ptr::<i32>().unwrap()), 42);
407+
}
408+
}
409+
410+
#[test]
411+
fn scalar_rwlock_write_to_dlpack() {
412+
let data = Arc::new(RwLock::new(vec![42i16]));
413+
let tensor: DLPackTensor = ReadWrite(Arc::clone(&data)).try_into().unwrap();
414+
assert_eq!(tensor.shape(), &[1]);
415+
unsafe {
416+
assert_eq!(std::ptr::read(tensor.data_ptr::<i16>().unwrap()), 42);
417+
}
418+
}
419+
420+
#[test]
421+
fn scalar_rwlock_read_to_dlpack() {
422+
let data = Arc::new(RwLock::new(vec![42u64]));
423+
let tensor: DLPackTensor = ReadOnly(Arc::clone(&data)).try_into().unwrap();
424+
assert_eq!(tensor.shape(), &[1]);
425+
unsafe {
426+
assert_eq!(std::ptr::read(tensor.data_ptr::<u64>().unwrap()), 42);
427+
}
428+
}
398429
}

src/sys.rs

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -256,7 +256,7 @@ pub struct DLTensor {
256256
///
257257
/// Note that as of Nov 2021, multiply libraries (CuPy, PyTorch, TensorFlow,
258258
/// TVM, perhaps others) do not adhere to this 256 byte alignment
259-
/// requirement on CPU/CUDA/ROCm, and always use `byte_offset=0`. This must
259+
/// requirement on CPU/CUDA/ROCm, and always use `byte_offset=0`. This must
260260
/// be fixed (after which this note will be updated); at the moment it is
261261
/// recommended to not rely on the data pointer being correctly aligned.
262262
///
@@ -287,17 +287,15 @@ pub struct DLTensor {
287287
pub shape: *mut i64,
288288
/// Strides of the tensor (in number of elements, not bytes).
289289
///
290-
/// can not be NULL if ndim != 0, must points to
291-
/// an array of ndim elements that specifies the strides,
292-
/// so consumer can always rely on strides[dim] being valid for 0 <= dim < ndim.
290+
/// can not be NULL if ndim != 0, must points to an array of ndim elements
291+
/// that specifies the strides, so consumer can always rely on strides[dim]
292+
/// being valid for 0 <= dim < ndim.
293293
///
294-
/// When ndim == 0, strides can be set to NULL.
294+
/// When ndim == 0, strides can be set to NULL.
295295
///
296-
/// NOTE: Before DLPack v1.2, strides can be NULL to indicate contiguous data.
297-
/// This is not allowed in DLPack v1.2 and later. The rationale
296+
/// NOTE: Before DLPack v1.2, strides can be NULL to indicate contiguous
297+
/// data. This is not allowed in DLPack v1.2 and later. The rationale
298298
/// is to simplify the consumer handling.
299-
///
300-
/// When ndim == 0, strides may represent NULL.
301299
pub strides: *mut i64,
302300
/// The offset in bytes to the beginning pointer to data
303301
pub byte_offset: u64,

src/vec.rs

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -304,7 +304,7 @@ mod tests {
304304
}
305305
}
306306

307-
unsafe extern "C" fn empty_deleter(tensor: *mut sys::DLManagedTensorVersioned) {
307+
unsafe extern "C" fn box_deleter(tensor: *mut sys::DLManagedTensorVersioned) {
308308
let _ = Box::from_raw(tensor);
309309
}
310310

@@ -329,7 +329,7 @@ mod tests {
329329
let managed = Box::new(crate::sys::DLManagedTensorVersioned {
330330
version: crate::sys::DLPackVersion::current(),
331331
manager_ctx: std::ptr::null_mut(),
332-
deleter: Some(empty_deleter),
332+
deleter: Some(box_deleter),
333333
flags: 0,
334334
dl_tensor,
335335
});
@@ -338,4 +338,44 @@ mod tests {
338338
let vec: Vec<i32> = tensor.try_into().unwrap();
339339
assert!(vec.is_empty());
340340
}
341+
342+
#[test]
343+
fn scalar_vec_to_dlpack() {
344+
let data = vec![42i32];
345+
let tensor: DLPackTensor = data.try_into().unwrap();
346+
assert_eq!(tensor.shape(), &[1]);
347+
assert!(!tensor.as_dltensor().data.is_null());
348+
}
349+
350+
#[test]
351+
fn scalar_dlpack_to_vec() {
352+
let mut shape = vec![1i64];
353+
let mut strides = vec![1i64];
354+
let mut value = 42f64;
355+
356+
let dl_tensor = crate::sys::DLTensor {
357+
data: (&mut value as *mut f64).cast(),
358+
device: crate::sys::DLDevice {
359+
device_type: crate::sys::DLDeviceType::kDLCPU,
360+
device_id: 0,
361+
},
362+
ndim: 1,
363+
dtype: f64::get_dlpack_data_type(),
364+
shape: shape.as_mut_ptr(),
365+
strides: strides.as_mut_ptr(),
366+
byte_offset: 0,
367+
};
368+
369+
let managed = Box::new(crate::sys::DLManagedTensorVersioned {
370+
version: crate::sys::DLPackVersion::current(),
371+
manager_ctx: std::ptr::null_mut(),
372+
deleter: Some(box_deleter),
373+
flags: 0,
374+
dl_tensor,
375+
});
376+
377+
let tensor = unsafe { DLPackTensor::from_ptr(Box::into_raw(managed)) };
378+
let vec: Vec<f64> = tensor.try_into().unwrap();
379+
assert_eq!(vec, vec![42.0]);
380+
}
341381
}

0 commit comments

Comments
 (0)