Skip to content

Commit ff5485a

Browse files
committed
[ORCJIT] Add shared session and high-level load_module
Introduce a process-wide shared ExecutionSession and a high-level load_module unit operation on the ORCJIT addon, replacing the low-level create_library / add / set_link_order surface. - Session: GlobalDefault() singleton resolving the ORC runtime path from a Python-registered tvm_ffi_orcjit.DefaultOrcRuntimePath hook; plain (non-recursive) mutex; low-level dylib ops (add object / lookup / finalize) are internal, composed by LoadModule. - load_module(objects, name, keep_module_alive): links one or more object files/images into a fresh JITDylib, injects context symbols eagerly, and expands any embedded library binary. keep_module_alive pins the module in the runtime's global registry so JIT-allocated Objects may outlive the local handle. - Reduced in-addon library-binary parser with adversarial bounds checks. - CI: build the wheel against the in-tree core (--no-build-isolation) so the addon's tvm_ffi ABI matches the core the tests reinstall. - Link libstdc++/libgcc dynamically and hide static-archive (LLVM/zlib/zstd) symbols via --exclude-libs,ALL, so the JIT resolves the C++ runtime from the process without leaking LLVM symbols that could interpose with the host. - Tests for session/load_module, malformed blobs, concurrency, and container returns; docs and examples updated for the unit-op API.
1 parent f64ddbe commit ff5485a

21 files changed

Lines changed: 1482 additions & 817 deletions

File tree

.github/actions/build-orcjit-wheel/action.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,10 @@ runs:
102102
CIBW_ENVIRONMENT: LLVM_PREFIX=/opt/llvm
103103
CIBW_ENVIRONMENT_WINDOWS: LLVM_PREFIX="C:/opt/llvm"
104104
CIBW_CONTAINER_ENGINE: "docker; create_args: --volume /opt/llvm:/opt/llvm"
105+
# Build against the in-tree core (not PyPI) so the addon's tvm_ffi ABI
106+
# matches the core the tests reinstall; a mismatch segfaults the JIT.
107+
CIBW_BEFORE_BUILD: pip install "scikit-build-core>=0.10.0" cmake ninja "{project}"
108+
CIBW_BUILD_FRONTEND: "pip; args: --no-build-isolation"
105109
CIBW_TEST_REQUIRES: pytest pytest-xdist ninja
106110
CIBW_TEST_COMMAND: >-
107111
pip install --force-reinstall {project} &&

addons/tvm_ffi_orcjit/CMakeLists.txt

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,9 +149,27 @@ if (APPLE)
149149
COMMENT "Fixing libc++ rpath to use system library"
150150
)
151151
elseif (UNIX AND NOT WIN32)
152+
# Static libstdc++/libgcc: the conda LLVM toolchain's libstdc++ is newer than the manylinux ABI
153+
# floor, so linking it dynamically would import too-recent GLIBCXX_* symbols and fail auditwheel.
152154
target_link_options(tvm_ffi_orcjit PRIVATE -static-libstdc++ -static-libgcc)
153155
endif ()
154156

157+
# Hide symbols pulled from static archives so they are not re-exported and cannot interpose with a
158+
# host process's own copies (e.g. PyTorch's bundled LLVM). Exclude LLVM/zlib/zstd by name -- NOT
159+
# --exclude-libs,ALL, which would also hide the static libstdc++ symbols (operator new, __throw_*,
160+
# ...) that liborc_rt and JIT'd C++ code resolve from this .so at run time. The LLVM list is derived
161+
# from llvm-config so it tracks the toolchain version.
162+
if (CMAKE_SYSTEM_NAME MATCHES "Linux|Android|FreeBSD|NetBSD|OpenBSD" AND CMAKE_CXX_COMPILER_ID
163+
MATCHES "GNU|Clang"
164+
)
165+
# --exclude-libs needs exact archive filenames (no globs), so turn each -lLLVM* from llvm-config
166+
# into libLLVM*.a and add zlib/zstd. One colon-separated list -- a -Wl comma list would be split.
167+
set(_exclude_archives ${_llvm_libs_list} "-lz" "-lzstd")
168+
list(TRANSFORM _exclude_archives REPLACE "^-l(.+)$" "lib\\1.a")
169+
list(JOIN _exclude_archives ":" _exclude_libs_arg)
170+
target_link_options(tvm_ffi_orcjit PRIVATE "-Wl,--exclude-libs=${_exclude_libs_arg}")
171+
endif ()
172+
155173
# ---- Find and bundle liborc_rt ----
156174
# Platform notes:
157175
#

