28 авг. 2026 · 15 мин чтения

Production-инференс LLM в облаке: разворачиваем vLLM и снижаем стоимость токена

Всеволод
Технический писатель · IT, облачные сервисы
15 мин чтения

Запустить открытую LLM в облаке — вопрос одной команды: движок скачает веса, поднимет OpenAI-совместимый API, и модель начнёт отвечать. Настоящая работа начинается дальше — когда сервис должен держать заявленный поток запросов в пик, не упираться в нехватку памяти на длинных промптах и при этом укладываться в приемлемый бюджет. Три требования — производительность, стабильность и цена — тянут в разные стороны: запас по памяти режет число одновременных запросов, погоня за низкой задержкой снижает утилизацию карты, а карта, простаивающая полсуток, удваивает стоимость каждого токена. Разберём, из чего это складывается: механику serving LLM — почему одиночный запрос не способен загрузить GPU и что превращает простой в токены; расчёт конфигурации от профиля нагрузки — сколько карт, какой контекст и какое квантование нужны под конкретный поток; и методику пересчёта почасовой аренды GPU в стоимость миллиона токенов. Все числовые примеры привязаны к явно оговорённым допущениям: для другой модели и другого железа абсолютные значения сдвинутся, но сам порядок расчёта останется прежним.

Как устроен инференс LLM и почему GPU простаивает

Предзаполнение и декодирование: две разные нагрузки в одном сервисе

Предзаполнение — разбор входного промпта: все токены прогоняются одним пакетом, тензорные ядра загружены полностью. Это compute-bound фаза, и она формирует TTFT (задержку до первого токена).

Декодирование — генерация по одному токену за шаг. На каждом шаге модель читает из памяти все свои веса ради одного токена: для 32B в BF16 это ~64 ГБ чтения из HBM на шаг. Декодирование memory-bound: скорость определяется пропускной способностью памяти, а не вычислительной производительностью. Отсюда следствие: одиночный запрос физически не может загрузить современную карту — способен только пакет, когда веса, прочитанные один раз, обслуживают десятки последовательностей.

Наращивать пакет мешает KV-кэш: движок хранит ключи и значения для каждого обработанного токена каждого слоя. Кэш растёт линейно с длиной последовательности и числом запросов, живёт в той же VRAM, что и веса, и именно он ограничивает число одновременных запросов. Модели с GQA снижают эту потребность в разы.

Непрерывное пакетирование и PagedAttention: как пакетирование превращает простой в токены

Статическое пакетирование для генерации работает плохо: весь пакет ждёт самую длинную последовательность. Непрерывное пакетирование работает на уровне шагов декодирования — завершившаяся последовательность немедленно покидает пакет, планировщик подставляет запрос из очереди.

PagedAttention хранит KV-кэш блоками фиксированного размера (значение по умолчанию — 16 токенов, оно конфигурируется, а для некоторых гибридных архитектур движок подбирает его сам) со страничной таблицей, как виртуальная память в ОС. До него кэш выделялся непрерывным блоком под max_model_len, и фрагментация занимала значительную часть памяти; теперь потери падают до единиц процентов. В современных версиях само вычисление внимания движок делегирует оптимизированным бэкендам внимания, которые умеют работать с таблицей блоков.

Порционное предзаполнение разбивает длинное предзаполнение на порции и перемешивает их с шагами декодирования, выравнивая ITL (интервал между токенами) ценой небольшого роста TTFT; в свежих версиях vLLM это поведение по умолчанию. В прежнем режиме планировщик, наоборот, приоритизировал предзаполнение и не смешивал его с декодированием: TTFT был лучше, а межтокенная задержка и утилизация GPU — хуже. Плюс prefix caching: общий префикс — системный промпт, фрагменты документа в RAG — считается один раз и переиспользуется.

Всё вместе даёт vLLM: OpenAI-совместимый API, быстрая поддержка свежих открытых моделей, вызов инструментов, мультимодальные и embedding-модели. Разумный выбор по умолчанию. Ограничение, о котором стоит знать заранее: GGUF-модели vLLM полноценно не обслуживает — под него веса берут в исходном формате или в AWQ. Альтернативы: под специфические сценарии есть и другие движки — часть эффективнее на сложных агентских маршрутах с ветвлением, часть извлекает из железа последние проценты производительности ценой долгой сборки движка, но для типовой задачи vLLM закрывает потребность.

Сколько VRAM нужно модели и как её сократить

Формула расчёта памяти: веса, KV-кэш, служебные буферы

Разберём на модели 32 млрд параметров, 64 слоя, 8 KV-голов, размерность головы 128.

