Skip to content

Commit c11a57a

Browse files
jtuylsclaude
andauthored
Replace iree-compile subprocess with compiler C API dynamic loading (#21)
Switch from invoking iree-compile as a subprocess to dynamically loading libIREECompiler.so via the IREE compiler C API. The runtime stays statically linked; only the compiler is loaded at runtime via dlopen. - Add 6-level compiler discovery chain: session option, env vars (IREE_EP_COMPILER_LIB, IREE_EP_COMPILER_LIB_DIR), build-time default, relative-to-EP search, bare dlopen fallback - CMake auto-detects pip-installed compiler with Python3_FIND_VIRTUALENV, validates the library exists, supports IREE_EP_REQUIRE_COMPILER_LIB - Replace .lds/.txt linker scripts with -fvisibility=hidden + EP_EXPORT - Add cross-platform support (Windows GetModuleHandleExA, macOS -unexported_symbols_list) - Thread-safe init via std::call_once with retry-on-failure semantics - Add compiler discovery test suite - Add get_compiler_library_path() Python helper Signed-off-by: Jorn <jorn.tuyls@gmail.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 8d7e4d0 commit c11a57a

File tree

14 files changed

+874
-84
lines changed

14 files changed

+874
-84
lines changed

.github/workflows/ci-macos.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ jobs:
3434
- name: Install Python package
3535
run: |
3636
ONNXRUNTIME_EP_IREE_BUILD_DIR=${{ github.workspace }}/${BUILD_DIR} \
37-
pip install -e python/
37+
pip install python/
3838
3939
- name: Run tests
4040
run: pytest test/ -v

.github/workflows/ci-windows.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ jobs:
3333
- name: Install Python package
3434
run: |
3535
$env:ONNXRUNTIME_EP_IREE_BUILD_DIR = "${{ github.workspace }}\${{ env.BUILD_DIR }}"
36-
pip install -e python/
36+
pip install python/
3737
3838
- name: Run tests
3939
run: pytest test/ -v

.github/workflows/ci.yml

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,10 @@ jobs:
3636
- name: Install Python package
3737
run: |
3838
ONNXRUNTIME_EP_IREE_BUILD_DIR=${{ github.workspace }}/${BUILD_DIR} \
39-
pip install -e python/
39+
pip install python/
4040
4141
- name: Run tests
4242
run: pytest test/ -v
4343

4444
- name: Run ExternDispatch tests
45-
run: |
46-
pytest test/test_extern_dispatch.py \
47-
--ep-lib ${{ github.workspace }}/${BUILD_DIR}/libonnxruntime_ep_iree.so \
48-
-v
45+
run: pytest test/test_extern_dispatch.py -v

CMakeLists.txt

Lines changed: 116 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -64,12 +64,9 @@ endif()
6464
# When not specified, FetchContent the IREE sources from GitHub at the tag
6565
# specified by `IREE_GIT_TAG`.
6666
#
67-
# We choose not to take a source dependency on the IREE compiler, which is
68-
# quite heavy and can be slow to build. It is left to the consumer of the EP to
69-
# make sure iree compiler tooling scripts are available in their environment.
70-
# This can be generally done by `pip install iree-base-compiler`.
71-
72-
# We may in future decide to build the IREE compiler from source.
67+
# The EP dynamically loads libIREECompiler.so at runtime via dlopen (no
68+
# compile-time linking to the compiler). The loader source and C API headers
69+
# come from the IREE source tree (compiler/bindings/c/).
7370
#
7471
################################################################################
7572

@@ -81,7 +78,7 @@ set(IREE_GIT_TAG "v3.10.0")
8178
set(IREE_SOURCE_DIR "" CACHE FILEPATH "Path to IREE source")
8279

8380
# Set IREE build flags.
84-
set(IREE_VISIBILITY_HIDDEN OFF)
81+
set(IREE_VISIBILITY_HIDDEN ON)
8582
set(IREE_BUILD_COMPILER OFF)
8683
set(IREE_BUILD_TESTS OFF)
8784
set(IREE_BUILD_SAMPLES OFF)
@@ -103,6 +100,9 @@ endif()
103100

104101
if(IREE_SOURCE_DIR)
105102
message(STATUS "Using existing IREE sources: ${IREE_SOURCE_DIR}")
103+
# Normalize so downstream code can use iree_SOURCE_DIR unconditionally
104+
# (FetchContent sets this automatically; local source needs it explicitly).
105+
set(iree_SOURCE_DIR "${IREE_SOURCE_DIR}")
106106
add_subdirectory(${IREE_SOURCE_DIR} iree SYSTEM EXCLUDE_FROM_ALL)
107107
else()
108108
message(STATUS "Fetching IREE sources from tag ${IREE_GIT_TAG}")
@@ -131,6 +131,65 @@ else()
131131
endif()
132132
endif()
133133

134+
################################################################################
135+
# Compiler Loader
136+
#
137+
# The EP uses the IREE compiler C API (embedding_api.h) via a dynamic loader
138+
# (loader.h / loader.cpp). At runtime, the EP loads libIREECompiler.so via
139+
# dlopen — there is no compile-time linking to the compiler. The loader source
140+
# and C API headers come from the IREE source tree.
141+
#
142+
# At runtime, the compiler library is resolved in this order:
143+
# 1. Session option: ep.iree.compiler_lib_path (explicit path)
144+
# 2. Env var: IREE_EP_COMPILER_LIB (full path to library file)
145+
# 3. Env var: IREE_EP_COMPILER_LIB_DIR (directory to search)
146+
# 4. Relative to EP .so: iree/compiler/_mlir_libs/ (pip co-installs)
147+
# 5. Fallback: dlopen("libIREECompiler.so") via standard search paths
148+
#
149+
################################################################################
150+
151+
# Helper: check whether a Python module is importable in the active environment.
152+
# Sets ${result_var} to TRUE/FALSE in the caller's scope.
153+
function(check_python_import module result_var)
154+
set(Python3_FIND_VIRTUALENV FIRST)
155+
find_package(Python3 COMPONENTS Interpreter QUIET)
156+
unset(Python3_FIND_VIRTUALENV)
157+
if(NOT Python3_FOUND)
158+
message(STATUS "Python3 interpreter not found; skipping ${module} check")
159+
set(${result_var} FALSE PARENT_SCOPE)
160+
return()
161+
endif()
162+
execute_process(
163+
COMMAND "${Python3_EXECUTABLE}" -c
164+
"import importlib.util; exit(0 if importlib.util.find_spec('${module}') else 1)"
165+
RESULT_VARIABLE _import_result
166+
OUTPUT_QUIET ERROR_QUIET
167+
)
168+
if(_import_result EQUAL 0)
169+
set(${result_var} TRUE PARENT_SCOPE)
170+
else()
171+
set(${result_var} FALSE PARENT_SCOPE)
172+
endif()
173+
endfunction()
174+
175+
# Check for iree-base-compiler. Not required at build time — the EP discovers
176+
# libIREECompiler.so at runtime. A warning is printed if not found.
177+
# Use iree.compiler._mlir_libs (not iree.compiler) because the IREE build tree
178+
# creates a namespace package that makes find_spec('iree.compiler') succeed
179+
# even without iree-base-compiler installed.
180+
check_python_import("iree.compiler._mlir_libs" _iree_compiler_installed)
181+
if(_iree_compiler_installed)
182+
message(STATUS "iree-base-compiler is installed")
183+
else()
184+
message(WARNING
185+
"iree-base-compiler is not installed in the active Python environment.\n"
186+
"The EP will still build but will fail to compile models at runtime unless "
187+
"iree-base-compiler is installed or IREE_EP_COMPILER_LIB / "
188+
"IREE_EP_COMPILER_LIB_DIR env var is set.")
189+
endif()
190+
191+
set(IREE_COMPILER_BINDINGS_DIR "${iree_SOURCE_DIR}/compiler/bindings/c")
192+
134193
# Build shared library (EP plugin)
135194
add_library(onnxruntime_ep_iree SHARED
136195
src/plugin_entry.cc
@@ -145,20 +204,61 @@ add_library(onnxruntime_ep_iree SHARED
145204
src/temp_file.cc
146205
)
147206

207+
# Build IREE compiler loader as a static library. The loader's trampoline
208+
# functions use IREE_EMBED_EXPORTED (visibility("default")) which we can't
209+
# override at compile time. Instead, --exclude-libs localizes all symbols
210+
# from this archive when linking into our shared library.
211+
add_library(iree_compiler_loader STATIC
212+
${IREE_COMPILER_BINDINGS_DIR}/iree/compiler/loader/loader.cpp
213+
)
214+
set_target_properties(iree_compiler_loader PROPERTIES POSITION_INDEPENDENT_CODE ON)
215+
target_include_directories(iree_compiler_loader PRIVATE
216+
${IREE_COMPILER_BINDINGS_DIR}
217+
)
218+
if(MSVC)
219+
target_compile_options(iree_compiler_loader PRIVATE /w)
220+
else()
221+
target_compile_options(iree_compiler_loader PRIVATE -w)
222+
endif()
223+
148224
# Statically link IREE runtime and io/parameter libraries.
225+
# Link dl for dlopen/dlsym (compiler loader).
149226
target_link_libraries(onnxruntime_ep_iree PRIVATE
150227
iree_runtime_unified
151228
iree_io_parameter_index
152229
iree_io_parameter_index_provider
153230
iree_io_formats_irpa_irpa
154231
iree_modules_io_parameters_parameters
232+
iree_compiler_loader
233+
${CMAKE_DL_LIBS}
155234
)
156235

236+
# Localize all symbols from the compiler loader static library so its
237+
# IREE_EMBED_EXPORTED trampolines don't leak into our public symbol table.
238+
if(UNIX AND NOT APPLE)
239+
# GNU ld: hide all symbols from the loader's static archive.
240+
target_link_options(onnxruntime_ep_iree PRIVATE
241+
"LINKER:--exclude-libs,libiree_compiler_loader.a"
242+
)
243+
elseif(APPLE)
244+
# macOS ld: generate an unexported symbols list from the loader's trampolines.
245+
# The ireeCompiler* symbols are generated by loader.cpp with
246+
# visibility("default") — we re-hide them here.
247+
set(_unexport_file "${CMAKE_CURRENT_BINARY_DIR}/iree_loader_unexported.txt")
248+
file(WRITE "${_unexport_file}" "_ireeCompiler*\n")
249+
target_link_options(onnxruntime_ep_iree PRIVATE
250+
"LINKER:-unexported_symbols_list,${_unexport_file}"
251+
)
252+
endif()
253+
# On Windows, MSVC only exports symbols explicitly marked __declspec(dllexport),
254+
# so the loader trampolines (which use visibility("default"), a no-op on MSVC)
255+
# are not exported. No additional linker flags needed.
157256

158257
# Include directories
159258
target_include_directories(onnxruntime_ep_iree PRIVATE
160259
${IREE_EP_ONNX_SRC_DIR}/include
161260
${CMAKE_CURRENT_SOURCE_DIR}/src
261+
${IREE_COMPILER_BINDINGS_DIR}
162262
)
163263

164264
# Compiler flags
@@ -177,23 +277,15 @@ else()
177277
)
178278
endif()
179279

180-
# Export symbols for plugin loading
181-
if(MSVC)
182-
# Windows: Use .def file (ORT_EXPORT is defined as empty in ORT headers on Windows)
183-
set_target_properties(onnxruntime_ep_iree PROPERTIES
184-
LINK_FLAGS "/DEF:${CMAKE_CURRENT_SOURCE_DIR}/src/symbols.def"
185-
)
186-
elseif(UNIX AND NOT APPLE)
187-
# Linux: Use version script
188-
set_target_properties(onnxruntime_ep_iree PROPERTIES
189-
LINK_FLAGS "-Wl,--version-script=${CMAKE_CURRENT_SOURCE_DIR}/src/symbols.lds"
190-
)
191-
elseif(APPLE)
192-
# macOS: Export all symbols
193-
set_target_properties(onnxruntime_ep_iree PROPERTIES
194-
LINK_FLAGS "-Wl,-exported_symbols_list,${CMAKE_CURRENT_SOURCE_DIR}/src/symbols.txt"
195-
)
196-
endif()
280+
# Hide all symbols by default. Only plugin entry points (CreateEpFactories,
281+
# ReleaseEpFactory) are exported via explicit visibility("default") attributes.
282+
# VISIBILITY_INLINES_HIDDEN suppresses weak inline/template symbols (e.g.
283+
# std::format, libstdc++ internals) that would otherwise leak into the ABI.
284+
set_target_properties(onnxruntime_ep_iree PROPERTIES
285+
CXX_VISIBILITY_PRESET hidden
286+
C_VISIBILITY_PRESET hidden
287+
VISIBILITY_INLINES_HIDDEN ON
288+
)
197289

198290
# Install
199291
install(TARGETS onnxruntime_ep_iree

python/onnxruntime_ep_iree/__init__.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@
55

66
from pathlib import Path
77

8-
__all__ = ["get_library_path", "get_ep_name", "get_ep_names"]
8+
__all__ = [
9+
"get_library_path",
10+
"get_ep_name",
11+
"get_ep_names",
12+
]
913

1014
_LIB_NAMES = [
1115
"libonnxruntime_ep_iree.dylib",

0 commit comments

Comments
 (0)