Все статьи
· 7 мин чтения

KV-кэш: почему LLM помнит без памяти и жрёт VRAM как не в себя 🧠💾

KV-кэш простыми словами: зачем он нужен, почему без него генерация была бы в разы медленнее, и почему именно из-за него у тебя OOM на длинном контексте. С интерактивом и калькулятором VRAM.

KV-кэш: почему LLM помнит без памяти и жрёт VRAM как не в себя 🧠💾

KV-кэш: почему LLM помнит без памяти и жрёт VRAM как не в себя 🧠💾

Ну чё, малютки, замечали такое: даёшь модели длинный промпт — она думает целую вечность перед первым словом, а потом строчит токены пулемётом? Или хостишь модельку на жирной видяхе, всё влезает — а на длинном контексте внезапно OOM и краш? За обоими случаями стоит одна штука — KV-кэш.

Без него генерация была бы в разы медленнее. С ним — у тебя на ровном месте кончается VRAM. Разберём, что это и зачем.

🔥Суть за 10 секунд

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


Что вообще кэшируем

Внутри модели каждый токен превращается в три вектора — Q, K и V. Проще всего через аналогию с поиском:

QQuery — поисковый запрос: «что мне сейчас важно?»
KKey — заголовки страниц, по которым ищем совпадение
VValue — само содержимое, которое заберём

Чтобы сгенерить новый токен, модель «гуглит» по всему тексту: берёт свой Q и сверяет с K каждого прошлого токена, а потом забирает их V. Фишка в том, что K и V прошлых токенов не меняются — у пятого токена они одни и те же, генеришь ты шестое слово или шестисотое. Так зачем считать их заново?


Проблема: модель считает одно и то же

Наивная реализация на каждом шаге прогоняет весь текст заново и пересчитывает K и V для всех токенов. Сгенерил 100-й токен — пересчитал для всех 100. 101-й — снова для всех 101. Работа копится квадратично.

Потыкай — оранжевым показано то, что считается заново каждый шаг, хотя результат тот же самый:

♻️ Без кэша vs с кэшем
Модель генерирует токены по одному. Оранжевый — K/V считаются заново на этом шаге, синий — лежат в кэше, зелёный — новый токен. Жми «Вперёд» и смотри, сколько лишней работы без кэша.
❌ Без кэша — пересчёт всего на каждом шаге1 вычислений K/V
Мама
мыла
раму
очень
чисто
и
аккуратно
✅ С кэшем — считаем только новый токен1 вычислений K/V
Мама
мыла
раму
очень
чисто
и
аккуратно
1
Операций без кэша · O(n²)
1
Операций с кэшем · O(n)
Токен 1/7

Программист в тебе должен сейчас орать: «посчитал — выбросил — посчитал опять».


Решение: просто запомни

Озарение уровня «а давайте не тупить»: K и V прошлых токенов не меняются — сохраним их и будем переиспользовать. Это и есть KV-кэш.

1Считаем K и V только для нового токена
2Дописываем их в кэш
3Для всех прошлых токенов берём готовое из кэша

Вот почему первый токен долгий (модель жуёт весь промпт и набивает кэш), а дальше слова сыпятся быстро — кэш уже готов, считать почти нечего.


Подвох: кэш сжирает VRAM

Халявы не бывает: мы променяли вычисления на память. И кэш растёт с каждым токеном — чем длиннее контекст, тем он больше. В отличие от весов модели (они фиксированы), кэш пухнет в рантайме на каждый запрос.

Смотри на видяхе 24 ГБ с Llama 3 8B. Тяни ползунок контекста:

📈 KV-кэш сжирает VRAM
Llama 3 8B на видеокарте 24 ГБ. Вес модели (16 ГБ) — фиксированный, KV-кэш растёт линейно с длиной контекста. Тяни ползунок: при ~64K токенов кэш забивает остаток памяти и всё падает в OOM — а это всего один запрос.
вес 16 ГБ
24 ГБ
Контекст8K токенов
KV-кэш1.00 ГБ
Вес + кэш17.0 / 24 ГБ
✅ Влезает, запас 7.0 ГБ

Ирония: модель весит 16 ГБ, и при контексте 128K её кэш — тоже 16 ГБ, догоняет саму модель. И это всего один запрос. А если обрабатывать 50 запросов разом — умножай на 50. Вот тут VRAM и заканчивается.

⚠️⚠️ Почему это важно

Чаще всего в OOM тебя упирает не размер модели, а именно KV-кэш. Он же главный лимит на то, сколько запросов влезет в видяху и какой длины контекст потянешь.


Сколько именно жрёт

Память кэша считается по простой формуле:

2 (Key + Value) × число слоёв × KV-головы × длина контекста × batch × байт на число

Покрути сам — выбери модель, контекст и батч, и смотри, как цифра улетает в космос:

🧰 Калькулятор VRAM под KV-кэш
Покрути параметры и посмотри, сколько памяти сожрёт кэш. Формула — та же, что в движках инференса (конфиги моделей — близкие к реальным). Сравни GQA-модели с MLA (Kimi, DeepSeek) — разница в разы.
1.00 ГБ
KV-кэш на 1 запрос · контекст 8K
На 1 токен128 КБ
На 1 запрос (8K)1.00 ГБ
Вес модели (fp16)14.90 ГБ
Кэш / вес модели6.7%
2 × слои(32) × kv_голов(8) × head_dim(128) × контекст(8192) × batch(1) × 2 байт

