Почему self-hosted LLM тормозит при 5 пользователях
Ваш сервер обрабатывает запросы последовательно из-за параметра num_parallel. Узнайте, как batching, KV cache и глубина очереди влияют на реальную пропускную способность модели.
Почему self-hosted LLM замедляется при увеличении числа пользователей?
Self-hosted LLM начинает «тормозить» при 5 одновременных пользователях, так как сервер генерирует только один ответ за раз, а остальные четыре запроса находятся в очереди. Документация Ollama прямо указывает на значение по умолчанию: OLLAMA_NUM_PARALLEL — это «максимальное количество параллельных запросов, которые модель будет обрабатывать одновременно, по умолчанию 1». Никакой поломки нет. Четверо из пяти ваших пользователей просто ждут своей очереди.
Решение редко заключается в покупке более мощного сервера. Вам нужен движок обслуживания (serving engine), который обрабатывает множество запросов через модель за один проход (forward pass), а также достаточный объем свободной памяти для хранения контекста диалога каждого пользователя во время обработки. Оба этих фактора важны, но именно второй определяет реальный предел производительности вашей системы.
Две фазы обработки каждого запроса
Prefill считывает весь промпт целиком и формирует для него кэш внимания (attention cache). Каждый токен промпта проходит через модель одновременно, поэтому prefill представляет собой одну большую операцию матричного умножения, ограниченную арифметической производительностью системы. Затем этап decode генерирует ответ по одному токену за раз. Для каждого токена необходимо снова считывать полные веса модели из памяти, при этом объем вычислений для одного токена крайне мал. Этап decode ограничен пропускной способностью памяти.
Эта асимметрия — основная причина эффективности пакетной обработки (batching). При декодировании для одного пользователя система считывает, например, 5 GB весов на токен, оставляя большую часть вычислительных блоков простаивающими. Если добавить второй запрос, движок считывает те же 5 GB один раз, а затем вычисляет два токена. Второй пользователь практически не увеличивает время обработки. Последовательная обработка запросов строго друг за другом сводит это преимущество на нет.
Два показателя определяют пользовательский опыт. TTFT (time to first token) — это время ожидания в очереди плюс время prefill. ITL (inter-token latency) — это интервал между потоковыми токенами, который определяется этапом decode. Медленная работа сервера обычно вызвана проблемами на одном из этих этапов, и способы их устранения различаются.
Статическая пакетная обработка заставляет всех ждать самого медленного ответа
Статическая пакетная обработка — это упрощенный подход, который вы получаете при самостоятельной группировке запросов в коде приложения. Движок собирает N запросов, выполняет их вместе и удерживает каждый слот до тех пор, пока не завершится генерация самого длинного ответа в группе.
Один пользователь, запрашивающий резюме на 1200 токенов, блокирует четыре однострочных ответа в пакете, так как пакет не освобождает ни одного слота, пока не завершится самый медленный процесс.
Это влечет за собой две проблемы. Завершенные последовательности продолжают занимать слоты, не выполняя полезных вычислений, поэтому эффективная пропускная способность падает при изменении длины вывода, а длина ответов в чате варьируется значительно. Запрос, поступивший через один шаг после формирования пакета, ожидает завершения всего пакета, прежде чем начнется этап prefill. Это означает, что время до получения первого токена (TTFT) для этого запроса определяется объемом работы другого пользователя.
Continuous batching принимает и завершает запросы на каждом токене
Continuous batching планирует выполнение на уровне отдельного шага декодирования. После каждого шага планировщик исключает последовательности, которые только что сгенерировали стоп-токен, и принимает ожидающие запросы в освободившиеся слоты. Ответ, завершающийся на шаге 40, освобождает свой слот на шаге 40, а не в конце всего пакета.
Это не экзотическая функция. llama-server описывает -cb, --cont-batching как «включение continuous batching (также известного как dynamic batching) (по умолчанию: включено)», и vLLM построена вокруг этой концепции. Ollama также обрабатывает параллельные запросы. Значение по умолчанию просто ограничивает количество запросов единицей, поэтому многие пользователи ошибочно полагают, что их оборудование не поддерживает параллелизм, хотя на самом деле это ограничение конфигурации.
Опубликованные результаты работы continuous batching обычно измеряются на серверных картах, у которых есть как избыток вычислительной мощности, так и десятки гигабайт памяти для кэша. Характер этих результатов применим к вашему оборудованию. Однако их масштаб — нет, и причина этого описана в разделе о памяти ниже.
Prefill конкурирует с decode за вычислительные ресурсы
Когда поступает новый запрос в момент, когда четыре ответа уже передаются в потоковом режиме, его prompt должен быть сначала обработан (prefill), а prefill требует значительных вычислительных мощностей. Если планировщик выделяет для этого prefill отдельный шаг, четыре пользователя, получающие ответы, не получают ни одного токена в этот момент. При длинном prompt это выглядит как заметная пауза в каждом открытом окне. Именно это «заикание» имеют в виду пользователи, когда говорят, что сервер «спотыкается» каждый раз, когда кто-то другой нажимает кнопку отправки.
Chunked prefill разбивает длинный prompt на части и смешивает каждую часть с тем же шагом, что и выполняемые операции decode. Руководство по настройке vLLM прямо указывает на этот компромисс: меньшие значения бюджета чанков «обеспечивают лучшее ITL, так как меньше операций prefill замедляют decode», в то время как более высокие значения «обеспечивают лучшее время до первого токена (TTFT), так как можно обрабатывать больше токенов prefill в одном пакете». Вы выбираете, чей пользовательский опыт защитить: того, кто ждет начала ответа, или тех, кто наблюдает за потоком текста.
Длина prompt определяет, насколько сильно это влияет на работу. Prompt на 6,000 токенов с ответом на 200 токенов — это 6,000 токенов работы prefill против 200 шагов decode. Чат с использованием RAG и длинные системные prompt переводят систему именно в такой режим, поэтому prefill перестает быть погрешностью и становится тем, чего ждут пользователи. Prefix caching помогает, когда длинная часть повторяется: vLLM предоставляет --enable-prefix-caching, который повторно использует кэш для общего префикса prompt вместо того, чтобы пересчитывать его для каждого запроса.
Первым делом заканчивается память под KV cache
Каждый токен в активном диалоге оставляет вектор ключа и вектор значения в каждом слое модели. Это и есть KV cache (кэш ключей и значений), который позволяет при декодировании не пересчитывать весь промпт для каждого нового токена. Его размер на один токен определяется архитектурой модели: 2 (один ключ, одно значение) умножить на количество слоёв, на количество голов ключей/значений, на размерность головы и на количество байт на значение. Эти числа можно найти в config.json модели.
Выполните расчёт один раз, и лимит перестанет быть загадкой. Типичная модель 8B с 36 слоями, 8 головами ключей/значений и размерностью головы 128, хранящая кэш в 16-битном формате, потребляет 2 36 8 128 2 байт на токен. Это 147 456 байт, примерно 144 KiB. Таким образом, для одного диалога на 8 192 токена требуется около 1.2 GB кэша. Пять таких диалогов потребуют около 6 GB сверх весов модели — это и есть реальный ответ на вопрос о том, сколько пользователей поместится в память.
Параллельность умножает контекст, и инструменты прямо говорят об этом. FAQ Ollama: «Параллельная обработка запросов для конкретной модели приводит к увеличению размера контекста пропорционально количеству параллельных запросов. Например, контекст 2K при 4 параллельных запросах приведёт к контексту 8K и дополнительному выделению памяти». Необходимый объём RAM масштабируется как OLLAMA_NUM_PARALLEL, умноженное на OLLAMA_CONTEXT_LENGTH. В llama-server контекст, который вы запрашиваете через -c, распределяется между -np слотами, поэтому увеличение количества слотов само по себе уменьшает объём памяти, доступный для каждого запроса. Читайте размер контекста на слот в логе запуска, а не делайте предположений.
vLLM вместо этого выполняет предварительное выделение памяти. --gpu-memory-utilization (по умолчанию 0.92) — это «доля памяти GPU, используемая для исполнителя модели». Всё, что остаётся после загрузки весов, становится пулом страничного KV-кэша. Когда этот пул истощается, планировщик вытесняет запрос, вместо того чтобы допустить сбой:
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 для vLLM режимом вытеснения по умолчанию является RECOMPUTE, поэтому вытесненный запрос отбрасывает свой кэш и заново выполняет префилл при повторном допуске. Эта работа выполняется дважды. Документация предупреждает, что «вытеснение и пересчёт могут негативно повлиять на общую задержку», и эта строка лога — лучшее объяснение того, почему один «неудачливый» пользователь ждал гораздо дольше остальных, в то время как средние показатели выглядели нормальными. Установите disable_log_stats=False для логирования совокупного количества вытеснений или считывайте счётчик вытеснений из метрик Prometheus, которые предоставляет vLLM.
Что меняется при 2, 5 и 20 одновременных пользователях
Два пользователя. На GPU с запасом кэша нагрузка почти незаметна, так как второй поток декодирования выполняется параллельно первому с минимальными дополнительными затратами времени. На VPS только с CPU и 4–8 ГБ оперативной памяти это не проходит бесследно: оба потока делят между собой ограниченное количество vCPU и пропускную способность RAM. В результате каждый пользователь получает примерно вдвое меньше токенов в секунду, а потребность в кэше удваивается при гораздо более жестких ограничениях.
Пять пользователей. На этом этапе стандартных настроек перестает хватать, и проблема начинает проявляться как очередь. При OLLAMA_NUM_PARALLEL равном 1 четыре человека ждут завершения генерации длинного ответа для первого, после чего каждый из них получает нормальную скорость, как только до него доходит очередь. Увеличение количества параллельных запросов меняет характер проблемы: пять слотов по 8K контекста каждый требуют 40K токенов кэша. Если этот объем не помещается в VRAM, движок выгружает слои в системную RAM, а если не хватает и её, система начинает использовать swap, и количество токенов в секунду резко падает.
Двадцать пользователей. Двадцать человек в чат-интерфейсе — это обычно не двадцать одновременных запросов, и это самое важное, что нужно понять перед покупкой оборудования. Человек читает ответ и обдумывает его от 20 до 60 секунд между итерациями, поэтому большую часть времени сессия простаивает. Двадцать агентов или двадцать задач по суммаризации документов — это двадцать реальных потоков без единой секунды простоя. Для такой нагрузки требуется другое оборудование.
Ваши пользователи работают одновременно или просто авторизованы?
Прежде чем приступать к оценке ресурсов, определите количество запросов, находящихся в обработке. Расчет прост: количество одновременных запросов равно числу пользователей, умноженному на время генерации одного ответа в секундах, деленному на интервал между запросами в секундах.
- Сначала измерьте собственную скорость обработки в однопоточном режиме, учитывая как предварительное заполнение (prefill), так и декодирование. Не используйте показатели с чужих видеокарт: измерьте количество токенов в секунду на своем оборудовании и используйте полученные данные.
- Оцените коэффициент загрузки. Двадцать пользователей чата, 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-токенов.
Когда стандартных настроек Ollama становится недостаточно
Увеличьте количество параллельных запросов через юнит службы, так как переменные, экспортированные в оболочке, не будут переданы демону, управляемому 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 psКоманда systemctl show должна вывести три переменные, которые вы только что задали. Если этого не произошло, значит, файл конфигурации (drop-in) не был сохранён, и дальнейшие действия не принесут результата. Команда ollama ps выведет список загруженных моделей с размером, превышающим размер самих весов, так как четыре слота по 8,192 токена резервируют 32,768 токенов кэша дополнительно. Если в столбце PROCESSOR отображается, что часть модели находится в CPU, хотя вы ожидали размещения всей модели в GPU, значит, вы запросили больше кэша, чем доступно на видеокарте. Уменьшите одно из этих двух значений.
Значение очереди по умолчанию требует особого внимания. Ollama ставит в очередь до OLLAMA_MAX_QUEUE запросов, при этом «значение по умолчанию — 512». При превышении этого лимита сервер отвечает «ошибкой 503, указывающей на перегрузку сервера». Очередь на 512 запросов на машине, которая обрабатывает только четыре запроса одновременно, — это невыполнимое обещание, так как клиент на 300-й позиции получит таймаут задолго до того, как до него дойдёт очередь. Короткая очередь возвращает ошибку, которую приложение может обработать повторно или отобразить пользователю, что лучше, чем бесконечное ожидание ответа.
Проведите реальное тестирование. Отправьте два запроса одновременно из двух терминалов и наблюдайте за обоими. Если второй запрос не начинает выполняться до завершения первого, значит, настройка параллелизма не вступила в силу.
Когда серверный движок начинает окупать себя
vLLM оправдывает затраты на настройку, если у вас есть GPU с запасом мощности и более четырех активных запросов одновременно. Планировщик vLLM работает на уровне токенов, кэш разбит на страницы для повторного использования свободных фрагментов, а неиспользуемая VRAM преобразуется в параллелизм вместо простоя. По состоянию на август 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 ГБ ОЗУ. В таких случаях лучше использовать модель меньшего размера с более коротким контекстом и собственной очередью запросов. В статье в чем разница между Ollama и vLLM как серверными движками подробно описан выбор, а в запуске Qwen 3 8B на VPS показано, какие ресурсы требуются модели среднего размера еще до появления первого пользователя.
Компромисс, который скрывает фольклор
Continuous batching повышает общую пропускную способность и обычно улучшает медианную задержку, так как запрос из очереди начинает обрабатываться быстрее. Однако задержки в «хвосте» распределения (tail latency) ведут себя иначе, и об этой стороне редко упоминают.
Каждая дополнительная последовательность в шаге добавляет объем работы, поэтому ITL растет для всех по мере заполнения батча. Prefill нового запроса отнимает часть времени шага, которое иначе досталось бы пользователям в режиме стриминга. При нехватке памяти планировщик выполняет вытеснение (preemption), что возвращает частично сгенерированный запрос к началу его prefill.
Интерфейс чата демонстрирует «хвосты», а не средние значения. Стриминг, который замирает на две секунды в середине предложения, воспринимается как сломанный, даже если общее время завершения генерации приемлемо. Измеряйте p95 TTFT и p95 ITL под ожидаемой нагрузкой и рассматривайте среднее количество токенов в секунду как показатель емкости, а не как описание пользовательского опыта.
Практическая настройка следует из этого вывода. Ограничьте конкурентность на уровне чуть ниже того, что позволяет память, чтобы движку никогда не приходилось выполнять вытеснение. Короткая предсказуемая очередь лучше, чем глубокий батч, вызывающий «пробуксовку» (thrashing), потому что пользователь, который ждет четыре секунды, а затем получает плавный стриминг, доволен больше, чем тот, у кого генерация начинается мгновенно, но дважды прерывается.
Что проверять при низкой производительности
Каждый пользователь работает в штатном режиме, но ожидание длительное. Это очередь, а не проблема скорости. Сначала проверьте параметр параллелизма. Модель обслуживает запросы корректно, по одному за раз.
Ошибка HTTP 503 от Ollama. Очередь переполнена. Либо сервер действительно достиг предела мощности, либо OLLAMA_MAX_QUEUE намеренно установлен на низкое значение для сброса нагрузки, что и является ожидаемым поведением.
Количество токенов в секунду падает под нагрузкой на CPU-сервере. Запустите vmstat 1 во время возникновения проблемы. Ненулевые значения в столбцах si и so означают, что система использует swap, поэтому веса модели считываются с диска для каждого токена. Никакие изменения конфигурации здесь не помогут. Уменьшите размер модели или количество слотов.
Один из десяти пользователей ждет значительно дольше остальных. Найдите в логах vLLM запись preempted. Обычно это вызвано вытеснением (preemption) и последующим пересчетом, что означает переполнение кэша для разрешенной длины контекста.
TTFT (время до первого токена) высокое, даже когда сервер простаивает. Это проблема префилла (prefill), а не конкурентности. Длинные промпты требуют реального времени до появления первого токена, поэтому прежде чем проверять оборудование, обратите внимание на размер промпта и кэширование префиксов.
FAQ
Почему мой self-hosted LLM замедляется, когда им пользуется второй человек?
Чаще всего он не замедляется, а встает в очередь. Ollama поставляется с OLLAMA_NUM_PARALLEL, равным 1, поэтому второй запрос ожидает, пока первый не выдаст свой финальный токен. Различить эти два случая можно, замерив скорость потока одного пользователя, пока ждет другой: если количество токенов в секунду у них в норме после начала генерации, значит, у вас очередь, и увеличение количества параллельных запросов решит проблему. Если оба потока работают на половинной скорости, вы действительно делите пропускную способность памяти, и это аппаратное ограничение.
Сколько одновременных пользователей может обслужить одна небольшая GPU?
Считайте память, а не пользователей. Сначала веса, затем KV-кэш, который стоит 2 умножить на количество слоев, умножить на количество key/value heads, умножить на размерность head и на количество байт, на каждый токен, на каждую активную беседу. Типичная модель 8B с 36 слоями, 8 key/value heads и размерностью head 128 требует около 144 KiB на токен в 16-битном представлении, поэтому беседа на 8,192 токена занимает примерно 1.2 GB. Карта на 24 GB, удерживающая эту модель в 16-битном представлении, имеет около 6 GB свободных для кэша, что составляет примерно пять бесед при полном контексте или больше, если сократить контекст.
Делает ли continuous batching ответ каждого пользователя медленнее?
Медианная задержка обычно улучшается, так как запросы перестают ждать завершения всей пачки. Хвостовая задержка (tail latency) ухудшается. Каждая дополнительная последовательность добавляет работу на каждом шаге декодирования, prefill нового запроса отнимает часть шага у пользователей, получающих поток, а вытесненный запрос должен проходить prefill дважды. Измеряйте p95 inter-token latency, а не среднее значение, потому что в окне чата паузы заметны так, как это скрывают средние показатели.
Стоит ли увеличивать OLLAMA_NUM_PARALLEL или переходить на vLLM?
Сначала увеличьте количество параллельных запросов. Это бесплатно, требует создания одного файла конфигурации и решает типичную проблему, когда четыре человека стоят в очереди за одним длинным ответом. Ограничение — память: параллельные запросы умножают контекст, который необходимо удерживать, поэтому следите за тем, чтобы слои не вытеснялись в CPU. Переходите на vLLM, когда у вас есть свободная VRAM на GPU и более четырех запросов, находящихся в активной обработке, так как именно в этот момент paged cache и планирование на уровне токенов дают больше выгоды, чем затрат.
Исправит ли большее количество ядер CPU медленную работу LLM-сервера?
Не для той части, которую пользователи замечают больше всего. Декодирование считывает всю модель из памяти для каждого токена, поэтому оно ограничено пропускной способностью RAM, и дополнительные ядра перестают помогать, как только пропускная способность насыщается. Prefill действительно масштабируется с количеством ядер, поэтому их увеличение сокращает время до первого токена при длинных промптах. На VPS с 4–8 GB RAM ограничивающим фактором обычно является объем памяти, и эффективным решением будет использование меньшей модели или более короткого контекста, а не увеличение количества vCPU.