Как самостоятельно развернуть инференс LLM с vLLM

Зачем самостоятельно разворачивать инференс LLM с vLLM
- Контроль над данными. Промпты и ответы остаются на машине, которую вы контролируете, и не нужно разбираться в чужих правилах хранения данных.
- Фиксированные расходы. При оплате за токены счёт растёт вместе с использованием. Выделенный GPU стоит одинаково, обслуживает ли он миллион токенов или миллиард, поэтому стоимость токена — это цена в месяц, делённая на число обслуженных токенов, и она снижается по мере роста загрузки.
- Задержка под вашим контролем. Никакой общей очереди, сервер рядом с вашим приложением и собственный баланс между размером батча и временем ответа.
- Гибкая настройка.
Любая открытая модель, ваши собственные модели после дообучения ( fine-tuning ) и адаптеры LoRA, структурированный вывод и ограничения контекста, подходящие вашему приложению.
Но и компромиссы вполне реальны. Стек обслуживаете вы: обновления, безопасность, мощности и отказоустойчивость — ваша забота. Вы платите и за часы простоя. Закрытые фронтирные модели развернуть у себя нельзя. А если объём запросов невелик или нагрузка скачкообразная, API с оплатой за токены часто обходится дешевле.
Подбор GPU: веса плюс KV-кэш
Пример расчёта: Llama 3.1 70B в FP8 на одном H200 со 141 ГБ при . vLLM может использовать около 130 ГБ. После примерно 70 ГБ весов и нескольких гигабайт на активации на кэш остаётся около 55 ГБ: это примерно 170 000 токенов в BF16, или 20 одновременных диалогов с полным контекстом по 8 192 токена. Кэш в FP8 удваивает это число — примерно до 40. Полная методика и другие примеры — в нашем руководстве по видеопамяти (VRAM) для LLM.
Установка vLLM и запуск OpenAI-совместимого сервера
. См. также нашу документацию по PyTorch и vLLM.
sudo apt-get update
sudo apt-get install -y python3-venv
python3 -m venv ~/vllm-env
source ~/vllm-env/bin/activate
pip install vllm
export HF_TOKEN=hf_your_token
export VLLM_API_KEY=$(openssl rand -hex 32)
vllm serve meta-llama/Llama-3.1-8B-Instruct \
--host 127.0.0.1 --port 8000 \
--api-key "$VLLM_API_KEY" \
--max-model-len 32768
При первом запуске веса скачиваются в кэш Hugging Face; если домашний каталог небольшой, укажите в HF_HOME каталог на локальном NVMe. Затем проверьте эндпоинт:
curl http://127.0.0.1:8000/v1/chat/completions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $VLLM_API_KEY" \
-d '{
"model": "meta-llama/Llama-3.1-8B-Instruct",
"messages": [
{"role": "user", "content": "Explain the KV cache in one sentence."}
],
"max_tokens": 100
}'
Поскольку API следует формату OpenAI, существующие клиенты заработают, как только вы поменяете базовый URL и ключ:
from openai import OpenAI
client = OpenAI(base_url="http://127.0.0.1:8000/v1", api_key="YOUR_KEY")
reply = client.chat.completions.create(
model="meta-llama/Llama-3.1-8B-Instruct",
messages=[{"role": "user", "content": "Hello"}],
)
print(reply.choices[0].message.content)
Если вы предпочитаете Docker, официальный образ принимает те же аргументы. Публикуйте порт только на 127.0.0.1, а в продакшене фиксируйте тег релиза вместо latest:
docker run --gpus all --ipc=host \
-p 127.0.0.1:8000:8000 \
-v ~/.cache/huggingface:/root/.cache/huggingface \
-e HF_TOKEN \
vllm/vllm-openai:latest \
--model meta-llama/Llama-3.1-8B-Instruct \
--api-key "$VLLM_API_KEY"
Тензорный параллелизм на нескольких GPU
Когда модель не помещается на один GPU, параметр
На сервере 8× H100 можно запустить две копии Llama 3.1 70B в BF16, каждую на четырёх GPU:
CUDA_VISIBLE_DEVICES=0,1,2,3 vllm serve meta-llama/Llama-3.1-70B-Instruct \
--tensor-parallel-size 4 --host 127.0.0.1 --port 8001
CUDA_VISIBLE_DEVICES=4,5,6,7 vllm serve meta-llama/Llama-3.1-70B-Instruct \
--tensor-parallel-size 4 --host 127.0.0.1 --port 8002
Каждая копия распределяет 140 ГБ весов по 320 ГБ памяти — около 35 ГБ на GPU, — так что при стандартной настройке памяти на каждом GPU для кэша остаётся примерно столько же. Обратный прокси распределяет запросы между двумя портами.
Эмпирическое правило: используйте тензорный параллелизм, когда модель не помещается на один GPU или когда нужна меньшая задержка на запрос. В остальных случаях запускайте по одной копии на каждом GPU за балансировщиком нагрузки — так трафика между GPU не будет вовсе. На картах, связанных только через PCIe, таких как L40S или RTX PRO 6000, лучше запускать отдельные копии или попробовать : этот режим требует меньше обмена данными, чем тензорный параллелизм.
Настройка памяти: FP8, кэширование префиксов и ограничения
Несколько параметров определяют, сколько поместится в память и насколько эффективно используется кэш:
преобразует веса из BF16 в FP8 при загрузке модели и вдвое сокращает память под веса на GPU Hopper, Ada и Blackwell. Проверьте, как это делает ваша версия vLLM: старые версии сначала загружали веса в полной точности, поэтому модель в BF16 должна была поместиться в память до преобразования. Для модели 70B на одном GPU используйте чекпоинт, уже квантованный в FP8; vLLM определит формат по конфигурации чекпоинта.--quantization fp8 вдвое уменьшает кэш и вдвое увеличивает число токенов, которые в нём помещаются. Прежде чем включать этот параметр, проверьте качество на задачах с длинным контекстом.--kv-cache-dtype fp8 - Кэширование префиксов
позволяет повторно использовать кэш для общих префиксов промптов: системных промптов, примеров few-shot , предыдущих реплик диалога и документов, которые повторяются в разныхRAG-запросах. В последних версиях оно включено по умолчанию; в более старых передайте. Чтобы извлечь из него пользу, ставьте в промптах неизменное содержимое в начало, а изменяющееся — в конец.--enable-prefix-caching ограничивает контекст одного запроса. По умолчанию это максимум модели — 131 072 токена для Llama 3.1, и vLLM не запустится, если одна последовательность такой длины не помещается в кэш, поэтому задайте значение, которое вам действительно нужно.--max-model-len --gpu-memory-utilization — доля памяти каждого GPU, которую vLLM может занять под веса, активации и кэш; по умолчанию 0,9. На выделенном GPU её можно поднять примерно до 0,95, чтобы получить больше кэша; уменьшите её, если GPU использует что-то ещё.ограничивает число последовательностей, которые обрабатываются одновременно, — простой способ удержать задержку под нагрузкой в разумных пределах.--max-num-seqs
Если собрать всё вместе, запуск заранее квантованного чекпоинта модели 70B в FP8 на одном H200 выглядит так:
vllm serve /models/llama-3.1-70b-instruct-fp8 \
--served-model-name llama-3.1-70b \
--max-model-len 32768 \
--gpu-memory-utilization 0.92 \
--kv-cache-dtype fp8 \
--host 127.0.0.1 --port 8000
Чем квантование оборачивается для качества, разобрано в нашей статье о точности FP8 и FP4.
Защита эндпоинта
Открытый эндпоинт инференса рано или поздно найдут и начнут использовать посторонние. Защищайте его в несколько уровней.
Привязка к localhost и обязательный ключ
В примерах выше vLLM привязан к 127.0.0.1, поэтому снаружи сервера к нему напрямую не обратиться. Параметр или переменная окружения VLLM_API_KEY заставляет клиентов передавать ключ в заголовке Authorization: Bearer. Ключ защищает маршруты API /v1
Файрвол
Разрешите SSH, HTTP и HTTPS, а всё остальное запретите:
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
Если подключаться должны только известные клиенты, замените правило для порта 443 отдельным правилом для каждого адреса источника, например sudo ufw allow from 203.0.113.10 to any port 443 proto tcp. Есть одна ловушка: порты, опубликованные Docker, обходят правила UFW, — именно поэтому в примере с Docker порт публикуется только на 127.0.0.1. Подробнее — в нашем руководстве по сети и файрволу
Обратный прокси с TLS
/etc/caddy/Caddyfile выглядит так:
llm.example.com {
reverse_proxy 127.0.0.1:8000
}
Перезагрузите конфигурацию командой sudo systemctl reload caddy. Потоковые ответы передаются клиенту по мере генерации. Чтобы распределять запросы между двумя копиями vLLM, перечислите оба бэкенда: reverse_proxy 127.0.0.1:8001 127.0.0.1:8002. Если вместо Caddy вы используете nginx, отключите proxy_buffering для блока location с API и увеличьте proxy_read_timeout
Запуск vLLM как службы и мониторинг
В продакшене запускайте vLLM под systemd от имени непривилегированного пользователя, чтобы он стартовал при загрузке системы и перезапускался после сбоя:
sudo useradd --system --user-group --create-home \
--home-dir /opt/vllm --shell /usr/sbin/nologin vllm
sudo -u vllm python3 -m venv /opt/vllm/venv
sudo -u vllm /opt/vllm/venv/bin/pip install vllm
sudo mkdir -p /etc/vllm
Храните секреты в файле /etc/vllm/vllm.env, доступном для чтения только root (sudo chmod 600 /etc/vllm/vllm.env); systemd читает этот файл до того, как понижает привилегии:
HF_TOKEN=hf_your_token
HF_HOME=/opt/vllm/hf-cache
VLLM_API_KEY=your-long-random-key
Затем создайте /etc/systemd/system/vllm.service:
[Unit]
Description=vLLM OpenAI-compatible server
After=network-online.target
Wants=network-online.target
[Service]
User=vllm
Group=vllm
EnvironmentFile=/etc/vllm/vllm.env
ExecStart=/opt/vllm/venv/bin/vllm serve meta-llama/Llama-3.1-8B-Instruct \
--host 127.0.0.1 --port 8000 --max-model-len 32768
Restart=on-failure
RestartSec=10
[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload
sudo systemctl enable --now vllm
journalctl -u vllm -f
Здесь vLLM берёт ключ из VLLM_API_KEY, поэтому он никогда не появляется в командной строке. При мониторинге следите за тремя уровнями:
- Работоспособность.
GET /healthвозвращает 200, когда сервер работает; настройте на него проверку доступности. - Метрики инференса.
/metricsотдаёт метрики Prometheus, в том числе число выполняемых и ожидающих запросов (vllm:num_requests_runningиvllm:num_requests_waiting), время до первого токена, сквозную задержку и заполненность KV-кэша. Настройте оповещения на случаи, когда запросы ждут по нескольку минут, когда растёт время до первого токена или когда кэш остаётся почти заполненным, что вынуждает vLLM прибегать к вытеснению (preemption) запросов. - Метрики GPU. Для быстрой проверки используйте
, а для Prometheus — экспортер DCGM от NVIDIA. Не удивляйтесь, что память показывает занятость около 90% даже в простое: vLLM резервирует свою долю при запуске. Следите за температурой, энергопотреблением и ошибками GPU в журнале ядра (nvidia-smi journalctl).-k
Подробнее о том, что происходит на стороне GPU, — в нашем руководстве по мониторингу GPU.
Выбор GPU
Подбирайте GPU под модель, точность и нужное вам число одновременных запросов. Скорость генерации во многом определяется пропускной способностью памяти, поэтому это второй показатель, который стоит сравнивать после объёма.
| Модель и точность | Веса | Подходящий GPU | Заявленная пропускная способность на GPU | Цена в месяц |
|---|---|---|---|---|
| 8B в BF16 | Около 16 ГБ | RTX 5090 (32 ГБ) или L40S (48 ГБ) | 1,79 ТБ/с или 864 ГБ/с | 449 $ или 890 $ |
| RTX PRO 6000 (96 ГБ) | 1,6 ТБ/с | 1 390 $ | ||
| 32B в BF16 | Около 64 ГБ | H100 (80 ГБ) | 3,35 ТБ/с | 2 190 $ |
| 70B в FP8 | Около 70 ГБ | H200 (141 ГБ) | 4,8 ТБ/с | 3 090 $ |
| 70B в BF16 | Около 140 ГБ | B200 (180 ГБ) или B300 (288 ГБ) | 8 ТБ/с | 4 590 $ или 5 190 $ |
| 405B в FP8 | Около 405 ГБ | 8× H100 (640 ГБ) или 8× H200 (1 128 ГБ) | 3,35 или 4,8 ТБ/с | 16 900 $ или 23 900 $ |
| Около 670 ГБ | 8× H200 или 8× B200 (1 440 ГБ) | 4,8 или 8 ТБ/с | 23 900 $ или 34 900 $ |
Модель 70B в FP8 помещается и на два H100 с тензорным параллелизмом, но за 4 380 $ в месяц это дороже одного H200, да ещё и добавляет трафик между GPU. vLLM поддерживает и GPU AMD через ROCm, поэтому для очень больших моделей подойдёт сервер 8× MI355X с 288 ГБ на GPU; сначала проверьте на нём свою модель. При фиксированной цене в месяц стоимость собственного эндпоинта известна заранее, а в наши тарифы входит 20 ТБ (один GPU) или 50 ТБ (8 GPU) исходящего трафика в месяц — гораздо больше, чем обычно нужно для потоковой передачи текста. Подробное сравнение — в нашем руководстве о лучшем GPU для инференса LLM или на странице NVIDIA H200.
Часто задаваемые вопросы
Какой GPU нужен, чтобы самостоятельно развернуть модель 70B с vLLM?
В FP8 веса занимают около 70 ГБ, поэтому один H200 со 141 ГБ обслуживает такую модель с запасом для одновременных запросов, а на H100 с 80 ГБ она помещается лишь впритык. В BF16 одни только веса занимают около 140 ГБ — значит, нужен B200, B300 или тензорный параллелизм на нескольких GPU.
Совместим ли vLLM с API OpenAI?
Как защитить эндпоинт vLLM?
Почему nvidia-smi показывает, что GPU почти заполнен, когда vLLM простаивает?
Что выбрать: тензорный параллелизм или несколько реплик?
Тензорный параллелизм нужен, когда модель не помещается на один GPU или когда требуется меньшая задержка на запрос, — в идеале через NVLink. Если модель помещается на один GPU, отдельные реплики за балансировщиком нагрузки избавляют от трафика между GPU и обычно дают большую суммарную производительность.
Выделенные GPU с помесячной оплатой