1. Веса. Число параметров × размер типа: BF16 — 64 ГБ, FP8/INT8 — 32 ГБ, INT4 (AWQ/GPTQ) — реально ~18 ГБ с метаданными.

2. KV-кэш. Формула на один токен: 2 (ключи и значения) × слои × KV-головы × размер_головы × размер_типа. Для нашей модели в BF16: 2 × 64 × 8 × 128 × 2 = 262 144 байта ≈ 256 КБ на токен. Дальше:

  • контекст 8K, 32 одновременных запроса: 64 ГБ
  • тот же контекст, KV в FP8: 32 ГБ
  • контекст 32K, 32 запроса, BF16: 256 ГБ — уже не в одну карту

При длинном контексте кэш легко перевешивает сами веса — главная причина неожиданного OOM.

3. Буферы активаций и CUDA Graphs — 2–6 ГБ. Отключение графов (--enforce-eager) экономит память, но добавляет 10–20% к времени шага декодирования.

4. Служебные расходы — контекст CUDA, аллокатор PyTorch, фрагментация: 1,5–3 ГБ на процесс.

Сведём для карты на 80 ГБ и контекста 8K:

Конфигурация

Веса

KV-кэш на 32 запроса

Буферы + служебное

Итого

BF16 веса + BF16 KV

64 ГБ

64 ГБ

6 ГБ

134 ГБ — не помещается

FP8 веса + BF16 KV

32 ГБ

64 ГБ

6 ГБ

102 ГБ — не помещается

FP8 веса + FP8 KV

32 ГБ

32 ГБ

6 ГБ

70 ГБ — помещается, до ~32 одновременных запросов

INT4 веса + FP8 KV

18 ГБ

32 ГБ

5 ГБ

55 ГБ — помещается, запас на ~48

Оценить размер весов, не скачивая их, можно за секунды: в config.json модели на Hugging Face есть все нужные размерности.

Квантование и параллелизм: что выбирать под своё поколение GPU

Поколение

Карты

Веса

KV-кэш

Комментарий

Ampere

A100, A10

INT4 (AWQ) или BF16

BF16

Нет аппаратного FP8

Hopper

H100, H200

FP8 (E4M3)

FP8

Баланс качества и скорости

Blackwell

B200, GB200

FP8, NVFP4/MXFP4

FP8

FP4 даёт максимальную плотность

Ключевой момент: FP8 на A100 не даёт выигрыша в скорости. Ampere не имеет аппаратной поддержки формата: FP8 E4M3 требует Hopper и новее, и попытка поднять FP8-модель на A100 нередко заканчивается прямой ошибкой вроде FP8 ops not supported при несовпадении авто-детекта vLLM с конфигом модели. Для парка A100 берите INT4 через AWQ либо W8A8 через SmoothQuant. На Hopper и новее FP8-веса плюс --kv-cache-dtype fp8 — конфигурация по умолчанию. На Blackwell к FP8 добавляются форматы FP4 — NVFP4 и MXFP4, но это уже не загрузка готовых весов, а отдельная работа по оптимизации под production.

Если модель не помещается даже после квантования:

  • Tensor parallelism (TP) — слои режутся между картами. Внутри узла с NVLink масштабируется отлично, через PCIe накладные расходы заметны уже на TP=4. Число KV-голов должно делиться на TP без остатка.
  • Pipeline parallelism (PP) — слои по узлам; оправдан при выходе за пределы одного сервера.
  • Сокращение max_model_len — самый недооценённый рычаг: если 95% запросов укладываются в 8K, объявленный контекст 128K просто расходует память под кэш.

Спекулятивный декодинг снижает TPOT при низкой загрузке, но при высоком числе одновременных запросов уменьшает суммарную пропускную способность.

От профиля нагрузки к конфигурации: обратный расчёт

Исходные данные. Внутренний сервис поддержки на 900 сотрудников: 200 запросов в минуту в пик, средний промпт 4000 токенов, средний ответ 500 токенов, TTFT не хуже 2 с по p95, модель 32B.

Шаг 1. Поток токенов. 200 запросов/мин = 3,33 запроса/с. Вход: 3,33 × 4000 ≈ 13 300 токенов/с. Выход: ≈ 1670 токенов/с.

Шаг 2. Производительность карты. Для 32B в FP8 на H100: предзаполнение ~9000–11 000 токенов/с, декодирование при пакете 32 — ~900–1100 токенов/с.

Шаг 3. Потребность. Предзаполнение: 13 300 ÷ 10 000 ≈ 1,33 карты. Декодирование: 1670 ÷ 1000 ≈ 1,67. Фазы делят одну карту, суммируем: ~3 карты при полной загрузке.

