Skip to content

[OrcJIT] Add LLVM ORC JIT v2 dynamic object loader addon#254

Open
yaoyaoding wants to merge 93 commits intoapache:mainfrom
yaoyaoding:orcjit
Open

[OrcJIT] Add LLVM ORC JIT v2 dynamic object loader addon#254
yaoyaoding wants to merge 93 commits intoapache:mainfrom
yaoyaoding:orcjit

Conversation

@yaoyaoding
Copy link
Contributor

This PR introduces a new addon package tvm-ffi-orcjit that enables dynamic loading of TVM-FFI exported object files (.o) at runtime using LLVM's ORC JIT v2 engine.

The addon provides a Python API for loading compiled object files, load the tvm-ffi functions defined in the object files.

The API is organized around three main concepts:

  • ExecutionSession: A JIT compilation context that manages the lifetime of dynamic libraries.
  • DynamicLibrary: Represents a shared library that links multiple object files.
  • Object Files: Compiled .o files containing TVM-FFI exported functions.

Usage Example

from tvm_ffi_orcjit import create_session

# Create an execution session (JIT compilation context)
session = create_session()

# Load an object file into the session, returns a DynamicLibrary handle
dylib = session.load("example.o")

# Get and call a function from the loaded object file
add_func = dylib.get_function("simple_add")
result = add_func(1, 2)
print(f"Result: {result}")  # Output: Result: 3

For incremental loading, you can add multiple object files to the same session:

session = create_session()

# Load first object file
dylib = session.load("math_ops.o")
add = dylib.get_function("simple_add")

# Incrementally load more object files into the same library
dylib.load("string_ops.o")

# Now both functions are accessible from the same library
concat = dylib.get_function("string_concat")

See the test for more example.

TODO

  • Support Windows platform (currently Linux and macOS only)
  • Customize memory allocation for dynamic libraries (allow user-provided allocators)

@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @yaoyaoding, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces a significant new capability to TVM-FFI by integrating LLVM's ORC JIT v2. This integration enables the dynamic loading and execution of C++ object files directly from Python, offering a flexible and efficient way to extend TVM-FFI's functionality at runtime. The new addon provides a clear Python API for managing JIT compilation contexts and dynamic libraries, making it easier for developers to incorporate custom C++ logic without requiring a full recompile of their applications.

Highlights

  • New Addon Package: Introduces tvm-ffi-orcjit, a new addon package designed to enable dynamic loading of TVM-FFI exported object files (.o) at runtime.
  • LLVM ORC JIT v2 Integration: Leverages LLVM's ORC JIT v2 engine for Just-In-Time compilation and execution of C++ code, providing a powerful and flexible runtime environment.
  • Python API: Provides a Pythonic API centered around ExecutionSession (JIT compilation context) and DynamicLibrary (manages object files and symbols) for seamless interaction.
  • Incremental Loading: Supports incremental loading, allowing multiple object files to be added to the same DynamicLibrary instance, with functions from all loaded files remaining accessible.
  • Comprehensive Examples and Tests: Includes a quick-start example demonstrating basic usage and a robust test suite covering core functionalities, including symbol resolution and conflict handling.
