KV-кэш: почему LLM помнит без памяти и жрёт VRAM как не в себя 🧠💾
Ну чё, малютки, замечали такое: даёшь модели длинный промпт — она думает целую вечность перед первым словом, а потом строчит токены пулемётом? Или хостишь модельку на жирной видяхе, всё влезает — а на длинном контексте внезапно OOM и краш? За обоими случаями стоит одна штука — KV-кэш.
Без него генерация была бы в разы медленнее. С ним — у тебя на ровном месте кончается VRAM. Разберём, что это и зачем.
Модель пишет текст по одному токену, и на каждом шаге ей нужны данные обо всех предыдущих токенах. KV-кэш просто сохраняет эти данные, чтобы не считать их заново на каждом шаге. Платим за скорость памятью: кэш растёт с длиной текста и сжирает VRAM — иногда больше, чем весит сама модель.
Что вообще кэшируем
Внутри модели каждый токен превращается в три вектора — Q, K и V. Проще всего через аналогию с поиском:
Чтобы сгенерить новый токен, модель «гуглит» по всему тексту: берёт свой Q и сверяет с K каждого прошлого токена, а потом забирает их V. Фишка в том, что K и V прошлых токенов не меняются — у пятого токена они одни и те же, генеришь ты шестое слово или шестисотое. Так зачем считать их заново?
Проблема: модель считает одно и то же
Наивная реализация на каждом шаге прогоняет весь текст заново и пересчитывает K и V для всех токенов. Сгенерил 100-й токен — пересчитал для всех 100. 101-й — снова для всех 101. Работа копится квадратично.
Потыкай — оранжевым показано то, что считается заново каждый шаг, хотя результат тот же самый:
Программист в тебе должен сейчас орать: «посчитал — выбросил — посчитал опять».
Решение: просто запомни
Озарение уровня «а давайте не тупить»: K и V прошлых токенов не меняются — сохраним их и будем переиспользовать. Это и есть KV-кэш.
Вот почему первый токен долгий (модель жуёт весь промпт и набивает кэш), а дальше слова сыпятся быстро — кэш уже готов, считать почти нечего.
Подвох: кэш сжирает VRAM
Халявы не бывает: мы променяли вычисления на память. И кэш растёт с каждым токеном — чем длиннее контекст, тем он больше. В отличие от весов модели (они фиксированы), кэш пухнет в рантайме на каждый запрос.
Смотри на видяхе 24 ГБ с Llama 3 8B. Тяни ползунок контекста:
Ирония: модель весит 16 ГБ, и при контексте 128K её кэш — тоже 16 ГБ, догоняет саму модель. И это всего один запрос. А если обрабатывать 50 запросов разом — умножай на 50. Вот тут VRAM и заканчивается.
Чаще всего в OOM тебя упирает не размер модели, а именно KV-кэш. Он же главный лимит на то, сколько запросов влезет в видяху и какой длины контекст потянешь.
Сколько именно жрёт
Память кэша считается по простой формуле:
2 (Key + Value) × число слоёв × KV-головы × длина контекста × batch × байт на число
Покрути сам — выбери модель, контекст и батч, и смотри, как цифра улетает в космос:
Llama 3.1 8B на 8K контекста = ровно 1 ГБ, удобно запомнить. А теперь переключи на Kimi K2.5 или DeepSeek-V3: модель на триллион параметров, а кэш — меньше, чем у Llama 70B. Это магия MLA, и про неё чуть ниже.
Нет. В Mixture-of-Experts разрежены только эксперты (FFN-слои) — на токен включается малая часть. А внимание остаётся плотным, поэтому KV-кэш у MoE-модели считается как обычно, по числу её слоёв и KV-голов. Активные 32B у Kimi на размер кэша не влияют.
Чем это лечат
Раз число KV-голов стоит в формуле множителем — главный способ ужать кэш это уменьшить число KV-голов. Так появились три варианта внимания. Потыкай:
Коротко: в 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, на нём построен vLLM. Разбирал с интерактивом в гайде по движкам инференса.
А что с квантизированными моделями?
Главная путаница новичков. Скачал квантизованную в 4 бита модель (GGUF Q4, GPTQ, AWQ), она занимает в 4 раза меньше — значит и кэш меньше? Нет.
Квантизация модели ужимает веса. KV-кэш — это отдельная от весов память, и по умолчанию он лежит в FP16, даже если веса в 4 битах. То есть 4-битная 70B-модель всё равно таскает за собой полноразмерный FP16-кэш. Вот почему народ ловит OOM на длинном контексте на «маленькой» квантованной модели.
Чтобы ужать сам кэш, его надо квантовать отдельным тумблером — это и есть «KV cache quantization»:
Как включить в 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
KV-кэш — классический инженерный компромисс: разменяли вычисления на память. Вспомни про него, когда в следующий раз поймаешь OOM на длинном контексте. 🫡
