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 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
+ ]