| # 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 __future__ import annotations |
| |
| from typing import Any, NamedTuple, NoReturn |
| |
| import numpy.typing as npt |
| import pytest |
| |
| try: |
| import torch |
| import torch.version |
| except ImportError: |
| torch = None # ty: ignore[invalid-assignment] |
| |
| import numpy as np |
| import tvm_ffi |
| |
| |
| def test_tensor_attributes() -> None: |
| data: npt.NDArray[Any] = np.zeros((10, 8, 4, 2), dtype="int16") |
| if not hasattr(data, "__dlpack__"): |
| return |
| x = tvm_ffi.from_dlpack(data) |
| assert isinstance(x, tvm_ffi.Tensor) |
| assert x.shape == (10, 8, 4, 2) |
| assert x.ndim == 4 |
| assert x.numel() == 640 |
| assert x.size(0) == 10 |
| assert x.size(-1) == 2 |
| assert x.is_contiguous() |
| assert x.strides == (64, 8, 2, 1) |
| assert x.dtype == tvm_ffi.dtype("int16") |
| assert x.device.dlpack_device_type() == tvm_ffi.DLDeviceType.kDLCPU |
| assert x.device.index == 0 |
| x2 = np.from_dlpack(x) |
| np.testing.assert_equal(x2, data) |
| |
| |
| def test_empty_tensor_is_contiguous() -> None: |
| # Empty tensors are trivially contiguous regardless of what |
| # strides the producer reports (numpy 2.3+ via __dlpack__ now |
| # reports (0, 0, 0) for shape (4, 0, 4)). See PR #607 review |
| # comment for context. |
| data: npt.NDArray[Any] = np.zeros((4, 0, 4), dtype="int16") |
| if not hasattr(data, "__dlpack__"): |
| return |
| x = tvm_ffi.from_dlpack(data) |
| assert x.is_contiguous() |
| |
| |
| def test_non_contiguous_tensor_attributes() -> None: |
| data: npt.NDArray[Any] = np.zeros((4, 4, 4), dtype="int16") |
| slice = data[1:3, :, 1:3] |
| if not hasattr(slice, "__dlpack__"): |
| return |
| x = tvm_ffi.from_dlpack(slice) |
| assert isinstance(x, tvm_ffi.Tensor) |
| assert x.shape == (2, 4, 2) |
| assert x.numel() == 16 |
| assert x.size(0) == 2 |
| assert x.size(-1) == 2 |
| assert not x.is_contiguous() |
| assert x.strides == (16, 4, 1) |
| |
| |
| def test_shape_object() -> None: |
| shape = tvm_ffi.Shape((10, 8, 4, 2)) |
| assert isinstance(shape, tvm_ffi.Shape) |
| assert shape == (10, 8, 4, 2) |
| |
| fecho = tvm_ffi.convert(lambda x: x) |
| shape2: tvm_ffi.Shape = fecho(shape) |
| assert shape2._tvm_ffi_cached_object.same_as(shape._tvm_ffi_cached_object) |
| assert isinstance(shape2, tvm_ffi.Shape) |
| assert isinstance(shape2, tuple) |
| |
| shape3: tvm_ffi.Shape = tvm_ffi.convert(shape) |
| assert shape3._tvm_ffi_cached_object.same_as(shape._tvm_ffi_cached_object) |
| assert isinstance(shape3, tvm_ffi.Shape) |
| |
| |
| @pytest.mark.skipif(torch is None, reason="Fast torch dlpack importer is not enabled") |
| def test_tensor_auto_dlpack() -> None: |
| assert torch is not None |
| x = torch.arange(128) |
| fecho = tvm_ffi.get_global_func("testing.echo") |
| y = fecho(x) |
| assert isinstance(y, torch.Tensor) |
| assert y.data_ptr() == x.data_ptr() |
| assert y.dtype == x.dtype |
| assert y.shape == x.shape |
| assert y.device == x.device |
| np.testing.assert_equal(y.numpy(), x.numpy()) |
| |
| |
| @pytest.mark.skipif(torch is None, reason="Fast torch dlpack importer is not enabled") |
| def test_tensor_auto_dlpack_with_error() -> None: |
| assert torch is not None |
| x = torch.arange(128) |
| |
| def raise_torch_error(x: Any) -> NoReturn: |
| raise ValueError("error XYZ") |
| |
| f = tvm_ffi.convert(raise_torch_error) |
| with pytest.raises(ValueError): |
| # pass in torch argment to trigger the error in set allocator path |
| f(x) |
| |
| |
| def test_tensor_class_override() -> None: |
| class MyTensor(tvm_ffi.Tensor): |
| pass |
| |
| old_tensor = tvm_ffi.core._CLASS_TENSOR |
| tvm_ffi.core._set_class_tensor(MyTensor) |
| |
| data: npt.NDArray[Any] = np.zeros((10, 8, 4, 2), dtype="int16") |
| if not hasattr(data, "__dlpack__"): |
| return |
| x = tvm_ffi.from_dlpack(data) |
| |
| fecho = tvm_ffi.get_global_func("testing.echo") |
| y = fecho(x) |
| assert isinstance(y, MyTensor) |
| tvm_ffi.core._set_class_tensor(old_tensor) |
| |
| |
| def test_tvm_ffi_tensor_compatible() -> None: |
| class MyTensor: |
| def __init__(self, tensor: tvm_ffi.Tensor) -> None: |
| """Initialize the MyTensor.""" |
| self._tensor = tensor |
| |
| def __tvm_ffi_object__(self) -> tvm_ffi.Tensor: |
| """Implement __tvm_ffi_object__ protocol.""" |
| return self._tensor |
| |
| data: npt.NDArray[Any] = np.zeros((10, 8, 4, 2), dtype="int32") |
| if not hasattr(data, "__dlpack__"): |
| return |
| x = tvm_ffi.from_dlpack(data) |
| y = MyTensor(x) |
| fecho = tvm_ffi.get_global_func("testing.echo") |
| z = fecho(y) |
| assert z.__chandle__() == x.__chandle__() |
| |
| class MyNamedTuple(NamedTuple): |
| a: MyTensor |
| b: int |
| |
| args = MyNamedTuple(a=y, b=1) |
| z = fecho(args) |
| assert z[0].__chandle__() == x.__chandle__() |
| assert z[1] == 1 |
| |
| class MyCustom: |
| def __init__(self, a: MyTensor, b: int) -> None: |
| self.a = a |
| self.b = b |
| |
| def __tvm_ffi_value__(self) -> Any: |
| """Implement __tvm_ffi_value__ protocol.""" |
| return (self.a, self.b) |
| |
| z = fecho(MyCustom(a=y, b=2)) |
| assert z[0].__chandle__() == x.__chandle__() |
| assert z[1] == 2 |
| |
| |
| @pytest.mark.skipif( |
| torch is None or not torch.cuda.is_available() or torch.version.hip is None, |
| reason="ROCm is not enabled in PyTorch", |
| ) |
| def test_tensor_from_pytorch_rocm() -> None: |
| assert torch is not None |
| |
| @tvm_ffi.register_global_func("testing.check_device", override=True) |
| def _check_device(x: tvm_ffi.Tensor) -> str: |
| return x.device.type |
| |
| # PyTorch uses device name "cuda" to represent ROCm device |
| x = torch.randn(128, device="cuda") |
| device_type = tvm_ffi.get_global_func("testing.check_device")(x) |
| assert device_type == "rocm" |
| |
| |
| def test_optional_tensor_view() -> None: |
| optional_tensor_view_has_value = tvm_ffi.get_global_func( |
| "testing.optional_tensor_view_has_value" |
| ) |
| assert not optional_tensor_view_has_value(None) |
| x: npt.NDArray[Any] = np.zeros((128,), dtype="float32") |
| if not hasattr(x, "__dlpack__"): |
| return |
| assert optional_tensor_view_has_value(x) |
| |
| |
| if __name__ == "__main__": |
| pytest.main([__file__]) |