Discovery
Попробуйте другие наши приложения
Как бенчмаркать локальную LLM в 2026: TTFT, TPOT, KV cache, context length и VRAM
30 апреля 2026 г.

Больше интересного про инференс, AI-инфраструктуру и практику с LLM я публикую в Telegram-канале @fuckup_files.

Локальную LLM в 2026 уже нельзя нормально сравнивать одной цифрой tokens/sec. Эта цифра удобная, но почти всегда обманывает: один человек меряет короткий prompt на холодном сервере, второй гоняет 128K контекст с prefix cache, третий смотрит только decode, четвёртый включает speculative decoding и получает красивый throughput, который разваливается на реальном agent workload.

Если вы хотите понять, как бенчмаркать локальную LLM в 2026, смотрите минимум на пять вещей: TTFT, TPOT, KV cache, context length и VRAM. Именно они отвечают на практические вопросы: будет ли чат отвечать быстро, потянет ли RTX 3090/4090 длинный контекст, почему в vLLM всё быстро на коротких запросах и внезапно плохо на агентных задачах, и где на самом деле узкое место — модель, память, batching или scheduler.

Ниже — практический разбор, как я бы снимал бенчмарк локальной LLM, чтобы цифрам можно было верить.

TL;DR

  • tokens/sec без контекста почти бесполезен. Всегда пишите модель, quantization, GPU, engine, версию engine, context length, prompt/output tokens и concurrency.

  • TTFT показывает, сколько пользователь ждёт первый токен. Он сильно растёт на длинном prompt, потому что prefill должен построить KV cache.

  • TPOT/ITL показывают, насколько плавно идёт генерация после первого токена. Тут чаще упираемся в memory bandwidth и размер batch.

  • KV cache — главный пожиратель VRAM на длинных контекстах. Вес модели фиксирован, а KV cache растёт с количеством токенов.

  • Для vLLM используйте vllm bench serve, сохраняйте JSON и percentile metrics: ttft,tpot,itl,e2el.

  • Бенчмарк должен включать несколько профилей: короткий чат, длинный prompt, agent loop, high concurrency и warm cache.

Почему tokens/sec недостаточно

Представим два результата:

Setup A: 120 tok/s
Setup B: 80 tok/s

На первый взгляд A лучше. Но теперь добавим детали:

Setup A:
- prompt: 256 токенов
- output: 256 токенов
- concurrency: 1
- context: 4K
- TTFT: 0.4s

Setup B:
- prompt: 32K токенов
- output: 1024 токена
- concurrency: 16
- context: 128K
- TTFT: 3.2s

Это уже не один и тот же тест. Первый больше похож на короткий чат. Второй — на RAG, coding-agent или длинный документ. Сравнивать их одной цифрой всё равно что сравнивать скорость велосипеда и грузовика по одному спидометру без веса, дороги и груза.

В 2026 локальные LLM всё чаще используют не для “напиши анекдот”, а для агентных задач: coding agents, длинные tool traces, большие system prompts, RAG, multi-turn history, structured output. Там важны не только токены в секунду, а форма задержки.

Базовые метрики: TTFT, TPOT, ITL, E2E

В официальном блоге vLLM метрики формулируются достаточно прямо: TTFT — время от отправки запроса до первого output token, ITL — задержка между соседними токенами, TPOT — средний ITL по output tokens внутри запроса, E2E latency — полное время от запроса до последнего токена. У vLLM есть отдельный CLI vllm bench {serve,latency,throughput} для таких измерений: vLLM blog: Anatomy of a High-Throughput LLM Inference System.

На практике:

МетрикаЧто показываетГде болит
TTFTсколько ждать первый токенprefill, длинный prompt, cold cache, очередь
TPOTсреднее время на output tokendecode, batch size, memory bandwidth
ITLинтервал между токенамиплавность streaming UX
E2Eполная задержка запросавесь pipeline целиком
Throughputтокены/сек или requests/secполезно для capacity planning
Goodputthroughput в рамках SLOсколько реально “годного” throughput

