scrna-matrix 0.1.6__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- scrna_matrix-0.1.6/.clang-format +5 -0
- scrna_matrix-0.1.6/.pytest_cache/README.md +8 -0
- scrna_matrix-0.1.6/CMakeLists.txt +342 -0
- scrna_matrix-0.1.6/PKG-INFO +356 -0
- scrna_matrix-0.1.6/README.md +332 -0
- scrna_matrix-0.1.6/docs/AUDIT.md +751 -0
- scrna_matrix-0.1.6/docs/BENCHMARKS.md +186 -0
- scrna_matrix-0.1.6/docs/MANUSCRIPT.tex +100 -0
- scrna_matrix-0.1.6/examples/scanpy_integration.ipynb +1006 -0
- scrna_matrix-0.1.6/examples/scrna_anndata.py +287 -0
- scrna_matrix-0.1.6/include/matrix/block_csr.hpp +438 -0
- scrna_matrix-0.1.6/include/matrix/hnsw_index.hpp +991 -0
- scrna_matrix-0.1.6/include/matrix/knn_graph.hpp +686 -0
- scrna_matrix-0.1.6/include/matrix/simd_math.hpp +874 -0
- scrna_matrix-0.1.6/pyproject.toml +140 -0
- scrna_matrix-0.1.6/python/scrna_matrix/__init__.py +62 -0
- scrna_matrix-0.1.6/src/python_bindings.cpp +737 -0
- scrna_matrix-0.1.6/tests/benchmark_1m_cells.py +213 -0
- scrna_matrix-0.1.6/tests/test_hnsw_persist.cpp +732 -0
- scrna_matrix-0.1.6/tests/test_matrix_ops.cpp +1494 -0
- scrna_matrix-0.1.6/tests/test_python_bindings.py +1130 -0
- scrna_matrix-0.1.6/tests/test_wide_accumulation.cpp +372 -0
- scrna_matrix-0.1.6/third_party/hnswlib/LICENSE +201 -0
- scrna_matrix-0.1.6/third_party/hnswlib/VERSION.txt +164 -0
- scrna_matrix-0.1.6/third_party/hnswlib/bruteforce.h +163 -0
- scrna_matrix-0.1.6/third_party/hnswlib/hnswalg.h +1536 -0
- scrna_matrix-0.1.6/third_party/hnswlib/hnswlib.h +228 -0
- scrna_matrix-0.1.6/third_party/hnswlib/space_ip.h +400 -0
- scrna_matrix-0.1.6/third_party/hnswlib/space_l2.h +324 -0
- scrna_matrix-0.1.6/third_party/hnswlib/stop_condition.h +276 -0
- scrna_matrix-0.1.6/third_party/hnswlib/visited_list_pool.h +78 -0
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# pytest cache directory #
|
|
2
|
+
|
|
3
|
+
This directory contains data from the pytest's cache plugin,
|
|
4
|
+
which provides the `--lf` and `--ff` options, as well as the `cache` fixture.
|
|
5
|
+
|
|
6
|
+
**Do not** commit this to version control.
|
|
7
|
+
|
|
8
|
+
See [the docs](https://docs.pytest.org/en/stable/how-to/cache.html) for more information.
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
cmake_minimum_required(VERSION 3.20)
|
|
2
|
+
project(scrna_matrix LANGUAGES CXX)
|
|
3
|
+
|
|
4
|
+
set(CMAKE_CXX_STANDARD 20)
|
|
5
|
+
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
|
6
|
+
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
|
7
|
+
|
|
8
|
+
if(NOT CMAKE_BUILD_TYPE)
|
|
9
|
+
set(CMAKE_BUILD_TYPE Release)
|
|
10
|
+
endif()
|
|
11
|
+
|
|
12
|
+
option(SCRNA_BUILD_TESTS "Build C++ unit tests" ON)
|
|
13
|
+
option(SCRNA_BUILD_PYTHON "Build pybind11 Python bindings" ON)
|
|
14
|
+
option(SCRNA_ENABLE_AVX512 "Enable AVX-512 code paths" OFF)
|
|
15
|
+
option(SCRNA_ENABLE_HNSW "Enable approximate k-NN via vendored hnswlib" ON)
|
|
16
|
+
option(SCRNA_HNSW_NATIVE_ISA
|
|
17
|
+
"Compile hnswlib's own AVX/AVX-512 kernels by putting ISA flags on the target. \
|
|
18
|
+
Off by default: it makes the binary require those extensions at runtime. \
|
|
19
|
+
Our ScrnaCosineSpace already reaches AVX-512 safely via target attributes." OFF)
|
|
20
|
+
|
|
21
|
+
# ---------------------------------------------------------------------------
|
|
22
|
+
# Header-only core library
|
|
23
|
+
# ---------------------------------------------------------------------------
|
|
24
|
+
add_library(scrna_matrix INTERFACE)
|
|
25
|
+
target_include_directories(scrna_matrix INTERFACE
|
|
26
|
+
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
|
|
27
|
+
$<INSTALL_INTERFACE:include>
|
|
28
|
+
)
|
|
29
|
+
target_compile_features(scrna_matrix INTERFACE cxx_std_20)
|
|
30
|
+
|
|
31
|
+
# ---------------------------------------------------------------------------
|
|
32
|
+
# SIMD / arch capability detection
|
|
33
|
+
#
|
|
34
|
+
# Deliberately NO -mavx2 / -mavx512f on the target. Applying an ISA flag to the
|
|
35
|
+
# whole interface lets the compiler emit those instructions anywhere it likes,
|
|
36
|
+
# including in code that never goes through a runtime capability check -- so the
|
|
37
|
+
# resulting binary SIGILLs on any CPU that lacks the extension. Instead the
|
|
38
|
+
# kernels in simd_math.hpp carry per-function `target` attributes and are
|
|
39
|
+
# selected at runtime, which keeps the rest of the binary at the baseline ISA.
|
|
40
|
+
#
|
|
41
|
+
# So what we probe for here is not "does the compiler accept -mavx2" but "can
|
|
42
|
+
# this compiler build a target-attributed AVX2 function", which is the mechanism
|
|
43
|
+
# actually relied upon. On non-x86 targets (arm64/Apple Silicon) both probes fail
|
|
44
|
+
# and only the scalar path is compiled.
|
|
45
|
+
# ---------------------------------------------------------------------------
|
|
46
|
+
include(CheckCXXSourceCompiles)
|
|
47
|
+
|
|
48
|
+
check_cxx_source_compiles("
|
|
49
|
+
#include <immintrin.h>
|
|
50
|
+
__attribute__((target(\"avx2,fma\")))
|
|
51
|
+
static float probe(const float* p, const int* q) {
|
|
52
|
+
__m256i vi = _mm256_loadu_si256(reinterpret_cast<const __m256i*>(q));
|
|
53
|
+
__m256 vg = _mm256_i32gather_ps(p, vi, 4);
|
|
54
|
+
return _mm256_cvtss_f32(_mm256_fmadd_ps(vg, vg, vg));
|
|
55
|
+
}
|
|
56
|
+
int main() { float a[8]={0}; int b[8]={0}; return (int)probe(a,b); }
|
|
57
|
+
" SCRNA_HAS_AVX2_TARGET_ATTR)
|
|
58
|
+
|
|
59
|
+
check_cxx_source_compiles("
|
|
60
|
+
#include <immintrin.h>
|
|
61
|
+
__attribute__((target(\"avx512f,avx512bw\")))
|
|
62
|
+
static float probe(const float* p, const int* q) {
|
|
63
|
+
__m512i vi = _mm512_loadu_si512(reinterpret_cast<const void*>(q));
|
|
64
|
+
__m512 vg = _mm512_i32gather_ps(vi, p, 4);
|
|
65
|
+
return _mm512_reduce_add_ps(vg);
|
|
66
|
+
}
|
|
67
|
+
int main() { float a[16]={0}; int b[16]={0}; return (int)probe(a,b); }
|
|
68
|
+
" SCRNA_HAS_AVX512_TARGET_ATTR)
|
|
69
|
+
|
|
70
|
+
if(SCRNA_HAS_AVX2_TARGET_ATTR)
|
|
71
|
+
target_compile_definitions(scrna_matrix INTERFACE SCRNA_HAVE_AVX2=1)
|
|
72
|
+
message(STATUS "scrna_matrix: AVX2 gather kernel compiled in (runtime-dispatched).")
|
|
73
|
+
else()
|
|
74
|
+
message(STATUS "scrna_matrix: AVX2 unavailable for this target; scalar path only.")
|
|
75
|
+
endif()
|
|
76
|
+
|
|
77
|
+
if(SCRNA_ENABLE_AVX512 AND SCRNA_HAS_AVX512_TARGET_ATTR)
|
|
78
|
+
target_compile_definitions(scrna_matrix INTERFACE SCRNA_HAVE_AVX512=1)
|
|
79
|
+
message(STATUS "scrna_matrix: AVX-512 gather kernel compiled in (runtime-dispatched).")
|
|
80
|
+
endif()
|
|
81
|
+
|
|
82
|
+
# ---------------------------------------------------------------------------
|
|
83
|
+
# hnswlib (vendored, header-only, Apache-2.0) for approximate k-NN
|
|
84
|
+
# ---------------------------------------------------------------------------
|
|
85
|
+
if(SCRNA_ENABLE_HNSW)
|
|
86
|
+
if(NOT EXISTS ${CMAKE_CURRENT_SOURCE_DIR}/third_party/hnswlib/hnswlib.h)
|
|
87
|
+
message(FATAL_ERROR "SCRNA_ENABLE_HNSW=ON but third_party/hnswlib/hnswlib.h is missing.")
|
|
88
|
+
endif()
|
|
89
|
+
# SYSTEM: hnswlib is vendored and we do not own its warnings. Without this,
|
|
90
|
+
# -Wshadow and -Wunused-parameter fire inside hnswalg.h and drown the
|
|
91
|
+
# first-party diagnostics that actually need acting on -- and make a
|
|
92
|
+
# -Werror build impossible for reasons that have nothing to do with our code.
|
|
93
|
+
target_include_directories(scrna_matrix SYSTEM INTERFACE
|
|
94
|
+
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/third_party>)
|
|
95
|
+
target_compile_definitions(scrna_matrix INTERFACE SCRNA_ENABLE_HNSW=1)
|
|
96
|
+
|
|
97
|
+
if(SCRNA_HNSW_NATIVE_ISA)
|
|
98
|
+
# hnswlib gates its AVX kernels on compile-time __AVX__/__AVX512F__ and does
|
|
99
|
+
# not use per-function target attributes, so reaching them means putting the
|
|
100
|
+
# ISA flag on the target -- which is exactly the arrangement the rest of this
|
|
101
|
+
# build avoids, because it lets the compiler emit those instructions in code
|
|
102
|
+
# that no runtime check guards.
|
|
103
|
+
if(SCRNA_HAS_AVX2_TARGET_ATTR)
|
|
104
|
+
target_compile_options(scrna_matrix INTERFACE -mavx2 -mfma)
|
|
105
|
+
endif()
|
|
106
|
+
if(SCRNA_ENABLE_AVX512 AND SCRNA_HAS_AVX512_TARGET_ATTR)
|
|
107
|
+
target_compile_options(scrna_matrix INTERFACE -mavx512f -mavx512bw)
|
|
108
|
+
endif()
|
|
109
|
+
message(WARNING
|
|
110
|
+
"SCRNA_HNSW_NATIVE_ISA=ON: the resulting binary may execute AVX2/AVX-512 "
|
|
111
|
+
"instructions without a runtime guard and will fault on CPUs lacking them. "
|
|
112
|
+
"Only use this when the build and target CPUs are known to match.")
|
|
113
|
+
else()
|
|
114
|
+
message(STATUS
|
|
115
|
+
"scrna_matrix: hnswlib enabled; its distances run through ScrnaCosineSpace "
|
|
116
|
+
"(runtime-dispatched AVX2/AVX-512). hnswlib's own kernels stay at baseline ISA.")
|
|
117
|
+
endif()
|
|
118
|
+
endif()
|
|
119
|
+
|
|
120
|
+
# ---------------------------------------------------------------------------
|
|
121
|
+
# OpenMP -- MANDATORY.
|
|
122
|
+
#
|
|
123
|
+
# This was `find_package(OpenMP QUIET)` with a `message(WARNING)` fallback, and
|
|
124
|
+
# that fallback was actively harmful. Without OpenMP, `_OPENMP` is undefined and
|
|
125
|
+
# every `#pragma omp` in knn_graph.hpp is ignored by the preprocessor, so the
|
|
126
|
+
# library still builds, still passes its whole test suite, and still reports
|
|
127
|
+
# "100% tests passed" -- while never executing a single parallel iteration.
|
|
128
|
+
#
|
|
129
|
+
# That is exactly what had happened here: the shipped configuration on the
|
|
130
|
+
# development host had OpenMP_CXX_FLAGS=NOTFOUND, so the concurrency paths
|
|
131
|
+
# (parallel_for_checked, the per-thread ThreadState indexing, the atomics
|
|
132
|
+
# ordering in the error latch) were dead code that CI was silently signing off
|
|
133
|
+
# on. A warning in a thousand-line configure log is not a control.
|
|
134
|
+
#
|
|
135
|
+
# So it is REQUIRED, and configuration fails on a host that cannot supply it.
|
|
136
|
+
# The single-threaded build is not a supported configuration: this library's
|
|
137
|
+
# stated purpose is k-NN over 1M+ cells, where losing the parallel loop is a
|
|
138
|
+
# correctness-of-benchmark and time-to-result problem, not a minor degradation.
|
|
139
|
+
#
|
|
140
|
+
# There is deliberately NO opt-out variable. If you need to build without
|
|
141
|
+
# OpenMP, that is a decision to make explicitly by editing this line, so it
|
|
142
|
+
# shows up in review and in `git blame`, rather than by setting a flag that
|
|
143
|
+
# silently reintroduces untested single-threaded stubs.
|
|
144
|
+
#
|
|
145
|
+
# Toolchain notes (this is the common failure and the message must fix it, not
|
|
146
|
+
# merely report it):
|
|
147
|
+
# * Apple Clang ships no OpenMP runtime. -> brew install libomp (auto-wired below)
|
|
148
|
+
# * Homebrew GCC has it built in. -> -DCMAKE_CXX_COMPILER=g++-15
|
|
149
|
+
# * Debian/Ubuntu Clang. -> apt install libomp-dev
|
|
150
|
+
|
|
151
|
+
# Resolve the toolchain before searching. On macOS + Apple Clang this locates
|
|
152
|
+
# Homebrew's keg-only libomp and hands FindOpenMP the flags it cannot derive
|
|
153
|
+
# itself; everywhere else it is a no-op. See cmake/PtoOpenMP.cmake -- it
|
|
154
|
+
# makes the requirement satisfiable without relaxing it.
|
|
155
|
+
include("${CMAKE_CURRENT_LIST_DIR}/../../cmake/PtoOpenMP.cmake" OPTIONAL
|
|
156
|
+
RESULT_VARIABLE _scrna_omp_helper)
|
|
157
|
+
if(_scrna_omp_helper)
|
|
158
|
+
pto_openmp_apply_hints()
|
|
159
|
+
endif()
|
|
160
|
+
|
|
161
|
+
find_package(OpenMP QUIET COMPONENTS CXX)
|
|
162
|
+
if(NOT OpenMP_CXX_FOUND)
|
|
163
|
+
if(COMMAND pto_openmp_failure_message)
|
|
164
|
+
pto_openmp_failure_message(_scrna_omp_help)
|
|
165
|
+
message(FATAL_ERROR "\n${_scrna_omp_help}\n")
|
|
166
|
+
endif()
|
|
167
|
+
message(FATAL_ERROR
|
|
168
|
+
"OpenMP is REQUIRED for scrna_matrix and was not found. "
|
|
169
|
+
"macOS: brew install libomp. Debian/Ubuntu: apt-get install libomp-dev.")
|
|
170
|
+
endif()
|
|
171
|
+
|
|
172
|
+
if(NOT TARGET OpenMP::OpenMP_CXX)
|
|
173
|
+
message(FATAL_ERROR
|
|
174
|
+
"OpenMP was reported as found but the OpenMP::OpenMP_CXX imported target "
|
|
175
|
+
"does not exist. Refusing to continue: linking would silently drop the "
|
|
176
|
+
"runtime and every '#pragma omp' would compile to serial code.")
|
|
177
|
+
endif()
|
|
178
|
+
|
|
179
|
+
target_link_libraries(scrna_matrix INTERFACE OpenMP::OpenMP_CXX)
|
|
180
|
+
|
|
181
|
+
# `find_package(OpenMP)` succeeding is necessary but not sufficient: it proves
|
|
182
|
+
# the compiler accepts the flag, not that `_OPENMP` is actually defined when
|
|
183
|
+
# compiling this target with these flags. Prove the macro end-to-end, because
|
|
184
|
+
# `_OPENMP` is the thing every parallel region in this library is guarded on.
|
|
185
|
+
include(CheckCXXSourceCompiles)
|
|
186
|
+
include(CMakePushCheckState)
|
|
187
|
+
cmake_push_check_state(RESET)
|
|
188
|
+
set(CMAKE_REQUIRED_FLAGS "${OpenMP_CXX_FLAGS}")
|
|
189
|
+
set(CMAKE_REQUIRED_LIBRARIES OpenMP::OpenMP_CXX)
|
|
190
|
+
check_cxx_source_compiles("
|
|
191
|
+
#include <omp.h>
|
|
192
|
+
#ifndef _OPENMP
|
|
193
|
+
#error _OPENMP is not defined; parallel regions would compile to serial code
|
|
194
|
+
#endif
|
|
195
|
+
int main() {
|
|
196
|
+
int n = 0;
|
|
197
|
+
#pragma omp parallel reduction(+ : n)
|
|
198
|
+
{ n += 1; }
|
|
199
|
+
return n > 0 ? 0 : 1;
|
|
200
|
+
}" SCRNA_OPENMP_PRAGMAS_ACTIVE)
|
|
201
|
+
cmake_pop_check_state()
|
|
202
|
+
|
|
203
|
+
if(NOT SCRNA_OPENMP_PRAGMAS_ACTIVE)
|
|
204
|
+
message(FATAL_ERROR
|
|
205
|
+
"OpenMP::OpenMP_CXX was found, but a translation unit compiled with "
|
|
206
|
+
"OpenMP_CXX_FLAGS ('${OpenMP_CXX_FLAGS}') either does not define _OPENMP or "
|
|
207
|
+
"cannot compile a '#pragma omp parallel' region.\n"
|
|
208
|
+
"Building in this state would produce a library whose k-NN construction is "
|
|
209
|
+
"silently single-threaded and whose concurrency code is never executed. "
|
|
210
|
+
"Refusing.\n"
|
|
211
|
+
" Apple Clang : brew install libomp\n"
|
|
212
|
+
" Homebrew GCC: cmake -DCMAKE_CXX_COMPILER=g++-15 ...\n"
|
|
213
|
+
" Debian/Ubuntu Clang: apt-get install libomp-dev")
|
|
214
|
+
endif()
|
|
215
|
+
|
|
216
|
+
message(STATUS " OpenMP : ${OpenMP_CXX_FLAGS} (pragmas verified active)")
|
|
217
|
+
|
|
218
|
+
# ---------------------------------------------------------------------------
|
|
219
|
+
# Eigen3 (optional; linked if present)
|
|
220
|
+
#
|
|
221
|
+
# Nothing in include/matrix currently uses Eigen -- the scalar fallbacks are
|
|
222
|
+
# plain C++ and have no third-party dependency. This stays as an opt-in hook for
|
|
223
|
+
# future dense-side work; its absence disables nothing.
|
|
224
|
+
# ---------------------------------------------------------------------------
|
|
225
|
+
find_package(Eigen3 QUIET NO_MODULE)
|
|
226
|
+
if(TARGET Eigen3::Eigen)
|
|
227
|
+
target_link_libraries(scrna_matrix INTERFACE Eigen3::Eigen)
|
|
228
|
+
endif()
|
|
229
|
+
|
|
230
|
+
# ---------------------------------------------------------------------------
|
|
231
|
+
# Tests
|
|
232
|
+
# ---------------------------------------------------------------------------
|
|
233
|
+
if(SCRNA_BUILD_TESTS)
|
|
234
|
+
enable_testing()
|
|
235
|
+
add_executable(test_matrix_ops tests/test_matrix_ops.cpp)
|
|
236
|
+
target_link_libraries(test_matrix_ops PRIVATE scrna_matrix)
|
|
237
|
+
add_test(NAME test_matrix_ops COMMAND test_matrix_ops)
|
|
238
|
+
|
|
239
|
+
# Wide accumulation (docs/AUDIT.md S2, REVIEW finding 5 + its residual, T5).
|
|
240
|
+
# The last case in this binary scans the module's own sources for a bare
|
|
241
|
+
# `float` accumulator, so it needs to know where they are. Passed as a
|
|
242
|
+
# definition rather than assumed relative to the cwd: ctest runs from the
|
|
243
|
+
# build tree, and a guard that silently finds no files reads as a pass.
|
|
244
|
+
add_executable(test_wide_accumulation tests/test_wide_accumulation.cpp)
|
|
245
|
+
target_link_libraries(test_wide_accumulation PRIVATE scrna_matrix)
|
|
246
|
+
target_compile_definitions(test_wide_accumulation
|
|
247
|
+
PRIVATE SCRNA_SOURCE_DIR="${CMAKE_CURRENT_SOURCE_DIR}")
|
|
248
|
+
add_test(NAME test_wide_accumulation COMMAND test_wide_accumulation)
|
|
249
|
+
|
|
250
|
+
# Index serialization. Built unconditionally; the translation unit compiles
|
|
251
|
+
# to a no-op main() when SCRNA_ENABLE_HNSW is off, so the test list does not
|
|
252
|
+
# change shape with the option.
|
|
253
|
+
add_executable(test_hnsw_persist tests/test_hnsw_persist.cpp)
|
|
254
|
+
target_link_libraries(test_hnsw_persist PRIVATE scrna_matrix)
|
|
255
|
+
add_test(NAME test_hnsw_persist COMMAND test_hnsw_persist)
|
|
256
|
+
endif()
|
|
257
|
+
|
|
258
|
+
# ---------------------------------------------------------------------------
|
|
259
|
+
# Python bindings (pybind11)
|
|
260
|
+
# ---------------------------------------------------------------------------
|
|
261
|
+
if(SCRNA_BUILD_PYTHON)
|
|
262
|
+
find_package(pybind11 CONFIG QUIET)
|
|
263
|
+
if(pybind11_FOUND)
|
|
264
|
+
pybind11_add_module(scrna_matrix_py src/python_bindings.cpp)
|
|
265
|
+
target_link_libraries(scrna_matrix_py PRIVATE scrna_matrix)
|
|
266
|
+
|
|
267
|
+
# Wheel packaging. scikit-build-core collects whatever CMake installs and
|
|
268
|
+
# lays it into the wheel, so the extension goes *inside* the Python package
|
|
269
|
+
# directory rather than at the wheel root -- python/scrna_matrix/__init__.py
|
|
270
|
+
# re-exports it from there. Harmless for an ordinary `cmake --install`.
|
|
271
|
+
install(TARGETS scrna_matrix_py
|
|
272
|
+
LIBRARY DESTINATION scrna_matrix
|
|
273
|
+
RUNTIME DESTINATION scrna_matrix)
|
|
274
|
+
|
|
275
|
+
# pybind11 spells this PYTHON_EXECUTABLE; FindPython spells it Python_EXECUTABLE.
|
|
276
|
+
set(_scrna_python "${PYTHON_EXECUTABLE}")
|
|
277
|
+
if(NOT _scrna_python)
|
|
278
|
+
set(_scrna_python "${Python_EXECUTABLE}")
|
|
279
|
+
endif()
|
|
280
|
+
|
|
281
|
+
# -------------------------------------------------------------------------
|
|
282
|
+
# De-duplicate the OpenMP runtime the extension loads (macOS only).
|
|
283
|
+
#
|
|
284
|
+
# scrna_matrix links OpenMP::OpenMP_CXX, which on Apple Clang resolves to
|
|
285
|
+
# Homebrew's keg-only libomp (PtoOpenMP.cmake, above). That is correct for
|
|
286
|
+
# the plain C++ build. But when this extension is imported into a Python
|
|
287
|
+
# whose numpy/scipy already loaded their OWN OpenMP runtime -- e.g. any
|
|
288
|
+
# conda-forge interpreter, since conda-forge's numpy/scipy are built against
|
|
289
|
+
# conda-forge's own `llvm-openmp` package -- dyld ends up with two DIFFERENT
|
|
290
|
+
# libomp.dylib images resident in one process. Both are the same upstream
|
|
291
|
+
# LLVM OpenMP runtime (verified: identical compatibility version 5.0.0,
|
|
292
|
+
# identical __kmpc_* export set), so they are ABI-compatible, but the
|
|
293
|
+
# runtime's own duplicate-initialization guard treats a second distinct
|
|
294
|
+
# image as unsafe and aborts the process with "OMP: Error #15". The
|
|
295
|
+
# unsafe workaround is KMP_DUPLICATE_LIB_OK=TRUE; the real fix is to never
|
|
296
|
+
# load two copies in the first place.
|
|
297
|
+
#
|
|
298
|
+
# So: if the target Python's own environment ships a libomp.dylib distinct
|
|
299
|
+
# from the one scrna_matrix compiled against, repoint the BUILT extension's
|
|
300
|
+
# load command at that one instead, post-link. Only the .dylib reference
|
|
301
|
+
# changes -- compilation still uses Homebrew's headers/flags, which is safe
|
|
302
|
+
# because the ABI is the same runtime. This is scoped to scrna_matrix_py
|
|
303
|
+
# only: the core library and its C++ test suite are untouched and keep
|
|
304
|
+
# using Homebrew's libomp exactly as before.
|
|
305
|
+
if(APPLE AND _scrna_python AND PTO_LIBOMP_PREFIX)
|
|
306
|
+
execute_process(
|
|
307
|
+
COMMAND "${_scrna_python}" -c "import sys; print(sys.prefix)"
|
|
308
|
+
OUTPUT_VARIABLE _scrna_py_prefix
|
|
309
|
+
OUTPUT_STRIP_TRAILING_WHITESPACE
|
|
310
|
+
ERROR_QUIET)
|
|
311
|
+
set(_scrna_py_libomp "${_scrna_py_prefix}/lib/libomp.dylib")
|
|
312
|
+
set(_scrna_built_libomp "${PTO_LIBOMP_PREFIX}/lib/libomp.dylib")
|
|
313
|
+
if(_scrna_py_prefix AND EXISTS "${_scrna_py_libomp}"
|
|
314
|
+
AND NOT "${_scrna_py_libomp}" STREQUAL "${_scrna_built_libomp}")
|
|
315
|
+
find_program(_scrna_install_name_tool install_name_tool)
|
|
316
|
+
if(_scrna_install_name_tool)
|
|
317
|
+
add_custom_command(TARGET scrna_matrix_py POST_BUILD
|
|
318
|
+
COMMAND "${_scrna_install_name_tool}" -change
|
|
319
|
+
"${_scrna_built_libomp}" "${_scrna_py_libomp}"
|
|
320
|
+
"$<TARGET_FILE:scrna_matrix_py>"
|
|
321
|
+
COMMENT "scrna_matrix_py: repointing OpenMP runtime at ${_scrna_py_libomp} (the target Python's own copy) to avoid a duplicate-runtime abort")
|
|
322
|
+
message(STATUS
|
|
323
|
+
"scrna_matrix_py: will load OpenMP from ${_scrna_py_libomp} at import "
|
|
324
|
+
"time instead of ${_scrna_built_libomp}, matching the runtime "
|
|
325
|
+
"numpy/scipy already initialize in this interpreter.")
|
|
326
|
+
endif()
|
|
327
|
+
endif()
|
|
328
|
+
endif()
|
|
329
|
+
|
|
330
|
+
# Binding-level tests (adoption vs copy, buffer lifetime, GIL release).
|
|
331
|
+
# Skips itself if pytest/numpy/scipy are unavailable.
|
|
332
|
+
if(SCRNA_BUILD_TESTS AND _scrna_python)
|
|
333
|
+
add_test(NAME test_python_bindings
|
|
334
|
+
COMMAND ${_scrna_python} -m pytest
|
|
335
|
+
${CMAKE_CURRENT_SOURCE_DIR}/tests/test_python_bindings.py -q)
|
|
336
|
+
set_tests_properties(test_python_bindings PROPERTIES
|
|
337
|
+
ENVIRONMENT "PYTHONPATH=$<TARGET_FILE_DIR:scrna_matrix_py>")
|
|
338
|
+
endif()
|
|
339
|
+
else()
|
|
340
|
+
message(WARNING "pybind11 not found (pip install pybind11 or add as submodule); skipping Python module.")
|
|
341
|
+
endif()
|
|
342
|
+
endif()
|