Llama 3.1 8B на 8K контекста = ровно 1 ГБ, удобно запомнить. А теперь переключи на Kimi K2.5 или DeepSeek-V3: модель на триллион параметров, а кэш — меньше, чем у Llama 70B. Это магия MLA, и про неё чуть ниже.

А MoE влияет на кэш?

Нет. В Mixture-of-Experts разрежены только эксперты (FFN-слои) — на токен включается малая часть. А внимание остаётся плотным, поэтому KV-кэш у MoE-модели считается как обычно, по числу её слоёв и KV-голов. Активные 32B у Kimi на размер кэша не влияют.


Чем это лечат

Раз число KV-голов стоит в формуле множителем — главный способ ужать кэш это уменьшить число KV-голов. Так появились три варианта внимания. Потыкай:

🧮 MHA → GQA → MQA: ужимаем кэш
Каждый цвет — группа Q-голов, которая делит одну пару Key/Value. Чем меньше KV-голов, тем меньше кэш. Тыкай по вариантам:
Q — головы запросов (8 штук)
Q1
Q2
Q3
Q4
Q5
Q6
Q7
Q8
K/V — пары ключ-значение, что и лежат в кэше (2 шт.)
KV1
KV2
2
KV-голов
×4
Меньше кэша
25%
От кэша MHA
Grouped-Query Attention — группа Q-голов делит одну пару K/V. Золотая середина, на ней сидит почти весь современный опенсорс.
Где используется: Llama 3.x, Qwen3, Mistral, GLM-4.5, Gemma 2

Коротко: в MHA у каждой головы своя пара K/V — качество топ, кэш максимальный. В MQA все головы делят одну пару — кэш в разы меньше, качество чуть проседает. GQA — золотая середина, поэтому почти весь свежий опенсорс (Llama 3, Qwen3, Mistral, GLM) на ней. Бесплатно режет кэш в 4–8 раз.

Следующий уровень — MLA (Multi-head Latent Attention), фишка DeepSeek-V3 и Kimi K2.5. Вместо того чтобы хранить K и V по головам, MLA сжимает их в один маленький латентный вектор и разжимает на лету. Отсюда тот самый фокус из калькулятора, и отсюда же — почему MLA-модели так дёшево тянут контекст в сотни тысяч токенов.

💡Бонус: PagedAttention

Есть ещё трюк — не ужимать кэш, а перестать профукивать его на фрагментации памяти (как виртуальная память в ОС). Это PagedAttention, на нём построен vLLM. Разбирал с интерактивом в гайде по движкам инференса.


А что с квантизированными моделями?

Главная путаница новичков. Скачал квантизованную в 4 бита модель (GGUF Q4, GPTQ, AWQ), она занимает в 4 раза меньше — значит и кэш меньше? Нет.

⚠️⚠️ Квантизация весов ≠ квантизация кэша

Квантизация модели ужимает веса. KV-кэш — это отдельная от весов память, и по умолчанию он лежит в FP16, даже если веса в 4 битах. То есть 4-битная 70B-модель всё равно таскает за собой полноразмерный FP16-кэш. Вот почему народ ловит OOM на длинном контексте на «маленькой» квантованной модели.

Чтобы ужать сам кэш, его надо квантовать отдельным тумблером — это и есть «KV cache quantization»:

Q88 бит (q8_0 / FP8) — почти без потерь, кэш вдвое меньше. Бери по умолчанию.
Q44 бита (q4_0) — вчетверо меньше, но качество на длинном контексте может подсесть. Тестируй на своей задаче.
FAНюанс: квантизация кэша обычно требует включённого Flash Attention.

Как включить в Ollama, vLLM, SGLang, llama.cpp

Сразу важное: сам KV-кэш всегда включён — без него инференс не работает, выключить нельзя. Настраивают не «вкл/выкл», а его точность, чтобы сэкономить VRAM. Шпаргалка:

llama.cpp — флагами при запуске (для квантизации кэша нужен -fa):

llama-server -m model.gguf -fa \
  --cache-type-k q8_0 --cache-type-v q8_0

Ollama — переменными окружения (под капотом тот же llama.cpp):

OLLAMA_FLASH_ATTENTION=1 \
OLLAMA_KV_CACHE_TYPE=q8_0 \
ollama serve

vLLM — кэш на PagedAttention включён всегда, квантуется флагом:

vllm serve Qwen/Qwen3-8B --kv-cache-dtype fp8

SGLang — аналогично, флагом при запуске:

python -m sglang.launch_server \
  --model-path Qwen/Qwen3-8B --kv-cache-dtype fp8_e5m2
Что выбрать на практике

Начни с 8-битного кэша (q8_0 / fp8) — это почти бесплатные −50% памяти под кэш. Если контекст огромный и всё равно не влезает — пробуй 4 бита и поглядывай, не поехало ли качество. И помни: квантизация весов и квантизация кэша — два независимых рычага.


TL;DR

Без кэша — пересчёт всего100 % работы
С кэшем — только новый токен15 % работы
1Зачем: чтобы не пересчитывать K/V всех прошлых токенов на каждом шаге
2Что внутри: сохранённые K и V для каждого токена
3Подвох: растёт с контекстом и батчем, жрёт VRAM, ловит OOM
4Лечение: GQA/MQA/MLA, квантизация кэша (отдельно от весов!), PagedAttention

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