## [0.9.19] - 2025-09-01
Some checks failed
Deploy on push / deploy (push) Failing after 5m57s

### 🚀 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:
2025-09-01 16:38:23 +03:00
parent 143157a771
commit b70901f8f7
5 changed files with 87 additions and 144 deletions

View File

@@ -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, так и строки

View File

@@ -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 \

View File

@@ -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")

View File

@@ -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)

View File

@@ -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: