Масштабирование инференса

Когда одного инстанса мало, инференс масштабируют тремя рычагами: батчинг, репликация, 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.
  • Зрелый сервинг сочетает все три рычага; скейлить поды без батчинга — дорого и неэффективно.
Проверьте себя
1. Почему батчинг повышает пропускную способность?
AУменьшает размер модели
BФиксированный оверхед вызова размазывается по многим примерам, особенно на GPU
CОтключает валидацию
DУдаляет реплики
2. Что делает HPA в Kubernetes?
AОбучает модель
BАвтоматически добавляет и убирает поды-реплики под нагрузкой
CВерсионирует данные
DШифрует трафик
3. Почему важно зафиксировать бюджет латентности перед максимизацией throughput?
AЧтобы отчёт был длиннее
BИначе слишком большое окно батча убьёт отзывчивость ради пропускной способности
CЛатентность не важна
DТак требует Docker