This is an automated email from the ASF dual-hosted git repository.
cyx-6 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 e251c6ab [FIX] Load ORC JIT addon through TVM-FFI (#809)
e251c6ab is described below
commit e251c6abee7a111c6467f84d854be512e33e2db0
Author: Yaxing Cai <[email protected]>
AuthorDate: Tue Sep 22 20:31:48 2026 +0800
[FIX] Load ORC JIT addon through TVM-FFI (#809)
## Why
#807 localized the ORCJIT addon and its linked runtimes, but retained a
second raw `ctypes.CDLL(..., RTLD_NODELETE)` handle to keep the addon
mapped through process shutdown. The addon should use the standard
`tvm_ffi.load_module` path exclusively.
Simply deleting the raw handle exposed a SIGSEGV during process exit
when a JIT module was pinned with `keep_module_alive=True`. TVM-FFI's
`ModuleGlobals` describes its entries as modules that should not be
unloaded, but the registry itself was a destructible function-local
static. Static teardown could therefore unload a DSO while the
process-global function table and other dependents still held callbacks
whose code belonged to it.
## What changed
- load and retain the ORCJIT addon only with `tvm_ffi.load_module(...,
keep_module_alive=True)`
- remove the duplicate `ctypes.CDLL`, `RTLD_NODELETE`, and Windows
DLL-directory handling
- stop library discovery after the first valid install/editable
candidate
- make TVM-FFI's keep-alive module registry genuinely process-lifetime,
consistent with its contract and with the existing process-lifetime
global function table
- update `keep_module_alive` documentation to state that pinned modules
live for the duration of the process
- add a child-process regression that pins a JIT module, drops the local
reference, and verifies clean shutdown
## Validation
- before the `ModuleGlobals` lifetime fix, the new shutdown reproducer
exited with SIGSEGV (`returncode == -11`)
- after the fix, the shutdown reproducer exits cleanly
- native `manylinux_2_28` ARM64 container with LLVM 22.1.0
- ORCJIT addon tests: 92 passed, 23 skipped in both serial and
pytest-xdist runs
- TVM-FFI build/load regression tests: 8 passed
- C and C++ ORCJIT quick-start examples
- `nm -D --defined-only`: exactly `TVMFFIOrcJITInitialize`
- all repository pre-commit hooks on changed files
- hosted CI pending
---------
Co-authored-by: tqchen <[email protected]>
---
addons/tvm_ffi_orcjit/CMakeLists.txt | 13 ++
addons/tvm_ffi_orcjit/ORCJIT_PRIMER.md | 48 +++---
addons/tvm_ffi_orcjit/README.md | 15 +-
.../python/tvm_ffi_orcjit/__init__.py | 28 +---
.../python/tvm_ffi_orcjit/session.py | 27 ++--
.../tvm_ffi_orcjit/src/ffi/llvm_patches/README.md | 19 ++-
.../src/ffi/llvm_patches/init_fini_plugin.h | 10 +-
.../src/ffi/llvm_patches/macho_cxa_atexit_shim.cc | 4 +-
.../src/ffi/llvm_patches/macho_cxa_atexit_shim.h | 3 +-
.../ffi/llvm_patches/win_dll_import_generator.cc | 27 ++--
addons/tvm_ffi_orcjit/src/ffi/orcjit_dylib.cc | 165 +++++++++++++++------
addons/tvm_ffi_orcjit/src/ffi/orcjit_dylib.h | 34 ++++-
.../src/ffi/orcjit_memory_manager.cc | 9 +-
.../tvm_ffi_orcjit/src/ffi/orcjit_memory_manager.h | 15 +-
addons/tvm_ffi_orcjit/src/ffi/orcjit_session.cc | 22 ++-
addons/tvm_ffi_orcjit/src/ffi/orcjit_session.h | 22 +--
addons/tvm_ffi_orcjit/src/ffi/orcjit_slab.cc | 48 ++++--
addons/tvm_ffi_orcjit/src/ffi/orcjit_slab.h | 26 ++--
addons/tvm_ffi_orcjit/tests/CMakeLists.txt | 4 +-
addons/tvm_ffi_orcjit/tests/README.md | 4 +-
.../tvm_ffi_orcjit/tests/sources/c/test_context.c | 28 +++-
.../tests/sources/c/test_link_order_base.c | 2 +-
.../tests/sources/c/test_link_order_caller.c | 6 +-
.../tests/sources/cc/test_link_order_base.cc | 2 +-
.../tests/sources/cc/test_link_order_caller.cc | 2 +-
addons/tvm_ffi_orcjit/tests/test_basic.py | 73 ++++++++-
.../tests/test_library_visibility.py | 37 +++++
.../tests/test_session_load_module.py | 8 +-
python/tvm_ffi/cpp/extension.py | 4 +-
python/tvm_ffi/module.py | 2 +-
src/ffi/extra/module.cc | 8 +-
31 files changed, 512 insertions(+), 203 deletions(-)
diff --git a/addons/tvm_ffi_orcjit/CMakeLists.txt
b/addons/tvm_ffi_orcjit/CMakeLists.txt
index af79db90..9ced5a16 100644
--- a/addons/tvm_ffi_orcjit/CMakeLists.txt
+++ b/addons/tvm_ffi_orcjit/CMakeLists.txt
@@ -22,6 +22,9 @@ endif ()
# ---- Find packages ----
find_package(LLVM REQUIRED CONFIG)
+if (LLVM_PACKAGE_VERSION VERSION_LESS 22.0)
+ message(FATAL_ERROR "tvm_ffi_orcjit requires LLVM 22 or newer; found
${LLVM_PACKAGE_VERSION}")
+endif ()
message(STATUS "Found LLVM ${LLVM_PACKAGE_VERSION} in ${LLVM_DIR}")
find_package(
@@ -76,12 +79,22 @@ execute_process(
COMMAND ${LLVM_CONFIG_EXE} --link-static --libs Core OrcJIT Support native
OUTPUT_STRIP_TRAILING_WHITESPACE
OUTPUT_VARIABLE _llvm_libs
+ ERROR_VARIABLE _llvm_libs_error
+ RESULT_VARIABLE _llvm_libs_rc
)
+if (NOT _llvm_libs_rc EQUAL 0)
+ message(FATAL_ERROR "llvm-config --link-static --libs failed:
${_llvm_libs_error}")
+endif ()
execute_process(
COMMAND ${LLVM_CONFIG_EXE} --link-static --ldflags
OUTPUT_STRIP_TRAILING_WHITESPACE
OUTPUT_VARIABLE _llvm_ldflags
+ ERROR_VARIABLE _llvm_ldflags_error
+ RESULT_VARIABLE _llvm_ldflags_rc
)
+if (NOT _llvm_ldflags_rc EQUAL 0)
+ message(FATAL_ERROR "llvm-config --link-static --ldflags failed:
${_llvm_ldflags_error}")
+endif ()
separate_arguments(_llvm_libs_list NATIVE_COMMAND "${_llvm_libs}")
separate_arguments(_llvm_ldflags_list NATIVE_COMMAND "${_llvm_ldflags}")
diff --git a/addons/tvm_ffi_orcjit/ORCJIT_PRIMER.md
b/addons/tvm_ffi_orcjit/ORCJIT_PRIMER.md
index 01472b98..9ab675ee 100644
--- a/addons/tvm_ffi_orcjit/ORCJIT_PRIMER.md
+++ b/addons/tvm_ffi_orcjit/ORCJIT_PRIMER.md
@@ -155,8 +155,9 @@ symbol resolution must be deferred until all relevant
objects are present.
## 3. LLVM ORC JIT v2
-**ORC** stands for *On Request Compilation*. LLVM ORC JIT v2 (introduced in
LLVM 9,
-stabilized in LLVM 13+) is a complete redesign of LLVM's JIT infrastructure.
It is
+**ORC** stands for *On Request Compilation*. The ORCv2 APIs have been
available since
+LLVM 7 and replaced ORCv1 completely in LLVM 12. They redesign LLVM's JIT
infrastructure
+around a composable, concurrent symbol/materialization model. ORCv2 is
designed to be composable, asynchronous, and correct for production use
(unlike the
older `MCJIT` which had several fundamental limitations around multi-module
linking).
@@ -312,7 +313,7 @@ The three platform objects in LLVM are:
| Platform | OS | Init section driven |
| --- | --- | --- |
| `MachOPlatform` | macOS / iOS | `__DATA,__mod_init_func` |
-| `ELFNativePlatform` | Linux / ELF | `.init_array`, TLS |
+| `ELFNixPlatform` | Linux / ELF | `.init_array`, TLS |
| `COFFPlatform` | Windows | `.CRT$XC*` init, `__cxa_atexit` interop |
`ExecutorNativePlatform` is a convenience builder that auto-selects the right
platform
@@ -322,17 +323,18 @@ for the host OS and loads the ORC runtime from a given
path.
The addon takes a different approach on each platform:
-- **macOS**: ORC platform support is *optional*. When the caller passes an ORC
runtime
- path to `ExecutionSession`, `ExecutorNativePlatform` activates
`MachOPlatform`.
- `jit_->initialize(dylib)` and `jit_->deinitialize(dylib)` then drive
`__mod_init_func`
- and `__cxa_atexit` teardown natively. Without the path, the addon falls back
to its
- own `InitFiniPlugin`.
+- **macOS**: the addon deliberately does not configure an ORC platform. It uses
+ `InitFiniPlugin` for `__mod_init_func` / `__mod_term_func` and a per-dylib
+ `__cxa_atexit` shim. This avoids a compact-unwind address-delta failure in
the
+ current `MachOPlatform` integration. The `orc_rt` argument is ignored.
- **Windows**: `COFFPlatform` is skipped entirely because it requires MSVC CRT
symbols
(`_CxxThrowException`, RTTI vtables, iostream objects) that are not
resolvable in
the JIT context. Instead, `InitFiniPlugin` manually handles `.CRT$XC*` /
`.CRT$XT*`
- init/fini sections.
-- **Linux**: `ELFNativePlatform` is not used. `InitFiniPlugin` handles
`.init_array` /
- `.fini_array` / `.ctors` / `.dtors` directly, without the ORC runtime.
+ init/fini sections. The `orc_rt` argument is ignored.
+- **Linux**: the default embedded `liborc_rt` configures
`ExecutorNativePlatform`
+ (and therefore `ELFNixPlatform`). A custom archive may be supplied by path
or bytes,
+ and `None` disables the platform. `InitFiniPlugin` still collects and
invokes ELF
+ init/fini arrays to work around current upstream behavior.
---
@@ -360,21 +362,28 @@ unit: load all its objects at once, then look up
functions on the result.
### 4.2 Loading and lookup
-`load_module` adds each object to the JITDylib, and JITLink parses it into a
-`LinkGraph`, resolves relocations, and allocates executable JIT memory.
-`get_function` looks the symbol up (materializing lazily), then wraps the raw
+`load_module` registers each object with the JITDylib. On the first relevant
lookup,
+JITLink parses it into a `LinkGraph`, resolves relocations, and allocates
executable
+JIT memory. `get_function` looks the symbol up (materializing lazily), then
wraps the raw
pointer as a `tvm_ffi::Function`. Symbols resolve against the dylib's own
-default link order (this dylib → Platform → process/runtime symbols); objects
+link order (this dylib → LLJIT defaults, with a compiler-selected C++ runtime
+inserted before process symbols on Linux); objects
that reference each other must be loaded together, since there is no linking
between separate `load_module` results.
-Two addon-specific pieces sit in this pipeline:
+Several addon-specific pieces sit in this pipeline:
- **`InitFiniPlugin`** — a JITLink pass plugin that keeps init/fini sections
(`.init_array`/`.ctors`/`.fini_array`/`.dtors`, `__mod_init_func`,
`.CRT$XC*`)
live, then collects their function pointers after fixup. The addon runs them
in priority order at first lookup and at teardown, replacing the ORC
platform's initializer machinery. See `llvm_patches/init_fini_plugin.h`.
+- **Linux slab-pool memory manager** — reserves contiguous virtual-address
regions,
+ separates executable from non-executable allocations, recycles freed
regions, and
+ grows by adding slabs. This keeps 32-bit PC-relative relocations in range
and offers
+ explicit reclamation through `clear_free_slabs()`.
+- **GOTPCRELX correction (Linux/x86-64)** — repairs or reverses unsafe JITLink
+ relaxations before fixup.
- **Windows DLL import stubs** — `DLLImportDefinitionGenerator` resolves
`__imp_XXX` references to host-DLL functions by emitting JIT-memory pointer +
trampoline stubs, keeping `PCRel32` fixups within ±2 GB of the JIT code. See
@@ -391,12 +400,12 @@ import tvm_ffi_orcjit as oj
sess = oj.default_session()
# 2. Load a compiled object file into a fresh JITDylib
-# → object parsed, JITLink links it, InitFiniPlugin collects ctors,
-# context symbols injected eagerly, embedded binary (if any) expanded
+# → objects registered; context and embedded-binary probes may materialize
+# objects immediately, and context slots are populated before ctors run
mod = sess.load_module("add.o") # returns a tvm_ffi.Module
# 3. Look up and call a function
-# → LLVM resolves "__tvm_ffi_add"; pending constructors fire on first lookup
+# → LLVM resolves "__tvm_ffi_add"; any pending constructors run before
return
result = mod.add(3, 4) # → 7
```
@@ -426,6 +435,7 @@ TVM_FFI_DLL_EXPORT_TYPED_FUNC(add, add_impl);
| `JITDylib` | Symbol namespace / virtual shared library |
`ORCJITDynamicLibraryObj::dylib_` |
| `JITLink` | LLVM's JIT-aware linker | Used inside `ObjectLinkingLayer` |
| JITLink pass pipeline | Pre-prune → post-alloc → post-fixup hooks | Where
`InitFiniPlugin` runs |
+| Slab pool | Contiguous, growable Linux JIT memory arena |
`SlabPoolMemoryManager` / `Slab` |
| `DefinitionGenerator` | Fallback symbol provider |
`DLLImportDefinitionGenerator` (Win) |
| Link order | Search path across JITDylibs for symbol resolution | LLJIT
default (Main → Platform → ProcessSymbols) |
| `__tvm_ffi_` prefix | Namespace for TVM-FFI exported functions | Used in
`GetFunction()` |
diff --git a/addons/tvm_ffi_orcjit/README.md b/addons/tvm_ffi_orcjit/README.md
index a8be7469..88ea74ff 100644
--- a/addons/tvm_ffi_orcjit/README.md
+++ b/addons/tvm_ffi_orcjit/README.md
@@ -26,8 +26,9 @@ TVM-FFI exported functions.
- **JIT Execution**: Load and execute compiled object files at runtime using
LLVM's ORC JIT v2
- **High-Level Loading**: `default_session().load_module(...)` mirrors
`tvm_ffi.load_module`, returning a plain `tvm_ffi.Module`
- **Unified Input**: Load from a file path, in-memory object bytes, or a list
mixing both
-- **Shared Session**: A process-wide session so multiple callers share one JIT
environment (process symbols, arena, linking)
+- **Shared Session**: A process-wide session so callers share process-symbol
resolution and the Linux slab pool while loaded modules remain isolated
- **Symbol Isolation**: Separate `load_module` calls define independent symbol
namespaces, so they can define the same symbol without conflicts
+- **Bounded-Range Memory**: A growable Linux slab pool keeps JIT code/data
close enough for 32-bit PC-relative relocations and supports explicit
drained-slab reclamation
- **Init/Fini Support**: Handles static constructors/destructors across ELF
(`.init_array`/`.ctors`), Mach-O (`__mod_init_func`), and COFF
(`.CRT$XC*`/`.CRT$XT*`)
- **Cross-Platform**: Linux (x86_64, aarch64), macOS (arm64), Windows (AMD64)
- **Multi-Compiler**: Tested with LLVM Clang, GCC, Apple Clang, MSVC, and
clang-cl
@@ -206,7 +207,8 @@ Compile: `clang -O2 -c -o example.o example.c`
relocations from COFF objects before JITLink graph building, working around a
JITLink limitation with COMDAT section symbols.
-Please refers to [ORCJIT_PRIMER.md](./ORCJIT_PRIMER.md) to learn more about
object file, linking, llvm orcjit v2, and how the addon works.
+Refer to [ORCJIT_PRIMER.md](./ORCJIT_PRIMER.md) for background on object files,
+linking, LLVM ORC JIT v2, and the addon's architecture.
## Project Structure
@@ -219,6 +221,9 @@ tvm_ffi_orcjit/
│ ├── orcjit_session.h
│ ├── orcjit_dylib.cc # JIT dylib module (object loading, symbol
lookup)
│ ├── orcjit_dylib.h
+│ ├── orcjit_memory_manager.* # Growable Linux slab pool
+│ ├── orcjit_slab.* # Contiguous-VA allocator and page protection
+│ ├── llvm_patches/ # Isolated upstream LLVM workarounds
│ └── orcjit_utils.h # LLVM error handling utilities
├── python/tvm_ffi_orcjit/
│ ├── __init__.py # Module exports and library loading
@@ -261,6 +266,12 @@ The package requires LLVM 22+. Set `LLVM_PREFIX` to the
LLVM install prefix:
export LLVM_PREFIX=/path/to/llvm
```
+### Reclaiming unused JIT memory on Linux
+
+Dropping a module returns its regions to the session's slab pool for reuse. To
+return fully drained slabs to the operating system, call
+`session.clear_free_slabs()`. The call is synchronized with concurrent JIT
work.
+
## License
Apache License 2.0
diff --git a/addons/tvm_ffi_orcjit/python/tvm_ffi_orcjit/__init__.py
b/addons/tvm_ffi_orcjit/python/tvm_ffi_orcjit/__init__.py
index e1650303..bc3cdee7 100644
--- a/addons/tvm_ffi_orcjit/python/tvm_ffi_orcjit/__init__.py
+++ b/addons/tvm_ffi_orcjit/python/tvm_ffi_orcjit/__init__.py
@@ -29,8 +29,6 @@ Examples
"""
-import ctypes
-import os
import platform
from pathlib import Path
@@ -53,29 +51,19 @@ _LIB_PATH = [
]
_lib_path = None
for path in _LIB_PATH:
- if path.exists():
- _ = load_module(str(path))
+ if path.is_file():
_lib_path = path
+ break
if _lib_path is None:
raise RuntimeError(
- f"Could not find {_LIB_NAME}. "
- f"Searched in {_LIB_PATH} and site-packages. "
- f"Please ensure the package is installed correctly."
+ f"Could not find {_LIB_NAME}. Searched {_LIB_PATH}. "
+ "Please ensure the package is installed correctly."
)
-# Keep a second, process-lifetime local handle. RTLD_NODELETE is important for
-# modules pinned by keep_module_alive: their object deleters point into JIT
code
-# owned by this DSO and may run during interpreter shutdown, after Python
module
-# globals have otherwise released their handles. This does not promote the DSO
-# or its statically linked LLVM into the process-global symbol namespace.
-if os.name == "posix":
- _c_lib = ctypes.CDLL(
- str(_lib_path),
- mode=ctypes.RTLD_LOCAL | getattr(os, "RTLD_NODELETE", 0),
- )
-else:
- _dll_directory = os.add_dll_directory(str(_lib_path.parent))
- _c_lib = ctypes.CDLL(str(_lib_path))
+# The TVM-FFI loader uses local symbol scope. Retain the returned module here
+# and in TVM-FFI's process-lifetime module registry so registered functions and
+# JIT-owned object deleters remain valid through interpreter shutdown.
+_lib_module = load_module(_lib_path, keep_module_alive=True)
from .session import ExecutionSession, default_session
diff --git a/addons/tvm_ffi_orcjit/python/tvm_ffi_orcjit/session.py
b/addons/tvm_ffi_orcjit/python/tvm_ffi_orcjit/session.py
index e00e9186..bc1c1c3a 100644
--- a/addons/tvm_ffi_orcjit/python/tvm_ffi_orcjit/session.py
+++ b/addons/tvm_ffi_orcjit/python/tvm_ffi_orcjit/session.py
@@ -119,10 +119,11 @@ class ExecutionSession(Object):
slab_size : int
Per-slab capacity in bytes for the JIT memory manager. Linux only —
ignored on macOS and Windows, where the slab allocator is compiled
- out. 0 = arch default (64 MB; initial slab halves on mmap failure
- down to 8 MB under RLIMIT_AS / container limits), >0 = custom size,
- <0 = disable slab allocator (LLJIT uses its default scattered-mmap
- allocator).
+ out. 0 = 64 MB default (the initial slab halves on mmap failure
+ down to 8 MB under RLIMIT_AS / container limits), >=4 MB = custom
+ size, <0 = disable the slab allocator (LLJIT uses its default
+ scattered-mmap allocator). Positive values below 4 MB are rejected
+ because both allocation pools need a 2 MB commit chunk.
The session holds a growable pool of slabs: a fresh slab is mmap'd
on demand when no existing one can fit a graph. Graphs that don't
@@ -190,8 +191,8 @@ class ExecutionSession(Object):
``keep_module_alive`` mirrors :func:`tvm_ffi.load_module`'s option of
the same name. When True, the module is inserted into the runtime's
process-global module registry, so its JITDylib — and every function
- pointer, deleter, and static allocation it owns — stays mapped until
- the interpreter unloads ``libtvm_ffi``. Use this when Objects produced
+ pointer, deleter, and static allocation it owns — stays mapped for the
+ duration of the process. Use this when Objects produced
by the module may outlive the local ``mod`` reference (e.g., a
JIT-allocated ``String`` or ``Array`` returned to Python and held past
``del mod``). When False (default), the caller owns the module's
@@ -243,10 +244,9 @@ class ExecutionSession(Object):
Fresh slabs that have never been allocated on are preserved, so
the session remains ready to accept new work.
- Safety: call when no JIT work is in flight on another thread. From
- single-threaded Python this is always safe; once ``del lib`` has
- returned, the C++ destructor has finished and the slab's live count
- reflects the drop.
+ The operation is serialized with JIT allocation and module teardown,
+ so it is safe to call while other host threads use the same session.
+ Only fully drained slabs are reclaimed.
Returns
-------
@@ -267,9 +267,10 @@ def default_session() -> ExecutionSession:
"""Return the process-wide shared execution session.
A single leaked, never-destroyed session shared by all callers in the
- process, so they share one LLVM ``ExecutionSession`` — hence process
- symbols, the slab arena, and cross-library linking. Created on first call
- and cached for the lifetime of the process.
+ process, so they share one LLVM ``ExecutionSession`` — hence process-symbol
+ resolution, the slab pool, and synchronization infrastructure. Separate
+ loaded modules remain isolated symbol namespaces. Created on first call and
+ cached for the lifetime of the process.
The session uses the ORC runtime embedded in the extension (no on-disk path
lookup). For an isolated session or a tuned arena, construct an
diff --git a/addons/tvm_ffi_orcjit/src/ffi/llvm_patches/README.md
b/addons/tvm_ffi_orcjit/src/ffi/llvm_patches/README.md
index c2706a71..2b43c8a4 100644
--- a/addons/tvm_ffi_orcjit/src/ffi/llvm_patches/README.md
+++ b/addons/tvm_ffi_orcjit/src/ffi/llvm_patches/README.md
@@ -45,8 +45,9 @@ Each patch file opens with a fixed-shape header describing:
- **ELF init/fini** (Linux branch of `init_fini_plugin.{h,cc}`)
LLVM issue:
[llvm/llvm-project#175981](https://github.com/llvm/llvm-project/issues/175981).
- Upstream status: open, patch submitted.
- Remove when: LLVM floor bumps past the release that contains the fix.
+ Upstream status: merged; included in LLVM 23.1.1, not LLVM 22.1.0.
+ Remove when: the LLVM floor reaches 23 and the addon switches its lifecycle
+ calls to the upstream `ELFNixPlatform` path.
- **COFF ctor/dtor** (Windows branch of `init_fini_plugin.{h,cc}`)
LLVM issue: COFFPlatform stalled.
@@ -54,8 +55,18 @@ Each patch file opens with a fixed-shape header describing:
Remove when: COFFPlatform becomes usable end-to-end with clang-cl /
MSVC objects.
-macOS already has working `MachOPlatform`, so no patch file is needed
-for that platform.
+- **Mach-O `__cxa_atexit` scoping** (`macho_cxa_atexit_shim.{h,cc}`)
+ Upstream dependency: re-enabling `MachOPlatform` after the compact-unwind
+ per-graph DSO-base fix is available.
+ Remove when: the addon can use `MachOPlatform` end-to-end.
+
+- **COFF unwind-data stripping** (`win_coff_pdata_strip.{h,cc}`)
+ Upstream dependency: usable `COFFPlatform` support with SEH registration.
+ Remove when: `.pdata` / `.xdata` can be registered and relocated normally.
+
+- **Windows DLL import stubs** (`win_dll_import_generator.{h,cc}`)
+ Upstream dependency: usable `COFFPlatform` and in-range DLL call stubs.
+ Remove when: host DLL imports work end-to-end without the custom generator.
## Removal checklist
diff --git a/addons/tvm_ffi_orcjit/src/ffi/llvm_patches/init_fini_plugin.h
b/addons/tvm_ffi_orcjit/src/ffi/llvm_patches/init_fini_plugin.h
index 21262e79..ee01a628 100644
--- a/addons/tvm_ffi_orcjit/src/ffi/llvm_patches/init_fini_plugin.h
+++ b/addons/tvm_ffi_orcjit/src/ffi/llvm_patches/init_fini_plugin.h
@@ -52,11 +52,11 @@
*
* ## Removal — Linux
*
- * LLVM issue: https://github.com/llvm/llvm-project/issues/175981
- * When the upstream fix lands and the project's minimum LLVM version
- * bumps past the first release containing it, replace this plugin's
- * Linux usage with `ELFNixPlatform` and delete the ELF handling path
- * from this file. Concretely:
+ * LLVM issue: https://github.com/llvm/llvm-project/pull/175981
+ * The upstream fix is included in LLVM 23.1.1 but not LLVM 22.1.0, the
+ * addon's current CI baseline. When the project's minimum LLVM version
+ * reaches 23 and its lifecycle calls use `ELFNixPlatform`, delete the ELF
+ * handling path from this file. Concretely:
* - Remove the ELF-section branches (`.init_array`, `.ctors`,
* `.fini_array`, `.dtors`) from `InitFiniPlugin::modifyPassConfig`.
* - If no platform still needs this plugin, delete this file outright
diff --git
a/addons/tvm_ffi_orcjit/src/ffi/llvm_patches/macho_cxa_atexit_shim.cc
b/addons/tvm_ffi_orcjit/src/ffi/llvm_patches/macho_cxa_atexit_shim.cc
index 0e1597ac..f2cd4824 100644
--- a/addons/tvm_ffi_orcjit/src/ffi/llvm_patches/macho_cxa_atexit_shim.cc
+++ b/addons/tvm_ffi_orcjit/src/ffi/llvm_patches/macho_cxa_atexit_shim.cc
@@ -67,12 +67,12 @@
CxaAtexitRecordsScope::CxaAtexitRecordsScope(CxaAtexitRecords* records)
}
CxaAtexitRecordsScope::~CxaAtexitRecordsScope() { g_active_cxa_records =
prev_; }
-void InstallCxaAtexitShim(llvm::orc::ExecutionSession& ES,
llvm::orc::JITDylib& jd) {
+llvm::Error InstallCxaAtexitShim(llvm::orc::ExecutionSession& ES,
llvm::orc::JITDylib& jd) {
llvm::orc::SymbolMap shim_syms;
shim_syms[ES.intern("___cxa_atexit")] = {
llvm::orc::ExecutorAddr::fromPtr(reinterpret_cast<void*>(&tvm_ffi_cxa_atexit_shim)),
llvm::JITSymbolFlags::Exported | llvm::JITSymbolFlags::Callable};
- llvm::cantFail(jd.define(llvm::orc::absoluteSymbols(std::move(shim_syms))));
+ return jd.define(llvm::orc::absoluteSymbols(std::move(shim_syms)));
}
void DrainCxaAtexit(CxaAtexitRecords& records) {
diff --git a/addons/tvm_ffi_orcjit/src/ffi/llvm_patches/macho_cxa_atexit_shim.h
b/addons/tvm_ffi_orcjit/src/ffi/llvm_patches/macho_cxa_atexit_shim.h
index 8aee61f0..f215e209 100644
--- a/addons/tvm_ffi_orcjit/src/ffi/llvm_patches/macho_cxa_atexit_shim.h
+++ b/addons/tvm_ffi_orcjit/src/ffi/llvm_patches/macho_cxa_atexit_shim.h
@@ -61,6 +61,7 @@
#ifdef __APPLE__
#include <llvm/ExecutionEngine/Orc/Core.h>
+#include <llvm/Support/Error.h>
#include <utility>
#include <vector>
@@ -100,7 +101,7 @@ class CxaAtexitRecordsScope {
* libSystem fallback — JITDylib::define-time symbols are searched before
* the link order.
*/
-void InstallCxaAtexitShim(llvm::orc::ExecutionSession& ES,
llvm::orc::JITDylib& jd);
+llvm::Error InstallCxaAtexitShim(llvm::orc::ExecutionSession& ES,
llvm::orc::JITDylib& jd);
/*! \brief Drain captured `(fn, arg)` records LIFO, running each dtor.
*
diff --git
a/addons/tvm_ffi_orcjit/src/ffi/llvm_patches/win_dll_import_generator.cc
b/addons/tvm_ffi_orcjit/src/ffi/llvm_patches/win_dll_import_generator.cc
index 59f86004..901f627f 100644
--- a/addons/tvm_ffi_orcjit/src/ffi/llvm_patches/win_dll_import_generator.cc
+++ b/addons/tvm_ffi_orcjit/src/ffi/llvm_patches/win_dll_import_generator.cc
@@ -52,6 +52,7 @@
#include <psapi.h>
// clang-format on
+#include <array>
#include <string>
namespace tvm {
@@ -61,22 +62,26 @@ namespace orcjit {
void* DLLImportDefinitionGenerator::FindInProcessModules(const std::string&
Name) {
// Try specific runtime DLLs first, then tvm_ffi.dll (loaded by Python),
// then all process modules, then LLVM's search.
- static const char* kRuntimeDLLs[] = {
+ static constexpr std::array<const char*, 4> kRuntimeDLLs = {
"vcruntime140.dll",
"vcruntime140_1.dll",
"ucrtbase.dll",
"msvcp140.dll",
};
- // NOTE: We intentionally do not call FreeLibrary() here. These runtime DLLs
- // (vcruntime140, ucrtbase, etc.) are already loaded by the process and will
- // remain loaded for its lifetime. LoadLibraryA merely increments the
refcount;
- // the extra refcount is harmless and avoids the overhead of balancing
- // Get/FreeLibrary for every symbol lookup.
- for (const char* dll : kRuntimeDLLs) {
- if (HMODULE hMod = LoadLibraryA(dll)) {
- if (auto addr = GetProcAddress(hMod, Name.c_str())) {
- return reinterpret_cast<void*>(addr);
- }
+ // Resolve the runtime handles once. GetModuleHandle avoids a refcount change
+ // for DLLs already present; LoadLibrary supplies a process-lifetime handle
for
+ // an optional runtime that was not yet loaded.
+ static const std::array<HMODULE, kRuntimeDLLs.size()> kRuntimeHandles = []()
{
+ std::array<HMODULE, kRuntimeDLLs.size()> handles{};
+ for (std::size_t i = 0; i < kRuntimeDLLs.size(); ++i) {
+ handles[i] = GetModuleHandleA(kRuntimeDLLs[i]);
+ if (handles[i] == nullptr) handles[i] = LoadLibraryA(kRuntimeDLLs[i]);
+ }
+ return handles;
+ }();
+ for (HMODULE module : kRuntimeHandles) {
+ if (module != nullptr) {
+ if (auto addr = GetProcAddress(module, Name.c_str())) return
reinterpret_cast<void*>(addr);
}
}
// Also check tvm_ffi.dll (host process symbol provider)
diff --git a/addons/tvm_ffi_orcjit/src/ffi/orcjit_dylib.cc
b/addons/tvm_ffi_orcjit/src/ffi/orcjit_dylib.cc
index a9f1dc1e..b0925214 100644
--- a/addons/tvm_ffi_orcjit/src/ffi/orcjit_dylib.cc
+++ b/addons/tvm_ffi_orcjit/src/ffi/orcjit_dylib.cc
@@ -281,7 +281,76 @@ Module ORCJITDynamicLibraryObj::Finalize() {
return GetRef<Module>(this);
}
-void* ORCJITDynamicLibraryObj::GetSymbol(const String& name) {
+void ORCJITDynamicLibraryObj::WaitForInitializers() {
+ std::unique_lock<std::mutex> lock(initializer_mutex_);
+ const std::thread::id current = std::this_thread::get_id();
+ initializer_cv_.wait(lock, [this, current]() {
+ return initializer_depth_ == 0 || initializer_thread_ == current;
+ });
+}
+
+bool ORCJITDynamicLibraryObj::InitializerTurnAvailable() {
+ std::lock_guard<std::mutex> lock(initializer_mutex_);
+ return initializer_depth_ == 0 || initializer_thread_ ==
std::this_thread::get_id();
+}
+
+void ORCJITDynamicLibraryObj::BeginInitializerRun() {
+ std::lock_guard<std::mutex> lock(initializer_mutex_);
+ const std::thread::id current = std::this_thread::get_id();
+ TVM_FFI_CHECK(initializer_depth_ == 0 || initializer_thread_ == current,
InternalError)
+ << "Initializer ownership changed unexpectedly";
+ if (initializer_depth_ == 0) initializer_thread_ = current;
+ ++initializer_depth_;
+}
+
+void ORCJITDynamicLibraryObj::EndInitializerRun() {
+ bool notify = false;
+ {
+ std::lock_guard<std::mutex> lock(initializer_mutex_);
+ TVM_FFI_CHECK(initializer_depth_ != 0 && initializer_thread_ ==
std::this_thread::get_id(),
+ InternalError)
+ << "Initializer completion does not match its owner";
+ if (--initializer_depth_ == 0) {
+ initializer_thread_ = std::thread::id();
+ notify = true;
+ }
+ }
+ if (notify) initializer_cv_.notify_all();
+}
+
+void ORCJITDynamicLibraryObj::RunInitializers(
+ const std::vector<ORCJITExecutionSessionObj::InitFiniEntry>& entries) {
+ if (entries.empty()) return;
+ try {
+#ifdef __APPLE__
+ // Route any __cxa_atexit registrations made during init to this dylib's
+ // records; see llvm_patches/macho_cxa_atexit_shim.h.
+ CxaAtexitRecordsScope scope(&cxa_atexit_records_);
+#endif
+ ORCJITExecutionSessionObj::RunInitFiniEntries(entries);
+ } catch (...) {
+ EndInitializerRun();
+ throw;
+ }
+ EndInitializerRun();
+}
+
+void ORCJITDynamicLibraryObj::RunPendingInitializers() {
+ std::vector<ORCJITExecutionSessionObj::InitFiniEntry> init;
+ while (true) {
+ WaitForInitializers();
+ std::unique_lock<std::mutex> session_lock(session_->mutex_);
+ // Another thread may have started initialization after the wait but before
+ // this session lock was acquired. Re-check under the session→gate lock
order.
+ if (!InitializerTurnAvailable()) continue;
+ init = session_->DrainPendingInitializers(GetJITDylib());
+ if (!init.empty()) BeginInitializerRun();
+ break;
+ }
+ RunInitializers(init);
+}
+
+void* ORCJITDynamicLibraryObj::GetSymbol(const String& name, bool
run_initializers) {
// Search this dylib only. Its JITDylib link order (set at creation) already
// chains to Main → Platform → ProcessSymbols for host/runtime symbols, so a
// single-entry search order resolves everything a self-contained module
needs.
@@ -293,42 +362,50 @@ void* ORCJITDynamicLibraryObj::GetSymbol(const String&
name) {
// Drain under the lock; run constructors after release (see below).
llvm::Expected<llvm::orc::ExecutorSymbolDef> symbol_or_err =
llvm::orc::ExecutorSymbolDef();
std::vector<ORCJITExecutionSessionObj::InitFiniEntry> init;
- {
- std::lock_guard<std::mutex> lock(session_->mutex_);
+ while (true) {
+ WaitForInitializers();
+ std::unique_lock<std::mutex> session_lock(session_->mutex_);
+ if (!InitializerTurnAvailable()) continue;
symbol_or_err =
jit_->getExecutionSession().lookup(search_order,
jit_->mangleAndIntern(name.c_str()));
- init = session_->DrainPendingInitializers(GetJITDylib());
+ if (symbol_or_err && run_initializers) {
+ init = session_->DrainPendingInitializers(GetJITDylib());
+ if (!init.empty()) BeginInitializerRun();
+ } else if (!symbol_or_err && run_initializers) {
+ // Never execute entries collected by a failed materialization: their
+ // target memory may already have been abandoned.
+ session_->DrainPendingInitializers(GetJITDylib());
+ }
+ break;
}
- // Run this dylib's constructors (drained above) with the lock released.
-#ifdef __APPLE__
- // Route any __cxa_atexit registrations made during init to this dylib's
- // records; see llvm_patches/macho_cxa_atexit_shim.h.
- CxaAtexitRecordsScope scope(&cxa_atexit_records_);
-#endif
- ORCJITExecutionSessionObj::RunInitFiniEntries(init);
-
if (!symbol_or_err) {
llvm::Error remaining =
llvm::handleErrors(symbol_or_err.takeError(), [](const
llvm::orc::SymbolsNotFound&) {});
if (remaining) TVM_FFI_ORCJIT_LLVM_CALL(std::move(remaining));
return nullptr;
}
+ // Run this dylib's constructors with the session lock released. The
+ // per-dylib initializer gate blocks other threads until completion while
+ // allowing same-thread constructor callbacks to re-enter the dylib.
+ RunInitializers(init);
return symbol_or_err->getAddress().toPtr<void*>();
}
void ORCJITDynamicLibraryObj::InitContextSymbols() {
// Called once from Finalize before the dylib is published, so no guard is
- // needed. Point the library-context slot at this module and inject any
- // registered context symbols.
- if (void** ctx_addr =
reinterpret_cast<void**>(GetSymbol(symbol::tvm_ffi_library_ctx))) {
+ // needed. Resolve every context slot without running initializers, populate
+ // the slots, then run the collected initializers. Constructors can therefore
+ // safely use the library context on their first instruction.
+ if (void** ctx_addr =
reinterpret_cast<void**>(GetSymbol(symbol::tvm_ffi_library_ctx, false))) {
*ctx_addr = this;
}
Module::VisitContextSymbols([this](const String& name, void* symbol) {
- if (void** ctx_addr = reinterpret_cast<void**>(GetSymbol(name))) {
+ if (void** ctx_addr = reinterpret_cast<void**>(GetSymbol(name, false))) {
*ctx_addr = symbol;
}
});
+ RunPendingInitializers();
}
llvm::orc::JITDylib& ORCJITDynamicLibraryObj::GetJITDylib() {
@@ -337,9 +414,9 @@ llvm::orc::JITDylib& ORCJITDynamicLibraryObj::GetJITDylib()
{
}
Optional<Function> ORCJITDynamicLibraryObj::GetFunction(const String& name) {
- // Pure symbol lookup. Context symbols were injected once at load time (see
- // Finalize), so this holds no lock and does no refresh — the returned
- // Function, once resolved, is invoked lock-free on the hot path.
+ // Context symbols were injected once at load time (see Finalize). Resolution
+ // is serialized by GetSymbol, but the returned Function is invoked lock-free
+ // on the hot path.
//
// TVM-FFI exports have the __tvm_ffi_ prefix.
std::string symbol_name = symbol::tvm_ffi_symbol_prefix + std::string(name);
@@ -356,31 +433,31 @@ Optional<Function>
ORCJITDynamicLibraryObj::GetFunction(const String& name) {
//-------------------------------------
static void RegisterOrcJITFunctions() {
- static bool registered = false;
- if (registered) return;
- registered = true;
-
- namespace refl = tvm::ffi::reflection;
-
- refl::ObjectDef<ORCJITExecutionSessionObj>();
-
- refl::GlobalDef()
- .def("tvm_ffi_orcjit.ExecutionSession",
- [](const Optional<Variant<String, Bytes>>& orc_rt, int64_t
slab_size_bytes) {
- return ORCJITExecutionSession(orc_rt, slab_size_bytes);
- })
- .def("tvm_ffi_orcjit.GlobalDefaultSession",
- []() { return ORCJITExecutionSessionObj::GlobalDefault(); })
- .def("tvm_ffi_orcjit.SessionLoadModule",
- [](const ORCJITExecutionSession& session, const
Array<Variant<String, Bytes>>& objects,
- const String& name, const Optional<String>& cxx_runtime_path,
- const Optional<String>& libstdcxx_nonshared_path) -> Module {
- return session->LoadModule(objects, name, cxx_runtime_path,
libstdcxx_nonshared_path);
- })
- .def("tvm_ffi_orcjit.SessionClearFreeSlabs",
- [](const ORCJITExecutionSession& session) -> int64_t {
- return session->ClearFreeSlabs();
- });
+ static std::once_flag once;
+ std::call_once(once, []() {
+ namespace refl = tvm::ffi::reflection;
+
+ refl::ObjectDef<ORCJITExecutionSessionObj>();
+
+ refl::GlobalDef()
+ .def("tvm_ffi_orcjit.ExecutionSession",
+ [](const Optional<Variant<String, Bytes>>& orc_rt, int64_t
slab_size_bytes) {
+ return ORCJITExecutionSession(orc_rt, slab_size_bytes);
+ })
+ .def("tvm_ffi_orcjit.GlobalDefaultSession",
+ []() { return ORCJITExecutionSessionObj::GlobalDefault(); })
+ .def("tvm_ffi_orcjit.SessionLoadModule",
+ [](const ORCJITExecutionSession& session, const
Array<Variant<String, Bytes>>& objects,
+ const String& name, const Optional<String>& cxx_runtime_path,
+ const Optional<String>& libstdcxx_nonshared_path) -> Module {
+ return session->LoadModule(objects, name, cxx_runtime_path,
+ libstdcxx_nonshared_path);
+ })
+ .def("tvm_ffi_orcjit.SessionClearFreeSlabs",
+ [](const ORCJITExecutionSession& session) -> int64_t {
+ return session->ClearFreeSlabs();
+ });
+ });
}
TVM_FFI_STATIC_INIT_BLOCK() { RegisterOrcJITFunctions(); }
diff --git a/addons/tvm_ffi_orcjit/src/ffi/orcjit_dylib.h
b/addons/tvm_ffi_orcjit/src/ffi/orcjit_dylib.h
index b458b127..370dffdd 100644
--- a/addons/tvm_ffi_orcjit/src/ffi/orcjit_dylib.h
+++ b/addons/tvm_ffi_orcjit/src/ffi/orcjit_dylib.h
@@ -31,7 +31,12 @@
#include <tvm/ffi/object.h>
#include <tvm/ffi/string.h>
+#include <condition_variable>
+#include <cstddef>
#include <memory>
+#include <mutex>
+#include <thread>
+#include <vector>
#include "llvm_patches/macho_cxa_atexit_shim.h"
#include "orcjit_session.h"
@@ -105,7 +110,25 @@ class ORCJITDynamicLibraryObj : public ModuleObj {
* \param name The symbol name to look up
* \return Pointer to the symbol, or nullptr if not found
*/
- void* GetSymbol(const String& name);
+ void* GetSymbol(const String& name, bool run_initializers = true);
+
+ /*! \brief Wait until another thread has finished running this dylib's
initializers. */
+ void WaitForInitializers();
+
+ /*! \brief Check the initializer gate while holding the session mutex. */
+ bool InitializerTurnAvailable();
+
+ /*! \brief Mark a (possibly nested) initializer run owned by the current
thread. */
+ void BeginInitializerRun();
+
+ /*! \brief Finish an initializer run and wake blocked lookup threads when
outermost. */
+ void EndInitializerRun();
+
+ /*! \brief Run entries outside the session mutex while preserving lookup
ordering. */
+ void RunInitializers(const
std::vector<ORCJITExecutionSessionObj::InitFiniEntry>& entries);
+
+ /*! \brief Drain and run every initializer collected by raw context lookups.
*/
+ void RunPendingInitializers();
/*!
* \brief Get the underlying LLVM JITDylib
@@ -128,6 +151,15 @@ class ORCJITDynamicLibraryObj : public ModuleObj {
/*! \brief Whether Finalize has run; guards against double-finalizing. */
bool finalized_{false};
+ // Constructors run without the session mutex to permit re-entry. These
+ // fields prevent a second thread from observing callable code before the
+ // first thread completes initialization; same-thread nested lookup remains
+ // allowed for constructor callbacks.
+ std::mutex initializer_mutex_;
+ std::condition_variable initializer_cv_;
+ std::thread::id initializer_thread_;
+ std::size_t initializer_depth_{0};
+
#ifdef __APPLE__
/*! \brief Per-dylib __cxa_atexit registry.
*
diff --git a/addons/tvm_ffi_orcjit/src/ffi/orcjit_memory_manager.cc
b/addons/tvm_ffi_orcjit/src/ffi/orcjit_memory_manager.cc
index a970c976..279d5478 100644
--- a/addons/tvm_ffi_orcjit/src/ffi/orcjit_memory_manager.cc
+++ b/addons/tvm_ffi_orcjit/src/ffi/orcjit_memory_manager.cc
@@ -27,6 +27,7 @@
#include <llvm/Support/Error.h>
#include <llvm/Support/FormatVariadic.h>
+#include <tvm/ffi/error.h>
#include <algorithm>
#include <optional>
@@ -60,8 +61,8 @@ SlabPoolMemoryManager::SlabPoolMemoryManager(std::size_t
page_size, std::size_t
}
cap /= 2;
}
- llvm::report_fatal_error("SlabPoolMemoryManager: failed to reserve at least
" +
- llvm::Twine(floor / (1024 * 1024)) + " MB of
virtual address space");
+ TVM_FFI_THROW(RuntimeError) << "SlabPoolMemoryManager: failed to reserve at
least "
+ << floor / (1024 * 1024) << " MB of virtual
address space";
}
std::unique_ptr<Slab> SlabPoolMemoryManager::createSlab(std::size_t capacity) {
@@ -79,8 +80,8 @@ void SlabPoolMemoryManager::allocate(const
llvm::jitlink::JITLinkDylib* /*JD*/,
// user callback, since the LLJIT linker issues nested lookups (and
// thus re-entrant allocate() calls via materialization) from inside
// OnAllocated and a coarse lock would deadlock. Snapshot raw pointers
- // under the lock; slabs are guaranteed to outlive this call because
- // clearFreeSlabs() is only safe when the session is quiescent.
+ // under the lock. Slabs outlive this call because the enclosing execution
+ // session serializes allocation, teardown, and clearFreeSlabs().
//
// Slab::allocate is synchronous (invokes its callback inline on every
// code path), so a captured std::optional observes the result before
diff --git a/addons/tvm_ffi_orcjit/src/ffi/orcjit_memory_manager.h
b/addons/tvm_ffi_orcjit/src/ffi/orcjit_memory_manager.h
index a71487dc..6ec89707 100644
--- a/addons/tvm_ffi_orcjit/src/ffi/orcjit_memory_manager.h
+++ b/addons/tvm_ffi_orcjit/src/ffi/orcjit_memory_manager.h
@@ -23,7 +23,7 @@
*
* `SlabPoolMemoryManager` implements `JITLinkMemoryManager` on top of a
* per-session `std::vector<std::unique_ptr<Slab>>`. On each `allocate`
- * it picks the first `Slab` that can fit the graph; if none do, it
+ * it picks the first `Slab` that can fit the graph; if none does, it
* `mmap`s a fresh slab sized to fit (`Slab::capacityForFootprint`) and
* appends it. Normal-size graphs land on a `slab_size`-sized slab;
* skewed or oversize graphs land on a power-of-2 larger slab whose
@@ -63,8 +63,8 @@ namespace orcjit {
/*!
* \brief `JITLinkMemoryManager` backed by a growable pool of `Slab`s.
*
- * The constructor reserves one initial slab (halving its capacity down
- * to `kMinSlabSize` if `mmap` fails under RLIMIT_AS). Subsequent
+ * The constructor reserves one initial slab, halving on `mmap` failure down to
+ * `min(slab_size, kMinSlabSize)`. Subsequent
* slabs are added on demand by `allocate()` at a capacity chosen by
* `Slab::capacityForFootprint` — `slab_size_` for normal graphs, the
* next power of two up for skewed / oversize graphs. No retry, no
@@ -78,8 +78,9 @@ class SlabPoolMemoryManager : public
llvm::jitlink::JITLinkMemoryManager {
// granule. Small enough that a pinned slab only wastes 64 MB of RSS.
static constexpr std::size_t kDefaultSlabSize = std::size_t{64} << 20; //
64 MB
- // Lower bound on initial-slab reservation. If the first `mmap`
- // fails and halving drops below this, the constructor aborts.
+ // Lower bound for initial-slab fallback when the requested capacity is at
+ // least this large. Smaller valid custom capacities use their own size. If
+ // every reservation fails, construction raises a RuntimeError.
// 8 MB is enough for a minimal JITDylib setup under very tight
// RLIMIT_AS.
static constexpr std::size_t kMinSlabSize = std::size_t{8} << 20; // 8 MB
@@ -108,8 +109,8 @@ class SlabPoolMemoryManager : public
llvm::jitlink::JITLinkMemoryManager {
* prior allocation — back to the OS via `munmap`.
*
* Returns the number of slabs reclaimed. Safe to call any time the
- * session is quiescent (no concurrent JIT work in flight). A typical
- * pattern is to call this after dropping a batch of libraries:
+ * The enclosing execution session serializes this operation with JIT work.
+ * A typical pattern is to call it after dropping a batch of libraries:
*
* for lib in libs: del lib
* session.clear_free_slabs() # Python API
diff --git a/addons/tvm_ffi_orcjit/src/ffi/orcjit_session.cc
b/addons/tvm_ffi_orcjit/src/ffi/orcjit_session.cc
index cbe17859..bb56eee4 100644
--- a/addons/tvm_ffi_orcjit/src/ffi/orcjit_session.cc
+++ b/addons/tvm_ffi_orcjit/src/ffi/orcjit_session.cc
@@ -24,6 +24,7 @@
#include "orcjit_session.h"
+#include <llvm/ADT/ScopeExit.h>
#include <llvm/ExecutionEngine/Orc/EPCDynamicLibrarySearchGenerator.h>
#include <llvm/ExecutionEngine/Orc/ExecutionUtils.h>
#include <llvm/ExecutionEngine/Orc/LLJIT.h>
@@ -40,6 +41,7 @@
#include <cstddef>
#include <cstdint>
#include <mutex>
+#include <sstream>
#if defined(__linux__) && defined(__GLIBCXX__)
#include <bits/functexcept.h>
@@ -198,7 +200,7 @@ ORCJITExecutionSessionObj::ORCJITExecutionSessionObj(const
Optional<Variant<Stri
// x86_64, ±4 GB AArch64). Eliminates scattered-mmap relocation overflow
// (LLVM #173269).
//
- // slab_size_bytes: 0 = arch default (1 GB x86_64 / AArch64, with fallback),
+ // slab_size_bytes: 0 = default (64 MB, with fallback),
// >0 = custom size, <0 = disable arena (LLJIT uses its
// default allocator — scattered mmap, no PC-rel guarantee).
// The parameter is Linux-only; on macOS/Windows the arena is compiled out
@@ -226,6 +228,10 @@ ORCJITExecutionSessionObj::ORCJITExecutionSessionObj(const
Optional<Variant<Stri
auto page_size = llvm::sys::Process::getPageSizeEstimate();
size_t slab_size;
if (slab_size_bytes > 0) {
+ constexpr std::size_t kMinCustomSlabSize = 2 * Slab::kCommitGranularity;
+ TVM_FFI_CHECK(static_cast<uint64_t>(slab_size_bytes) >=
kMinCustomSlabSize, ValueError)
+ << "slab_size must be 0, negative (disabled), or at least " <<
kMinCustomSlabSize
+ << " bytes, but got " << slab_size_bytes;
slab_size = static_cast<size_t>(slab_size_bytes);
} else {
slab_size = SlabPoolMemoryManager::kDefaultSlabSize;
@@ -402,6 +408,9 @@ ORCJITDynamicLibrary
ORCJITExecutionSessionObj::CreateDynamicLibrary(
llvm::orc::JITDylib& jit_dylib =
TVM_FFI_ORCJIT_LLVM_CALL(jit_->getExecutionSession().createJITDylib(lib_name.c_str()));
+ // If any subsequent link-order/generator setup fails, remove the partially
+ // configured dylib while the session mutex is still held.
+ llvm::scope_exit cleanup_dylib([this, &jit_dylib]() {
RemoveDylib(&jit_dylib); });
#if defined(__linux__) && defined(__GLIBCXX__)
llvm::orc::JITDylib* cxx_runtime_dylib = nullptr;
if (cxx_runtime) {
@@ -452,16 +461,16 @@ ORCJITDynamicLibrary
ORCJITExecutionSessionObj::CreateDynamicLibrary(
}
#endif
- auto dylib_obj =
make_object<ORCJITDynamicLibraryObj>(GetRef<ORCJITExecutionSession>(this),
- &jit_dylib,
jit_.get(), lib_name);
-
#ifdef __APPLE__
// Inject ___cxa_atexit on the user JITDylib so it wins over <Platform>'s
// fallback (which resolves to libSystem's and would orphan dtors from
// our drop-time drain). See llvm_patches/macho_cxa_atexit_shim.h.
- InstallCxaAtexitShim(jit_->getExecutionSession(), jit_dylib);
+ TVM_FFI_ORCJIT_LLVM_CALL(InstallCxaAtexitShim(jit_->getExecutionSession(),
jit_dylib));
#endif
+ auto dylib_obj =
make_object<ORCJITDynamicLibraryObj>(GetRef<ORCJITExecutionSession>(this),
+ &jit_dylib,
jit_.get(), lib_name);
+ cleanup_dylib.release();
return ORCJITDynamicLibrary(std::move(dylib_obj));
}
@@ -528,6 +537,9 @@ void
ORCJITExecutionSessionObj::AddPendingDeinitializer(llvm::orc::JITDylib* jit
int64_t ORCJITExecutionSessionObj::ClearFreeSlabs() {
#ifdef __linux__
if (memory_manager_) {
+ // Synchronize with lookup/materialization and dylib teardown. This makes
+ // the public API safe even when another host thread is using the session.
+ std::lock_guard<std::mutex> lock(mutex_);
return static_cast<int64_t>(memory_manager_->clearFreeSlabs());
}
#endif
diff --git a/addons/tvm_ffi_orcjit/src/ffi/orcjit_session.h
b/addons/tvm_ffi_orcjit/src/ffi/orcjit_session.h
index 7c2ea4ef..b08b7c61 100644
--- a/addons/tvm_ffi_orcjit/src/ffi/orcjit_session.h
+++ b/addons/tvm_ffi_orcjit/src/ffi/orcjit_session.h
@@ -34,7 +34,6 @@
#include <tvm/ffi/optional.h>
#include <tvm/ffi/string.h>
-#include <atomic>
#include <memory>
#include <mutex>
#include <string>
@@ -79,9 +78,10 @@ class ORCJITExecutionSessionObj : public Object {
* \brief Get the process-wide shared execution session.
*
* A leaked, never-destroyed singleton so multiple callers in one process
- * share one LLVM ExecutionSession — hence process symbols, the slab arena,
- * and cross-library linking. Never torn down (interpreter finalization could
- * otherwise call back into the host language during teardown).
+ * share one LLVM ExecutionSession — hence process-symbol resolution, the
slab
+ * pool, and synchronization infrastructure. Loaded modules remain isolated
+ * symbol namespaces. Never torn down (interpreter finalization could
otherwise
+ * call back into the host language during teardown).
*
* Always uses the ORC runtime embedded in this extension; deliberately not
* user-configurable (a shared process-wide singleton with a hidden runtime
@@ -111,9 +111,9 @@ class ORCJITExecutionSessionObj : public Object {
* \c String path or in-memory \c Bytes image), injects context symbols
* eagerly, and — if the objects embed a library binary — reconstructs the
* import tree so the result behaves like a normally-loaded tvm-ffi module.
- * The whole sequence runs under one recursive session lock, and the dylib is
- * never exposed in a partially-loaded state, so finalization happens exactly
- * once and cannot be repeated or interleaved.
+ * The dylib is not exposed until finalization completes. Leaf operations are
+ * serialized by the session mutex, while JIT constructors run with that
mutex
+ * released so they can safely re-enter the session.
*
* \param objects Array whose elements are each a \c String path or \c Bytes
* object-file image.
@@ -195,9 +195,9 @@ class ORCJITExecutionSessionObj : public Object {
/*!
* \brief Release drained slabs (no live JIT allocations) back to the OS.
*
- * Returns the number of slabs reclaimed. No-op on macOS/Windows
- * where the slab pool is compiled out, or when the pool has been
- * disabled via `slab_size < 0`.
+ * Returns the number of slabs reclaimed. No-op on macOS/Windows, where the
+ * slab pool is compiled out, or when the pool has been disabled via
+ * `slab_size < 0`. Serialized with allocation and teardown by \ref mutex_.
*/
int64_t ClearFreeSlabs();
@@ -213,7 +213,7 @@ class ORCJITExecutionSessionObj : public Object {
std::unique_ptr<llvm::orc::LLJIT> jit_;
/*! \brief Counter for auto-generating library names */
- std::atomic<int> dylib_counter_{0};
+ int dylib_counter_{0};
/*! \brief Compiler-selected C++ runtime search JITDylibs, keyed by
shared-library path. */
std::unordered_map<std::string, llvm::orc::JITDylib*> cxx_runtime_dylibs_;
diff --git a/addons/tvm_ffi_orcjit/src/ffi/orcjit_slab.cc
b/addons/tvm_ffi_orcjit/src/ffi/orcjit_slab.cc
index 5c2d629c..b24e37b4 100644
--- a/addons/tvm_ffi_orcjit/src/ffi/orcjit_slab.cc
+++ b/addons/tvm_ffi_orcjit/src/ffi/orcjit_slab.cc
@@ -37,10 +37,12 @@
#include <sys/mman.h>
#include <algorithm>
+#include <cassert>
#include <cerrno>
#include <cstdio>
#include <cstring>
#include <limits>
+#include <thread>
namespace tvm {
namespace ffi {
@@ -167,20 +169,38 @@ Error Slab::commitPages(void* addr, std::size_t size) {
std::size_t last_chunk = (offset + size - 1) / kCommitGranularity;
for (std::size_t i = first_chunk; i <= last_chunk; ++i) {
- if (committed_[i].load(std::memory_order_acquire) != 0) continue;
- std::size_t chunk_offset = i * kCommitGranularity;
- std::size_t chunk_len = std::min(kCommitGranularity, arena_capacity_ -
chunk_offset);
- // mprotect is idempotent, so a concurrent racer calling it on the same
chunk
- // is harmless. Only flip the flag after success — otherwise a failed
commit
- // followed by freeRegion() would leave committed_[i] == 1, causing a
- // later allocation to skip mprotect and write into PROT_NONE memory.
- if (::mprotect(arena_base_ + chunk_offset, chunk_len, PROT_READ |
PROT_WRITE) != 0) {
- return make_error<StringError>("Slab: mprotect(RW) failed for chunk at
offset " +
- formatv("{0:x}", chunk_offset) + ": "
+
- std::strerror(errno),
- inconvertibleErrorCode());
+ constexpr std::uint8_t kUncommitted = 0;
+ constexpr std::uint8_t kCommitting = 1;
+ constexpr std::uint8_t kCommitted = 2;
+ auto& state = committed_[i];
+ while (true) {
+ std::uint8_t observed = state.load(std::memory_order_acquire);
+ if (observed == kCommitted) break;
+ if (observed == kCommitting) {
+ std::this_thread::yield();
+ continue;
+ }
+ if (!state.compare_exchange_weak(observed, kCommitting,
std::memory_order_acq_rel,
+ std::memory_order_acquire)) {
+ continue;
+ }
+
+ // Only the thread that claimed this chunk may change its protection.
+ // A duplicate mprotect(RW) racing after another allocation finalized to
+ // RX/R would silently remove that allocation's execute/read-only state.
+ std::size_t chunk_offset = i * kCommitGranularity;
+ std::size_t chunk_len = std::min(kCommitGranularity, arena_capacity_ -
chunk_offset);
+ if (::mprotect(arena_base_ + chunk_offset, chunk_len, PROT_READ |
PROT_WRITE) != 0) {
+ int commit_errno = errno;
+ state.store(kUncommitted, std::memory_order_release);
+ return make_error<StringError>("Slab: mprotect(RW) failed for chunk at
offset " +
+ formatv("{0:x}", chunk_offset) + ":
" +
+ std::strerror(commit_errno),
+ inconvertibleErrorCode());
+ }
+ state.store(kCommitted, std::memory_order_release);
+ break;
}
- committed_[i].store(1, std::memory_order_release);
}
return Error::success();
}
@@ -411,7 +431,7 @@ Expected<std::size_t> Slab::bumpAllocate(std::size_t size,
bool is_exec) {
}
// Bump allocate within the pool's limit.
- if (bump + size > limit) {
+ if (size > limit - bump) {
return make_error<SlabPoolExhaustedError>(is_exec ? "exec" : "non-exec",
bump, size, limit);
}
diff --git a/addons/tvm_ffi_orcjit/src/ffi/orcjit_slab.h
b/addons/tvm_ffi_orcjit/src/ffi/orcjit_slab.h
index 0029dc51..e7f1d215 100644
--- a/addons/tvm_ffi_orcjit/src/ffi/orcjit_slab.h
+++ b/addons/tvm_ffi_orcjit/src/ffi/orcjit_slab.h
@@ -25,11 +25,9 @@
* bump-allocates from it, keeping all JIT allocations within range of
* PC-relative relocations (±2 GB on x86_64, ±4 GB on AArch64).
*
- * The `Slab` is the unit-of-VA-reservation for the OrcJIT memory manager.
- * Today it is used as a single-slab arena owned by
- * `ArenaJITLinkMemoryManager`. Stage B of the refactor will introduce a
- * `SlabPoolMemoryManager` that holds multiple Slabs and grows by mmap-ing
- * new ones on demand.
+ * The `Slab` is the unit of VA reservation for `SlabPoolMemoryManager`,
+ * which owns a growable set of slabs and adds one when no existing slab can
+ * satisfy a graph's per-pool footprint.
*
* ## Page commit + Transparent Huge Page (THP) support
*
@@ -37,8 +35,9 @@
* size matches the Linux huge-page granule on both x86_64 and AArch64,
* enabling THP promotion via `madvise(MADV_HUGEPAGE)` on the full
* reservation. Each 2 MB commit-chunk is `mprotect`-ed to RW exactly once
- * via an atomic bitmap flag (`committed_`), avoiding lock contention with
- * the per-pool allocator mutex.
+ * via an atomic state bitmap (`committed_`), avoiding lock contention with
+ * the per-pool allocator mutex while ensuring only one thread changes the
+ * protection of a commit chunk.
*
* ## Dual-pool exec / non-exec split
*
@@ -111,8 +110,8 @@ class SlabPoolExhaustedError : public
llvm::ErrorInfo<SlabPoolExhaustedError> {
* Zero-sized sub-regions indicate no allocation from that pool.
*
* \p owner points to the Slab that handed out this allocation. With one
- * slab per session today this is redundant, but stamping it now makes
- * Stage B's pool-manager routing O(1) without address comparison.
+ * Stamping the owning slab makes pool-manager deallocation routing O(1)
+ * without address comparison.
*/
struct FinalizedAllocInfo {
Slab* owner; ///< Slab that owns these offsets.
@@ -160,8 +159,8 @@ class Slab {
/*! \brief Construct a Slab and reserve \p capacity bytes of VA.
*
* On reservation failure, returns with \c base() == nullptr — the
- * caller is expected to retry at a smaller capacity or
- * \c report_fatal_error.
+ * caller is expected to retry at a smaller capacity or propagate a
+ * recoverable allocation error.
*/
Slab(std::size_t page_size, std::size_t capacity);
@@ -302,9 +301,8 @@ class Slab {
std::vector<FreeBlock> free_list_non_exec_;
std::vector<FreeBlock> free_list_exec_;
- /*! \brief Per-commit-chunk flags (0 = uncommitted, 1 = committed).
- * Lock-free: each chunk is mprotect'd exactly once via
- * compare_exchange. */
+ /*! \brief Per-commit-chunk states (0 = uncommitted, 1 = committing,
+ * 2 = committed). Each chunk is mprotect'd exactly once. */
std::unique_ptr<std::atomic<std::uint8_t>[]> committed_;
std::size_t num_commit_chunks_ = 0;
diff --git a/addons/tvm_ffi_orcjit/tests/CMakeLists.txt
b/addons/tvm_ffi_orcjit/tests/CMakeLists.txt
index 92bca5e5..e5e0f338 100644
--- a/addons/tvm_ffi_orcjit/tests/CMakeLists.txt
+++ b/addons/tvm_ffi_orcjit/tests/CMakeLists.txt
@@ -88,7 +88,7 @@ if (NOT WIN32)
endif ()
# Pure C object files — built on all platforms (no C++ runtime deps)
-enablelanguage(C)
+enable_language(C)
add_test_object(sources/c/test_funcs.c)
add_test_object(sources/c/test_funcs2.c)
add_test_object(sources/c/test_funcs_conflict.c)
@@ -103,7 +103,7 @@ add_test_object(sources/c/test_ctor_dtor.c)
# CUDA object files — optional
find_package(CUDAToolkit)
if (CUDAToolkit_FOUND)
- enablelanguage(CUDA)
+ enable_language(CUDA)
message(STATUS "CUDA found: ${CUDAToolkit_VERSION}")
add_test_object(sources/cuda/test_funcs.cu)
endif ()
diff --git a/addons/tvm_ffi_orcjit/tests/README.md
b/addons/tvm_ffi_orcjit/tests/README.md
index beb9f51b..379946bc 100644
--- a/addons/tvm_ffi_orcjit/tests/README.md
+++ b/addons/tvm_ffi_orcjit/tests/README.md
@@ -87,9 +87,9 @@ variant subdirectories (c/, cc/, c-gcc/, etc.).
| `test_funcs2` | More arithmetic (subtract, divide) |
| `test_funcs_conflict` | Symbol conflict testing (duplicate `add`) |
| `test_call_global` | Callbacks into Python-registered global functions |
-| `test_context` | First-lookup library-context injection |
+| `test_context` | Eager library-context injection, including constructor
ordering |
| `test_types` | Zero-arg, multi-arg, float, void return types |
-| `test_link_order_base` / `test_link_order_caller` | Cross-library symbol
resolution |
+| `test_link_order_base` / `test_link_order_caller` | Cross-object symbol
resolution within one module |
| `test_error` | Error propagation from JIT'd code |
| `test_ctor_dtor` | Constructor/destructor and init/fini sections |
diff --git a/addons/tvm_ffi_orcjit/tests/sources/c/test_context.c
b/addons/tvm_ffi_orcjit/tests/sources/c/test_context.c
index d4a40760..8e6b0a5c 100644
--- a/addons/tvm_ffi_orcjit/tests/sources/c/test_context.c
+++ b/addons/tvm_ffi_orcjit/tests/sources/c/test_context.c
@@ -19,7 +19,22 @@
#include <tvm/ffi/c_api.h>
-TVM_FFI_DLL_EXPORT void* __tvm_ffi__library_ctx = NULL;
+// The host writes this slot after compilation and before constructors run.
+// Volatile keeps the constructor's read from being folded to NULL.
+TVM_FFI_DLL_EXPORT void* volatile __tvm_ffi__library_ctx = NULL;
+static int context_was_set_during_init = 0;
+
+static void record_context_during_init(void) {
+ context_was_set_during_init = __tvm_ffi__library_ctx != NULL;
+}
+
+#ifdef _MSC_VER
+typedef void(__cdecl* ctor_t)(void);
+#pragma section(".CRT$XCU", read)
+__declspec(allocate(".CRT$XCU")) ctor_t __tvm_test_context_init =
record_context_during_init;
+#else
+__attribute__((constructor)) static void context_init(void) {
record_context_during_init(); }
+#endif
TVM_FFI_DLL_EXPORT int __tvm_ffi_context_is_set(void* self, const TVMFFIAny*
args, int32_t num_args,
TVMFFIAny* result) {
@@ -31,3 +46,14 @@ TVM_FFI_DLL_EXPORT int __tvm_ffi_context_is_set(void* self,
const TVMFFIAny* arg
result->v_int64 = __tvm_ffi__library_ctx != NULL;
return 0;
}
+
+TVM_FFI_DLL_EXPORT int __tvm_ffi_context_was_set_during_init(void* self, const
TVMFFIAny* args,
+ int32_t num_args,
TVMFFIAny* result) {
+ (void)self;
+ (void)args;
+ (void)num_args;
+ result->type_index = kTVMFFIInt;
+ result->zero_padding = 0;
+ result->v_int64 = context_was_set_during_init;
+ return 0;
+}
diff --git a/addons/tvm_ffi_orcjit/tests/sources/c/test_link_order_base.c
b/addons/tvm_ffi_orcjit/tests/sources/c/test_link_order_base.c
index 21f26df9..377d3f8c 100644
--- a/addons/tvm_ffi_orcjit/tests/sources/c/test_link_order_base.c
+++ b/addons/tvm_ffi_orcjit/tests/sources/c/test_link_order_base.c
@@ -18,7 +18,7 @@
*/
/*
- * Base library for cross-library linking test.
+ * Base object for the intra-module linking test.
* Exports helper_add which is called by test_link_order_caller.c.
*/
#include <tvm/ffi/c_api.h>
diff --git a/addons/tvm_ffi_orcjit/tests/sources/c/test_link_order_caller.c
b/addons/tvm_ffi_orcjit/tests/sources/c/test_link_order_caller.c
index 215a267b..9214ed75 100644
--- a/addons/tvm_ffi_orcjit/tests/sources/c/test_link_order_caller.c
+++ b/addons/tvm_ffi_orcjit/tests/sources/c/test_link_order_caller.c
@@ -18,17 +18,17 @@
*/
/*
- * Caller library for cross-library linking test.
+ * Caller object for the intra-module linking test.
* References __tvm_ffi_helper_add from test_link_order_base.c via extern
* declaration, and exports cross_lib_add which forwards to it.
*/
#include <tvm/ffi/c_api.h>
-/* Declare external symbol from the base library */
+/* Declare the external symbol from the base object. */
extern int __tvm_ffi_helper_add(void* self, const TVMFFIAny* args, int32_t
num_args,
TVMFFIAny* result);
-/* cross_lib_add: forwards to helper_add in the base library */
+/* cross_lib_add: forwards to helper_add in the base object. */
TVM_FFI_DLL_EXPORT int __tvm_ffi_cross_lib_add(void* self, const TVMFFIAny*
args, int32_t num_args,
TVMFFIAny* result) {
return __tvm_ffi_helper_add(self, args, num_args, result);
diff --git a/addons/tvm_ffi_orcjit/tests/sources/cc/test_link_order_base.cc
b/addons/tvm_ffi_orcjit/tests/sources/cc/test_link_order_base.cc
index c878c6ea..a5df5815 100644
--- a/addons/tvm_ffi_orcjit/tests/sources/cc/test_link_order_base.cc
+++ b/addons/tvm_ffi_orcjit/tests/sources/cc/test_link_order_base.cc
@@ -15,7 +15,7 @@
// specific language governing permissions and limitations
// under the License.
-// Base library for cross-library linking test (C++ version).
+// Base object for the intra-module linking test (C++ version).
// Exports helper_add which is called by test_link_order_caller.cc.
#include <tvm/ffi/function.h>
diff --git a/addons/tvm_ffi_orcjit/tests/sources/cc/test_link_order_caller.cc
b/addons/tvm_ffi_orcjit/tests/sources/cc/test_link_order_caller.cc
index 85dde579..63a5b824 100644
--- a/addons/tvm_ffi_orcjit/tests/sources/cc/test_link_order_caller.cc
+++ b/addons/tvm_ffi_orcjit/tests/sources/cc/test_link_order_caller.cc
@@ -15,7 +15,7 @@
// specific language governing permissions and limitations
// under the License.
-// Caller library for cross-library linking test (C++ version).
+// Caller object for the intra-module linking test (C++ version).
// References __tvm_ffi_helper_add from test_link_order_base.cc and
// exports cross_lib_add which forwards to it.
diff --git a/addons/tvm_ffi_orcjit/tests/test_basic.py
b/addons/tvm_ffi_orcjit/tests/test_basic.py
index c7b9e822..570e4151 100644
--- a/addons/tvm_ffi_orcjit/tests/test_basic.py
+++ b/addons/tvm_ffi_orcjit/tests/test_basic.py
@@ -106,6 +106,14 @@ class Variant:
"""Return path prefix for test_call_global object."""
return f"{self.subdir}/test_call_global"
+ def link_order_base_obj(self) -> str:
+ """Return path prefix for the intra-module dependency object."""
+ return f"{self.subdir}/test_link_order_base"
+
+ def link_order_caller_obj(self) -> str:
+ """Return path prefix for the object that calls the dependency."""
+ return f"{self.subdir}/test_link_order_caller"
+
def types_obj(self) -> str:
"""Return path prefix for test_types object."""
return f"{self.subdir}/test_types"
@@ -191,6 +199,17 @@ def test_multiple_objects_in_one_module(v: Variant) ->
None:
assert mod.get_function(v.fn("test_divide"))(20, 4) == 5
[email protected]("v", _all_variants, ids=_variant_id)
[email protected]("reverse", [False, True], ids=["caller-first",
"base-first"])
+def test_intra_module_dependency_is_input_order_independent(v: Variant,
reverse: bool) -> None:
+ """Undefined symbols resolve between objects regardless of input order."""
+ objects = [v.link_order_caller_obj(), v.link_order_base_obj()]
+ if reverse:
+ objects.reverse()
+ mod = load(*objects)
+ assert mod.get_function(v.fn("cross_lib_add"))(17, 25) == 42
+
+
@pytest.mark.parametrize("v", _all_variants, ids=_variant_id)
def test_function_not_found(v: Variant) -> None:
"""Raise AttributeError for a missing function name."""
@@ -285,8 +304,9 @@ def test_call_global(v: Variant) -> None:
def test_context_injected_at_load() -> None:
- """Context is injected eagerly at load, so concurrent lookups all observe
it."""
+ """Context is injected before constructors and concurrent lookups observe
it."""
mod = load("c/test_context")
+ assert mod.context_was_set_during_init() == 1
num_workers = 8
barrier = threading.Barrier(num_workers)
@@ -461,6 +481,42 @@ def test_ctor_dtor(v: Variant) -> None:
assert "<dtors>" not in log
+def test_concurrent_first_lookup_waits_for_initializers() -> None:
+ """A second thread cannot call newly materialized code before init
completes."""
+ ctor_entered = threading.Event()
+ release_ctor = threading.Event()
+ second_started = threading.Event()
+ second_returned = threading.Event()
+
+ @tvm_ffi.register_global_func("append_log", override=True)
+ def _block_first_ctor(_value: str) -> None:
+ if not ctor_entered.is_set():
+ ctor_entered.set()
+ release_ctor.wait(timeout=5)
+
+ mod = load("c/test_ctor_dtor")
+
+ def first_call() -> None:
+ mod.main()
+
+ def second_call() -> None:
+ second_started.set()
+ mod.main()
+ second_returned.set()
+
+ with ThreadPoolExecutor(max_workers=2) as pool:
+ first = pool.submit(first_call)
+ assert ctor_entered.wait(timeout=5), "constructor did not start"
+ second = pool.submit(second_call)
+ assert second_started.wait(timeout=5), "second lookup did not start"
+ assert not second_returned.wait(timeout=0.25), (
+ "second call returned before the first thread completed
initialization"
+ )
+ release_ctor.set()
+ first.result(timeout=5)
+ second.result(timeout=5)
+
+
# ---------------------------------------------------------------------------
# Module drop — dropping a loaded Module while its session is still alive.
#
@@ -630,7 +686,7 @@ def test_containers_tuple(v: Variant) -> None:
# ---------------------------------------------------------------------------
-# Slab-pool growth (Stage B).
+# Slab-pool growth.
#
# A session holds a growable pool of Slabs, each `slab_size` bytes. When a
# JITLink graph won't fit in any existing slab, the pool mmap's a new one;
@@ -640,10 +696,8 @@ def test_containers_tuple(v: Variant) -> None:
# ---------------------------------------------------------------------------
-# 8 MB is the practical floor — Slab::kCommitGranularity is 2 MB and the
-# dual-pool midpoint needs at least two commit chunks of headroom above it,
-# so smaller capacities break the pool layout. See SlabPoolMemoryManager
-# kMinSlabSize.
+# Use 8 MB to force frequent growth while leaving more headroom than the 4 MB
+# structural minimum (one 2 MB commit chunk per allocation pool).
_SMALL_SLAB = 8 * 1024 * 1024
@@ -850,3 +904,10 @@ def test_clear_free_slabs_disabled_pool() -> None:
"""When the slab pool is disabled, clear_free_slabs is a no-op (returns
0)."""
session = ExecutionSession(slab_size=-1)
assert session.clear_free_slabs() == 0
+
+
[email protected](sys.platform != "linux", reason="slab pool is Linux-only")
+def test_rejects_too_small_custom_slab() -> None:
+ """A slab needs at least one 2 MB commit chunk per allocation pool."""
+ with pytest.raises(ValueError, match="slab_size must be"):
+ ExecutionSession(slab_size=2 * 1024 * 1024)
diff --git a/addons/tvm_ffi_orcjit/tests/test_library_visibility.py
b/addons/tvm_ffi_orcjit/tests/test_library_visibility.py
index 8f853313..bd8833aa 100644
--- a/addons/tvm_ffi_orcjit/tests/test_library_visibility.py
+++ b/addons/tvm_ffi_orcjit/tests/test_library_visibility.py
@@ -22,9 +22,11 @@ import ctypes
import platform
import shutil
import subprocess
+import sys
import pytest
import tvm_ffi_orcjit
+from utils import build_test_objects
@pytest.mark.skipif(platform.system() == "Windows", reason="RTLD_DEFAULT is
POSIX-only")
@@ -52,3 +54,38 @@ def test_addon_exports_only_initializer() -> None:
output = subprocess.run(command, check=True, capture_output=True,
text=True).stdout
exported = {line.split()[0].split("@@", 1)[0] for line in
output.splitlines() if line.strip()}
assert exported == expected
+
+
+def test_tvm_ffi_loader_survives_process_shutdown() -> None:
+ """The TVM-FFI keep-alive registry must retain the addon through
shutdown."""
+ obj_dir = build_test_objects()
+ candidates = [
+ "cc-gcc/test_funcs.o",
+ "cc/test_funcs.o",
+ "cc-appleclang/test_funcs.o",
+ "c-msvc/test_funcs.o",
+ "c-clang-cl/test_funcs.o",
+ "c/test_funcs.o",
+ "c-gcc/test_funcs.o",
+ ]
+ obj_path = next(
+ (obj_dir / candidate for candidate in candidates if (obj_dir /
candidate).is_file()), None
+ )
+ if obj_path is None:
+ pytest.skip("no test object is available")
+
+ script = """
+import sys
+from tvm_ffi_orcjit import ExecutionSession
+
+module = ExecutionSession().load_module(sys.argv[1], keep_module_alive=True)
+assert module.test_add(2, 3) == 5
+del module
+"""
+ result = subprocess.run(
+ [sys.executable, "-c", script, str(obj_path.resolve())],
+ check=False,
+ capture_output=True,
+ text=True,
+ )
+ assert result.returncode == 0, result.stderr
diff --git a/addons/tvm_ffi_orcjit/tests/test_session_load_module.py
b/addons/tvm_ffi_orcjit/tests/test_session_load_module.py
index 1b11b753..562ede31 100644
--- a/addons/tvm_ffi_orcjit/tests/test_session_load_module.py
+++ b/addons/tvm_ffi_orcjit/tests/test_session_load_module.py
@@ -311,8 +311,8 @@ def test_load_module_expands_embedded_library_bin(tmp_path:
Path) -> None:
"""An embedded library binary is deserialized and its imports are wired."""
# A custom module kind whose loader returns a real orcjit module. This also
- # forces the loader to re-enter load_module while the outer call holds the
- # session lock — exercising the recursive session lock.
+ # forces load_module to re-enter the shared session during outer-module
+ # finalization, exercising nested loading without deadlock.
@tvm_ffi.register_global_func("ffi.Module.load_from_bytes.orcjit_test_probe",
override=True)
def _load_probe(_data: bytes) -> tvm_ffi.Module:
return default_session().load_module(obj("c/test_funcs2"))
@@ -342,8 +342,8 @@ def test_load_module_expands_embedded_library_bin(tmp_path:
Path) -> None:
# ---------------------------------------------------------------------------
# Concurrency — the shared session driven by many threads at once.
#
-# Exercises the recursive session lock: overlapping create / add / lookup /
-# drop on one shared ExecutionSession must not corrupt linker state.
+# Exercises the session lock and per-dylib initializer gate: overlapping
+# create / add / lookup / drop must not corrupt linker state.
# ---------------------------------------------------------------------------
diff --git a/python/tvm_ffi/cpp/extension.py b/python/tvm_ffi/cpp/extension.py
index 09bb2f6a..f11b3284 100644
--- a/python/tvm_ffi/cpp/extension.py
+++ b/python/tvm_ffi/cpp/extension.py
@@ -1116,7 +1116,7 @@ def load_inline( # noqa: PLR0913
keep_module_alive
Whether to keep the module alive. If True, the module will be kept
alive
- for the duration of the program until libtvm_ffi.so is unloaded.
+ for the duration of the process.
backend
The GPU backend to use. It can be "cuda" or "hip".
@@ -1441,7 +1441,7 @@ def load( # noqa: PLR0913
keep_module_alive
Whether to keep the module alive. If True, the module will be kept
alive
- for the duration of the program until libtvm_ffi.so is unloaded.
+ for the duration of the process.
backend
The GPU backend to use. It can be "cuda" or "hip".
diff --git a/python/tvm_ffi/module.py b/python/tvm_ffi/module.py
index 8c69eaeb..ee795dd8 100644
--- a/python/tvm_ffi/module.py
+++ b/python/tvm_ffi/module.py
@@ -445,7 +445,7 @@ def load_module(path: str | PathLike, keep_module_alive:
bool = True) -> Module:
keep_module_alive
Whether to keep the module alive. If True, the module will be kept
alive
- for the duration of the program until libtvm_ffi.so is unloaded.
+ for the duration of the process.
Returns
-------
diff --git a/src/ffi/extra/module.cc b/src/ffi/extra/module.cc
index 31bb95bb..f8c2cd3c 100644
--- a/src/ffi/extra/module.cc
+++ b/src/ffi/extra/module.cc
@@ -49,8 +49,12 @@ class ModuleGlobals {
}
static ModuleGlobals* Get() {
- static ModuleGlobals instance;
- return &instance;
+ // Process-lifetime by design. A pinned DSO may have registered functions
+ // whose callable contexts and deleters live in that DSO. Destroying this
+ // registry during static teardown can unload the DSO before the global
+ // function registry and other dependents release those callbacks.
+ static auto* instance = new ModuleGlobals();
+ return instance;
}
private: