embedding-runtime/README.md

330 lines
17 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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": "<MODEL_NAME>",
"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": "<MODEL_NAME>",
"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` оценивается
разбиением по пробелам, а не реальным токенизатором.