aksha 0.2.1__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.
- aksha/__init__.py +27 -0
- aksha/_launcher.py +143 -0
- aksha/gpu_profile_cache.py +475 -0
- aksha/hmm_databases.json +293 -0
- aksha/initialize.py +920 -0
- aksha/jackhmmer.py +54 -0
- aksha/main.py +172 -0
- aksha/nucsearch.py +213 -0
- aksha/phmmer.py +54 -0
- aksha/rp16.py +290 -0
- aksha/scan.py +273 -0
- aksha/search.py +5070 -0
- aksha-0.2.1.dist-info/METADATA +106 -0
- aksha-0.2.1.dist-info/RECORD +18 -0
- aksha-0.2.1.dist-info/WHEEL +5 -0
- aksha-0.2.1.dist-info/entry_points.txt +2 -0
- aksha-0.2.1.dist-info/licenses/LICENSE +21 -0
- aksha-0.2.1.dist-info/top_level.txt +1 -0
aksha/__init__.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# aksha/__init__.py
|
|
2
|
+
|
|
3
|
+
def main():
|
|
4
|
+
import argparse
|
|
5
|
+
|
|
6
|
+
parser = argparse.ArgumentParser(description='Aksha: A suite of sequence analysis tools.')
|
|
7
|
+
parser.add_argument('command', choices=['search', 'initialize', 'nucsearch', 'phmmer', 'jackhmmer'],
|
|
8
|
+
help='Subcommand to run')
|
|
9
|
+
|
|
10
|
+
# Parse only the first argument to determine the command
|
|
11
|
+
args, remaining_args = parser.parse_known_args()
|
|
12
|
+
|
|
13
|
+
if args.command == 'search':
|
|
14
|
+
from . import search
|
|
15
|
+
search.main(remaining_args)
|
|
16
|
+
elif args.command == 'initialize':
|
|
17
|
+
from . import initialize
|
|
18
|
+
initialize.main(remaining_args)
|
|
19
|
+
elif args.command == 'nucsearch':
|
|
20
|
+
from . import nucsearch
|
|
21
|
+
nucsearch.main(remaining_args)
|
|
22
|
+
elif args.command == 'phmmer':
|
|
23
|
+
from . import phmmer
|
|
24
|
+
phmmer.main(remaining_args)
|
|
25
|
+
elif args.command == 'jackhmmer':
|
|
26
|
+
from . import jackhmmer
|
|
27
|
+
jackhmmer.main(remaining_args)
|
aksha/_launcher.py
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""Minimal console launcher for Aksha's high-thread allocator policy.
|
|
2
|
+
|
|
3
|
+
This module must remain free of imports from :mod:`aksha.main`, PyHMMER, or
|
|
4
|
+
other allocation-heavy runtime modules. The console entry point can then set
|
|
5
|
+
the glibc arena limit and replace itself with a fresh interpreter before any
|
|
6
|
+
of those modules are imported. Programmatic/library entry points bypass this
|
|
7
|
+
module and retain their existing behavior.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import os
|
|
13
|
+
import sys
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
_GLIBC_ARENA_ENV = "MALLOC_ARENA_MAX"
|
|
17
|
+
_ASTRA_ARENA_ENV = "ASTRA_CPU_MALLOC_ARENA_MAX"
|
|
18
|
+
_REEXEC_SENTINEL_ENV = "_ASTRA_CPU_ALLOCATOR_REEXEC"
|
|
19
|
+
_DEFAULT_ARENA_MAX = 24
|
|
20
|
+
_MINIMUM_AUTO_THREADS = 64
|
|
21
|
+
_MAXIMUM_ARENA_MAX = (1 << 31) - 1
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _is_linux_glibc() -> bool:
|
|
25
|
+
"""Return whether the current interpreter uses glibc on Linux."""
|
|
26
|
+
if not sys.platform.startswith("linux"):
|
|
27
|
+
return False
|
|
28
|
+
try:
|
|
29
|
+
version = os.confstr("CS_GNU_LIBC_VERSION")
|
|
30
|
+
except (AttributeError, OSError, ValueError):
|
|
31
|
+
return False
|
|
32
|
+
return isinstance(version, str) and version.startswith("glibc ")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _requested_threads(arguments: tuple[str, ...]) -> int | None:
|
|
36
|
+
"""Return the CLI worker count, or ``None`` for malformed input.
|
|
37
|
+
|
|
38
|
+
Aksha's parser owns diagnostics for malformed ``--threads`` arguments.
|
|
39
|
+
The launcher simply declines to re-exec in that case so it cannot mask the
|
|
40
|
+
existing command-line error or alter its ordering.
|
|
41
|
+
"""
|
|
42
|
+
value = "1"
|
|
43
|
+
index = 0
|
|
44
|
+
while index < len(arguments):
|
|
45
|
+
argument = arguments[index]
|
|
46
|
+
if argument == "--threads":
|
|
47
|
+
index += 1
|
|
48
|
+
if index >= len(arguments):
|
|
49
|
+
return None
|
|
50
|
+
value = arguments[index]
|
|
51
|
+
elif argument.startswith("--threads="):
|
|
52
|
+
value = argument.partition("=")[2]
|
|
53
|
+
index += 1
|
|
54
|
+
try:
|
|
55
|
+
return int(value)
|
|
56
|
+
except ValueError:
|
|
57
|
+
return None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _is_high_thread_search(arguments: tuple[str, ...]) -> bool:
|
|
61
|
+
"""Return whether raw arguments select a search with at least 64 threads.
|
|
62
|
+
|
|
63
|
+
The allocator policy applies before Aksha can resolve CPU versus GPU
|
|
64
|
+
execution. Both routes run high-thread HMMER continuations and have
|
|
65
|
+
independently retained the 24-arena setting.
|
|
66
|
+
"""
|
|
67
|
+
if not arguments or arguments[0] != "search":
|
|
68
|
+
return False
|
|
69
|
+
threads = _requested_threads(arguments[1:])
|
|
70
|
+
return threads is not None and threads >= _MINIMUM_AUTO_THREADS
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _arena_max(environment: dict[str, str]) -> int | None:
|
|
74
|
+
"""Parse the Aksha-specific arena control.
|
|
75
|
+
|
|
76
|
+
Zero is an explicit opt-out. A standard ``MALLOC_ARENA_MAX`` value is
|
|
77
|
+
handled by the caller before this parser and always takes precedence.
|
|
78
|
+
"""
|
|
79
|
+
value = environment.get(_ASTRA_ARENA_ENV)
|
|
80
|
+
if value is None:
|
|
81
|
+
return _DEFAULT_ARENA_MAX
|
|
82
|
+
if value == "0":
|
|
83
|
+
return None
|
|
84
|
+
if (
|
|
85
|
+
not value
|
|
86
|
+
or not value.isascii()
|
|
87
|
+
or not value.isdigit()
|
|
88
|
+
or value[0] == "0"
|
|
89
|
+
):
|
|
90
|
+
raise ValueError(
|
|
91
|
+
f"{_ASTRA_ARENA_ENV} must be 0 or a canonical positive integer"
|
|
92
|
+
)
|
|
93
|
+
parsed = int(value)
|
|
94
|
+
if parsed > _MAXIMUM_ARENA_MAX:
|
|
95
|
+
raise ValueError(
|
|
96
|
+
f"{_ASTRA_ARENA_ENV} must be at most {_MAXIMUM_ARENA_MAX}"
|
|
97
|
+
)
|
|
98
|
+
return parsed
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _reexec_environment(
|
|
102
|
+
arguments: tuple[str, ...], environment: dict[str, str]
|
|
103
|
+
) -> dict[str, str] | None:
|
|
104
|
+
"""Return the environment for a required allocator re-exec, if any."""
|
|
105
|
+
# The standard glibc setting is entirely user-owned, including an empty or
|
|
106
|
+
# otherwise unusual value. Never reinterpret or overwrite it.
|
|
107
|
+
if _GLIBC_ARENA_ENV in environment:
|
|
108
|
+
return None
|
|
109
|
+
if _REEXEC_SENTINEL_ENV in environment:
|
|
110
|
+
return None
|
|
111
|
+
if not _is_high_thread_search(arguments) or not _is_linux_glibc():
|
|
112
|
+
return None
|
|
113
|
+
arena_max = _arena_max(environment)
|
|
114
|
+
if arena_max is None:
|
|
115
|
+
return None
|
|
116
|
+
updated = environment.copy()
|
|
117
|
+
updated[_GLIBC_ARENA_ENV] = str(arena_max)
|
|
118
|
+
updated[_REEXEC_SENTINEL_ENV] = "1"
|
|
119
|
+
return updated
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _dispatch() -> object:
|
|
123
|
+
"""Import and run the existing CLI only after allocator routing."""
|
|
124
|
+
from .main import main as astra_main
|
|
125
|
+
|
|
126
|
+
return astra_main()
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def main() -> object:
|
|
130
|
+
"""Run the Aksha console command with the high-thread arena policy."""
|
|
131
|
+
arguments = tuple(sys.argv[1:])
|
|
132
|
+
try:
|
|
133
|
+
environment = _reexec_environment(arguments, dict(os.environ))
|
|
134
|
+
except ValueError as error:
|
|
135
|
+
raise SystemExit(str(error)) from None
|
|
136
|
+
if environment is not None:
|
|
137
|
+
os.execvpe(
|
|
138
|
+
sys.executable,
|
|
139
|
+
[sys.executable, "-m", "aksha.main", *arguments],
|
|
140
|
+
environment,
|
|
141
|
+
)
|
|
142
|
+
raise RuntimeError("os.execvpe returned unexpectedly")
|
|
143
|
+
return _dispatch()
|
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
"""Explicit, bounded reuse of immutable plan7 GPU profile sessions.
|
|
2
|
+
|
|
3
|
+
The cache is deliberately process-local and opt-in. A normal one-shot Aksha
|
|
4
|
+
CLI invocation therefore retains its existing ownership and cleanup behavior.
|
|
5
|
+
Long-lived callers may own one cache, pass it to successive searches, and close
|
|
6
|
+
it deterministically when the worker retires.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
import hashlib
|
|
13
|
+
import os
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from threading import RLock
|
|
16
|
+
import time
|
|
17
|
+
from typing import Any, Callable, Hashable, Optional, Union
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
PROFILE_SEMANTICS = "canonical-amino-local-multihit-exact-forward-v1"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class GPUProfileCacheBusyError(RuntimeError):
|
|
24
|
+
"""Raised when a cache entry already has an active search lease."""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass(frozen=True)
|
|
28
|
+
class Plan7RuntimeIdentity:
|
|
29
|
+
"""Code and private-ABI identity for one loaded plan7 runtime."""
|
|
30
|
+
|
|
31
|
+
pyhmmer_version: str
|
|
32
|
+
pyhmmer_private_abi_sha256: str
|
|
33
|
+
adapter_sha256: str
|
|
34
|
+
native_extension_sha256: str
|
|
35
|
+
pipeline_extension_sha256: str
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(frozen=True)
|
|
39
|
+
class GPUProfileCacheKey:
|
|
40
|
+
"""Every input that can change or invalidate a cached profile session."""
|
|
41
|
+
|
|
42
|
+
canonical_base: Path
|
|
43
|
+
manifest_sha256: str
|
|
44
|
+
pressed_stat_token: Hashable
|
|
45
|
+
runtime: Plan7RuntimeIdentity
|
|
46
|
+
device_key: Hashable
|
|
47
|
+
build_workers: int
|
|
48
|
+
selection_workers: int
|
|
49
|
+
profile_semantics: str
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass
|
|
53
|
+
class _CacheEntry:
|
|
54
|
+
key: GPUProfileCacheKey
|
|
55
|
+
pairs: tuple[Any, ...]
|
|
56
|
+
session: Any
|
|
57
|
+
leased: bool = False
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _sha256_file(path: Path) -> str:
|
|
61
|
+
digest = hashlib.sha256()
|
|
62
|
+
with path.open("rb") as stream:
|
|
63
|
+
for block in iter(lambda: stream.read(1024 * 1024), b""):
|
|
64
|
+
digest.update(block)
|
|
65
|
+
return digest.hexdigest()
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def plan7_runtime_identity() -> Plan7RuntimeIdentity:
|
|
69
|
+
"""Fingerprint the already-loaded plan7/PyHMMER implementation once.
|
|
70
|
+
|
|
71
|
+
The cache cannot cross a process boundary, but recording the exact Python
|
|
72
|
+
adapter and native extension artifacts makes its compatibility contract
|
|
73
|
+
explicit and auditable instead of relying only on a package version.
|
|
74
|
+
"""
|
|
75
|
+
import astra_pyhmmer
|
|
76
|
+
from plan7_gpu import _native, _pipeline
|
|
77
|
+
from plan7_gpu import adapter
|
|
78
|
+
from plan7_gpu._abi import pyhmmer_abi_fingerprint
|
|
79
|
+
|
|
80
|
+
def module_digest(module: Any) -> str:
|
|
81
|
+
origin = getattr(module, "__file__", None)
|
|
82
|
+
if not origin:
|
|
83
|
+
raise RuntimeError("plan7 runtime module has no filesystem origin")
|
|
84
|
+
return _sha256_file(Path(origin).resolve(strict=True))
|
|
85
|
+
|
|
86
|
+
return Plan7RuntimeIdentity(
|
|
87
|
+
pyhmmer_version=str(astra_pyhmmer.__version__),
|
|
88
|
+
pyhmmer_private_abi_sha256=pyhmmer_abi_fingerprint(),
|
|
89
|
+
adapter_sha256=module_digest(adapter),
|
|
90
|
+
native_extension_sha256=module_digest(_native),
|
|
91
|
+
pipeline_extension_sha256=module_digest(_pipeline),
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class GPUProfileSessionLease:
|
|
96
|
+
"""Exclusive access to one cached immutable profile session."""
|
|
97
|
+
|
|
98
|
+
__slots__ = (
|
|
99
|
+
"_cache",
|
|
100
|
+
"_entry",
|
|
101
|
+
"_released",
|
|
102
|
+
"reused",
|
|
103
|
+
"validation_seconds",
|
|
104
|
+
"profile_load_seconds",
|
|
105
|
+
"session_build_seconds",
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
def __init__(
|
|
109
|
+
self,
|
|
110
|
+
cache: GPUProfileSessionCache,
|
|
111
|
+
entry: _CacheEntry,
|
|
112
|
+
*,
|
|
113
|
+
reused: bool,
|
|
114
|
+
validation_seconds: float,
|
|
115
|
+
profile_load_seconds: float,
|
|
116
|
+
session_build_seconds: float,
|
|
117
|
+
) -> None:
|
|
118
|
+
self._cache = cache
|
|
119
|
+
self._entry = entry
|
|
120
|
+
self._released = False
|
|
121
|
+
self.reused = reused
|
|
122
|
+
self.validation_seconds = validation_seconds
|
|
123
|
+
self.profile_load_seconds = profile_load_seconds
|
|
124
|
+
self.session_build_seconds = session_build_seconds
|
|
125
|
+
|
|
126
|
+
def _require_open(self) -> _CacheEntry:
|
|
127
|
+
if self._released or self._entry is None:
|
|
128
|
+
raise RuntimeError("GPU profile-session lease is closed")
|
|
129
|
+
return self._entry
|
|
130
|
+
|
|
131
|
+
@property
|
|
132
|
+
def profile_pairs(self) -> tuple[Any, ...]:
|
|
133
|
+
return self._require_open().pairs
|
|
134
|
+
|
|
135
|
+
@property
|
|
136
|
+
def cache_key(self) -> GPUProfileCacheKey:
|
|
137
|
+
return self._require_open().key
|
|
138
|
+
|
|
139
|
+
@property
|
|
140
|
+
def closed(self) -> bool:
|
|
141
|
+
entry = self._entry
|
|
142
|
+
return self._released or entry is None or bool(entry.session.closed)
|
|
143
|
+
|
|
144
|
+
@property
|
|
145
|
+
def statistics(self) -> dict[str, int]:
|
|
146
|
+
return self._require_open().session.statistics
|
|
147
|
+
|
|
148
|
+
def __len__(self) -> int:
|
|
149
|
+
return len(self._require_open().session)
|
|
150
|
+
|
|
151
|
+
def select(self, indices: Any) -> Any:
|
|
152
|
+
return self._require_open().session.select(indices)
|
|
153
|
+
|
|
154
|
+
def close(self) -> None:
|
|
155
|
+
if not self._released:
|
|
156
|
+
cache = self._cache
|
|
157
|
+
entry = self._entry
|
|
158
|
+
try:
|
|
159
|
+
cache._release(entry)
|
|
160
|
+
finally:
|
|
161
|
+
# A retired lease must not keep the cache, its 1.15 GB profile
|
|
162
|
+
# tuple, or its native session alive through stale references.
|
|
163
|
+
self._released = True
|
|
164
|
+
self._cache = None
|
|
165
|
+
self._entry = None
|
|
166
|
+
|
|
167
|
+
def __enter__(self) -> GPUProfileSessionLease:
|
|
168
|
+
self._require_open()
|
|
169
|
+
return self
|
|
170
|
+
|
|
171
|
+
def __exit__(self, *_: Any) -> None:
|
|
172
|
+
self.close()
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class GPUProfileSessionReservation:
|
|
176
|
+
"""An exclusive pre-allocation claim for one upcoming cache lease."""
|
|
177
|
+
|
|
178
|
+
__slots__ = ("_cache", "_active")
|
|
179
|
+
|
|
180
|
+
def __init__(self, cache: GPUProfileSessionCache) -> None:
|
|
181
|
+
self._cache = cache
|
|
182
|
+
self._active = True
|
|
183
|
+
|
|
184
|
+
@property
|
|
185
|
+
def active(self) -> bool:
|
|
186
|
+
return self._active
|
|
187
|
+
|
|
188
|
+
def close(self) -> None:
|
|
189
|
+
if self._active:
|
|
190
|
+
cache = self._cache
|
|
191
|
+
try:
|
|
192
|
+
cache._cancel_reservation(self)
|
|
193
|
+
finally:
|
|
194
|
+
if not self._active:
|
|
195
|
+
self._cache = None
|
|
196
|
+
|
|
197
|
+
def __enter__(self) -> GPUProfileSessionReservation:
|
|
198
|
+
if not self._active:
|
|
199
|
+
raise RuntimeError("GPU profile-session reservation is closed")
|
|
200
|
+
return self
|
|
201
|
+
|
|
202
|
+
def __exit__(self, *_: Any) -> None:
|
|
203
|
+
self.close()
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
class GPUProfileSessionCache:
|
|
207
|
+
"""A one-entry, exclusive-lease cache for very large profile sessions.
|
|
208
|
+
|
|
209
|
+
PFAM's immutable host snapshot is roughly 1.15 GB, so this first production
|
|
210
|
+
seam intentionally permits only one resident database and one active search
|
|
211
|
+
at a time. A different key evicts the idle entry before allocating its
|
|
212
|
+
replacement; two full sessions are never retained by the cache itself.
|
|
213
|
+
"""
|
|
214
|
+
|
|
215
|
+
def __init__(
|
|
216
|
+
self,
|
|
217
|
+
*,
|
|
218
|
+
runtime_identity: Optional[Plan7RuntimeIdentity] = None,
|
|
219
|
+
validator: Optional[Callable[..., Any]] = None,
|
|
220
|
+
loader: Optional[Callable[..., Any]] = None,
|
|
221
|
+
session_factory: Optional[Callable[..., Any]] = None,
|
|
222
|
+
) -> None:
|
|
223
|
+
if validator is None or loader is None or session_factory is None:
|
|
224
|
+
from plan7_gpu import ProfileSession, load_pressed_profiles
|
|
225
|
+
from plan7_gpu.pressed_manifest import validate_pressed_manifest
|
|
226
|
+
|
|
227
|
+
if validator is None:
|
|
228
|
+
validator = validate_pressed_manifest
|
|
229
|
+
if loader is None:
|
|
230
|
+
loader = load_pressed_profiles
|
|
231
|
+
if session_factory is None:
|
|
232
|
+
session_factory = ProfileSession
|
|
233
|
+
self._runtime = runtime_identity or plan7_runtime_identity()
|
|
234
|
+
self._validator = validator
|
|
235
|
+
self._loader = loader
|
|
236
|
+
self._session_factory = session_factory
|
|
237
|
+
self._lock = RLock()
|
|
238
|
+
self._entry: Optional[_CacheEntry] = None
|
|
239
|
+
self._reservation: Optional[GPUProfileSessionReservation] = None
|
|
240
|
+
self._closed = False
|
|
241
|
+
|
|
242
|
+
@property
|
|
243
|
+
def closed(self) -> bool:
|
|
244
|
+
with self._lock:
|
|
245
|
+
return self._closed
|
|
246
|
+
|
|
247
|
+
@property
|
|
248
|
+
def resident(self) -> bool:
|
|
249
|
+
with self._lock:
|
|
250
|
+
return self._entry is not None
|
|
251
|
+
|
|
252
|
+
@property
|
|
253
|
+
def resident_key(self) -> Optional[GPUProfileCacheKey]:
|
|
254
|
+
with self._lock:
|
|
255
|
+
return None if self._entry is None else self._entry.key
|
|
256
|
+
|
|
257
|
+
def reserve(self) -> GPUProfileSessionReservation:
|
|
258
|
+
"""Claim the cache before allocating a per-search target batch."""
|
|
259
|
+
with self._lock:
|
|
260
|
+
if self._closed:
|
|
261
|
+
raise RuntimeError("GPU profile-session cache is closed")
|
|
262
|
+
if (
|
|
263
|
+
self._reservation is not None
|
|
264
|
+
or (self._entry is not None and self._entry.leased)
|
|
265
|
+
):
|
|
266
|
+
raise GPUProfileCacheBusyError(
|
|
267
|
+
"GPU profile-session cache already has an active search lease"
|
|
268
|
+
)
|
|
269
|
+
reservation = GPUProfileSessionReservation(self)
|
|
270
|
+
self._reservation = reservation
|
|
271
|
+
return reservation
|
|
272
|
+
|
|
273
|
+
def acquire(
|
|
274
|
+
self,
|
|
275
|
+
pressed_base: Union[str, os.PathLike[str]],
|
|
276
|
+
manifest_path: Union[str, os.PathLike[str]],
|
|
277
|
+
*,
|
|
278
|
+
device_key: Hashable,
|
|
279
|
+
build_workers: int,
|
|
280
|
+
selection_workers: int = 0,
|
|
281
|
+
profile_semantics: str = PROFILE_SEMANTICS,
|
|
282
|
+
reservation: Optional[GPUProfileSessionReservation] = None,
|
|
283
|
+
) -> GPUProfileSessionLease:
|
|
284
|
+
if isinstance(build_workers, bool) or not isinstance(build_workers, int):
|
|
285
|
+
raise TypeError("build_workers must be a positive integer")
|
|
286
|
+
if build_workers <= 0:
|
|
287
|
+
raise ValueError("build_workers must be a positive integer")
|
|
288
|
+
if (
|
|
289
|
+
isinstance(selection_workers, bool)
|
|
290
|
+
or not isinstance(selection_workers, int)
|
|
291
|
+
):
|
|
292
|
+
raise TypeError("selection_workers must be a nonnegative integer")
|
|
293
|
+
if selection_workers < 0:
|
|
294
|
+
raise ValueError("selection_workers must be a nonnegative integer")
|
|
295
|
+
if not isinstance(profile_semantics, str) or not profile_semantics:
|
|
296
|
+
raise TypeError("profile_semantics must be a non-empty string")
|
|
297
|
+
try:
|
|
298
|
+
hash(device_key)
|
|
299
|
+
except TypeError as error:
|
|
300
|
+
raise TypeError("device_key must be hashable") from error
|
|
301
|
+
|
|
302
|
+
canonical_base = Path(pressed_base).resolve(strict=False)
|
|
303
|
+
with self._lock:
|
|
304
|
+
if self._closed:
|
|
305
|
+
raise RuntimeError("GPU profile-session cache is closed")
|
|
306
|
+
if reservation is None:
|
|
307
|
+
if (
|
|
308
|
+
self._reservation is not None
|
|
309
|
+
or (self._entry is not None and self._entry.leased)
|
|
310
|
+
):
|
|
311
|
+
raise GPUProfileCacheBusyError(
|
|
312
|
+
"GPU profile-session cache already has an active search lease"
|
|
313
|
+
)
|
|
314
|
+
elif (
|
|
315
|
+
reservation._cache is not self
|
|
316
|
+
or not reservation._active
|
|
317
|
+
or self._reservation is not reservation
|
|
318
|
+
):
|
|
319
|
+
raise RuntimeError("GPU profile-session cache reservation is invalid")
|
|
320
|
+
|
|
321
|
+
validation_started = time.perf_counter()
|
|
322
|
+
validation = self._validator(canonical_base, manifest_path)
|
|
323
|
+
validation_seconds = time.perf_counter() - validation_started
|
|
324
|
+
if validation.canonical_base != canonical_base:
|
|
325
|
+
raise ValueError("pressed manifest canonical database mismatch")
|
|
326
|
+
manifest_sha256 = validation.manifest_sha256
|
|
327
|
+
if (
|
|
328
|
+
not isinstance(manifest_sha256, str)
|
|
329
|
+
or len(manifest_sha256) != 64
|
|
330
|
+
or any(character not in "0123456789abcdef" for character in manifest_sha256)
|
|
331
|
+
):
|
|
332
|
+
raise RuntimeError("pressed manifest has an invalid SHA256 identity")
|
|
333
|
+
try:
|
|
334
|
+
hash(validation.stat_token)
|
|
335
|
+
except TypeError as error:
|
|
336
|
+
raise RuntimeError("pressed database stat token is not hashable") from error
|
|
337
|
+
|
|
338
|
+
key = GPUProfileCacheKey(
|
|
339
|
+
canonical_base=canonical_base,
|
|
340
|
+
manifest_sha256=manifest_sha256,
|
|
341
|
+
pressed_stat_token=validation.stat_token,
|
|
342
|
+
runtime=self._runtime,
|
|
343
|
+
device_key=device_key,
|
|
344
|
+
build_workers=build_workers,
|
|
345
|
+
selection_workers=selection_workers,
|
|
346
|
+
profile_semantics=profile_semantics,
|
|
347
|
+
)
|
|
348
|
+
entry = self._entry
|
|
349
|
+
if entry is not None and entry.key == key and not entry.session.closed:
|
|
350
|
+
entry.leased = True
|
|
351
|
+
self._consume_reservation(reservation)
|
|
352
|
+
return GPUProfileSessionLease(
|
|
353
|
+
self,
|
|
354
|
+
entry,
|
|
355
|
+
reused=True,
|
|
356
|
+
validation_seconds=validation_seconds,
|
|
357
|
+
profile_load_seconds=0.0,
|
|
358
|
+
session_build_seconds=0.0,
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
if entry is not None:
|
|
362
|
+
self._entry = None
|
|
363
|
+
try:
|
|
364
|
+
entry.session.close()
|
|
365
|
+
finally:
|
|
366
|
+
# Do not carry the old PFAM tuple into replacement load:
|
|
367
|
+
# even this local entry would otherwise overlap two large
|
|
368
|
+
# immutable profile sets during key invalidation.
|
|
369
|
+
entry.pairs = ()
|
|
370
|
+
entry.session = None
|
|
371
|
+
entry = None
|
|
372
|
+
|
|
373
|
+
load_started = time.perf_counter()
|
|
374
|
+
pairs = tuple(self._loader(canonical_base, manifest=manifest_path))
|
|
375
|
+
profile_load_seconds = time.perf_counter() - load_started
|
|
376
|
+
if len(pairs) != validation.model_count:
|
|
377
|
+
raise RuntimeError(
|
|
378
|
+
"loaded profile count differs from validated manifest"
|
|
379
|
+
)
|
|
380
|
+
session_started = time.perf_counter()
|
|
381
|
+
session = self._session_factory(
|
|
382
|
+
pairs,
|
|
383
|
+
build_workers=build_workers,
|
|
384
|
+
selection_workers=selection_workers,
|
|
385
|
+
)
|
|
386
|
+
session_build_seconds = time.perf_counter() - session_started
|
|
387
|
+
try:
|
|
388
|
+
if session.closed:
|
|
389
|
+
raise RuntimeError("new GPU profile session is already closed")
|
|
390
|
+
if len(session) != len(pairs):
|
|
391
|
+
raise RuntimeError(
|
|
392
|
+
"GPU profile session does not cover loaded profiles"
|
|
393
|
+
)
|
|
394
|
+
except BaseException:
|
|
395
|
+
session.close()
|
|
396
|
+
raise
|
|
397
|
+
entry = _CacheEntry(key=key, pairs=pairs, session=session, leased=True)
|
|
398
|
+
self._entry = entry
|
|
399
|
+
self._consume_reservation(reservation)
|
|
400
|
+
return GPUProfileSessionLease(
|
|
401
|
+
self,
|
|
402
|
+
entry,
|
|
403
|
+
reused=False,
|
|
404
|
+
validation_seconds=validation_seconds,
|
|
405
|
+
profile_load_seconds=profile_load_seconds,
|
|
406
|
+
session_build_seconds=session_build_seconds,
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
def _consume_reservation(
|
|
410
|
+
self,
|
|
411
|
+
reservation: Optional[GPUProfileSessionReservation],
|
|
412
|
+
) -> None:
|
|
413
|
+
if reservation is not None:
|
|
414
|
+
if self._reservation is not reservation or not reservation._active:
|
|
415
|
+
raise RuntimeError("GPU profile-session cache reservation is invalid")
|
|
416
|
+
self._reservation = None
|
|
417
|
+
reservation._active = False
|
|
418
|
+
reservation._cache = None
|
|
419
|
+
|
|
420
|
+
def _cancel_reservation(
|
|
421
|
+
self, reservation: GPUProfileSessionReservation
|
|
422
|
+
) -> None:
|
|
423
|
+
with self._lock:
|
|
424
|
+
if self._reservation is reservation and reservation._active:
|
|
425
|
+
self._reservation = None
|
|
426
|
+
reservation._active = False
|
|
427
|
+
reservation._cache = None
|
|
428
|
+
return
|
|
429
|
+
if reservation._active:
|
|
430
|
+
raise RuntimeError("GPU profile-session cache reservation is invalid")
|
|
431
|
+
|
|
432
|
+
def _release(self, entry: _CacheEntry) -> None:
|
|
433
|
+
with self._lock:
|
|
434
|
+
if self._entry is not entry or not entry.leased:
|
|
435
|
+
raise RuntimeError("GPU profile-session cache lease is invalid")
|
|
436
|
+
entry.leased = False
|
|
437
|
+
if self._closed:
|
|
438
|
+
self._entry = None
|
|
439
|
+
entry.session.close()
|
|
440
|
+
|
|
441
|
+
def close(self) -> None:
|
|
442
|
+
with self._lock:
|
|
443
|
+
if self._closed:
|
|
444
|
+
return
|
|
445
|
+
self._closed = True
|
|
446
|
+
reservation = self._reservation
|
|
447
|
+
if reservation is not None:
|
|
448
|
+
self._reservation = None
|
|
449
|
+
reservation._active = False
|
|
450
|
+
reservation._cache = None
|
|
451
|
+
entry = self._entry
|
|
452
|
+
if entry is not None and not entry.leased:
|
|
453
|
+
self._entry = None
|
|
454
|
+
entry.session.close()
|
|
455
|
+
|
|
456
|
+
def __enter__(self) -> GPUProfileSessionCache:
|
|
457
|
+
with self._lock:
|
|
458
|
+
if self._closed:
|
|
459
|
+
raise RuntimeError("GPU profile-session cache is closed")
|
|
460
|
+
return self
|
|
461
|
+
|
|
462
|
+
def __exit__(self, *_: Any) -> None:
|
|
463
|
+
self.close()
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
__all__ = [
|
|
467
|
+
"GPUProfileCacheBusyError",
|
|
468
|
+
"GPUProfileCacheKey",
|
|
469
|
+
"GPUProfileSessionCache",
|
|
470
|
+
"GPUProfileSessionLease",
|
|
471
|
+
"GPUProfileSessionReservation",
|
|
472
|
+
"PROFILE_SEMANTICS",
|
|
473
|
+
"Plan7RuntimeIdentity",
|
|
474
|
+
"plan7_runtime_identity",
|
|
475
|
+
]
|