addons/tvm_ffi_orcjit/ORCJIT_PRIMER.md

Lines changed: 42 additions & 135 deletions
Original file line numberDiff line numberDiff line change
@@ -345,128 +345,40 @@ With the background above, here is how the addon maps onto the concepts.
345345
```text
346346
Python / C++ API LLVM ORC v2 concept
347347
─────────────────────────────────────────────────────────────────
348+
default_session() shared (leaked) LLJIT + ExecutionSession
348349
ExecutionSession LLJIT + ExecutionSession
349-
DynamicLibrary JITDylib
350-
dylib.add("foo.o") ObjectLinkingLayer.add(buffer)
351-
dylib.get_function("add") ExecutionSession.lookup("__tvm_ffi_add")
352-
dylib.set_link_order([a, b]) JITDylib.setLinkOrder([a, b, main, ...])
350+
session.load_module(objs) createJITDylib + addObjectFile(s) + wire imports
351+
(returned) Module JITDylib, wrapped as a tvm_ffi.Module
352+
module.get_function("add") ExecutionSession.lookup("__tvm_ffi_add")
353353
```
354354

355-
### 4.2 Object File Loading Pipeline
356-
357-
```text
358-
dylib.add("foo.o")
359-
360-
▼ ORCJITDynamicLibraryObj::AddObjectFile()
361-
│ jit_->addObjectFile(*dylib_, MemoryBuffer)
362-
363-
▼ ObjectTransformLayer (macOS: skipped; Linux/Win: may strip .pdata)
364-
│ Windows: strip .pdata/.xdata avoids JITLink COMDAT limitation
365-
│ fix __ImageBase fixes Pointer32NB relocations
366-
367-
▼ ObjectLinkingLayer (JITLink)
368-
│ Parse object → LinkGraph
369-
│ Run InitFiniPlugin passes:
370-
│ PrePrunePasses: mark .init_array / .ctors blocks as live
371-
│ PostAllocationPasses: (Windows) set __ImageBase, strip SEH sections
372-
│ PostFixupPasses: extract resolved init/fini function pointers
373-
│ → session.AddPendingInitializer(dylib, entry)
374-
│ Resolve relocations via ExecutionSession.lookup()
375-
│ Allocate JIT memory, write code, mark executable
376-
377-
▼ JITDylib symbol table updated
378-
(symbols are defined but constructors not yet run)
379-
```
380-
381-
### 4.3 Symbol Lookup and Initialization
382-
383-
```text
384-
dylib.get_function("add")
385-
386-
▼ ORCJITDynamicLibraryObj::GetFunction("add")
387-
│ → GetSymbol("__tvm_ffi_add")
388-
│ build JITDylibSearchOrder: [this, linked dylibs, LLJIT default]
389-
│ jit_->getExecutionSession().lookup(order, "__tvm_ffi_add")
390-
│ ← ExecutorAddr
391-
392-
▼ Linux/Windows: RunPendingInitializers()
393-
│ Sort entries by priority
394-
│ Call each function pointer (C++ ctors, .init_array entries)
395-
396-
▼ macOS: jit_->initialize(*dylib_)
397-
│ ORC MachOPlatform calls __mod_init_func pointers
398-
399-
▼ Wrap raw function pointer as tvm_ffi::Function
400-
via Function::FromPacked lambda (marshals AnyView args ↔ TVMFFIAny)
401-
```
402-
403-
### 4.4 InitFiniPlugin Detail
404-
405-
`InitFiniPlugin` is an `ObjectLinkingLayer::Plugin` — it receives callbacks during
406-
JITLink's pass pipeline for every object being linked.
407-
408-
```text
409-
JITLink pass pipeline for each object file:
410-
─────────────────────────────────────────────────────────────
411-
PrePrunePasses
412-
→ InitFiniPlugin::modifyPassConfig (PrePrunePasses)
413-
For each section named .init_array* / .ctors* / .fini_array* / .dtors*
414-
(ELF) or __DATA,__mod_init_func (Mach-O) or .CRT$XC* (COFF):
415-
Mark all blocks in that section as "keep" (live)
416-
→ Prevents dead-code elimination from removing constructors
417-
418-
PostAllocationPasses (Windows only)
419-
→ InitFiniPlugin::modifyPassConfig (PostAllocationPasses)
420-
Set __ImageBase = lowest allocated block address
421-
Strip .pdata / .xdata exception handler sections
422-
423-
PostFixupPasses
424-
→ InitFiniPlugin::modifyPassConfig (PostFixupPasses)
425-
Iterate all blocks in init/fini sections
426-
Read each 8-byte slot as an ExecutorAddr
427-
Parse section name to determine: priority, is_init vs is_fini
428-
Call session->AddPendingInitializer(dylib, InitFiniEntry{addr, section, priority})
429-
(or AddPendingDeinitializer for dtors/fini_array)
430-
```
431-
432-
### 4.5 Cross-Library Symbol Resolution
433-
434-
```text
435-
libA depends on a symbol defined in libB:
436-
437-
libA.set_link_order([libB])
438-
→ JITDylibSearchOrder for libA: [libA, libB, main(ProcessSymbols)]
439-
440-
When libA's object file has an unresolved symbol "foo":
441-
JITLink asks ExecutionSession.lookup([libA, libB, main], "foo")
442-
→ found in libB → returns libB's ExecutorAddr for "foo"
443-
→ relocation in libA patched with that address
444-
```
445-
446-
### 4.6 Windows DLL Import Stubs
447-
448-
Windows MSVC objects reference DLL functions through `__imp_XXX` pointer stubs (the
449-
Import Address Table pattern). At static link time the linker creates these stubs. In
450-
JIT mode there is no linker, so `DLLImportDefinitionGenerator` creates them on demand:
451-
452-
```text
453-
JITLink encounters undefined symbol "__imp_malloc"
454-
455-
▼ DLLImportDefinitionGenerator::tryToGenerate()
456-
│ Search ucrtbase.dll, msvcrt.dll, then all process modules
457-
│ for the real address of "malloc"
458-
459-
▼ Allocate two JIT-memory stubs:
460-
│ __imp_malloc → 8-byte slot containing &malloc (host address)
461-
│ malloc → x86_64 jmp [__imp_malloc] trampoline
462-
463-
▼ Define both symbols in the JITDylib
464-
→ JITLink can now apply PCRel32 reloc to __imp_malloc (stub is close in JIT memory)
465-
```
466-
467-
The stubs must live in JIT-allocated memory (not at the host process address) because
468-
x86_64 `PCRel32` relocations can only reach ±2 GB. The host's `malloc` may be farther
469-
than 2 GB from the JIT allocation.
355+
`session.load_module(objects)` is the sole public loader: it creates one
356+
`JITDylib`, adds every object (path or in-memory bytes), injects context symbols
357+
eagerly, expands any embedded library binary into an import tree, and returns a
358+
plain `tvm_ffi.Module`. There is no incremental library API — a module is a
359+
unit: load all its objects at once, then look up functions on the result.
360+
361+
### 4.2 Loading and lookup
362+
363+
`load_module` adds each object to the JITDylib, and JITLink parses it into a
364+
`LinkGraph`, resolves relocations, and allocates executable JIT memory.
365+
`get_function` looks the symbol up (materializing lazily), then wraps the raw
366+
pointer as a `tvm_ffi::Function`. Symbols resolve against the dylib's own
367+
default link order (this dylib → Platform → process/runtime symbols); objects
368+
that reference each other must be loaded together, since there is no linking
369+
between separate `load_module` results.
370+
371+
Two addon-specific pieces sit in this pipeline:
372+
373+
- **`InitFiniPlugin`** — a JITLink pass plugin that keeps init/fini sections
374+
(`.init_array`/`.ctors`/`.fini_array`/`.dtors`, `__mod_init_func`, `.CRT$XC*`)
375+
live, then collects their function pointers after fixup. The addon runs them
376+
in priority order at first lookup and at teardown, replacing the ORC
377+
platform's initializer machinery. See `llvm_patches/init_fini_plugin.h`.
378+
- **Windows DLL import stubs**`DLLImportDefinitionGenerator` resolves
379+
`__imp_XXX` references to host-DLL functions by emitting JIT-memory pointer +
380+
trampoline stubs, keeping `PCRel32` fixups within ±2 GB of the JIT code. See
381+
`llvm_patches/win_dll_import_generator.h`.
470382

