Skip to content

Commit 098a323

Browse files
committed
Build and use a loader library to link in libomp and libomptarget
- Avoid platform specific issues in the order of loading those libraries to ensure inter-dependent symbol resolution
1 parent 395c127 commit 098a323

4 files changed

Lines changed: 130 additions & 39 deletions

File tree

buildscripts/conda-recipes/pyomp/meta.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,8 @@ test:
7171
- test -f $SP_DIR/numba/openmp/libs/openmp/lib/libomptarget-amdgpu.bc # [linux]
7272
- test -f $SP_DIR/numba/openmp/libs/openmp/lib/libomptarget-nvptx.bc # [linux]
7373
- test -f $SP_DIR/numba/openmp/libs/openmp/lib/libomptarget.so.{{ LLVM_VERSION_MAJOR }}.{{ LLVM_VERSION_MINOR }} # [linux]
74+
- test -f $SP_DIR/numba/openmp/libs/openmp/lib/libpyomp_loader.dylib # [osx]
75+
- test -f $SP_DIR/numba/openmp/libs/openmp/lib/libpyomp_loader.so # [linux]
7476

7577
about:
7678
home: https://github.com/Python-for-HPC/PyOMP

setup.py

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,10 @@ def run(self):
5454
else:
5555
super().run()
5656

57+
# Compile the loader wrapper after building all extensions to link with
58+
# libomp and libomptarget.
59+
self._build_loader_wrapper()
60+
5761
def finalize_options(self):
5862
super().finalize_options()
5963
# Create placeholder directories for package-data validation.
@@ -146,6 +150,81 @@ def _env_toolchain_args(self, ext):
146150
args.append(f"-DCMAKE_CXX_COMPILER={os.environ['CXX']}")
147151
return args
148152

153+
def _build_loader_wrapper(self):
154+
"""Compiles the pyomp_loader wrapper to lock in load order."""
155+
# Find build directory.
156+
if self.inplace:
157+
lib_dir = Path(
158+
self.get_finalized_command("build_py").get_package_dir(
159+
"numba.openmp.libs"
160+
)
161+
)
162+
else:
163+
lib_dir = Path(self.build_lib) / "numba/openmp/libs"
164+
165+
target_lib_dir = lib_dir / "openmp" / "lib"
166+
target_lib_dir.mkdir(parents=True, exist_ok=True)
167+
168+
# Find libomp in build directory or system library paths.
169+
omp_libs = list(target_lib_dir.glob("libomp.*"))
170+
if omp_libs:
171+
link_omp = str(omp_libs[0])
172+
print(f"Wrapper linking local libomp: {link_omp}")
173+
else:
174+
link_omp = "-lomp"
175+
print("Wrapper linking system libomp (-lomp)")
176+
177+
# Find libomptarget in build directory or system library paths (Linux
178+
# only). For now we always build libomptarget but we may want to link
179+
# with system libomptarget if it becomes available.
180+
if sys.platform.startswith("linux"):
181+
tgt_libs = list(target_lib_dir.glob("libomptarget.so*"))
182+
if tgt_libs:
183+
assert len(tgt_libs) == 1, "Expected single libomptarget library"
184+
link_tgt = str(tgt_libs[0])
185+
print(f"Wrapper linking local libomptarget: {link_tgt}")
186+
else:
187+
link_tgt = "-lomptarget"
188+
print("Wrapper linking system libomptarget (-lomptarget)")
189+
else:
190+
link_tgt = ""
191+
192+
# Generate the C wrapper file.
193+
loader_c = Path(self.build_temp) / "pyomp_loader.c"
194+
loader_c.parent.mkdir(parents=True, exist_ok=True)
195+
loader_c.write_text("void pyomp_loader_init(void) {}\n")
196+
197+
# Determine library extension and platform-specific linker flags.
198+
if sys.platform.startswith("linux"):
199+
lib_ext = ".so"
200+
rpath_flag = "-Wl,-rpath=$ORIGIN"
201+
extra_link_flags = ["-Wl,--no-as-needed"]
202+
elif sys.platform == "darwin":
203+
lib_ext = ".dylib"
204+
rpath_flag = "-Wl,-rpath,@loader_path"
205+
extra_link_flags = ["-Wl,-undefined,dynamic_lookup"]
206+
else:
207+
raise RuntimeError(f"Unsupported platform: {sys.platform}")
208+
209+
loader_so = target_lib_dir / f"libpyomp_loader{lib_ext}"
210+
cc = os.environ.get("CC", "gcc")
211+
212+
# Compile the wrapper.
213+
cmd = [
214+
cc,
215+
"-shared",
216+
"-fPIC",
217+
str(loader_c),
218+
"-o",
219+
str(loader_so),
220+
link_omp,
221+
link_tgt,
222+
rpath_flag,
223+
] + extra_link_flags
224+
225+
print("Compiling pyomp_loader wrapper")
226+
subprocess.run(cmd, check=True)
227+
149228

