# embedding-runtime OpenAI-совместимый сервер эмбеддингов и переранжирования для линии векторного инференса SmartMLOps. Это кастомный контейнер предиктора KServe (`ClusterServingRuntime`), обслуживающий модели sentence-transformers в тех случаях, когда нативный рантайм TEI не справляется — а именно: модели с инструкциями/префиксами (E5, Qwen3-Embedding, Instructor) и модели с переключением адаптеров (Jina v5 base с task-специфичными LoRA-адаптерами). KServe регистрирует и управляет им как `embedding-runtime-cpu` / `embedding-runtime-cuda` с `modelFormat: embedding-runtime`. vector-gateway направляет трафик эмбеддингов и переранжирования на InferenceService, backed этим рантаймом. Данный репозиторий владеет только кодом обслуживания; KServe берёт на себя планирование, масштабирование и монтирование артефактов модели. > Этот компонент был недавно выделен из монорепозитория в отдельный git-субмодуль. > Файл `.git` в этой директории является указателем субмодуля; клонируйте через корневой > репозиторий `mlops-2`, чтобы получить корректно настроенное рабочее дерево. --- ## Место в архитектуре ``` vector-gateway └── /v1/embeddings, /v1/rerank → KServe InferenceService (modelFormat: embedding-runtime) └── контейнер embedding-runtime (этот репозиторий) └── модель sentence-transformers └── опциональный LoRA-адаптер (PEFT) ``` KServe монтирует артефакт модели по пути `/mnt/models`. Сервер читает этот путь через `EMBEDDING_MODEL_DIR` (по умолчанию `/mnt/models`) и передаёт его в качестве имени модели в `SentenceTransformer`. Переменная `EMBEDDING_MODEL_NAME` позволяет переопределить идентификатор модели, когда он отличается от пути монтирования (например, для использования HuggingFace Hub ID при загрузке по требованию). --- ## Эндпоинты ### `POST /v1/embeddings` Кодирует один или несколько текстов в нормализованные векторы эмбеддингов. **Запрос** ```json { "input": "text or list of texts", "model": "optional — ignored server-side, echoed in response", "task": "retrieval.query", "instruction": "optional explicit prefix; overrides the per-task default" } ``` Поле `input` принимает как одиночную строку, так и массив строк. **Ответ** ```json { "object": "list", "model": "", "data": [ {"object": "embedding", "index": 0, "embedding": [0.12, -0.34, ...]} ], "usage": {"prompt_tokens": 3, "total_tokens": 3} } ``` Эмбеддинги L2-нормализованы (`normalize_embeddings=True`); скалярное произведение равно косинусному сходству. --- ### `POST /v1/rerank` Ранжирует документы по запросу с использованием косинусного сходства по нормализованным эмбеддингам. Отдельный кросс-энкодер не используется; работает с любой базовой моделью эмбеддингов. **Запрос** ```json { "query": "search query", "documents": ["doc 1", "doc 2", "doc 3"], "top_n": 2, "model": "optional", "task": "optional — overrides the default query/passage tasks", "instruction": "optional explicit prefix" } ``` Если `task` не задан, запрос кодируется с `retrieval.query`, а каждый документ — с `retrieval.passage`. Параметр `top_n` ограничивает список результатов; если он не указан, возвращаются все документы. **Ответ** ```json { "model": "", "results": [ {"index": 1, "relevance_score": 0.95, "document": "doc 2"}, {"index": 0, "relevance_score": 0.42, "document": "doc 1"} ] } ``` Результаты отсортированы по убыванию `relevance_score`. --- ### `GET /health` Проверка работоспособности (liveness и readiness probe). Возвращает `200` немедленно, без загрузки модели. ```json {"status": "ok", "device": "cuda", "model": "/mnt/models"} ``` --- ## Загрузка модели и адаптеров ### Базовая модель Модель загружается лениво при первом инференс-запросе с помощью `SentenceTransformer(MODEL_NAME, device=DEVICE, trust_remote_code=True)`. Флаг `trust_remote_code=True` включён всегда — он необходим для Qwen3-Embedding и других моделей, которые поставляются с кастомным кодом токенизатора или модели на Hub. Используйте только артефакты из доверенных источников. `DEVICE` определяется автоматически: `cuda` при наличии GPU, `cpu` в противном случае. ### Переключение задач — класс с инструкциями/префиксами Применяется к E5, Qwen3-Embedding, Instructor и любым моделям, у которых качество представлений зависит от ведущей строки-инструкции. Перед кодированием к каждому входному тексту добавляется строка-префикс. Соответствие имени задачи префиксу: | Задача | Префикс по умолчанию | |---|---| | `retrieval.query` | `Represent this query for retrieving relevant documents: ` | | `retrieval.passage` | `Represent this document for retrieval: ` | | `classification` | `Represent this text for classification: ` | | `clustering` | `Represent this text for clustering: ` | Таблицу можно переопределить или расширить при запуске через `EMBEDDING_TASK_INSTRUCTIONS`. Явное поле `instruction` в запросе имеет приоритет над умолчанием для задачи. ### Переключение задач — класс с LoRA-адаптерами Применяется к моделям вроде Jina v5 base, которые рассчитаны на замену task-специфичных LoRA-адаптеров во время инференса. Загрузка адаптеров ленивая и кэшируется. При поступлении запроса с `task`, для которого настроен путь к адаптеру, сервер вызывает `model[0].auto_model.load_adapter()` и `set_adapter()` через PEFT. В один момент времени активен только один адаптер на процесс. Загрузка пропускается, если запрошенный адаптер уже активен. Адаптеры настраиваются через `EMBEDDING_TASK_ADAPTERS` (см. раздел «Переменные окружения»). --- ## Переменные окружения | Переменная | Значение по умолчанию | Описание | |---|---|---| | `EMBEDDING_MODEL_DIR` | `/mnt/models` | Директория, куда KServe монтирует артефакт модели. Также используется как идентификатор модели по умолчанию. | | `EMBEDDING_MODEL_NAME` | значение `EMBEDDING_MODEL_DIR` | Переопределяет идентификатор модели для SentenceTransformer (путь или репозиторий HuggingFace Hub). | | `EMBEDDING_TASK_INSTRUCTIONS` | `{}` | JSON-объект, отображающий имена задач в строки-префиксы инструкций. Накладывается поверх четырёх встроенных умолчаний. Пример: `{"retrieval.query": "Query: ", "retrieval.passage": "Passage: "}` | | `EMBEDDING_TASK_ADAPTERS` | `{}` | JSON-объект, отображающий имена задач в пути к LoRA-адаптерам или Hub-репозитории. Пример: `{"retrieval": "./adapters/retrieval", "classification": "org/cls-adapter"}` | --- ## Сборка образа Сборка из корня монорепозитория с контекстом, ограниченным этой поддиректорией: ```bash docker build -t smartmlops/embedding-runtime:dev apps/embedding-runtime ``` Или непосредственно из этой директории: ```bash docker build -t smartmlops/embedding-runtime:dev . ``` Образ основан на `python:3.11-slim`. Зависимости зафиксированы в `requirements.txt`: ``` fastapi==0.115.6 uvicorn[standard]==0.34.0 sentence-transformers==3.3.1 transformers==4.47.1 torch==2.5.1 peft==0.14.0 numpy==2.2.1 ``` Точка входа: `uvicorn server:app --host 0.0.0.0 --port 8080`. --- ## Локальный запуск Контейнер скачивает модель с HuggingFace Hub, если `EMBEDDING_MODEL_NAME` является идентификатором Hub-репозитория, а `EMBEDDING_MODEL_DIR` ещё не содержит весов. ```bash # smoke-запуск — при первом старте скачивает BAAI/bge-small-en-v1.5 docker run --rm -p 8080:8080 \ -e EMBEDDING_MODEL_NAME=BAAI/bge-small-en-v1.5 \ smartmlops/embedding-runtime:dev # проверка эмбеддингов curl -s localhost:8080/v1/embeddings \ -H 'content-type: application/json' \ -d '{"input": ["hello world"], "task": "retrieval.query"}' | python3 -m json.tool # проверка переранжирования curl -s localhost:8080/v1/rerank \ -H 'content-type: application/json' \ -d '{"query": "cats", "documents": ["dogs", "cats", "fish"], "top_n": 2}' | python3 -m json.tool # проверка работоспособности curl -s localhost:8080/health ``` Смонтируйте заранее скачанную директорию с моделью, чтобы избежать повторной загрузки при каждом запуске: ```bash docker run --rm -p 8080:8080 \ -v /path/to/model:/mnt/models:ro \ smartmlops/embedding-runtime:dev ``` Для GPU-инференса передайте `--gpus all` и используйте тег образа `embedding-runtime-cuda` (который требует базового CUDA-образа; соответственно обновите строку `FROM` в Dockerfile). --- ## Запуск тестов Цель `quality` в `Dockerfile` задаёт воспроизводимую среду проверки на том же закреплённом по digest образе Chainguard Python, что и стадия сборки зависимостей. Она устанавливает только версии из `requirements.txt` и `requirements-test.txt`, а затем выполняет ровно 8 tests, Ruff, Ty и проверку компиляции: ```bash docker build --target quality -t embedding-runtime:quality . ``` Внутри стадии выполняются точные команды: ```bash /home/nonroot/venv/bin/python -m pytest -p no:cacheprovider tests /home/nonroot/venv/bin/ruff check server.py tests /home/nonroot/venv/bin/ty check server.py tests /home/nonroot/venv/bin/python -m compileall -q server.py tests ``` Ty получает точный runtime venv через `VIRTUAL_ENV=/home/nonroot/venv`. Для bytecode используется объявленный writable prefix `PYTHONPYCACHEPREFIX=/tmp/embedding-runtime-pycache`; исходники остаются read-only совместимыми. Ruff `0.15.19` и Ty `0.0.53` закреплены в `requirements-test.txt` вместе с pytest и HTTP-зависимостями. SBOM-проверка различает экосистемы по purl. Python-дистрибутив `pkg:pypi/wheel` отсутствует из финального venv. Базовый Chainguard-образ при этом содержит отдельный системный APK `pkg:apk/wolfi/py3-pip-wheel`; он не скрывается и не считается Python-дистрибутивом `wheel`. Допустимость этого APK подтверждается свежим Trivy-сканом без HIGH/CRITICAL findings, а не утверждением об отсутствии пакета. --- ## Развёртывание в KServe Рантайм зарегистрирован как `ClusterServingRuntime` в платформе. Пример `InferenceService`, использующего его: ```yaml apiVersion: serving.kserve.io/v1beta1 kind: InferenceService metadata: name: my-embedding-model namespace: tenant-smoke spec: predictor: model: modelFormat: name: embedding-runtime storageUri: hf://BAAI/bge-small-en-v1.5 # Инструкции по задачам для моделей класса instruction: env: - name: EMBEDDING_TASK_INSTRUCTIONS value: '{"retrieval.query": "Query: ", "retrieval.passage": "Passage: "}' ``` KServe монтирует модель по пути `/mnt/models` внутри контейнера предиктора. Ресурс `ClusterServingRuntime` (`embedding-runtime-cpu` для CPU-узлов, `embedding-runtime-cuda` для GPU-узлов) связывает образ контейнера, порт `8080` и селектор `modelFormat`. --- ## Ограничения и особенности - **`trust_remote_code=False` включён безусловно.** Рантайм не исполняет код, поставляемый вместе с артефактом модели; совместимость модели должна обеспечиваться штатной реализацией `sentence-transformers`. - **Только один активный LoRA-адаптер на процесс.** При одновременных запросах с разными значениями `task`, отображающимися на разные адаптеры, будет происходить сериализация на переключении адаптера. Для высоконагруженных multi-task адаптерных сценариев рассмотрите развёртывание отдельных InferenceService на каждую задачу. - **CPU-инференс медленный.** Вариант рантайма `-cpu` подходит для разработки и малонагруженных сценариев, однако производственные нагрузки по эмбеддингам следует направлять на GPU-узел, используя вариант `-cuda`. - **Переранжирование основано на схожести эмбеддингов, а не на кросс-энкодере.** Эндпоинт `/v1/rerank` оценивает документы с помощью скалярного произведения нормализованных эмбеддингов, а не отдельной модели кросс-энкодера. Это быстрее и работает с любой базовой моделью, однако кросс-энкодер обеспечивает более высокую точность для задач, где важна прецизионность. - **Количество токенов в `usage` приблизительное.** Значение `prompt_tokens` оценивается разбиением по пробелам, а не реальным токенизатором.