471383
---
472384

@@ -475,22 +387,17 @@ than 2 GB from the JIT allocation.
475387
```python
476388
import tvm_ffi_orcjit as oj
477389

478-
# 1. Create an ExecutionSession (wraps LLJIT)
479-
sess = oj.ExecutionSession()
480-
481-
# 2. Create a JITDylib
482-
lib = sess.create_dynamic_library()
483-
484-
# 3. Load a compiled object file
485-
# → object parsed, JITLink links it, InitFiniPlugin collects ctors
486-
lib.add("add.o")
390+
# 1. Get the shared ExecutionSession (wraps LLJIT; created once per process)
391+
sess = oj.default_session()
487392

488-
# 4. Look up a function
489-
# → LLVM resolves "__tvm_ffi_add", RunPendingInitializers() fires ctors
490-
add = lib.get_function("add") # returns tvm_ffi.Function
393+
# 2. Load a compiled object file into a fresh JITDylib
394+
# → object parsed, JITLink links it, InitFiniPlugin collects ctors,
395+
# context symbols injected eagerly, embedded binary (if any) expanded
396+
mod = sess.load_module("add.o") # returns a tvm_ffi.Module
491397

492-
# 5. Call it
493-
result = add(3, 4) # → 7
398+
# 3. Look up and call a function
399+
# → LLVM resolves "__tvm_ffi_add"; pending constructors fire on first lookup
400+
result = mod.add(3, 4) # → 7
494401
```
495402

