Туториалы25 августа 2026 г. · 9 мин чтения

Нагрузочное тестирование LLM-инференса: токены, TTFT и GPU под капотом

LLM-эндпоинты ломают все классические допущения нагрузочного тестирования. Разбираемся, как моделировать реалистичную нагрузку на OpenAI-совместимые серверы и измерять то, что действительно важно: TTFT, токены/с, TPOT, насыщение GPU и стоимость.

Автор: Команда Perfscale

Нагрузочное тестирование LLM-инференса: токены, TTFT и GPU под капотом

Нагрузочное тестирование LLM-эндпоинта отличается от HTTP-тестирования одним фундаментальным образом: вы измеряете поток токенов, который выжимает из себя насыщенный GPU, а не латентность запрос/ответ против stateless-сервиса.

«Быстрый» API, отвечающий 200 за 5 мс, ничего не говорит об LLM-сервере. Ответ начинается с первого токена и заканчивается — когда-нибудь — когда модель догенерирует текст. И весь этот процесс работает на GPU с жёстким пределом пропускной способности: как только он насыщен, каждый новый конкурентный запрос замедляет все выполняющиеся запросы, а не только сам себя.

Почему классические метрики врут

Четыре допущения из HTTP-нагрузочного тестирования ломаются на LLM-инференсе:

  • Латентность — не одно число. Time-to-first-token (TTFT) измеряет prefill и очерёдность; всё остальное — это decode. TTFT 120 мс с медленной генерацией ощущается хуже, чем TTFT 500 мс, после которого ответ стримится со скоростью 80 ток/с. Нужны оба числа, по отдельности.
  • Ответ — это поток. Пользователи воспринимают межтокенную задержку (ITL) — паузу между чанками. Именно её p99 делает чат «тормозящим», даже когда средняя скорость в токенах/с выглядит нормально.
  • Узкое место — GPU, а не сеть. Когда утилизация SM упирается в 100%, а VRAM заполняется, TTFT начинает расти у всех. «Сервер насыщен» и «сервер неправильно настроен» неотличимы без телеметрии GPU рядом с числами латентности.
  • У каждого запроса есть цена в долларах. Токены тарифицируются. Нагрузочный тест против облачного API — это счёт; а план мощностей без стоимости за токен — половина плана.

Что измеряет PerfScale

В OSS-движке есть нативный шаг std/llm@v1. Один шаг = одно chat completion. Стриминг включён по умолчанию для OpenAI-совместимых и Anthropic-эндпоинтов: ответ читается как server-sent events, TTFT — момент прибытия первого чанка с контентом, а счётчики токенов берутся из отчёта usage самого сервера:

steps:
  - name: chat completion
    use: std/llm@v1
    with:
      url: http://127.0.0.1:11434/v1/chat/completions
      model: llama3.1
      prompt: "Summarize the CAP theorem in two sentences."
      max_tokens: 256
    check:
      status: 200

Каждый успешный запрос порождает метрики llm_*, которые ведут себя как любые другие trend/counter — сводка, --summary-export, thresholds:

  • llm_ttft_ms — время до первого токена (стриминговые запросы)
  • llm_tokens_per_sec — скорость генерации после первого токена
  • llm_prompt_tokens / llm_completion_tokens — расход токенов
  • llm_chunks — полученные SSE-чанки

Эндпоинт openai покрывает любой OpenAI-совместимый сервер — Ollama, vLLM, LM Studio, Together, Groq. anthropic говорит по messages API, а generic обрабатывает всё остальное: тело запроса — это ваш объект params как есть, а правила extract (dotted-пути или регулярные выражения) вытаскивают текст и счётчики токенов из любого ответа.

Платные агенты добавляют детальный слой поверх тех же потоков: перцентили ITL (pro_llm_itl_avg_ms, pro_llm_itl_p50/p95/p99_ms), TPOT (pro_llm_tpot_ms — время на выходной токен, (total − ttft) / (completion_tokens − 1)) и стоимость (pro_llm_cost_usd), посчитанную по вашему прайс-листу через PERFSCALE_LLM_PRICES — агент не поставляет никаких таблиц цен.

Две модели нагрузки — два вопроса

Серверы GPU-инференса деградируют нелинейно, поэтому модель нагрузки важнее обычного. В репозитории есть обе, в bench/gpu/:

Закрытая модель (stages.yaml) — ступенчатый рост VU с плато на 2, 8 и 16 конкурентных запросов. Она отвечает на вопрос: как деградируют ток/с и TTFT с ростом конкурентности? Каждое плато — точка измерения: периодические строки [stats] делят шкалу времени с таймсериями GPU, поэтому вы читаете поведение по плато, а не только агрегаты прогона.

Открытая модель (arrival.yaml) — arrival-rate, 1 → 16 completion/с с max_vus: 64. Новые запросы приходят по расписанию, даже когда сервер не успевает. Она отвечает на вопрос: какую скорость сервер реально выдерживает? Rate, при котором TTFT начинает расти, а dropped_iterations впервые уходит выше нуля — практическая ёмкость для этой модели и промпта:

arrival:
  max_vus: 64
  pre_allocated_vus: 2
  stages:
    - { duration: 1m, rate: 1 }
    - { duration: 1m, rate: 2 }
    - { duration: 1m, rate: 4 }
    - { duration: 1m, rate: 8 }
    - { duration: 1m, rate: 16 }
    - { duration: 30s, rate: 0 }  # drain in-flight requests
gpu:
  enabled: true
  interval_ms: 1000
  source: nvidia-smi

GPU — это и есть тестируемая система

Блок gpu: сэмплирует GPU хоста весь прогон — утилизацию, VRAM, температуру, мощность — через nvidia-smi (по умолчанию, достаточно бинарника драйвера) или эндпоинт dcgm-exporter. Полная таймсерия попадает в экспорт сводки, а прогон печатает компактный блок:

gpu: 1 device, 300 samples every 1000ms (nvidia-smi)
gpu0: util avg=64.3% max=100.0% vram max=41088/81559MiB temp max=71.0C power max=512.3W

Читайте его вместе с числами латентности:

  • ток/с не меняется между плато, а утилизация GPU сидит на ~100% → узкое место — GPU. Меньше модель, квантизация, больше VRAM — или примите этот rate.
  • утилизация заметно ниже 100% при растущем TTFT p95 → узкое место — сервер (очереди, лимиты контекста/батчинга). Крутите сервер, а не GPU.
  • vram max, ползущий к объёму карты, объясняет эвикции и OOM в середине прогона.
  • dropped > 0 в arrival-профиле означает, что rate превысил возможности воркеров — колено ёмкости чуть раньше этого значения.

Про-источник nvidia-smi-pro добавляет частоты SM/памяти, причины троттлинга и попроцессные агрегаты VRAM: падение частоты рядом с ненулевой битмаской троттлинга во время фазы нагрузки — это дымящийся пистолет термального троттлинга, а счётчики процессов показывают, не делит ли GPU с сервером что-то ещё.

Поставьте это на гейт

Thresholds работают со всеми этими метриками, так что CI может проверять LLM SLO наравне с любыми другими. Профиль stages из bench-сьюта завершается fail-гейтом:

after:
  - name: SLO gate
    use: std/thresholds@v1
    with:
      llm_ttft_ms: ["p50<500", "p95<2000"]
      llm_tokens_per_sec: ["avg>20"]
      llm_ttft_ms_failed: ["rate<0.01"]

На платном агенте гейт можно поставить на хвост самого потока: pro_llm_itl_p95_ms: p95 < 100. На OSS-сборке метрики pro_llm_* просто никогда не появляются — прогон работает без изменений.

Одна оговорка, которую стоит повторить: приведённые числа — стартовая точка, откалиброванная под GPU среднего класса. Прогоните базовую линию на своей карте, а потом ужесточайте гейты — строгие thresholds и есть цель, когда вы знаете своё железо.

Попробуйте

ollama pull llama3.2:3b
perfscale run -f bench/gpu/ollama.yaml -c bench/gpu/stages.yaml

Полный справочник параметров — в docs/core/llm.md и docs/core/gpu.md; слой pro-метрик задокументирован на странице LLM и GPU метрики.

Комментарии

Ответить в Bluesky