feat: initial microservices project structure
Services: - api: FastAPI gateway with JWT auth, async endpoints, WebSocket - worker-gpu: CUDA sentence-transformers, FAISS IVFFlat, Ollama LLM - worker-indexer: Winnowing+MinHash plagiarism detection, PDF/DOCX extraction - worker-notifier: SMTP email notifications - worker-gost: GOST 7.1-2003 and GOST R 7.0.5-2008 formatting Infrastructure: - docker-compose.yml (production) + docker-compose.dev.yml (hot reload) - Nginx reverse proxy + WebSocket support - PostgreSQL 16 with Alembic migrations - Elasticsearch 8 with Russian/English analyzers - MinIO, RabbitMQ, Redis, Ollama Frontend: - React 18 + Vite + TypeScript + TailwindCSS + Zustand + React Query v5 - 9 pages: Home, Search, Cabinet, Task, Check, Bibliography, Pricing, Login, Register Scripts: - Parser stubs: OpenAlex, КиберЛенинка, arXiv (Phase 0 - to be filled) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
81
services/worker-gpu/app/model_manager.py
Normal file
81
services/worker-gpu/app/model_manager.py
Normal file
@@ -0,0 +1,81 @@
|
||||
"""Singleton менеджер sentence-transformers модели.
|
||||
|
||||
Модель загружается один раз при первом обращении и кэшируется в памяти GPU.
|
||||
"""
|
||||
|
||||
import logging
|
||||
|
||||
import numpy as np
|
||||
|
||||
from app.config import settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ModelManager:
|
||||
"""Singleton для управления эмбеддинг-моделью."""
|
||||
|
||||
_instance = None
|
||||
_model = None
|
||||
_device: str = settings.EMBED_DEVICE
|
||||
|
||||
@classmethod
|
||||
def get_model(cls):
|
||||
"""Получить или загрузить модель sentence-transformers."""
|
||||
if cls._model is None:
|
||||
# Импорт здесь, чтобы не блокировать импорт модуля если torch не установлен
|
||||
from sentence_transformers import SentenceTransformer
|
||||
|
||||
logger.info(
|
||||
f"Загрузка модели {settings.EMBED_MODEL!r} на устройство {settings.EMBED_DEVICE!r}..."
|
||||
)
|
||||
try:
|
||||
cls._model = SentenceTransformer(
|
||||
settings.EMBED_MODEL,
|
||||
device=settings.EMBED_DEVICE,
|
||||
)
|
||||
logger.info(
|
||||
f"Модель загружена. Размерность вектора: {cls._model.get_sentence_embedding_dimension()}"
|
||||
)
|
||||
except Exception as e:
|
||||
# Graceful degradation на CPU если CUDA недоступна
|
||||
logger.warning(f"Не удалось загрузить модель на CUDA: {e}. Переключение на CPU.")
|
||||
cls._device = "cpu"
|
||||
cls._model = SentenceTransformer(settings.EMBED_MODEL, device="cpu")
|
||||
|
||||
return cls._model
|
||||
|
||||
@classmethod
|
||||
def encode(cls, texts: list[str]) -> np.ndarray:
|
||||
"""
|
||||
Закодировать тексты в векторы.
|
||||
|
||||
Args:
|
||||
texts: Список текстов для кодирования
|
||||
|
||||
Returns:
|
||||
numpy массив формы (len(texts), 768), нормализованный для cosine similarity
|
||||
"""
|
||||
if not texts:
|
||||
return np.array([]).reshape(0, settings.EMBED_DIM)
|
||||
|
||||
model = cls.get_model()
|
||||
vectors = model.encode(
|
||||
texts,
|
||||
batch_size=settings.EMBED_BATCH_SIZE,
|
||||
normalize_embeddings=True, # Нормализация для cosine через inner product
|
||||
show_progress_bar=len(texts) > 100,
|
||||
convert_to_numpy=True,
|
||||
)
|
||||
return vectors.astype(np.float32)
|
||||
|
||||
@classmethod
|
||||
def encode_single(cls, text: str) -> np.ndarray:
|
||||
"""Закодировать один текст. Удобный метод."""
|
||||
return cls.encode([text])[0]
|
||||
|
||||
@classmethod
|
||||
def unload(cls) -> None:
|
||||
"""Выгрузить модель из памяти (для тестов/диагностики)."""
|
||||
cls._model = None
|
||||
logger.info("Модель выгружена из памяти")
|
||||
Reference in New Issue
Block a user