Перейти к содержанию

Бенчмаркинг LLM Serving: методология и метрики

~7 минут чтения

Как правильно измерять производительность LLM в продакшене. Метрики (TTFT, TPOT, throughput, SLO attainment), методология, инструменты (llmperf, genai-perf), типичные ошибки

Предварительно: Оптимизация инференса LLM, Сравнение движков инференса

Зачем это нужно. Неправильный бенчмарк стоит денег: выбор движка на основе теста с concurrency=1 вместо реальных 100 может привести к 3-5x перерасходу на GPU ($50K-200K/год на одном кластере). BentoML в 2024 показал, что их движок "на 40% быстрее vLLM" -- но при concurrency=1 и 128 output tokens. При реалистичной нагрузке (concurrency=32, variable lengths) разница была <5%. Правильная методология бенчмаркинга -- это не "запустить curl", а инженерная дисциплина с warmup, sweep по concurrency, и обязательным SLO attainment rate.


Зачем отдельная статья про бенчмарки?

Выбрать движок инференса (vLLM, SGLang, TRT-LLM) -- полдела. Правильно измерить его производительность -- вторая половина, и именно тут большинство ошибается. Два инженера могут получить противоположные результаты на одном и том же движке, потому что:

  • Один тестировал с concurrency=1, другой с 100
  • Один считал throughput по output tokens, другой включал input
  • Один разогрел CUDA, другой нет

Бенчмаркинг LLM serving -- это не time curl. Это инженерная дисциплина.


Ключевые метрики

Latency-метрики

Метрика Что измеряет Почему важна Target (production)
TTFT (Time to First Token) Задержка до первого токена UX: пользователь видит "ответ начался" < 200ms (chat), < 500ms (batch)
TPOT (Time Per Output Token) Среднее время генерации одного токена Скорость "печати" ответа < 30ms (40+ tok/s для чтения)
E2E Latency (p50/p95/p99) Полная задержка от запроса до последнего токена Хвостовые задержки убивают SLO p99 < 5s для 512-token ответа
ITL (Inter-Token Latency) Разброс между токенами "Заикания" в стриминге Стабильный, без spikes

Связь метрик

Для запроса с \(n\) output tokens:

\[E2E = TTFT + (n - 1) \times TPOT\]

Пример: TTFT=150ms, TPOT=25ms, n=200 tokens:

$$E2E = 150 + 199 \times 25 = 5125\text{ms} \approx 5.1\text{s}$$

TTFT доминирует при коротких ответах, TPOT -- при длинных.

Throughput-метрики

Метрика Формула Типичные значения (A100, Llama 70B)
Output tokens/sec \(\frac{\text{total output tokens}}{\text{wall time}}\) 500-2000 tok/s (batch)
Requests/sec \(\frac{\text{completed requests}}{\text{wall time}}\) 5-50 req/s
Effective throughput Output tokens/sec при соблюдении SLO Меньше raw throughput на 10-30%

Business-метрика: SLO Attainment Rate

Самая важная метрика для продакшена -- какой процент запросов укладывается в SLO:

def slo_attainment(latencies: list[float], slo_ms: float) -> float:
    """Доля запросов, уложившихся в SLO."""
    within_slo = sum(1 for lat in latencies if lat <= slo_ms)
    return within_slo / len(latencies)

# Пример: SLO = 3 секунды для chat
latencies = [1.2, 2.5, 3.1, 0.8, 2.9, 4.2, 1.5, 2.0]
print(f"SLO attainment: {slo_attainment(latencies, 3.0):.0%}")
# SLO attainment: 62% -- ПЛОХО, нужно > 95%

Raw throughput vs Effective throughput

Движок может показывать 2000 tok/s throughput, но если 40% запросов нарушают SLO -- это бесполезно. Effective throughput считает только токены из запросов, которые уложились в SLO. Всегда спрашивайте: "при каком SLO attainment rate?"


Методология бенчмаркинга

Что нужно зафиксировать

Бенчмарк без фиксации условий -- не бенчмарк:

