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 426ec96 [DOCS] Update packaging and binding guide (#164)
426ec96 is described below
commit 426ec96c1b9234d1f63ed430d315e9c5ea5628c1
Author: Tianqi Chen <[email protected]>
AuthorDate: Fri Oct 17 14:35:09 2025 -0400
[DOCS] Update packaging and binding guide (#164)
This PR provides an updates to the python packaging and binding guide.
---
docs/guides/{packaging.md => python_packaging.md} | 201 +++++++++++++++++++--
docs/index.rst | 2 +-
.../packaging/python/my_ffi_extension/__init__.py | 15 +-
examples/packaging/run_example.py | 11 +-
examples/packaging/src/extension.cc | 42 +++++
5 files changed, 255 insertions(+), 16 deletions(-)
diff --git a/docs/guides/packaging.md b/docs/guides/python_packaging.md
similarity index 52%
rename from docs/guides/packaging.md
rename to docs/guides/python_packaging.md
index 4979d02..0c191e9 100644
--- a/docs/guides/packaging.md
+++ b/docs/guides/python_packaging.md
@@ -14,21 +14,196 @@
<!--- KIND, either express or implied. See the License for the -->
<!--- specific language governing permissions and limitations -->
<!--- under the License. -->
-# Packaging
+# Python Binding and Packaging
-This guide explains how to package a tvm-ffi-based library into a Python
ABI-agnostic wheel.
-It demonstrates both source-level builds (for cross-compilation) and builds
based on pre-shipped shared libraries.
+This guide explains how to leverage tvm-ffi to expose C++ functions into
Python and package them into a wheel.
At a high level, packaging with tvm-ffi offers several benefits:
-- **ABI-agnostic wheels**: Works across different Python versions with minimal
dependency.
-- **Universally deployable**: Build once with tvm-ffi and ship to different
environments, including Python and non-Python environments.
+- **Ship one wheel** that can be used across Python versions, including
free-threaded Python.
+- **Multi-language access** to functions from Python, C++, Rust and other
languages that connect to the ABI.
+- **ML Systems Interop** with ML frameworks, DSLs, and libraries while
maintaining minimal dependency.
-While this guide shows how to build a wheel package, the resulting
`my_ffi_extension.so` is agnostic
-to Python, comes with minimal dependencies, and can be used in other
deployment scenarios.
+## Directly using Exported Library
-## Build and Run the Example
+If you just need to expose a simple set of functions,
+you can declare an exported symbol in C++:
-Let's start by building and running the example.
+```c++
+// Compiles to mylib.so
+#include <tvm/ffi/function.h>
+
+int add_one(int x) {
+ return x + 1;
+}
+
+TVM_FFI_DLL_EXPORT_TYPED_FUNC(add_one, add_one)
+```
+
+You then load the exported function in your Python project via
{py:func}`tvm_ffi.load_module`.
+
+```python
+# In your __init__.py
+import tvm_ffi
+
+_LIB = tvm_ffi.load_module("/path/to/mlib.so")
+
+def add_one(x):
+ """Expose mylib.add_one"""
+ return _LIB.add_one(x)
+```
+
+This approach is like using {py:mod}`ctypes` to load and run DLLs, except we
have more powerful features:
+
+- We can pass in and return a richer set of data types such as
{py:class}`tvm_ffi.Tensor` and strings.
+- {py:class}`tvm_ffi.Function` enables natural callbacks to Python lambdas or
other languages.
+- Exceptions are propagated naturally across language boundaries.
+
+## Pybind11 and Nanobind style Usage
+
+For advanced use cases where users may wish to register global functions or
custom object types,
+we also provide a pybind11/nanobind style API to register functions and custom
objects.
+
+```c++
+#include <tvm/ffi/error.h>
+#include <tvm/ffi/reflection/registry.h>
+
+namespace my_ffi_extension {
+
+namespace ffi = tvm::ffi;
+
+/*!
+ * \brief Example of a custom object that is exposed to the FFI library
+ */
+class IntPairObj : public ffi::Object {
+ public:
+ int64_t a;
+ int64_t b;
+
+ IntPairObj() = default;
+ IntPairObj(int64_t a, int64_t b) : a(a), b(b) {}
+
+ int64_t GetFirst() const { return this->a; }
+
+ // Required: declare type information
+ TVM_FFI_DECLARE_OBJECT_INFO_FINAL("my_ffi_extension.IntPair", IntPairObj,
ffi::Object);
+};
+
+/*!
+ * \brief Defines an explicit reference to IntPairObj
+ *
+ * A reference wrapper serves as a reference-counted pointer to the object.
+ * You can use obj->field to access the fields of the object.
+ */
+class IntPair : public tvm::ffi::ObjectRef {
+ public:
+ // Constructor
+ explicit IntPair(int64_t a, int64_t b) {
+ data_ = tvm::ffi::make_object<IntPairObj>(a, b);
+ }
+
+ // Required: define object reference methods
+ TVM_FFI_DEFINE_OBJECT_REF_METHODS_NULLABLE(IntPair, tvm::ffi::ObjectRef,
IntPairObj);
+};
+
+void RaiseError(ffi::String msg) { TVM_FFI_THROW(RuntimeError) << msg; }
+
+TVM_FFI_STATIC_INIT_BLOCK() {
+ namespace refl = tvm::ffi::reflection;
+ refl::GlobalDef()
+ .def("my_ffi_extension.raise_error", RaiseError);
+ // register object definition
+ refl::ObjectDef<IntPairObj>()
+ .def(refl::init<int64_t, int64_t>())
+ // Example static method that returns the second element of the pair
+ .def_static("static_get_second", [](IntPair pair) -> int64_t { return
pair->b; })
+ // Example to bind an instance method
+ .def("get_first", &IntPairObj::GetFirst)
+ .def_ro("a", &IntPairObj::a)
+ .def_ro("b", &IntPairObj::b);
+}
+} // namespace my_ffi_extension
+```
+
+Then these functions and objects can be accessed from Python as long as the
library is loaded.
+You can use {py:func}`tvm_ffi.load_module` or simply use
{py:class}`ctypes.CDLL`. Then you can access
+the function through {py:func}`tvm_ffi.get_global_func` or
{py:func}`tvm_ffi.init_ffi_api`.
+We also allow direct exposure of object via {py:func}`tvm_ffi.register_object`.
+
+```python
+# __init__.py
+import tvm_ffi
+
+def raise_error(msg: str):
+ """Wrap raise error function."""
+ # Usually we reorganize these functions into a _ffi_api.py and load once
+ func = tvm_ffi.get_global_func("my_ffi_extension.raise_error")
+ func(msg)
+
+
+@tvm_ffi.register_object("my_ffi_extension.IntPair")
+class IntPair(tvm_ffi.Object):
+ """IntPair object."""
+
+ def __init__(self, a: int, b: int) -> None:
+ """Construct the object."""
+ # __ffi_init__ call into the refl::init<> registered
+ # in the static initialization block of the extension library
+ self.__ffi_init__(a, b)
+
+
+def run_example():
+ pair = IntPair(1, 2)
+ # prints 1
+ print(pair.get_first())
+ # prints 2
+ print(IntPair.static_get_second(pair))
+ # Raises a RuntimeError("error happens")
+ raise_error("error happens")
+```
+
+### Relations to Existing Solutions
+
+Most current binding systems focus on creating one-to-one bindings
+that take a source language and bind to an existing target language runtime
and ABI.
+We deliberately take a more decoupled approach here:
+
+- Build stable, minimal ABI convention that is agnostic to the target language.
+- Create bindings to connect the source and target language to the ABI.
+
+The focus of this project is the ABI itself which we believe can help the
overall ecosystem.
+We also anticipate there are possibilities for existing binding generators to
also target the tvm-ffi ABI.
+
+**Design philosophy**. We have the following design philosophies focusing on
ML systems.
+
+- FFI and cross-language interop should be first-class citizens in ML systems
rather than an add-on.
+- Enable multi-environment support in both source and target languages.
+- The same ABI should be minimal and targetable by DSL compilers.
+
+Of course, there is always a tradeoff. It is by design impossible to support
arbitrary advanced language features
+in the target language, as different programming languages have their own
design considerations.
+We do believe it is possible to build a universal, effective, and minimal ABI
for machine learning
+system use cases. Based on the above design philosophies, we focus our
cross-language
+interaction interface through the FFI ABI for machine learning systems.
+
+So if you are building projects related to machine learning compilers,
runtimes,
+libraries, frameworks, DSLs, or generally scientific computing, we encourage
you
+to try it out. The extension mechanism can likely support features in other
domains as well
+and we welcome you to try it out as well.
+
+### Mix with Existing Solutions
+
+Because the global registry mechanism only relies on the code being linked,
+you can also partially use tvm-ffi-based registration together with
pybind11/nanobind in your project.
+Just add the related code, link to `libtvm_ffi` and make sure you `import
tvm_ffi` before importing
+your module to ensure related symbols are available.
+This approach may help to quickly leverage some of the cross-language features
we have.
+It also provides more powerful interaction with the host Python language, but
of course the tradeoff
+is that the final library will now also depend on the Python ABI.
+
+## Example Project Walk Through
+
+To get hands-on experience with the packaging flow,
+you can try out an example project in our folder.
First, obtain a copy of the tvm-ffi source code.
```bash
@@ -37,7 +212,7 @@ cd tvm-ffi
```
The examples are now in the examples folder. You can quickly build
-and install the example using the following command.
+and install the example using the following commands.
```bash
cd examples/packaging
@@ -88,7 +263,7 @@ wheel.py-api = "py3"
We use scikit-build-core for building the wheel. Make sure you add tvm-ffi as
a build-system requirement.
Importantly, we should set `wheel.py-api` to `py3` to indicate it is
ABI-generic.
-## Setup CMakeLists.txt
+### Setup CMakeLists.txt
The CMakeLists.txt handles the build and linking of the project.
There are two ways you can build with tvm-ffi:
@@ -136,7 +311,7 @@ In Python or other cases when we dynamically load
libtvm_ffi shipped with the de
you do not need to ship libtvm_ffi.so in your package even if you build
tvm-ffi from source.
The built objects are only used to supply the linking information.
-## Exposing C++ Functions
+### Exposing C++ Functions
The C++ implementation is defined in `src/extension.cc`.
There are two ways one can expose a function in C++ to the FFI library.
@@ -175,7 +350,7 @@ and is expected to stay throughout the lifetime of the
program.
We recommend using `TVM_FFI_DLL_EXPORT_TYPED_FUNC` for functions that are
supposed to be dynamically
loaded (such as JIT scenarios) so they won't be exposed to the global function
table.
-## Library Loading in Python
+### Library Loading in Python
The base module handles loading the compiled extension:
diff --git a/docs/index.rst b/docs/index.rst
index b3a7519..ecea8e6 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -45,7 +45,7 @@ Table of Contents
:maxdepth: 1
:caption: Guides
- guides/packaging.md
+ guides/python_packaging.md
guides/cpp_guide.md
guides/python_guide.md
guides/stable_c_abi.md
diff --git a/examples/packaging/python/my_ffi_extension/__init__.py
b/examples/packaging/python/my_ffi_extension/__init__.py
index ae4abfd..292a03b 100644
--- a/examples/packaging/python/my_ffi_extension/__init__.py
+++ b/examples/packaging/python/my_ffi_extension/__init__.py
@@ -17,12 +17,25 @@
# isort: skip_file
"""Public Python API for the example tvm-ffi extension package."""
-from typing import Any
+from typing import Any, TYPE_CHECKING
+
+import tvm_ffi
from .base import _LIB
from . import _ffi_api
+@tvm_ffi.register_object("my_ffi_extension.IntPair")
+class IntPair(tvm_ffi.Object):
+ """IntPair object."""
+
+ def __init__(self, a: int, b: int) -> None:
+ """Construct the object."""
+ # __ffi_init__ call into the refl::init<> registered
+ # in the static initialization block of the extension library
+ self.__ffi_init__(a, b)
+
+
def add_one(x: Any, y: Any) -> None:
"""Add one to the input tensor.
diff --git a/examples/packaging/run_example.py
b/examples/packaging/run_example.py
index 6b5120f..f2c79f4 100644
--- a/examples/packaging/run_example.py
+++ b/examples/packaging/run_example.py
@@ -35,11 +35,20 @@ def run_raise_error() -> None:
my_ffi_extension.raise_error("This is an error")
+def run_int_pair() -> None:
+ """Invoke IntPair from the extension to demonstrate object handling."""
+ pair = my_ffi_extension.IntPair(1, 2)
+ print(f"first={pair.get_first()}")
+ print(f"second={my_ffi_extension.IntPair.static_get_second(pair)}")
+
+
if __name__ == "__main__":
if len(sys.argv) > 1:
if sys.argv[1] == "add_one":
run_add_one()
+ elif sys.argv[1] == "int_pair":
+ run_int_pair()
elif sys.argv[1] == "raise_error":
run_raise_error()
else:
- print("Usage: python run_example.py <add_one|raise_error>")
+ print("Usage: python run_example.py <add_one|int_pair|raise_error>")
diff --git a/examples/packaging/src/extension.cc
b/examples/packaging/src/extension.cc
index 8d2c504..38525c9 100644
--- a/examples/packaging/src/extension.cc
+++ b/examples/packaging/src/extension.cc
@@ -60,6 +60,38 @@ void AddOne(ffi::TensorView x, ffi::TensorView y) {
// expose global symbol add_one
TVM_FFI_DLL_EXPORT_TYPED_FUNC(add_one, my_ffi_extension::AddOne);
+/*!
+ * \brief Example of a custom object that is exposed to the FFI library
+ */
+class IntPairObj : public tvm::ffi::Object {
+ public:
+ int64_t a;
+ int64_t b;
+
+ IntPairObj() = default;
+ IntPairObj(int64_t a, int64_t b) : a(a), b(b) {}
+
+ int64_t GetFirst() const { return this->a; }
+
+ // Required: declare type information
+ TVM_FFI_DECLARE_OBJECT_INFO_FINAL("my_ffi_extension.IntPair", IntPairObj,
tvm::ffi::Object);
+};
+
+/*!
+ * \brief Defines an explicit reference to IntPairObj
+ *
+ * A reference wrapper serves as a reference-counted ptr to the object.
+ * you can use obj->field to access the fields of the object.
+ */
+class IntPair : public tvm::ffi::ObjectRef {
+ public:
+ // Constructor
+ explicit IntPair(int64_t a, int64_t b) { data_ =
tvm::ffi::make_object<IntPairObj>(a, b); }
+
+ // Required: define object reference methods
+ TVM_FFI_DEFINE_OBJECT_REF_METHODS_NULLABLE(IntPair, tvm::ffi::ObjectRef,
IntPairObj);
+};
+
// The static initialization block is
// called once when the library is loaded.
TVM_FFI_STATIC_INIT_BLOCK() {
@@ -85,5 +117,15 @@ TVM_FFI_STATIC_INIT_BLOCK() {
// tvm::ffi::Module::LoadFromFile, instead, just load the dll or simply
bundle into the
// final project
refl::GlobalDef().def("my_ffi_extension.raise_error", RaiseError);
+ // register the object into the system
+ // register field accessors and a global static function `__ffi_init__` as
ffi::Function
+ refl::ObjectDef<IntPairObj>()
+ .def(refl::init<int64_t, int64_t>())
+ // Example static method that returns the second element of the pair
+ .def_static("static_get_second", [](IntPair pair) -> int64_t { return
pair->b; })
+ // Example to bind an instance method
+ .def("get_first", &IntPairObj::GetFirst)
+ .def_ro("a", &IntPairObj::a)
+ .def_ro("b", &IntPairObj::b);
}
} // namespace my_ffi_extension