| <!--- 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. --> |
| # Python Guide |
| |
| This guide introduces the `tvm_ffi` Python package. |
| At a high level, the `tvm_ffi` Python package provides first-class Python support for |
| |
| - Pythonic classes to represent values in TVM FFI Any ABI. |
| - Mechanisms to call into TVM FFI ABI compatible functions. |
| - Conversion between Python values and `tvm_ffi` values. |
| |
| In this guide, we will run examples that make use of pre-registered testing functions in `tvm_ffi`. |
| If so, we will also briefly copy snippets that show the corresponding C++ behavior. |
| |
| ## Load and Run Module |
| |
| The most common use case of TVM FFI is to load a runnable module and run the corresponding function. |
| You can follow the [quickstart guide](../get_started/quickstart.rst) for details on building the |
| library `build/add_one_cpu.so`. Let's walk through the load and run example again for NumPy |
| |
| ```python |
| import tvm_ffi |
| import numpy as np |
| |
| # Load the compiled module |
| mod = tvm_ffi.load_module("build/add_one_cpu.so") |
| |
| # Create input and output arrays |
| x = np.array([1, 2, 3, 4, 5], dtype=np.float32) |
| y = np.empty_like(x) |
| |
| # Call the function |
| mod.add_one_cpu(x, y) |
| ``` |
| |
| In this case, {py:func}`tvm_ffi.load_module` will return a {py:class}`tvm_ffi.Module` class that contains |
| the exported functions. You can access the functions by their names. |
| |
| ## Tensor |
| |
| `tvm_ffi` provides a managed DLPack-compatible Tensor. |
| |
| ```python |
| import numpy as np |
| import tvm_ffi |
| |
| # Demonstrate DLPack conversion between NumPy and TVM FFI |
| np_data = np.array([1, 2, 3, 4], dtype=np.float32) |
| tvm_array = tvm_ffi.from_dlpack(np_data) |
| # Convert back to NumPy |
| np_result = np.from_dlpack(tvm_array) |
| ``` |
| |
| In most cases, however, you do not have to explicitly create Tensors. |
| The Python interface can take in `torch.Tensor` and `numpy.ndarray` objects |
| and automatically convert them to {py:class}`tvm_ffi.Tensor`. |
| |
| ## Functions and Callbacks |
| |
| {py:class}`tvm_ffi.Function` provides the Python interface for `ffi::Function` in the C++. |
| You can retrieve globally registered functions via {py:func}`tvm_ffi.get_global_func`. |
| |
| ```python |
| import tvm_ffi |
| |
| # testing.echo is defined and registered in C++ |
| # [](ffi::Any x) { return x; } |
| fecho = tvm_ffi.get_global_func("testing.echo") |
| assert fecho(1) == 1 |
| ``` |
| |
| You can pass a Python function as an argument to another FFI function as callbacks. |
| Under the hood, {py:func}`tvm_ffi.convert` is called to convert the Python function into a |
| {py:class}`tvm_ffi.Function`. |
| |
| ```python |
| import tvm_ffi |
| |
| # testing.apply is registered in C++ |
| # [](ffi::Function f, ffi::Any val) { return f(x); } |
| fapply = tvm_ffi.get_global_func("testing.apply") |
| # invoke fapply with lambda callback as f |
| assert fapply(lambda x: x + 1, 1) == 2 |
| ``` |
| |
| This is a very powerful pattern that allows us to inject Python callbacks into the C++ code. |
| You can also register a Python callback as a global function. |
| |
| ```python |
| import tvm_ffi |
| |
| @tvm_ffi.register_global_func("example.add_one") |
| def add_one(a): |
| return a + 1 |
| |
| assert tvm_ffi.get_global_func("example.add_one")(1) == 2 |
| ``` |
| |
| (python-argument-conversion-protocols)= |
| |
| ### Argument conversion protocols |
| |
| When Python calls a {py:class}`tvm_ffi.Function`, each argument is converted into the |
| TVM FFI `Any` calling convention. Most common Python values are handled directly: |
| `int`, `float`, `str`, `bytes`, `list`, `tuple`, `dict`, Python callables, |
| {py:class}`tvm_ffi.Object`, {py:class}`tvm_ffi.Tensor`, DLPack-compatible tensors, |
| and exceptions all have built-in conversions. |
| |
| Function calls also recognize the following special protocols that define |
| argument conversion behavior. {py:func}`tvm_ffi.convert` also recognizes some |
| of them, but it may preserve protocol objects whose final target type is only |
| known when a function is called. |
| |
| For DLPack-compatible objects, TVM-FFI follows the |
| [DLPack protocol](https://dmlc.github.io/dlpack/latest/). |
| |
| | Protocol | Expected return | Behavior | |
| | -------- | --------------- | -------- | |
| | `__tvm_ffi_object__(self)` | A {py:class}`tvm_ffi.Object` instance | Passes the returned FFI object handle directly. Use this for wrapper classes that own or expose an existing FFI object. | |
| | `__tvm_ffi_value__(self)` | Another Python value | Recursively converts the returned value. This is useful for lightweight wrappers around values such as strings, arrays, maps, objects, or scalars. Conversion detects recursive cycles and reports them as conversion/type errors. | |
| | `__tvm_ffi_opaque_ptr__(self)` | An integer pointer value | Passes the value as an opaque pointer. This is for low-level interop with raw memory structs. | |
| | `__tvm_ffi_int__(self)` | An integer | Passes the value as an FFI integer. | |
| | `__tvm_ffi_float__(self)` | A float | Passes the value as an FFI float. | |
| | `__tvm_ffi_env_stream__(self)` | An integer stream handle | Used with non-CPU tensor inputs that expose DLPack. The stream is recorded in the FFI call context so the callee can observe the producer framework's current stream. | |
| | `__cuda_stream__(self)` | A [CUDA stream protocol](https://nvidia.github.io/cuda-python/cuda-core/latest/interoperability.html) value | Passes the stream as an opaque pointer. | |
| |
| The protocols above should be implemented on the class, not installed dynamically |
| on a single object instance. The converter checks the value's type for most |
| protocols so dispatch can be cached efficiently. |
| |
| For example, a DSL compiler can use `__tvm_ffi_opaque_ptr__` to pass objects |
| that map to raw memory pointers: |
| |
| ```python |
| import ctypes |
| import tvm_ffi |
| |
| |
| class Workspace: |
| def __init__(self, size: int): |
| self.buffer = (ctypes.c_uint8 * size)() |
| |
| def __tvm_ffi_opaque_ptr__(self) -> int: |
| return ctypes.addressof(self.buffer) |
| |
| |
| workspace = Workspace(4096) |
| compile_kernel = tvm_ffi.get_global_func("my_compiler.compile_kernel") |
| compile_kernel(workspace) |
| ``` |
| |
| ## Container Types |
| |
| TVM FFI provides five container types that split into **immutable** (copy-on-write) and |
| **mutable** (shared reference) variants. See the [Containers concept page](../concepts/containers.rst) for a |
| detailed overview. |
| |
| | Type | Mutability | Python ABC | Semantics | |
| | ------ | ----------- | ------------ | ----------- | |
| | {py:class}`tvm_ffi.Array` | Immutable | `Sequence[T]` | Homogeneous sequence (copy-on-write) | |
| | {py:class}`tvm_ffi.List` | Mutable | `MutableSequence[T]` | Homogeneous sequence (shared reference) | |
| | {py:class}`tvm_ffi.Map` | Immutable | `Mapping[K, V]` | Homogeneous mapping (copy-on-write) | |
| | {py:class}`tvm_ffi.Dict` | Mutable | `MutableMapping[K, V]` | Homogeneous mapping (shared reference) | |
| |
| ### Array (immutable sequence) |
| |
| When an FFI function takes arguments from lists/tuples, they will be converted into {py:class}`tvm_ffi.Array`. |
| Arrays are **read-only** from Python -- they support indexing and iteration but not mutation. |
| |
| ```python |
| import tvm_ffi |
| |
| # Python lists become Arrays |
| arr = tvm_ffi.convert([1, 2, 3, 4]) |
| assert isinstance(arr, tvm_ffi.Array) |
| assert len(arr) == 4 |
| assert arr[0] == 1 |
| # arr[0] = 10 # TypeError: Array does not support item assignment |
| ``` |
| |
| ### List (mutable sequence) |
| |
| {py:class}`tvm_ffi.List` is a mutable sequence with shared-reference semantics. |
| All handles sharing the same underlying `ListObj` see mutations immediately. |
| |
| ```python |
| import tvm_ffi |
| |
| lst = tvm_ffi.List([1, 2, 3]) |
| assert len(lst) == 3 |
| |
| # Mutate in-place |
| lst.append(4) |
| assert len(lst) == 4 |
| lst[0] = 10 |
| assert lst[0] == 10 |
| ``` |
| |
| ### Map (immutable mapping) |
| |
| Dictionaries will be converted to {py:class}`tvm_ffi.Map`. |
| Maps are **read-only** from Python -- they support key lookup and iteration but not mutation. |
| |
| ```python |
| import tvm_ffi |
| |
| map_obj = tvm_ffi.convert({"a": 1, "b": 2}) |
| assert isinstance(map_obj, tvm_ffi.Map) |
| assert len(map_obj) == 2 |
| assert map_obj["a"] == 1 |
| assert map_obj["b"] == 2 |
| # map_obj["c"] = 3 # TypeError: Map does not support item assignment |
| ``` |
| |
| ### Dict (mutable mapping) |
| |
| {py:class}`tvm_ffi.Dict` is a mutable mapping with shared-reference semantics. |
| All handles sharing the same underlying `DictObj` see mutations immediately. |
| |
| ```python |
| import tvm_ffi |
| |
| d = tvm_ffi.Dict({"a": 1, "b": 2}) |
| d["c"] = 3 |
| assert len(d) == 3 |
| del d["a"] |
| assert len(d) == 2 |
| ``` |
| |
| ### Tuple conversion |
| |
| When a Python tuple is passed to an FFI function, it is converted to {py:class}`tvm_ffi.Array` |
| (the same backing type). The C++ `Tuple<Ts...>` template provides compile-time |
| heterogeneous type safety, but on the Python side both lists and tuples map to `Array`. |
| |
| ```python |
| import tvm_ffi |
| |
| t = tvm_ffi.convert((1, "hello", 3.14)) |
| assert isinstance(t, tvm_ffi.Array) |
| ``` |
| |
| When container values are returned from FFI functions, they are stored in the |
| corresponding container type (Array, List, Map, or Dict). |
| |
| ## Inline Module |
| |
| You can also load a _inline module_ where the C++/CUDA code is directly embedded in the Python script and then compiled |
| on the fly. For example, we can define a simple kernel that adds one to each element of an array as follows: |
| |
| ```python |
| import torch |
| from tvm_ffi import Module |
| import tvm_ffi.cpp |
| |
| # define the cpp source code |
| cpp_source = ''' |
| void add_one_cpu(tvm::ffi::TensorView x, tvm::ffi::TensorView y) { |
| // implementation of a library function |
| TVM_FFI_ICHECK(x.ndim() == 1) << "x must be a 1D tensor"; |
| DLDataType f32_dtype{kDLFloat, 32, 1}; |
| TVM_FFI_ICHECK(x.dtype() == f32_dtype) << "x must be a float tensor"; |
| TVM_FFI_ICHECK(y.ndim() == 1) << "y must be a 1D tensor"; |
| TVM_FFI_ICHECK(y.dtype() == f32_dtype) << "y must be a float tensor"; |
| TVM_FFI_ICHECK(x.size(0) == y.size(0)) << "x and y must have the same shape"; |
| for (int i = 0; i < x.size(0); ++i) { |
| static_cast<float*>(y.data_ptr())[i] = static_cast<float*>(x.data_ptr())[i] + 1; |
| } |
| } |
| ''' |
| |
| # compile the cpp source code and load the module |
| mod: Module = tvm_ffi.cpp.load_inline( |
| name='hello', cpp_sources=cpp_source, functions='add_one_cpu' |
| ) |
| |
| # use the function from the loaded module to perform |
| x = torch.tensor([1, 2, 3, 4, 5], dtype=torch.float32) |
| y = torch.empty_like(x) |
| mod.add_one_cpu(x, y) |
| torch.testing.assert_close(x + 1, y) |
| ``` |
| |
| The above code defines a C++ function `add_one_cpu` in Python script, compiles it on the fly and then loads the compiled |
| {py:class}`tvm_ffi.Module` object via {py:func}`tvm_ffi.cpp.load_inline`. You can then call the function `add_one_cpu` |
| from the module as usual. |
| |
| We can also use {py:func}`tvm_ffi.cpp.build_inline` to build the inline module without loading it. The built shared library is returned |
| and can be loaded via {py:func}`tvm_ffi.load_module`. |
| |
| ## Error Handling |
| |
| An FFI function may raise an error. In such cases, the Python package will automatically |
| translate the error to the corresponding error kind in Python |
| |
| ```python |
| import tvm_ffi |
| |
| # defined in C++ |
| # [](String kind, String msg) { throw Error(kind, msg, backtrace); } |
| test_raise_error = tvm_ffi.get_global_func("testing.test_raise_error") |
| |
| test_raise_error("ValueError", "message") |
| ``` |
| |
| The above code shows an example where an error is raised in C++, resulting in the following error trace |
| |
| ```text |
| Traceback (most recent call last): |
| File "example.py", line 7, in <module> |
| test_raise_error("ValueError", "message") |
| ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^ |
| File "python/tvm_ffi/cython/function.pxi", line 927, in tvm_ffi.core.Function.__call__ |
| raise error.py_error() |
| ^^^ |
| File "src/ffi/extra/testing.cc", line 60, in void tvm::ffi::TestRaiseError(tvm::ffi::String, tvm::ffi::String) |
| throw ffi::Error(kind, msg, TVMFFITraceback(__FILE__, __LINE__, TVM_FFI_FUNC_SIG, 0)); |
| |
| ``` |
| |
| We register common error kinds. You can also register extra error dispatch via the {py:func}`tvm_ffi.register_error` function. |
| |
| ## Advanced: Register Your Own Object |
| |
| For advanced use cases, you may want to register your own objects. This can be achieved through the |
| reflection registry in the TVM-FFI API. First, let's review the C++ side of the code. For this |
| example, you do not need to change the C++ side as this code is pre-shipped with the testing module of the `tvm_ffi` package. |
| |
| ```cpp |
| #include <tvm/ffi/reflection/registry.h> |
| |
| // Step 1: Define the object class (stores the actual data) |
| class TestIntPairObj : public tvm::ffi::Object { |
| public: |
| int64_t a; |
| int64_t b; |
| |
| TestIntPairObj() = default; |
| TestIntPairObj(int64_t a, int64_t b) : a(a), b(b) {} |
| |
| // Required: declare type information |
| TVM_FFI_DECLARE_OBJECT_INFO_FINAL("testing.TestIntPair", TestIntPairObj, tvm::ffi::Object); |
| }; |
| |
| // Step 2: Define the reference wrapper (user-facing interface) |
| class TestIntPair : public tvm::ffi::ObjectRef { |
| public: |
| // Constructor |
| explicit TestIntPair(int64_t a, int64_t b) { |
| data_ = tvm::ffi::make_object<TestIntPairObj>(a, b); |
| } |
| |
| // Required: define object reference methods |
| TVM_FFI_DEFINE_OBJECT_REF_METHODS_NULLABLE(TestIntPair, tvm::ffi::ObjectRef, TestIntPairObj); |
| }; |
| |
| TVM_FFI_STATIC_INIT_BLOCK() { |
| namespace refl = tvm::ffi::reflection; |
| // register the object into the system |
| // register field accessors and a global static function `__ffi_init__` as ffi::Function |
| refl::ObjectDef<TestIntPairObj>() |
| .def(refl::init<int64_t, int64_t>()) |
| .def_ro("a", &TestIntPairObj::a) |
| .def_ro("b", &TestIntPairObj::b); |
| } |
| ``` |
| |
| You can then create wrapper classes for objects that are in the library as follows: |
| |
| ```python |
| import tvm_ffi |
| |
| # Register the class |
| @tvm_ffi.register_object("testing.TestIntPair") |
| class TestIntPair(tvm_ffi.Object): |
| def __init__(self, a, b): |
| # This is a special method to call an FFI function whose return |
| # value exactly initializes the object handle of the object |
| self.__ffi_init__(a, b) |
| |
| test_int_pair = TestIntPair(1, 2) |
| # We can access the fields by name |
| # The properties are populated by the reflection mechanism |
| assert test_int_pair.a == 1 |
| assert test_int_pair.b == 2 |
| ``` |
| |
| Under the hood, we leverage the information registered through the reflection registry to |
| generate efficient field accessors and methods for each class. |
| |
| Importantly, when you have multiple inheritance, you need to call {py:func}`tvm_ffi.register_object` |
| on both the base class and the child class. |