Параметр Что варьируют Что фиксируют
Модель Размер (7B/13B/70B) Квантизация, формат весов
Hardware GPU тип Driver version, CUDA version
Workload Concurrency, input/output длина Распределение длин
Engine Версия, конфигурация Все параметры (batch size, cache)
Warmup -- Обязательно 10-50 запросов

Паттерны нагрузки

Реальный трафик -- не постоянный поток одинаковых запросов:

import numpy as np

# Паттерн 1: Чат (короткие in/out)
chat_workload = {
    "input_len": np.random.lognormal(mean=5, sigma=0.8, size=1000).astype(int),  # ~150 tokens median
    "output_len": np.random.lognormal(mean=4, sigma=0.7, size=1000).astype(int),  # ~55 tokens median
    "concurrency": 50,
}

# Паттерн 2: Code generation (короткий in, длинный out)
code_workload = {
    "input_len": np.random.lognormal(mean=5.5, sigma=0.5, size=1000).astype(int),  # ~245 tokens
    "output_len": np.random.lognormal(mean=6, sigma=0.8, size=1000).astype(int),  # ~400 tokens
    "concurrency": 20,
}

# Паттерн 3: RAG/Summarization (длинный in, короткий out)
rag_workload = {
    "input_len": np.random.lognormal(mean=7.5, sigma=0.5, size=1000).astype(int),  # ~1800 tokens
    "output_len": np.random.lognormal(mean=4.5, sigma=0.5, size=1000).astype(int),  # ~90 tokens
    "concurrency": 30,
}

Ключевой инсайт: движок, который лидирует на chat workload, может проигрывать на RAG. Бенчмарк на одном паттерне -- не бенчмарк.


Инструменты

1. genai-perf (NVIDIA)

Стандарт для бенчмаркинга OpenAI-compatible API:

# Установка
pip install genai-perf

# Бенчмарк vLLM-сервера
genai-perf profile \
  -m meta-llama/Llama-3.1-8B-Instruct \
  --service-kind openai \
  --endpoint-type chat \
  --streaming \
  --concurrency 32 \
  --measurement-interval 60000 \
  --url localhost:8000

Выдает: TTFT (p50/p95/p99), TPOT, throughput, request latency distribution.

2. llmperf (Anyscale)

Open-source, фокус на SLO attainment:

pip install llmperf

python -m llmperf.token_benchmark_ray \
  --model meta-llama/Llama-3.1-8B-Instruct \
  --mean-input-tokens 256 \
  --stddev-input-tokens 64 \
  --mean-output-tokens 128 \
  --stddev-output-tokens 32 \
  --num-concurrent-requests 32 \
  --max-num-completed-requests 500 \
  --llm-api openai

3. Своя нагрузка (для нестандартных сценариев)

import asyncio
import time
import aiohttp
from dataclasses import dataclass

@dataclass
class RequestMetrics:
    ttft: float
    tpot: float
    total_tokens: int
    e2e_latency: float

async def benchmark_request(
    session: aiohttp.ClientSession,
    url: str,
    prompt: str,
    max_tokens: int,
) -> RequestMetrics:
    start = time.perf_counter()
    first_token_time = None
    token_count = 0

    payload = {
        "model": "llama-3.1-8b",
        "messages": [{"role": "user", "content": prompt}],
        "max_tokens": max_tokens,
        "stream": True,
    }

    async with session.post(url, json=payload) as resp:
        async for line in resp.content:
            decoded = line.decode().strip()
            if decoded.startswith("data: ") and decoded != "data: [DONE]":
                if first_token_time is None:
                    first_token_time = time.perf_counter()
                token_count += 1

    end = time.perf_counter()
    ttft = first_token_time - start if first_token_time else end - start
    generation_time = end - first_token_time if first_token_time else 0
    tpot = generation_time / max(token_count - 1, 1)

    return RequestMetrics(
        ttft=ttft,
        tpot=tpot,
        total_tokens=token_count,
        e2e_latency=end - start,
    )

Benchmark Results (2025-2026)

Throughput при разной concurrency (Llama 3.1 8B, A100 80GB)

Concurrency vLLM 0.6+ SGLang 0.4+ TRT-LLM TGI 2.x
1 85 tok/s 90 tok/s 105 tok/s 70 tok/s
8 550 tok/s 580 tok/s 620 tok/s 420 tok/s
32 1800 tok/s 1950 tok/s 1700 tok/s 1200 tok/s
128 2400 tok/s 2500 tok/s degraded 1500 tok/s

Паттерн: TRT-LLM лидирует при низкой concurrency (оптимизированные CUDA kernels). SGLang и vLLM обгоняют при высокой (лучший scheduling, continuous batching).

TTFT (p50, ms)

Concurrency vLLM SGLang TRT-LLM TGI
1 45 40 30 55
32 120 100 180 150
128 250 280 timeout 400

Decision Framework

graph TD
    START["Выбор движка инференса"] --> LAT{"Главный приоритет?"}

    LAT -->|"Min latency<br/>(concurrency < 16)"| TRT["TensorRT-LLM"]
    LAT -->|"Max throughput<br/>(concurrency 32+)"| THROUGHPUT{"Structured output?"}
    LAT -->|"Простота деплоя"| SIMPLE{"Нужен Enterprise?"}

    THROUGHPUT -->|"Да, JSON/tools"| SGL["SGLang"]
    THROUGHPUT -->|"Нет, general"| VLLM["vLLM"]

    SIMPLE -->|"Да"| TGI["TGI / Inference Endpoints"]
    SIMPLE -->|"Нет"| VLLM2["vLLM"]

    style START fill:#e8eaf6,stroke:#3f51b5
    style TRT fill:#fff3e0,stroke:#ef6c00
    style SGL fill:#e8f5e9,stroke:#4caf50
    style VLLM fill:#e8f5e9,stroke:#4caf50
    style TGI fill:#f3e5f5,stroke:#9c27b0
    style VLLM2 fill:#e8f5e9,stroke:#4caf50

Типичные ошибки бенчмаркинга

Бенчмарк без warmup

Первые 10-50 запросов к LLM движку значительно медленнее: JIT-компиляция CUDA kernels, заполнение KV cache, инициализация memory pools. Без warmup TTFT может быть в 5-10x хуже реального. Всегда делайте warmup и отбрасывайте первые запросы.

Тестирование при concurrency=1

Реальный production трафик -- десятки-сотни параллельных запросов. При concurrency=1 выигрывает движок с лучшей single-request latency (TRT-LLM), но при concurrency=100 лидер может смениться (vLLM, SGLang). Тестируйте при реалистичной нагрузке.

Фиксированные input/output длины

Запросы "Hello, tell me about X" -> 128 tokens -- нерелевантный бенчмарк. Реальные распределения длин лог-нормальные с длинным хвостом. Используйте вариацию: mean +/- 2*stddev.


Как читать чужие бенчмарки

Чеклист при чтении benchmark report:

  1. Кто автор? Бенчмарк от BentoML может быть предвзят к BentoML. Проверяйте independent sources.
  2. Какая версия? vLLM 0.3 vs 0.6 -- разные движки. Результаты устаревают за месяцы.
  3. Какой workload? Один input/output length vs распределение. Один concurrency vs sweep.
  4. Warmup? Если не указано -- скорее всего нет, и числа хуже реальных.
  5. Hardware? A100 40GB vs 80GB -- разные результаты. H100 vs A100 -- другая лига.
  6. Quantization? FP16 vs INT8 vs INT4 -- несопоставимо напрямую.

Самопроверка

Вам показывают бенчмарк: "Наш движок X на 40% быстрее vLLM". Input: 128 tokens, output: 128 tokens, concurrency: 1, GPU: H100, модель: Llama 3.1 8B.

Какие вопросы вы зададите? (Подумайте перед тем как читать дальше.)

Ответ: (1) Какая версия vLLM? (2) Был ли warmup? (3) Что при concurrency=32/128? (4) А на 70B модели? (5) Какой TTFT vs throughput отдельно? (6) Какой SLO attainment?


Interview Questions

Q: Чем TTFT отличается от TPOT и когда каждая метрика важнее?

❌ Red flag: "TTFT -- время первого токена, TPOT -- время каждого токена. TTFT для чата, TPOT для батчей."

✅ Strong answer: "TTFT (Time to First Token) -- задержка до первого токена, определяет UX восприятие: пользователь видит 'ответ начался'. TPOT (Time Per Output Token) -- средняя скорость генерации, определяет скорость 'печати'. Формула связи: E2E = TTFT + (n-1) * TPOT. Для chat c коротким ответом (50 tokens) доминирует TTFT -- target < 200ms. Для batch/long generation (500+ tokens) доминирует TPOT -- target < 30ms (40+ tok/s для комфортного чтения). TTFT зависит от prefill phase, TPOT -- от decode phase. Разные фазы -> разные bottlenecks -> разные оптимизации."

Q: Что такое effective throughput и почему он важнее raw throughput?

❌ Red flag: "Effective throughput -- это throughput с учетом ошибок."

✅ Strong answer: "Effective throughput считает только токены из запросов, уложившихся в SLO. Движок может показывать 2000 tok/s raw throughput, но если 40% запросов нарушают SLO (>3s latency) -- effective throughput = 1200 tok/s. При 50% SLO attainment effective = 50% от raw. Это ключевая business-метрика: пользователю всё равно, что движок 'в среднем быстрый', если его конкретный запрос зависает на 10 секунд."

Q: Как бы вы спроектировали бенчмарк для выбора движка инференса?

❌ Red flag: "Отправить 1000 одинаковых запросов и замерить среднее время."

✅ Strong answer: "5 шагов: (1) Определить реальный workload -- распределение input/output длин (лог-нормальное, не фиксированное), concurrency pattern. (2) Зафиксировать условия: GPU, driver, CUDA version, engine version, quantization. (3) Warmup 10-50 запросов, отбросить. (4) Sweep по concurrency (1, 8, 32, 128) -- лидер может меняться. (5) Мерить TTFT p50/p95/p99 + TPOT + throughput + SLO attainment. Минимум 3 прогона для статистической значимости. Обязательно тестировать на своем реальном workload, а не на синтетическом 'Hello, tell me about X -> 128 tokens'."

Q: Напишите функцию, которая вычисляет SLO attainment rate и effective throughput.

❌ Red flag: Считает только средний latency без SLO проверки.

✅ Strong answer: "Функция принимает список RequestMetrics (ttft, tpot, total_tokens, e2e_latency) и SLO порог. SLO attainment = доля запросов с e2e_latency <= SLO. Effective throughput = сумма total_tokens только для запросов в SLO, деленная на wall time. Production target: SLO attainment >= 95%, иначе нужно снижать concurrency или масштабировать GPU."


Заблуждение: throughput в tokens/sec -- универсальная метрика

Разные бенчмарки считают throughput по-разному: одни включают input tokens, другие только output. Output-only throughput в 2-3x ниже total throughput на одном и том же движке. Кроме того, throughput при batch size=1 и при batch size=128 -- разные числа в 10-20x. Всегда уточняйте: output-only или total, при какой concurrency, с каким SLO attainment rate.

Заблуждение: один бенчмарк -- один winner

Движок, лидирующий на chat workload (короткий in/out, concurrency=50), может проигрывать на RAG workload (длинный input, короткий output, concurrency=30). TRT-LLM лидирует при concurrency < 16, но SGLang обгоняет при 32+. Бенчмарк на одном паттерне нагрузки не дает ответа "какой движок лучше" -- только "какой лучше для этого конкретного сценария".

See Also

Sources

  1. genai-perf Documentation -- NVIDIA
  2. llmperf -- Anyscale/Ray
  3. BentoML LLM Benchmark -- June 2024
  4. SGLang Benchmark Report -- LMSYS, 2024
  5. Anyscale LLMPerf Leaderboard -- comparative results