中文翻译暂不可用,显示俄语原文。

Как вы делаете load testing для LLM endpoint? Какие метрики ключевые?

Краткий тезис

Нагрузочное тестирование (load testing) LLM endpoint — это процесс имитации реальной пользовательской нагрузки для оценки производительности, стабильности и выявления узких мест. В отличие от обычного REST API, LLM endpoint требует учёта специфических метрик: TTFT (Time to First Token), TPOT (Time Per Output Token), пропускной способности в токенах/с, утилизации GPU и длины очереди запросов. Ключевые инструменты — Locust, k6 и кастомные скрипты на Python с asyncio. Тестовые данные должны включать реальные промпты из production с разной длиной и сложностью.


1. Термины и определения

Load testing — вид тестирования, при котором система нагружается запросами, близкими к ожидаемым пиковым значениям, для проверки времени отклика, пропускной способности и частоты ошибок.

LLM endpointHTTP-сервер (например, на базе vLLM, TGI, Ollama или собственного инференса), который принимает промпты и возвращает сгенерированный текст (потоком или целиком).

Latency — задержка ответа. Для LLM делится на:

  • TTFT (Time to First Token) — время от отправки запроса до получения первого токена.
  • TPOT (Time Per Output Token) — среднее время генерации одного последующего токена.

Throughput — пропускная способность: количество запросов в секунду (req/s) или токенов в секунду (tokens/s).

Error rate — доля запросов, завершившихся с HTTP-кодом 4xx (клиентская ошибка) или 5xx (серверная ошибка), включая таймауты.

GPU utilization — загрузка GPU в процентах (память, compute). Ключевой ресурс для LLM.

Queue length — количество запросов, ожидающих обработки (в буфере сервера или балансировщика).


2. Зачем нужно нагрузочное тестирование LLM endpoint?

LLM endpoint принципиально отличается от обычного REST API:

  • Генерация текста — ответ не фиксированного размера, время обработки зависит от длины промпта и количества генерируемых токенов.
  • Высокое потребление GPU — каждый запрос требует значительных вычислительных ресурсов, особенно при больших моделях (7B, 13B, 70B параметров).
  • Асинхронность и батчинг — сервер может обрабатывать запросы батчами (batch inference), что усложняет прогнозирование latency.
  • Потоковая vs не-потоковая передача — streaming (Server-Sent Events) меняет метрики: TTFT становится критичным для UX.

Без нагрузочного тестирования невозможно:

  • Определить максимальное количество одновременных пользователей.
  • Выставить корректные лимиты (rate limiting, max concurrency).
  • Обнаружить утечки памяти или деградацию под длительной нагрузкой.
  • Сравнить разные конфигурации деплоя (batch size, quantization, tensor parallelism).

3. Инструменты нагрузочного тестирования

ИнструментЯзыкОсобенностиПлюсыМинусы
LocustPythonДекларативное описание сценариев, веб-интерфейсПростота, поддержка asyncio, распределённый режимМеньше встроенных метрик для LLM
k6JavaScriptВысокая производительность, много метрикБыстрый, встроенные пороги (thresholds), облачные опцииНужно писать на JS, сложнее кастомизация
GatlingScalaМощный, для enterpriseХорошие отчёты, поддержка WebSocketТяжёлый, неудобен для быстрых прототипов
Custom (Python + aiohttp)PythonПолный контрольГибкость, можно замерять TTFT/TPOT напрямуюНужно писать с нуля, нет готовых отчётов

Для LLM endpoint чаще всего используют Locust (из-за простоты и Python) или кастомные скрипты (для точного измерения TTFT/TPOT).


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

4.1 Latency

TTFT (Time to First Token) — критично для потоковых приложений (чат-боты). Измеряется как время от отправки запроса до получения первого байта ответа (при streaming) или первого токена.

TPOT (Time Per Output Token) — среднее время генерации одного токена после первого. Вычисляется как (total_time - TTFT) / (num_output_tokens - 1).

Percentiles (p50, p95, p99) — распределение задержек. p99 показывает худшие случаи (tail latency).

