| """Lazy model / tokenizer loader with HF Hub + local fallback.""" |
| from __future__ import annotations |
|
|
| import os |
| import logging |
| from typing import Tuple |
|
|
| import torch |
| from transformers import AutoModelForCausalLM, AutoTokenizer |
|
|
| from src.config import HF_MODEL_ID, LEGACY_MODEL_PATH, FORCE_CPU, DTYPE |
|
|
| logger = logging.getLogger(__name__) |
|
|
|
|
| def _resolve_model_path() -> str: |
| """Return the local path to the model, falling back from HF Hub.""" |
| |
| env_path = os.environ.get("RYUGAKU_LOCAL_MODEL") |
| if env_path and os.path.isdir(env_path): |
| logger.info("Using local model from RYUGAKU_LOCAL_MODEL: %s", env_path) |
| return env_path |
|
|
| |
| if os.path.isdir(LEGACY_MODEL_PATH): |
| logger.info("Using legacy local model: %s", LEGACY_MODEL_PATH) |
| return LEGACY_MODEL_PATH |
|
|
| |
| logger.info("Using HF Hub model id: %s", HF_MODEL_ID) |
| return HF_MODEL_ID |
|
|
|
|
| class ModelCache: |
| """Simple singleton cache for the model and tokenizer.""" |
|
|
| _instance: ModelCache | None = None |
| model: AutoModelForCausalLM | None = None |
| tokenizer: AutoTokenizer | None = None |
| model_path: str | None = None |
| loading: bool = False |
|
|
| def __new__(cls) -> ModelCache: |
| if cls._instance is None: |
| cls._instance = super().__new__(cls) |
| return cls._instance |
|
|
| def load(self) -> Tuple[AutoModelForCausalLM, AutoTokenizer]: |
| if self.model is not None and self.tokenizer is not None: |
| return self.model, self.tokenizer |
|
|
| if self.loading: |
| raise RuntimeError("Model is already loading") |
|
|
| self.loading = True |
| try: |
| self.model_path = _resolve_model_path() |
| logger.info("Loading tokenizer from %s", self.model_path) |
| self.tokenizer = AutoTokenizer.from_pretrained( |
| self.model_path, |
| trust_remote_code=True, |
| ) |
| |
| if self.tokenizer.pad_token is None: |
| self.tokenizer.pad_token = self.tokenizer.eos_token |
|
|
| logger.info("Loading model from %s", self.model_path) |
| kwargs = { |
| "trust_remote_code": True, |
| } |
| if FORCE_CPU: |
| kwargs["dtype"] = torch.float32 |
| kwargs["device_map"] = "cpu" |
| else: |
| kwargs["dtype"] = DTYPE |
| kwargs["device_map"] = "auto" |
|
|
| self.model = AutoModelForCausalLM.from_pretrained( |
| self.model_path, |
| **kwargs, |
| ) |
| logger.info("Model loaded successfully") |
| finally: |
| self.loading = False |
|
|
| return self.model, self.tokenizer |
|
|
|
|
| def get_model_and_tokenizer() -> Tuple[AutoModelForCausalLM, AutoTokenizer]: |
| return ModelCache().load() |
|
|
|
|
| def is_model_ready() -> bool: |
| cache = ModelCache() |
| return cache.model is not None and cache.tokenizer is not None |
|
|