Шаг 4. Запас. На 100% работать нельзя: очередь растёт нелинейно, TTFT при загрузке выше 80% резко возрастает. Целевая загрузка 65–70% в пик даёт 4 карты H100 плюс возможность вывести одну на обновление модели.

Шаг 5. Проверка TTFT. Промпт 4000 токенов при порционном предзаполнении и загрузке 70% даёт TTFT 0,6–1,2 с — в требование укладываемся. При промпте 32K предзаполнение занимало бы 3+ секунды.

Оценку обязательно проверяют замером — расчёт даёт стартовую точку, а не финальную конфигурацию.

Ключевые параметры запуска vLLM и их влияние на память и задержку

Параметр

Что делает

На что влияет

Практика

--gpu-memory-utilization

Доля VRAM под процесс

Объём KV-кэша

0,90–0,92 на выделенной карте; 0,85 при соседях

--max-model-len

Максимальный контекст

Память под кэш

По p99 реальных промптов, не по паспорту

--max-num-seqs

Потолок последовательностей

Пропускная способность и TPOT

64–256; выше — растёт задержка потока

--max-num-batched-tokens

Токенов на шаг планировщика

Баланс предзаполнения и декодирования

8192–16384

--kv-cache-dtype fp8

Квантование кэша

Вдвое больше одновременных запросов

Hopper и новее — по умолчанию

--enable-prefix-caching

Переиспользование префиксов

TTFT на общих промптах

Критично для RAG и агентов

--tensor-parallel-size

Число карт под модель

Размещение в памяти, задержка

Минимально достаточное

Запуск для нашего примера:

vllm serve /models/qwen3-32b-fp8 \
  --served-model-name support-llm \
  --max-model-len 8192 \
  --max-num-seqs 128 \
  --max-num-batched-tokens 8192 \
  --gpu-memory-utilization 0.92 \
  --kv-cache-dtype fp8 \
  --tensor-parallel-size 1 \
  --port 8000

vLLM резервирует долю от общего объёма карты, а не от свободного: если на ней уже запущен второй процесс, значение 0,95 гарантированно приведёт к падению по памяти.

Покупка сервера с четырьмя H100 окупается только при круглосуточной загрузке выше 60–70%, а на старте профиль нагрузки обычно неизвестен. Для этапа, когда нужно проверить гипотезу на своих данных и снять реальные метрики, подойдёт LLM-платформа от Cloud4U — облачный сервис для развёртывания и дообучения открытых моделей на GPU-инфраструктуре провайдера. Конфигурацию можно менять по итогам замеров — что и требуется, пока подбираются max_num_seqs и число карт.

Процедура нагрузочного теста: профиль, метрики, критерий остановки

Тест нужен для одного: найти максимальное число одновременных запросов, при котором сервис укладывается в SLA. Встроенный vllm bench serve покрывает 90% задач — он сохраняет JSON с процентилями по ttft, tpot, itl, e2el. Синтетические равномерные промпты дают завышенные цифры — выгрузите 500–1000 реальных запросов из журналов и подайте как есть; полезно прогнать несколько профилей: короткий чат, длинный промпт, агентский цикл, высокая параллельная нагрузка и прогретый кэш. Прогрев 100 запросов в отброс, ступени по числу одновременных запросов 1, 4, 8, 16, 32, 64, 128 по 300 завершённых запросов на каждой; снимаются TTFT, TPOT, ITL, E2EL по p50/p95/p99, пропускная способность, vllm:gpu_cache_usage_perc и vllm:num_preemptions_total.

Критерий остановки — первая ступень, где p95 TTFT превышает SLA, p95 ITL превышает 50 мс на токен, пропускная способность выходит на плато или появляются вытеснения. Рабочая точка — предыдущая ступень минус 20–25%.

Одновременные запросы

TTFT p95, с

ITL p95, мс

Пропускная способность, ток/с

Вытеснения

4

0,42

14

210

нет

16

0,68

19

640

нет

32

1,10

27

980

нет

64

2,35

44

1120

нет

128

6,80

91

1150

4,2% запросов

Видно плато между 64 и 128 и точку срыва по TTFT. Рабочая точка — около 32–40 одновременных запросов на карту.

Стоимость токена: от рублей в час к рублям за миллион токенов

Аренда GPU считается в рублях за час, внешние API — за миллион токенов. Базовая формула:

Цена за 1M токенов = (стоимость часа аренды ÷ 3600) × 1 000 000 ÷ пропускная способность в токенах/с

Знаменатель берётся из нагрузочного теста, не из паспорта карты.

Почему входные и выходные токены стоят по-разному

Входной токен обрабатывается на этапе предзаполнения, где карта обрабатывает тысячи токенов в секунду; выходной рождается на этапе декодирования, где на каждый шаг перечитываются все веса. Разница в удельной себестоимости — порядок и больше, поэтому внешние провайдеры берут за выход в 3–5 раз дороже.

Допущения: одна H100 80 ГБ, ~250 000 ₽/мес (~342 ₽/час), модель 32B в FP8, рабочая точка — 32 одновременных запроса, 980 токенов/с суммарно. При профиле 4000/500 на предзаполнение уходит ~40% времени карты, на декодирование ~60%.

Показатель

Входные токены

Выходные токены

Доля стоимости часа

137 ₽/час (40%)

205 ₽/час (60%)

Обработано за час при загрузке 100%

48,0 млн

3,5 млн

Стоимость 1M токенов, загрузка 100%

2,9 ₽

58,6 ₽

Стоимость 1M токенов, загрузка 65%

4,4 ₽

90,2 ₽

Стоимость 1M токенов, загрузка 30%

9,5 ₽

195,3 ₽

Цена токена — функция утилизации, а не карты: простаивающая ночью карта делает дневной токен вдвое дороже. Цена падает с ростом числа одновременных запросов и выходит на плато: при 4 — около 270 ₽ за миллион выходных, при 32 — уже 59 ₽, при 64 прирост всего 14%. Неравномерный дневной трафик — главный источник удорожания: корпоративный сервис работает с 9 до 19, средняя суточная загрузка выходит 25–35%. Рычаги — загружать ночные простои пакетными задачами, собирать несколько сервисов на одну карту, масштабировать реплики по расписанию.

Собственный инференс или внешний API: расчёт точки перехода

Вариант А — внешний API по 15 ₽ за 1M входных и 60 ₽ за 1M выходных. Вариант Б — свой инференс на 4 картах H100. Профиль тот же: 200 запросов/мин в пик, ~10 рабочих часов, 250 рабочих дней.

Статья затрат за 12 месяцев

Внешний API

Свой инференс на vLLM

Аренда GPU (4×H100, круглосуточно)

—

12 000 000 ₽

Оплата токенов (30,0 млрд входных, 3,75 млрд выходных)

675 000 ₽

—

Внедрение и настройка (~120 ч MLOps-инженера)

90 000 ₽

480 000 ₽

Сопровождение (0,15 и 0,4 ставки инженера)

396 000 ₽

1 056 000 ₽

Мониторинг и журналирование

36 000 ₽

96 000 ₽

Проверка качества и регрессионные прогоны

60 000 ₽

180 000 ₽

Итого за год

1 257 000 ₽

13 812 000 ₽

Точка перехода: дополнительные затраты своего контура — около 12,7 млн ₽ в год. При соотношении 8 входных к 1 выходному на один миллион выходных токенов приходится 8 млн входных, то есть 180 ₽. Делим: ~560 млрд входных и ~70 млрд выходных токенов в год — примерно двадцатикратный рост относительно нашего профиля, то есть среднесуточная загрузка карт выше 55–60%. До этой отметки экономических причин нет, но есть неэкономические: персональные данные или коммерческая тайна в промптах, закрытый контур без выхода в интернет, дообучение на своих данных, отраслевые требования к размещению данных в РФ. Суммы ориентировочны: отклонение до ±25% нормально.

Маршрутизация запросов между моделями как рычаг снижения расходов на LLM

Всё предыдущее оптимизировало один экземпляр vLLM с одной моделью. Но не каждый запрос требует самой крупной модели — и здесь появляется отдельный рычаг снижения расходов на LLM: маршрутизация LLM. Перед несколькими моделями ставится LLM-роутер (его же называют AI-роутером или LLM router), который для каждого запроса делает выбор модели: простые обращения уводит на маленькую или сильнее квантованную модель, сложные оставляет крупной. Простые запросы обходятся кратно дешевле, а на трудных качество не снижается.

Роль единой точки входа берёт на себя LLM gateway — шлюз, который централизует ключи, лимиты частоты, учёт затрат по моделям и запасной маршрут при отказе. Он же собирает статистику, по которой видно, какую долю трафика реально можно увести на дешёвую модель. Цена подхода — дополнительная задержка на классификацию запроса и необходимость проверять качество на каждой модели маршрута: экономия оправдана, когда доля простых запросов велика, а разброс в стоимости моделей — кратный.

Эксплуатация: качество, мониторинг и безопасность контура

Проверка деградации качества после квантования на своих данных

