fastpose 1.0.0__py3-none-any.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.
- fastpose/__init__.py +6 -0
- fastpose/clean_cache.py +170 -0
- fastpose/cuda/__init__.py +104 -0
- fastpose/cuda/backend.py +63 -0
- fastpose/cuda/lm.py +412 -0
- fastpose/cuda/problem.py +102 -0
- fastpose/cuda/problems/__init__.py +12 -0
- fastpose/cuda/problems/absolute.py +228 -0
- fastpose/cuda/problems/absolute_focal.py +233 -0
- fastpose/cuda/problems/common.py +100 -0
- fastpose/cuda/problems/essential.py +271 -0
- fastpose/cuda/problems/fundamental.py +253 -0
- fastpose/cuda/problems/homography.py +215 -0
- fastpose/cuda/problems/monodepth.py +482 -0
- fastpose/cuda/problems/shared_focal.py +297 -0
- fastpose/cuda/problems/varying_focal.py +269 -0
- fastpose/cuda/ransac.py +432 -0
- fastpose/cuda/reductions.py +104 -0
- fastpose/cuda/registry.py +53 -0
- fastpose/cuda/scoring.py +239 -0
- fastpose/estimators/__init__.py +12 -0
- fastpose/estimators/absolute.py +155 -0
- fastpose/estimators/absolute_focal.py +146 -0
- fastpose/estimators/essential.py +187 -0
- fastpose/estimators/fundamental.py +158 -0
- fastpose/estimators/homography.py +148 -0
- fastpose/estimators/monodepth.py +462 -0
- fastpose/estimators/ransac.py +463 -0
- fastpose/estimators/shared_focal.py +172 -0
- fastpose/estimators/utils.py +122 -0
- fastpose/estimators/varying_focal.py +171 -0
- fastpose/estimators/warmup.py +557 -0
- fastpose/jit_backend.py +23 -0
- fastpose/kernel_cache.py +131 -0
- fastpose/refiners/__init__.py +12 -0
- fastpose/refiners/absolute.py +204 -0
- fastpose/refiners/absolute_focal.py +143 -0
- fastpose/refiners/essential.py +152 -0
- fastpose/refiners/fundamental.py +81 -0
- fastpose/refiners/homography.py +385 -0
- fastpose/refiners/lm.py +157 -0
- fastpose/refiners/losses.py +119 -0
- fastpose/refiners/monodepth.py +928 -0
- fastpose/refiners/shared_focal.py +160 -0
- fastpose/refiners/utils.py +660 -0
- fastpose/refiners/varying_focal.py +164 -0
- fastpose/scorers/__init__.py +17 -0
- fastpose/scorers/reprojection.py +240 -0
- fastpose/scorers/sampson.py +550 -0
- fastpose/scorers/transfer.py +308 -0
- fastpose/solvers/__init__.py +10 -0
- fastpose/solvers/essential.py +916 -0
- fastpose/solvers/fundamental.py +290 -0
- fastpose/solvers/homography.py +208 -0
- fastpose/solvers/monodepth.py +825 -0
- fastpose/solvers/p3p.py +416 -0
- fastpose/solvers/p4pf.py +821 -0
- fastpose/solvers/shared_focal.py +1767 -0
- fastpose/solvers/utils.py +33 -0
- fastpose/solvers/varying_focal.py +206 -0
- fastpose-1.0.0.dist-info/METADATA +759 -0
- fastpose-1.0.0.dist-info/RECORD +66 -0
- fastpose-1.0.0.dist-info/WHEEL +5 -0
- fastpose-1.0.0.dist-info/entry_points.txt +3 -0
- fastpose-1.0.0.dist-info/licenses/LICENSE +28 -0
- fastpose-1.0.0.dist-info/top_level.txt +1 -0
fastpose/__init__.py
ADDED
fastpose/clean_cache.py
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"""Command-line removal of fastpose's cached Numba kernels.
|
|
2
|
+
|
|
3
|
+
`fastpose-warmup` writes a lot: a full warmup produces hundreds of MB of
|
|
4
|
+
`.nbc`/`.nbi` files, and nothing ever reclaims them. Superseded entries are not
|
|
5
|
+
overwritten in place either - the index is keyed on each source file's
|
|
6
|
+
timestamp and size and on the host CPU, so editing a kernel, upgrading numba or
|
|
7
|
+
upgrading fastpose orphans the old entries rather than replacing them, and pip
|
|
8
|
+
does not remove them on upgrade because they are generated at runtime and never
|
|
9
|
+
appear in the wheel's RECORD.
|
|
10
|
+
|
|
11
|
+
Numba may put them in any of three places depending on how fastpose is
|
|
12
|
+
installed, which is why finding them by hand is awkward:
|
|
13
|
+
|
|
14
|
+
NUMBA_CACHE_DIR if that variable is set (UserProvidedCacheLocator)
|
|
15
|
+
<package>/**/__pycache__ the usual case (InTreeCacheLocator)
|
|
16
|
+
a user-wide cache directory, when the install directory is not writable,
|
|
17
|
+
e.g. a system or container install (UserWideCacheLocator)
|
|
18
|
+
|
|
19
|
+
This checks all three. It only ever deletes `.nbc` and `.nbi` files that sit in
|
|
20
|
+
a cache directory belonging to a fastpose source directory: never `.py`, never
|
|
21
|
+
`.pyc` (those are CPython's own and are cheap to rebuild), and never anything
|
|
22
|
+
under another package. Deleting them is always safe - the kernels recompile on
|
|
23
|
+
next use, or run `fastpose-warmup` to rebuild them up front.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
import argparse
|
|
27
|
+
import os
|
|
28
|
+
|
|
29
|
+
import fastpose
|
|
30
|
+
|
|
31
|
+
# what numba writes: the per-signature index and the compiled data files
|
|
32
|
+
CACHE_SUFFIXES = ('.nbi', '.nbc')
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _package_dirs():
|
|
36
|
+
"""Every directory of the installed fastpose package that holds sources."""
|
|
37
|
+
root = os.path.dirname(os.path.abspath(fastpose.__file__))
|
|
38
|
+
dirs = []
|
|
39
|
+
for dirpath, dirnames, filenames in os.walk(root):
|
|
40
|
+
dirnames[:] = [d for d in dirnames if d != '__pycache__']
|
|
41
|
+
if any(f.endswith('.py') for f in filenames):
|
|
42
|
+
dirs.append(dirpath)
|
|
43
|
+
return dirs
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def cache_dirs():
|
|
47
|
+
"""Cache directories numba could have used for fastpose's kernels.
|
|
48
|
+
|
|
49
|
+
Mirrors the three locators in `numba.core.caching` that apply to a package
|
|
50
|
+
backed by real source files, using numba's own subpath helper so this keeps
|
|
51
|
+
agreeing with it. Directories that do not exist are still returned; callers
|
|
52
|
+
filter them.
|
|
53
|
+
"""
|
|
54
|
+
from numba import config
|
|
55
|
+
from numba.core.caching import _CacheLocator
|
|
56
|
+
|
|
57
|
+
candidates = []
|
|
58
|
+
bases = []
|
|
59
|
+
if getattr(config, 'CACHE_DIR', None):
|
|
60
|
+
bases.append(config.CACHE_DIR)
|
|
61
|
+
try:
|
|
62
|
+
from numba.misc.appdirs import AppDirs
|
|
63
|
+
bases.append(AppDirs(appname='numba', appauthor=False).user_cache_dir)
|
|
64
|
+
except Exception: # pragma: no cover - numba internals moved
|
|
65
|
+
pass
|
|
66
|
+
|
|
67
|
+
for source_dir in _package_dirs():
|
|
68
|
+
# in-tree: the __pycache__ beside the sources
|
|
69
|
+
candidates.append(os.path.join(source_dir, '__pycache__'))
|
|
70
|
+
# the redirected and user-wide variants, which hash the source path
|
|
71
|
+
marker = os.path.join(source_dir, 'x.py')
|
|
72
|
+
for base in bases:
|
|
73
|
+
subpath = _CacheLocator.get_suitable_cache_subpath(marker)
|
|
74
|
+
candidates.append(os.path.join(base, subpath))
|
|
75
|
+
|
|
76
|
+
seen = set()
|
|
77
|
+
unique = []
|
|
78
|
+
for path in candidates:
|
|
79
|
+
key = os.path.normcase(os.path.abspath(path))
|
|
80
|
+
if key not in seen:
|
|
81
|
+
seen.add(key)
|
|
82
|
+
unique.append(path)
|
|
83
|
+
return unique
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def find_cache_files():
|
|
87
|
+
"""(path, size) of every fastpose kernel cache file, largest first."""
|
|
88
|
+
found = []
|
|
89
|
+
for directory in cache_dirs():
|
|
90
|
+
if not os.path.isdir(directory):
|
|
91
|
+
continue
|
|
92
|
+
for name in os.listdir(directory):
|
|
93
|
+
if not name.endswith(CACHE_SUFFIXES):
|
|
94
|
+
continue
|
|
95
|
+
path = os.path.join(directory, name)
|
|
96
|
+
if os.path.isfile(path):
|
|
97
|
+
found.append((path, os.path.getsize(path)))
|
|
98
|
+
found.sort(key=lambda item: -item[1])
|
|
99
|
+
return found
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _human(num_bytes):
|
|
103
|
+
size = float(num_bytes)
|
|
104
|
+
for unit in ('B', 'KB', 'MB', 'GB'):
|
|
105
|
+
if size < 1024.0 or unit == 'GB':
|
|
106
|
+
return f'{size:.0f} {unit}' if unit == 'B' else f'{size:.1f} {unit}'
|
|
107
|
+
size /= 1024.0
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def clean(dry_run=False, verbose=False):
|
|
111
|
+
"""Delete the cache files; returns (num_removed, bytes_removed)."""
|
|
112
|
+
files = find_cache_files()
|
|
113
|
+
removed = 0
|
|
114
|
+
freed = 0
|
|
115
|
+
for path, size in files:
|
|
116
|
+
if dry_run:
|
|
117
|
+
removed += 1
|
|
118
|
+
freed += size
|
|
119
|
+
if verbose:
|
|
120
|
+
print(f' would remove {path} ({_human(size)})')
|
|
121
|
+
continue
|
|
122
|
+
try:
|
|
123
|
+
os.remove(path)
|
|
124
|
+
except OSError as exc:
|
|
125
|
+
print(f' could not remove {path}: {exc}')
|
|
126
|
+
continue
|
|
127
|
+
removed += 1
|
|
128
|
+
freed += size
|
|
129
|
+
if verbose:
|
|
130
|
+
print(f' removed {path} ({_human(size)})')
|
|
131
|
+
return removed, freed
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def main(argv=None):
|
|
135
|
+
parser = argparse.ArgumentParser(
|
|
136
|
+
description="Remove fastpose's cached Numba kernels (.nbi/.nbc). They "
|
|
137
|
+
"recompile on next use; `fastpose-warmup` rebuilds them "
|
|
138
|
+
"up front.",
|
|
139
|
+
)
|
|
140
|
+
parser.add_argument(
|
|
141
|
+
'-n', '--dry-run', action='store_true',
|
|
142
|
+
help='Report what would be removed without deleting anything.',
|
|
143
|
+
)
|
|
144
|
+
parser.add_argument(
|
|
145
|
+
'-v', '--verbose', action='store_true',
|
|
146
|
+
help='List every file rather than just the total.',
|
|
147
|
+
)
|
|
148
|
+
parser.add_argument(
|
|
149
|
+
'--list-dirs', action='store_true',
|
|
150
|
+
help='Print the cache directories that were searched and exit.',
|
|
151
|
+
)
|
|
152
|
+
args = parser.parse_args(argv)
|
|
153
|
+
|
|
154
|
+
if args.list_dirs:
|
|
155
|
+
for directory in cache_dirs():
|
|
156
|
+
mark = '' if os.path.isdir(directory) else ' (does not exist)'
|
|
157
|
+
print(f'{directory}{mark}')
|
|
158
|
+
return 0
|
|
159
|
+
|
|
160
|
+
removed, freed = clean(dry_run=args.dry_run, verbose=args.verbose)
|
|
161
|
+
if not removed:
|
|
162
|
+
print('No cached kernels found.')
|
|
163
|
+
return 0
|
|
164
|
+
verb = 'Would remove' if args.dry_run else 'Removed'
|
|
165
|
+
print(f'{verb} {removed} file(s), {_human(freed)}.')
|
|
166
|
+
return 0
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
if __name__ == '__main__':
|
|
170
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""CUDA backend: batch-parallel LO-RANSAC on the GPU.
|
|
2
|
+
|
|
3
|
+
The CPU engine draws one hypothesis at a time (or a small batch across numba
|
|
4
|
+
threads) and is bounded by how fast a single core can solve and score. This
|
|
5
|
+
backend instead keeps thousands of hypotheses in flight: a round of `batch`
|
|
6
|
+
minimal samples is solved one-thread-per-hypothesis, scored one-block-per-
|
|
7
|
+
hypothesis with the block reducing over all correspondences, and locally
|
|
8
|
+
optimized one-block-per-candidate. Only a handful of scalars cross the PCIe
|
|
9
|
+
bus per round.
|
|
10
|
+
|
|
11
|
+
That makes it a win in exactly the regime the CPU driver is worst at - many
|
|
12
|
+
iterations and many matches - and a loss on small problems, where a round of
|
|
13
|
+
kernel launches (~30-60us) costs more than the few hundred microseconds the
|
|
14
|
+
CPU driver would have needed in total.
|
|
15
|
+
|
|
16
|
+
Layout
|
|
17
|
+
------
|
|
18
|
+
Everything that is not specific to one estimation problem is shared:
|
|
19
|
+
|
|
20
|
+
ransac.py the batched driver - round loop, local-optimization gate,
|
|
21
|
+
adaptive termination, packed readback
|
|
22
|
+
scoring.py the block-reduction truncated (MSAC) scorer
|
|
23
|
+
lm.py the whole LM loop inside one kernel launch
|
|
24
|
+
reductions.py the tree reductions and the damped Cholesky solve
|
|
25
|
+
problem.py `CudaProblem`, the seam between the two
|
|
26
|
+
problems/ one module per estimation problem: the solve kernel's
|
|
27
|
+
scratch, and the per-point device functions the scorer and
|
|
28
|
+
the LM call
|
|
29
|
+
|
|
30
|
+
What is shared with the CPU path and what is not
|
|
31
|
+
------------------------------------------------
|
|
32
|
+
The minimal-solver math is *the same source*: `solvers/essential.py` builds
|
|
33
|
+
its kernels through `build_five_point_kernels(jit)` and `problems/essential.py`
|
|
34
|
+
instantiates that factory with `cuda.jit(device=True)` instead of `njit`.
|
|
35
|
+
There is no second copy of the Sturm/Danilevsky chain to keep in sync, and the
|
|
36
|
+
same holds for every other problem.
|
|
37
|
+
|
|
38
|
+
Two things could not be shared, both because numba's runtime is host-only, so
|
|
39
|
+
neither `np.empty` nor `.reshape` compiles in device code:
|
|
40
|
+
|
|
41
|
+
- scratch is passed in pre-shaped (see the note in `solvers/essential.py`);
|
|
42
|
+
the CUDA kernels allocate it with `cuda.local.array`, which the hardware
|
|
43
|
+
interleaves across threads, so the accesses coalesce for free.
|
|
44
|
+
- the scorer and the LM accumulate are *reductions* here, not serial loops,
|
|
45
|
+
so their drivers are written fresh in `scoring.py` and `lm.py` rather than
|
|
46
|
+
re-decorated. They are checked against the CPU kernels point-for-point in
|
|
47
|
+
`tests/test_cuda.py`.
|
|
48
|
+
|
|
49
|
+
Availability
|
|
50
|
+
------------
|
|
51
|
+
`is_available()` reports whether a usable CUDA device is present; `require()`
|
|
52
|
+
raises with the reason if not. Import of this package never fails and never
|
|
53
|
+
initializes a context, so `import fastpose` stays cheap on machines with no
|
|
54
|
+
GPU.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
_UNAVAILABLE_REASON = None
|
|
58
|
+
_AVAILABLE = None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _probe():
|
|
62
|
+
global _AVAILABLE, _UNAVAILABLE_REASON
|
|
63
|
+
if _AVAILABLE is not None:
|
|
64
|
+
return _AVAILABLE
|
|
65
|
+
try:
|
|
66
|
+
from numba import cuda
|
|
67
|
+
except ImportError as exc: # pragma: no cover - numba is a hard dependency
|
|
68
|
+
_AVAILABLE, _UNAVAILABLE_REASON = False, f"numba.cuda unimportable: {exc}"
|
|
69
|
+
return False
|
|
70
|
+
try:
|
|
71
|
+
if not cuda.is_available():
|
|
72
|
+
_AVAILABLE = False
|
|
73
|
+
_UNAVAILABLE_REASON = (
|
|
74
|
+
"numba.cuda.is_available() is False - no CUDA driver or no "
|
|
75
|
+
"visible device")
|
|
76
|
+
return False
|
|
77
|
+
# is_available() only checks the driver; touching a device is what
|
|
78
|
+
# catches a driver/toolkit mismatch or an already-exhausted GPU
|
|
79
|
+
cuda.current_context()
|
|
80
|
+
except Exception as exc:
|
|
81
|
+
_AVAILABLE, _UNAVAILABLE_REASON = False, f"{type(exc).__name__}: {exc}"
|
|
82
|
+
return False
|
|
83
|
+
_AVAILABLE, _UNAVAILABLE_REASON = True, None
|
|
84
|
+
return True
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def is_available():
|
|
88
|
+
# True when a CUDA device can actually be used, not merely imported
|
|
89
|
+
return _probe()
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def unavailable_reason():
|
|
93
|
+
# human-readable reason is_available() returned False, or None
|
|
94
|
+
_probe()
|
|
95
|
+
return _UNAVAILABLE_REASON
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def require():
|
|
99
|
+
# raises RuntimeError with the probe's reason; used by the device='cuda'
|
|
100
|
+
# entry points so a missing GPU fails with something actionable
|
|
101
|
+
if not _probe():
|
|
102
|
+
raise RuntimeError(
|
|
103
|
+
"device='cuda' requires a working CUDA device: "
|
|
104
|
+
f"{unavailable_reason()}")
|
fastpose/cuda/backend.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""CUDA side of the `jit` shim in `fastpose/jit_backend.py`.
|
|
2
|
+
|
|
3
|
+
Kept in its own module so importing it is the only thing that touches
|
|
4
|
+
`numba.cuda`; `fastpose.cuda.__init__` stays importable (and cheap) on a
|
|
5
|
+
machine with no GPU.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import contextlib
|
|
9
|
+
import warnings
|
|
10
|
+
|
|
11
|
+
from numba import cuda
|
|
12
|
+
from numba.core.errors import NumbaPerformanceWarning
|
|
13
|
+
|
|
14
|
+
# Threads per block for the reduction kernels (scorer, LM accumulate). Must be
|
|
15
|
+
# a power of two - the tree reductions halve the stride - and at least a warp.
|
|
16
|
+
# 128 measured a reasonable default: large enough to hide the global-memory
|
|
17
|
+
# latency of streaming the correspondence columns, small enough that a block
|
|
18
|
+
# per hypothesis still fills the machine at modest batch sizes.
|
|
19
|
+
THREADS_PER_BLOCK = 128
|
|
20
|
+
|
|
21
|
+
# Threads per block for the one-thread-per-hypothesis solve kernel. The 5-point
|
|
22
|
+
# solver carries ~6.9 KB of per-thread local memory (see solvers.py), so a
|
|
23
|
+
# small block keeps the per-SM local-memory footprint sane.
|
|
24
|
+
SOLVE_THREADS_PER_BLOCK = 64
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def cuda_jit(fastmath=False, inline=False):
|
|
28
|
+
# GPU instantiation of a kernel written against the shim. Device functions
|
|
29
|
+
# are inlined into the kernel that calls them, so there is no separate
|
|
30
|
+
# on-disk cache entry for them - `cache=True` goes on the kernels instead.
|
|
31
|
+
return cuda.jit(device=True, fastmath=fastmath, inline=inline)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@contextlib.contextmanager
|
|
35
|
+
def quiet_low_occupancy():
|
|
36
|
+
"""Silence numba's low-occupancy warning for the launches inside.
|
|
37
|
+
|
|
38
|
+
numba warns on every launch whose grid is under 128 blocks, and several of
|
|
39
|
+
this backend's launches are deliberately smaller: sampling and solving are
|
|
40
|
+
one thread per hypothesis (32 and 64 blocks at the default batch), local
|
|
41
|
+
optimization refines a single candidate per round (one block, see the note
|
|
42
|
+
in cuda/ransac.py), and the last round of an adaptive run scores however
|
|
43
|
+
many iterations are left. Only the main scorer's grid is a function of the
|
|
44
|
+
batch, and that one is 4096 blocks by default. So the warning says nothing
|
|
45
|
+
actionable here.
|
|
46
|
+
|
|
47
|
+
Filtered by message rather than by category, so a NumbaPerformanceWarning
|
|
48
|
+
that *is* actionable - the host-array copy one, say - still comes through.
|
|
49
|
+
The leading `.*` is not decoration: numba's error classes render `str()`
|
|
50
|
+
with ANSI highlighting, and the filter is matched against that, so a
|
|
51
|
+
pattern anchored at 'Grid size' never fires.
|
|
52
|
+
|
|
53
|
+
`warnings` filters are process-global state, so this is scoped to the
|
|
54
|
+
driver's own launches and is one more reason `CudaRansacEstimator` is not
|
|
55
|
+
thread-safe.
|
|
56
|
+
"""
|
|
57
|
+
with warnings.catch_warnings():
|
|
58
|
+
warnings.filterwarnings(
|
|
59
|
+
'ignore',
|
|
60
|
+
message=r'.*Grid size \d+ will likely result in GPU '
|
|
61
|
+
r'under-utilization',
|
|
62
|
+
category=NumbaPerformanceWarning)
|
|
63
|
+
yield
|