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