| # 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 |
| |
| import ctypes |
| import gc |
| import sys |
| from typing import Any |
| |
| import numpy as np |
| import pytest |
| import tvm_ffi |
| import tvm_ffi.cpp |
| |
| try: |
| import torch |
| except ImportError: |
| torch = None # ty: ignore[invalid-assignment] |
| |
| _HAS_TORCH_DLPACK_API = torch is not None and hasattr(torch.Tensor, "__dlpack_c_exchange_api__") |
| |
| |
| def test_echo() -> None: |
| fecho = tvm_ffi.get_global_func("testing.echo") |
| assert isinstance(fecho, tvm_ffi.Function) |
| # test each type |
| assert fecho(None) is None |
| |
| # test bool |
| bool_result = fecho(True) |
| assert isinstance(bool_result, bool) |
| assert bool_result is True |
| bool_result = fecho(False) |
| assert isinstance(bool_result, bool) |
| assert bool_result is False |
| |
| # test int/float |
| assert fecho(1) == 1 |
| assert fecho(1.2) == 1.2 |
| |
| # test str |
| str_result = fecho("hello") |
| assert isinstance(str_result, str) |
| assert str_result == "hello" |
| |
| # test bytes |
| bytes_result = fecho(b"abc") |
| assert isinstance(bytes_result, bytes) |
| assert bytes_result == b"abc" |
| |
| # test dtype |
| dtype_result = fecho(tvm_ffi.dtype("float32")) |
| assert isinstance(dtype_result, tvm_ffi.dtype) |
| assert dtype_result == tvm_ffi.dtype("float32") |
| |
| c_dtype_result = fecho(tvm_ffi.core.DataType("float32")) |
| assert isinstance(c_dtype_result, tvm_ffi.dtype) |
| assert c_dtype_result == tvm_ffi.dtype("float32") |
| |
| # test device |
| device_result = fecho(tvm_ffi.device("cuda:1")) |
| assert isinstance(device_result, tvm_ffi.Device) |
| assert device_result.dlpack_device_type() == tvm_ffi.DLDeviceType.kDLCUDA |
| assert device_result.index == 1 |
| assert str(device_result) == "cuda:1" |
| assert device_result.__repr__() == "device(type='cuda', index=1)" |
| |
| # test c_void_p |
| c_void_p_result = fecho(ctypes.c_void_p(0x12345678)) |
| assert isinstance(c_void_p_result, ctypes.c_void_p) |
| assert c_void_p_result.value == 0x12345678 |
| |
| # test c_void_p for nullptr |
| c_void_p_nullptr_result = fecho(ctypes.c_void_p(0)) |
| c_void_p_nullptr_result is None |
| |
| # test function: aka object |
| fadd = tvm_ffi.convert(lambda a, b: a + b) |
| fadd1 = fecho(fadd) |
| assert fadd1(1, 2) == 3 |
| assert fadd1.same_as(fadd) |
| |
| def check_tensor() -> None: |
| np_data = np.arange(10, dtype="int32") |
| if not hasattr(np_data, "__dlpack__"): |
| return |
| # test Tensor |
| x = tvm_ffi.from_dlpack(np_data) |
| assert isinstance(x, tvm_ffi.Tensor) |
| tensor_result = fecho(x) |
| assert isinstance(tensor_result, tvm_ffi.Tensor) |
| assert tensor_result.shape == (10,) |
| assert tensor_result.dtype == tvm_ffi.dtype("int32") |
| assert tensor_result.device.dlpack_device_type() == tvm_ffi.DLDeviceType.kDLCPU |
| assert tensor_result.device.index == 0 |
| |
| check_tensor() |
| |
| |
| def test_typed_device_arg_accepts_string() -> None: |
| schema_id_device = tvm_ffi.get_global_func("testing.schema_id_device") |
| result = schema_id_device("cpu") |
| assert isinstance(result, tvm_ffi.Device) |
| assert result == tvm_ffi.device("cpu", 0) |
| |
| result = schema_id_device("cuda:1") |
| assert result == tvm_ffi.device("cuda", 1) |
| |
| |
| def test_return_raw_str_bytes() -> None: |
| assert tvm_ffi.convert(lambda: "hello")() == "hello" |
| assert tvm_ffi.convert(lambda: b"hello")() == b"hello" |
| assert tvm_ffi.convert(lambda: bytearray(b"hello"))() == b"hello" |
| |
| |
| def test_string_bytes_passing() -> None: |
| fecho = tvm_ffi.get_global_func("testing.echo") |
| use_count = tvm_ffi.get_global_func("testing.object_use_count") |
| # small string |
| assert fecho("hello") == "hello" |
| # large string |
| x = "hello" * 100 |
| y = fecho(x) |
| assert y == x |
| assert y._tvm_ffi_cached_object is not None |
| use_count(y) == 1 |
| # small bytes |
| assert fecho(b"hello") == b"hello" |
| # large bytes |
| x2 = b"hello" * 100 |
| y2 = fecho(x2) |
| assert y2 == x2 |
| assert y2._tvm_ffi_cached_object is not None |
| fecho(y2) == 1 |
| |
| |
| def test_nested_container_passing() -> None: |
| # test and make sure our ref counting is correct |
| fecho = tvm_ffi.get_global_func("testing.echo") |
| use_count = tvm_ffi.get_global_func("testing.object_use_count") |
| obj = tvm_ffi.convert((1, 2, 3)) |
| assert use_count(obj) == 1 |
| y = fecho([obj, {"a": 1, "b": obj}]) |
| assert use_count(y) == 1 |
| assert use_count(obj) == 3 |
| assert use_count(y[1]) == 2 |
| |
| |
| def test_pyfunc_convert() -> None: |
| def add(a: int, b: int) -> int: |
| return a + b |
| |
| fadd = tvm_ffi.convert(add) |
| assert isinstance(fadd, tvm_ffi.Function) |
| assert fadd(1, 2) == 3 |
| |
| def fapply(f: Any, *args: Any) -> Any: |
| return f(*args) |
| |
| fapply = tvm_ffi.convert(fapply) |
| assert fapply(add, 1, 3.3) == 4.3 |
| |
| |
| def test_global_func() -> None: |
| @tvm_ffi.register_global_func("mytest.echo") |
| def echo(x: Any) -> Any: |
| return x |
| |
| f = tvm_ffi.get_global_func("mytest.echo") |
| assert f.same_as(echo) |
| assert f(1) == 1 |
| |
| assert "mytest.echo" in tvm_ffi.registry.list_global_func_names() |
| |
| tvm_ffi.registry.remove_global_func("mytest.echo") |
| assert "mytest.echo" not in tvm_ffi.registry.list_global_func_names() |
| assert tvm_ffi.get_global_func("mytest.echo", allow_missing=True) is None |
| |
| |
| def test_rvalue_ref() -> None: |
| use_count = tvm_ffi.get_global_func("testing.object_use_count") |
| |
| def callback(x: Any, expected_count: int) -> Any: |
| # The use count of TVM FFI objects is decremented as part of |
| # `ObjectRef.__del__`, which runs when the Python object is |
| # destructed. However, Python object destruction is not |
| # deterministic, and even CPython's reference-counting is |
| # considered an implementation detail. Therefore, to ensure |
| # correct results from this test, `gc.collect()` must be |
| # explicitly called. |
| gc.collect() |
| assert expected_count == use_count(x) |
| return x._move() |
| |
| f = tvm_ffi.convert(callback) |
| |
| def check0() -> None: |
| x = tvm_ffi.convert([1, 2]) |
| assert use_count(x) == 1 |
| f(x, 2) |
| f(x._move(), 1) |
| assert x.__ctypes_handle__().value is None |
| |
| def check1() -> None: |
| x = tvm_ffi.convert([1, 2]) |
| assert use_count(x) == 1 |
| y = f(x, 2) |
| f(x._move(), 2) |
| assert x.__ctypes_handle__().value is None |
| assert y.__ctypes_handle__().value is not None |
| |
| check0() |
| check1() |
| |
| |
| def test_echo_with_opaque_object() -> None: |
| class MyObject: |
| def __init__(self, value: Any) -> None: |
| self.value = value |
| |
| fecho = tvm_ffi.get_global_func("testing.echo") |
| x = MyObject("hello") |
| assert sys.getrefcount(x) == 2 |
| y = fecho(x) |
| assert isinstance(y, MyObject) |
| assert y is x |
| assert sys.getrefcount(x) == 3 |
| |
| def py_callback(z: Any) -> Any: |
| """Python callback with opaque object.""" |
| assert z is x |
| return z |
| |
| fcallback = tvm_ffi.convert(py_callback) |
| z = fcallback(x) |
| assert z is x |
| assert sys.getrefcount(x) == 4 |
| |
| |
| def test_function_from_c_symbol() -> None: |
| add_one_c_symbol = tvm_ffi.get_global_func("testing.get_add_one_c_symbol")() |
| fadd_one = tvm_ffi.Function.__from_extern_c__(add_one_c_symbol) |
| assert fadd_one(1) == 2 |
| assert fadd_one(2) == 3 |
| |
| with pytest.raises(TypeError): |
| fadd_one(None) |
| |
| keep_alive = [1, 2, 3] |
| base_ref_count = sys.getrefcount(keep_alive) |
| fadd_one = tvm_ffi.Function.__from_extern_c__(add_one_c_symbol, keep_alive_object=keep_alive) |
| assert fadd_one(1) == 2 |
| assert fadd_one(2) == 3 |
| assert sys.getrefcount(keep_alive) == base_ref_count + 1 |
| fadd_one = None |
| assert sys.getrefcount(keep_alive) == base_ref_count |
| |
| |
| def test_function_from_mlir_packed_safe_call() -> None: |
| add_one_c_symbol = tvm_ffi.get_global_func("testing.get_mlir_add_one_c_symbol")() |
| fadd_one = tvm_ffi.Function.__from_mlir_packed_safe_call__(add_one_c_symbol) |
| assert fadd_one(1) == 2 |
| assert fadd_one(2) == 3 |
| |
| keep_alive = [1, 2, 3] |
| base_ref_count = sys.getrefcount(keep_alive) |
| fadd_one = tvm_ffi.Function.__from_mlir_packed_safe_call__( |
| add_one_c_symbol, keep_alive_object=keep_alive |
| ) |
| |
| with pytest.raises(TypeError): |
| fadd_one(None) |
| |
| assert fadd_one(1) == 2 |
| assert fadd_one(2) == 3 |
| assert sys.getrefcount(keep_alive) == base_ref_count + 1 |
| fadd_one = None |
| assert sys.getrefcount(keep_alive) == base_ref_count |
| |
| |
| def test_function_subclass() -> None: |
| class JitFunction: |
| def __init__(self, metadata: Any) -> None: |
| self.metadata = metadata |
| |
| class MyFunction(tvm_ffi.Function, JitFunction): |
| def __init__(self, metadata: Any) -> None: |
| # Explicitly initialize the mixin. `super()` is not used because `tvm_ffi.Function` |
| # is an extension type without a standard `__init__`. |
| JitFunction.__init__(self, metadata) |
| |
| # When subclassing a Cython cdef class and overriding `__init__`, |
| # special methods like `__call__` may not be inherited automatically. |
| # This explicit assignment ensures the subclass remains callable. |
| __call__ = tvm_ffi.Function.__call__ |
| |
| f = tvm_ffi.convert(lambda x: x) |
| assert isinstance(f, tvm_ffi.Function) |
| f_sub = MyFunction(128) |
| # move handle from f to f_sub an existing function |
| f_sub.__move_handle_from__(f) |
| assert isinstance(f_sub, MyFunction) |
| assert isinstance(f_sub, JitFunction) |
| assert f_sub.metadata == 128 |
| |
| y: int = f_sub(2) |
| assert y == 2 |
| echo = tvm_ffi.get_global_func("testing.echo") |
| fechoed = echo(f_sub) |
| assert isinstance(fechoed, tvm_ffi.Function) |
| assert fechoed.__chandle__() == f_sub.__chandle__() |
| assert fechoed(10) == 10 |
| |
| |
| def test_function_with_opaque_ptr_protocol() -> None: |
| class MyObject: |
| def __init__(self, value: Any) -> None: |
| self.value = value |
| |
| def __tvm_ffi_opaque_ptr__(self) -> Any: |
| return self.value |
| |
| fecho = tvm_ffi.get_global_func("testing.echo") |
| x = MyObject(10) |
| y = fecho(x) |
| assert isinstance(y, ctypes.c_void_p) |
| assert y.value == 10 |
| |
| |
| def test_function_with_dlpack_data_type_protocol() -> None: |
| class DLPackDataTypeProtocol: |
| def __init__(self, dlpack_data_type: tuple[int, int, int]) -> None: |
| self.dlpack_data_type = dlpack_data_type |
| |
| def __dlpack_data_type__(self) -> tuple[int, int, int]: |
| return self.dlpack_data_type |
| |
| dtype = tvm_ffi.dtype("float32") |
| fecho = tvm_ffi.get_global_func("testing.echo") |
| x = DLPackDataTypeProtocol((dtype.type_code, dtype.bits, dtype.lanes)) |
| y = fecho(x) |
| assert y == dtype |
| converted_y = tvm_ffi.convert(x) |
| assert converted_y == dtype |
| |
| |
| def test_function_with_dlpack_device_protocol() -> None: |
| device = tvm_ffi.device("cuda:1") |
| |
| class DLPackDeviceProtocol: |
| def __init__(self, device: tvm_ffi.Device) -> None: |
| self.device = device |
| |
| def __dlpack_device__(self) -> tuple[int, int]: |
| return (self.device.dlpack_device_type(), self.device.index) |
| |
| fecho = tvm_ffi.get_global_func("testing.echo") |
| x = DLPackDeviceProtocol(device) |
| y = fecho(x) |
| assert y == device |
| |
| |
| def test_integral_float_variants_passing() -> None: |
| fecho = tvm_ffi.get_global_func("testing.echo") |
| y = fecho(np.int32(1)) |
| assert isinstance(y, int) |
| assert y == 1 |
| |
| y = fecho(np.float64(2.0)) |
| assert isinstance(y, float) |
| assert y == 2.0 |
| |
| class IntProtocol: |
| def __init__(self, value: int) -> None: |
| self.value = value |
| |
| def __tvm_ffi_int__(self) -> int: |
| return self.value |
| |
| y = fecho(IntProtocol(10)) |
| assert isinstance(y, int) |
| assert y == 10 |
| |
| class FloatProtocol: |
| def __init__(self, value: float) -> None: |
| self.value = value |
| |
| def __tvm_ffi_float__(self) -> float: |
| return self.value |
| |
| y = fecho(FloatProtocol(10)) |
| assert isinstance(y, float) |
| assert y == 10 |
| |
| |
| def test_function_with_value_protocol() -> None: |
| class ValueProtocol: |
| def __init__(self, value: Any) -> None: |
| self.value = value |
| |
| def __tvm_ffi_value__(self) -> Any: |
| return self.value |
| |
| fecho = tvm_ffi.get_global_func("testing.echo") |
| assert fecho(ValueProtocol(10)) == 10 |
| assert tuple(fecho(ValueProtocol([1, 2, 3]))) == (1, 2, 3) |
| assert tuple(fecho(ValueProtocol([1, 2, ValueProtocol(3)]))) == (1, 2, 3) |
| nested_value_protocol = ValueProtocol(ValueProtocol(ValueProtocol(10))) |
| assert fecho(nested_value_protocol) == 10 |
| |
| nested_value_protocol = ValueProtocol([ValueProtocol(1), ValueProtocol(2), ValueProtocol(3)]) |
| assert tuple(fecho(nested_value_protocol)) == (1, 2, 3) |
| |
| |
| def test_convert_func_tensor_cls_missing_attribute() -> None: |
| """Passing a class without __dlpack_c_exchange_api__ raises TypeError.""" |
| |
| class DummyTensor: |
| pass |
| |
| with pytest.raises(TypeError, match="__dlpack_c_exchange_api__"): |
| tvm_ffi.convert_func(lambda x: x, tensor_cls=DummyTensor) |
| |
| with pytest.raises(TypeError, match="__dlpack_c_exchange_api__"): |
| tvm_ffi.convert_func(lambda x: x, tensor_cls=object) |
| |
| |
| def test_convert_func_raises_propagates() -> None: |
| """An exception raised inside the callback propagates out to the caller.""" |
| |
| def raises(x: int) -> None: |
| raise ValueError(f"boom {x}") |
| |
| f = tvm_ffi.convert_func(raises) |
| with pytest.raises(ValueError, match="boom 42"): |
| f(42) |
| |
| |
| @pytest.mark.skipif( |
| not _HAS_TORCH_DLPACK_API, |
| reason="torch.Tensor.__dlpack_c_exchange_api__ not available", |
| ) |
| def test_convert_func_with_torch_tensor_cls() -> None: |
| """tensor_cls=torch.Tensor delivers torch.Tensor instances to the callback. |
| |
| Asserts the type *inside* the callback (which runs on the C++ -> Python |
| side of the FFI boundary) — the return value's Python type depends on |
| the outer caller's conversion path, so we verify shape survives the |
| round-trip rather than isinstance on the return. |
| """ |
| assert torch is not None |
| torch_mod = torch |
| calls = 0 |
| |
| def callback(a: Any, b: Any, c: Any) -> Any: |
| nonlocal calls |
| calls += 1 |
| assert isinstance(a, torch_mod.Tensor) |
| assert isinstance(b, torch_mod.Tensor) |
| assert isinstance(c, torch_mod.Tensor) |
| assert list(a.shape) == [2] |
| assert list(b.shape) == [3] |
| assert list(c.shape) == [4] |
| return b |
| |
| f = tvm_ffi.convert_func(callback, tensor_cls=torch_mod.Tensor) |
| a = torch_mod.zeros(2) |
| b = torch_mod.ones(3) |
| c = torch_mod.full((4,), 2.0) |
| out = f(a, b, c) |
| assert calls == 1 |
| assert tuple(out.shape) == (3,) |
| |
| |
| def test_callback_rawstr_and_bytearrayptr_args() -> None: |
| """Regression: C++ -> Python callback with kTVMFFIRawStr / kTVMFFIByteArrayPtr args. |
| |
| When C++ invokes a Python callback with non-owning RawStr or ByteArrayPtr |
| arg shapes, the callback arg setter must materialise a Python str / bytes |
| directly rather than hitting the ``raise ValueError`` guard that formerly |
| existed in ``TVMFFICyCallbackArgSetterFactory``. |
| |
| Two trampolines are compiled via cpp.load_inline: |
| - ``invoke_with_raw_str(callback)`` — calls ``callback("hello rawstr")`` |
| using a C-string literal, which the TypeTraits pack as kTVMFFIRawStr. |
| - ``invoke_with_byte_array_ptr(callback)`` — calls ``callback(&byte_arr)`` |
| where ``byte_arr`` is a ``TVMFFIByteArray`` on the stack, packed as |
| kTVMFFIByteArrayPtr. |
| """ |
| mod = tvm_ffi.cpp.load_inline( |
| name="test_callback_rawstr_bytearrayptr", |
| cpp_sources=r""" |
| void invoke_with_raw_str(tvm::ffi::Function callback) { |
| // Passing a string literal packs as kTVMFFIRawStr (const char* TypeTraits). |
| callback("hello rawstr"); |
| } |
| |
| void invoke_with_byte_array_ptr(tvm::ffi::Function callback) { |
| // Passing a TVMFFIByteArray* packs as kTVMFFIByteArrayPtr. |
| static const char kData[] = "hello bytearrayptr"; |
| TVMFFIByteArray byte_arr{kData, sizeof(kData) - 1}; |
| callback(&byte_arr); |
| } |
| """, |
| functions=["invoke_with_raw_str", "invoke_with_byte_array_ptr"], |
| ) |
| |
| # --- kTVMFFIRawStr path --- |
| str_received: list[Any] = [] |
| str_cb = tvm_ffi.convert(str_received.append) |
| mod.invoke_with_raw_str(str_cb) |
| assert len(str_received) == 1 |
| assert isinstance(str_received[0], str), f"expected str, got {type(str_received[0])}" |
| assert str_received[0] == "hello rawstr" |
| |
| # --- kTVMFFIByteArrayPtr path --- |
| bytes_received: list[Any] = [] |
| bytes_cb = tvm_ffi.convert(bytes_received.append) |
| mod.invoke_with_byte_array_ptr(bytes_cb) |
| assert len(bytes_received) == 1 |
| assert isinstance(bytes_received[0], bytes), f"expected bytes, got {type(bytes_received[0])}" |
| assert bytes_received[0] == b"hello bytearrayptr" |