Локальный инференс · планирование ёмкости
Планирование памяти для локальной LLM: веса, KV-кэш и параллельные запросы
Файл модели помещается в видеопамять — но сервер всё равно завершается с ошибкой при длинном диалоге или наплыве пользователей. Причина обычно в том, что размер весов приняли за полное потребление памяти.
Из чего складывается память инференса
Во время генерации сервер хранит не только веса, но и KV-кэш — представления ключей и значений внимания для уже обработанных токенов. Кэш ускоряет продолжение последовательности, однако растёт вместе с контекстом и обычно создаётся отдельно для каждого активного запроса.
Для предварительного планирования используйте модель:
M_total = M_weights
+ M_kv
+ M_runtime
+ M_requests
+ M_reserve
M_weights- Веса модели после квантования, метаданные формата и возможные дополнительные тензоры.
M_kv- KV-кэш всех одновременно обслуживаемых последовательностей.
M_runtime- Рабочие буферы, CUDA-контекст, графы исполнения, временные тензоры и память самого движка.
M_requests- Буферы токенизации, логитов, семплирования и прочие расходы на запросы.
M_reserve- Запас против фрагментации, отклонений профиля и кратковременных пиков.
Шаг 1. Оцените память весов
Если известны число параметров P и среднее число бит на параметр b, теоретическая нижняя граница равна:
M_weights_raw = P × b / 8
Для перевода байтов в GiB делите на 1024³. Например, гипотетическая модель с 8 миллиардами параметров при эффективных 4,5 бита на параметр требует:
8 000 000 000 × 4.5 / 8 / 1024³ ≈ 4.19 GiB
Это именно пример расчёта, а не характеристика конкретной модели. У квантованных файлов присутствуют масштабы, служебные блоки и метаданные; часть тензоров может храниться в более высокой точности. Поэтому для уже загруженного артефакта лучше начинать с фактического размера файла, а затем сверять его с потреблением процесса.
Безопасно посмотреть размер локального файла, ничего не изменяя:
du -h /path/to/model-file
stat --format='%s bytes' /path/to/model-file
Если модель состоит из нескольких шардов, учитывайте все файлы весов, а не только первый. Не суммируйте README, токенизатор и конфигурацию с VRAM автоматически: они могут оставаться в оперативной памяти.
Шаг 2. Рассчитайте KV-кэш на токен
Для обычного декодерного трансформера приближённый размер KV-кэша:
M_kv = L × T × 2 × H_kv × D_head × B × S
Где:
L— число слоёв;T— число занятых токенов в последовательности;2— отдельные тензоры K и V;H_kv— число KV-голов;D_head— размерность одной головы;B— байтов на элемент KV-кэша;S— число одновременно резидентных последовательностей.
При multi-head attention число KV-голов обычно совпадает с числом голов внимания. При grouped-query или multi-query attention оно меньше. Подстановка общего числа голов вместо H_kv может значительно завысить оценку, а обратная ошибка — привести к нехватке памяти.
Воспроизводимый пример
Возьмём условную архитектуру со следующими параметрами:
| Параметр | Значение примера |
|---|---|
Слои L | 32 |
KV-головы H_kv | 8 |
Размер головы D_head | 128 |
| Формат KV | FP16, то есть 2 байта |
Контекст T | 16 384 токена |
Последовательности S | 4 |
Сначала найдём стоимость одного токена одной последовательности:
32 × 2 × 8 × 128 × 2 = 131 072 bytes
131 072 / 1024² = 0.125 MiB на токен
Затем умножим на длину и параллелизм:
0.125 MiB × 16 384 × 4 = 8 192 MiB = 8 GiB
Таким образом, в этом условном примере один только полностью занятый KV-кэш требует 8 GiB. При восьми последовательностях он потребовал бы 16 GiB, если движок действительно резервирует или заполняет такой объём и не применяет сжатие либо совместное использование префиксов.
Шаг 3. Правильно задайте параллелизм
«Четыре пользователя» не всегда означают четыре активные последовательности. Пользователь может держать соединение открытым, но ничего не генерировать; один запрос, напротив, может создавать несколько лучей поиска или кандидатов. Для ёмкости важен максимум одновременно резидентных последовательностей в планировщике.
Простая верхняя оценка при одинаковом лимите контекста:
S = active_requests × sequences_per_request
T = max_input_tokens + max_output_tokens
Не забывайте о выходных токенах. Запрос с промптом на 12 000 токенов и лимитом ответа 4 000 способен занять 16 000 позиций, даже если типичный ответ короче.
Если длины запросов различаются, точнее суммировать их отдельно:
M_kv = bytes_per_token × (T₁ + T₂ + ... + Tₛ)
При continuous batching активный состав пакета меняется во времени. Поэтому планируйте либо жёсткий лимит суммарных токенов, либо худший допустимый набор запросов — не среднее значение из спокойного периода.
Шаг 4. Выполните расчёт без сторонних зависимостей
Следующая команда только вычисляет числа и печатает результат. Она не загружает модель, не обращается к сети и не изменяет файлы. Значения относятся к условному примеру выше; замените их параметрами из конфигурации своей модели и настройками сервера.
python3 - <<'PY'
layers = 32
kv_heads = 8
head_dim = 128
kv_bytes = 2
context_tokens = 16_384
sequences = 4
weights_gib = 4.8
runtime_gib = 1.5
request_overhead_gib = 0.3
reserve_fraction = 0.15
bytes_per_token = layers * 2 * kv_heads * head_dim * kv_bytes
kv_gib = bytes_per_token * context_tokens * sequences / 1024**3
subtotal_gib = weights_gib + kv_gib + runtime_gib + request_overhead_gib
required_gib = subtotal_gib * (1 + reserve_fraction)
print(f"KV на токен и последовательность: {bytes_per_token:,} bytes")
print(f"KV-кэш: {kv_gib:.2f} GiB")
print(f"До резерва: {subtotal_gib:.2f} GiB")
print(f"Требуется с резервом: {required_gib:.2f} GiB")
PY
Для приведённых значений ожидаемый результат:
KV на токен и последовательность: 131,072 bytes
KV-кэш: 8.00 GiB
До резерва: 14.60 GiB
Требуется с резервом: 16.79 GiB
weights_gib, runtime_gib и request_overhead_gib здесь являются допущениями примера. Их нельзя переносить на другую модель или движок без измерения.
Шаг 5. Добавьте расходы движка и резерв
Универсальной константы для M_runtime нет. На неё влияют backend, размер batch, выбранные ядра, CUDA Graphs, максимальная длина последовательности, speculative decoding, распределение слоёв между CPU и GPU и реализация менеджера памяти.
Рабочий процесс состоит из двух проходов:
- Получите расчётную нижнюю границу для весов и KV-кэша.
- Запустите целевой сервер с заданными лимитами и измерьте постоянные и пиковые накладные расходы.
Резерв в 10–20% можно использовать как начальное инженерное допущение, но это не гарантия. Для нестабильной нагрузки, общего GPU или плохо предсказуемых размеров batch нужен больший запас либо более строгие лимиты.
Проверка результата на целевой системе
Тест должен повторять будущую конфигурацию: тот же файл модели, формат KV-кэша, максимальный контекст, batch-политика и число параллельных последовательностей. Синтетические промпты допустимы, если их назначение явно обозначено и они не содержат секретов или пользовательских данных.
- Зафиксируйте свободную память до запуска.
- Запустите сервер с одним коротким запросом и измерьте базовую стоимость загрузки.
- Подайте один запрос почти на максимальную разрешённую длину.
- Повторите его до целевого числа параллельных последовательностей.
- Дождитесь генерации выходных токенов и зафиксируйте пик, а не только состояние сразу после загрузки.
- Повторите цикл несколько раз, проверяя отсутствие OOM и роста памяти между завершёнными запросами.
Для NVIDIA GPU доступна безопасная команда наблюдения:
nvidia-smi --query-compute-apps=pid,process_name,used_memory \
--format=csv -l 1
Она только читает телеметрию. Если GPU разделяется с другими процессами, дополнительно наблюдайте общую память:
nvidia-smi --query-gpu=index,memory.total,memory.used,memory.free \
--format=csv -l 1
Критерий успеха следует сформулировать заранее: целевой параллелизм обслуживается при максимальном разрешённом числе токенов, процесс не завершается с OOM, а измеренный пик остаётся ниже доступной памяти с выбранным резервом.
Лимиты, которые превращают расчёт в гарантию сервиса
Расчёт бесполезен, если API позволяет любому запросу обойти принятые ограничения. Зафиксируйте в конфигурации сервера:
- максимальную сумму входных и выходных токенов;
- максимальное число активных последовательностей;
- лимит суммарных токенов активного batch;
- формат KV-кэша;
- поведение очереди при заполнении лимита;
- тайм-ауты и отмену запросов после отключения клиента.
Предпочтительнее поставить лишний запрос в ограниченную очередь или вернуть контролируемую ошибку перегрузки, чем принять его и завершить весь процесс из-за нехватки памяти.
Абстрактный пример политики, не привязанный к конкретному движку:
model:
max_total_tokens_per_sequence: 16384
kv_cache_dtype: fp16
scheduler:
max_active_sequences: 4
max_batched_tokens: 65536
overload_policy: queue
queue_limit: 16
Названия полей иллюстративны. Не копируйте этот фрагмент в рабочий сервер без сопоставления с документацией вашего движка.
Типовые ошибки
Считать только размер файла
Файл описывает в основном веса. KV-кэш, рабочие буферы и резерв появляются во время исполнения.
Умножать KV-кэш только на batch size
Параметр batch у разных движков может означать число токенов prefill, запросов или последовательностей. В формуле нужно фактическое число одновременно резидентных последовательностей и их занятые токены.
Игнорировать GQA и MQA
Размер KV-кэша определяется числом KV-голов, а не обязательно общим числом голов внимания. Берите параметры из конфигурации архитектуры.
Считать только входной промпт
Генерируемые токены тоже добавляются в кэш. Планируйте сумму максимального входа и максимального ответа.
Путать GB и GiB
Производители и инструменты могут использовать разные единицы. Один GB равен 109 байт, один GiB — 230 байт. В формулах этой статьи используется GiB.
Складывать память нескольких GPU в один непрерывный пул
Тензорный или конвейерный параллелизм распределяет данные по устройствам не всегда равномерно. Успех определяется самым загруженным GPU, а коммуникационные буферы создают дополнительные расходы.
Проверять только загрузку модели
Пиковая память проявляется при prefill длинного контекста, формировании batch или генерации нескольких ответов. Простой модели недостаточно.
Ограничения расчётной модели
Формула KV-кэша хорошо подходит для первичного планирования стандартного декодерного трансформера, но не описывает все реализации. Итог может измениться из-за paged attention, квантования KV, sliding-window attention, гибридных слоёв, offload на CPU, prefix caching, speculative decoding и особенностей выравнивания блоков.
Некоторые движки резервируют кэш заранее до заданной доли VRAM; другие расширяют его по мере нагрузки. В первом случае память может выглядеть занятой ещё до запросов, во втором риск проявляется позже. Общие префиксы иногда сокращают физическое дублирование, но закладывать эту экономию в обязательную ёмкость стоит только после проверки конкретной реализации.
Оперативная память также требует отдельного бюджета. При CPU-offload, memory mapping или загрузке шардов VRAM может быть достаточно, а процесс завершится из-за нехватки RAM либо лимита контейнера.
Итоговый чек-лист
- Возьмите фактический размер весов или рассчитайте нижнюю границу по параметрам и битности.
- Найдите в конфигурации число слоёв, KV-голов и размер головы.
- Уточните реальный формат KV-кэша и байты на элемент.
- Сложите максимальные входные и выходные токены.
- Определите число одновременно резидентных последовательностей.
- Рассчитайте KV-кэш и прибавьте измеренные расходы движка.
- Добавьте резерв и проверьте ограничение отдельно для каждого GPU и для RAM.
- Закрепите лимиты в сервере и воспроизведите худший допустимый профиль нагрузки.
Главная единица планирования — не размер модели, а профиль обслуживания: конкретная архитектура, формат кэша, максимальная сумма токенов и число активных последовательностей. Именно этот профиль нужно рассчитать, ограничить и затем подтвердить измерением.