Платформа оценки latency и стоимости ML‑инференса

Внутренний инструмент для профилировки latency, throughput и $/req моделей в проде

Платформа оценки latency и стоимости ML‑инференса

One-liner: За 8 недель превратили ML-инфраструктуру из черного ящика в прозрачную и управляемую систему с точной стоимостью каждого запроса.


1. Почему бизнесу была нужна платформа

В начале проекта ML-инфраструктура была разрозненной и неуправляемой:

  • Нет единого подхода к деплою моделей: одни команды деплоили через TorchServe, другие использовали ONNX Runtime, третьи работали через KServe.
  • Отсутствие прозрачности: никто не понимал точную стоимость одного inference-запроса. GPU простаивали ночью, а днем перегревались, вызывая пиковые скачки latency выше SLA.
  • Бюджет горел: инфраструктурная команда временно ограничивала количество нод вручную, чтобы уложиться в лимиты. Иногда это ломало прод, но отследить было невозможно.

2. Что было сделано и зачем

Мы построили универсальный инструмент мониторинга и оптимизации затрат, полностью интегрированный в Kubernetes-среду компании. Платформа собирает все критически важные метрики ML-инфраструктуры:

  • Latency: измерение задержек с разбивкой по percentiles (p50, p90, p99).
  • Throughput: количество запросов в секунду, деградация при разной нагрузке.
  • GPU/CPU-utilization: мониторинг загрузки ресурсов, выявление простоев.
  • $/req: точная стоимость каждого ML-запроса на основе Spot API и Kubecost.

Платформа дает actionable-рекомендации:

  • замена типа GPU-инстансов (V100 → T4)
  • оптимальный batching
  • квантизация и pruning моделей

3. Архитектура решения

Платформа мониторинга построена вокруг Kubernetes и сервисов-экспортеров:

Inference-Service (Torch / ONNX)
   ├── /metrics endpoint (Prometheus-compatible)
   │     ├── latency_histogram (p50, p90, p99)
   │     ├── throughput_rps
   │     └── gpu_utilization / cpu_utilization

   ├── Torch / ONNX Profiler
   │     └── performance traces (JSON → Perfetto)

   └── Kubecost Exporter (via Spot API)
         ├── spot-instance cost mapping
         └── billing aggregation by namespace / model_id labels


        ┌────────────────────┐
        │   Prometheus DB    │
        └────────────────────┘

          ┌────────────────┐
          │ Alertmanager   │ ◄──── SLA breaches (e.g. p99 > 800ms)
          └────────────────┘

        ┌────────────────────┐
        │   Grafana Panels   │
        ├────────────────────┤
        │ • $/req by model   │
        │ • p95 latency heat │
        │ • throughput trend │
        │ • GPU utilization  │
        └────────────────────┘

        Auto-scaling policy input
          (via KEDA / HPA / Argo)

Пример метрик в Prometheus:

histogram_quantile(0.99, sum(rate(model_inference_latency_seconds_bucket[5m])) by (le))

Эта метрика вычисляет p99 latency, помогая отлавливать пики и своевременно реагировать на аномалии.


4. Профилировка и нагрузочные тесты

Отдельно были настроены регулярные профилировки моделей:

  • PyTorch Profiler: В рамках платформы реализовали отдельный модуль для профилировки latency и ресурсного потребления моделей. Он используется в dev- и CI-средах, чтобы выявлять узкие места после дообучения или при деградации SLA. Ниже приведен пример скрипта, применяемого для такой диагностики. Профили сохраняются в TensorBoard или Perfetto, результаты анализируются вручную или автоматически через threshold-based алерты.

from __future__ import annotations
import argparse, os, sys, time, torch
from pathlib import Path
from contextlib import nullcontext
from torch import nn
from torch.profiler import profile, record_function, tensorboard_trace_handler, ProfilerActivity

def cfg():
    p=argparse.ArgumentParser()
    p.add_argument("--model",default="resnet50")
    p.add_argument("-b","--batch",type=int,default=32)
    p.add_argument("--steps",type=int,default=50)
    p.add_argument("--device",default="auto")
    p.add_argument("--no-amp",dest="amp",action="store_false")
    p.add_argument("--no-compile",dest="comp",action="store_false")
    p.add_argument("--log",type=Path,default=Path("./logs"))
    p.add_argument("--seed",type=int,default=42)
    p.add_argument("--disable-prof",dest="prof",action="store_false")
    return p.parse_args()

def pick(d):
    if d!="auto": return torch.device(d)
    if torch.cuda.is_available(): return torch.device("cuda")
    if torch.backends.mps.is_available(): return torch.device("mps")
    return torch.device("cpu")

def load(m,dev,comp):
    net=torch.load(m,map_location=dev) if os.path.isfile(m) else torch.hub.load("pytorch/vision",m,pretrained=True)
    net=net.to(dev).eval()
    if comp and torch.__version__>="2.0":
        try: net=torch.compile(net,mode="reduce-overhead",fullgraph=False)
        except: pass
    if torch.cuda.device_count()>1 and dev.type=="cuda": net=nn.DataParallel(net)
    return net

def prof(c):
    if not c.prof: return nullcontext()
    kw=dict(activities=[ProfilerActivity.CPU],schedule=torch.profiler.schedule(wait=2,warmup=2,active=c.steps),
             record_shapes=True,profile_memory=True,with_stack=True,with_modules=True)
    if torch.cuda.is_available(): kw["activities"].append(ProfilerActivity.CUDA)
    if torch.__version__>="2.3": kw["with_flops"]=True
    return profile(on_trace_ready=tensorboard_trace_handler(str(c.log)),**kw)

def main():
    c=cfg(); dev=pick(c.device); torch.manual_seed(c.seed); torch.backends.cudnn.deterministic=True
    c.log.mkdir(parents=True,exist_ok=True)
    net=load(c.model,dev,c.comp); x=torch.randn(c.batch,3,224,224,device=dev)
    ac=torch.autocast(dev.type,enabled=c.amp)
    pc=prof(c); start=time.perf_counter()
    with pc as p:
        for _ in range(c.steps):
            with torch.no_grad(),ac,record_function("infer"): net(x)
            if c.prof: p.step()
    total=time.perf_counter()-start
    print(f"{c.steps} steps {total:.2f}s {c.steps/total:.2f} it/s on {dev.type.upper()}")
    if c.prof:
        key="self_cuda_time_total" if dev.type=="cuda" else "self_cpu_time_total"
        print(p.key_averages().table(sort_by=key,row_limit=20))

if __name__=="__main__":
    try: main()
    except KeyboardInterrupt: sys.exit(0)

  • ONNX Profiler: для ONNX Runtime использовали tracing API, экспортируя профили в JSON и визуализируя их через Perfetto UI.

Эти профили позволяли увидеть горячие точки в моделях. Например, выделение времени CPU-преобразования (image → tensor → device) занимало до 40% времени inference, что оперативно устранялось.


5. Результаты после запуска платформы

За месяц после запуска платформы получили:

МетрикаДо внедренияПосле внедренияДельта
Latency p99400–900 мс (нестаб.)стабильно ~420 мсстабилизирован
GPU-utilization~36 %54 %+18 pp ↑
$/req (на модели LLM)базовая-43 % (V100 → T4)−43 % ↓
Точечные алертыотсутствовалиавтоматизированывнедрено

Пример реальной ситуации:

  • Spot-инстанс упал ночью → платформа сразу отправила алерт и подсветила скачок стоимости в 3 раза → быстро откатили конфигурацию, минимизировав финансовые потери.

6. Почему это важно для бизнеса и технических команд

Платформа изменила подход к работе с ML-инфраструктурой:

  • Полная прозрачность: CFO и CTO получили детальную аналитику по стоимости ML-проектов в реальном времени.
  • Контролируемые бюджеты: теперь можно открыть Grafana и за 10 секунд узнать: сколько стоил 1M запросов на конкретной модели за сутки
  • Управляемый performance: на основе платформы принимаются решения о смене GPU, выборе batch size и запуске retrain при снижении throughput.

Bottom line

За 8 недель мы превратили фрагментированную ML-инфраструктуру в управляемую систему с прозрачными метриками latency, throughput и $/req. Платформа позволила сократить затраты на 43 %, стабилизировать p99 latency и внедрить алерты, срабатывающие при SLA-деградациях и росте стоимости. Теперь каждое решение о запуске модели опирается на цифры на уровне модели, инстанса и бюджета.

Связаться

Контакты

Готов к обсуждению ML‑проектов и внедрений, отвечаю лично.

Игорь Якушев,
ML-инженер

Фото Игоря Якушева, ML-инженера обо мне
1 слот открыт для проекта на август

Решения с упором на продукт и System Design. Меня привлекают задачи с потенциалом системного роста.

Как начать разговор:

  1. 1. Напишите мне напрямую Свяжитесь так, как удобно вам: Telegram, email или LinkedIn.
  2. 2. Расскажите о задаче Пара строк: контекст, цель, формат - этого достаточно.
  3. 3. Если вижу, что могу помочь, договоримся о старте Предложу следующий шаг.

Быстрее всего

Написать в Telegram