330 lines
17 KiB
Markdown
330 lines
17 KiB
Markdown
# 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` оценивается
|
||
разбиением по пробелам, а не реальным токенизатором.
|