Як виміряти токени за секунду для локальної LLM
Перевірте реальну швидкість локальної LLM: виконайте sweep за concurrency, знайдіть мінімальний throughput для окупності GPU та порівняйте його з оплатою API.
Чому кількість токенів за секунду визначає, чи окупається GPU
Кількість токенів за секунду — це швидкість, з якою ваш сервер генерує текст відповіді. Саме цей показник визначає, чи дешевше орендувати GPU, ніж оплачувати API за токени. Вартість GPU-сервера нараховується погодинно, незалежно від того, використовується він чи простоює. Hosted API оплачується за кількістю токенів. Тому GPU вигідніший лише тоді, коли протягом більшості оплачених годин ви підтримуєте достатньо високу швидкість генерації.
Отже, потрібне вимірювання, а не значення, знайдене десь в іншому джерелі. На цій сторінці визначено чотири показники, які варто записувати. Також наведено команди для їх отримання та формули, за допомогою яких ці дані перетворюються на рішення.
Чому опублікований показник токенів за секунду не є вашим показником
DigitalOcean опублікував у липні 2026 року показники пропускної здатності для однієї NVIDIA H200, яка запускає llama3.3-70b-instruct у форматі FP8 (8-бітова плаваюча кома) через vLLM. Ці показники корисні, але вони не є вашими.
The data behind this chart
[
{
"config": "H200, one stream",
"tok_s": "47"
},
{
"config": "H100, saturated",
"tok_s": "236"
},
{
"config": "H200, saturated",
"tok_s": "2,036"
},
{
"config": "H200, saturated, in+out",
"tok_s": "4,071.6"
}
]Кожен рядок вище наведено з тієї сторінки. Два рядки є нижньою межею зазначеного там діапазону, тому сприймайте їх саме як мінімальні значення. Ми не вимірювали жоден показник у цій таблиці.
Почнімо з останніх двох рядків. Заголовковий показник становить 4,071.6 токенів/с, а швидкість лише для виведення — 2,036 токенів/с. Заголовковий показник враховує вхідні та вихідні токени разом. У цьому тесті використовували 1,024 вхідних токени та 1,024 вихідних токени, тому на виведення припадає майже рівно половина заголовкового показника. Це розділення важливе, оскільки ви платите саме за вихідні токени, а їх обробка повільніша. Під час prefill модель обробляє всі вхідні токени за один прохід. Під час decode вона генерує по одному токену за раз. Заголовковий показник загальної пропускної здатності усереднює дешеву та дорогу операцію.
Тепер розгляньмо перший рядок. Та сама H200, яка обслуговує один запит за раз, забезпечує 47 токенів/с. Отже, показник для насиченого режиму більш ніж у сорок разів вищий на тому самому обладнанні. Така різниця виникає тому, що під час одного кроку decode GPU більшу частину часу очікує на дані в пам’яті, а паралельні запити заповнюють цей простій. Другий рядок, 236 токенів/с, показує результат для однієї H100 на тій самій моделі. Її обмежує KV cache (кеш ключів і значень, тобто пам’ять для кожного запиту, яку обслуговуваний діалог зберігає на карті). Карта на 80 GB може одночасно обслуговувати менше запитів для моделі 70B, тому насичення настає за нижчого показника.
Змініть модель або співвідношення вхідних і вихідних токенів — і кожне число вище зміниться. Опубліковані показники визначають ваші очікування, але не бюджет. Це те саме правило, яке застосовується під час чесного тестування VPS для диска та мережі.
Чотири важливі показники
- Час до першого токена, TTFT. Затримка між надсиланням запиту та отриманням першого вихідного токена. Вона складається з часу prefill і часу очікування в черзі. Користувач безпосередньо відчуває цей показник.
- Вихідні токени за секунду на один потік. Швидкість формування однієї відповіді після її початку. За швидкості приблизно понад 20 tok/s відповідь уже формується швидше, ніж більшість людей читає, тому додаткова швидкість тут майже не дає переваг.
- Сумарна пропускна здатність за насиченого навантаження. Сума показників для всіх одночасних потоків, коли сервер повністю завантажений. Це показник пропускної здатності, який безпосередньо визначає витрати на GPU.
- p50 і p99 TTFT за одночасного навантаження. p50 — це значення для запиту посередині розподілу. p99 — значення, нижче за яке вкладаються 99 зі 100 запитів. Черга спочатку завжди проявляється в p99.
Перші два показники покращуються, коли сервер майже не завантажений. Третій покращується за високого навантаження. Вони суперечать один одному, тому жодне окреме число не описує сервер для обслуговування запитів.
Зафіксуйте довжину вхідних і вихідних даних перед вимірюванням
Пропускна здатність залежить від характеристик трафіку. Запит на 4,000 токенів із відповіддю на 50 токенів — це робота з переважанням prefill. Запит на 200 токенів із відповіддю на 2,000 токенів — це робота з переважанням decode. Той самий сервер показує дуже різну кількість токенів за секунду для цих двох варіантів. Тому виберіть одне співвідношення, записуйте його поруч із кожним виміряним значенням і не порівнюйте результати для різних співвідношень. Співвідношення 1,024 вхідних і 1,024 вихідних токенів є практичним значенням за замовчуванням, оскільки кілька постачальників публікують показники саме для нього. Якщо ви знаєте характеристики реального трафіку, використовуйте їх.
Також примусово задайте довжину вихідних даних. Якщо модель зупиняється після 60 токенів через stop token, коротший запуск виглядатиме швидшим, оскільки TTFT становитиме більшу його частину. Прапорець --ignore-eos у клієнті бенчмаркінгу vLLM змушує кожен запит генерувати точно вказану кількість токенів, тому результати двох запусків залишаються порівнюваними. Вибір моделі впливає на ці показники сильніше, ніж будь-який прапорець: у матеріалі розміщення моделі Qwen 3 на одному VPS GPU розглянуто аспект використання пам’яті.
Спочатку виміряйте один потік
Почніть із найпростішого випадку. Це перевірка коректності та верхня межа продуктивності. Ollama виводить власні часові показники.
ollama run llama3.1:8b --verbose "Write 200 words about disk scheduling."Потрібний рядок — eval rate. Це кількість вихідних токенів за секунду. prompt eval rate — швидкість prefill, а load duration — час завантаження моделі у VRAM. Під час першого виклику після холодного запуску load duration має велике значення, тому total duration вводить в оману. Виконайте команду двічі та перегляньте другий результат. За замовчуванням Ollama вивантажує неактивну модель через п’ять хвилин, тому довга пауза між запусками знову переводить систему в холодний стан.
Ті самі поля можна отримати через API, що зручніше для написання скриптів.
curl -s http://127.0.0.1:11434/api/generate -d '{
"model": "llama3.1:8b",
"prompt": "Write 200 words about disk scheduling.",
"stream": false
}' | jq '{eval_count, eval_duration,
tok_s: (.eval_count / (.eval_duration / 1000000000))}'eval_duration вимірюється в наносекундах, тому ділення на 1,000,000,000 дає секунди. Саме такий поділ рекомендує документація API Ollama для обчислення кількості токенів за секунду. Якщо сервер ще не запущений, у матеріалі Як розгорнути LLM з Ollama на VPS описано встановлення та unit systemd.
Для вимірювання TTFT потрібен потоковий запит, а curl може виконати це вимірювання.
curl -s -o /dev/null -N \
-w 'ttfb=%{time_starttransfer}s total=%{time_total}s\n' \
http://127.0.0.1:8000/v1/chat/completions \
-H 'Content-Type: application/json' \
-d '{"model":"Qwen/Qwen3-8B",
"messages":[{"role":"user","content":"Write 200 words about disk scheduling."}],
"stream":true,"max_tokens":256}'time_starttransfer — момент надходження першого байта тіла відповіді. У потоковому chat completion цей байт належить до першої server-sent event, яка містить або перший токен вмісту, або delta лише з роллю, надіслану безпосередньо перед ним. Тому сприймайте це значення як TTFT із похибкою приблизно в одну подію. Його достатньо для порівняння двох запусків на одному сервері.
Показники для одного потоку двічі створюють завищене враження про можливості сервера. TTFT буде найкращим, оскільки перед вашим запитом немає черги. Швидкість одного потоку також буде найкращою, оскільки вся відеокарта обслуговує один запит. Жоден із цих показників не показує, яке навантаження може витримати сервер.
Як виконати sweep навантаження зі зростанням concurrency?
Sweep запускає одне фіксоване навантаження за поступового збільшення concurrency та записує результат на кожному кроці. vLLM постачається з клієнтом для цього завдання. Він працює через OpenAI API, тому також сумісний з Ollama та іншими OpenAI-compatible системами.
vllm bench serve \
--backend openai-chat \
--base-url http://127.0.0.1:8000 \
--endpoint /v1/chat/completions \
--model Qwen/Qwen3-8B \
--dataset-name random \
--random-input-len 1024 \
--random-output-len 1024 \
--ignore-eos \
--num-prompts 160 \
--max-concurrency 16 \
--percentile-metrics ttft,tpot,itl,e2el \
--metric-percentiles 50,99--max-concurrency обмежує кількість запитів у роботі та є змінною, яку ви послідовно збільшуєте. --num-prompts — це загальна кількість надісланих запитів, тому встановіть її приблизно на рівні десятикратного значення concurrency, щоб отримати стабільне середнє. У зведенні виводяться Output token throughput (tok/s): і Total token throughput (tok/s):, а потім Mean TTFT (ms):, Median TTFT (ms): і P99 TTFT (ms): під заголовком Time to First Token.
У цьому виводі немає швидкості для окремого потоку, але її легко обчислити. Mean TPOT (ms): — це середній час на output token після першого токена, тому 25 ms на токен відповідають 40 токенам за секунду для одного потоку. Якщо поділити output throughput на concurrency, ви отримаєте те саме значення.
Потім виконайте цикл і збережіть результати кожного запуску.
for C in 1 4 16 32 64 128; do
vllm bench serve \
--backend openai-chat \
--base-url http://127.0.0.1:8000 \
--endpoint /v1/chat/completions \
--model Qwen/Qwen3-8B \
--dataset-name random \
--random-input-len 1024 \
--random-output-len 1024 \
--ignore-eos \
--num-prompts $(( C * 10 )) \
--max-concurrency "$C" \
--percentile-metrics ttft,tpot,itl,e2el \
--metric-percentiles 50,99 \
--save-result --result-filename "sweep-c$C.json"
doneЧитання збережених JSON-файлів
Кожен запуск записує один файл, тому одразу отримайте потрібні поля з усіх файлів.
for f in sweep-c*.json; do
jq -r --arg f "$f" \
'[$f, .output_throughput, .total_token_throughput,
.median_ttft_ms, .p99_ttft_ms] | @tsv' "$f"
doneoutput_throughput — це кількість output tokens за секунду. total_token_throughput додає input tokens, тому за співвідношення 1:1 це значення буде близьким до подвоєного. p99_ttft_ms існує лише тому, що --metric-percentiles містив 99; якщо запросити percentile, який не було запитано, jq виведе null.
Що насправді показує sweep за рівнем concurrency?
The data behind this chart
[
{
"label": "1 stream",
"per_stream_tok_s": 92,
"total_tok_s": 92,
"ttft_p50_ms": 48,
"ttft_p99_ms": 61
},
{
"label": "4 streams",
"per_stream_tok_s": 88,
"total_tok_s": 352,
"ttft_p50_ms": 71,
"ttft_p99_ms": 96
},
{
"label": "16 streams",
"per_stream_tok_s": 71,
"total_tok_s": 1136,
"ttft_p50_ms": 152,
"ttft_p99_ms": 244
},
{
"label": "32 streams",
"per_stream_tok_s": 54,
"total_tok_s": 1728,
"ttft_p50_ms": 287,
"ttft_p99_ms": 498
},
{
"label": "64 streams",
"per_stream_tok_s": 34,
"total_tok_s": 2176,
"ttft_p50_ms": 611,
"ttft_p99_ms": 1240
},
{
"label": "128 streams",
"per_stream_tok_s": 18,
"total_tok_s": 2304,
"ttft_p50_ms": 1490,
"ttft_p99_ms": 3820
}
]Ці 6 рядків ілюструють форму результатів sweep на невеликому орендованому GPU-сервері за реалістичних порядків величин. Це не вимірювання вашого сервера і не дані постачальника. Запустіть наведений вище цикл і замініть їх власними значеннями.
Аналізуйте форму кривої, оскільки саме ця форма узагальнюється. За одного потоку весь сервер обробляє 92 токенів за секунду, а p99 TTFT становить 61 мс. За 128 streams загальна швидкість досягає 2304 токенів за секунду, що у двадцять п’ять разів більше, тоді як швидкість кожного окремого потоку знижується до 18 токенів за секунду, а p99 TTFT зростає до 3820 мс. Загальна пропускна здатність зростає, оскільки batching перетворює простої під час очікування пам’яті на корисну роботу. Швидкість окремого потоку знижується, оскільки ті самі обчислювальні ресурси тепер розподіляються між потоками.
Останнє подвоєння показове. Перехід від 64 до 128 потоків збільшує загальний показник менш ніж на шість відсотків, тоді як p99 TTFT приблизно потроюється. Це означає, що KV cache заповнений, а запити стають у чергу, замість того щоб виконуватися. Оптимальна робоча точка розташована раніше: за 32 потоків сервер усе ще повертає 1728 токенів за секунду, тобто 75 відсотків пікового значення, зі швидкістю 54 токенів за секунду на потік і p99 TTFT 498 мс. Вказуйте цю точку як свою пропускну здатність. Пікове значення кривої не можна використовувати для обслуговування користувачів.
Ollama і vLLM вимірюють не одне й те саме
Запустіть цей прогін проти сервера Ollama із типовими налаштуваннями, і загальний результат майже не зміниться. OLLAMA_NUM_PARALLEL за замовчуванням дорівнює 1, тому один запит виконується, а решта очікують. Саме черга підвищує p99 TTFT, тоді як загальний обсяг виводу залишається сталим. Збільште це значення до початку вимірювань.
sudo systemctl edit ollama.service[Service]
Environment="OLLAMA_NUM_PARALLEL=8"Перезапустіть сервер із sudo systemctl restart ollama, а потім переконайтеся, що модель і надалі вміщується в пам’ять. Кожен паралельний слот отримує власну частину вікна контексту, тому в документації Ollama зазначено: контекст 2K для 4 паралельних запитів розподіляється як 8K. Якщо збільшити кількість слотів надмірно, модель вийде за межі VRAM. Перевірте ollama ps: якщо в стовпці PROCESSOR відображається значення на кшталт 48%/52% CPU/GPU, частина моделі перебуває в пам’яті CPU. Після цього пропускна здатність зменшуватиметься зі збільшенням паралелізму, а не зростатиме. Після вичерпання паралельних слотів запити стають у чергу до OLLAMA_MAX_QUEUE, що за замовчуванням дорівнює 512. Після досягнення цього ліміту сервер повертає 503.
vLLM використовує безперервне пакетування: нові запити додаються до поточного пакета, щойно звільняються слоти. Тому його показники продовжують зростати, доки не вичерпається KV cache. Ollama оптимізовано для однієї моделі на одному комп’ютері з мінімальними витратами на налаштування. Тому ці два рушії дають різні результати для того самого прогону. Саме це є основною темою матеріалу Порівняння Ollama і vLLM як рушіїв обслуговування. Для кожного показника зафіксуйте рушій і його версію.
П’ять способів виміряти не те
- Клієнт розташований далеко. Бенчмаркінг із ноутбука через інтернет додає час проходження мережевого шляху до кожного TTFT, тому ви фактично вимірюєте домашнє підключення. Запускайте клієнт у тому самому регіоні, що й сервер.
- Модель не була прогріта. Перший запит витрачає час на завантаження ваг, а у vLLM також може витрачатися час на захоплення графа. Надішліть пакет для прогрівання та відкиньте результат.
- Вам відповів кеш префіксів. vLLM типово вмикає автоматичне кешування префіксів, тому повторне надсилання того самого запиту вимірює кеш, а не prefill, і TTFT зменшується до частки фактичного значення.
--dataset-name randomуникає цього, оскільки кожен запит відрізняється. Щоб переконатися в цьому, запустіть сервер із--no-enable-prefix-caching. - Відповіді були короткими. Для відповідей на 32 токени TTFT домінує в кожному запиті, тому ваш показник токенів за секунду фактично описує prefill. Використовуйте
--ignore-eosіз реалістичною довжиною відповіді. - Ви вказали рівень конкурентності 1. Це найсприятливіше число у звіті, і воно не має стосунку до вартості.
Перетворіть виміряне значення на рішення
Візьміть пропускну здатність за насиченого навантаження з результатів sweep, а не швидкість для одного потоку, і порівняйте її з ціною за токен. Точка беззбитковості визначається одним діленням:
break_even_tok_s = (price_per_hour / price_per_million_output_tokens) * 1000000 / 3600Розглянемо це на прикладі цін DigitalOcean за July 2026. Їхній dedicated inference endpoint H200 коштував $4.47 за годину, а еквівалентний serverless варіант — $0.65 за мільйон токенів. Отже, 4.47 поділити на 0.65 — це 6.88 мільйона токенів за годину, а після ділення на 3,600 секунд виходить приблизно 1,910 вихідних токенів за секунду. Ціни належать їм. Ділення виконали ми.
Вирішальним тут є слово стабільна. Досягнення швидкості 1,910 токенів за секунду за насиченого навантаження протягом двох годин на день не означає стабільну швидкість 1,910 токенів за секунду, оскільки платити потрібно також за решту двадцяти двох годин. Власна точка перетину DigitalOcean для дешевшого GPU Droplet за $3.44 за годину відповідає стабільному середньому використанню 72.2 відсотка. За нижчого використання вигіднішою є ціна за токен. Саме години простою GPU, а не низька швидкість обробки токенів, зазвичай роблять self-hosting невигідним.
Отже, рішення залежить від двох показників. Результати sweep визначають верхню межу. Характер вашого трафіку визначає частку цієї межі, яку ви фактично використовуєте. Перемножте їх, а потім зіставте результат із точкою беззбитковості між GPU VPS і API з оплатою за токени та визначте відповідь для свого обсягу.
FAQ
Якою є прийнятна швидкість у токенах за секунду для self-hosted LLM?
Є дві відповіді, оскільки цей показник має два призначення. Для однієї людини, яка читає результат, будь-яка швидкість понад приблизно 20 вихідних токенів за секунду в одному потоці вже перевищує швидкість читання, тому більша швидкість не допомагає. Для оцінювання вартості важлива насичена загальна пропускна здатність за вихідними токенами. Прийнятним є значення, за якого досягається беззбитковість. За ціни $0.65 за мільйон токенів і вартості сервера $4.47 за годину цей поріг у цінах за July 2026 становить приблизно 1,910 вихідних токенів за секунду в стабільному режимі. Один потік на великій моделі ніколи його не досягає, тому й потрібне пакетне оброблення.
Чому пропускна здатність Ollama не змінюється, коли я додаю паралельні запити?
OLLAMA_NUM_PARALLEL за замовчуванням дорівнює 1, тому сервер обробляє один запит за раз для кожної моделі, а решту ставить у чергу — до OLLAMA_MAX_QUEUE (за замовчуванням 512), після чого повертає 503. Загальна швидкість генерації не змінюється, тоді як p99 TTFT зростає. Це ознака черги, а не високого навантаження на GPU. Задайте змінну в drop-in для systemd і перезапустіть сервіс, а потім перевірте ollama ps, оскільки кожен паралельний слот збільшує виділений контекст і може перемістити частину моделі на CPU.
Чи слід вимірювати час до першого токена або кількість токенів за секунду?
І те, і інше, оскільки зі зростанням навантаження ці показники змінюються в протилежних напрямках. TTFT визначає сприйняття швидкості користувачем, а насичена пропускна здатність за вихідними токенами визначає суму у вашому рахунку. Записуйте p50 і p99 TTFT на кожному кроці збільшення паралельності, а потім виберіть найбільший рівень паралельності, за якого p99 TTFT усе ще є прийнятним для вас. Вважайте пропускну здатність на цьому рівні вашою пропускною здатністю, а не максимальне значення з верхньої точки кривої.
Чи завжди більша кількість токенів за секунду означає нижчу вартість токена?
Ні. Вартість токена — це погодинна ціна, поділена на кількість токенів, які сервер фактично згенерував за цю годину. Тому швидкий сервер, який більшу частину дня простоює, усе одно має високу вартість токена. Вирішальним є рівень використання, а не пікова швидкість. Також перевіряйте одиниці вимірювання: зазначена загальна пропускна здатність за токенами враховує вхідні токени. За співвідношення вхідних і вихідних токенів 1:1 вона приблизно вдвічі перевищує швидкість вихідних токенів, за якою вам виставляють рахунок.