blob: 21b0310825d273041b0fdabdac5b2793fc9db843 [file]
# 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.
from typing import Any
__dlpack_version__: tuple[int, int] = (DLPACK_MAJOR_VERSION, DLPACK_MINOR_VERSION)
_CLASS_TENSOR = None
def _set_class_tensor(cls):
global _CLASS_TENSOR
_CLASS_TENSOR = cls
cdef const char* _c_str_dltensor = "dltensor"
cdef const char* _c_str_used_dltensor = "used_dltensor"
cdef const char* _c_str_dltensor_versioned = "dltensor_versioned"
cdef const char* _c_str_used_dltensor_versioned = "used_dltensor_versioned"
cdef const char* _c_str_dlpack_exchange_api = "dlpack_exchange_api"
cdef int _get_dlpack_exchange_api(
object dlpack_exchange_api_obj,
const DLPackExchangeAPI** out_ptr
) except -1:
if isinstance(dlpack_exchange_api_obj, int):
out_ptr[0] = <const DLPackExchangeAPI*>(<long long>dlpack_exchange_api_obj)
return 0
if pycapsule.PyCapsule_IsValid(dlpack_exchange_api_obj, _c_str_dlpack_exchange_api):
out_ptr[0] = <const DLPackExchangeAPI*>pycapsule.PyCapsule_GetPointer(
dlpack_exchange_api_obj, _c_str_dlpack_exchange_api
)
return 0
raise ValueError("Expect a dlpack_exchange_api field")
cdef void _c_dlpack_deleter(object pycaps):
cdef DLManagedTensor* dltensor
if pycapsule.PyCapsule_IsValid(pycaps, _c_str_dltensor):
dltensor = <DLManagedTensor*>pycapsule.PyCapsule_GetPointer(pycaps, _c_str_dltensor)
dltensor.deleter(dltensor)
cdef void _c_dlpack_versioned_deleter(object pycaps):
cdef DLManagedTensorVersioned* dltensor
if pycapsule.PyCapsule_IsValid(pycaps, _c_str_dltensor_versioned):
dltensor = <DLManagedTensorVersioned*>pycapsule.PyCapsule_GetPointer(
pycaps, _c_str_dltensor_versioned)
dltensor.deleter(dltensor)
cdef inline int _from_dlpack(
object dltensor, int require_alignment,
int require_contiguous, TVMFFIObjectHandle* out
) except -1:
cdef DLManagedTensor* ptr
cdef int c_api_ret_code
cdef int c_req_alignment = require_alignment
cdef int c_req_contiguous = require_contiguous
if pycapsule.PyCapsule_IsValid(dltensor, _c_str_dltensor):
ptr = <DLManagedTensor*>pycapsule.PyCapsule_GetPointer(dltensor, _c_str_dltensor)
c_api_ret_code = TVMFFITensorFromDLPack(
ptr, c_req_alignment, c_req_contiguous, out)
CHECK_CALL(c_api_ret_code)
# set name and destructor to be empty
pycapsule.PyCapsule_SetDestructor(dltensor, NULL)
pycapsule.PyCapsule_SetName(dltensor, _c_str_used_dltensor)
return 0
raise ValueError("Expect a dltensor field, PyCapsule can only be consumed once")
cdef inline int _from_dlpack_versioned(
object dltensor, int require_alignment,
int require_contiguous, TVMFFIObjectHandle* out
) except -1:
cdef DLManagedTensorVersioned* ptr
cdef int c_api_ret_code
cdef int c_req_alignment = require_alignment
cdef int c_req_contiguous = require_contiguous
if pycapsule.PyCapsule_IsValid(dltensor, _c_str_dltensor_versioned):
ptr = <DLManagedTensorVersioned*>pycapsule.PyCapsule_GetPointer(
dltensor, _c_str_dltensor_versioned)
c_api_ret_code = TVMFFITensorFromDLPackVersioned(
ptr, c_req_alignment, c_req_contiguous, out)
CHECK_CALL(c_api_ret_code)
# set name and destructor to be empty
pycapsule.PyCapsule_SetDestructor(dltensor, NULL)
pycapsule.PyCapsule_SetName(dltensor, _c_str_used_dltensor_versioned)
return 0
raise ValueError("Expect a dltensor_versioned field, PyCapsule can only be consumed once")
cdef inline int _from_dlpack_exchange_api(
object ext_tensor, const DLPackExchangeAPI* exchange_api, int require_alignment,
int require_contiguous, TVMFFIObjectHandle* out
) except -1:
cdef DLManagedTensorVersioned* temp_managed_tensor
cdef PyObject* ext_tensor_pyobj = <PyObject*>ext_tensor
if exchange_api.managed_tensor_from_py_object_no_sync(ext_tensor_pyobj, &temp_managed_tensor) != 0:
return -1
# Convert to TVM Tensor
if TVMFFITensorFromDLPackVersioned(
temp_managed_tensor, require_alignment, require_contiguous, out
) != 0:
# recycle the managed tensor to avoid leak
if temp_managed_tensor.deleter != NULL:
temp_managed_tensor.deleter(temp_managed_tensor)
raise BufferError("Failed to convert DLManagedTensorVersioned to ffi.Tensor")
return 0
cdef inline int _from_dlpack_universal(
object ext_tensor, int require_alignment,
int require_contiguous, TVMFFIObjectHandle* out
) except -1:
# as of most frameworks do not yet support v1.1
# move to false as most frameworks get upgraded.
cdef int favor_legacy_dlpack = True
cdef const DLPackExchangeAPI* exchange_api = NULL
if hasattr(ext_tensor, "__dlpack_c_exchange_api__"):
try:
_get_dlpack_exchange_api(ext_tensor.__dlpack_c_exchange_api__, &exchange_api)
return _from_dlpack_exchange_api(
ext_tensor,
exchange_api,
require_alignment,
require_contiguous,
out
)
except BufferError:
pass
if hasattr(ext_tensor, "__dlpack__"):
if favor_legacy_dlpack:
return _from_dlpack(
ext_tensor.__dlpack__(),
require_alignment,
require_contiguous,
out
)
else:
try:
return _from_dlpack_versioned(
ext_tensor.__dlpack__(max_version=__dlpack_version__),
require_alignment,
require_contiguous,
out
)
except TypeError:
return _from_dlpack(
ext_tensor.__dlpack__(),
require_alignment,
require_contiguous,
out
)
else:
if pycapsule.PyCapsule_IsValid(ext_tensor, _c_str_dltensor_versioned):
return _from_dlpack_versioned(
ext_tensor,
require_alignment,
require_contiguous,
out
)
elif pycapsule.PyCapsule_IsValid(ext_tensor, _c_str_dltensor):
return _from_dlpack(
ext_tensor,
require_alignment,
require_contiguous,
out
)
else:
raise TypeError("Expect from_dlpack to take either a compatible tensor or PyCapsule")
def from_dlpack(
ext_tensor: Any, *, require_alignment: int = 0, require_contiguous: bool = False
) -> Tensor:
"""Import a foreign array that implements the DLPack producer protocol.
Parameters
----------
ext_tensor
An object supporting :py:meth:`__dlpack__ <array_api.array.__dlpack__>`
and :py:meth:`__dlpack_device__ <array_api.array.__dlpack_device__>`.
require_alignment
If greater than zero, require the underlying data pointer to be
aligned to this many bytes. Misaligned inputs raise
:class:`ValueError`.
require_contiguous : bool, optional
When True, require the layout to be contiguous. Non-contiguous
inputs raise :class:`ValueError`.
Returns
-------
Tensor
A TVM FFI :class:`Tensor` that references the same memory.
Examples
--------
.. code-block:: python
import numpy as np
import tvm_ffi
x_np = np.arange(8, dtype="int32")
x = tvm_ffi.from_dlpack(x_np)
y_np = np.from_dlpack(x)
assert np.shares_memory(x_np, y_np)
""" # noqa: E501
cdef TVMFFIObjectHandle chandle
_from_dlpack_universal(ext_tensor, require_alignment, require_contiguous, &chandle)
return make_tensor_from_chandle(chandle)
# helper class for shape handling
def _shape_obj_get_py_tuple(obj: "CObject") -> tuple[int, ...]:
cdef TVMFFIShapeCell* shape = TVMFFIShapeGetCellPtr((<CObject>obj).chandle)
return tuple(shape.data[i] for i in range(shape.size))
def _make_strides_from_shape(tuple shape: tuple[int, ...]) -> tuple[int, ...]:
cdef int64_t expected_stride = 1
cdef list strides = []
cdef int64_t ndim = len(shape)
cdef int64_t reverse_index
for i in range(ndim):
reverse_index = ndim - i - 1
strides.append(expected_stride)
expected_stride *= shape[reverse_index]
return tuple(reversed(strides))
cdef class Tensor(CObject):
"""Managed n-dimensional array compatible with DLPack.
It provides zero-copy interoperability with array libraries
through the DLPack protocol. Instances are typically created with
:func:`from_dlpack` or returned from FFI functions.
Examples
--------
.. code-block:: python
import numpy as np
import tvm_ffi
x = tvm_ffi.from_dlpack(np.arange(6, dtype="int32"))
assert x.shape == (6,)
assert x.dtype == tvm_ffi.dtype("int32")
# Round-trip through NumPy using DLPack
np.testing.assert_equal(np.from_dlpack(x), np.arange(6, dtype="int32"))
"""
__slots__ = ()
cdef DLTensor* cdltensor
@property
def shape(self) -> tuple[int, ...]:
"""Tensor shape as a tuple of integers."""
return tuple(self.cdltensor.shape[i] for i in range(self.cdltensor.ndim))
@property
def ndim(self) -> int:
"""Number of dimensions of the tensor."""
return self.cdltensor.ndim
def numel(self) -> int:
"""Total number of elements in the tensor."""
cdef int64_t count = 1
cdef int i
for i in range(self.cdltensor.ndim):
count *= self.cdltensor.shape[i]
return count
def size(self, idx: int) -> int:
"""Get the size of the ``idx``-th dimension. Negative ``idx`` counts from the last dimension."""
cdef int ndim = self.cdltensor.ndim
if idx < -ndim or idx >= ndim:
raise IndexError(
f"Dimension {idx} out of range for tensor with {ndim} dimensions"
)
if idx < 0:
idx += ndim
return self.cdltensor.shape[idx]
def is_contiguous(self) -> bool:
"""True if the Tensor is C-contiguous (row-major), False otherwise."""
if self.cdltensor.strides == NULL:
return True
# An empty tensor (numel == 0) is trivially contiguous regardless of strides,
# matching NumPy/PyTorch semantics.
cdef int i
cdef int k
for i in range(self.cdltensor.ndim):
if self.cdltensor.shape[i] == 0:
return True
cdef int64_t expected_stride = 1
for i in range(self.cdltensor.ndim, 0, -1):
k = i - 1
if self.cdltensor.shape[k] == 1:
continue
if self.cdltensor.strides[k] != expected_stride:
return False
expected_stride *= self.cdltensor.shape[k]
return True
@property
def strides(self) -> tuple[int, ...]:
"""Tensor strides as a tuple of integers."""
if self.cdltensor.strides == NULL:
return _make_strides_from_shape(self.shape)
return tuple(self.cdltensor.strides[i] for i in range(self.cdltensor.ndim))
@property
def dtype(self) -> Any:
"""Data type as :class:`tvm_ffi.dtype` (``str`` subclass)."""
cdef TVMFFIAny dtype_any
dtype_any.v_dtype = self.cdltensor.dtype
return make_ret_dtype(dtype_any)
@property
def device(self) -> Device:
"""The :class:`Device` on which the tensor is placed."""
cdef TVMFFIAny device_any
device_any.v_device = self.cdltensor.device
return make_ret_device(device_any)
def _to_dlpack(self) -> object:
"""Return a DLPack capsule representing this tensor (internal)."""
cdef DLManagedTensor* dltensor
cdef int c_api_ret_code
c_api_ret_code = TVMFFITensorToDLPack(self.chandle, &dltensor)
CHECK_CALL(c_api_ret_code)
return pycapsule.PyCapsule_New(dltensor, _c_str_dltensor, <PyCapsule_Destructor>_c_dlpack_deleter)
def _to_dlpack_versioned(self) -> object:
"""Return a versioned DLPack capsule (internal)."""
cdef DLManagedTensorVersioned* dltensor
cdef int c_api_ret_code
c_api_ret_code = TVMFFITensorToDLPackVersioned(self.chandle, &dltensor)
CHECK_CALL(c_api_ret_code)
return pycapsule.PyCapsule_New(
dltensor, _c_str_dltensor_versioned, <PyCapsule_Destructor>_c_dlpack_versioned_deleter)
def __dlpack_device__(self) -> tuple[int, int]:
"""Implement the standard :py:meth:`__dlpack_device__ <array_api.array.__dlpack_device__>` protocol."""
cdef int device_type = self.cdltensor.device.device_type
cdef int device_id = self.cdltensor.device.device_id
return (device_type, device_id)
def __dlpack__(
self,
*,
stream: Any | None = None,
max_version: tuple[int, int] | None = None,
dl_device: tuple[int, int] | None = None,
copy: bool | None = None,
) -> object:
"""Implement the standard :py:meth:`__dlpack__ <array_api.array.__dlpack__>` protocol.
Parameters
----------
stream
Framework-specific stream/context object.
max_version
Upper bound on the supported DLPack version of the
consumer. When ``None``, use the built-in protocol version.
dl_device
Override the device reported by :py:meth:`__dlpack_device__`.
copy
If ``True``, produce a copy rather than exporting in-place.
Raises
------
BufferError
If the requested behavior cannot be satisfied.
""" # noqa: E501
if max_version is None:
# Keep and use the DLPack 0.X implementation
# Note: from March 2025 onwards (but ideally as late as
# possible), it's okay to raise BufferError here
return self._to_dlpack()
else:
# We get to produce `DLManagedTensorVersioned` now. Note that
# our_own_dlpack_version is the max version that the *producer*
# supports and fills in the `DLManagedTensorVersioned::version`
# field
if max_version[0] >= __dlpack_version__[0]:
if dl_device is not None and dl_device != self.__dlpack_device__():
raise BufferError("dl_device of different type not supported")
if copy is not None and copy:
raise BufferError("copy not yet supported")
return self._to_dlpack_versioned()
elif max_version[0] < 1:
return self.__ctypes_handle__to_dlpack()
else:
raise BufferError(f"Unsupported max_version {max_version}")
_set_class_tensor(Tensor)
cdef int _dltensor_test_wrapper_from_pyobject(
void* obj, DLManagedTensorVersioned** out
) except -1:
"""DLPackExchangeAPI: managed_tensor_from_py_object_no_sync"""
cdef PyObject* py_obj = <PyObject*>obj
cdef DLTensorTestWrapper wrapper = <DLTensorTestWrapper>py_obj
return TVMFFITensorToDLPackVersioned(wrapper.tensor.chandle, out)
cdef int _dltensor_test_wrapper_to_pyobject(
DLManagedTensorVersioned* tensor, void** out_py_object
) except -1:
"""DLPackExchangeAPI: managed_tensor_to_py_object_no_sync"""
cdef TVMFFIObjectHandle temp_chandle
if TVMFFITensorFromDLPackVersioned(tensor, 0, 0, &temp_chandle) != 0:
return -1
py_tensor = make_tensor_from_chandle(temp_chandle)
Py_INCREF(py_tensor)
out_py_object[0] = <void*>(<PyObject*>py_tensor)
return 0
cdef int _dltensor_test_wrapper_current_work_stream(
int device_type, int32_t device_id, void** out_stream
) except -1:
"""DLPackExchangeAPI: current_work_stream"""
if device_type != kDLCPU:
out_stream[0] = <void*>TVMFFIEnvGetStream(device_type, device_id)
return 0
# Module-level static DLPackExchangeAPI for DLTensorTestWrapper
cdef DLPackExchangeAPI _dltensor_test_wrapper_static_api
cdef DLPackExchangeAPI* _dltensor_test_wrapper_get_exchange_api() noexcept:
"""Get the static DLPackExchangeAPI instance for DLTensorTestWrapper."""
global _dltensor_test_wrapper_static_api
# Initialize header using macros from dlpack.h
_dltensor_test_wrapper_static_api.header.version.major = DLPACK_MAJOR_VERSION
_dltensor_test_wrapper_static_api.header.version.minor = DLPACK_MINOR_VERSION
_dltensor_test_wrapper_static_api.header.prev_api = NULL
# Initialize function pointers
_dltensor_test_wrapper_static_api.managed_tensor_allocator = NULL
_dltensor_test_wrapper_static_api.managed_tensor_from_py_object_no_sync = (
<DLPackManagedTensorFromPyObjectNoSync>_dltensor_test_wrapper_from_pyobject
)
_dltensor_test_wrapper_static_api.managed_tensor_to_py_object_no_sync = (
<DLPackManagedTensorToPyObjectNoSync>_dltensor_test_wrapper_to_pyobject
)
_dltensor_test_wrapper_static_api.dltensor_from_py_object_no_sync = NULL
_dltensor_test_wrapper_static_api.current_work_stream = (
<DLPackCurrentWorkStream>_dltensor_test_wrapper_current_work_stream
)
return &_dltensor_test_wrapper_static_api
cdef class DLTensorTestWrapper:
"""Wrapper of a Tensor that exposes DLPack protocol, only for testing purpose.
"""
__slots__ = ()
__dlpack_c_exchange_api__ = pycapsule.PyCapsule_New(
_dltensor_test_wrapper_get_exchange_api(),
b"dlpack_exchange_api",
NULL
)
cdef Tensor tensor
cdef dict __dict__
def __init__(self, tensor: Tensor) -> None:
self.tensor = tensor
def __tvm_ffi_env_stream__(self) -> int:
cdef TVMFFIStreamHandle stream
cdef long long stream_as_int
cdef int c_api_ret_code
stream = TVMFFIEnvGetStream(
self.tensor.cdltensor.device.device_type, self.tensor.cdltensor.device.device_id)
stream_as_int = <long long>stream
return stream_as_int
def __dlpack_device__(self) -> tuple[int, int]:
return self.tensor.__dlpack_device__()
def __dlpack__(self, *, **kwargs: Any) -> object:
return self.tensor.__dlpack__(**kwargs)
cdef inline object make_ret_dltensor(TVMFFIAny result):
cdef DLTensor* dltensor
dltensor = <DLTensor*>result.v_ptr
tensor = _CLASS_TENSOR.__new__(_CLASS_TENSOR)
(<CObject>tensor).chandle = NULL
(<Tensor>tensor).cdltensor = dltensor
return tensor
cdef inline object make_tensor_from_chandle(
TVMFFIObjectHandle chandle, const DLPackExchangeAPI* c_ctx_dlpack_api = NULL
):
cdef object tensor
cdef void* py_obj
cdef DLManagedTensorVersioned* dlpack
if c_ctx_dlpack_api != NULL and c_ctx_dlpack_api.managed_tensor_to_py_object_no_sync != NULL:
# try convert and import into the environment array if possible
if TVMFFITensorToDLPackVersioned(chandle, &dlpack) == 0:
try:
# note that py_obj already holds an extra reference to the tensor
# so we need to decref it after the conversion
c_ctx_dlpack_api.managed_tensor_to_py_object_no_sync(dlpack, &py_obj)
tensor = <object>(<PyObject*>py_obj)
Py_DECREF(tensor)
# decref original handle to prevent leak.
# note that DLManagedTensor also hold a reference to the tensor
# so we need to decref the original handle if the conversion is successful
TVMFFIObjectDecRef(chandle)
return tensor
except Exception:
# call the deleter to free the memory since we will continue to use the chandle
dlpack.deleter(dlpack)
pass
# default return the tensor.
# NOTE: we deliberately do NOT bind this wrapper as canonical (no
# TVMFFIPyCompareAndRebindPyObject) — this factory may wrap the same chandle more than
# once (e.g. once per arg-setter callback), and rebinding would corrupt the
# tying cache. Tensors returned through the FFI go via make_ret_object instead.
tensor = _CLASS_TENSOR.__new__(_CLASS_TENSOR)
(<CObject>tensor).chandle = chandle
(<Tensor>tensor).cdltensor = TVMFFITensorGetDLTensorPtr(chandle)
return tensor
cdef inline object make_tensor_from_any(TVMFFIAny any, const DLPackExchangeAPI* c_ctx_dlpack_api):
return make_tensor_from_chandle(any.v_ptr, c_ctx_dlpack_api)