В середине 2025 года команда X5 Tech столкнулась с классической болью: CV-сервис модерации изображений товаров перестал справляться с пиковыми нагрузками. 10 миллионов проверок в месяц — цифра красивая, пока за ней не стоят 4-секундные таймауты, падения нод и горящие глаза продакт-ов. Как выкарабкались — под капотом.
Ключевые цифры до рефакторинга: P99 latency = 3.2с, утилизация GPU = 35%, частота ручных ребалансов Kafka = 4 раза в неделю. После — P99 = 480мс, GPU @ 85%, Kafka тишина.
Слон в комнате: почему монолитная CV-модель ломается в проде
Типовой CV-пайплайн в ритейле выглядит так: фронт загружает фото -> микросервис отправляет задачу в очередь -> воркер грузит модель (YOLO, DETR, ViT) на GPU -> инференс -> запись результата. Звучит стройно. Но когда вы дергаете одну тяжелую модель (скажем, YOLOv8x с 68 млн параметров) на каждое изображение, вы получаете:
- Простой GPU на 65% времени (модель загружена, но ядра простаивают в ожидании батча)
- Перекос партиций Kafka: ключи по id товара создают «горячие» партиции для популярных категорий
- Рост латентности при масштабировании — добавление нод не даёт линейного прироста из-за конкуренции за I/O
Команда X5 Tech пошла другим путём: они разбили задачу на две стадии, каждая со своим инференс-сервером и очередью. Звучит как «ну разбили, подумаешь», но дьявол в деталях.
Архитектура нового пайплайна: Triton + vLLM + Kafka RPC
Вот как выглядит итоговая схема (спойлер: без магии, только инженерная дисциплина):
| Компонент | Роль | Почему именно он |
|---|---|---|
| FastAPI-гейт | Принимает запросы, ставит в Kafka | Буферизация пиков |
| Kafka (2 топика) | Первая стадия: фильтрация мусора | Убирает 40% заведомо плохих фото |
| Triton Inference Server | Инференс легкой модели (MobileNetV4) | Поддержка динамического батчинга, ensemble |
| vLLM (через OpenAI API) | Вторая стадия: детальный анализ | Дешево и быстро для ViT-подобных моделей |
| Kafka RPC (кастомный) | Синхронизация результатов стадий | Исключил ручные ребалансы |
Почему Triton, а не TorchServe или собранный на коленке?
TorchServe — хорош для экспериментов, но в проде он начинает «тормозить» на динамических батчах. Triton Inference Server от NVIDIA имеет встроенный Dynamic Batcher: он собирает запросы от разных воркеров в батч до истечения таймаута. Это дало +60% пропускной способности на той же карте A10G.
Дополнительный плюс — Triton поддерживает Model Ensemble: вы можете объединить препроцессинг, инференс и постпроцессинг в один DAG, не выходя из GPU. X5 Tech сделали ensemble из двух моделей: классификатор мусор/норма и модератор контента. Латентность упала ещё на 20%.
# Пример конфига ensemble для Triton
name: "moderation_pipeline"
platform: "ensemble"
input: [
{
name: "INPUT_IMAGE",
data_type: TYPE_UINT8,
dims: [ -1, 224, 224, 3 ]
}
]
output: [
{
name: "OUTPUT_LABEL",
data_type: TYPE_INT64,
dims: [ 1 ]
}
]
ensemble_scheduling:
step: [
{
model_name: "preprocessor",
model_version: -1,
input_map: { INPUT: "INPUT_IMAGE" },
output_map: { PREPROCESSED: "preprocessed_image" }
},
{
model_name: "mobilenet_v4",
model_version: 1,
input_map: { IMAGE: "preprocessed_image" },
output_map: { SCORE: "raw_score" }
},
{
model_name: "classifier",
model_version: 2,
input_map: { SCORE: "raw_score" },
output_map: { LABEL: "OUTPUT_LABEL" }
}
]
Обратите внимание: для каждого шага можно указать свою версию модели. Это позволило X5 Tech выкатывать новые веса без даунтайма — делали rolling update ensemble, подменяя модель по одной.
Kafka RPC: как убрать ручной ребаланс и не сойти с ума
Типичная проблема с Kafka в ML-пайплайнах — перекос партиций. Когда вы используете key=product_id, все фото одного товара летят в одну партицию. Один товар с 1000 фото блокирует всю партицию, остальные простаивают.
X5 Tech внедрили Kafka RPC — кастомный слой поверх Kafka, который работает как Request-Reply брокер. Каждый воркер подписывается на «свою» партицию, но результат отправляется в отдельный reply-топик с корреляционным ID. Это позволило:
- Равномерно распределять нагрузку — ключом стал случайный UUID, не product_id
- Получать ответы асинхронно — не нужно ждать все партиции
- Автоматически балансировать воркеры — партиции перераспределяются через group coordinator
Реализация заняла 2 недели и одну кастомную библиотеку (кстати, X5 Tech выложили её в open source). Если лень писать свою — можно взять Kafka-Python + aiokafka, но придётся вручную обрабатывать корреляцию.
Метрики, ради которых всё затевалось
Через месяц после запуска новой архитектуры замерили результаты (сравнение с предыдущим монолитным пайплайном на TorchServe + ResNet-50):
| Метрика | До | После | Изменение |
|---|---|---|---|
| P99 latency | 3.2 с | 480 мс | -85% |
| P50 latency | 1.1 с | 210 мс | -81% |
| Утилизация GPU | 35% | 85% | +143% |
| Стоимость инстансов (в месяц) | $12 400 | $6 200 | -50% |
| Количество ручных инцидентов | 4/нед | 0/мес | ~100% |
Экономия в 2 раза на инфраструктуре — приятный бонус. Но главное — P99 latency ниже 500 мс позволил добавить синхронный режим для онлайн-модерации, чего раньше не было.
Топ-5 граблей, на которые наступила команда
- Не включали Prefetch Queue в Triton. По умолчанию Triton ждёт полный батч перед инференсом. Для CV с разными размерами изображений это катастрофа — запросы копятся минут по 5. Решение: настроили
max_queue_delay_microseconds: 5000иpreferred_batch_size: [4,8,16]. - Сессия vLLM падала при длинных промптах. Модель Qwen2-VL любит большие изображения, а мы пихали ей 4K фото. Пришлось добавить ресайз до 1024×1024 на входе в очередь. Удивительно, но это не повлияло на accuracy — модель Fine-tuned на таком разрешении.
- Kafka RPC без идемпотентности. При повторной отправке сообщения (из-за retries) мы получали дубликаты в reply-топике. Добавили дедупликацию по correlationId на стороне клиента.
- Ensemble в Triton не логирует промежуточные результаты. Когда MobileNet выдавал score 0.5, а classifier решал «мусор», было трудно дебажить. Пришлось добавить кастомный слой логирования через shared memory.
- Забыли про cold start. После перезапуска Triton грузит все модели последовательно — 5 минут простоя. Решили через preloading моделей в фоне и graceful shutdown.
Не советую так делать: пытаться запустить Triton без ограничения по времени на загрузку модели. Однажды production упал на 20 минут, потому что ensemble не дождался загрузки vLLM и завис.
Как тестировали пайплайн (и при чём тут CI/CD)
Для регрессии качества использовали синтетический датасет из 10 000 изображений с известными метками. Каждый коммит в репозиторий с весами модели запускал пайплайн Evals as Code — подход, который monday.com успешно применили для LLM-агентов. В X5 Tech сделали аналогично, но под CV: в CI/CD джобе гоняют батч через Triton, сравнивают с эталонными лейблами через конфиги метрик (F1, Precision, Recall). Если метрика падает ниже порога — джоба красная, деплой блокируется. Подробнее про этот подход читайте в нашей статье Evals as Code: как monday.com ускорила фидбэк в 8.7 раз.
Интересно, что без такого подхода первый же апдейт MobileNet снёс бы точность детекции лиц на 30%. Хорошо, что CI/CD поймал.
А что если у вас нет GPU-кластера? Или есть, но старый?
Не обязательно иметь A100. В X5 Tech использовали A10G (24 GB). vLLM на Qwen2-VL-7B с квантованием FP16 потреблял ~16 GB. Triton с MobileNet — ещё 2 GB. Обе модели влезали на одну карту. Если видеопамяти меньше — можно разделить на две ноды или использовать CPU-инференс через OpenVINO для первой стадии. Но тогда латентность вырастет до 1-2 секунд на слабых CPU.
Кстати, если ваша команда переходит с монолита на микросервисы, рекомендую почитать кейс Финтех на стероидах: как AI coding сжал команду с 40 до 10 человек — там похожие паттерны с Kafka и CI/CD, хоть и про другой домен.
Подведём? Нет, не будем
Вместо сухих выводов — совет, который вы получите только после года боёв в проде: не пытайтесь оптимизировать всё сразу. В X5 Tech начали с замены очереди (Kafka RPC) и получили 50% прироста производительности без смены моделей. Потом уже взялись за Triton и vLLM. Делайте smallest possible change — измеряйте — итеративно улучшайте.
И ещё: если ваш пайплайн обрабатывает меньше 100 000 запросов в день — описанная архитектура избыточна. Для таких объёмов хватит простой FastAPI + ONNX Runtime на одной ноде. Но когда нагрузка растёт, Triton и Kafka RPC становятся не роскошью, а необходимостью.