Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Gotchas when installing on MacOS #106

Closed
djhoese opened this issue Jan 14, 2024 · 27 comments · Fixed by #107
Closed

Gotchas when installing on MacOS #106

djhoese opened this issue Jan 14, 2024 · 27 comments · Fixed by #107

Comments

@djhoese
Copy link
Collaborator

djhoese commented Jan 14, 2024

Modern versions of pykdtree are released on PyPI as both binary wheels and a source distribution (sdist). Pykdtree includes C extensions that require compilation if installed from the sdist, but if installed from the binary wheel then no C compiler is required on the user's machine as the shared libraries (ex. .so files on linux) are bundled with the wheel. Pykdtree also optionally depends on OpenMP. If the extensions are built on a system with OpenMP installed then the extensions are linked to and therefore require OpenMP to exist at runtime. This means if pykdtree's wheels on PyPI were built with OpenMP then any user installing the wheels must also have OpenMP installed or they will get an error when importing pykdtree.

This gets complicated in the case of MacOS where the build image used in pykdtree's CI environment includes OpenMP, but it only exists because it is a dependency of one of the other tools/libraries that GitHub Actions includes (via homebrew) in their runner image. Since this isn't a "standard" package when users do pip install pykdtree and get the wheel built with OpenMP, they get an error at import time. See bjlittle/geovista#620 for one of these cases.
The workarounds are to:

  1. Install OpenMP (ex. brew install omp I think is the command - I don't have a mac)
  2. Install from source which requires a compiler (ex. pip install --force-reinstall --no-binary pykdtree pykdtree)

So our (as pykdtree maintainers) options are:

  1. Always require the user to build from source on macos (requires a compiler) by not building a macos wheel.
  2. Provide a no OpenMP version which will install, but not perform as well as it could and users might expect.
  3. Keep the OpenMP-based wheel and have some users run into runtime issues if they don't have OpenMP installed.

We could (and should) of course document all of these gotchas.

@mraspaud
Copy link
Collaborator

Is there a way we could activate openmp support at runtime? so that it's compiled with it, but if not present at runtime it issues a warning and runs without it?
Otherwise I think 3 might be the best, if we then issue an error with a link on how to recompile without openmp when openmp isn't installed.

@user27182
Copy link

In bjlittle/geovista#620 the pykdtree runtime error occurred only when importing pykdtree.kdtree. It does not occur if only pykdtree is imported. Perhaps this means that a simple try..catch block can be used in an __init__.py file somewhere to, at the very least, convert this cryptic and broad error

ImportError: dlopen(/.../venv/lib/python3.12/site-packages/pykdtree/kdtree.cpython-312-darwin.so, 0x0002): symbol not found in flat namespace '___kmpc_for_static_fini'`

into a much more helpful error message which may point the user to a sensible solution? (e.g. msg = "install with brew or install with --no-binary", or msg = "visit pykdree/#106 for details")?

This way, although the runtime error is still present, at least the user can easily try one of the two solutions hinted from the error message, and resolve this issue very quickly and easily. Perhaps this isn't ideal, but at least the user will still be able to take advantage of the performance improvements with OpenMP.

@djhoese
Copy link
Collaborator Author

djhoese commented Jan 16, 2024

I had a similar thought. I was wondering if it would be possible at build time to build both versions of the extensions. We always build the one without OpenMP, but if we detect OpenMP we build that version too and have two possible extensions to import. Then we try/except on the OpenMP extension import and warn and fallback if we have to use the non-OpenMP version. If the OpenMP extension file(s) don't even exist then we know that the build process wasn't even able to find OpenMP so there is no reason to warn in that case...I think.

@djhoese
Copy link
Collaborator Author

djhoese commented Jan 17, 2024

So I played around with my idea mentioned above with having two extensions, one compiled with OpenMP or at least attempted and one explicitly not including OpenMP. It all kind of worked except it seemed that Cython only compiled the cython module for a specific name once so when trying to import the second no-OpenMP version it gets confused by what the module name is.

Subject: [PATCH] Dual extensions with and without openmp
---
Index: setup.py
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/setup.py b/setup.py
--- a/setup.py	(revision 75804c99cc873ccc74b19e47c3a90003fd6fce4b)
+++ b/setup.py	(date 1705517886814)
@@ -20,8 +20,9 @@
 import re
 
 import numpy as np
-from Cython.Build import cythonize
-from setuptools import setup, Extension
+from Cython.Build import build_ext
+from Cython.Distutils import Extension
+from setuptools import setup
 from setuptools.command.build_ext import build_ext
 
 
@@ -56,17 +57,20 @@
         comp = self.compiler.compiler_type
         omp_comp, omp_link = _omp_compile_link_args(comp)
         if comp in ('unix', 'cygwin', 'mingw32'):
-            extra_compile_args = ['-std=c99', '-O3'] + omp_comp
+            extra_compile_args = ['-std=c99', '-O3']
             extra_link_args = omp_link
         elif comp == 'msvc':
-            extra_compile_args = ['/Ox'] + omp_comp
+            extra_compile_args = ['/Ox']
             extra_link_args = omp_link
         else:
             # Add support for more compilers here
             raise ValueError('Compiler flags undefined for %s. Please modify setup.py and add compiler flags'
                              % comp)
-        self.extensions[0].extra_compile_args = extra_compile_args
+        # Only apply OpenMP specific flags to OpenMP
+        self.extensions[0].extra_compile_args = extra_compile_args + omp_comp
         self.extensions[0].extra_link_args = extra_link_args
+        # No OpenMP version of the extension
+        self.extensions[1].extra_compile_args = extra_compile_args
         build_ext.build_extensions(self)
 
 
@@ -186,12 +190,19 @@
     readme = readme_file.read()
 
 extensions = [
-    Extension('pykdtree.kdtree', sources=['pykdtree/kdtree.pyx', 'pykdtree/_kdtree_core.c'],
+    Extension('pykdtree.kdtree_tryomp', sources=['pykdtree/kdtree.pyx', 'pykdtree/_kdtree_core.c'],
+              include_dirs=[np.get_include()],
+              define_macros=[("NPY_NO_DEPRECATED_API", "NPY_1_7_API_VERSION")],
+              cython_directives={"language_level": "3"},
+              ),
+    # NOTE: Compilation flags handled in build_ext_subclass (assumes second extension is noomp)
+    Extension('pykdtree.kdtree_noomp', sources=['pykdtree/kdtree.pyx', 'pykdtree/_kdtree_core.c'],
               include_dirs=[np.get_include()],
               define_macros=[("NPY_NO_DEPRECATED_API", "NPY_1_7_API_VERSION")],
-              compiler_directions={"language_level": "3"},
+              cython_directives={"language_level": "3"},
               ),
 ]
+
 
 setup(
     name='pykdtree',
@@ -206,7 +217,7 @@
     install_requires=['numpy'],
     tests_require=['pytest'],
     zip_safe=False,
-    ext_modules=cythonize(extensions),
+    ext_modules=extensions,
     cmdclass={'build_ext': build_ext_subclass},
     classifiers=[
       'Development Status :: 5 - Production/Stable',
Index: pykdtree/__init__.py
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/pykdtree/__init__.py b/pykdtree/__init__.py
--- a/pykdtree/__init__.py	(revision 75804c99cc873ccc74b19e47c3a90003fd6fce4b)
+++ b/pykdtree/__init__.py	(date 1705518033734)
@@ -1,0 +1,15 @@
+"""Simple wrapper around importing pykdtree for OpenMP use or not."""
+
+try:
+    from . import kdtree_tryomp as kdtree
+    raise ImportError("pykdtree")
+except ImportError:
+    import warnings
+    warnings.warn(
+        """Pykdtree failed to import its C extension. This usually means it
+was built with OpenMP (C-level parallelization library) support but could not
+find it on your system. Pykdtree will use a less performant version of the
+algorithm instead. To enable better performance OpenMP must be installed
+(ex. ``brew install omp`` on Mac with HomeBrew)."""
+    )
+    from . import kdtree_noomp as kdtree
\ No newline at end of file
$ python -c "from pykdtree import kdtree"
/home/davidh/repos/git/pykdtree/pykdtree/__init__.py:8: UserWarning: Pykdtree failed to import it's C extension. This usually means it
was built with OpenMP (C-level parellelization library) support but could not
find it on your system. Pykdtree will use a less performant version of the
algorithm instead. To enable better performance OpenMP must be installed
(ex. ``brew install omp`` on Mac with HomeBrew).
  warnings.warn(
Traceback (most recent call last):
  File "/home/davidh/repos/git/pykdtree/pykdtree/__init__.py", line 5, in <module>
    raise ImportError("pykdtree")
ImportError: pykdtree

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "<string>", line 1, in <module>
  File "/home/davidh/repos/git/pykdtree/pykdtree/__init__.py", line 15, in <module>
    from . import kdtree_noomp as kdtree
ImportError: dynamic module does not define module export function (PyInit_kdtree_noomp)

@schlegelp
Copy link

schlegelp commented Oct 8, 2024

FYI: brew install omp doesn't work - that formula does not exist.

I ended up following these instructions. Copied here for convenience:

install https://brew.sh/

Install required packages

brew install llvm libomp

in command line set variables that will be used for compilation (can be done by editing .zprofile):

export PATH="/opt/homebrew/opt/llvm/bin:$PATH"
export LDFLAGS="-L/opt/homebrew/opt/llvm/lib"
export CPPFLAGS="-I/opt/homebrew/opt/llvm/include"

The above didn't work for me. I also tried adding /opt/homebrew/opt/llvm/lib, /opt/homebrew/opt/llvm/include, /opt/homebrew/opt/libomp/include and /opt/homebrew/opt/libomp/lib to PATH but no luck.

Any ideas?

Edit: I only get the Pykdtree failed to import its C extension [...] error with 1.3.13 and e.g. 1.3.12 still works like a charm.

@schlegelp
Copy link

schlegelp commented Oct 8, 2024

  1. Always require the user to build from source on macos (requires a compiler) by not building a macos wheel.
  2. Provide a no OpenMP version which will install, but not perform as well as it could and users might expect.
  3. Keep the OpenMP-based wheel and have some users run into runtime issues if they don't have OpenMP installed.

There is a fourth option that you could consider:

  1. Make the default version for pykdtree on OSX be without OMP but put a second package - e.g. pykdtree-omp - on PyPI where all wheels are with OMP (for users that require OMP support and/or know what they are doing).

Obviously, that also works in reverse: pykdtree with OMP and e.g. pykdtree-osx-no-omp without OMP on OSX.

@djhoese
Copy link
Collaborator Author

djhoese commented Oct 8, 2024

@schlegelp

Edit: I only get the Pykdtree failed to import its C extension [...] error with 1.3.13 and e.g. 1.3.12 still works like a charm.

The CI and wheel building process had to change between these versions because GitHub deprecated their old mac runners and if I remember correctly homebrew bumped their minimum build target to only work for newer versions of MacOS. What version of MacOS are you using now?

@djhoese
Copy link
Collaborator Author

djhoese commented Oct 8, 2024

FYI: brew install omp doesn't work - that formula does not exist.

Ugh I can't believe I got that wrong. Would you mind submitting a pull request to fix the typo?

@schlegelp
Copy link

schlegelp commented Oct 8, 2024

What version of MacOS are you using now?

I'm on ARM and OSX 14.6.1 Sonoma. On my system, the wheel for 1.3.13 is pykdtree-1.3.13-cp311-cp311-macosx_12_0_arm64.whl. For 1.3.12 it was pykdtree-1.3.12-cp311-cp311-macosx_11_0_arm64.whl - so yes, the build target was bumped.

Would you mind submitting a pull request to fix the typo?

Sure, can do!

I've also had a look at the build logs for version 1.3.13 from Sept 4th and it looks like that build used libomp version 18.1.8. Unfortunately, the homebrew formula was updated to 19.1.0 on Sept 18th which is what I have on my system right now.

Installing previous versions of libomp via brew is a bit of a pain in the neck so I haven't actually tried but is it possible that is the reason it keeps failing despite my efforts to add directories to PATH? Also: is pykdtree even looking at PATH or is it expecting to find libomp in a hard-coded location? The logs mention copying files from /usr/local/Cellar/libomp/18.1.8/lib which, oddly, is not where homebrew installed to on my system (/opt/homebrew/Cellar/libomp/19.1.0/).

Sorry, I know this is a pain in the neck to debug, especially if you're not even on the same system, but it would be good to incorporate any findings into the error message, right?

@djhoese
Copy link
Collaborator Author

djhoese commented Oct 8, 2024

Hm. So I don't think you ever specified what error you are currently getting. What's the message and traceback (if there is one)?

When pykdtree is built and turned into a wheel, it builds with whatever OpenMP it can find. The pykdtree kdtree.cpython-311-darwin.so (or .dylib or whatever it is on Mac) is dynamically linked to the openmp library like any other C/C++ dynamic library. I won't pretend to completely understand what the various wheel packaging and repairing utilities are doing, but my understanding is that they do some kind of copying of the libraries used and put them into the wheel itself. But when I download the Mac wheel from PyPI and unpack it I don't see openmp anywhere.

As for PATH, pykdtree or rather python itself is using whatever dynamic library linking is available on Mac. The "hack" way of manipulating this at run time is to specify LD_LIBRARY_PATH and set it to the directory where libopenmp's .so/.dylib files are.

@schlegelp
Copy link

Sorry, should have shared it in my first post: I get the Pykdtree failed to import its C extension. exception when I use 1.3.13.

>>> from pykdtree.kdtree import kdtree
---------------------------------------------------------------------------
ImportError                               Traceback (most recent call last)
File ~/.pyenv/versions/3.11.8/lib/python3.11/site-packages/pykdtree/__init__.py:4
      3 try:
----> 4     from . import kdtree
      5 except ImportError as err:

ImportError: dlopen(/Users/philipps/.pyenv/versions/3.11.8/lib/python3.11/site-packages/pykdtree/kdtree.cpython-311-darwin.so, 0x0002): symbol not found in flat namespace '___kmpc_for_static_fini'

The above exception was the direct cause of the following exception:

ImportError                               Traceback (most recent call last)
Cell In[1], line 1
----> 1 from pykdtree.kdtree import kdtree

File ~/.pyenv/versions/3.11.8/lib/python3.11/site-packages/pykdtree/__init__.py:6
      4     from . import kdtree
      5 except ImportError as err:
----> 6     raise ImportError(
      7         "Pykdtree failed to import its C extension. This usually means it "
      8         "was built with OpenMP (C-level parallelization library) support but "
      9         "could not find it on your system. To enable better performance "
     10         "OpenMP must be installed (ex. ``brew install omp`` on Mac with "
     11         "HomeBrew). Otherwise, try installing Pykdtree from source (ex. "
     12         "``pip install --no-binary pykdtree --force-install pykdtree``)."
     13     ) from err

ImportError: Pykdtree failed to import its C extension. This usually means it was built with OpenMP (C-level parallelization library) support but could not find it on your system. To enable better performance OpenMP must be installed (ex. ``brew install omp`` on Mac with HomeBrew). Otherwise, try installing Pykdtree from source (ex. ``pip install --no-binary pykdtree --force-install pykdtree``).

I just quickly tried setting LD_LIBRARY_PATH="/opt/homebrew/Cellar/libomp/19.1.0/lib/" but that didn't fix it nor did it change the error message.

In case that's helpful:

$ brew ls --verbose libomp                                  
/opt/homebrew/Cellar/libomp/19.1.0/INSTALL_RECEIPT.json
/opt/homebrew/Cellar/libomp/19.1.0/.brew/libomp.rb
/opt/homebrew/Cellar/libomp/19.1.0/include/ompx.h
/opt/homebrew/Cellar/libomp/19.1.0/include/ompt.h
/opt/homebrew/Cellar/libomp/19.1.0/include/omp.h
/opt/homebrew/Cellar/libomp/19.1.0/include/omp-tools.h
/opt/homebrew/Cellar/libomp/19.1.0/sbom.spdx.json
/opt/homebrew/Cellar/libomp/19.1.0/lib/libomp.dylib
/opt/homebrew/Cellar/libomp/19.1.0/lib/libomp.a

@djhoese
Copy link
Collaborator Author

djhoese commented Oct 8, 2024

This must be an OpenMP version thing. It isn't complaining that it can't find libopenmp, but a specific symbol (___kmpc_for_static_fini). However, from what I can tell that symbol likely still exists in modern OpenMP (https://github.com/search?q=repo%3Allvm%2Fllvm-project%20___kmpc_for_static_fini&type=code).

A stackoverflow answer suggests you can list all symbols for a library on mac with:

nm -gU /opt/homebrew/Cellar/libomp/19.1.0/lib/libomp.dylib | grep static_fini

Does that return anything?

@schlegelp
Copy link

$ nm -gU /opt/homebrew/Cellar/libomp/19.1.0/lib/libomp.dylib | grep static_fini
00000000000111f8 T ___kmpc_for_static_fini

@rayg-ssec
Copy link
Contributor

rayg-ssec commented Oct 10, 2024

A fourth option is to copy the libomp.dylib from the build environment into the distributed binary wheel and ensure that it gets used, since libomp is not a system feature.

A good basic intro to some of the extended rpath linking on macOS is at https://itwenty.me/posts/01-understanding-rpath/

Something like this might be what's needed to pull the dependency into the wheel and situate it next to the compiled module:

Repos/git/pykdtree  master ✗ 
▶ file /opt/local/lib/libomp/libomp.dylib
/opt/local/lib/libomp/libomp.dylib: Mach-O 64-bit arm64 dynamically linked shared library, flags:<NOUNDEFS|DYLDLINK|TWOLEVEL|WEAK_DEFINES|BINDS_TO_WEAK|NO_REEXPORTED_DYLIBS|HAS_TLV_DESCRIPTORS>

Repos/git/pykdtree  master ✗ 
▶ otool -L build/lib.macosx-15.0-arm64-cpython-311/pykdtree/kdtree.cpython-311-darwin.so
build/lib.macosx-15.0-arm64-cpython-311/pykdtree/kdtree.cpython-311-darwin.so:
	/opt/local/lib/libomp/libomp.dylib (compatibility version 5.0.0, current version 5.0.0)
	/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1351.0.0)

Repos/git/pykdtree  master ✗ 
▶ cp /opt/local/lib/libomp/libomp.dylib build/lib.macosx-15.0-arm64-cpython-311/pykdtree/

Repos/git/pykdtree  master ✗ 
▶ install_name_tool -change /opt/local/lib/libomp/libomp.dylib @loader_path/libomp.dylib build/lib.macosx-15.0-arm64-cpython-311/pykdtree/kdtree.cpython-311-darwin.so  

Repos/git/pykdtree  master ✗ 
▶ otool -L build/lib.macosx-15.0-arm64-cpython-311/pykdtree/kdtree.cpython-311-darwin.so
build/lib.macosx-15.0-arm64-cpython-311/pykdtree/kdtree.cpython-311-darwin.so:
	@loader_path/libomp.dylib (compatibility version 5.0.0, current version 5.0.0)
	/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1351.0.0)

Update: I did confirm that this results in a co-resident libomp that gets used:

pykdtree/build/lib.macosx-15.0-arm64-cpython-311  master ✗ 
▶ PYTHONPATH=$PWD python3.11 
Python 3.11.10 (main, Sep  7 2024, 08:05:54) [Clang 16.0.0 (clang-1600.0.26.3)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import pykdtree
>>> ^D

pykdtree/build/lib.macosx-15.0-arm64-cpython-311  master ✗ 
▶ rm pykdtree/libomp.dylib 

pykdtree/build/lib.macosx-15.0-arm64-cpython-311  master ✗ 
▶ PYTHONPATH=$PWD python3.11 
Python 3.11.10 (main, Sep  7 2024, 08:05:54) [Clang 16.0.0 (clang-1600.0.26.3)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import pykdtree
Traceback (most recent call last):
  File "/Users/keoni/Repos/git/pykdtree/build/lib.macosx-15.0-arm64-cpython-311/pykdtree/__init__.py", line 4, in <module>
    from . import kdtree
ImportError: dlopen(/Users/keoni/Repos/git/pykdtree/build/lib.macosx-15.0-arm64-cpython-311/pykdtree/kdtree.cpython-311-darwin.so, 0x0002): Library not loaded: @loader_path/libomp.dylib
  Referenced from: <11BDB8DD-8612-3BB4-8E30-105FB8DDAF0A> /Users/keoni/Repos/git/pykdtree/build/lib.macosx-15.0-arm64-cpython-311/pykdtree/kdtree.cpython-311-darwin.so
  Reason: tried: '/Users/keoni/Repos/git/pykdtree/build/lib.macosx-15.0-arm64-cpython-311/pykdtree/libomp.dylib' (no such file)

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/keoni/Repos/git/pykdtree/build/lib.macosx-15.0-arm64-cpython-311/pykdtree/__init__.py", line 6, in <module>
    raise ImportError(
ImportError: Pykdtree failed to import its C extension. This usually means it was built with OpenMP (C-level parallelization library) support but could not find it on your system. To enable better performance OpenMP must be installed (ex. ``brew install omp`` on Mac with HomeBrew). Otherwise, try installing Pykdtree from source (ex. ``pip install --no-binary pykdtree --force-reinstall pykdtree``).

@djhoese
Copy link
Collaborator Author

djhoese commented Oct 14, 2024

I read this and thought "oh I bet I could mimic what the pyproj library does since I know that bundles the PROJ library in wheels but still prefers system PROJ if available". But it looks complicated enough that I'm scared, especially for a lower-level library like OpenMP. For reference here is pyproj's wheel building job:

https://github.com/pyproj4/pyproj/blob/39cf3c95c7b81e364fbff275a62fd44ba8113cff/.github/workflows/release.yaml#L127-L161

And the script it is calling before wheel building to build PROJ inside the source directory:

https://github.com/pyproj4/pyproj/blob/39cf3c95c7b81e364fbff275a62fd44ba8113cff/ci/proj-compile-wheels.sh

So I'm not sure I understand what would be a good next step. I guess I (only one maintainer of this package) am OK bundling OpenMP but I definitely don't have the time to figure it out. I'd also be OK doing another release of pykdtree so it builds with the newer OpenMP on homebrew and would hopefully work for more people (we assume).

Just to be sure, @rayg-ssec if you have any time, could you try installing pykdtree from PyPI with pip with old (assuming you haven't updated homebrew) and new (assuming you're OK upgrading) OpenMP from homebrew and see if you get the same errors as @schlegelp?

I wonder if the OpenMP formula/recipe in homebrew changed enough that we need to somehow get pykdtree/wheel building to be smarter about where to look and what RPATH to use to find OpenMP. Like is there a generic non-versioned path to the latest install of OpenMP that homebrew creates that we could use instead of the versioned one?

Lastly, I'm nervous that if we bundle OpenMP and pykdtree starts sourcing it over any system OpenMPs then we may break much more "important" libraries that use OpenMP (ex. numba) because pykdtree is imported first.

@rayg-ssec
Copy link
Contributor

Complication: I'm using macports, not homebrew. In its case, the paths for (currently version 18) libomp are:

▶ port contents libomp
Port libomp @18.1.8_0 contains:
  /opt/local/include/libomp/omp-tools.h
  /opt/local/include/libomp/omp.h
  /opt/local/include/libomp/ompt.h
  /opt/local/include/libomp/ompx.h
  /opt/local/lib/libomp/libgomp.dylib
  /opt/local/lib/libomp/libiomp5.dylib
  /opt/local/lib/libomp/libomp.dylib
  /opt/local/share/doc/libomp/LICENSE.TXT
  /opt/local/share/doc/libomp/README.txt

@rayg-ssec
Copy link
Contributor

rayg-ssec commented Oct 14, 2024

It looks like R project solved this by maintaining a set of libomp signed libraries intended to match the version of the system clang, to be shipped with binaries. https://mac.r-project.org/openmp/ . More recent ones of universal binaries (i.e. usable on arm64 and x86-64 builds).

@djhoese
Copy link
Collaborator Author

djhoese commented Oct 15, 2024

@schlegelp Could you run the equivalent of this command with your installed pykdtree and tell us what the output is?

otool -L <python_env>/lib/python3.11/site-packages/pykdtree/kdtree.cpython-311-darwin.so

Edit: As another check, in your code, is pykdtree the first thing you are importing? If not, can you try importing it first and see what happens.

@schlegelp
Copy link

Hi. Sorry for the radio silence - I've had a few busy days.

For these tests I always import pykdtree first.

with 1.3.13:

$ otool -L <...>/lib/python3.11/site-packages/pykdtree/kdtree.cpython-311-darwin.so
<...>/lib/python3.11/site-packages/pykdtree/kdtree.cpython-311-darwin.so:
	/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1319.0.0)

with 1.3.12:

$ otool -L <...>/lib/python3.11/site-packages/pykdtree/kdtree.cpython-311-darwin.so
<...>/lib/python3.11/site-packages/pykdtree/kdtree.cpython-311-darwin.so:
	/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1311.0.0)

@djhoese
Copy link
Collaborator Author

djhoese commented Oct 17, 2024

@schlegelp That output is a little surprising. Notice how @rayg-ssec's output mentions OpenMP and yours doesn't. @rayg-ssec I'm unfamiliar with otool output, but shouldn't it at least list openmp even if it can't find it?

@rayg-ssec
Copy link
Contributor

The otool -L lists external linkages but does not probe them the way ldd does. I confirmed this by editing my binary to point to /usr/local/lib/libomp.dylib which doesn't exist.

pykdtree/build/lib.macosx-15.0-arm64-cpython-311  master ✗ 
▶ otool -L pykdtree/kdtree.cpython-311-darwin.so
pykdtree/kdtree.cpython-311-darwin.so:
	/usr/local/lib/libomp.dylib (compatibility version 5.0.0, current version 5.0.0)
	/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1351.0.0)

pykdtree/build/lib.macosx-15.0-arm64-cpython-311  master ✗ 
▶ PYTHONPATH=$PWD python3.11 -m pykdtree.test_tree
Traceback (most recent call last):
  File "/Users/keoni/Repos/git/pykdtree/build/lib.macosx-15.0-arm64-cpython-311/pykdtree/__init__.py", line 4, in <module>
    from . import kdtree
ImportError: dlopen(/Users/keoni/Repos/git/pykdtree/build/lib.macosx-15.0-arm64-cpython-311/pykdtree/kdtree.cpython-311-darwin.so, 0x0002): Library not loaded: /usr/local/lib/libomp.dylib
---8<---

Basically it looks like the module being probed does not have any OMP dependency at all to be satisfied.

So now the question is whether there's another preferred pykdtree module somewhere in the python path that has an unsatisfied OMP dependency. One of the easiest ways to cause that problem might be via "pip install --user" adding to ~/.local , instead of using a virtualenv. I've seen too many cases of collateral damage from --user installs of modules to ever recommend using it.

@schlegelp
Copy link

Not sure if that's actually what you're suggesting @rayg-ssec but I did double check and there is no additional pykdtree install on my system.

Perhaps as another data point: if I build pykdtree myself using pip install -e ., otool give the expected result:

pykdtree/kdtree.cpython-311-darwin.so:
	/opt/homebrew/opt/libomp/lib/libomp.dylib (compatibility version 5.0.0, current version 5.0.0)
	/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1345.100.2)

After building with USE_OMP=0:

pykdtree/kdtree.cpython-311-darwin.so:
	/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1345.100.2)

@rayg-ssec
Copy link
Contributor

Given the above, now I'm wondering if the module is being built with OMP dependencies/header usage but not linked to libomp, resulting in symbol resolution problem - though that shouldn't pass a CI import test presumably?

@djhoese
Copy link
Collaborator Author

djhoese commented Oct 21, 2024

@rayg-ssec would it make sense to download any of the macos wheels here on PyPI and run otool on the pykdtree/kdtree*.so inside? Regardless of installation in an environment or not, the .so should remain the same.

@rayg-ssec
Copy link
Contributor

rayg-ssec commented Oct 22, 2024

Done using the python 3.13 wheel:

~/tmp   
▶ unzip ~/Downloads/pykdtree-1.3.13-cp313-cp313-macosx_12_0_arm64.whl 
Archive:  /Users/keoni/Downloads/pykdtree-1.3.13-cp313-cp313-macosx_12_0_arm64.whl
   creating: pykdtree/
   creating: pykdtree-1.3.13.dist-info/
  inflating: pykdtree/test_tree.py   
  inflating: pykdtree/__init__.py    
  inflating: pykdtree/kdtree.cpython-313-darwin.so  
  inflating: pykdtree/render_template.py  
  inflating: pykdtree-1.3.13.dist-info/RECORD  
  inflating: pykdtree-1.3.13.dist-info/WHEEL  
  inflating: pykdtree-1.3.13.dist-info/top_level.txt  
  inflating: pykdtree-1.3.13.dist-info/LICENSE.txt  
  inflating: pykdtree-1.3.13.dist-info/METADATA  

~/tmp   
▶ PYTHONPATH=$PWD python3.13
Python 3.13.0 (main, Oct 12 2024, 04:39:52) [Clang 16.0.0 (clang-1600.0.26.3)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import pykdtree
Traceback (most recent call last):
  File "/Users/keoni/tmp/pykdtree/__init__.py", line 4, in <module>
    from . import kdtree
ImportError: dlopen(/Users/keoni/tmp/pykdtree/kdtree.cpython-313-darwin.so, 0x0002): symbol not found in flat namespace '___kmpc_for_static_fini'

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "<python-input-0>", line 1, in <module>
    import pykdtree
  File "/Users/keoni/tmp/pykdtree/__init__.py", line 6, in <module>
    raise ImportError(
    ...<6 lines>...
    ) from err
ImportError: Pykdtree failed to import its C extension. This usually means it was built with OpenMP (C-level parallelization library) support but could not find it on your system. To enable better performance OpenMP must be installed (ex. ``brew install omp`` on Mac with HomeBrew). Otherwise, try installing Pykdtree from source (ex. ``pip install --no-binary pykdtree --force-install pykdtree``).
>>> 

~/tmp   
▶ otool -L pykdtree/kdtree.cpython-313-darwin.so 
pykdtree/kdtree.cpython-313-darwin.so:
	/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1319.0.0)

~/tmp   
▶ nm -gU pykdtree/kdtree.cpython-313-darwin.so 
0000000000005598 T _PyInit_kdtree
000000000001cb18 S ___pyx_module_is_main_pykdtree__kdtree
00000000000049e0 T _calc_dist_double
0000000000003250 T _calc_dist_float
0000000000004298 T _construct_subtree_double
0000000000002b04 T _construct_subtree_float
0000000000004458 T _construct_tree_double
0000000000002cc4 T _construct_tree_float
0000000000004410 T _create_node_double
0000000000002c7c T _create_node_float
00000000000048c8 T _delete_subtree_double
0000000000003134 T _delete_subtree_float
0000000000004904 T _delete_tree_double
0000000000003170 T _delete_tree_float
0000000000003f64 T _get_bounding_box_double
00000000000027d0 T _get_bounding_box_float
0000000000004a78 T _get_cube_offset_double
00000000000032fc T _get_cube_offset_float
0000000000004ab4 T _get_min_dist_double
0000000000003338 T _get_min_dist_float
0000000000003f08 T _insert_point_double
000000000000274c T _insert_point_float
0000000000004094 T _partition_double
0000000000002900 T _partition_float
000000000000493c T _print_tree_double
00000000000031a8 T _print_tree_float
0000000000004b1c T _search_leaf_double
0000000000004db4 T _search_leaf_double_mask
00000000000033a0 T _search_leaf_float
00000000000036b0 T _search_leaf_float_mask
000000000000505c T _search_splitnode_double
00000000000039c8 T _search_splitnode_float
0000000000005298 T _search_tree_double
0000000000003c04 T _search_tree_float

[edit: Removed some URL artifacts due to rich text pasting from my logbook]

@djhoese
Copy link
Collaborator Author

djhoese commented Oct 22, 2024

How can it be looking for/using OpenMP symbols (error message), but not be linked against it (otool)? We could try adding an import test to the CI, but I'm still not sure how this happens.

@rayg-ssec
Copy link
Contributor

The simplest way I think (read: venture a guess) this could be made to happen is if the -fopenmp option is present at compile time but absent a link time? Hopefully not something semi-arcane like an opportunistic but incomplete header-is-present-so-will-use in a layer below explicit use-OMP options.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants