Нагрузочное тестирование 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 метрики.
