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-сервер редко ограничивается одной настройкой — вот что обычно идёт следом: