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 aedb1ede [FEAT][Rust] Allocate tensors with the environment allocator
(#827)
aedb1ede is described below
commit aedb1ede7a19f4052bd6afede9ca8c8ad0126ae6
Author: Gabriel Wu <[email protected]>
AuthorDate: Mon Sep 28 22:22:55 2026 +0800
[FEAT][Rust] Allocate tensors with the environment allocator (#827)
## Why
C++ kernel libraries allocate intermediate tensors with
`Tensor::FromEnvAlloc(TVMFFIEnvTensorAlloc, ...)`, which `tensor.h`
recommends: the memory comes from the DLPack allocator the caller set
with `TVMFFIEnvSetDLPackManagedTensorAllocator`. The Python binding, for
example, sets the framework's allocator while it calls a function with
that framework's tensors. The Rust crates have no way to do this:
- `tvm-ffi-sys` does not bind `TVMFFIEnvTensorAlloc`.
- `TVMFFIEnvGetDLPackManagedTensorAllocator` is declared to return a
non-nullable `DLPackManagedTensorAllocator`, but the C API returns null
when no allocator is set. Calling it on a thread without an allocator
produces an invalid value. `TVMFFIEnvSetDLPackManagedTensorAllocator`
likewise cannot unset the allocator or report an unset original one.
## What
- `tvm-ffi-sys`:
- bind `TVMFFIEnvTensorAlloc`;
- the getter returns, and the setter takes and writes back,
`Option<DLPackManagedTensorAllocator>`. `Option` of a function pointer
has the same ABI as the nullable C pointer, so only the Rust types
change.
- `tvm-ffi`: add `Tensor::from_env_alloc(shape, dtype, device) ->
Result<Tensor>`, the counterpart of C++ `Tensor::FromEnvAlloc`:
- it fills a prototype with the device, dtype and shape and calls
`TVMFFIEnvTensorAlloc`;
- it returns the allocator's error, which is a `RuntimeError` when no
allocator is set.
---
rust/tvm-ffi-sys/src/c_env_api.rs | 15 +++-
rust/tvm-ffi/src/collections/tensor.rs | 37 +++++++++
rust/tvm-ffi/tests/test_tensor.rs | 144 +++++++++++++++++++++++++++++++++
3 files changed, 193 insertions(+), 3 deletions(-)
diff --git a/rust/tvm-ffi-sys/src/c_env_api.rs
b/rust/tvm-ffi-sys/src/c_env_api.rs
index 94feec12..25fed43c 100644
--- a/rust/tvm-ffi-sys/src/c_env_api.rs
+++ b/rust/tvm-ffi-sys/src/c_env_api.rs
@@ -53,13 +53,22 @@ unsafe extern "C" {
pub fn TVMFFIEnvGetStream(device_type: i32, device_id: i32) ->
TVMFFIStreamHandle;
+ /// Set the allocator of this thread, or unset it with `None`; the
+ /// original allocator, possibly `None`, is written to
+ /// `opt_out_original_allocator` unless it is null.
pub fn TVMFFIEnvSetDLPackManagedTensorAllocator(
- allocator: DLPackManagedTensorAllocator,
+ allocator: Option<DLPackManagedTensorAllocator>,
write_to_global_context: i32,
- opt_out_original_allocator: *mut DLPackManagedTensorAllocator,
+ opt_out_original_allocator: *mut Option<DLPackManagedTensorAllocator>,
) -> i32;
- pub fn TVMFFIEnvGetDLPackManagedTensorAllocator() ->
DLPackManagedTensorAllocator;
+ /// The current allocator, or `None` when none is set.
+ pub fn TVMFFIEnvGetDLPackManagedTensorAllocator() ->
Option<DLPackManagedTensorAllocator>;
+
+ /// Allocate a tensor with the current allocator. Only the device, dtype,
+ /// ndim and shape of `prototype` are used; `out` receives an owned
+ /// `ffi.Tensor` handle.
+ pub fn TVMFFIEnvTensorAlloc(prototype: *mut DLTensor, out: *mut
TVMFFIObjectHandle) -> i32;
pub fn TVMFFIEnvCheckSignals() -> i32;
diff --git a/rust/tvm-ffi/src/collections/tensor.rs
b/rust/tvm-ffi/src/collections/tensor.rs
index 78928049..ebf184dd 100644
--- a/rust/tvm-ffi/src/collections/tensor.rs
+++ b/rust/tvm-ffi/src/collections/tensor.rs
@@ -24,6 +24,7 @@ use crate::error::Result;
use crate::object::{Object, ObjectArc, ObjectCore, ObjectCoreWithExtraItems};
use tvm_ffi_sys::dlpack::{DLDataType, DLDevice, DLDeviceType, DLTensor};
use tvm_ffi_sys::TVMFFITypeIndex as TypeIndex;
+use tvm_ffi_sys::{TVMFFIEnvTensorAlloc, TVMFFIObjectHandle};
//-----------------------------------------------------
// NDAllocator Trait
@@ -298,6 +299,42 @@ impl Tensor {
}
}
}
+ /// Create a Tensor with the environment allocator of this thread, as C++
+ /// `Tensor::FromEnvAlloc(TVMFFIEnvTensorAlloc, ...)` does.
+ ///
+ /// Kernel libraries allocate intermediate tensors this way, so that they
+ /// come from the caller's allocator, such as the one a framework sets
+ /// with `TVMFFIEnvSetDLPackManagedTensorAllocator`.
+ ///
+ /// # Arguments
+ /// * `shape` - The shape of the Tensor
+ /// * `dtype` - The data type of the Tensor
+ /// * `device` - The device of the Tensor
+ ///
+ /// # Returns
+ /// * `Result<Tensor>` - The created Tensor, or the error the allocator
+ /// raised, which is a `RuntimeError` when no allocator is set
+ pub fn from_env_alloc(shape: &[i64], dtype: DLDataType, device: DLDevice)
-> Result<Self> {
+ let mut prototype = DLTensor {
+ data: std::ptr::null_mut(),
+ device,
+ ndim: shape.len() as i32,
+ dtype,
+ shape: shape.as_ptr() as *mut i64,
+ strides: std::ptr::null_mut(),
+ byte_offset: 0,
+ };
+ let mut out: TVMFFIObjectHandle = std::ptr::null_mut();
+ unsafe {
+ if TVMFFIEnvTensorAlloc(&mut prototype, &mut out) != 0 {
+ return Err(crate::error::Error::from_raised());
+ }
+ Ok(Self {
+ data: ObjectArc::from_raw(out as *const TensorObj),
+ })
+ }
+ }
+
/// Create a Tensor from a slice
///
/// # Arguments
diff --git a/rust/tvm-ffi/tests/test_tensor.rs
b/rust/tvm-ffi/tests/test_tensor.rs
index d7e8f9bd..1a6207ca 100644
--- a/rust/tvm-ffi/tests/test_tensor.rs
+++ b/rust/tvm-ffi/tests/test_tensor.rs
@@ -110,3 +110,147 @@ fn test_any_tensor() {
assert_eq!(converted_view.shape(), &shape);
assert_eq!(converted_view.dtype().code, DLDataTypeCode::kDLFloat as u8);
}
+
+// ============================================================================
+// Environment allocator tests
+// ============================================================================
+
+mod env_alloc {
+ use std::ffi::{c_char, c_void, CStr};
+ use tvm_ffi::tvm_ffi_sys::{
+ dlpack::DLTensor, DLPackManagedTensorAllocator,
TVMFFIEnvSetDLPackManagedTensorAllocator,
+ };
+ use tvm_ffi::*;
+
+ /// DLPack's `DLManagedTensorVersioned`.
+ #[repr(C)]
+ struct ManagedTensor {
+ version: [u32; 2],
+ manager_ctx: *mut c_void,
+ deleter: Option<unsafe extern "C" fn(*mut ManagedTensor)>,
+ flags: u64,
+ dl_tensor: DLTensor,
+ }
+
+ /// The memory of a tensor made by `cpu_allocator`.
+ struct Owned {
+ shape: Vec<i64>,
+ data: Vec<u8>,
+ }
+
+ unsafe extern "C" fn delete(tensor: *mut ManagedTensor) {
+ let tensor = Box::from_raw(tensor);
+ drop(Box::from_raw(tensor.manager_ctx as *mut Owned));
+ }
+
+ unsafe extern "C" fn cpu_allocator(
+ prototype: *mut DLTensor,
+ out: *mut *mut c_void,
+ _error_ctx: *mut c_void,
+ _set_error: unsafe extern "C" fn(*mut c_void, *const c_char, *const
c_char),
+ ) -> i32 {
+ let prototype = &*prototype;
+ let shape = std::slice::from_raw_parts(prototype.shape, prototype.ndim
as usize).to_vec();
+ let bytes = shape.iter().product::<i64>() as usize *
(prototype.dtype.bits as usize / 8);
+ let mut owned = Box::new(Owned {
+ shape,
+ data: vec![0; bytes],
+ });
+ let dl_tensor = DLTensor {
+ data: owned.data.as_mut_ptr() as *mut c_void,
+ device: prototype.device,
+ ndim: prototype.ndim,
+ dtype: prototype.dtype,
+ shape: owned.shape.as_mut_ptr(),
+ strides: std::ptr::null_mut(),
+ byte_offset: 0,
+ };
+ let tensor = Box::new(ManagedTensor {
+ version: [1, 1],
+ manager_ctx: Box::into_raw(owned) as *mut c_void,
+ deleter: Some(delete),
+ flags: 0,
+ dl_tensor,
+ });
+ *out = Box::into_raw(tensor) as *mut c_void;
+ 0
+ }
+
+ unsafe extern "C" fn failing_allocator(
+ _prototype: *mut DLTensor,
+ _out: *mut *mut c_void,
+ error_ctx: *mut c_void,
+ set_error: unsafe extern "C" fn(*mut c_void, *const c_char, *const
c_char),
+ ) -> i32 {
+ let kind = CStr::from_bytes_with_nul(b"MemoryError\0").unwrap();
+ let message = CStr::from_bytes_with_nul(b"out of test
memory\0").unwrap();
+ set_error(error_ctx, kind.as_ptr(), message.as_ptr());
+ -1
+ }
+
+ /// Runs `f` with `allocator` set on this thread, then restores the
+ /// original allocator.
+ fn with_allocator<R>(
+ allocator: Option<DLPackManagedTensorAllocator>,
+ f: impl FnOnce() -> R,
+ ) -> R {
+ let mut original = None;
+ unsafe {
+ assert_eq!(
+ TVMFFIEnvSetDLPackManagedTensorAllocator(allocator, 0, &mut
original),
+ 0
+ );
+ }
+ let result = f();
+ unsafe {
+ assert_eq!(
+ TVMFFIEnvSetDLPackManagedTensorAllocator(original, 0,
std::ptr::null_mut()),
+ 0
+ );
+ }
+ result
+ }
+
+ fn f32_on_cpu() -> (DLDataType, DLDevice) {
+ (
+ DLDataType::new(DLDataTypeCode::kDLFloat, 32, 1),
+ DLDevice::new(DLDeviceType::kDLCPU, 0),
+ )
+ }
+
+ #[test]
+ fn allocates_with_the_environment_allocator() {
+ let (dtype, device) = f32_on_cpu();
+ let tensor = with_allocator(Some(cpu_allocator), || {
+ Tensor::from_env_alloc(&[1, 2, 3], dtype, device)
+ })
+ .unwrap();
+ assert_eq!(tensor.shape(), &[1, 2, 3]);
+ assert_eq!(tensor.strides(), &[6, 3, 1]);
+ assert_eq!(tensor.dtype(), dtype);
+ assert_eq!(tensor.device(), device);
+ assert!(!tensor.data_ptr().is_null());
+ tensor.data_as_slice::<f32>().unwrap();
+ }
+
+ #[test]
+ fn raises_the_allocator_error() {
+ let (dtype, device) = f32_on_cpu();
+ let error = with_allocator(Some(failing_allocator), || {
+ Tensor::from_env_alloc(&[4], dtype, device)
+ })
+ .err()
+ .unwrap();
+ assert_eq!(error.kind().as_str(), "MemoryError");
+ assert_eq!(error.message(), "out of test memory");
+ }
+
+ #[test]
+ fn fails_without_an_allocator() {
+ let (dtype, device) = f32_on_cpu();
+ let error = with_allocator(None, || Tensor::from_env_alloc(&[4],
dtype, device))
+ .err()
+ .unwrap();
+ assert_eq!(error.kind(), RUNTIME_ERROR);
+ }
+}