KV cache — это буфер в GPU-памяти, где хранятся уже вычисленные проекции Key и Value для предыдущих токенов. Без него autoregressive генерация была бы квадратичной по вычислениям: на каждом шаге пришлось бы заново прогонять весь контекст через все слои attention.
В transformer-слое с self-attention каждый токен проецируется в три вектора: Query (Q), Key (K) и Value (V). Attention score считается как softmax(QK^T / √d), а выход — как взвешенная сумма V.
При генерации token-by-token на шаге t модель обрабатывает только один новый токен, но для attention ему нужны K и V всех предыдущих позиций (0..t−1). Если не кэшировать, на каждом шаге пришлось бы пересчитывать K и V для всей последовательности — суммарно O(n²) операций на генерацию из n токенов.
KV cache устраняет эту избыточность: после первого прогона (prefill) K и V всех токенов сохраняются в памяти. На каждом следующем шаге (decode) вычисляются только K и V нового токена, дописываются в буфер, и attention считается по всему накопленному кэшу.
Две фазы инференса принципиально различаются по характеру нагрузки:
Именно поэтому на этапе decode скорость генерации падает с ростом длины контекста: объём данных, которые нужно прочитать из VRAM на каждом шаге, растёт линейно.
Разница между Llama-2 7B и Llama-3 8B при сопоставимом числе параметров — четырёхкратная. Причина — переход от full MHA (32 KV-головы) к GQA (8 KV-голов). Это показывает, что архитектурный выбор влияет на потребление памяти сильнее, чем размер модели.
Для Llama-3 8B: 2 × 32 × 8 × 128 × 4096 × 2 = 536 870 912 байт ≈ 512 МБ. Если сервер выделяет заметно больше — вероятно, используется MHA-вариант или кэш хранится в FP32.
При batch-обработке объём кэша умножается на число одновременных запросов. Для 32 параллельных последовательностей к Llama-3 8B с контекстом 4096 токенов нужно 32 × 512 МБ = 16 ГБ только на KV cache — сопоставимо с весом самой модели в FP16.
Вторая проблема — фрагментация. Запросы имеют разную длину, и заранее выделить непрерывный буфер максимального размера для каждого означает потратить память впустую. В наивной реализации фрагментация может достигать 60–80 % выделенной памяти.
Третья проблема — непредсказуемость длины генерации. Сервер не знает заранее, сколько токенов сгенерирует модель, поэтому вынужден резервировать память под максимальную длину контекста или рисковать OOM.
Multi-Head Attention (MHA) — каждая Q-голова имеет свою пару K/V. Максимальное качество, максимальный расход памяти.
Multi-Query Attention (MQA) — все Q-головы делят одну пару K/V. Экономия памяти радикальная (в 32 раза для 32 голов), но качество может деградировать на задачах, требующих точного attention.
Grouped-Query Attention (GQA) — компромисс: Q-головы разбиты на группы, каждая группа делит одну пару K/V. Llama-3 использует 8 KV-голов на 32 Q-головы (группы по 4). Потеря качества минимальна, экономия памяти четырёхкратная.
Выбор между MHA, GQA и MQA делается на этапе обучения модели и не может быть изменён при инференсе. Однако при выборе модели для развёртывания это один из ключевых параметров, на который стоит обращать внимание.
PagedAttention, реализованный в vLLM, решает проблему фрагментации. Идея заимствована из виртуальной памяти операционных систем:
Это позволяет почти полностью устранить фрагментацию и увеличить число одновременно обслуживаемых запросов. vLLM также поддерживает continuous batching — новые запросы добавляются в batch по мере освобождения ресурсов, а не ждут завершения всего batch.
Дополнительно vLLM поддерживает chunked prefill: длинный входной промпт разбивается на части, которые обрабатываются вперемешку с decode-шагами других запросов. Это снижает пиковое потребление памяти и уменьшает задержку для коротких запросов, которые иначе ждали бы завершения длинного prefill.
Если несколько запросов имеют общий префикс (системный промпт, few-shot примеры, контекст RAG), KV cache для этого префикса вычисляется один раз и переиспользуется. В vLLM эта функция включается автоматически при обнаружении совпадающих префиксов.
Экономия особенно заметна в сценариях, где системный промпт занимает сотни или тысячи токенов, а пользовательская часть короткая. Например, при системном промпте в 2000 токенов и 100 параллельных запросах к Llama-3 8B prefix caching экономит около 100 × 250 МБ = 25 ГБ, которые иначе пришлось бы выделять под дублирующие вычисления и память.
Подробности — в Prefix Caching в vLLM: как ускорить повторяющиеся запросы и экономить KV cache.
Хранение K и V в FP8 вместо FP16 сокращает объём кэша вдвое при минимальной потере качества. vLLM поддерживает FP8-квантизацию KV cache. INT8 также возможен, но требует калибровки для сохранения точности attention scores.
Это ортогонально квантизации весов модели Квантизация LLM: AWQ, GPTQ и FP8 — как уменьшить память без лишней потери качества: можно квантизовать веса в INT4, а KV cache держать в FP8, или наоборот. На практике комбинация INT4-весов и FP8-кэша позволяет разместить модель с длинным контекстом на GPU с ограниченной памятью.
Важно различать два уровня квантизации:
Некоторые модели (Mistral, Gemma 2) ограничивают attention последними W токенами. KV cache при этом хранит только W позиций, а не всю последовательность. Для Mistral 7B окно составляет 4096 токенов: даже если контекст модели поддерживает 32K, кэш растёт только до 4096.
Ограничение: модель не может «вспомнить» информацию за пределами окна без дополнительных механизмов (например, RAG Что такое RAG: как подключить собственные данные к LLM и не утонуть в галлюцинациях).
Методы выборочного удаления записей из KV cache (H2O, Scissorhands, StreamingLLM) оставляют только наиболее «важные» токены — те, на которые attention обращается чаще всего. Это позволяет обрабатывать последовательности длиннее доступной памяти, но с потерей информации.
StreamingLLM, например, сохраняет несколько начальных токенов (attention sinks) плюс последние W позиций. Это даёт стабильную генерацию на бесконечном потоке, но не подходит для задач, где нужен доступ к произвольной позиции в истории.
Механика: зачем нужен кэш
В transformer-слое с self-attention каждый токен проецируется в три вектора: Query (Q), Key (K) и Value (V). Attention score считается как softmax(QK^T / √d), а выход — как взвешенная сумма V.
При генерации token-by-token на шаге t модель обрабатывает только один новый токен, но для attention ему нужны K и V всех предыдущих позиций (0..t−1). Если не кэшировать, на каждом шаге пришлось бы пересчитывать K и V для всей последовательности — суммарно O(n²) операций на генерацию из n токенов.
KV cache устраняет эту избыточность: после первого прогона (prefill) K и V всех токенов сохраняются в памяти. На каждом следующем шаге (decode) вычисляются только K и V нового токена, дописываются в буфер, и attention считается по всему накопленному кэшу.
Две фазы инференса принципиально различаются по характеру нагрузки:
- Prefill — обработка всего входного промпта за один проход. Вычисления ограничены производительностью GPU (compute-bound), память кэша заполняется целиком.
- Decode — генерация по одному токену за шаг. Каждый шаг требует чтения всего накопленного KV cache из памяти, поэтому фаза ограничена пропускной способностью памяти (memory-bound).
Именно поэтому на этапе decode скорость генерации падает с ростом длины контекста: объём данных, которые нужно прочитать из VRAM на каждом шаге, растёт линейно.
Формула размера
Код:
KV_cache_bytes = 2 × num_layers × num_kv_heads × head_dim × seq_len × bytes_per_element
2— хранятся и K, и V
num_layers— число transformer-слоёв
num_kv_heads— число голов K/V (при GQA/MQA меньше числа Q-голов)
head_dim— размерность одной головы (обычно 128)
seq_len— длина последовательности включая сгенерированные токены
bytes_per_element— 2 для FP16/BF16, 1 для FP8/INT8
Примеры расчёта
| Модель | Слои | KV-головы | head_dim | Dtype | На токен | 4096 токенов |
|---|---|---|---|---|---|---|
| Llama-3 8B | 32 | 8 (GQA) | 128 | FP16 | 128 КБ | ~512 МБ |
| Llama-2 7B | 32 | 32 (MHA) | 128 | FP16 | 512 КБ | ~2 ГБ |
| Llama-3 70B | 80 | 8 (GQA) | 128 | FP16 | 320 КБ | ~1,25 ГБ |
Разница между Llama-2 7B и Llama-3 8B при сопоставимом числе параметров — четырёхкратная. Причина — переход от full MHA (32 KV-головы) к GQA (8 KV-голов). Это показывает, что архитектурный выбор влияет на потребление памяти сильнее, чем размер модели.
Проверка на практике
Для Llama-3 8B: 2 × 32 × 8 × 128 × 4096 × 2 = 536 870 912 байт ≈ 512 МБ. Если сервер выделяет заметно больше — вероятно, используется MHA-вариант или кэш хранится в FP32.
Почему KV cache становится узким местом
При batch-обработке объём кэша умножается на число одновременных запросов. Для 32 параллельных последовательностей к Llama-3 8B с контекстом 4096 токенов нужно 32 × 512 МБ = 16 ГБ только на KV cache — сопоставимо с весом самой модели в FP16.
Вторая проблема — фрагментация. Запросы имеют разную длину, и заранее выделить непрерывный буфер максимального размера для каждого означает потратить память впустую. В наивной реализации фрагментация может достигать 60–80 % выделенной памяти.
Третья проблема — непредсказуемость длины генерации. Сервер не знает заранее, сколько токенов сгенерирует модель, поэтому вынужден резервировать память под максимальную длину контекста или рисковать OOM.
Архитектурные методы: GQA и MQA
Multi-Head Attention (MHA) — каждая Q-голова имеет свою пару K/V. Максимальное качество, максимальный расход памяти.
Multi-Query Attention (MQA) — все Q-головы делят одну пару K/V. Экономия памяти радикальная (в 32 раза для 32 голов), но качество может деградировать на задачах, требующих точного attention.
Grouped-Query Attention (GQA) — компромисс: Q-головы разбиты на группы, каждая группа делит одну пару K/V. Llama-3 использует 8 KV-голов на 32 Q-головы (группы по 4). Потеря качества минимальна, экономия памяти четырёхкратная.
Выбор между MHA, GQA и MQA делается на этапе обучения модели и не может быть изменён при инференсе. Однако при выборе модели для развёртывания это один из ключевых параметров, на который стоит обращать внимание.
PagedAttention: управление памятью как в ОС
PagedAttention, реализованный в vLLM, решает проблему фрагментации. Идея заимствована из виртуальной памяти операционных систем:
- KV cache разбивается на блоки фиксированного размера (страницы).
- Каждой последовательности назначается таблица страниц, которая может быть разреженной.
- Физические блоки выделяются по мере роста последовательности, а не заранее.
- Блоки разных последовательностей не обязаны быть смежными в памяти.
Это позволяет почти полностью устранить фрагментацию и увеличить число одновременно обслуживаемых запросов. vLLM также поддерживает continuous batching — новые запросы добавляются в batch по мере освобождения ресурсов, а не ждут завершения всего batch.
Дополнительно vLLM поддерживает chunked prefill: длинный входной промпт разбивается на части, которые обрабатываются вперемешку с decode-шагами других запросов. Это снижает пиковое потребление памяти и уменьшает задержку для коротких запросов, которые иначе ждали бы завершения длинного prefill.
Prefix caching
Если несколько запросов имеют общий префикс (системный промпт, few-shot примеры, контекст RAG), KV cache для этого префикса вычисляется один раз и переиспользуется. В vLLM эта функция включается автоматически при обнаружении совпадающих префиксов.
Экономия особенно заметна в сценариях, где системный промпт занимает сотни или тысячи токенов, а пользовательская часть короткая. Например, при системном промпте в 2000 токенов и 100 параллельных запросах к Llama-3 8B prefix caching экономит около 100 × 250 МБ = 25 ГБ, которые иначе пришлось бы выделять под дублирующие вычисления и память.
Подробности — в Prefix Caching в vLLM: как ускорить повторяющиеся запросы и экономить KV cache.
Квантизация KV cache
Хранение K и V в FP8 вместо FP16 сокращает объём кэша вдвое при минимальной потере качества. vLLM поддерживает FP8-квантизацию KV cache. INT8 также возможен, но требует калибровки для сохранения точности attention scores.
Это ортогонально квантизации весов модели Квантизация LLM: AWQ, GPTQ и FP8 — как уменьшить память без лишней потери качества: можно квантизовать веса в INT4, а KV cache держать в FP8, или наоборот. На практике комбинация INT4-весов и FP8-кэша позволяет разместить модель с длинным контекстом на GPU с ограниченной памятью.
Важно различать два уровня квантизации:
- Квантизация весов уменьшает размер модели и ускоряет загрузку, но не влияет на рост памяти с длиной контекста.
- Квантизация KV cache напрямую снижает скорость роста потребления памяти при увеличении длины последовательности и batch size.
Sliding window attention
Некоторые модели (Mistral, Gemma 2) ограничивают attention последними W токенами. KV cache при этом хранит только W позиций, а не всю последовательность. Для Mistral 7B окно составляет 4096 токенов: даже если контекст модели поддерживает 32K, кэш растёт только до 4096.
Ограничение: модель не может «вспомнить» информацию за пределами окна без дополнительных механизмов (например, RAG Что такое RAG: как подключить собственные данные к LLM и не утонуть в галлюцинациях).
Eviction и компрессия
Методы выборочного удаления записей из KV cache (H2O, Scissorhands, StreamingLLM) оставляют только наиболее «важные» токены — те, на которые attention обращается чаще всего. Это позволяет обрабатывать последовательности длиннее доступной памяти, но с потерей информации.
StreamingLLM, например, сохраняет несколько начальных токенов (attention sinks) плюс последние W позиций. Это даёт стабильную генерацию на бесконечном потоке, но не подходит для задач, где нужен доступ к произвольной позиции в истории.
Сравнение методов оптимизации
| Метод | Уровень применения | Экономия | Потеря качества | Когда применять |
|---|---|---|---|---|
| GQA/MQA | Архитектура модели | До 32× на KV | Минимальная (GQA) | При выборе модели |
| PagedAttention | Сервинг | Устраняет фрагментацию | Нет | Всегда при batch-сервинге |
| Prefix caching | Сервинг | Зависит от общего префикса | Нет | Повторяющиеся системные промпты |
| FP8 KV cache | Сервинг | 2× | Минимальная | Ограниченная VRAM |
| Sliding window | Архитектура модели | Фиксированный потолок | Потеря дальнего контекста | При выборе модели |
| Eviction (H2O и др.) | Сервинг | Настраиваемая | Зависит от порога | Потоковая генерация |
| Chunked prefill | Сервинг | Снижает пик памяти | Нет | Длинные входные промпты |
Типичные ошибки при оценке памяти
- Учёт только весов модели. Модель в FP16 занимает, скажем, 16 ГБ, и кажется, что на GPU с 24 ГБ всё поместится. Но KV cache для batch из 16 запросов с контекстом 4096 добавляет ещё 8 ГБ (для GQA-модели), и итог выходит за пределы доступной памяти.
- Игнорирование batch size. Формула линейна по числу одновременных последовательностей. Удвоение batch удваивает потребление KV cache.
- Предположение о фиксированной длине. Если модель может сгенерировать до 8192 токенов, кэш нужно планировать на эту длину, а не на среднюю.
- Смешивание dtype весов и кэша. Квантизация весов в INT4 не уменьшает KV cache — он по умолчанию остаётся в FP16, если явно не указана квантизация кэша.
Практический чек-лист
- Оцените бюджет памяти по формуле выше до развёртывания. Учитывайте batch size и максимальную длину контекста.
- Выбирайте модель с GQA, если работаете с длинными контекстами и ограниченной памятью.
- Используйте vLLM или аналог с PagedAttention для production-сервинга — это устраняет фрагментацию без изменения модели.
- Включите prefix caching, если запросы имеют общий префикс (системный промпт, few-shot).
- Квантизуйте KV cache в FP8, если качество приемлемо — это удваивает эффективную ёмкость.
- Проверяйте реальное потребление через метрики сервера (vLLM отдаёт статистику использования KV cache через Prometheus-эндпоинт), а не только теоретический расчёт.
- Мониторьте fragmentation ratio — если он высок, убедитесь, что PagedAttention активен и размер блока подобран адекватно типичной длине запросов.