Ignored Files
  • Ignored by pattern: .github/workflows/** (3)
    • .github/workflows/orcjit-publish.yml
    • .github/workflows/orcjit-tests.yml
    • .github/workflows/tvm-ffi-orcjit/ci_test.yml
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a new addon package, tvm-ffi-orcjit, to enable dynamic loading of object files using LLVM's ORC JIT v2. The overall design is robust, with a clear separation between the C++ backend and the Python frontend. The implementation demonstrates a deep understanding of LLVM's JIT capabilities, including a clever workaround for the __dso_handle issue. The tests are comprehensive and cover important scenarios like symbol conflicts.

I have identified a few areas for improvement, primarily concerning inconsistencies in documentation, build scripts, and a potential bug in the CMake configuration. There is also a mismatch between a Python API and its C++ backend. Addressing these points will enhance the quality and usability of this new addon.

target_link_libraries(
tvm_ffi_orcjit
PUBLIC tvm_ffi
PRIVATE LLVM
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The find_package(LLVM ... CONFIG) command does not create a single LLVM target. Instead, llvm_map_components_to_libnames populates the LLVM_LIBS variable with a list of component library targets. You should link against ${LLVM_LIBS}.

  PRIVATE ${LLVM_LIBS}

Comment on lines +83 to +105
def link_against(self, *libraries: DynamicLibrary) -> None:
"""Link this library against other dynamic libraries.

Sets the search order for symbol resolution. Symbols not found in this library
will be searched in the linked libraries in the order specified.

Parameters
----------
*libraries : DynamicLibrary
One or more dynamic libraries to link against.

Examples
--------
>>> session = create_session()
>>> lib_utils = session.create_library()
>>> lib_utils.add("utils.o")
>>> lib_main = session.create_library()
>>> lib_main.add("main.o")
>>> lib_main.link_against(lib_utils) # main can call utils symbols

"""
handles = [lib._handle for lib in libraries]
self._link_func(self._handle, *handles)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The link_against method accepts *libraries, which implies it can link against multiple libraries. However, the C++ backend function orcjit.DynamicLibraryLinkAgainst only supports linking against a single library at a time. This will cause a TypeError if more than one library is passed.

To fix this, you should either update the Python method to accept only one library or modify the C++ backend to handle multiple libraries. Given the current C++ implementation, changing the Python API is the most direct solution.

    def link_against(self, library: DynamicLibrary) -> None:
        """Link this library against another dynamic library.

        Sets the search order for symbol resolution. Symbols not found in this library
        will be searched in the linked library.

        Parameters
        ----------
        library : DynamicLibrary
            The dynamic library to link against.

        Examples
        --------
        >>> session = create_session()
        >>> lib_utils = session.create_library()
        >>> lib_utils.add("utils.o")
        >>> lib_main = session.create_library()
        >>> lib_main.add("main.o")
        >>> lib_main.link_against(lib_utils)  # main can call utils symbols

        """
        self._link_func(self._handle, library._handle)

Comment on lines +19 to +20
separate_arguments(LLVM_DEFINITIONS_LIST NATIVE_COMMAND ${LLVM_DEFINITIONS})
add_definitions(${LLVM_DEFINITIONS_LIST})
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

These add_definitions are redundant as target_compile_definitions is used for the tvm_ffi_orcjit target on line 93. It's better to use target_compile_definitions for target-specific definitions. I suggest removing these lines to avoid redundancy.

Comment on lines +93 to +127
from tvm_ffi_orcjit import ObjectLoader

# Create a loader instance
loader = ObjectLoader()

# Load an object file
loader.load("example.o")

# Get and call a function
add_func = loader.get_function("simple_add")
result = add_func(1, 2)
print(f"Result: {result}") # Output: Result: 3
```

### Incremental Loading

Load multiple object files and access functions from all of them:

```python
from tvm_ffi_orcjit import ObjectLoader

loader = ObjectLoader()

# Load first object file
loader.load("math_ops.o")
add = loader.get_function("simple_add")

# Load second object file - functions from first remain accessible
loader.load("string_ops.o")
concat = loader.get_function("string_concat")

# Both functions work
print(add(10, 20)) # From math_ops.o
print(concat("Hello", "World")) # From string_ops.o
```
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The usage examples in this README appear to be outdated. They refer to an ObjectLoader class which is not present in the current implementation. The correct API, as demonstrated in examples/quick-start/run.py, uses create_session, session.create_library, and lib.add(). Please update the examples to reflect the current API to avoid confusion for new users.

Comment on lines +129 to +142
### Direct Module Access

You can also use TVM-FFI's `load_module` directly (`.o` files are automatically handled):

```python
import tvm_ffi

# Load object file as a module
module = tvm_ffi.load_module("example.o")

# Get function
func = module.get_function("my_function")
result = func(arg1, arg2)
```
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This section on "Direct Module Access" suggests that tvm_ffi.load_module("example.o") is supported. However, the implementation does not seem to register a module loader for .o files. If this feature is not yet implemented, it would be best to remove this section or clearly mark it as a future capability to prevent user confusion.

yaoyaoding and others added 3 commits November 10, 2025 18:40
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
int (*)(void* handle, const TVMFFIAny* args, int32_t num_args, TVMFFIAny* rv);
auto c_func = reinterpret_cast<TVMFFISafeCallType>(symbol);

return Function::FromPacked([c_func, name](PackedArgs args, Any* rv) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Refer to existing LibraryModule impl.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@yzh119
Copy link
Member

yzh119 commented Nov 11, 2025

One general question, how do we plan to manage the versions of these add ons?

@tqchen
Copy link
Member

tqchen commented Nov 11, 2025

I think most likely they can evolve independently for now and optional

cyx-6 and others added 29 commits March 9, 2026 08:20
- CMakeLists.txt: statically link LLVM via llvm-config --link-static,
  find and bundle liborc_rt.a via clang -print-runtime-dir, support
  LLVM_CONFIG_PATH and CLANG_PATH env var overrides
- session.py: simplified to find bundled liborc_rt from _lib_dir,
  removed runtime clang dependency
- __init__.py: export _lib_dir, reduced library search paths
- pyproject.toml: added cibuildwheel config with conda-forge LLVM,
  auditwheel exclude for libtvm_ffi.so, fixed metadata
- Added tools/install_llvm.sh for cross-platform LLVM install
- Added .github/workflows/orcjit-build.yml CI workflow

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Since the output is a shared library, system dependencies like
-lxml2 -lz -lzstd are resolved by the dynamic linker at load time.
This avoids needing dev packages or symlinks in the build container.

Also removes libxml2/zlib/zstd from conda install and LDFLAGS from
cibuildwheel environment as they are no longer needed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Remove llvm-config --system-libs from link command; since the output
  is a .so, system deps resolve via dynamic linker at load time.
- Remove libxml2/zlib/zstd from conda install (no longer needed).
- Target manylinux_2_35 (GLIBC 2.35, Ubuntu 22.04+) to match the
  GLIBC version used by conda-forge LLVM 22 static libraries.
- Restore auditwheel repair with libtvm_ffi.so exclusion.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
gcc-toolset-14 in manylinux_2_28 produces binaries with glibc symbols
too recent for the manylinux_2_28 ABI. Use conda-forge's clang instead,
which targets glibc 2.17 and is compatible with manylinux_2_28.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The --plat manylinux_2_35_x86_64 flag handles the glibc compatibility.
No need to override the container's default compiler.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Install zlib and libzstd-static via conda-forge, link them as static
archives instead of -lz/-lzstd, and add -static-libstdc++ -static-libgcc
to eliminate GLIBCXX_3.4.30 runtime dependency. This lets auditwheel
use the default manylinux_2_28 platform tag.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Use system packages instead of conda/source-build for static zlib and
zstd. Also search /usr/lib64 in CMake find_library hints.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Build on AlmaLinux 9 (manylinux_2_34) where static zstd is available
via dnf, but tag wheels as manylinux_2_28 since static linking avoids
newer glibc deps.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
No AlmaLinux distro (8 or 9) ships a libzstd static package.
Use yum zlib-static + build zstd 1.5.7 from source into PREFIX.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
System libz.a is not PIC-compiled and can't link into a shared lib.
Use conda-forge zlib instead. Build zstd with CMAKE_POSITION_INDEPENDENT_CODE=ON.
Also search lib64/ for zstd since cmake installs there on x86_64.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…dd CI tests

Intercept __cxa_atexit with a per-dylib custom implementation so static
destructors run at JIT module teardown instead of process exit (avoiding
use-after-free). Add a one-time warning when JIT'd functions return
reference-counted objects. Add pytest and quick-start testing to the
orcjit-build CI workflow.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The test cmake needs the venv python to find tvm_ffi.config.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The test venv doesn't have apache-tvm-ffi installed when
CIBW_BEFORE_TEST runs, causing cmake to fail finding tvm_ffi headers.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Keep only orcjit-build.yml which handles both building and testing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants