Spaces:
Running
Running
Download webapp/model_cache.py from claessonlab/Taxonova: direct link, hf CLI and curl.
- Browser
- Download file 4.53 kB
-
https://huggingface.co/spaces/claessonlab/Taxonova/resolve/main/webapp/model_cache.py
- Command line
-
hf download hf://spaces/claessonlab/Taxonova/webapp/model_cache.py
-
curl -L -o model_cache.py https://huggingface.co/spaces/claessonlab/Taxonova/resolve/main/webapp/model_cache.py
4.53 kB
| """Models are loaded when first asked for and kept in a bounded cache. | |
| The Space has 16 GB of memory and the registry lists more models each | |
| release, so start-up loads only the default model and the others are loaded | |
| on first use. Loaded models are kept, least recently used first out, while | |
| their estimated memory stays within a budget (``TAXONOVA_WEB_MODEL_BUDGET_MB``, | |
| default ``DEFAULT_BUDGET_MB``). The estimate is the checkpoint file size times | |
| ``MEMORY_PER_FILE_BYTE``. The model being asked for is always loaded, even if | |
| it alone exceeds the budget. | |
| Measured 2026-11-04 for the 26.11 models (taxonova 26.11.0, torch 2.5.1 CPU, | |
| 2 threads, one model per process): resident memory grew by 263 to 640 MB per | |
| loaded model (1.2 to 1.3 times the file size; v4 334 MB, v4v5 640 MB), and | |
| loading peaked about 0.35 GB above that. A 2,000-read job (rRNA screen and | |
| classification) took 25 to 58 s and raised the process peak to 0.8 to 1.2 GB. | |
| Jobs run one at a time in one worker thread, so a model is never evicted | |
| while it is in use; the lock only protects the bookkeeping against the page | |
| and health requests that read it. | |
| """ | |
| from __future__ import annotations | |
| import ctypes | |
| import gc | |
| import logging | |
| import os | |
| import threading | |
| from collections import OrderedDict | |
| from pathlib import Path | |
| from typing import Callable, Dict, List, Optional | |
| logger = logging.getLogger(__name__) | |
| #: Estimated memory per byte of checkpoint file: the measured 1.2 to 1.3 once | |
| #: loaded, with margin for the load peak and allocator overhead. | |
| MEMORY_PER_FILE_BYTE = 2.0 | |
| #: About 4 GB resident at the measured ratio, which leaves most of the | |
| #: Space's 16 GB for a 20,000-sequence job, the rRNA screen and the server. | |
| DEFAULT_BUDGET_MB = 6000 | |
| BUDGET_ENV = "TAXONOVA_WEB_MODEL_BUDGET_MB" | |
| def budget_mb() -> int: | |
| try: | |
| return max(0, int(os.environ.get(BUDGET_ENV, DEFAULT_BUDGET_MB))) | |
| except ValueError: | |
| return DEFAULT_BUDGET_MB | |
| def estimate_mb(path) -> float: | |
| return Path(path).stat().st_size * MEMORY_PER_FILE_BYTE / 2 ** 20 | |
| def _release_memory() -> None: | |
| gc.collect() | |
| try: # hand freed heap pages back to the system (glibc only) | |
| ctypes.CDLL("libc.so.6").malloc_trim(0) | |
| except (OSError, AttributeError): | |
| pass | |
| class ModelCache: | |
| """Loaded models by id, least recently used first, within a memory budget.""" | |
| def __init__(self, loader: Callable[[str], object], budget: Optional[float] = None, | |
| estimate: Callable[[str], float] = estimate_mb): | |
| self._loader = loader | |
| self._estimate = estimate | |
| self.budget = budget_mb() if budget is None else budget | |
| self._items: "OrderedDict[str, object]" = OrderedDict() | |
| self._sizes: Dict[str, float] = {} | |
| self._lock = threading.Lock() | |
| self.loads = 0 | |
| self.evictions = 0 | |
| def loaded_ids(self) -> List[str]: | |
| with self._lock: | |
| return list(self._items) | |
| def used_mb(self) -> float: | |
| with self._lock: | |
| return sum(self._sizes.values()) | |
| def is_loaded(self, model_id: str) -> bool: | |
| with self._lock: | |
| return model_id in self._items | |
| def put(self, model_id: str, loaded, size_mb: float = 0.0) -> None: | |
| """Insert an already loaded model (tests, or a model loaded elsewhere).""" | |
| with self._lock: | |
| self._items[model_id] = loaded | |
| self._items.move_to_end(model_id) | |
| self._sizes[model_id] = size_mb | |
| def get(self, model_id: str, path) -> object: | |
| """The loaded model, loading it (and evicting others) if needed.""" | |
| with self._lock: | |
| if model_id in self._items: | |
| self._items.move_to_end(model_id) | |
| return self._items[model_id] | |
| need = self._estimate(str(path)) | |
| evicted = [] | |
| with self._lock: | |
| while self._items and sum(self._sizes.values()) + need > self.budget: | |
| old, _ = self._items.popitem(last=False) | |
| self._sizes.pop(old, None) | |
| evicted.append(old) | |
| if evicted: | |
| self.evictions += len(evicted) | |
| logger.info("model cache: unloaded %s to make room for %s", ", ".join(evicted), model_id) | |
| _release_memory() | |
| logger.info("loading %s from %s (about %.0f MB)", model_id, path, need) | |
| loaded = self._loader(str(path)) | |
| self.loads += 1 | |
| with self._lock: | |
| self._items[model_id] = loaded | |
| self._sizes[model_id] = need | |
| return loaded | |