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.
- frame_analytics/__init__.py +80 -0
- frame_analytics/_cabi.py +530 -0
- frame_analytics/backend.py +666 -0
- frame_analytics/csrc/fa_abi.h +206 -0
- frame_analytics/csrc/fa_cpu.cpp +943 -0
- frame_analytics/csrc/fa_cuda.cu +1459 -0
- frame_analytics/functional.py +1207 -0
- frame_analytics/lib/fa_cpu.dll +0 -0
- frame_analytics/lib/fa_cpu_avx2.dll +0 -0
- frame_analytics/lib/fa_cuda.dll +0 -0
- frame_analytics/modules.py +370 -0
- frame_analytics/reference.py +318 -0
- frame_analytics-0.2.0.dist-info/METADATA +420 -0
- frame_analytics-0.2.0.dist-info/RECORD +17 -0
- frame_analytics-0.2.0.dist-info/WHEEL +5 -0
- frame_analytics-0.2.0.dist-info/licenses/LICENSE +202 -0
- frame_analytics-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -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()
|
frame_analytics/_cabi.py
ADDED
|
@@ -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
|