150229
class PrepareOpenMP:
151230
setup_done = False

src/numba/openmp/__init__.py

Lines changed: 34 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -60,48 +60,43 @@
6060

6161
def _init():
6262
sys_platform = sys.platform
63-
from ctypes.util import find_library
6463

65-
omplib = (
66-
libpath
67-
/ "openmp"
68-
/ "lib"
69-
/ f"libomp{'.dylib' if sys_platform == 'darwin' else '.so'}"
70-
)
64+
# Find the wrapper library, which links with libomp and libomptarget, and
65+
# loads them in the correct order with global visibility. Ensures RTLD_NEXT
66+
# works correctly for libomp to find libomptarget symbols.
67+
lib_ext = "dylib" if sys_platform == "darwin" else "so"
68+
wrapper_lib = libpath / "openmp" / "lib" / f"libpyomp_loader.{lib_ext}"
7169

72-
# Prefer bundled libomp if it exists.
73-
if omplib.exists():
74-
if DEBUG_OPENMP >= 1:
75-
print("Found bundled OpenMP runtime library at", omplib)
76-
ll.load_library_permanently(str(omplib))
77-
else:
78-
# There is no bundled libomp, try to find it in standard library paths.
79-
system_omplib = find_library("omp")
80-
if system_omplib:
81-
if DEBUG_OPENMP >= 1:
82-
print(f"Found system OpenMP runtime library: {system_omplib}")
83-
ll.load_library_permanently(system_omplib)
84-
else:
85-
raise RuntimeError(
86-
f"OpenMP runtime not found. Bundled library missing at {omplib} "
87-
"and no system libomp found via ctypes.util.find_library('omp'). "
88-
"Ensure libomp is available in library paths."
89-
)
90-
91-
# libomptarget is unavailable on apple, windows, so return.
92-
if sys_platform.startswith("darwin") or sys_platform.startswith("win32"):
93-
return
94-
95-
llvm_major, llvm_minor, _ = ll.llvm_version_info
96-
omptargetlib = (
97-
libpath / "openmp" / "lib" / f"libomptarget.so.{llvm_major}.{llvm_minor}"
98-
)
99-
if omptargetlib.exists():
70+
if not wrapper_lib.exists():
71+
raise RuntimeError(
72+
f"OpenMP loader wrapper not found at {wrapper_lib}. "
73+
"Ensure the package was built correctly."
74+
)
75+
76+
if DEBUG_OPENMP >= 1:
77+
print("Loading OpenMP runtimes via wrapper at", wrapper_lib)
78+
79+
# Load the wrapper.
80+
ll.load_library_permanently(str(wrapper_lib))
81+
82+
# Initialize the OpenMP target runtime.
83+
from ctypes import CFUNCTYPE
84+
85+
try:
86+
addr = ll.address_of_symbol("__tgt_rtl_init")
87+
if addr:
88+
cfunctype = CFUNCTYPE(None)
89+
cfunc = cfunctype(addr)
90+
cfunc()
91+
92+
addr = ll.address_of_symbol("__tgt_init_all_rtls")
93+
if addr:
94+
cfunc = cfunctype(addr)
95+
cfunc()
96+
97+
except Exception as e:
10098
if DEBUG_OPENMP >= 1:
101-
print("Found OpenMP target runtime library at", omptargetlib)
102-
ll.load_library_permanently(str(omptargetlib))
103-
else:
104-
raise RuntimeError(f"OpenMP target runtime not found at {omptargetlib}")
99+
print(f"Warning: Failed to initialize OpenMP target runtime: {e}")
105100

106101

107102
_init()

src/numba/openmp/tests/test_openmp.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5051,6 +5051,7 @@ def test_impl(lb, num_steps, pi_comp_func):
50515051
njit_output = njit(test_impl)(0, 1024, test_pi_comp_njit)
50525052
self.assert_outputs_equal(py_output, njit_output)
50535053

5054+
50545055
class TestOpenmpRuntimeFunctions(TestOpenmpBase):
50555056
def __init__(self, *args):
50565057
TestOpenmpBase.__init__(self, *args)
@@ -5073,5 +5074,19 @@ def test_impl():
50735074
python_num_procs = omp_get_num_procs()
50745075
self.assertEqual(jit_num_procs, python_num_procs)
50755076

5077+
@linux_only
5078+
def test_omp_get_num_devices(self):
5079+
from numba.openmp import omp_get_num_devices
5080+
5081+
@njit
5082+
def test_impl():
5083+
return omp_get_num_devices()
5084+
5085+
jit_num_devices = test_impl()
5086+
python_num_devices = omp_get_num_devices()
5087+
self.assertEqual(jit_num_devices, python_num_devices)
5088+
self.assertGreater(jit_num_devices, 0)
5089+
5090+
50765091
if __name__ == "__main__":
50775092
unittest.main()

0 commit comments

Comments
 (0)