Небольшой HTTP-сервис для получения эмбеддингов из модели sergeyzh/rubert-mini-frida.
Сервис:
- принимает текст через
POST /embedи возвращает эмбеддинг; - загружает модель один раз при старте приложения;
- имеет служебный эндпоинт
GET /health; - запускается через Docker;
- сопровождается тестами и отдельным скриптом для бенчмарка.
Для реализации сервиса был выбран FastAPI.
Почему именно он:
- для небольшого inference service это один из самых удобных вариантов по соотношению простоты и возможностей;
- он позволяет сразу описать понятные схемы запросов и ответов через
Pydantic; - из коробки даёт валидацию входных данных, что особенно удобно для API;
- хорошо сочетается с
uvicornи без лишних сложностей запускается в Docker; - для сервиса с одним основным эндпоинтом требует совсем немного шаблонного кода.
app/main.py— точка входа в приложение, создание API и загрузка модели при старте;app/service.py— логика подготовки текста, токенизации, mean pooling и нормализации эмбеддинга;benchmarks/runner.py— запуск нагрузочного сценария и сохранение результатов;tests/— базовые API-тесты.
Эмбеддинги получаются через transformers.
Проверка, что сервис поднят и модель успешно загружена.
Пример ответа:
{
"status": "ok",
"model": "sergeyzh/rubert-mini-frida",
"dimensions": 312,
"device": "cpu"
}Принимает JSON:
{
"text": "Текст для эмбеддинга",
"prefix": "categorize: "
}Поле prefix необязательное. Если его не передать, используется categorize: .
Пример ответа:
{
"embedding": [0.123, 0.456, 0.789],
"dimensions": 312,
"model": "sergeyzh/rubert-mini-frida",
"prefix_used": "categorize: "
}Дополнительно сервис пишет время инференса в заголовок X-Inference-Time-Ms.
docker compose up --build -dПосле старта сервис будет доступен на http://127.0.0.1:8000.
pytestТесты покрывают базовую работоспособность эндпоинтов /health и /embed.
Для сервиса были выбраны четыре основные группы метрик:
Latency P50 / P95 / P99Throughput (RPS)Model inference timeMemory usage
Что показывает:
Это полное время ответа HTTP-сервиса.
P50 отражает типичную задержку, а P95 и P99 позволяют увидеть, как сервис ведёт себя в более тяжёлых случаях и насколько сильно растут хвосты распределения под нагрузкой.
Целевые пороги:
P95 <= 200 msP99 <= 250 ms
Почему именно такие:
Для CPU-only сервиса с небольшой моделью такие значения выглядят реалистичными и достаточными для интерактивного использования.
Что делать при нарушении порога:
- уменьшать лишние накладные расходы в обработчике;
- проверять сериализацию ответа;
- выносить тяжёлые операции из request-path;
- рассматривать ONNX Runtime, квантизацию или запуск нескольких worker-процессов.
Что показывает:
Сколько запросов в секунду сервис может обработать на выбранном железе при фиксированном уровне конкуренции.
Целевой порог:
>= 8 RPS
Почему именно такой:
Это умеренное требование для CPU-only окружения и компактной BERT-подобной модели.
Что делать при нарушении порога:
- увеличивать число процессов;
- использовать более быстрый runtime;
Что показывает:
Время работы самой модели внутри сервиса, без учёта сетевой части и клиентских накладных расходов.
Целевой порог:
P95 <= 150 ms
Почему именно такой:
Если большая часть задержки приходится на модель, оптимизация только HTTP-слоя уже почти ничего не даст.
Что делать при нарушении порога:
- оптимизировать runtime модели;
- пробовать ONNX Runtime;
- использовать dynamic quantization;
- ограничивать длину входного текста.
Что показывает:
Базовое и пиковое потребление памяти процессом сервиса после загрузки модели и во время нагрузки.
Целевой порог:
peak RSS <= 900 MB
Почему именно такой:
Модель небольшая, поэтому сильный выход за этот порог обычно говорит о лишнем потреблении памяти или неудачной конфигурации контейнера.
Что делать при нарушении порога:
- проверять, не дублируется ли модель;
- ограничивать число worker-процессов;
- уменьшать лишние буферы;
- переходить на более компактный runtime.
Сценарий в benchmarks/runner.py:
- использует уже запущенный Docker-контейнер сервиса
- ждёт, пока начнёт отвечать
GET /health - выполняет прогрев
- отправляет серию POST-запросов на
/embed - считает
latency p50/p95/p99,throughput,inference time,peak memory - сохраняет результаты в JSON и Markdown
Запуск:
python -m benchmarks.runnerПо умолчанию бенчмарк ожидает контейнер rubert-inference-service на http://127.0.0.1:8000.
Если имя контейнера или адрес отличаются, можно переопределить их так:
python -m benchmarks.runner --base-url http://127.0.0.1:8000 --container-name rubert-inference-serviceАктуальные результаты Docker-прогона сохраняются в:
benchmarks/results/docker_cpu.jsonbenchmarks/results/docker_cpu.md
Ниже приведены значения последнего замера, выполненного в этом репозитории.
- macOS 14.4 arm64
- Python 3.11.9
- CPU: 8 vCPU
- RAM: 8 GB
- контейнер:
rubert-inference-service - сценарий:
120запросов,concurrency=6,warmup=12
| Метрика | Значение |
|---|---|
| Latency mean, ms | 149.774 |
| Latency p50, ms | 147.895 |
| Latency p95, ms | 174.195 |
| Latency p99, ms | 181.042 |
| Throughput, RPS | 39.007 |
| Inference time mean, ms | 25.460 |
| Inference time p95, ms | 29.208 |
| Peak RSS, MB | 278.500 |
| Delta RSS, MB | 8.100 |
По итогам замеров сервис укладывается во все выбранные пороги.
Что видно по результатам:
- задержка остаётся в комфортных пределах;
P95иP99не выходят за заданные ограничения;- throughput заметно выше минимального целевого значения;
- время самого инференса существенно меньше полного HTTP latency;
- память после загрузки модели ведёт себя стабильно, а рост под нагрузкой остаётся небольшим.
В рамках задания было реализовано и проверено следующее:
- API-тесты для
/healthи/embed; - отдельный сценарий нагрузочного теста;
- реальный запуск сервиса и фактический замер метрик;
- запуск сервиса через Docker.