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.
Files changed (66) hide show
  1. fastpose/__init__.py +6 -0
  2. fastpose/clean_cache.py +170 -0
  3. fastpose/cuda/__init__.py +104 -0
  4. fastpose/cuda/backend.py +63 -0
  5. fastpose/cuda/lm.py +412 -0
  6. fastpose/cuda/problem.py +102 -0
  7. fastpose/cuda/problems/__init__.py +12 -0
  8. fastpose/cuda/problems/absolute.py +228 -0
  9. fastpose/cuda/problems/absolute_focal.py +233 -0
  10. fastpose/cuda/problems/common.py +100 -0
  11. fastpose/cuda/problems/essential.py +271 -0
  12. fastpose/cuda/problems/fundamental.py +253 -0
  13. fastpose/cuda/problems/homography.py +215 -0
  14. fastpose/cuda/problems/monodepth.py +482 -0
  15. fastpose/cuda/problems/shared_focal.py +297 -0
  16. fastpose/cuda/problems/varying_focal.py +269 -0
  17. fastpose/cuda/ransac.py +432 -0
  18. fastpose/cuda/reductions.py +104 -0
  19. fastpose/cuda/registry.py +53 -0
  20. fastpose/cuda/scoring.py +239 -0
  21. fastpose/estimators/__init__.py +12 -0
  22. fastpose/estimators/absolute.py +155 -0
  23. fastpose/estimators/absolute_focal.py +146 -0
  24. fastpose/estimators/essential.py +187 -0
  25. fastpose/estimators/fundamental.py +158 -0
  26. fastpose/estimators/homography.py +148 -0
  27. fastpose/estimators/monodepth.py +462 -0
  28. fastpose/estimators/ransac.py +463 -0
  29. fastpose/estimators/shared_focal.py +172 -0
  30. fastpose/estimators/utils.py +122 -0
  31. fastpose/estimators/varying_focal.py +171 -0
  32. fastpose/estimators/warmup.py +557 -0
  33. fastpose/jit_backend.py +23 -0
  34. fastpose/kernel_cache.py +131 -0
  35. fastpose/refiners/__init__.py +12 -0
  36. fastpose/refiners/absolute.py +204 -0
  37. fastpose/refiners/absolute_focal.py +143 -0
  38. fastpose/refiners/essential.py +152 -0
  39. fastpose/refiners/fundamental.py +81 -0
  40. fastpose/refiners/homography.py +385 -0
  41. fastpose/refiners/lm.py +157 -0
  42. fastpose/refiners/losses.py +119 -0
  43. fastpose/refiners/monodepth.py +928 -0
  44. fastpose/refiners/shared_focal.py +160 -0
  45. fastpose/refiners/utils.py +660 -0
  46. fastpose/refiners/varying_focal.py +164 -0
  47. fastpose/scorers/__init__.py +17 -0
  48. fastpose/scorers/reprojection.py +240 -0
  49. fastpose/scorers/sampson.py +550 -0
  50. fastpose/scorers/transfer.py +308 -0
  51. fastpose/solvers/__init__.py +10 -0
  52. fastpose/solvers/essential.py +916 -0
  53. fastpose/solvers/fundamental.py +290 -0
  54. fastpose/solvers/homography.py +208 -0
  55. fastpose/solvers/monodepth.py +825 -0
  56. fastpose/solvers/p3p.py +416 -0
  57. fastpose/solvers/p4pf.py +821 -0
  58. fastpose/solvers/shared_focal.py +1767 -0
  59. fastpose/solvers/utils.py +33 -0
  60. fastpose/solvers/varying_focal.py +206 -0
  61. fastpose-1.0.0.dist-info/METADATA +759 -0
  62. fastpose-1.0.0.dist-info/RECORD +66 -0
  63. fastpose-1.0.0.dist-info/WHEEL +5 -0
  64. fastpose-1.0.0.dist-info/entry_points.txt +3 -0
  65. fastpose-1.0.0.dist-info/licenses/LICENSE +28 -0
  66. fastpose-1.0.0.dist-info/top_level.txt +1 -0
fastpose/__init__.py ADDED
@@ -0,0 +1,6 @@
1
+ import fastpose.estimators
2
+ import fastpose.refiners
3
+ import fastpose.scorers
4
+ import fastpose.solvers
5
+
6
+ __all__ = ["estimators", "refiners", "scorers", "solvers"]
@@ -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()}")
@@ -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