vLLM: высокопроизводительный inference-сервер для команды
Ollama отлично подходит для старта — она проста в настройке и не требует разбираться в тонкостях inference, но у простоты есть цена: под нагрузкой от десятка одновременных пользователей Ollama начинает заметно проседать по скорости, потому что не умеет эффективно объединять параллельные запросы в один батч на GPU. Когда команда вырастает и AI-сервер становится не игрушкой для тестов, а рабочим инструментом всей компании, имеет смысл посмотреть в сторону vLLM — inference-сервера, спроектированного именно под высокую пропускную способность и параллельную нагрузку.
Разворачиваем vLLM на srv-ai01 (сеть 192.168.10.0/24, домен itpages.lab.local) как замену или дополнение к Ollama из предыдущих статей серии — оба движка можно держать на одном сервере, переключая Open WebUI на нужный бэкенд в зависимости от модели.
Чем vLLM отличается от Ollama
Ключевая технология vLLM — PagedAttention, механизм управления KV-кэшем (промежуточными вычислениями внимания модели), который работает по принципу виртуальной памяти операционной системы: память под контекст каждого запроса выделяется страницами, а не сплошным блоком. Это резко снижает фрагментацию видеопамяти и позволяет держать в обработке кратно больше параллельных запросов на той же GPU по сравнению с наивной реализацией, которую использует Ollama. На практике это означает, что при 10-20 одновременных пользователях vLLM даёт заметно более высокую суммарную пропускную способность токенов в секунду, хотя задержка ответа на одиночный запрос у Ollama может быть даже чуть ниже.
Второе важное отличие — vLLM изначально спроектирован как production-сервер с OpenAI-совместимым API, что упрощает интеграцию: любой инструмент, который умеет работать с API OpenAI (включая сам Open WebUI), подключается к vLLM почти без изменений конфигурации.
Развёртывание vLLM в Docker Compose
Добавь сервис vllm в существующий docker-compose.yml на srv-ai01 рядом с уже работающими ollama и openwebui:
vllm:
image: vllm/vllm-openai:latest
container_name: vllm
restart: unless-stopped
ports:
- "8000:8000"
volumes:
- /srv/ai/models-hf:/root/.cache/huggingface
environment:
- HUGGING_FACE_HUB_TOKEN=hf_xxxxxxxxxxxxxxxx
command:
- --model
- Qwen/Qwen2.5-14B-Instruct-AWQ
- --quantization
- awq
- --max-model-len
- "8192"
- --gpu-memory-utilization
- "0.90"
- --api-key
- vllm-internal-key-change-me
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: all
capabilities: [gpu]
networks:
- ai-net
В отличие от Ollama, которая скачивает модели по короткому имени из собственного реестра, vLLM работает напрямую с моделями из Hugging Face Hub — для приватных или ограниченных по лицензии моделей понадобится токен доступа HUGGING_FACE_HUB_TOKEN. Параметр gpu-memory-utilization задаёт долю видеопамяти, которую vLLM резервирует под веса модели и KV-кэш — значение 0.90 оставляет небольшой запас для системных нужд драйвера, поднимать его до 1.0 не стоит, это часто приводит к падению процесса при пиковой нагрузке.
Подключение к Open WebUI
Поскольку vLLM отдаёт OpenAI-совместимый API, подключить его к Open WebUI можно как дополнительного провайдера моделей, без переписывания основного docker-compose.yml сервиса openwebui — достаточно добавить соединение через панель администратора (Admin Panel → Settings → Connections → OpenAI API) или переменной окружения:
openwebui:
environment:
- OLLAMA_BASE_URL=http://ollama:11434
- OPENAI_API_BASE_URL=http://vllm:8000/v1
- OPENAI_API_KEY=vllm-internal-key-change-me
После перезапуска контейнера openwebui модель, которую раздаёт vLLM, появится в общем списке моделей рядом с моделями Ollama, и пользователи смогут выбирать между ними в интерфейсе чата так же, как выбирают между GPT-3.5 и GPT-4 в обычном ChatGPT — без разницы в UX, только с разницей в скорости и качестве ответа под капотом.
Проверка производительности
Прежде чем полностью переводить команду на vLLM, стоит прогнать простой нагрузочный тест и сравнить пропускную способность с текущей настройкой Ollama. У vLLM есть встроенный бенчмарк-скрипт, но для практической проверки достаточно параллельных curl-запросов к API:
for i in $(seq 1 20); do
curl -s http://192.168.10.50:8000/v1/chat/completions \
-H "Authorization: Bearer vllm-internal-key-change-me" \
-H "Content-Type: application/json" \
-d '{"model": "Qwen/Qwen2.5-14B-Instruct-AWQ", "messages": [{"role":"user","content":"Напиши короткое резюме про Docker"}]}' &
done
wait
Замерь суммарное время выполнения двадцати параллельных запросов и сравни с тем же тестом против Ollama на том же сервере — разница в пропускной способности при десятке и более одновременных запросов обычно оказывается в пользу vLLM в разы, особенно на моделях среднего и крупного размера.
Квантование моделей: AWQ и GPTQ
Для эффективной работы на одной карте vLLM хорошо поддерживает квантованные форматы AWQ и GPTQ — они сжимают веса модели до 4 бит на параметр с минимальной потерей качества, что позволяет уместить модель на 14-32 млрд параметров в 24 ГБ видеопамяти одной RTX 4090. Готовые квантованные версии популярных моделей обычно уже опубликованы сообществом на Hugging Face — искать стоит по суффиксу -AWQ или -GPTQ в названии репозитория, не заниматься квантованием самостоятельно, если в этом нет специфической необходимости. Разница между AWQ и GPTQ на практике невелика: AWQ чуть лучше сохраняет качество на моделях с большим количеством выбросов в весах, а GPTQ немного шире поддерживается старыми версиями инструментов — для новых развёртываний на актуальной версии vLLM можно ориентироваться в первую очередь на то, какой вариант квантования вообще доступен для конкретной интересующей модели.
docker exec -it vllm python3 -c "
from transformers import AutoConfig
config = AutoConfig.from_pretrained('Qwen/Qwen2.5-14B-Instruct-AWQ')
print(config)
"
Если видеопамяти достаточно и приоритет — максимальное качество ответов, можно запускать модель и без квантования, в исходной точности bfloat16, просто указав имя модели без квантованного суффикса и убрав параметр —quantization из команды запуска контейнера.
Когда vLLM не нужен
vLLM не всегда оправдан. Для команды до пяти-семи человек с умеренной нагрузкой прирост пропускной способности вряд ли будет заметен на практике, а Ollama при этом остаётся проще в администрировании: не нужно вручную подбирать параметры квантования и объём контекста, модели скачиваются командой в одну строку. Переходить на vLLM имеет смысл, когда метрики (о мониторинге GPU и моделей поговорим в отдельной статье серии) показывают, что очередь запросов на сервере регулярно растёт в часы пик, а не одиночными всплесками.
Настройка непрерывного батчинга и очереди
Основная причина, по которой vLLM выигрывает у наивных реализаций на нагрузке, — continuous batching: вместо того чтобы обрабатывать запросы строго по очереди или ждать формирования полного батча фиксированного размера, vLLM динамически добавляет новые запросы в уже выполняющийся батч на каждом шаге генерации токена. Это значит, что если пользователь A отправил длинный запрос, а через секунду пользователь B — короткий, короткий запрос B не будет ждать полного завершения генерации A, а присоединится к вычислениям на лету. Настраивать этот механизм вручную обычно не требуется, но полезно знать два параметра, которые влияют на его поведение под высокой нагрузкой:
command:
- --model
- Qwen/Qwen2.5-14B-Instruct-AWQ
- --quantization
- awq
- --max-num-seqs
- "64"
- --max-num-batched-tokens
- "8192"
Параметр max-num-seqs ограничивает число одновременно обрабатываемых последовательностей — если поставить его слишком высоким для объёма видеопамяти карты, vLLM начнёт упираться в нехватку памяти под KV-кэш и откатывать (preemption) часть запросов обратно в очередь, что на практике ощущается пользователями как случайные задержки. Начинай с консервативного значения 32-64 для карты уровня RTX 4090 и повышай постепенно, наблюдая за метриками в логах контейнера.
Логи и диагностика
vLLM подробно логирует метрики пропускной способности прямо в stdout контейнера — число обрабатываемых токенов в секунду, число запросов в очереди на ожидании (pending) и число вытесненных запросов (preempted). Эти значения — первый сигнал, что серверу пора добавлять ресурсы или переходить к балансировке между несколькими GPU:
docker logs -f vllm | grep "Avg prompt throughput"
Регулярный рост числа preempted-запросов в логах — надёжный индикатор того, что текущая GPU уже не справляется с параллельной нагрузкой команды, и это тот самый момент, когда стоит переходить к статье про балансировку между srv-ai01 и srv-ai02, а не пытаться и дальше выжимать производительность из одной карты бесконечной подстройкой параметров.
Tensor parallelism на нескольких GPU в одном сервере
Если на srv-ai01 стоит не одна, а две и более видеокарты, vLLM умеет распределять веса одной модели между ними через tensor parallelism — это отдельный сценарий от балансировки между серверами (о ней в статье про srv-ai01/srv-ai02), здесь речь про одну модель на нескольких GPU одной машины. Это особенно полезно для моделей, которые не помещаются в память одной карты даже с квантованием, например, на 70+ млрд параметров:
command:
- --model
- Qwen/Qwen2.5-72B-Instruct-AWQ
- --quantization
- awq
- --tensor-parallel-size
- "2"
- --max-model-len
- "8192"
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: 2
capabilities: [gpu]
Значение tensor-parallel-size должно совпадать с числом видеокарт, которые контейнер получает через deploy.resources, и по опыту работает надёжнее всего, когда карты одинаковой модели — смешивать в одном tensor-parallel-группе, например, RTX 4090 и более старую карту с иным объёмом памяти vLLM формально не запрещает, но на практике узким местом становится карта с наименьшим объёмом памяти, что сводит на нет выгоду от второй карты. Учитывай также, что обмен данными между картами при tensor parallelism идёт через шину PCIe (или NVLink, если он есть), и на серверах без NVLink прирост от двух карт будет заметно меньше двукратного из-за накладных расходов на синхронизацию между шагами генерации.
Приоритизация запросов и адаптеры LoRA
В смешанной нагрузке — часть запросов от интерактивного чата, часть от фонового скрипта индексации или пакетной обработки заявок — полезно не давать фоновым задачам конкурировать за GPU наравне с сотрудником, который ждёт ответа в браузере. vLLM поддерживает параметр приоритета запроса в самом API, если запущен с соответствующим планировщиком:
command:
- --model
- Qwen/Qwen2.5-14B-Instruct-AWQ
- --scheduling-policy
- priority
curl http://192.168.10.50:8000/v1/chat/completions \
-H "Authorization: Bearer vllm-internal-key-change-me" \
-H "Content-Type: application/json" \
-d '{"model":"Qwen/Qwen2.5-14B-Instruct-AWQ","priority":10,"messages":[{"role":"user","content":"..."}]}'
Отдельно стоит упомянуть поддержку LoRA-адаптеров — если в компании есть несколько специализированных дообученных вариантов одной базовой модели (например, отдельно дообученный адаптер под юридические тексты и отдельно под техническую поддержку), vLLM может обслуживать их все с одного развёрнутого экземпляра базовой модели, подгружая нужный адаптер прямо в запросе, без запуска отдельного тяжёлого контейнера под каждый вариант:
command:
- --model
- Qwen/Qwen2.5-14B-Instruct
- --enable-lora
- --lora-modules
- legal=/srv/ai/loras/legal-adapter
- support=/srv/ai/loras/support-adapter
Это заметно экономнее по видеопамяти, чем держать несколько полных копий модели ради разных специализаций, и хорошо подходит компаниям, у которых уже есть команда, дообучающая небольшие LoRA-адаптеры под конкретные задачи, а не разворачивающая полноценный fine-tuning с нуля.
Частые ошибки и как их избежать
- gpu-memory-utilization выставлен на максимум. Значение, близкое к 1.0, не оставляет запаса под системные нужды драйвера и часто приводит к падению процесса vLLM при первом же пиковом запросе с длинным контекстом. Держи значение в районе 0.85-0.90.
- Попытка запустить неквантованную модель большого размера на одной карте. Модель на 32-70 млрд параметров в полной точности просто не поместится в 24 ГБ видеопамяти, и контейнер не запустится с ошибкой нехватки памяти. Проверяй объём видеопамяти, требуемый моделью, до попытки запуска, а не после.
- Отсутствие API-ключа на внутреннем сервисе. Даже во внутренней сети 192.168.10.0/24 доступ к API vLLM стоит закрывать ключом — без него любой узел в сети может генерировать запросы на дорогой GPU-ресурс без ограничений.
Что дальше
Свой AI-сервер редко ограничивается одной настройкой — вот что обычно идёт следом: