Skip to content

Commit 43c90af

Browse files
committed
[ORCJIT] Add keep_module_alive to ExecutionSession.load_module
Mirror tvm_ffi.load_module's keep_module_alive option on the ORCJIT session so callers can pin a JIT module in the runtime's process-global registry. Without pinning, JIT-allocated heap Objects (String, Array, Map, Tuple, ...) that escape past their owning Module deref a deleter pointer into unmapped JIT memory and crash the process at teardown. - session.py: forward keep_module_alive through to ModuleGlobalsAdd; Notes document the transitive session-pin and slab-granularity RSS trade-off. - examples/quick-start: drop std::string-returning concat (it hit the escape hazard without opting into the pin); example is now int-only. - tests/sources/cc/test_containers.cc: new source covering String / Array / Map / Tuple returns, one function per container. - tests/test_basic.py: load() gains keep_module_alive; new parametrized tests verify each container return survives del mod when pinned.
1 parent f3da15a commit 43c90af

6 files changed

Lines changed: 174 additions & 18 deletions

File tree

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);

addons/tvm_ffi_orcjit/examples/quick-start/run.py

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -98,13 +98,6 @@ def _run_tests(obj_file: str, lang: str) -> None:
9898
print(f"fibonacci(10) = {result}")
9999
assert result == 55, f"Expected 55, got {result}"
100100

101-
if lang == "cpp":
102-
# String concatenation only available in C++ variant (uses std::string)
103-
print("\n=== Testing concat function ===")
104-
result = mod.concat("Hello, ", "World!")
105-
print(f"concat('Hello, ', 'World!') = '{result}'")
106-
assert result == "Hello, World!", f"Expected 'Hello, World!', got '{result}'"
107-
108101
print("\n" + "=" * 50)
109102
print("All tests passed successfully!")
110103
print("=" * 50)

addons/tvm_ffi_orcjit/python/tvm_ffi_orcjit/session.py

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ def load_module(
119119
self,
120120
objects: str | Path | bytes | bytearray | Sequence[str | Path | bytes | bytearray],
121121
name: str = "",
122+
keep_module_alive: bool = False,
122123
) -> Module:
123124
"""Load one or more object files into a fresh module.
124125
@@ -132,19 +133,39 @@ def load_module(
132133
Object files to load, given as paths and/or in-memory object-file
133134
images. A single item is accepted as shorthand for a one-element list.
134135
name : str
135-
Optional name for the underlying library. If empty, a unique name is
136-
generated.
136+
Optional name for the underlying library. Auto-generated if empty.
137+
keep_module_alive : bool
138+
If True, pin the module in the runtime's process-global registry
139+
(see Notes). Defaults to False.
137140
138141
Returns
139142
-------
140143
Module
141144
A :class:`tvm_ffi.Module` whose imports and library context are fully
142-
wired. Dropping every reference to it frees the JIT memory.
145+
wired.
146+
147+
Notes
148+
-----
149+
``keep_module_alive`` mirrors :func:`tvm_ffi.load_module`'s option of
150+
the same name. When True, the module is inserted into the runtime's
151+
process-global module registry, so its JITDylib — and every function
152+
pointer, deleter, and static allocation it owns — stays mapped until
153+
the interpreter unloads ``libtvm_ffi``. Use this when Objects produced
154+
by the module may outlive the local ``mod`` reference (e.g., a
155+
JIT-allocated ``String`` or ``Array`` returned to Python and held past
156+
``del mod``). When False (default), the caller owns the module's
157+
lifetime and its JIT memory is reclaimed on drop; callers must ensure
158+
no JIT-produced Object outlives the returned module.
159+
160+
Pinning is transitive: the module holds a strong reference to its
161+
:class:`ExecutionSession`, so pinning one module also keeps that
162+
session (and its slab-pool memory manager) alive for the process
163+
lifetime. Reclaim is slab-granular — a slab shared between a pinned
164+
and an unpinned module stays mapped until the pinned module is gone.
143165
144166
Examples
145167
--------
146168
>>> session = default_session()
147-
>>> mod = session.load_module("dylib0.o")
148169
>>> mod = session.load_module(["a.o", "b.o", Path("c.o").read_bytes()])
149170
>>> mod.my_function(1, 2)
150171
@@ -162,7 +183,10 @@ def load_module(
162183
"load_module objects must be a path (str or Path) or object-file "
163184
f"bytes, but got {type(obj).__name__}"
164185
)
165-
return _ffi_api.SessionLoadModule(self, normalized, name) # type: ignore
186+
mod = _ffi_api.SessionLoadModule(self, normalized, name) # type: ignore
187+
if keep_module_alive:
188+
tvm_ffi._ffi_api.ModuleGlobalsAdd(mod) # type: ignore
189+
return mod
166190

167191
def clear_free_slabs(self) -> int:
168192
"""Release drained slabs (no live JIT allocations) back to the OS.

addons/tvm_ffi_orcjit/tests/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ if (NOT WIN32)
8484
add_test_object(sources/cc/test_link_order_base.cc)
8585
add_test_object(sources/cc/test_link_order_caller.cc)
8686
add_test_object(sources/cc/test_error.cc)
87+
add_test_object(sources/cc/test_containers.cc)
8788
endif ()
8889

8990
# Pure C object files — built on all platforms (no C++ runtime deps)
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The ASF licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
18+
// One JIT-allocated FFI container per test. Every returned Object is freshly
19+
// constructed inside this translation unit, so its header.deleter points into
20+
// JIT-mapped memory — the escape hazard that keep_module_alive=True pins the
21+
// JITDylib against.
22+
23+
#include <tvm/ffi/container/array.h>
24+
#include <tvm/ffi/container/map.h>
25+
#include <tvm/ffi/container/tuple.h>
26+
#include <tvm/ffi/function.h>
27+
#include <tvm/ffi/string.h>
28+
29+
#include <string>
30+
31+
using tvm::ffi::Array;
32+
using tvm::ffi::Map;
33+
using tvm::ffi::String;
34+
using tvm::ffi::Tuple;
35+
36+
// Character replace in a String.
37+
String test_string_replace_impl(String s, String from, String to) {
38+
std::string out(s);
39+
char c_from = std::string(from)[0];
40+
char c_to = std::string(to)[0];
41+
for (char& c : out) {
42+
if (c == c_from) c = c_to;
43+
}
44+
return String(out);
45+
}
46+
TVM_FFI_DLL_EXPORT_TYPED_FUNC(test_string_replace, test_string_replace_impl);
47+
48+
// Concatenate two Array<int64_t>s.
49+
Array<int64_t> test_array_concat_impl(Array<int64_t> a, Array<int64_t> b) {
50+
Array<int64_t> out;
51+
for (int64_t x : a) out.push_back(x);
52+
for (int64_t x : b) out.push_back(x);
53+
return out;
54+
}
55+
TVM_FFI_DLL_EXPORT_TYPED_FUNC(test_array_concat, test_array_concat_impl);
56+
57+
// Hand-built Map<String, int64_t>.
58+
Map<String, int64_t> test_map_build_impl() {
59+
Map<String, int64_t> out;
60+
out.Set(String("a"), 1);
61+
out.Set(String("b"), 2);
62+
return out;
63+
}
64+
TVM_FFI_DLL_EXPORT_TYPED_FUNC(test_map_build, test_map_build_impl);
65+
66+
// Reverse a four-element Tuple.
67+
Tuple<String, int64_t, String, int64_t> test_tuple_reverse_impl(
68+
Tuple<int64_t, String, int64_t, String> t) {
69+
return Tuple<String, int64_t, String, int64_t>(t.get<3>(), t.get<2>(), t.get<1>(), t.get<0>());
70+
}
71+
TVM_FFI_DLL_EXPORT_TYPED_FUNC(test_tuple_reverse, test_tuple_reverse_impl);

addons/tvm_ffi_orcjit/tests/test_basic.py

Lines changed: 73 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,11 +63,18 @@ def obj(name: str) -> str:
6363
return str(path)
6464

6565

66-
def load(*obj_names: str, session: ExecutionSession | None = None, name: str = "") -> Module:
66+
def load(
67+
*obj_names: str,
68+
session: ExecutionSession | None = None,
69+
name: str = "",
70+
keep_module_alive: bool = False,
71+
) -> Module:
6772
"""Load one or more object files into a fresh module on ``session``."""
6873
if session is None:
6974
session = ExecutionSession()
70-
return session.load_module([obj(o) for o in obj_names], name)
75+
return session.load_module(
76+
[obj(o) for o in obj_names], name, keep_module_alive=keep_module_alive
77+
)
7178

7279

7380
# ---------------------------------------------------------------------------
@@ -111,6 +118,16 @@ def ctor_dtor_obj(self) -> str:
111118
"""Return path prefix for test_ctor_dtor object."""
112119
return f"{self.subdir}/test_ctor_dtor"
113120

121+
def containers_obj(self) -> str:
122+
"""Return path prefix for test_containers object.
123+
124+
C++-only source; C variants return an empty prefix so ``obj()`` sees
125+
a missing path and skips the parametrized case for that variant.
126+
"""
127+
if not self.subdir.startswith("cc"):
128+
return ""
129+
return f"{self.subdir}/test_containers"
130+
114131
def fn(self, base_name: str) -> str:
115132
"""Return the function name for a given base name."""
116133
return base_name
@@ -558,6 +575,60 @@ def test_drop_all_modules_then_session(v: Variant) -> None:
558575
# surface as a crash under pytest.
559576

560577

578+
# ---------------------------------------------------------------------------
579+
# FFI-container returns under keep_module_alive.
580+
#
581+
# JIT-emitted code allocates each returned Object through inline templates
582+
# (make_object / make_inplace_array_object), so the object's header.deleter
583+
# points into JIT-mapped memory. If the owning Module were torn down before
584+
# the escaped Object, the deleter would dangle. keep_module_alive=True pins
585+
# the module in the runtime's process-global registry, keeping the JITDylib
586+
# mapped for the process lifetime. Each test loads with the flag, drops the
587+
# local mod reference, forces GC, and then reads the returned container —
588+
# proving the deleter is still reachable after the local mod is gone.
589+
# ---------------------------------------------------------------------------
590+
591+
592+
@pytest.mark.parametrize("v", _all_variants, ids=_variant_id)
593+
def test_containers_string(v: Variant) -> None:
594+
"""String return survives del mod when pinned."""
595+
mod = load(v.containers_obj(), keep_module_alive=True)
596+
result = mod.test_string_replace("hello world", "l", "L")
597+
del mod
598+
gc.collect()
599+
assert str(result) == "heLLo worLd"
600+
601+
602+
@pytest.mark.parametrize("v", _all_variants, ids=_variant_id)
603+
def test_containers_array(v: Variant) -> None:
604+
"""Array<int64_t> return survives del mod when pinned."""
605+
mod = load(v.containers_obj(), keep_module_alive=True)
606+
result = mod.test_array_concat([1, 2, 3], [4, 5, 6])
607+
del mod
608+
gc.collect()
609+
assert list(result) == [1, 2, 3, 4, 5, 6]
610+
611+
612+
@pytest.mark.parametrize("v", _all_variants, ids=_variant_id)
613+
def test_containers_map(v: Variant) -> None:
614+
"""Map<String, int64_t> return survives del mod when pinned."""
615+
mod = load(v.containers_obj(), keep_module_alive=True)
616+
result = mod.test_map_build()
617+
del mod
618+
gc.collect()
619+
assert dict(result) == {"a": 1, "b": 2}
620+
621+
622+
@pytest.mark.parametrize("v", _all_variants, ids=_variant_id)
623+
def test_containers_tuple(v: Variant) -> None:
624+
"""Tuple<...> return survives del mod when pinned."""
625+
mod = load(v.containers_obj(), keep_module_alive=True)
626+
result = mod.test_tuple_reverse((1, "a", 2, "b"))
627+
del mod
628+
gc.collect()
629+
assert tuple(result) == ("b", 2, "a", 1)
630+
631+
561632
# ---------------------------------------------------------------------------
562633
# Slab-pool growth (Stage B).
563634
#

0 commit comments

Comments
 (0)