Self-hosted LLM гальмує на 5 користувачах: чому
Один користувач працює нормально, п’ятеро чекають. Дізнайтеся, як batching, KV cache, prefill і queue depth визначають місткість LLM-сервера.
Чому self-hosted LLM сповільнюється, коли користувачів стає більше?
Self-hosted LLM починає працювати із затримками вже за 5 одночасних користувачів, оскільки сервер усе ще генерує відповіді по одній, а решта чотирьох запитів стоять у черзі. Документація Ollama прямо описує значення за замовчуванням: OLLAMA_NUM_PARALLEL — «максимальна кількість паралельних запитів, які кожна модель обробляє одночасно; за замовчуванням — 1». Нічого не зламано. Четверо з п’яти користувачів просто чекають своєї черги.
Зазвичай проблему вирішує не потужніший сервер. Потрібен serving engine, який обробляє багато запитів у межах одного forward pass моделі, а також достатній запас пам’яті для зберігання контексту всіх розмов під час обробки. Важливі обидві складові, але саме друга фактично визначає граничну кількість одночасних запитів.
Дві фази, через які проходить кожен запит
Prefill зчитує весь prompt одночасно й будує для нього attention cache. Усі токени prompt проходять через модель разом, тому prefill — це одне велике matrix multiply; його швидкість обмежує arithmetic throughput. Decode формує відповідь по одному токену. Для кожного токена потрібно знову зчитати з пам’яті всі ваги моделі, тоді як обсяг обчислень для одного токена дуже малий. Швидкість decode обмежує memory bandwidth.
Саме ця асиметрія пояснює, чому batching працює. Під час decode для одного користувача потрібно зчитати, наприклад, 5 GB ваг для кожного токена, а більшість arithmetic units простоює. Якщо додати другий запит, engine один раз зчитує ті самі 5 GB, а потім обчислює на їх основі два токени. Другий користувач майже не збільшує час обробки. Послідовне обслуговування запитів позбавляє цієї переваги.
Два показники визначають сприйняття роботи системи користувачем. TTFT (time to first token) — це час очікування в черзі плюс prefill. ITL (inter-token latency) — інтервал між токенами, що надходять у stream; його визначає decode. Повільний сервер зазвичай має проблеми з одним із цих показників, і способи їх усунення відрізняються.
Статичне пакетування змушує всіх чекати на найповільнішу відповідь
Статичне пакетування — це наївний варіант, який ви отримуєте, якщо самостійно групуєте запити в коді застосунку. Рушій збирає N запитів, обробляє їх разом і утримує кожен слот, доки не завершиться найдовша генерація в групі.
Один користувач, який запитав підсумок на 1,200 токенів, утримує в пакеті чотири однорядкові відповіді, оскільки пакет не звільняє жодного слота, доки не завершиться його найповільніший елемент.
Це призводить до двох проблем. Завершені послідовності продовжують займати слоти, хоча більше не виконують корисних обчислень. Через це ефективна пропускна здатність знижується, коли довжина виводу відрізняється, а в чатах довжина відповідей часто дуже різна. Запит, який надходить через один крок після формування пакета, чекає, доки весь пакет не завершить обробку, і лише потім починає prefill. Отже, його TTFT визначається чиїмось довгим текстом.
Неперервне пакетування приймає та видаляє запити на кожному токені
Неперервне пакетування планує виконання на рівні одного кроку декодування. Після кожного кроку планувальник видаляє послідовності, які щойно видали токен завершення, а потім приймає запити, що очікують, у вільні слоти. Відповідь, яка завершується на кроці 40, звільняє свій слот на кроці 40, а не після завершення пакета.
Це не екзотична функція. llama-server описує -cb, --cont-batching як «чи ввімкнути неперервне пакетування (також відоме як динамічне пакетування) (типово: увімкнено)», а vLLM побудовано саме на цій ідеї. Ollama також обслуговує паралельні запити. Типове налаштування просто обмежує їх кількість одним запитом. Саме тому багато хто робить висновок, що їхнє обладнання не підтримує конкурентне виконання, хоча насправді це забороняє конфігурація.
Опубліковані результати для неперервного пакетування зазвичай вимірюють на серверних картах, які мають і вільні обчислювальні ресурси, і десятки гігабайтів пам’яті для cache. Характер цих результатів можна перенести на ваш сервер. Їхній масштаб — ні. Причину пояснено в розділі про пам’ять нижче.
Попереднє опрацювання конкурує з декодуванням за ті самі обчислювальні ресурси
Коли надходить новий запит, а чотири відповіді вже передаються потоково, спочатку потрібно виконати prefill для його промпту. Prefill потребує значних обчислювальних ресурсів. Якщо планувальник виділить цьому prefill окремий крок, чотири користувачі, які отримують потокові відповіді, не отримають жодного токена протягом цього кроку. Для довгого промпту це помітна пауза в кожному відкритому вікні. Саме це мають на увазі, коли кажуть, що сервер починає працювати ривками щоразу, коли хтось інший натискає кнопку надсилання.
Chunked prefill розбиває довгий промпт на частини та додає кожну частину до того самого кроку, на якому виконується декодування поточних відповідей. У посібнику з налаштування vLLM цей компроміс описано безпосередньо: менші бюджети chunk "забезпечують кращий ITL, оскільки менше операцій prefill сповільнюють декодування", тоді як більші значення "забезпечують кращий час до першого токена (TTFT), оскільки за один пакет можна обробити більше токенів prefill". Ви обираєте, чий досвід важливіше захистити: користувача, який очікує на початок відповіді, чи користувачів, які спостерігають за потоковим виведенням тексту.
Довжина промпту визначає масштаб проблеми. Промпт на 6,000 токенів із відповіддю на 200 токенів означає 6,000 токенів роботи prefill проти 200 кроків декодування. Retrieval-augmented chat і довгі системні промпти часто створюють саме такі умови. Тому prefill перестає бути незначною складовою та стає операцією, на яку користувачі змушені чекати. Prefix caching допомагає, якщо довга частина повторюється: vLLM надає --enable-prefix-caching, який повторно використовує кеш спільного префікса промпту замість його повторного обчислення для кожного запиту.
Першим закінчується KV cache
Кожен токен у кожному активному діалозі залишає key vector і value vector у кожному шарі моделі. Це KV cache (key/value cache). Саме він дає змогу під час decode не обчислювати весь prompt заново для кожного нового токена. Розмір кешу на один токен визначається структурою моделі: 2 (один key і один value), помножене на кількість шарів, кількість key/value heads, head dimension і кількість bytes per value. Зчитайте ці значення з config.json моделі.
Обчисліть це один раз, і граничний обсяг перестане бути загадкою. Типова модель 8B із 36 шарами, 8 key/value heads і head dimension 128, яка зберігає кеш у 16-bit, потребує 2 36 8 128 2 bytes на токен. Це 147,456 bytes, або приблизно 144 KiB. Один діалог на 8,192 токени потребує приблизно 1.2 GB кешу. Для п’яти таких діалогів потрібно приблизно 6 GB додатково до ваг моделі. Саме це визначає, скільки користувачів може одночасно обслуговувати система.
Паралельність збільшує контекст. Інструменти прямо показують це в повідомленнях. У FAQ Ollama зазначено: "Parallel request processing for a given model results in increasing the context size by the number of parallel requests. For example, a 2K context with 4 parallel requests will result in an 8K context and additional memory allocation." Потрібний обсяг RAM масштабується як OLLAMA_NUM_PARALLEL, помножене на OLLAMA_CONTEXT_LENGTH. У llama-server контекст, який ви задаєте через -c, розподіляється між -np слотами. Тому збільшення кількості слотів саме по собі зменшує обсяг, доступний для кожного запиту. Читайте контекст на один слот у startup log, а не визначайте його приблизно.
vLLM натомість попередньо виділяє пам’ять. --gpu-memory-utilization (значення за замовчуванням — 0.92) — це "the fraction of GPU memory to be used for the model executor". Пам’ять, що залишилася після завантаження ваг, стає paged KV pool. Коли місце в цьому пулі закінчується, scheduler витісняє запит, а не завершує його з помилкою:
WARNING 05-09 00:49:33 scheduler.py:1057] Sequence group 0 is preempted by PreemptionMode.RECOMPUTE mode because there is not enough KV cache space.У V1 engine vLLM режим preemption за замовчуванням — RECOMPUTE. Тому витіснений запит втрачає свій кеш і після повторного допуску знову проходить prefill. Ця робота виконується двічі. У документації зазначено, що "preemption and recomputation can adversely affect end-to-end latency". Цей рядок журналу найкраще пояснює, чому один користувач чекав значно довше за інших, хоча середня затримка залишалася нормальною. Встановіть disable_log_stats=False для журналювання накопичувальної кількості або зчитайте лічильник preemption із метрик Prometheus, які надає vLLM.
Що змінюється за 2, 5 і 20 одночасних користувачів
Два користувачі. На GPU із вільною пам’яттю це майже непомітно, оскільки другий потік декодування додає дуже мало часу до першого. На VPS лише з CPU і 4–8 GB RAM це не безкоштовно: обидва потоки використовують ті самі кілька vCPU і ту саму пропускну здатність RAM, тому кожен користувач отримує приблизно половину токенів за секунду, а потреба в кеші подвоюється за значно меншого доступного обсягу.
П’ять користувачів. Саме тут стандартних налаштувань уже недостатньо, і спочатку проблема перетворюється на проблему черги. Якщо OLLAMA_NUM_PARALLEL дорівнює 1, четверо людей чекають на того, хто запросив довгу відповідь, а після настання їхньої черги кожен отримує звичайну швидкість. Якщо збільшити кількість паралельних запитів, проблема змінюється: п’ять слотів із контекстом по 8K — це кеш на 40K токенів, який потрібно розмістити. Якщо він не поміщається у VRAM, рушій вивантажує шари в системну RAM, а якщо кеш не поміщається в RAM, сервер починає використовувати swap, і кількість токенів за секунду різко падає.
Двадцять користувачів. Двадцять людей у chat UI зазвичай не означають двадцять одночасних запитів. Це найважливіше зрозуміти перед придбанням обладнання. Людина читає відповідь і думає 20–60 секунд між повідомленнями, тому більшу частину часу її сеанс простоює. Двадцять агентів або двадцять завдань із підсумовування документів — це двадцять реальних потоків без простою. Для них потрібна інша машина.
Чи є ваші користувачі одночасно активними, чи лише авторизованими?
Перш ніж щось розраховувати, визначте кількість запитів у процесі. Розрахунок простий: кількість запитів у процесі дорівнює кількості користувачів, помноженій на кількість секунд генерації одного повідомлення та поділеній на кількість секунд між повідомленнями.
- Спочатку виміряйте швидкість одного потоку у власній системі для prefill і decode. Не використовуйте показники чужої відеокарти: виміряйте кількість токенів за секунду у власній системі і використайте отримане значення.
- Оцініть робочий цикл. Для 20 користувачів чати з генерацією протягом 12 секунд на повідомлення та одним повідомленням кожні 90 секунд маємо 20 * 12 / 90, тобто приблизно 2.7 запиту в процесі.
- Встановіть кількість слотів трохи більшою за це значення, а потім перевірте її за пам’яттю: кількість слотів, помножена на контекст одного запиту, має вміщуватися в доступну кількість токенів кешу.
- Обмежте чергу, щоб надлишкові запити швидко завершувалися з помітною помилкою.
Доступну кількість токенів кешу визначають як вільну пам’ять після завантаження ваг, поділену на вартість одного токена з попереднього розділу. Відеокарта на 24 GB, на якій запущено модель 8B у 16-бітному форматі, використовує приблизно 16 GB для ваг і має близько 6 GB доступного кешу за стандартного рівня використання. Цього вистачає приблизно на п’ять діалогів із контекстом 8K. Щоб умістити більше, зменште контекст одного запиту або зберігайте кеш у 8-бітному форматі (llama-server займає --cache-type-k q8_0). Обидва способи підвищують паралельність, але вимагають компромісів. Перш ніж витрачати кошти на обладнання, ознайомтеся з реальним порівнянням, за якого GPU VPS стає вигіднішим за API-токени: коли GPU VPS окупається порівняно з API-токенами.
Коли стандартних параметрів Ollama вже недостатньо
Збільшуйте кількість паралельних запитів через unit сервісу, оскільки експорт змінної в shell не буде доступний демону, яким керує systemd.
sudo systemctl edit ollama.service[Service]
Environment="OLLAMA_NUM_PARALLEL=4"
Environment="OLLAMA_CONTEXT_LENGTH=8192"
Environment="OLLAMA_MAX_QUEUE=64"sudo systemctl daemon-reload
sudo systemctl restart ollama
systemctl show ollama --property=Environment
ollama pssystemctl show має вивести три щойно встановлені змінні. Якщо цього не сталося, drop-in не зберігся, і подальші дії не матимуть результату. ollama ps потім показує завантажену модель із розміром, більшим за самі ваги, оскільки чотири слоти по 8,192 токенів резервують ще 32,768 токенів кешу. Якщо в колонці PROCESSOR указано частину моделі на CPU, хоча ви очікували повного розміщення на GPU, це означає, що для кешу запитано більше пам’яті, ніж залишалося на карті. Зменште одне з двох значень.
Стандартне значення черги потребує окремої перевірки. Ollama ставить у чергу до OLLAMA_MAX_QUEUE запитів, а «стандартне значення — 512». Після цього вона повертає «помилку 503 із повідомленням, що сервер перевантажений». Черга глибиною 512 на сервері, який одночасно обробляє чотири запити, створює невиконувану обіцянку, оскільки клієнт на позиції 300 дочекається тайм-ауту задовго до початку обробки. Коротка черга повертає помилку, яку застосунок може повторити або показати користувачу. Це краще за індикатор завантаження, який не завершується.
Перевірте налаштування на практиці. Надішліть два запити одночасно з двох терміналів і спостерігайте за обома. Якщо другий запит не дає результату до завершення першого, параметр паралельного виконання не застосувався.
Коли повноцінний serving engine починає виправдовувати витрати
vLLM виправдовує додаткове налаштування, якщо у вас є GPU із запасом ресурсів і одночасно обробляється більше приблизно чотирьох запитів. Його планувальник працює на рівні токенів, кеш розподіляється сторінками, тому вільні фрагменти використовуються повторно, а вільна VRAM перетворюється на паралельність замість простою. Станом на August 2026 документовані встановлення та запуск виконуються двома командами:
uv pip install vllm --torch-backend=auto
vllm serve Qwen/Qwen2.5-1.5B-Instructcurl http://localhost:8000/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "Qwen/Qwen2.5-1.5B-Instruct",
"messages": [{"role": "user", "content": "Say hello."}]
}'Відповідь, що містить масив choices, означає, що сервер запущений, а модель завантажена. Під навантаженням важливі два параметри: --max-num-seqs — «максимальна кількість послідовностей, які обробляються за одну ітерацію», і --max-num-batched-tokens — «максимальна кількість токенів, які можна обробити за одну ітерацію». Перший обмежує паралельність. Другий визначає бюджет chunked prefill, описаний вище.
Якщо одночасно обробляється менше приблизно чотирьох запитів або сервер не має підтримуваного GPU, vLLM додає складності, але майже не дає переваг. Для нього потрібна відеокарта класу CUDA, і під час запуску він резервує більшу частину пам’яті. Для VPS із 4–8 GB це невдалий компроміс. У такому випадку краще використати меншу модель із коротшим контекстом і чергою, якою ви керуєте самостійно. У матеріалі як Ollama і vLLM відрізняються як serving engines цей вибір розглянуто докладно, а в матеріалі запуск Qwen 3 8B на VPS показано, які ресурси потребує модель середнього розміру ще до додавання першого додаткового користувача.
Компроміс, який приховує загальноприйняте уявлення
Continuous batching підвищує загальну пропускну здатність і зазвичай також покращує медіанну затримку, оскільки запит із черги починає виконуватися раніше. Tail latency змінюється в протилежний бік, але про це рідко згадують.
Кожна додаткова sequence у кроці додає певний обсяг роботи, тому ITL зростає для всіх, коли batch заповнюється. Pre-fill нового запиту займає частину кроку, яка інакше була б доступна користувачам зі streaming. За дефіциту cache scheduler виконує preemption, через що частково згенерований запит повертається на початок pre-fill.
Chat UI показує tail latency, а не середні значення. Потік, який переривається на дві секунди посеред речення, сприймається як несправний, навіть якщо загальний час до завершення хороший. Вимірюйте p95 TTFT і p95 ITL за навантаження, яке очікуєте в роботі, а середню кількість tokens per second розглядайте як показник пропускної здатності, а не як опис користувацького досвіду.
Практичне налаштування випливає саме з цього. Обмежте concurrency трохи нижче рівня, який дає змогу memory, щоб engine ніколи не був змушений виконувати preemption. Коротка передбачувана черга краща за глибокий batch, який постійно thrash-ить, оскільки користувач, який чекає чотири секунди, а потім отримує плавний stream, задоволеніший за користувача, чий запит запускається одразу, але двічі зупиняється.
Що перевірити, якщо сервіс працює повільно
Для кожного користувача швидкість нормальна, але очікування тривале. Це черга, а не проблема швидкодії. Спочатку перевірте параметр паралельності. Модель працює правильно, обробляючи по одному запиту за раз.
Ollama повертає HTTP 503. Черга заповнена. Або сервер справді працює на межі можливостей, або OLLAMA_MAX_QUEUE навмисно встановлено на низьке значення для відхилення надлишкового навантаження. Саме для цього цей параметр і використовується.
Кількість токенів за секунду різко падає під навантаженням на сервері з CPU. У цей момент виконайте vmstat 1. Ненульові значення у стовпцях si і so означають, що система використовує swap. Тому ваги моделі зчитуються з диска для кожного токена. Зміна конфігурації не усуне цю проблему. Зменште розмір моделі або кількість слотів.
Один із десяти користувачів очікує значно довше за інших. Знайдіть у журналі vLLM записи з preempted. Зазвичай причина полягає у витісненні та повторному обчисленні. Це означає, що кеш переповнюється для дозволеної довжини контексту.
TTFT має високе значення, навіть коли сервер не завантажений. Це проблема prefill, а не паралельності. Обробка довгих запитів потребує часу до появи першого токена. Тому спочатку перевірте розмір запиту та prefix caching, а вже потім апаратне забезпечення.
FAQ
Чому мій self-hosted LLM сповільнюється, коли ним користується друга людина?
Найчастіше він узагалі не сповільнюється. Запит стає в чергу. Ollama постачається з параметром OLLAMA_NUM_PARALLEL, що має значення 1, тому другий запит чекає, доки перший не згенерує свій останній токен. Розрізнити ці випадки можна, вимірявши швидкість генерації потоку одного користувача, поки інший чекає: якщо після початку генерації кількість токенів за секунду нормальна, проблема в черзі, і збільшення кількості паралельних запитів її усуне. Якщо обидва потоки працюють удвічі повільніше, пам’ять справді використовується спільно, і це апаратне обмеження.
Скільки одночасних користувачів може обслуговувати одна невелика GPU?
Рахуйте не користувачів, а пам’ять. Спочатку потрібна пам’ять для ваг моделі, потім для KV cache: він потребує 2 × кількість шарів × кількість key/value heads × розмірність head × кількість байтів на токен для кожної активної розмови. Типова модель 8B із 36 шарами, 8 key/value heads і розмірністю head 128 потребує приблизно 144 KiB на токен у 16-бітному форматі, тому розмова на 8,192 токени потребує близько 1.2 GB. На карті обсягом 24 GB, де ця модель зберігається у 16-бітному форматі, для cache залишається близько 6 GB. Цього вистачає приблизно на п’ять розмов із повним контекстом або на більшу кількість розмов, якщо скоротити контекст.
Чи робить continuous batching відповідь для кожного користувача повільнішою?
Медіанна затримка зазвичай зменшується, оскільки запитам більше не потрібно чекати завершення всього batch. Затримка у хвості розподілу зростає. Кожна додаткова послідовність додає роботу на кожному кроці декодування, новий запит забирає частину кроку в користувачів, які отримують потік, а витіснений запит потрібно попередньо обробити двічі. Вимірюйте p95 затримки між токенами, а не середнє значення, оскільки у вікні чату паузи помітні, а усереднення їх приховує.
Чи слід збільшити OLLAMA_NUM_PARALLEL або перейти на vLLM?
Спочатку збільште кількість паралельних запитів. Це безкоштовно й потребує лише одного drop-in файлу. Так ви усунете поширену ситуацію, коли четверо людей стоять у черзі за однією довгою відповіддю. Обмеженням є пам’ять: паралельні запити збільшують обсяг контексту, який потрібно утримувати, тому стежте, чи не переміщуються шари до CPU. Переходьте на vLLM, коли маєте GPU із вільною VRAM і в роботі справді перебуває понад приблизно чотири запити. Саме тоді paged cache і планування для кожного токена дають більше переваг, ніж витрат.
Чи допоможуть додаткові ядра CPU повільному LLM-серверу?
Не для тієї частини роботи, яку користувачі помічають найбільше. Під час decode модель повністю читається з пам’яті для кожного токена, тому швидкість обмежує пропускна здатність RAM. Додаткові ядра перестають допомагати, щойно пропускну здатність вичерпано. Prefill масштабується за кількістю ядер, тому більша їх кількість скорочує час до першого токена для довгих prompt. На VPS із 4–8 GB основним обмеженням зазвичай є обсяг пам’яті. Практичне рішення — менша модель або коротший контекст, а не збільшення кількості vCPU.