### 🚀 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
|
## [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
|
### 🔧 Reactions Type Compatibility Fix
|
||||||
- **🐛 rating functions**: Исправлена ошибка `AttributeError: 'str' object has no attribute 'value'` в создании реакций
|
- **🐛 rating functions**: Исправлена ошибка `AttributeError: 'str' object has no attribute 'value'` в создании реакций
|
||||||
- Функции `is_positive()` и `is_negative()` в `orm/rating.py` теперь поддерживают как `ReactionKind` enum, так и строки
|
- Функции `is_positive()` и `is_negative()` в `orm/rating.py` теперь поддерживают как `ReactionKind` enum, так и строки
|
||||||
|
|||||||
@@ -25,9 +25,6 @@ RUN uv sync --no-install-project
|
|||||||
COPY . .
|
COPY . .
|
||||||
RUN uv sync --no-editable
|
RUN uv sync --no-editable
|
||||||
|
|
||||||
# 🚀 Предзагрузка HuggingFace моделей для ускорения первого запуска
|
|
||||||
RUN uv run python scripts/preload_models.py
|
|
||||||
|
|
||||||
# Установка Node.js LTS и npm
|
# Установка Node.js LTS и npm
|
||||||
RUN curl -fsSL https://deb.nodesource.com/setup_lts.x | bash - && \
|
RUN curl -fsSL https://deb.nodesource.com/setup_lts.x | bash - && \
|
||||||
apt-get install -y nsolid \
|
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.precache import precache_data
|
||||||
from cache.revalidator import revalidation_manager
|
from cache.revalidator import revalidation_manager
|
||||||
from rbac import initialize_rbac
|
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 services.viewed import ViewedStorage
|
||||||
from settings import DEV_SERVER_PID_FILE_NAME
|
from settings import DEV_SERVER_PID_FILE_NAME
|
||||||
from storage.redis import redis
|
from storage.redis import redis
|
||||||
@@ -263,6 +263,11 @@ async def lifespan(app: Starlette):
|
|||||||
await initialize_search_index_with_data()
|
await initialize_search_index_with_data()
|
||||||
print("[lifespan] Search service initialized with Muvera")
|
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
|
yield
|
||||||
finally:
|
finally:
|
||||||
print("[lifespan] Shutting down application services")
|
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] = []
|
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():
|
def _lazy_import_sentence_transformers():
|
||||||
"""🔄 Lazy import SentenceTransformer для избежания блокировки старта приложения"""
|
"""🔄 Lazy import SentenceTransformer для избежания блокировки старта приложения"""
|
||||||
global SentenceTransformer # noqa: PLW0603
|
global SentenceTransformer # noqa: PLW0603
|
||||||
@@ -83,37 +154,6 @@ class MuveraWrapper:
|
|||||||
self.encoder = None
|
self.encoder = None
|
||||||
self._model_loaded = False
|
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:
|
def _ensure_model_loaded(self) -> bool:
|
||||||
"""🔄 Убеждаемся что модель загружена (lazy loading)"""
|
"""🔄 Убеждаемся что модель загружена (lazy loading)"""
|
||||||
if self._model_loaded:
|
if self._model_loaded:
|
||||||
@@ -129,7 +169,7 @@ class MuveraWrapper:
|
|||||||
logger.info(f"💾 Using models cache directory: {MODELS_CACHE_DIR}")
|
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:
|
if is_cached:
|
||||||
logger.info(f"🔍 Found cached model: {primary_model}")
|
logger.info(f"🔍 Found cached model: {primary_model}")
|
||||||
else:
|
else:
|
||||||
@@ -150,7 +190,7 @@ class MuveraWrapper:
|
|||||||
# Fallback - простая модель
|
# Fallback - простая модель
|
||||||
try:
|
try:
|
||||||
fallback_model = "all-MiniLM-L6-v2"
|
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:
|
if is_fallback_cached:
|
||||||
logger.info(f"🔍 Found cached fallback model: {fallback_model}")
|
logger.info(f"🔍 Found cached fallback model: {fallback_model}")
|
||||||
else:
|
else:
|
||||||
|
|||||||
Reference in New Issue
Block a user