Logo Craft Homelab Docs Контакты Telegram
Планирование VRAM для локальных LLM: пул движка и геометрия KV-кэша Трендовые github проекты в нашем телеграм канале. Подпишись →
8 сентября 2026 г.

Сколько запросов реально влезает в одну GPU

Когда поднимаешь локальную LLM на своей карте или арендуешь GPU на час, первый вопрос звучит просто: влезет ли модель и сколько параллельных запросов она вытянет. Онлайн-калькуляторы VRAM отвечают на него быстро и почти всегда неточно. Причина в двух местах, где арифметика «веса плюс KV-кэш меньше объёма карты» расходится с тем, как работают современные inference-движки.

Движок резервирует память заранее

Интуиция подсказывает, что движок положит в память веса модели, а остаток будет раздавать под KV-кэш по мере поступления запросов. Так не делает ни один из распространённых движков.

vLLM при старте резервирует крупный непрерывный кусок VRAM под пул и уже внутри него страницами размещает KV-кэш. Долей управляет флаг gpu_memory_utilization, по умолчанию 0.92. В SGLang тот же механизм называется mem_fraction_static (авто, около 0.9), в TensorRT-LLM — kv_cache_free_gpu_mem_fraction. Веса грузятся внутрь этого куска, KV-кэш занимает то, что осталось после весов и служебных расходов. Свободные проценты VRAM за пределами пула под кэш не пойдут никогда.

Отсюда ёмкость KV-пула считается так:

KV-пул = util · VRAM − веса − overhead

Механизм подробно описан в работе про PagedAttention (arXiv:2309.06180): пул выделяется одним куском, чтобы избежать фрагментации и раздавать страницы кэша под запросы дёшево.

Проверить расчёт можно без аренды карты. vLLM при старте печатает строку вида # GPU blocks: N — это и есть фактический размер KV-пула в блоках. Если заранее посчитать ёмкость по формуле выше и перевести в блоки, число должно совпасть с логом старта. На Llama-3-8B, 1×A100, контекст 8192 наивная оценка без учёта util обещает около 60 одновременных запросов, поправка на util даёт 54, живой замер — 55.

Геометрия KV-кэша: GQA, MLA, MoE

Второе место, где калькуляторы промахиваются, — размер KV-кэша на один токен. Он зависит от архитектуры внимания, и в уме его посчитать сложно.

GQA (grouped-query attention). Классический случай, байты на токен:

2 · n_kv · head_dim · L · p

где n_kv — число KV-голов, head_dim — размерность головы, L — число слоёв, p — байт на элемент (2 для fp16).

MLA (multi-head latent attention). У DeepSeek внимание сжато в латентное представление, и формула другая:

(d_c + d_rope) · L · p

где d_c — размерность сжатого латента, d_rope — RoPE-часть.

Разница огромная. Для DeepSeek-V2-Lite из его config.json:

  • MLA, корректно: (512 + 64) · 27 · 2 = 31 104 байт на токен;
  • наивная GQA-формула с head_dim = 128: 221 184 байт на токен — завышение в 7,1 раза;
  • та же формула с head_dim = 192: 331 776 байт на токен — завышение в 10,7 раза.

Калькулятор, который трактует MLA как обычное внимание, завышает KV-кэш в 7–11 раз в зависимости от того, какой head_dim он подставит. Промах уводит в сторону: кажется, что модель требует почти на порядок больше карт, и от неё отказываются или берут стойку вместо одной GPU. Сжатие KV в MLA задокументировано в статье про DeepSeek-V2 (arXiv:2405.04434).

MoE. У DeepSeek-V2-Lite общий объём весов — 16B параметров, активных на токен — 2B. Память платится за все 16B, скорость декодинга считается по 2B. Эти две цифры легко перепутать при планировании.

Как считать перед арендой GPU

Порядок действий, который даёт оценку в пределах нескольких процентов от факта:

  1. Скачать с HuggingFace только config.json и индекс safetensors — это пара килобайт, GPU не нужен.
  2. По конфигу определить архитектуру внимания: GQA, MLA или гибрид, а также MoE.
  3. Посчитать байты на токен по формуле под конкретную архитектуру.
  4. Оценить overhead: активации плюс CUDA-графы. Это эмпирическая величина, коэффициент обычно в диапазоне 1,5–2,2 от базовой оценки активаций; её приходится калибровать по логам конкретного движка.
  5. Подставить всё в KV-пул = util · VRAM − веса − overhead и поделить на размер KV-кэша одного запроса при нужной длине контекста.

Квантование весов и KV-кэша задаётся раздельно. Флаг --kv-cache-dtype fp8 вдвое увеличивает ёмкость пула по числу токенов. Комбинация fp4-весов с fp16-кэшем встречается чаще, чем осознанно выбирается, — стоит проверять оба dtype отдельно.

Чего такой расчёт не даёт

  • Скорость. TTFT и утилизацию вычислителя (MFU) чисто из конфига получить нельзя, нужен замер на железе. Пропускная способность памяти при декоде (MBU) меряется стабильно, ей верить можно.
  • Смешанная нагрузка. Запросы разной длины с ограничением --max-num-seqs считаются хуже, чем однородный батч; здесь оценка ёмкости будет грубее.
  • Несколько GPU. При тензорном и конвейерном параллелизме активации шардятся между картами, а CUDA-графы остаются на каждой карте целиком.
  • Спекулятивный декодинг. Он переводит decode из режима memory-bound в compute-bound и сдвигает точку баланса, из-за чего профиль потребления памяти меняется.
  • Нелинейный контекст. Mamba/SSM, sliding-window и гибриды ломают предположение, что KV-кэш растёт линейно по длине контекста.

Практический чек-лист

  • Не полагаться на «веса + KV < VRAM»: движок заберёт под пул фиксированную долю (gpu_memory_utilization и аналоги) при старте.
  • Считать ёмкость через util · VRAM − веса − overhead, а не через полный объём карты.
  • Определять архитектуру внимания перед расчётом KV: MLA даёт кратно меньший кэш, чем подсказывает GQA-формула.
  • Для MoE держать в голове две разные цифры — параметры общие и активные.
  • Сверять расчёт со строкой # GPU blocks из лога старта vLLM: если числа расходятся, ошибка в оценке весов или overhead.
  • Ловили OOM там, где по расчёту всё влезало? Почти всегда это недоучтённый overhead или неверно определённая архитектура внимания.