496403
The corresponding C++ side of `add.o`:
@@ -511,7 +418,7 @@ TVM_FFI_DLL_EXPORT_TYPED_FUNC(add, add_impl);
511418
512419
| Concept | What it is | Where in the addon |
513420
| --- | --- | --- |
514-
| Object file | Container of machine code, data, symbols, relocations | Input to `dylib.add()` |
421+
| Object file | Container of machine code, data, symbols, relocations | Input to `session.load_module()` |
515422
| Relocation | Recipe to patch a code address at link/JIT time | Applied by JITLink |
516423
| `.init_array` / `.ctors` | Array of C++ constructor pointers in ELF objects | Collected by `InitFiniPlugin` |
517424
| `ExecutionSession` | Root of the ORC JIT environment | `ORCJITExecutionSessionObj` |
@@ -520,7 +427,7 @@ TVM_FFI_DLL_EXPORT_TYPED_FUNC(add, add_impl);
520427
| `JITLink` | LLVM's JIT-aware linker | Used inside `ObjectLinkingLayer` |
521428
| JITLink pass pipeline | Pre-prune → post-alloc → post-fixup hooks | Where `InitFiniPlugin` runs |
522429
| `DefinitionGenerator` | Fallback symbol provider | `DLLImportDefinitionGenerator` (Win) |
523-
| Link order | Search path across JITDylibs for symbol resolution | `SetLinkOrder()` |
430+
| Link order | Search path across JITDylibs for symbol resolution | LLJIT default (Main → Platform → ProcessSymbols) |
524431
| `__tvm_ffi_` prefix | Namespace for TVM-FFI exported functions | Used in `GetFunction()` |
525432
526433
---

addons/tvm_ffi_orcjit/README.md

Lines changed: 39 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,10 @@ TVM-FFI exported functions.
2424
## Features
2525

2626
- **JIT Execution**: Load and execute compiled object files at runtime using LLVM's ORC JIT v2
27-
- **Multiple Libraries**: Create separate dynamic libraries with independent symbol namespaces
28-
- **Incremental Loading**: Add multiple object files to the same library incrementally
29-
- **Symbol Isolation**: Different libraries can define the same symbol without conflicts
27+
- **High-Level Loading**: `default_session().load_module(...)` mirrors `tvm_ffi.load_module`, returning a plain `tvm_ffi.Module`
28+
- **Unified Input**: Load from a file path, in-memory object bytes, or a list mixing both
29+
- **Shared Session**: A process-wide session so multiple callers share one JIT environment (process symbols, arena, linking)
30+
- **Symbol Isolation**: Separate `load_module` calls define independent symbol namespaces, so they can define the same symbol without conflicts
3031
- **Init/Fini Support**: Handles static constructors/destructors across ELF (`.init_array`/`.ctors`), Mach-O (`__mod_init_func`), and COFF (`.CRT$XC*`/`.CRT$XT*`)
3132
- **Cross-Platform**: Linux (x86_64, aarch64), macOS (arm64), Windows (AMD64)
3233
- **Multi-Compiler**: Tested with LLVM Clang, GCC, Apple Clang, MSVC, and clang-cl
@@ -105,55 +106,54 @@ auto-discover it and `LLVM_PREFIX` is not needed.
105106

106107
### Basic Example
107108

108-
```python
109-
from tvm_ffi_orcjit import ExecutionSession
109+
The high-level API mirrors `tvm_ffi.load_module`: a process-wide shared session
110+
plus a `load_module` that accepts a path, in-memory object bytes, or a list of
111+
either, and returns a plain `tvm_ffi.Module`.
110112

111-
# Create an execution session
112-
session = ExecutionSession()
113+
```python
114+
import tvm_ffi_orcjit as oj
113115

