|
1 | 1 | from __future__ import annotations |
2 | 2 |
|
| 3 | +from collections import OrderedDict |
3 | 4 | from pathlib import Path |
4 | 5 | from functools import lru_cache |
5 | 6 | import gzip |
| 7 | +import hashlib |
6 | 8 | import json |
7 | 9 | import os |
8 | 10 | import subprocess |
| 11 | +import threading |
9 | 12 | from typing import Tuple |
10 | 13 |
|
11 | 14 | import numpy as np |
|
22 | 25 | } |
23 | 26 | _VALID_AAS = set("ACDEFGHIKLMNPQRSTVWY") |
24 | 27 | _MODEL_CACHE_SIZE = max(int(os.environ.get("CATPRED_MODEL_CACHE_SIZE", "6")), 0) |
| 28 | +_PREDICTION_CACHE_SIZE = max(int(os.environ.get("CATPRED_PREDICTION_CACHE_SIZE", "128")), 0) |
| 29 | +_PREDICTION_CACHE: OrderedDict[tuple, str] = OrderedDict() |
| 30 | +_PREDICTION_CACHE_LOCK = threading.Lock() |
25 | 31 |
|
26 | 32 |
|
27 | 33 | def _validate_parameter(parameter: str) -> str: |
@@ -285,6 +291,89 @@ def _expand_deduplicated_prediction_output( |
285 | 291 | expanded_df.to_csv(original_paths.output_csv, index=False) |
286 | 292 |
|
287 | 293 |
|
| 294 | +def _file_digest(path: str) -> str: |
| 295 | + digest = hashlib.sha256() |
| 296 | + with open(path, "rb") as handle: |
| 297 | + for chunk in iter(lambda: handle.read(1024 * 1024), b""): |
| 298 | + digest.update(chunk) |
| 299 | + return digest.hexdigest() |
| 300 | + |
| 301 | + |
| 302 | +def _prediction_checkpoint_paths(checkpoint_dir: str, repo_root: Path) -> tuple[str, ...]: |
| 303 | + checkpoint_path = Path(checkpoint_dir) |
| 304 | + if not checkpoint_path.is_absolute(): |
| 305 | + checkpoint_path = (repo_root / checkpoint_path).resolve() |
| 306 | + if checkpoint_path.is_file(): |
| 307 | + return (str(checkpoint_path),) |
| 308 | + if not checkpoint_path.is_dir(): |
| 309 | + raise FileNotFoundError(f'Checkpoint directory not found: "{checkpoint_path}"') |
| 310 | + model_paths = sorted(str(path.resolve()) for path in checkpoint_path.rglob("model.pt")) |
| 311 | + if not model_paths: |
| 312 | + raise FileNotFoundError(f'No model.pt checkpoints found in "{checkpoint_path}"') |
| 313 | + return tuple(model_paths) |
| 314 | + |
| 315 | + |
| 316 | +def _prediction_cache_key( |
| 317 | + parameter: str, |
| 318 | + request: PredictionRequest, |
| 319 | + paths: PreparedInputPaths, |
| 320 | + repo_root: Path, |
| 321 | +) -> tuple: |
| 322 | + protein_records_digest = None |
| 323 | + if request.protein_records_file: |
| 324 | + protein_records_path = _resolve_existing_path( |
| 325 | + request.protein_records_file, |
| 326 | + repo_root=repo_root, |
| 327 | + purpose="Protein records file", |
| 328 | + ) |
| 329 | + protein_records_digest = _file_digest(str(protein_records_path)) |
| 330 | + |
| 331 | + checkpoint_paths = _prediction_checkpoint_paths(request.checkpoint_dir, repo_root) |
| 332 | + return ( |
| 333 | + parameter, |
| 334 | + bool(request.use_gpu), |
| 335 | + _file_digest(paths.input_csv), |
| 336 | + protein_records_digest, |
| 337 | + _checkpoint_fingerprint(checkpoint_paths), |
| 338 | + ) |
| 339 | + |
| 340 | + |
| 341 | +def _prediction_cache_get(cache_key: tuple) -> str | None: |
| 342 | + if _PREDICTION_CACHE_SIZE <= 0: |
| 343 | + return None |
| 344 | + with _PREDICTION_CACHE_LOCK: |
| 345 | + cached = _PREDICTION_CACHE.get(cache_key) |
| 346 | + if cached is not None: |
| 347 | + _PREDICTION_CACHE.move_to_end(cache_key) |
| 348 | + return cached |
| 349 | + |
| 350 | + |
| 351 | +def _prediction_cache_put(cache_key: tuple, csv_text: str) -> None: |
| 352 | + if _PREDICTION_CACHE_SIZE <= 0: |
| 353 | + return |
| 354 | + with _PREDICTION_CACHE_LOCK: |
| 355 | + _PREDICTION_CACHE[cache_key] = csv_text |
| 356 | + _PREDICTION_CACHE.move_to_end(cache_key) |
| 357 | + while len(_PREDICTION_CACHE) > _PREDICTION_CACHE_SIZE: |
| 358 | + _PREDICTION_CACHE.popitem(last=False) |
| 359 | + |
| 360 | + |
| 361 | +def _write_cached_prediction( |
| 362 | + cached_csv: str, |
| 363 | + paths: PreparedInputPaths, |
| 364 | + repo_root: str | None, |
| 365 | + results_dir: str, |
| 366 | +) -> str: |
| 367 | + results_path = Path(results_dir) |
| 368 | + if not results_path.is_absolute(): |
| 369 | + results_path = (_resolve_repo_root(repo_root) / results_path).resolve() |
| 370 | + results_path.mkdir(parents=True, exist_ok=True) |
| 371 | + |
| 372 | + final_output = results_path / Path(paths.output_csv).name |
| 373 | + final_output.write_text(cached_csv, encoding="utf-8") |
| 374 | + return str(final_output) |
| 375 | + |
| 376 | + |
288 | 377 | @lru_cache(maxsize=_MODEL_CACHE_SIZE) |
289 | 378 | def _load_cached_model_objects( |
290 | 379 | checkpoint_paths: tuple[str, ...], |
@@ -441,14 +530,22 @@ def run_inprocess_prediction_pipeline( |
441 | 530 | ) -> str: |
442 | 531 | parameter = _validate_parameter(request.parameter) |
443 | 532 | paths = prepare_prediction_inputs(parameter, request.input_file, request.repo_root) |
| 533 | + root = _resolve_repo_root(request.repo_root) |
| 534 | + cache_key = _prediction_cache_key(parameter, request, paths, root) |
| 535 | + cached_csv = _prediction_cache_get(cache_key) |
| 536 | + if cached_csv is not None: |
| 537 | + return _write_cached_prediction(cached_csv, paths, request.repo_root, results_dir) |
| 538 | + |
444 | 539 | if request.protein_records_file: |
445 | 540 | prediction_paths, was_deduplicated = paths, False |
446 | 541 | else: |
447 | 542 | prediction_paths, was_deduplicated = _deduplicate_prediction_input(paths) |
448 | 543 | run_inprocess_prediction(request, prediction_paths) |
449 | 544 | if was_deduplicated: |
450 | 545 | _expand_deduplicated_prediction_output(paths, prediction_paths) |
451 | | - return _write_postprocessed_predictions(parameter, paths, request.repo_root, results_dir) |
| 546 | + final_output = _write_postprocessed_predictions(parameter, paths, request.repo_root, results_dir) |
| 547 | + _prediction_cache_put(cache_key, Path(final_output).read_text(encoding="utf-8")) |
| 548 | + return final_output |
452 | 549 |
|
453 | 550 |
|
454 | 551 | def _write_postprocessed_predictions( |
|
0 commit comments