Anyscale в своих docs отдельно подчёркивает различие TPOT и ITL: для одного запроса TPOT можно считать как (E2E latency - TTFT) / (#output_tokens - 1), а на множестве запросов средний TPOT и средний ITL агрегируются по-разному: Anyscale docs: LLM latency and throughput metrics.

Это важно: если один запрос сгенерировал 2 токена, а другой 1000, средний TPOT и token-weighted ITL будут вести себя по-разному. Для пользовательского UX смотрим percentiles, для capacity planning — throughput и goodput.

Как prefill и decode ломают простые выводы

LLM inference состоит из двух разных фаз:

  1. Prefill — модель читает весь prompt и строит KV cache. Это обычно compute-heavy фаза.

  2. Decode — модель генерирует токены один за другим, постоянно читая KV cache. Это часто memory-bandwidth-heavy фаза.

Отсюда простой вывод:

  • длинный prompt ухудшает TTFT;

  • большой output ухудшает E2E;

  • большой batch улучшает throughput, но может ухудшить latency;

  • большой context увеличивает KV cache и начинает давить на VRAM;

  • prefix caching может резко улучшить TTFT, но только если префиксы действительно совпадают.

Если вы уже читали мой разбор KV-cache в моделях transformers, то это ровно тот же механизм, только теперь мы смотрим на него не как на теорию, а как на источник конкретных цифр в бенчмарке.

Что обязательно записывать в каждом прогоне

Хороший локальный бенчмарк — это не только результат, но и metadata. Без metadata вы через неделю сами не поймёте, что именно измеряли.

Минимальный паспорт прогона:

model: Qwen/Qwen3.5-4B
engine: vLLM
engine_version: 0.20.0
backend: openai-compatible server
gpu:
  model: RTX 4090
  count: 1
  vram_gb: 24
driver:
  cuda: 13.x
  pytorch: 2.11.x
model_format:
  dtype: fp8
  quantization: null
serving_flags:
  max_model_len: 32768
  gpu_memory_utilization: 0.90
  max_num_seqs: 16
  enable_prefix_caching: true
  enable_chunked_prefill: true
benchmark:
  input_tokens: 4096
  output_tokens: 512
  concurrency: 8
  request_rate: 2
  warmup_requests: 16
metrics:
  ttft_p50_ms: 0
  ttft_p95_ms: 0
  tpot_p50_ms: 0
  tpot_p95_ms: 0
  e2e_p95_ms: 0
  output_tok_s: 0
  peak_vram_gb: 0

В свежем посте про LocalMaxxing обсуждали похожую идею: бенчмарк локальной LLM должен фиксировать не только модель и скорость, но и platform, GPU model/quantity, engine/version/flags, quantization, context length, batch size, input/output tokens, TTFT и peak VRAM: пример обсуждения в X.

Это правильное направление. Без таких полей локальный benchmark превращается в “у меня быстрее, чем у тебя”, а не в инженерное измерение.

Как запустить vLLM benchmark serve

Для online-serving сценария используйте vllm bench serve. В актуальной CLI-документации vLLM есть параметры для сохранения результата в JSON, detailed per-request info, percentile metrics, goodput и SLO по ttft/tpot/e2el: vLLM docs: vllm bench serve.

Сначала поднимаем сервер:

vllm serve Qwen/Qwen3.5-4B \
  --host 0.0.0.0 \
  --port 8000 \
  --gpu-memory-utilization 0.90 \
  --max-model-len 32768 \
  --enable-prefix-caching \
  --enable-chunked-prefill

Потом прогоняем serving benchmark:

vllm bench serve \
  --backend vllm \
  --base-url http://127.0.0.1:8000 \
  --model Qwen/Qwen3.5-4B \
  --dataset-name random \
  --random-input-len 4096 \
  --random-output-len 512 \
  --num-prompts 256 \
  --request-rate 2 \
  --max-concurrency 8 \
  --num-warmups 16 \
  --percentile-metrics ttft,tpot,itl,e2el \
  --metric-percentiles 50,90,95,99 \
  --save-result \
  --save-detailed \
  --metadata engine=vllm \
  --metadata test=local-llm-4096x512-c8

Ключевые параметры:

  • --random-input-len — длина prompt в токенах.

  • --random-output-len — сколько токенов генерировать.

  • --request-rate — входящий RPS. inf превращает тест в “завалить сервер всем сразу”, что полезно для throughput, но не всегда похоже на реальный traffic.

  • --max-concurrency — верхняя граница одновременных запросов.

  • --num-warmups — прогрев.

  • --save-detailed — per-request данные, без них сложнее объяснить хвосты.

  • --goodput ttft:1000 tpot:50 — можно считать не просто throughput, а throughput, который уложился в SLO.

Пять профилей, которые стоит гонять

Один benchmark ничего не доказывает. Я бы делал минимум пять профилей.

1. Короткий чат

input: 512 токенов
output: 256 токенов
concurrency: 1-8

Показывает обычный chat UX. Тут важны TTFT p50/p95 и ITL/TPOT. Если этот тест плохой, всё остальное будет ещё хуже.

2. Длинный prompt

input: 16K-64K токенов
output: 256-512 токенов
concurrency: 1-4

Это тест на prefill. Он отвечает на вопрос: “что будет, если скормить модели документацию, большой issue, длинную историю чата или RAG context”.

3. Agent loop

input: 8K-64K токенов
output: 512-2048 токенов
concurrency: 4-16
shared prefix: включить отдельный прогон

Агентные задачи часто имеют длинный стабильный system prompt, tool definitions и растущую историю. Тут важно сравнить:

  • без prefix caching;

  • с prefix caching;

  • с чуть изменённым prompt, который ломает cache hit.

Если prefix caching даёт красивый результат только в синтетике, а в реальном agent loop hit rate низкий — это не оптимизация, а иллюзия.

Подробнее про отличие vLLM block-level caching и SGLang radix tree я уже разбирал в статье vLLM vs SGLang: radix tree против block-level prefix caching.

4. High concurrency

input: 1K-4K токенов
output: 256-1024 токена
concurrency: 16-128
request_rate: ступеньками

Это capacity planning. Тут смотрим не только средние, а p95/p99. Сервер может красиво держать средний TPOT, но раздавать p99 TTFT в десятки секунд.

5. Long-context VRAM test

input: 32K / 64K / 128K
output: 128-512
concurrency: 1

Это тест на предел VRAM. Hardware Corner хорошо формулирует базовую механику: context window — это количество информации в токенах, а KV cache растёт линейно с длиной контекста; на очень длинных контекстах KV cache может стать больше, чем сами веса модели: What Is Context Length in LLMs and How It Impacts Your VRAM.

Именно поэтому “модель влезла” не значит “модель нормально работает на 128K context”.

Как смотреть VRAM правильно

nvidia-smi полезен, но его мало. Он показывает занятость памяти, но не объясняет, почему она занята. Для локального benchmark фиксируйте:

nvidia-smi --query-gpu=timestamp,name,memory.used,memory.total,utilization.gpu,utilization.memory,power.draw \
  --format=csv -l 1

И параллельно сохраняйте логи engine:

vllm serve ... 2>&1 | tee vllm-server.log

Что искать:

  • peak VRAM на старте;

  • peak VRAM после прогрева;

  • VRAM на длинном prompt;

  • падает ли throughput после нескольких минут;

  • появляются ли OOM/retry/swap/offload;

  • растёт ли память после завершения запросов.

Если видите странный рост памяти в vLLM или distributed serving — это уже не benchmark, а debugging. Для такого случая у меня есть отдельный разбор Куча лжи: отладка утечки памяти в vLLM.

Как context length влияет на результат

Context length влияет сразу на три вещи:

  1. TTFT — длинный input дольше prefill-ится.

  2. VRAM — KV cache растёт вместе с числом токенов.

  3. TPOT/ITL — decode читает всё больше cache, особенно на длинной истории.

Очень грубая модель:

VRAM_total ≈ weights + KV_cache + activations + runtime_overhead

Вес модели почти фиксирован:

weights ≈ params * bytes_per_param

KV cache зависит от архитектуры:

KV_cache ∝ layers * kv_heads * head_dim * context_tokens * bytes_per_value

Из этого следуют практические выводы:

  • 4-bit веса не делают KV cache 4-bit автоматически.

  • 24 GB VRAM могут быть нормальны для 32B/70B в quantized setup на коротком context, но развалиться на 64K+.

  • max_model_len — не просто “разрешить больше токенов”, а зарезервировать потенциально огромный memory budget.

  • KV cache quantization может дать больше пользы на long-context, чем очередное ужатие весов.

Что сравнивать: холодный, прогретый и повторяющийся workload

Для каждого профиля делайте три режима.

Cold

Сервер только поднялся, cache пустой, всё впервые. Это худший realistic сценарий после deploy/restart.

Warm

Сделали warmup requests, kernels прогрелись, allocator стабилизировался. Это нормальный steady-state.

Repeated prefix

Одинаковый system prompt, одинаковые tool definitions, разные user inputs. Это тест на prefix caching.

system prompt: одинаковый
tools: одинаковые и в одном порядке
few-shot examples: одинаковые
user question: меняется

Если в начало prompt попадает timestamp, request id или случайный порядок JSON tool definitions — prefix caching может сломаться. Тогда вы будете платить prefill снова и снова.

Как не обмануть себя benchmark-флагами

Вот типичные ловушки:

request_rate=inf

Хорошо для максимального throughput, плохо для имитации реального сервиса. Для capacity planning лучше прогонять ступеньки:

RPS: 0.5 → 1 → 2 → 4 → 8

И смотреть, где p95 TTFT/TPOT пересекают SLO.

Слишком короткий prompt

Если вы тестируете только 512 токенов, вы ничего не знаете про RAG, coding agents и long-context.

Только batch=1

Это полезно для интерактивного single-user UX, но ничего не говорит о сервере под несколькими пользователями.

Только средние значения

Средний TTFT может быть 600 ms, а p99 — 18 секунд. Пользователь запомнит p99.

Без версии engine

vLLM/SGLang меняются быстро. Результат без версии engine и flags почти нельзя воспроизвести.

Минимальная таблица результата

Я бы публиковал результаты примерно так:

ProfileInputOutputConcurrencyTTFT p50/p95TPOT p50/p95Output tok/sPeak VRAM
short-chat5122561180/260 ms18/25 ms5518.2 GB
long-prompt3276851212400/3100 ms24/31 ms4122.7 GB
agent-loop1638410248900/1800 ms38/61 ms18023.4 GB
high-concurrency2048512321300/6500 ms45/120 ms41023.8 GB

Цифры здесь примерные, не результат конкретного теста. Важен формат: сразу видно, какой profile измерялся и почему один результат нельзя напрямую сравнивать с другим.

Какой benchmark нужен для RTX 3090/4090

Для 24 GB карт я бы начинал с такого набора:

1. 7B/8B model, BF16 или FP8
2. 14B/32B model, 4-bit/FP8
3. 30B/32B MoE или dense, если engine нормально поддерживает формат
4. 64K context stress test
5. agent-loop с prefix caching

Почему не начинать с 70B? Потому что вы сначала хотите понять форму кривой: как растёт TTFT, где упирается VRAM, как ведёт себя TPOT, что происходит при concurrency. Большая модель без baseline только запутает.

Для multi-GPU уже добавляются отдельные вопросы:

  • tensor parallel или pipeline parallel;

  • есть ли NVLink;

  • PCIe bottleneck;

  • NCCL настройки;

  • насколько стабилен long-context;

  • насколько сильно растёт TTFT при распределении.

Если вы идёте в multi-node, полезно сначала прочитать Распределенный запуск LLM на нескольких GPU с помощью Ray и vLLM, потому что там начинаются проблемы уже не benchmark-уровня, а сетевого и runtime-уровня.

Итоговый чеклист

Перед тем как верить локальному benchmark LLM, проверьте:

  • указаны модель, quantization, engine, версия engine и flags;

  • есть input/output token lengths;

  • есть context length и max_model_len;

  • есть concurrency и request rate;

  • есть TTFT p50/p95/p99;

  • есть TPOT или ITL p50/p95/p99;

  • есть E2E latency;

  • есть peak VRAM;

  • есть warmup;

  • есть cold/warm сравнение;

  • есть long-context профиль;

  • есть agent/repeated-prefix профиль;

  • есть сохранённый JSON результата;

  • есть raw logs или хотя бы команда запуска.

Если этого нет, перед вами не benchmark, а скриншот настроения.

Частые вопросы

Что важнее: TTFT или tokens/sec?

Для чата и agent UX важнее TTFT и p95/p99 latency. Для batch processing важнее throughput. Для production serving нужен баланс: throughput, который укладывается в SLO, то есть goodput.

Почему на коротком prompt всё быстро, а на длинном резко плохо?

Потому что длинный prompt увеличивает prefill и KV cache. TTFT растёт, VRAM забивается, decode начинает читать больше данных.

Можно ли сравнивать llama.cpp, Ollama, vLLM и SGLang одной таблицей?

Можно, но только если одинаковые model weights, quantization, prompt/output lengths, concurrency, context и hardware. Иначе это сравнение разных задач.

Почему включил prefix caching, а ускорения нет?

Скорее всего, нет стабильного общего префикса. Проверьте system prompt, порядок tool definitions, timestamps, random ids и формат chat template.

Почему GPU utilization низкий, но latency высокая?

Decode часто упирается не в compute, а в memory bandwidth и scheduler. Низкая GPU utilization не всегда означает, что “GPU простаивает”; иногда она ждёт память, batch, сеть или CPU-side orchestration.

Вывод

Бенчмарк локальной LLM в 2026 — это не “сколько токенов в секунду выдаёт моя RTX 4090”. Нормальный benchmark отвечает на более полезные вопросы:

  • сколько ждать первый токен;

  • насколько плавно идёт stream;

  • какой context реально помещается в VRAM;

  • что происходит при concurrency;

  • помогает ли prefix caching;

  • где падает goodput;

  • можно ли повторить результат через месяц.

Если вы снимаете только tok/s, вы видите одну тень системы. Если снимаете TTFT, TPOT, KV cache, context length и VRAM вместе — вы наконец начинаете видеть саму систему.


Заинтересовало? Больше практических разборов про LLM, инференс и AI-инфраструктуру — в моём Telegram-канале @fuckup_files.

Мой тг · про факапы@fuckup_files