### 🚀 ML Models Runtime Preloading - **🔧 models loading**: Перенесена предзагрузка ML моделей из Docker build в runtime startup - Убрана предзагрузка из `Dockerfile` - модели теперь загружаются после монтирования `/dump` папки - Добавлена async функция `preload_models()` в `services/search.py` для фоновой загрузки - Интеграция предзагрузки в `lifespan` функцию `main.py` - Использование `asyncio.run_in_executor()` для неблокирующей загрузки моделей - Исправлена проблема с недоступностью `/dump` папки во время сборки Docker образа
This commit is contained in:
@@ -2,6 +2,14 @@
|
||||
|
||||
## [0.9.19] - 2025-09-01
|
||||
|
||||
### 🚀 ML Models Runtime Preloading
|
||||
- **🔧 models loading**: Перенесена предзагрузка ML моделей из Docker build в runtime startup
|
||||
- Убрана предзагрузка из `Dockerfile` - модели теперь загружаются после монтирования `/dump` папки
|
||||
- Добавлена async функция `preload_models()` в `services/search.py` для фоновой загрузки
|
||||
- Интеграция предзагрузки в `lifespan` функцию `main.py`
|
||||
- Использование `asyncio.run_in_executor()` для неблокирующей загрузки моделей
|
||||
- Исправлена проблема с недоступностью `/dump` папки во время сборки Docker образа
|
||||
|
||||
### 🔧 Reactions Type Compatibility Fix
|
||||
- **🐛 rating functions**: Исправлена ошибка `AttributeError: 'str' object has no attribute 'value'` в создании реакций
|
||||
- Функции `is_positive()` и `is_negative()` в `orm/rating.py` теперь поддерживают как `ReactionKind` enum, так и строки
|
||||
|
||||
@@ -25,9 +25,6 @@ RUN uv sync --no-install-project
|
||||
COPY . .
|
||||
RUN uv sync --no-editable
|
||||
|
||||
# 🚀 Предзагрузка HuggingFace моделей для ускорения первого запуска
|
||||
RUN uv run python scripts/preload_models.py
|
||||
|
||||
# Установка Node.js LTS и npm
|
||||
RUN curl -fsSL https://deb.nodesource.com/setup_lts.x | bash - && \
|
||||
apt-get install -y nsolid \
|
||||
|
||||
7
main.py
7
main.py
@@ -22,7 +22,7 @@ from auth.oauth import oauth_callback, oauth_login
|
||||
from cache.precache import precache_data
|
||||
from cache.revalidator import revalidation_manager
|
||||
from rbac import initialize_rbac
|
||||
from services.search import check_search_service, initialize_search_index, search_service
|
||||
from services.search import check_search_service, initialize_search_index, preload_models, search_service
|
||||
from services.viewed import ViewedStorage
|
||||
from settings import DEV_SERVER_PID_FILE_NAME
|
||||
from storage.redis import redis
|
||||
@@ -263,6 +263,11 @@ async def lifespan(app: Starlette):
|
||||
await initialize_search_index_with_data()
|
||||
print("[lifespan] Search service initialized with Muvera")
|
||||
|
||||
# 🚀 Предзагружаем ML модели после монтирования /dump
|
||||
print("[lifespan] Starting ML models preloading...")
|
||||
await preload_models()
|
||||
print("[lifespan] ML models preloading completed")
|
||||
|
||||
yield
|
||||
finally:
|
||||
print("[lifespan] Shutting down application services")
|
||||
|
||||
@@ -1,107 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
🚀 Предзагрузка HuggingFace моделей для кеширования в Docker
|
||||
|
||||
Этот скрипт загружает модели заранее при сборке Docker образа,
|
||||
чтобы избежать загрузки во время первого запуска приложения.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def get_models_cache_dir() -> str:
|
||||
"""Определяет лучшую папку для кеша моделей"""
|
||||
# Пробуем /dump если доступен для записи
|
||||
dump_path = Path("/dump")
|
||||
print(
|
||||
f"🔍 Checking /dump - exists: {dump_path.exists()}, writable: {os.access('/dump', os.W_OK) if dump_path.exists() else 'N/A'}"
|
||||
)
|
||||
|
||||
if dump_path.exists() and os.access("/dump", os.W_OK):
|
||||
cache_dir = "/dump/huggingface"
|
||||
try:
|
||||
Path(cache_dir).mkdir(parents=True, exist_ok=True)
|
||||
print(f"✅ Using mounted storage: {cache_dir}")
|
||||
return cache_dir
|
||||
except Exception as e:
|
||||
print(f"❌ Failed to create {cache_dir}: {e}")
|
||||
|
||||
# Fallback - локальная папка ./dump
|
||||
cache_dir = "./dump/huggingface"
|
||||
Path(cache_dir).mkdir(parents=True, exist_ok=True)
|
||||
print(f"📁 Using local fallback: {cache_dir}")
|
||||
return cache_dir
|
||||
|
||||
|
||||
# Настройка переменных окружения для кеша
|
||||
MODELS_CACHE_DIR = get_models_cache_dir()
|
||||
os.environ["TRANSFORMERS_CACHE"] = MODELS_CACHE_DIR
|
||||
os.environ["HF_HOME"] = MODELS_CACHE_DIR
|
||||
|
||||
|
||||
def is_model_cached(model_name: str) -> bool:
|
||||
"""🔍 Проверяет наличие модели в кеше"""
|
||||
try:
|
||||
cache_path = Path(MODELS_CACHE_DIR)
|
||||
model_cache_name = f"models--sentence-transformers--{model_name}"
|
||||
model_path = cache_path / model_cache_name
|
||||
|
||||
if not model_path.exists():
|
||||
return False
|
||||
|
||||
# Проверяем наличие snapshots папки (новый формат HuggingFace)
|
||||
snapshots_path = model_path / "snapshots"
|
||||
if snapshots_path.exists():
|
||||
# Ищем любой snapshot с config.json
|
||||
for snapshot_dir in snapshots_path.iterdir():
|
||||
if snapshot_dir.is_dir():
|
||||
config_file = snapshot_dir / "config.json"
|
||||
if config_file.exists():
|
||||
return True
|
||||
|
||||
# Fallback: проверяем старый формат
|
||||
config_file = model_path / "config.json"
|
||||
return config_file.exists()
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
try:
|
||||
from sentence_transformers import SentenceTransformer
|
||||
|
||||
# Создаем папку для кеша
|
||||
Path(MODELS_CACHE_DIR).mkdir(parents=True, exist_ok=True)
|
||||
print(f"📁 Created cache directory: {MODELS_CACHE_DIR}")
|
||||
|
||||
# Список моделей для предзагрузки
|
||||
models = [
|
||||
"paraphrase-multilingual-MiniLM-L12-v2", # Основная многоязычная модель
|
||||
"all-MiniLM-L6-v2", # Fallback модель
|
||||
]
|
||||
|
||||
for model_name in models:
|
||||
try:
|
||||
if is_model_cached(model_name):
|
||||
print(f"🔍 Found cached model: {model_name}")
|
||||
continue
|
||||
|
||||
print(f"🔽 Downloading model: {model_name}")
|
||||
model = SentenceTransformer(model_name, cache_folder=MODELS_CACHE_DIR)
|
||||
print(f"✅ Successfully cached: {model_name}")
|
||||
|
||||
# Освобождаем память
|
||||
del model
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Failed to download {model_name}: {e}")
|
||||
|
||||
print("🚀 Model preloading completed!")
|
||||
|
||||
except ImportError as e:
|
||||
print(f"❌ Failed to import dependencies: {e}")
|
||||
sys.exit(1)
|
||||
except Exception as e:
|
||||
print(f"❌ Unexpected error: {e}")
|
||||
sys.exit(1)
|
||||
@@ -51,6 +51,77 @@ os.environ.setdefault("HF_HOME", MODELS_CACHE_DIR)
|
||||
background_tasks: List[asyncio.Task] = []
|
||||
|
||||
|
||||
async def preload_models() -> None:
|
||||
"""🚀 Асинхронная предзагрузка моделей для кеширования"""
|
||||
logger.info("🔄 Начинаем предзагрузку моделей...")
|
||||
|
||||
# Ждем импорта SentenceTransformer
|
||||
_lazy_import_sentence_transformers()
|
||||
|
||||
if SentenceTransformer is None:
|
||||
logger.error("❌ SentenceTransformer недоступен для предзагрузки")
|
||||
return
|
||||
|
||||
# Создаем папку для кеша
|
||||
Path(MODELS_CACHE_DIR).mkdir(parents=True, exist_ok=True)
|
||||
logger.info(f"📁 Используем кеш директорию: {MODELS_CACHE_DIR}")
|
||||
|
||||
# Список моделей для предзагрузки
|
||||
models = [
|
||||
"paraphrase-multilingual-MiniLM-L12-v2", # Основная многоязычная модель
|
||||
"all-MiniLM-L6-v2", # Fallback модель
|
||||
]
|
||||
|
||||
for model_name in models:
|
||||
try:
|
||||
# Проверяем, есть ли модель в кеше
|
||||
if _is_model_cached(model_name):
|
||||
logger.info(f"🔍 Модель уже в кеше: {model_name}")
|
||||
continue
|
||||
|
||||
logger.info(f"🔽 Загружаем модель: {model_name}")
|
||||
|
||||
# Запускаем загрузку в executor чтобы не блокировать event loop
|
||||
loop = asyncio.get_event_loop()
|
||||
await loop.run_in_executor(
|
||||
None, lambda name=model_name: SentenceTransformer(name, cache_folder=MODELS_CACHE_DIR)
|
||||
)
|
||||
|
||||
logger.info(f"✅ Модель загружена: {model_name}")
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"❌ Не удалось загрузить {model_name}: {e}")
|
||||
|
||||
logger.info("🚀 Предзагрузка моделей завершена!")
|
||||
|
||||
|
||||
def _is_model_cached(model_name: str) -> bool:
|
||||
"""🔍 Проверяет наличие модели в кеше"""
|
||||
try:
|
||||
cache_path = Path(MODELS_CACHE_DIR)
|
||||
model_cache_name = f"models--sentence-transformers--{model_name}"
|
||||
model_path = cache_path / model_cache_name
|
||||
|
||||
if not model_path.exists():
|
||||
return False
|
||||
|
||||
# Проверяем наличие snapshots папки (новый формат HuggingFace)
|
||||
snapshots_path = model_path / "snapshots"
|
||||
if snapshots_path.exists():
|
||||
# Ищем любой snapshot с config.json
|
||||
for snapshot_dir in snapshots_path.iterdir():
|
||||
if snapshot_dir.is_dir():
|
||||
config_file = snapshot_dir / "config.json"
|
||||
if config_file.exists():
|
||||
return True
|
||||
|
||||
# Fallback: проверяем старый формат
|
||||
config_file = model_path / "config.json"
|
||||
return config_file.exists()
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def _lazy_import_sentence_transformers():
|
||||
"""🔄 Lazy import SentenceTransformer для избежания блокировки старта приложения"""
|
||||
global SentenceTransformer # noqa: PLW0603
|
||||
@@ -83,37 +154,6 @@ class MuveraWrapper:
|
||||
self.encoder = None
|
||||
self._model_loaded = False
|
||||
|
||||
def _is_model_cached(self, model_name: str) -> bool:
|
||||
"""🔍 Проверяет наличие модели в кеше"""
|
||||
try:
|
||||
# Проверяем наличие папки модели в кеше
|
||||
cache_path = Path(MODELS_CACHE_DIR)
|
||||
|
||||
# SentenceTransformer сохраняет модели в формате models--org--model-name
|
||||
model_cache_name = f"models--sentence-transformers--{model_name}"
|
||||
model_path = cache_path / model_cache_name
|
||||
|
||||
# Проверяем существование папки модели
|
||||
if not model_path.exists():
|
||||
return False
|
||||
|
||||
# Проверяем наличие snapshots папки (новый формат HuggingFace)
|
||||
snapshots_path = model_path / "snapshots"
|
||||
if snapshots_path.exists():
|
||||
# Ищем любой snapshot с config.json
|
||||
for snapshot_dir in snapshots_path.iterdir():
|
||||
if snapshot_dir.is_dir():
|
||||
config_file = snapshot_dir / "config.json"
|
||||
if config_file.exists():
|
||||
return True
|
||||
|
||||
# Fallback: проверяем старый формат
|
||||
config_file = model_path / "config.json"
|
||||
return config_file.exists()
|
||||
except Exception as e:
|
||||
logger.debug(f"Error checking model cache for {model_name}: {e}")
|
||||
return False
|
||||
|
||||
def _ensure_model_loaded(self) -> bool:
|
||||
"""🔄 Убеждаемся что модель загружена (lazy loading)"""
|
||||
if self._model_loaded:
|
||||
@@ -129,7 +169,7 @@ class MuveraWrapper:
|
||||
logger.info(f"💾 Using models cache directory: {MODELS_CACHE_DIR}")
|
||||
|
||||
# Проверяем наличие основной модели
|
||||
is_cached = self._is_model_cached(primary_model)
|
||||
is_cached = _is_model_cached(primary_model)
|
||||
if is_cached:
|
||||
logger.info(f"🔍 Found cached model: {primary_model}")
|
||||
else:
|
||||
@@ -150,7 +190,7 @@ class MuveraWrapper:
|
||||
# Fallback - простая модель
|
||||
try:
|
||||
fallback_model = "all-MiniLM-L6-v2"
|
||||
is_fallback_cached = self._is_model_cached(fallback_model)
|
||||
is_fallback_cached = _is_model_cached(fallback_model)
|
||||
if is_fallback_cached:
|
||||
logger.info(f"🔍 Found cached fallback model: {fallback_model}")
|
||||
else:
|
||||
|
||||
Reference in New Issue
Block a user