frame-analytics 0.2.0__py3-none-win_amd64.whl

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.
@@ -0,0 +1,80 @@
1
+ """frame_analytics -- fast MSE / PSNR / SSIM for PyTorch (CPU + CUDA).
2
+
3
+ >>> import torch, frame_analytics as fa
4
+ >>> a = torch.randint(0, 256, (8, 3, 1080, 1920), dtype=torch.uint8, device="cuda")
5
+ >>> b = (a.float() + torch.randn_like(a, dtype=torch.float32) * 4).clamp(0, 255).byte()
6
+ >>> fa.psnr(a, b), fa.ssim(a, b)
7
+
8
+ Three layers, all producing the same numbers:
9
+
10
+ ``frame_analytics.reference``
11
+ float64 transcription of Wang et al. 2004 / ``ssim_index.m``. Ground truth.
12
+ ``frame_analytics.functional``
13
+ Portable PyTorch. Separable window, five planes packed into one
14
+ convolution, ``torch.compile``d epilogue.
15
+ ``frame_analytics.backend``
16
+ Optional C++/CUDA extension. Single fused kernel, zero intermediates.
17
+ Used automatically when it builds; skipped silently when it does not.
18
+ """
19
+
20
+ from .functional import ( # noqa: F401
21
+ MS_SSIM_WEIGHTS,
22
+ charbonnier,
23
+ gaussian_window_1d,
24
+ gms,
25
+ gmsd,
26
+ huber,
27
+ l1,
28
+ ms_ssim,
29
+ mse,
30
+ psnr,
31
+ rgb_to_luma,
32
+ set_compile_enabled,
33
+ ssim,
34
+ )
35
+ from .modules import ( # noqa: F401
36
+ GMSD,
37
+ MSE,
38
+ PSNR,
39
+ SSIM,
40
+ Charbonnier,
41
+ Huber,
42
+ L1,
43
+ MSSSIM,
44
+ StreamingMetrics,
45
+ )
46
+
47
+ __version__ = "0.2.0"
48
+
49
+ __all__ = [
50
+ "mse",
51
+ "psnr",
52
+ "ssim",
53
+ "ms_ssim",
54
+ "gmsd",
55
+ "gms",
56
+ "l1",
57
+ "charbonnier",
58
+ "huber",
59
+ "rgb_to_luma",
60
+ "MSE",
61
+ "PSNR",
62
+ "SSIM",
63
+ "MSSSIM",
64
+ "GMSD",
65
+ "L1",
66
+ "Charbonnier",
67
+ "Huber",
68
+ "StreamingMetrics",
69
+ "gaussian_window_1d",
70
+ "set_compile_enabled",
71
+ "backend_status",
72
+ "MS_SSIM_WEIGHTS",
73
+ ]
74
+
75
+
76
+ def backend_status() -> dict:
77
+ """Report whether the native extension loaded, and why not if it did not."""
78
+ from . import backend
79
+
80
+ return backend.status()
@@ -0,0 +1,530 @@
1
+ """Locate, build and bind the C-ABI kernel libraries.
2
+
3
+ Three things live here and nothing else does:
4
+
5
+ * **finding a prebuilt binary.** Wheels ship ``frame_analytics/lib/`` with the
6
+ shared libraries already compiled. Because the boundary in ``fa_abi.h``
7
+ mentions neither libtorch nor Python, one such binary per platform is valid
8
+ for every Python version and every torch version -- which is the only reason
9
+ prebuilt kernels can be published on PyPI at all.
10
+ * **building one on demand.** A source-only install (or a platform CI never
11
+ built for) compiles the same sources with the host's ``cl``/``c++``/``nvcc``
12
+ and caches the result. This is the old JIT path, minus torch's extension
13
+ machinery: there is no ABI to match any more, so it needs no torch headers.
14
+ * **binding.** ``ctypes`` prototypes, so a wrong argument is a Python
15
+ ``TypeError`` rather than a corrupted stack.
16
+
17
+ The CPU library is compiled twice, at the platform baseline and with AVX2, and
18
+ the baseline build -- which is safe to load anywhere -- is asked at runtime
19
+ which one the host can actually run. Shipping a single ``/arch:AVX2`` binary
20
+ would fault on pre-Haswell hardware; shipping only a baseline one would give up
21
+ the vector width the reduction loops exist for.
22
+
23
+ CUDA lives in its own library, loaded only when torch reports a CUDA device.
24
+ On macOS it is never built, never shipped and never referenced.
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ import ctypes
30
+ import hashlib
31
+ import os
32
+ import platform
33
+ import shutil
34
+ import subprocess
35
+ import sys
36
+ import tempfile
37
+ from pathlib import Path
38
+ from typing import List, Optional, Sequence
39
+
40
+ _PKG = Path(__file__).resolve().parent
41
+ _CSRC = _PKG / "csrc"
42
+ _LIBDIR = _PKG / "lib"
43
+
44
+ FA_ABI_VERSION = 1
45
+
46
+ # Mirrors the codes in fa_abi.h.
47
+ FA_OK = 0
48
+ _STATUS_TEXT = {
49
+ 0: "ok",
50
+ 1: "unsupported dtype",
51
+ 2: "unsupported shape",
52
+ 3: "unsupported window size",
53
+ 4: "unknown pixel op",
54
+ 5: "invalid argument",
55
+ 6: "internal error",
56
+ 7: "no CUDA device",
57
+ }
58
+
59
+
60
+ class KernelError(RuntimeError):
61
+ """A kernel entry point returned a non-zero status."""
62
+
63
+
64
+ def status_text(code: int, cuda_lib=None) -> str:
65
+ if code < 0:
66
+ if cuda_lib is not None:
67
+ try:
68
+ s = cuda_lib.fa_cuda_error_string(code)
69
+ if s:
70
+ return s.decode("utf-8", "replace")
71
+ except Exception:
72
+ pass
73
+ return f"CUDA runtime error {-code}"
74
+ return _STATUS_TEXT.get(code, f"status {code}")
75
+
76
+
77
+ # --------------------------------------------------------------------------- #
78
+ # host compiler discovery (Windows)
79
+ # --------------------------------------------------------------------------- #
80
+
81
+ _msvc_done = False
82
+
83
+
84
+ def ensure_host_compiler() -> None:
85
+ """Idempotent wrapper -- also used by the ``torch.compile`` CPU backend,
86
+ which needs ``cl.exe`` on PATH to probe for AVX support."""
87
+ global _msvc_done
88
+ if _msvc_done:
89
+ return
90
+ _msvc_done = True
91
+ try:
92
+ _ensure_msvc_env()
93
+ except Exception:
94
+ pass
95
+
96
+
97
+ def _ensure_msvc_env() -> None:
98
+ """Import a Visual Studio build environment into ``os.environ``.
99
+
100
+ Building anything here shells out to ``cl.exe`` and ``nvcc`` and expects
101
+ both on PATH. On a machine where VS was installed but no developer prompt
102
+ is active, we source ``vcvars64.bat`` ourselves.
103
+ """
104
+ if sys.platform != "win32":
105
+ return
106
+ if shutil.which("cl") is not None:
107
+ return
108
+
109
+ candidates: List[Path] = []
110
+ vswhere = Path(os.environ.get("ProgramFiles(x86)", r"C:\Program Files (x86)")) / \
111
+ "Microsoft Visual Studio" / "Installer" / "vswhere.exe"
112
+ if vswhere.exists():
113
+ try:
114
+ out = subprocess.run(
115
+ [str(vswhere), "-latest", "-products", "*",
116
+ "-requires", "Microsoft.VisualStudio.Component.VC.Tools.x86.x64",
117
+ "-property", "installationPath"],
118
+ capture_output=True, text=True, timeout=30,
119
+ ).stdout.strip()
120
+ if out:
121
+ candidates.append(Path(out) / "VC" / "Auxiliary" / "Build" / "vcvars64.bat")
122
+ except Exception:
123
+ pass
124
+ for root in (r"C:\Program Files\Microsoft Visual Studio",
125
+ r"C:\Program Files (x86)\Microsoft Visual Studio"):
126
+ p = Path(root)
127
+ if p.exists():
128
+ candidates.extend(sorted(p.glob("*/*/VC/Auxiliary/Build/vcvars64.bat"), reverse=True))
129
+
130
+ for vc in candidates:
131
+ if not vc.exists():
132
+ continue
133
+ try:
134
+ res = subprocess.run(f'"{vc}" >nul 2>&1 && set', shell=True,
135
+ capture_output=True, text=True, timeout=120)
136
+ except Exception:
137
+ continue
138
+ if res.returncode != 0:
139
+ continue
140
+ for line in res.stdout.splitlines():
141
+ if "=" in line:
142
+ k, v = line.split("=", 1)
143
+ os.environ[k] = v
144
+ if shutil.which("cl") is not None:
145
+ return
146
+
147
+
148
+ # --------------------------------------------------------------------------- #
149
+ # naming / paths
150
+ # --------------------------------------------------------------------------- #
151
+
152
+
153
+ def library_filename(stem: str) -> str:
154
+ if sys.platform == "win32":
155
+ return f"{stem}.dll"
156
+ if sys.platform == "darwin":
157
+ return f"lib{stem}.dylib"
158
+ return f"lib{stem}.so"
159
+
160
+
161
+ def _is_x86() -> bool:
162
+ return platform.machine().lower() in (
163
+ "x86_64", "amd64", "x64", "i386", "i686", "x86",
164
+ )
165
+
166
+
167
+ def build_root() -> Path:
168
+ env = os.environ.get("FA_BUILD_DIR")
169
+ if env:
170
+ return Path(env)
171
+ try:
172
+ from torch.utils.cpp_extension import get_default_build_root
173
+
174
+ return Path(get_default_build_root()) / "frame_analytics_cabi"
175
+ except Exception:
176
+ return Path(tempfile.gettempdir()) / "frame_analytics_cabi"
177
+
178
+
179
+ def _verbose() -> bool:
180
+ return os.environ.get("FA_VERBOSE", "0") == "1"
181
+
182
+
183
+ # --------------------------------------------------------------------------- #
184
+ # building
185
+ # --------------------------------------------------------------------------- #
186
+
187
+
188
+ def _cuda_home() -> Optional[Path]:
189
+ for var in ("CUDA_HOME", "CUDA_PATH"):
190
+ v = os.environ.get(var)
191
+ if v and Path(v).exists():
192
+ return Path(v)
193
+ try:
194
+ from torch.utils.cpp_extension import CUDA_HOME
195
+
196
+ if CUDA_HOME:
197
+ return Path(CUDA_HOME)
198
+ except Exception:
199
+ pass
200
+ nvcc = shutil.which("nvcc")
201
+ if nvcc:
202
+ return Path(nvcc).resolve().parent.parent
203
+ return None
204
+
205
+
206
+ def _nvcc() -> Optional[str]:
207
+ home = _cuda_home()
208
+ if home is not None:
209
+ cand = home / "bin" / ("nvcc.exe" if sys.platform == "win32" else "nvcc")
210
+ if cand.exists():
211
+ return str(cand)
212
+ return shutil.which("nvcc")
213
+
214
+
215
+ def cuda_arch_flags() -> List[str]:
216
+ """Architectures to build for.
217
+
218
+ A local build targets the installed GPU, which halves compile time and is
219
+ all a JIT build could ever need. ``FA_CUDA_ARCHS`` overrides it -- CI sets
220
+ a list plus a trailing PTX target so one binary keeps working on hardware
221
+ newer than the toolkit that built it.
222
+ """
223
+ env = os.environ.get("FA_CUDA_ARCHS")
224
+ if env:
225
+ flags: List[str] = []
226
+ for spec in env.replace(",", " ").split():
227
+ spec = spec.strip()
228
+ if not spec:
229
+ continue
230
+ if spec.endswith("+PTX"):
231
+ num = spec[:-4].replace(".", "")
232
+ flags.append(f"-gencode=arch=compute_{num},code=compute_{num}")
233
+ else:
234
+ num = spec.replace(".", "")
235
+ flags.append(f"-gencode=arch=compute_{num},code=sm_{num}")
236
+ return flags
237
+ try:
238
+ import torch
239
+
240
+ if torch.cuda.is_available():
241
+ major, minor = torch.cuda.get_device_capability()
242
+ return [f"-gencode=arch=compute_{major}{minor},code=sm_{major}{minor}"]
243
+ except Exception:
244
+ pass
245
+ return []
246
+
247
+
248
+ def cpu_compile_command(source: Path, out: Path, avx2: bool,
249
+ objdir: Path) -> List[str]:
250
+ if sys.platform == "win32":
251
+ # /MT, not /MD: the library allocates nothing that crosses the boundary
252
+ # (every buffer is the caller's), so a private static CRT is safe -- and
253
+ # it drops the VC++ redistributable from the list of things a user has
254
+ # to have installed for a downloaded wheel to import.
255
+ cmd = ["cl", "/nologo", "/LD", "/O2", "/fp:fast", "/EHsc", "/std:c++17",
256
+ "/Zc:preprocessor", "/DNDEBUG", "/MT"]
257
+ if avx2:
258
+ cmd.append("/arch:AVX2")
259
+ cmd += [str(source), f"/Fo{objdir}{os.sep}", f"/Fe{out}"]
260
+ return cmd
261
+ cxx = os.environ.get("CXX") or "c++"
262
+ cmd = [cxx, "-O3", "-ffast-math", "-std=c++17", "-fPIC", "-pthread",
263
+ "-fvisibility=hidden", "-DNDEBUG"]
264
+ if avx2:
265
+ cmd += ["-mavx2", "-mfma"]
266
+ if sys.platform == "darwin":
267
+ # FA_MACOS_ARCHS lets CI emit one universal2 binary from a single
268
+ # runner, which matters because the Intel macOS runners are being
269
+ # retired and queue for hours. Unset (an ordinary build on a user's
270
+ # Mac) means native, as it should. AVX2 only exists on x86_64, so that
271
+ # sibling is never fat.
272
+ archs = ["x86_64"] if avx2 else os.environ.get("FA_MACOS_ARCHS", "").split()
273
+ for arch in archs:
274
+ cmd += ["-arch", arch]
275
+ cmd.append("-dynamiclib")
276
+ else:
277
+ # A released Linux wheel has to run against whatever libstdc++ the
278
+ # user's distro shipped, and this library exports no C++ types -- so
279
+ # linking the C++ runtime in statically removes the only ABI question
280
+ # left and leaves glibc as the sole dependency.
281
+ cmd += ["-shared", "-static-libstdc++", "-static-libgcc"]
282
+ cmd += [str(source), "-o", str(out)]
283
+ return cmd
284
+
285
+
286
+ def cuda_compile_command(source: Path, out: Path,
287
+ archs: Optional[Sequence[str]] = None) -> List[str]:
288
+ nvcc = _nvcc()
289
+ if nvcc is None:
290
+ raise FileNotFoundError("nvcc not found")
291
+ # Deliberately *not* --use_fast_math: it swaps in approximate division, and
292
+ # the final num/den is where SSIM's accuracy lives. The kernel is
293
+ # bandwidth-bound anyway, so IEEE division costs nothing measurable.
294
+ #
295
+ # -cudart static is what makes the result self-contained: the CUDA runtime
296
+ # is linked in and the driver is opened lazily, so the binary imports
297
+ # nothing but the platform C runtime.
298
+ cmd = [nvcc, "-O3", "-shared", "-cudart", "static", "-std=c++17",
299
+ "--expt-relaxed-constexpr", "-prec-div=true", "-prec-sqrt=true",
300
+ "-ftz=false", "-DNDEBUG"]
301
+ # Every CUDA release hard-refuses host compilers newer than the ones it was
302
+ # tested against -- CUDA 12.6 stops at gcc 13, and a current distro (or the
303
+ # manylinux_2_28 image) ships 14 or later. `FA_NVCC_CCBIN` points nvcc at an
304
+ # older g++ that is installed alongside; overriding the check with
305
+ # -allow-unsupported-compiler instead would trade a build error for a
306
+ # runtime one.
307
+ ccbin = os.environ.get("FA_NVCC_CCBIN")
308
+ if ccbin:
309
+ cmd += ["-ccbin", ccbin]
310
+ cmd += list(archs if archs is not None else cuda_arch_flags())
311
+ if sys.platform == "win32":
312
+ # CUDA 13's CCCL headers refuse to build against MSVC's traditional
313
+ # preprocessor; /wd4819 silences the codepage warning on non-UTF8 hosts.
314
+ # /MT matches the statically linked cudart, which is built against the
315
+ # static CRT; /MD here produces an LNK4098 and two runtimes in one DLL.
316
+ cmd += ["-Xcompiler", "/Zc:preprocessor", "-Xcompiler", "/wd4819",
317
+ "-Xcompiler", "/MT"]
318
+ else:
319
+ cmd += ["-Xcompiler", "-fPIC", "-Xcompiler", "-fvisibility=hidden",
320
+ "-Xcompiler", "-static-libstdc++", "-Xcompiler", "-static-libgcc"]
321
+ cmd += [str(source), "-o", str(out)]
322
+ return cmd
323
+
324
+
325
+ def _source_stamp(sources: Sequence[Path], cmd: Sequence[str]) -> str:
326
+ h = hashlib.sha256()
327
+ h.update(f"abi{FA_ABI_VERSION}\n".encode())
328
+ for s in sources:
329
+ h.update(s.read_bytes())
330
+ # the command line is part of the identity: an AVX2 build and a baseline
331
+ # build come from byte-identical sources
332
+ h.update("\n".join(str(c) for c in cmd[:-1]).encode())
333
+ return h.hexdigest()[:16]
334
+
335
+
336
+ def _run_build(cmd: Sequence[str], cwd: Path) -> None:
337
+ if _verbose():
338
+ print("frame_analytics: " + " ".join(str(c) for c in cmd), file=sys.stderr)
339
+ res = subprocess.run([str(c) for c in cmd], cwd=str(cwd),
340
+ capture_output=not _verbose(), text=True)
341
+ if res.returncode != 0:
342
+ detail = ""
343
+ if not _verbose():
344
+ detail = "\n" + (res.stderr or res.stdout or "").strip()[-4000:]
345
+ raise RuntimeError(
346
+ f"build failed ({' '.join(str(c) for c in cmd[:1])} exited "
347
+ f"{res.returncode}); set FA_VERBOSE=1 for the full output{detail}"
348
+ )
349
+
350
+
351
+ def _build_cached(stem: str, sources: Sequence[Path],
352
+ make_cmd) -> Path:
353
+ """Compile ``sources`` into ``build_root()/<stem>`` unless already current.
354
+
355
+ The stamp file holds a hash of the sources *and* the command line, so an
356
+ edited kernel or a changed flag rebuilds and an unchanged one does not.
357
+ """
358
+ root = build_root() / stem
359
+ root.mkdir(parents=True, exist_ok=True)
360
+ out = root / library_filename(stem)
361
+ cmd = make_cmd(out, root)
362
+ stamp_want = _source_stamp(sources, cmd)
363
+ stamp_file = root / "stamp"
364
+ if out.exists() and stamp_file.exists():
365
+ try:
366
+ if stamp_file.read_text().strip() == stamp_want:
367
+ return out
368
+ except OSError:
369
+ pass
370
+
371
+ # build to a unique name and rename, so two interpreters racing on the same
372
+ # cache cannot hand each other a half-written library
373
+ tmp = root / f".{os.getpid()}{library_filename(stem)}"
374
+ tmp_cmd = make_cmd(tmp, root)
375
+ try:
376
+ _run_build(tmp_cmd, root)
377
+ os.replace(tmp, out)
378
+ stamp_file.write_text(stamp_want)
379
+ finally:
380
+ for junk in root.glob(f".{os.getpid()}*"):
381
+ try:
382
+ junk.unlink()
383
+ except OSError:
384
+ pass
385
+ return out
386
+
387
+
388
+ # --------------------------------------------------------------------------- #
389
+ # loading
390
+ # --------------------------------------------------------------------------- #
391
+
392
+
393
+ def _dlopen(path: Path) -> ctypes.CDLL:
394
+ if sys.platform == "win32":
395
+ # the libraries import nothing but the platform C runtime, so the
396
+ # default search path is enough; winmode=0 keeps it that way
397
+ return ctypes.CDLL(str(path), winmode=0)
398
+ return ctypes.CDLL(str(path))
399
+
400
+
401
+ def _prebuilt(stem: str) -> Optional[Path]:
402
+ if os.environ.get("FA_FORCE_JIT", "0") == "1":
403
+ return None
404
+ p = _LIBDIR / library_filename(stem)
405
+ return p if p.exists() else None
406
+
407
+
408
+ _P = ctypes.c_void_p
409
+ _I = ctypes.c_int
410
+ _L = ctypes.c_int64
411
+ _D = ctypes.c_double
412
+
413
+
414
+ def _bind_cpu(lib: ctypes.CDLL) -> ctypes.CDLL:
415
+ sig = {
416
+ "fa_cpu_abi_version": ([], _I),
417
+ "fa_cpu_has_avx2": ([], _I),
418
+ "fa_cpu_set_num_threads": ([_I], _I),
419
+ "fa_cpu_num_threads": ([], _I),
420
+ "fa_cpu_pixel_reduce": ([_P, _P, _I, _L, _L, _I, _D, _D, _I, _P], _I),
421
+ "fa_cpu_ssim": ([_P, _P, _I, _I, _I, _I, _I, _P, _I, _D, _D, _D, _P, _P], _I),
422
+ "fa_cpu_ssim_cs": ([_P, _P, _I, _I, _I, _I, _I, _P, _I, _D, _D, _D, _P, _P], _I),
423
+ "fa_cpu_gmsd": ([_P, _P, _I, _I, _I, _I, _I, _D, _D, _I, _P, _P], _I),
424
+ }
425
+ for name, (args, res) in sig.items():
426
+ fn = getattr(lib, name)
427
+ fn.argtypes = args
428
+ fn.restype = res
429
+ return lib
430
+
431
+
432
+ def _bind_cuda(lib: ctypes.CDLL) -> ctypes.CDLL:
433
+ sig = {
434
+ "fa_cuda_abi_version": ([], _I),
435
+ "fa_cuda_error_string": ([_I], ctypes.c_char_p),
436
+ "fa_cuda_device_count": ([_P], _I),
437
+ "fa_cuda_pixel_workspace": ([_L, _L, _I, _P], _I),
438
+ "fa_cuda_pixel_reduce":
439
+ ([_P, _P, _I, _L, _L, _I, _D, _D, _P, _I, _P, _P, _P, _P, _I, _P], _I),
440
+ "fa_cuda_ssim_workspace": ([_I, _I, _I, _P], _I),
441
+ "fa_cuda_ssim":
442
+ ([_P, _P, _I, _I, _I, _I, _I, _P, _I, _D, _D, _D, _P, _P, _I, _P, _P,
443
+ _I, _P], _I),
444
+ "fa_cuda_ssim_cs":
445
+ ([_P, _P, _I, _I, _I, _I, _I, _P, _I, _D, _D, _D, _P, _P, _I, _P, _P,
446
+ _I, _P], _I),
447
+ "fa_cuda_ssim_backward":
448
+ ([_P, _P, _P, _P, _P, _I, _I, _I, _I, _I, _D, _D, _D, _P, _P, _P,
449
+ _I, _P], _I),
450
+ "fa_cuda_ssim_cs_backward":
451
+ ([_P, _P, _P, _P, _P, _I, _I, _I, _I, _I, _D, _D, _D, _P, _P, _P,
452
+ _I, _P], _I),
453
+ "fa_cuda_gmsd_workspace": ([_I, _I, _I, _P], _I),
454
+ "fa_cuda_gmsd":
455
+ ([_P, _P, _I, _I, _I, _I, _I, _D, _D, _I, _P, _P, _I, _P, _P, _I,
456
+ _P], _I),
457
+ }
458
+ for name, (args, res) in sig.items():
459
+ fn = getattr(lib, name)
460
+ fn.argtypes = args
461
+ fn.restype = res
462
+ return lib
463
+
464
+
465
+ def _check_abi(lib: ctypes.CDLL, getter: str, origin: Path) -> None:
466
+ got = getattr(lib, getter)()
467
+ if got != FA_ABI_VERSION:
468
+ raise RuntimeError(
469
+ f"{origin.name} reports ABI version {got}, this build expects "
470
+ f"{FA_ABI_VERSION}; delete it and let it rebuild"
471
+ )
472
+
473
+
474
+ def load_cpu():
475
+ """Return ``(lib, origin, isa)`` for the CPU kernels."""
476
+ baseline_path = _prebuilt("fa_cpu")
477
+ origin = "prebuilt"
478
+ if baseline_path is None:
479
+ ensure_host_compiler()
480
+ baseline_path = _build_cached(
481
+ "fa_cpu", [_CSRC / "fa_cpu.cpp", _CSRC / "fa_abi.h"],
482
+ lambda out, objdir: cpu_compile_command(_CSRC / "fa_cpu.cpp", out,
483
+ False, objdir),
484
+ )
485
+ origin = "jit"
486
+ lib = _bind_cpu(_dlopen(baseline_path))
487
+ _check_abi(lib, "fa_cpu_abi_version", baseline_path)
488
+ isa = "baseline"
489
+
490
+ if _is_x86() and lib.fa_cpu_has_avx2():
491
+ try:
492
+ avx_path = _prebuilt("fa_cpu_avx2")
493
+ avx_origin = "prebuilt"
494
+ if avx_path is None:
495
+ ensure_host_compiler()
496
+ avx_path = _build_cached(
497
+ "fa_cpu_avx2", [_CSRC / "fa_cpu.cpp", _CSRC / "fa_abi.h"],
498
+ lambda out, objdir: cpu_compile_command(
499
+ _CSRC / "fa_cpu.cpp", out, True, objdir),
500
+ )
501
+ avx_origin = "jit"
502
+ avx = _bind_cpu(_dlopen(avx_path))
503
+ _check_abi(avx, "fa_cpu_abi_version", avx_path)
504
+ lib, origin, isa = avx, avx_origin, "avx2"
505
+ except Exception:
506
+ # the baseline build is already loaded and correct; a missing or
507
+ # unbuildable AVX2 sibling costs speed, never results
508
+ if _verbose():
509
+ import traceback
510
+
511
+ traceback.print_exc()
512
+ return lib, origin, isa
513
+
514
+
515
+ def load_cuda():
516
+ """Return ``(lib, origin)`` for the CUDA kernels."""
517
+ if sys.platform == "darwin":
518
+ raise RuntimeError("no CUDA on macOS")
519
+ path = _prebuilt("fa_cuda")
520
+ origin = "prebuilt"
521
+ if path is None:
522
+ ensure_host_compiler()
523
+ path = _build_cached(
524
+ "fa_cuda", [_CSRC / "fa_cuda.cu", _CSRC / "fa_abi.h"],
525
+ lambda out, objdir: cuda_compile_command(_CSRC / "fa_cuda.cu", out),
526
+ )
527
+ origin = "jit"
528
+ lib = _bind_cuda(_dlopen(path))
529
+ _check_abi(lib, "fa_cuda_abi_version", path)
530
+ return lib, origin