114-
# Create a dynamic library
115-
lib = session.create_library()
116+
# Shared process-wide session (created once, cached).
117+
session = oj.default_session()
116118

117-
# Load an object file
118-
lib.add("example.o")
119+
# Load a single object file by path.
120+
mod = session.load_module("example.o")
119121

120-
# Get and call a function
121-
add_func = lib.get_function("add")
122-
result = add_func(1, 2)
122+
# Call an exported function.
123+
result = mod.add(1, 2)
123124
print(f"Result: {result}") # Output: Result: 3
124125
```
125126

126-
### Multiple Libraries with Symbol Isolation
127-
128-
```python
129-
session = ExecutionSession()
127+
### Loading Multiple Objects and In-Memory Bytes
130128

131-
lib1 = session.create_library("lib1")
132-
lib2 = session.create_library("lib2")
129+
Objects passed together are linked into one module (the same way a multi-object
130+
shared library links). Each element may be a path or an object-file image in
131+
memory.
133132

134-
lib1.add("implementation_v1.o")
135-
lib2.add("implementation_v2.o")
133+
```python
134+
from pathlib import Path
136135

137-
add_v1 = lib1.get_function("add")
138-
add_v2 = lib2.get_function("add")
136+
session = oj.default_session()
139137

140-
print(add_v1(5, 3)) # Uses implementation from lib1
141-
print(add_v2(5, 3)) # Uses implementation from lib2
138+
mod = session.load_module(
139+
[
140+
"math_ops.o", # path
141+
Path("kernel.o").read_bytes(), # in-memory object bytes
142+
]
143+
)
144+
result = mod.call_math(10, 20)
142145
```
143146

144-
### Cross-Library Linking
145-
146-
```python
147-
session = ExecutionSession()
147+
### Isolated Sessions
148148

149-
base_lib = session.create_library("base")
150-
base_lib.add("math_ops.o")
149+
`default_session()` is shared across the process. For an isolated symbol
150+
namespace or a tuned memory arena, construct an `ExecutionSession` directly:
151151

152-
caller_lib = session.create_library("caller")
153-
caller_lib.set_link_order(base_lib) # Can resolve symbols from base_lib
154-
caller_lib.add("caller.o")
152+
```python
153+
from tvm_ffi_orcjit import ExecutionSession
155154

156-
result = caller_lib.get_function("call_math")(10, 20)
155+
session = ExecutionSession() # independent LLVM ExecutionSession
156+
mod = session.load_module("impl.o")
157157
```
158158

159159
## Writing Functions for OrcJIT
@@ -211,13 +211,12 @@ tvm_ffi_orcjit/
211211
├── src/ffi/
212212
│ ├── orcjit_session.cc # ExecutionSession (LLJIT setup, plugins)
213213
│ ├── orcjit_session.h
214-
│ ├── orcjit_dylib.cc # DynamicLibrary (object loading, symbol lookup)
214+
│ ├── orcjit_dylib.cc # JIT dylib module (object loading, symbol lookup)
215215
│ ├── orcjit_dylib.h
216216
│ └── orcjit_utils.h # LLVM error handling utilities
217217
├── python/tvm_ffi_orcjit/
218218
│ ├── __init__.py # Module exports and library loading
219-
│ ├── session.py # Python ExecutionSession wrapper
220-
│ └── dylib.py # Python DynamicLibrary wrapper
219+
│ └── session.py # Python ExecutionSession + default_session
221220
├── tests/ # See tests/README.md
222221
└── examples/quick-start/ # Complete example with CMake
223222
```
@@ -226,7 +225,7 @@ tvm_ffi_orcjit/
226225

227226
Runs on Linux (x86_64, aarch64), macOS (arm64), Windows (AMD64) via
228227
`cibuildwheel`. Each platform builds test objects with multiple compilers
229-
and runs the full test suite. See `.github/workflows/tvm_ffi_orcjit.yml`.
228+
and runs the full test suite. See the `orcjit` job in `.github/workflows/ci_test.yml`.
230229

231230
## Troubleshooting
232231

addons/tvm_ffi_orcjit/examples/quick-start/add.cc

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,3 @@ int fib_impl(int n) {
4040
return fib_impl(n - 1) + fib_impl(n - 2);
4141
}
4242
TVM_FFI_DLL_EXPORT_TYPED_FUNC(fibonacci, fib_impl);
43-
44-
// String concatenation example
45-
std::string concat_impl(std::string a, std::string b) { return a + b; }
46-
TVM_FFI_DLL_EXPORT_TYPED_FUNC(concat, concat_impl);

0 commit comments

Comments
 (0)