Пример измерения в Python:

import time
import aiohttp

async def measure_llm(prompt):
    start = time.monotonic()
    async with aiohttp.ClientSession() as session:
        async with session.post(url, json={"prompt": prompt, "stream": True}) as resp:
            first_token_time = None
            tokens = 0
            async for chunk in resp.content:
                if first_token_time is None:
                    first_token_time = time.monotonic()
                tokens += 1
            end = time.monotonic()
    ttft = first_token_time - start
    tpot = (end - first_token_time) / max(tokens - 1, 1)
    return ttft, tpot, tokens

4.2 Throughput

  • Requests per second (req/s) — сколько запросов сервер обрабатывает в секунду.
  • Tokens per second (tokens/s) — общая скорость генерации токенов (сумма output токенов всех запросов за секунду). Более информативно для LLM, так как запросы разной длины.

4.3 Error rate

  • 4xx — превышение лимитов (429 Too Many Requests), некорректные запросы.
  • 5xx — внутренние ошибки сервера (перегрузка, OOM, падение модели).
  • Timeout — запрос не завершился за заданный таймаут (обычно 30–60 с).

4.4 GPU utilization

  • GPU memory used — объём занятой видеопамяти (важно для batch size).
  • GPU compute utilization — процент времени, когда GPU занят вычислениями.
  • Power consumption — энергопотребление (для оценки стоимости).

4.5 Queue length

Количество запросов, ожидающих в очереди перед обработкой. Если очередь растёт — сервер не справляется, latency будет увеличиваться.


5. Как измерять: мониторинг и логирование

Для сбора метрик во время теста необходимо:

  • Логировать каждый запрос: timestamp, TTFT, TPOT, количество токенов, HTTP-статус.
  • Использовать системные метрики: nvidia-smi (GPU), psutil (CPU, RAM), iostat (диск).
  • Агрегировать в реальном времени: Prometheus + Grafana или встроенные дашборды Locust.

Пример сбора GPU метрик в Python:

import subprocess
import json

def get_gpu_metrics():
    result = subprocess.run(
        ["nvidia-smi", "--query-gpu=utilization.gpu,memory.used,memory.total",
         "--format=csv,noheader,nounits"],
        capture_output=True, text=True
    )
    lines = result.stdout.strip().split("\n")
    metrics = []
    for line in lines:
        parts = line.split(", ")
        metrics.append({
            "gpu_util": float(parts[0]),
            "mem_used": float(parts[1]),
            "mem_total": float(parts[2])
        })
    return metrics

6. Тестовые данные

Используйте реальные промпты из production логов — это даст наиболее релевантную нагрузку. Если production нет, синтезируйте датасет:

  • Короткие промпты (1–50 токенов) — имитация простых вопросов.
  • Средние (50–500 токенов) — типичные запросы с контекстом.
  • Длинные (500–2000+ токенов) — обработка больших документов.
  • Разное количество output токенов — настройте параметр max_tokens от 50 до 2048.

Распределение должно повторять реальное (например, 60% коротких, 30% средних, 10% длинных).


7. Сценарии тестирования

СценарийОписаниеЦель
Ramp-upПостепенное увеличение числа пользователей (например, +1 пользователь/сек)Определить точку насыщения
Steady stateФиксированная нагрузка (например, 50 req/s) в течение 30 минутПроверить стабильность, утечки памяти
Spike testРезкое увеличение нагрузки (с 10 до 100 req/s за 1 сек)Проверить поведение при внезапных пиках
Soak testДлительная нагрузка (несколько часов)Выявить деградацию со временем

8. Анализ результатов и бутылочные горлышки

Типичные узкие места:

  • GPU compute — высокая утилизация GPU (>95%) и рост TPOT. Решение: уменьшить batch size, использовать меньшую модель, добавить GPU.
  • GPU memory — OOM (out of memory) при большом batch size. Решение: уменьшить batch size, использовать quantization (int8, fp16).
  • CPU / Network — низкая GPU утилизация, но высокий latency. Значит, узкое место в пред/постобработке или сети. Решение: оптимизировать токенизацию, увеличить bandwidth.
  • Queue length — растёт, latency растёт. Решение: добавить реплики, настроить балансировщик, увеличить max concurrency.

