This is an automated email from the ASF dual-hosted git repository.

tqchen pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tvm-ffi.git


The following commit(s) were added to refs/heads/main by this push:
     new 2eb546fd [FEAT][Rust] Add TensorView for DLTensor* arguments (#813)
2eb546fd is described below

commit 2eb546fd93d70b4424266e174a3e7aad81c58f76
Author: Gabriel Wu <[email protected]>
AuthorDate: Wed Sep 30 07:09:26 2026 +0800

    [FEAT][Rust] Add TensorView for DLTensor* arguments (#813)
    
    ## Why
    
    C and C++ callers pass tensors to tvm-ffi functions as borrowed
    `DLTensor*` (`kTVMFFIDLTensorPtr`), which is also how C++ `TensorView`
    arguments are passed on. The Rust `Tensor` only accepts `ffi.Tensor`
    objects, so a Rust function exported with
    `tvm_ffi_dll_export_typed_func!` fails for those callers with `Cannot
    convert from type 'DLTensor*' to 'ffi.Tensor'`. Python callers are not
    affected, because they pass `ffi.Tensor`.
    
    ## What
    
    `tvm_ffi::TensorView` mirrors C++ `tvm::ffi::TensorView`:
    - it accepts `DLTensor*` and `ffi.Tensor` arguments and is passed on as
    a `DLTensor*`;
    - it is non-owning: it holds a pointer to the `DLTensor`, and the caller
    keeps the tensor alive, as in C++;
    - its accessors follow `Tensor`'s: `data_ptr`, `device`, `dtype`,
    `ndim`, `shape`, `numel`, `strides`, `is_contiguous`. `strides` panics
    on null strides, which DLPack forbids since v1.2, as C++
    `TensorView::strides` checks. `is_contiguous` follows C++
    `tvm::ffi::IsContiguous`: null strides and tensors without elements are
    contiguous, and a dimension of extent 1 may have any stride.
    - as with C++ `TypeTraits<DLTensor*>` / `TypeTraits<TensorView>`, a view
    can be borrowed as an `AnyView` but not moved into or out of an owned
    `Any`, since it does not retain ownership; doing so panics with the C++
    error message.
    - `TryFrom<AnyView>` converts a `DLTensor*` or a tensor object to a
    view, as C++ `AnyView::cast<TensorView>` / `try_cast` do.
    `try_as::<TensorView>()` stays aligned with C++ `as` and accepts only a
    `DLTensor*`. There is no `TryFrom<Any>`, since the view would point into
    a value the conversion drops.
    
    Typed functions require `'static` argument types, so the view carries no
    lifetime. The alternative, borrow-checked views of call arguments, would
    need changes to typed-function argument conversion and is left for a
    follow-up discussion.
    
    ## Tests
    
    `rust/tvm-ffi/tests/test_tensor_view.rs`: the same typed function called
    with a `DLTensor*` built as a C caller would and with an `ffi.Tensor`,
    rejection of other types, contiguity (including null strides, empty
    tensors and extent-1 dimensions), the null-strides panic of `strides`,
    the panic on moving a view into `Any`, and `TryFrom<AnyView>` from both
    a tensor object and a `DLTensor*`, with other values rejected. `cargo
    test` passes against libtvm_ffi built from `main`.
    
    Also checked end to end: a Rust `cdylib` exporting a CUDA kernel entry
    with `TensorView` arguments, called from C with `DLTensor*` and from
    Python with torch tensors.
    
    🤖 Generated with [Claude Code](https://claude.com/claude-code)
    
    ---------
    
    Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
---
 rust/tvm-ffi/src/collections/tensor.rs | 185 +++++++++++++++++++++++++++++++++
 rust/tvm-ffi/src/lib.rs                |   2 +-
 rust/tvm-ffi/tests/test_tensor_view.rs | 155 +++++++++++++++++++++++++++
 3 files changed, 341 insertions(+), 1 deletion(-)

diff --git a/rust/tvm-ffi/src/collections/tensor.rs 
b/rust/tvm-ffi/src/collections/tensor.rs
index ebf184dd..bb92f3ed 100644
--- a/rust/tvm-ffi/src/collections/tensor.rs
+++ b/rust/tvm-ffi/src/collections/tensor.rs
@@ -16,13 +16,16 @@
  * specific language governing permissions and limitations
  * under the License.
  */
+use crate::any::{AnyView, TryFromTemp};
 use crate::collections::shape::Shape;
 use crate::derive::{Object, ObjectRef};
 use crate::dtype::AsDLDataType;
 use crate::dtype::DLDataTypeExt;
 use crate::error::Result;
 use crate::object::{Object, ObjectArc, ObjectCore, ObjectCoreWithExtraItems};
+use crate::type_traits::AnyCompatible;
 use tvm_ffi_sys::dlpack::{DLDataType, DLDevice, DLDeviceType, DLTensor};
+use tvm_ffi_sys::TVMFFIAny;
 use tvm_ffi_sys::TVMFFITypeIndex as TypeIndex;
 use tvm_ffi_sys::{TVMFFIEnvTensorAlloc, TVMFFIObjectHandle};
 
@@ -379,3 +382,185 @@ unsafe impl NDAllocator for CPUNDAlloc {
         std::alloc::dealloc(tensor.data as *mut u8, layout);
     }
 }
+
+/// A non-owning view of a tensor: a pointer to a `DLTensor`.
+///
+/// This mirrors `tvm::ffi::TensorView` in C++. A function argument of this
+/// type accepts both a borrowed `DLTensor*` (type index `kTVMFFIDLTensorPtr`,
+/// how C and C++ callers pass tensors) and a `Tensor` object (type index
+/// `kTVMFFITensor`), and is passed on as a `DLTensor*`. Prefer it over
+/// [`Tensor`] for arguments of exported functions that only read a tensor's
+/// metadata and data.
+///
+/// The view does not keep anything alive. The caller must ensure that the
+/// `DLTensor` it points to, and the memory that the `DLTensor` points to
+/// (data, shape and strides), outlive every use of the view. For an argument
+/// of an exported function, that is the duration of the call. For the same
+/// reason, as in C++, a view cannot be moved into an owned 
[`Any`](crate::Any);
+/// use [`Tensor`] instead.
+#[derive(Clone, Copy, Debug)]
+pub struct TensorView {
+    tensor: *const DLTensor,
+}
+
+impl TensorView {
+    /// Creates a view of a `DLTensor`.
+    ///
+    /// # Safety
+    /// `tensor` must be non-null and valid for every use of the view.
+    pub unsafe fn from_raw(tensor: *const DLTensor) -> Self {
+        assert!(!tensor.is_null(), "TensorView of a null DLTensor");
+        Self { tensor }
+    }
+
+    /// The viewed `DLTensor`.
+    pub fn as_raw(&self) -> *const DLTensor {
+        self.tensor
+    }
+
+    fn dltensor(&self) -> &DLTensor {
+        unsafe { &*self.tensor }
+    }
+
+    /// The data pointer, as in the `DLTensor`; `byte_offset` is not applied.
+    pub fn data_ptr(&self) -> *mut core::ffi::c_void {
+        self.dltensor().data
+    }
+
+    pub fn device(&self) -> DLDevice {
+        self.dltensor().device
+    }
+
+    pub fn dtype(&self) -> DLDataType {
+        self.dltensor().dtype
+    }
+
+    pub fn ndim(&self) -> usize {
+        self.dltensor().ndim as usize
+    }
+
+    pub fn shape(&self) -> &[i64] {
+        let t = self.dltensor();
+        if t.ndim == 0 {
+            return &[];
+        }
+        unsafe { std::slice::from_raw_parts(t.shape, t.ndim as usize) }
+    }
+
+    pub fn numel(&self) -> usize {
+        self.shape().iter().product::<i64>() as usize
+    }
+
+    /// The strides, in elements.
+    ///
+    /// # Panics
+    /// If the `DLTensor` has null strides and at least one dimension, which
+    /// DLPack forbids since v1.2, as C++ `TensorView::strides` checks.
+    pub fn strides(&self) -> &[i64] {
+        let t = self.dltensor();
+        if t.ndim == 0 {
+            return &[];
+        }
+        assert!(
+            !t.strides.is_null(),
+            "TensorView::strides of a DLTensor with null strides"
+        );
+        unsafe { std::slice::from_raw_parts(t.strides, t.ndim as usize) }
+    }
+
+    /// Whether the elements are laid out in row-major order without gaps, as
+    /// C++ `tvm::ffi::IsContiguous` decides: a `DLTensor` with null strides
+    /// or no elements is contiguous, and a dimension of extent 1 may have any
+    /// stride.
+    pub fn is_contiguous(&self) -> bool {
+        if self.dltensor().strides.is_null() {
+            return true;
+        }
+        let shape = self.shape();
+        if shape.contains(&0) {
+            return true;
+        }
+        let mut expected_stride = 1;
+        for (size, stride) in shape.iter().zip(self.strides()).rev() {
+            if *size == 1 {
+                continue;
+            }
+            if *stride != expected_stride {
+                return false;
+            }
+            expected_stride *= size;
+        }
+        true
+    }
+}
+
+impl From<&Tensor> for TensorView {
+    fn from(tensor: &Tensor) -> Self {
+        Self {
+            tensor: &tensor.data.dltensor as *const DLTensor,
+        }
+    }
+}
+
+// A borrowed `AnyView` converts as C++ `AnyView::cast<TensorView>` does: from
+// a `DLTensor*` or from a tensor object. `try_as`, like C++ `as`, accepts only
+// a `DLTensor*`. There is no conversion from an owned `Any`, which would leave
+// the view pointing into a value the conversion drops.
+impl<'a> TryFrom<AnyView<'a>> for TensorView {
+    type Error = crate::error::Error;
+    #[inline]
+    fn try_from(value: AnyView<'a>) -> Result<Self> {
+        TryFromTemp::<Self>::try_from(value).map(TryFromTemp::into_value)
+    }
+}
+
+// As in C++, where `TypeTraits<DLTensor*>::MoveToAny` throws this, and
+// `TypeTraits<TensorView>` does not support moves.
+const NOT_OWNED: &str =
+    "DLTensor* cannot be held in Any as it does not retain ownership, use 
Tensor instead";
+
+unsafe impl AnyCompatible for TensorView {
+    fn type_str() -> String {
+        // make it consistent with c++ representation
+        "DLTensor*".to_string()
+    }
+
+    unsafe fn copy_to_any_view(src: &Self, data: &mut TVMFFIAny) {
+        data.type_index = TypeIndex::kTVMFFIDLTensorPtr as i32;
+        data.small_str_len = 0;
+        data.data_union.v_uint64 = 0;
+        data.data_union.v_ptr = src.tensor as *mut core::ffi::c_void;
+    }
+
+    unsafe fn move_to_any(_src: Self, _data: &mut TVMFFIAny) {
+        panic!("{}", NOT_OWNED);
+    }
+
+    unsafe fn check_any_strict(data: &TVMFFIAny) -> bool {
+        data.type_index == TypeIndex::kTVMFFIDLTensorPtr as i32
+    }
+
+    unsafe fn copy_from_any_view_after_check(data: &TVMFFIAny) -> Self {
+        Self {
+            tensor: data.data_union.v_ptr as *const DLTensor,
+        }
+    }
+
+    unsafe fn move_from_any_after_check(_data: &mut TVMFFIAny) -> Self {
+        panic!("{}", NOT_OWNED);
+    }
+
+    unsafe fn try_cast_from_any_view(data: &TVMFFIAny) -> 
std::result::Result<Self, ()> {
+        if data.type_index == TypeIndex::kTVMFFIDLTensorPtr as i32 {
+            Ok(Self::copy_from_any_view_after_check(data))
+        } else if data.type_index == TypeIndex::kTVMFFITensor as i32 {
+            // A tensor object's DLTensor follows its object header.
+            let obj = data.data_union.v_obj as *const TensorObj;
+            Ok(Self {
+                tensor: &(*obj).dltensor as *const DLTensor,
+            })
+        } else {
+            Err(())
+        }
+    }
+}
diff --git a/rust/tvm-ffi/src/lib.rs b/rust/tvm-ffi/src/lib.rs
index 5d6425c0..8b3cbfd1 100644
--- a/rust/tvm-ffi/src/lib.rs
+++ b/rust/tvm-ffi/src/lib.rs
@@ -42,7 +42,7 @@ pub use crate::big_int::BigInt;
 pub use crate::collections::array::Array;
 pub use crate::collections::map::Map;
 pub use crate::collections::shape::Shape;
-pub use crate::collections::tensor::{CPUNDAlloc, NDAllocator, Tensor};
+pub use crate::collections::tensor::{CPUNDAlloc, NDAllocator, Tensor, 
TensorView};
 pub use crate::device::{current_stream, with_stream};
 pub use crate::dtype::DLDataTypeExt;
 pub use crate::error::{Error, ErrorKind, Result};
diff --git a/rust/tvm-ffi/tests/test_tensor_view.rs 
b/rust/tvm-ffi/tests/test_tensor_view.rs
new file mode 100644
index 00000000..c0fe1409
--- /dev/null
+++ b/rust/tvm-ffi/tests/test_tensor_view.rs
@@ -0,0 +1,155 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+use tvm_ffi::tvm_ffi_sys::dlpack::{DLDataType, DLDataTypeCode, DLDevice, 
DLDeviceType, DLTensor};
+use tvm_ffi::*;
+
+/// A 2x3 f32 DLTensor over `data`, with the given strides, as a C caller
+/// would build it.
+fn dltensor(data: &mut [f32; 6], shape: &mut [i64; 2], strides: *mut i64) -> 
DLTensor {
+    DLTensor {
+        data: data.as_mut_ptr() as *mut core::ffi::c_void,
+        device: DLDevice::new(DLDeviceType::kDLCPU, 0),
+        ndim: 2,
+        dtype: DLDataType::new(DLDataTypeCode::kDLFloat, 32, 1),
+        shape: shape.as_mut_ptr(),
+        strides,
+        byte_offset: 0,
+    }
+}
+
+/// `(ndim, shape[1], strides[0], first element)` of the argument.
+fn describe() -> Function {
+    Function::from_typed(|t: TensorView| -> Result<Array<i64>> {
+        let first = unsafe { *(t.data_ptr() as *const f32) } as i64;
+        Ok(Array::new(vec![
+            t.ndim() as i64,
+            t.shape()[1],
+            t.strides()[0],
+            first,
+        ]))
+    })
+}
+
+fn call(f: &Function, arg: AnyView) -> Result<Vec<i64>> {
+    let out = Array::<i64>::try_from(f.call_packed(&[arg])?)?;
+    (0..out.len()).map(|i| Ok(i64::try_from(out[i])?)).collect()
+}
+
+#[test]
+fn test_tensor_view_from_dltensor_ptr() {
+    let (mut data, mut shape, mut strides) = ([7.0f32, 1., 2., 3., 4., 5.], 
[2, 3], [3i64, 1]);
+    let raw = dltensor(&mut data, &mut shape, strides.as_mut_ptr());
+    let view = unsafe { TensorView::from_raw(&raw) };
+    let arg = AnyView::from(&view);
+    assert_eq!(arg.type_index(), TypeIndex::kTVMFFIDLTensorPtr as i32);
+    assert_eq!(call(&describe(), arg).unwrap(), vec![2, 3, 3, 7]);
+}
+
+#[test]
+fn test_tensor_view_from_tensor_object() {
+    let tensor = Tensor::from_slice(&[9.0f32, 1., 2., 3., 4., 5.], &[2, 
3]).unwrap();
+    let arg = AnyView::from(&tensor);
+    assert_eq!(arg.type_index(), TypeIndex::kTVMFFITensor as i32);
+    assert_eq!(call(&describe(), arg).unwrap(), vec![2, 3, 3, 9]);
+    let view = TensorView::from(&tensor);
+    assert_eq!(view.shape(), tensor.shape());
+    assert_eq!(view.strides(), tensor.strides());
+    assert_eq!(
+        view.data_ptr() as *const core::ffi::c_void,
+        tensor.data_ptr()
+    );
+}
+
+#[test]
+fn test_tensor_view_rejects_other_types() {
+    let err = call(&describe(), AnyView::from(&1i32)).unwrap_err();
+    assert!(err.message().contains("DLTensor*"), "{}", err.message());
+}
+
+#[test]
+fn test_tensor_view_contiguity() {
+    let (mut data, mut shape) = ([0f32; 6], [2i64, 3]);
+    let mut compact_strides = [3i64, 1];
+    let compact = dltensor(&mut data, &mut shape, 
compact_strides.as_mut_ptr());
+    let view = unsafe { TensorView::from_raw(&compact) };
+    assert!(view.is_contiguous());
+    assert_eq!(view.numel(), 6);
+    let mut padded_strides = [4i64, 1];
+    let padded = dltensor(&mut data, &mut shape, padded_strides.as_mut_ptr());
+    assert!(!unsafe { TensorView::from_raw(&padded) }.is_contiguous());
+    // As in C++ `IsContiguous`: null strides and empty tensors are
+    // contiguous, and a dimension of extent 1 may have any stride.
+    let unstrided = dltensor(&mut data, &mut shape, std::ptr::null_mut());
+    assert!(unsafe { TensorView::from_raw(&unstrided) }.is_contiguous());
+    let mut row_shape = [1i64, 3];
+    let mut row_strides = [7i64, 1];
+    let row = dltensor(&mut data, &mut row_shape, row_strides.as_mut_ptr());
+    assert!(unsafe { TensorView::from_raw(&row) }.is_contiguous());
+    let mut empty_shape = [0i64, 3];
+    let empty = dltensor(&mut data, &mut empty_shape, 
padded_strides.as_mut_ptr());
+    assert!(unsafe { TensorView::from_raw(&empty) }.is_contiguous());
+}
+
+#[test]
+#[should_panic(expected = "null strides")]
+fn test_tensor_view_null_strides() {
+    let (mut data, mut shape) = ([0f32; 6], [2i64, 3]);
+    let compact = dltensor(&mut data, &mut shape, std::ptr::null_mut());
+    unsafe { TensorView::from_raw(&compact) }.strides();
+}
+
+#[test]
+#[should_panic(expected = "does not retain ownership")]
+fn test_tensor_view_cannot_move_to_any() {
+    let tensor = Tensor::from_slice(&[0.0f32; 6], &[2, 3]).unwrap();
+    let _ = Any::from(TensorView::from(&tensor));
+}
+
+#[test]
+fn test_tensor_view_try_from_any_view() {
+    // From a tensor object, as C++ `AnyView::cast<TensorView>` converts it; 
the
+    // strict `try_as`, like C++ `as`, does not.
+    let tensor = Tensor::from_slice(&[5.0f32, 1., 2., 3., 4., 5.], &[2, 
3]).unwrap();
+    let object = AnyView::from(&tensor);
+    let view = TensorView::try_from(object).unwrap();
+    assert_eq!(
+        view.data_ptr() as *const core::ffi::c_void,
+        tensor.data_ptr()
+    );
+    assert_eq!(view.shape(), tensor.shape());
+    assert!(object.try_as::<TensorView>().is_none());
+
+    // From a DLTensor*, which both accept.
+    let (mut data, mut shape, mut strides) = ([7.0f32, 1., 2., 3., 4., 5.], 
[2, 3], [3i64, 1]);
+    let raw = dltensor(&mut data, &mut shape, strides.as_mut_ptr());
+    let borrowed = unsafe { TensorView::from_raw(&raw) };
+    let pointer = AnyView::from(&borrowed);
+    assert_eq!(
+        TensorView::try_from(pointer).unwrap().as_raw(),
+        &raw as *const DLTensor
+    );
+    assert_eq!(
+        pointer.try_as::<TensorView>().unwrap().as_raw(),
+        &raw as *const DLTensor
+    );
+
+    // Other values are rejected with the expected type.
+    let err = TensorView::try_from(AnyView::from(&1i32)).unwrap_err();
+    assert!(err.message().contains("DLTensor*"), "{}", err.message());
+}

Reply via email to