Масштабирование инференса
Когда одного инстанса мало, инференс масштабируют тремя рычагами: батчинг, репликация, GPU.
Масштабирование инференса — увеличение пропускной способности сервиса предсказаний при сохранении приемлемой задержки через батчинг, добавление реплик и эффективное использование ускорителей.
Латентность против пропускной способности
Два разных требования. Латентность — как быстро отвечает один запрос. Пропускная способность (throughput) — сколько запросов в секунду обслуживает система. Их часто приходится балансировать: батчинг повышает throughput, но может слегка увеличить латентность отдельного запроса. Сначала определите бюджет задержки (например, p99 < 100 мс), затем максимизируйте throughput внутри него.
Рычаг 1: батчинг
Современное железо (особенно GPU) считает пачку из 32 примеров почти за то же время, что и один — за счёт параллелизма матричных операций. Динамический батчинг копит входящие запросы несколько миллисекунд и обрабатывает их одним вызовом модели. Покажем выигрыш на упрощённой модели стоимости.
# Упрощённая модель: фиксированный оверхед на вызов + малая цена за пример
OVERHEAD_MS = 8.0 # подготовка вызова, передача на GPU
PER_ITEM_MS = 0.5 # обработка одного примера
def time_per_request(batch_size):
total = OVERHEAD_MS + PER_ITEM_MS * batch_size
return total / batch_size # амортизированное время на 1 запрос
for b in [1, 8, 32, 64]:
t = time_per_request(b)
rps = 1000 / t
print(f"batch={b:3d}: {t:5.2f} мс/запрос, ~{rps:6.0f} rps")
Вывод:
batch= 1: 8.50 мс/запрос, ~ 118 rps batch= 8: 1.50 мс/запрос, ~ 667 rps batch= 32: 0.75 мс/запрос, ~ 1333 rps batch= 64: 0.62 мс/запрос, ~ 1600 rps
Видно: при батче 32 фиксированный оверхед размазывается по 32 запросам, и амортизированное время на запрос падает в разы, а throughput растёт. Но окно ожидания батча добавляет немного к латентности — отсюда баланс.
Рычаг 2: горизонтальный автоскейлинг
Когда один инстанс упёрся в потолок, добавляют реплики за балансировщиком. В Kubernetes это делает HPA (Horizontal Pod Autoscaler): он смотрит на нагрузку (CPU, RPS, длину очереди) и сам добавляет/убирает поды.
+--- pod 1 (модель)
клиенты --> LB --+--- pod 2 (модель) <-- HPA добавляет поды под нагрузкой
+--- pod 3 (модель)
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: model-hpa
spec:
scaleTargetRef:
kind: Deployment
name: fraud-detector
minReplicas: 2
maxReplicas: 20
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
Рычаг 3: GPU и оптимизация модели
Тяжёлые модели (нейросети) на GPU считаются на порядок быстрее, но GPU дорог — его нужно загружать батчами на полную. Дополнительно модель оптимизируют: квантизация (float32 → int8), дистилляция (обучить лёгкую модель имитировать тяжёлую), компиляция (TensorRT, ONNX Runtime). Это снижает и латентность, и стоимость инференса.
Как работает под капотом
Масштабирование — это игра с фиксированными и переменными издержками. Батчинг амортизирует фиксированный оверхед вызова по многим примерам (вертикальная эффективность). Автоскейлинг добавляет параллельные мощности под переменную нагрузку (горизонтальная эффективность). GPU-оптимизация снижает саму стоимость одного примера. Зрелый сервинг (Triton, BentoML) сочетает все три: динамический батчинг внутри пода + HPA по подам + оптимизированная модель на GPU.
Частые ошибки
- Гнаться за throughput, забыв про латентность. Слишком большое окно батча убьёт отзывчивость.
- Скейлить поды без батчинга. Дорого: добавляете железо вместо того, чтобы эффективнее использовать имеющееся.
- Держать GPU недозагруженным. Маленькие батчи на дорогом GPU — выброшенные деньги.
- Скейлить только по CPU. Для очередей часто точнее метрика длины очереди или RPS.
Итог
- Масштабируют тремя рычагами: батчинг (амортизирует оверхед), автоскейлинг (добавляет реплики), GPU-оптимизация (снижает цену примера).
- Латентность и throughput — разные цели; сначала фиксируют бюджет задержки, потом максимизируют throughput.
- Зрелый сервинг сочетает все три рычага; скейлить поды без батчинга — дорого и неэффективно.