Чужие тесты общего назначения вашу задачу не описывают. 200–300 примеров хватает, чтобы выявить деградацию больше 3–4 п.п.; набор должен повторять реальное распределение запросов. Мерить: для классификации и извлечения сущностей — точность, полноту, F1 против эталона; для генерации по базе знаний — долю фактических ошибок при экспертной проверке; для вызова инструментов — долю корректных вызовов.

BF16 и квантованную версию поднимаем рядом, прогоняем один набор с temperature=0 и фиксированным seed. Порог отката: до 1,5 п.п. — норма; 1,5–3 п.п. — смотрим, на каких категориях упало качество, часто устраняется правкой промпта; больше 3 п.п. — откат на менее агрессивный формат: INT4 → FP8 → BF16. Отдельно проверьте длинные генерации: квантование чаще проявляется на хвостах — модель уходит в повторы после 1500–2000 токенов.

Мониторинг, закрытый контур и изоляция арендаторов

vLLM отдаёт метрики Prometheus на /metrics. Минимум для дежурной панели:

  • vllm:num_requests_running и vllm:num_requests_waiting — растущая очередь при неполном пакете означает нехватку памяти под кэш, а не вычислений;
  • vllm:gpu_cache_usage_perc — использование KV-кэша в долях единицы; устойчиво выше 90% предвещает вытеснения;
  • vllm:num_preemptions_total — любой ненулевой рост требует разбора.

Оповещения по существу: p95 TTFT выше SLA пять минут подряд; очередь больше max_num_seqs десять минут; появление вытеснений; заполнение кэша выше 95%.

При переполнении кэша планировщик вытесняет последовательности: их кэш сбрасывается в RAM (--swap-space) либо пересчитывается заново. Сервис не падает, но p99 уходит в разы выше p50. Лечится снижением max_num_seqs, сокращением max_model_len, квантованием кэша или добавлением карты.

Холодный старт. Загрузка весов 32B с локального NVMe — 40–90 секунд, компиляция и захват графов — ещё 1–3 минуты: реплика поднимается дольше, чем длится всплеск. Помогает кэш компиляции на постоянном томе, прогретый образ с весами внутри и одна тёплая реплика в резерве.

Закрытый контур без Hugging Face. Скачиваем полный репозиторий модели, включая config.json, файлы токенизатора, generation_config.json и все шарды весов (неполная выкладка — самая частая причина падения старта), на узле инференса указываем локальный путь и выставляем HF_HUB_OFFLINE=1 и TRANSFORMERS_OFFLINE=1 — иначе библиотека будет ждать до истечения тайм-аута.

Изоляция арендаторов. Общий префиксный кэш — известный канал утечки: время ответа зависит от того, лежит ли чужой префикс в кэше, и на этом строятся timing side-channel атаки на serving-системы, вплоть до восстановления фрагментов чужих промптов подбором. В исследованиях по снижению риска предлагают разделение кэша между арендаторами — на практике достаточно не смешивать арендаторов в одном процессе vLLM либо разделять кэш по арендаторам. Дальше: ограничение частоты и потолок токенов на арендатора на шлюзе; журналирование промптов выключить (--disable-log-requests), иначе персональные данные сохраняются в журналах; отдельные ключи API и разграничение по моделям на шлюзе — у vLLM нет полноценной модели прав доступа.

152-ФЗ и промпты. ФИО клиента, номер договора или медицинские данные в запросе делают промпт обработкой персональных данных: инференс размещается в аттестованном сегменте на территории РФ, промпты и ответы либо не сохраняются, либо хранятся в защищённом контуре, отправка во внешний API без правовых оснований недопустима.

Главные выводы

Механика serving LLM определяет, сколько запросов карта обслужит физически: декодирование упирается в пропускную способность памяти, KV-кэш ограничивает число одновременных запросов сильнее размера весов, непрерывное пакетирование с PagedAttention превращает простаивающие такты в токены. Расчёт памяти и параметры vLLM переводят механику в конфигурацию — число карт, контекст, квантование под конкретное поколение GPU. И только замер под своей нагрузкой даёт стоимость токена: аренда карты, делённая на пропускную способность в рабочей точке, с поправкой на суточную утилизацию.

Собственный инференс дешевле внешнего API только при высокой и ровной загрузке — при корпоративном профиле с дневным пиком карта половину суток простаивает. Поэтому решение о своём контуре чаще принимается по требованиям к данным: персональные данные в промптах, закрытый сегмент, дообучение на внутренних документах, размещение на территории РФ. Если эти требования есть — считайте конфигурацию от профиля нагрузки, закладывайте запас на пики, проверяйте деградацию квантования на своих примерах и снимайте метрики с первого дня.

Вверх!