SLA (Service Level Agreement) — определите целевые значения:

  • p95 TTFT < 500 мс для потоковых приложений.
  • p95 TPOT < 50 мс/токен.
  • Error rate < 1%.
  • Throughput не ниже N tokens/s.

9. Пример кастомного нагрузочного теста на Python

import asyncio
import aiohttp
import time
import statistics

async def worker(sem, url, prompt, results):
    async with sem:
        start = time.monotonic()
        try:
            async with aiohttp.ClientSession() as session:
                async with session.post(url, json={"prompt": prompt, "max_tokens": 100}) as resp:
                    data = await resp.json()
                    latency = time.monotonic() - start
                    results.append({
                        "latency": latency,
                        "status": resp.status,
                        "tokens": len(data.get("choices", [{}])[0].get("text", "").split())
                    })
        except Exception as e:
            results.append({"latency": time.monotonic() - start, "status": 0, "error": str(e)})

async def run_load(url, prompts, concurrency=10):
    sem = asyncio.Semaphore(concurrency)
    results = []
    tasks = [worker(sem, url, p, results) for p in prompts]
    await asyncio.gather(*tasks)
    return results

# Использование
url = "http://localhost:8000/v1/completions"
prompts = ["What is AI?"] * 100  # 100 одинаковых запросов
results = asyncio.run(run_load(url, prompts, concurrency=20))

latencies = [r["latency"] for r in results if r["status"] == 200]
print(f"p50: {statistics.median(latencies):.3f}s")
print(f"p95: {sorted(latencies)[int(len(latencies)*0.95)]:.3f}s")
print(f"Error rate: {sum(1 for r in results if r['status'] != 200)/len(results)*100:.1f}%")

10. Best practices

  • Прогревайте модель — первые запросы могут быть медленнее из-за инициализации кэша. Выполните 10–20 «прогревочных» запросов перед замером.
  • Учитывайте batch size — сервер может батчить запросы, что снижает latency для отдельных запросов, но увеличивает TTFT для всех в батче.
  • Rate limiting — настройте ограничения на стороне клиента, чтобы не перегружать сервер раньше времени.
  • Мониторинг GPU — используйте nvidia-smi dmon или Prometheus exporter для сбора в реальном времени.
  • Повторяйте тесты — минимум 3 раза для статистической значимости.

Пет-проект для закрепления

Задача: Написать нагрузочный тест для локального LLM endpoint (например, через Ollama или vLLM) с измерением TTFT, TPOT, throughput и GPU utilization.

Инструменты: Python, aiohttp, asyncio, nvidia-smi (или pynvml).

Шаги:

  1. Разверните LLM endpoint локально (например, ollama run llama3.2).
  2. Создайте датасет из 50 промптов разной длины (от 10 до 500 токенов).
  3. Напишите скрипт, который отправляет запросы с разной степенью конкурентности (1, 5, 10, 20).
  4. Для каждого уровня замерьте:
    • Средний TTFT и TPOT (p50, p95).
    • Throughput в req/s и tokens/s.
    • Error rate.
    • GPU utilization (с помощью nvidia-smi --query).
  5. Постройте графики зависимости latency и throughput от конкурентности.
  6. Определите максимальную конкурентность, при которой p95 TTFT < 1 с и error rate < 1%.

Ожидаемый результат: Отчёт с графиками и рекомендациями по оптимальному количеству одновременных пользователей для данного endpoint.


Связь с другими вопросами

ВопросТема
450Деплой LLM модели в production
452Мониторинг LLM endpoint в реальном времени
453A/B тестирование разных версий модели
454Оптимизация latency LLM инференса
455Масштабирование LLM сервиса (горизонтальное/вертикальное)
460Выбор hardware для LLM инференса

Навигация