No description
Find a file
2026-07-20 23:38:56 +03:00
tests build(runtime): harden embedding image policy 2026-07-20 23:38:56 +03:00
Dockerfile build(runtime): harden embedding image policy 2026-07-20 23:38:56 +03:00
README.md docs(runtime): document embedding image policy 2026-07-20 23:38:56 +03:00
requirements-test.txt fix(runtime): harden embedding API behavior 2026-07-15 10:43:55 +03:00
requirements.txt fix(runtime): harden embedding API behavior 2026-07-15 10:43:55 +03:00
server.py fix(runtime): harden embedding API behavior 2026-07-15 10:43:55 +03:00

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

Кодирует один или несколько текстов в нормализованные векторы эмбеддингов.

Запрос

{
  "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 принимает как одиночную строку, так и массив строк.

Ответ

{
  "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

Ранжирует документы по запросу с использованием косинусного сходства по нормализованным эмбеддингам. Отдельный кросс-энкодер не используется; работает с любой базовой моделью эмбеддингов.

Запрос

{
  "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 ограничивает список результатов; если он не указан, возвращаются все документы.

Ответ

{
  "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 немедленно, без загрузки модели.

{"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"}

Сборка образа

Сборка из корня монорепозитория с контекстом, ограниченным этой поддиректорией:

docker build -t smartmlops/embedding-runtime:dev apps/embedding-runtime

Или непосредственно из этой директории:

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 ещё не содержит весов.

# 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

Смонтируйте заранее скачанную директорию с моделью, чтобы избежать повторной загрузки при каждом запуске:

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 и проверку компиляции:

docker build --target quality -t embedding-runtime:quality .

Внутри стадии выполняются точные команды:

/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, использующего его:

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