Запустить открытую 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 и их влияние на память и задержку
|
Параметр |
Что делает |
На что влияет |
Практика |
|---|---|---|---|
|
|
Доля VRAM под процесс |
Объём KV-кэша |
0,90–0,92 на выделенной карте; 0,85 при соседях |
|
|
Максимальный контекст |
Память под кэш |
По p99 реальных промптов, не по паспорту |
|
|
Потолок последовательностей |
Пропускная способность и TPOT |
64–256; выше — растёт задержка потока |
|
|
Токенов на шаг планировщика |
Баланс предзаполнения и декодирования |
8192–16384 |
|
|
Квантование кэша |
Вдвое больше одновременных запросов |
Hopper и новее — по умолчанию |
|
|
Переиспользование префиксов |
TTFT на общих промптах |
Критично для RAG и агентов |
|
|
Число карт под модель |
Размещение в памяти, задержка |
Минимально достаточное |
Запуск для нашего примера:
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 только при высокой и ровной загрузке — при корпоративном профиле с дневным пиком карта половину суток простаивает. Поэтому решение о своём контуре чаще принимается по требованиям к данным: персональные данные в промптах, закрытый сегмент, дообучение на внутренних документах, размещение на территории РФ. Если эти требования есть — считайте конфигурацию от профиля нагрузки, закладывайте запас на пики, проверяйте деградацию квантования на своих примерах и снимайте метрики с первого дня.
