Что такое KV-кеш и почему длинный диалог ест видеопамять
Модель весит 8 гигабайт, карта на 16 — казалось бы, запас двойной. Но через полчаса переписки с локальным ассистентом сервер падает с CUDA out of memory, хотя веса никуда не делись и никто их не удваивал. Разгадка — в KV-кеше: механизме, который делает генерацию быстрой, но платит за это видеопамятью, растущей вместе с длиной диалога. Разберёмся, что это такое и почему длинный контекст — это отдельная, и не маленькая, статья расходов VRAM.
Содержание
- Зачем нужен KV-кеш: что пересчитывает трансформер без него
- Что именно хранится в кеше и откуда берётся ускорение
- Почему кеш растёт линейно с длиной диалога — и почему это не мелочь
- VRAM-бюджет: веса модели — это только часть счёта
- Практика: почему модель «помещалась», а длинный диалог уронил её в OOM
- Как снизить давление KV-кеша на память
Зачем нужен KV-кеш: что пересчитывает трансформер без него
Трансформер генерирует ответ по одному токену за раз: посчитал следующий токен, приписал его к последовательности, посчитал следующий — и так до конца ответа. Каждый токен на каждом слое проходит через self-attention, а в attention токен должен «посмотреть» на все предыдущие токены в последовательности, чтобы решить, какие из них важны для его собственного представления.
Технически для этого у каждого токена на каждом слое и в каждой голове внимания считаются три вектора: query (Q, «что я ищу»), key (K, «что я предлагаю») и value (V, «что я отдаю, если меня выбрали»). Attention сравнивает query текущего токена с key всех токенов в контексте и взвешивает их value этим сравнением.
Наивная реализация без всякого кеша на каждом новом токене пересчитывала бы K и V для абсолютно всех предыдущих токенов заново — просто потому, что формально ничего не мешает их пересчитать. Это расточительно: K и V токена №5 не меняются от того, что вы дописали токен №200 — они зависят только от самого токена №5 и токенов перед ним, а не от того, что будет сгенерировано позже. Разработчики трансформеров это заметили быстро, и с самых ранних практических реализаций GPT-подобных моделей K и V стали не пересчитывать, а кешировать.
Что именно хранится в кеше и откуда берётся ускорение
KV-кеш — это просто буфер в видеопамяти, куда после обработки каждого токена складываются его векторы key и value, отдельно для каждого слоя модели и отдельно для каждой головы внимания. Когда приходит следующий токен, модели нужно посчитать K и V только для него самого — один новый токен, а не вся история заново. Дальше его свежий query сравнивается с key уже накопленными в кеше, без единого лишнего матричного умножения по старым токеном.
Разница на пальцах: без кеша генерация N-го токена в среднем требует работы, пропорциональной всей длине уже сгенерированного текста, а по сумме на весь ответ вычислительная нагрузка растёт квадратично от длины контекста. С кешем каждый новый токен — это фиксированная порция работы (посчитать K/V для одного токена плюс пробежаться attention по уже готовому кешу), и по сумме на ответ зависимость от длины становится линейной. Именно поэтому современные LLM вообще способны отвечать с приемлемой скоростью на длинных диалогах — без KV-кеша инференс на контексте в несколько тысяч токенов был бы практически неюзабельным по времени.
Это же объясняет, почему prefill (обработка входного промпта) и decode (генерация ответа токен за токеном) в вашем локальном LLM-сервере ведут себя по-разному: на prefill модель обрабатывает весь входной текст пачкой и заполняет кеш для него целиком, а на decode добавляет в кеш по одному токену за шаг. Если вы замечали в логах Ollama или vLLM отдельные метрики prompt eval и eval (генерация) — это ровно граница между двумя этими фазами, и КV-кеш — общий механизм для обеих.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Развернуть ИИ на сервереПочему кеш растёт линейно с длиной диалога — и почему это не мелочь
У ускорения есть цена: кеш не бесплатный, он занимает реальную видеопамять, и занимает её пропорционально четырём вещам сразу — числу слоёв модели, числу голов внимания (точнее, голов key/value — об этом ниже), размерности каждой головы и, самое главное для вас как для пользователя, длине контекста, то есть количеству токенов, уже находящихся в диалоге.
Грубо говоря, объём KV-кеша на один токен — величина фиксированная для конкретной модели (она зависит от архитектуры: числа слоёв, голов, их размерности и от того, в каком формате хранятся числа — fp16, fp8, int8). А общий размер кеша — это эта фиксированная «цена токена», умноженная на общее число токенов в контексте: системный промпт, вся история переписки, вставленные документы, весь код, который вы вставили в чат. Зависимость линейная — вдвое длиннее диалог, вдвое больше кеш, — но именно поэтому она незаметно подкрадывается: пока диалог короткий, кеш почти не виден на фоне весов модели, а после нескольких десятков сообщений или одной длинной вставки кода он превращается в заметную, а то и доминирующую статью расхода видеопамяти.
Есть архитектурный нюанс, который сильно меняет итоговую арифметику: в старых моделях количество голов key/value равнялось количеству голов query (multi-head attention, MHA), и кеш был максимально прожорливым. Современные модели (Llama 3, Mistral, Qwen и большинство актуальных открытых LLM на конец августа 2026 года) используют grouped-query attention (GQA) или даже multi-query attention (MQA) — при них голов key/value в разы меньше, чем голов query, и они разделяются между несколькими query-головами. Это не бесплатный трюк (есть небольшая потеря в качестве по сравнению с полным MHA), но кеш от этого может быть в разы компактнее при той же архитектуре модели в остальном. Конкретную кратность и точные цифры для вашей модели считать нужно по её конфигу (config.json, поля вида num_attention_heads и num_key_value_heads) — я намеренно не привожу здесь усреднённых цифр «в разы», потому что для разных моделей разница разная, и выдавать это за универсальный бенчмарк было бы нечестно.
VRAM-бюджет: веса модели — это только часть счёта
Отсюда прямое следствие для планирования сервера: видеопамять, которая нужна модели в работе, — это не размер файла с весами, как часто предполагают, когда впервые считают «влезет ли модель». Реальный бюджет VRAM складывается минимум из трёх слагаемых:
- Веса модели — то, что вы скачали, зафиксированное число, не зависящее от диалога;
- Активации на прямом проходе — временная память под промежуточные вычисления, обычно относительно небольшая по сравнению с двумя другими пунктами, но растущая с размером батча и длиной обрабатываемого за раз куска;
- KV-кеш — переменная часть, которая растёт линейно с текущей длиной контекста и умножается на число одновременных запросов (если сервер обслуживает несколько диалогов параллельно, кеш нужен под каждый из них отдельно).
Именно поэтому в описании модели на HuggingFace или в анонсе релиза часто отдельно указывают, сколько VRAM нужно «под саму модель» и отдельно — что для полного контекстного окна (когда оно исчисляется десятками или сотнями тысяч токенов) может потребоваться существенно больше памяти, чем под веса. Соотношение сильно зависит от конкретной архитектуры и длины контекста, поэтому я не берусь называть здесь универсальный множитель — но сам факт, что это отдельная статья расхода, а не округление в пределах погрешности, важно держать в голове при выборе GPU под задачу.
Разные движки инференса ведут себя тут по-разному. Ollama и llama.cpp по умолчанию выделяют память под кеш ленивее — под заданный лимит контекста (num_ctx в Ollama, --ctx-size в llama.cpp), но не обязательно резервируют её всю заранее в одном большом куске. vLLM, наоборот, на старте сам профилирует пик активаций и сразу резервирует под KV-кеш весь оставшийся после весов бюджет видеопамяти (управляется флагом --gpu-memory-utilization, по умолчанию 0.9) — из-за этого при старте vLLM в логе видно явные числа вида «под кеш зарезервировано N ГиБ» и «максимум M токенов кеша», и именно на этой стадии он падает с OutOfMemoryError, если паспортная длина контекста (--max-model-len) физически не помещается в оставшийся бюджет.
Практика: почему модель «помещалась», а длинный диалог уронил её в OOM
Типичный сценарий, с которым сталкиваются на локальном GPU-сервере: модель скачали, запустили, задали короткий тестовый вопрос — всё отвечает быстро и без ошибок, nvidia-smi показывает комфортный запас видеопамяти. Через день работы в диалоге, который вырос до нескольких десятков сообщений (или после того, как в чат один раз вставили длинный лог или файл с кодом), сервер падает с нехваткой памяти — при абсолютно той же модели и тех же настройках.
Происходит ровно то, что описано выше: веса модели не изменились ни на байт, а KV-кеш вырос вместе с диалогом и в какой-то момент вместе с активациями текущего запроса перестал помещаться в оставшуюся после весов видеопамять. Несколько практических моментов, которые стоит проверить, если это ваш случай:
- Посмотрите заданный лимит контекста. В Ollama это
num_ctx(параметр вModelfileили в API-запросе), в llama.cpp — флаг-c/--ctx-size, в vLLM —--max-model-len. Если лимит выставлен «на всякий случай» большим (скажем, 32k или 128k токенов вместо реально нужных 4–8k), движок либо сразу резервирует память под этот максимум (vLLM), либо позволяет кешу дорасти до этого потолка в процессе диалога (Ollama, llama.cpp) — и именно этот потолок в итоге не помещается в карту. - Проверьте, не накапливается ли контекст без нужды. Если UI или обвязка чат-бота при каждом новом сообщении подсовывает модели всю историю диалога целиком (а не суммаризирует старые сообщения или не обрезает историю окном), кеш растёт без остановки в течение всей сессии — и рано или поздно упрётся в потолок VRAM независимо от того, насколько щедрым был запас в начале.
- Мониторьте видеопамять в динамике, а не разово. Разовый
nvidia-smiпри старте покажет только веса плюс минимальный кеш пустого диалога — реальную картину даёт наблюдение за использованием VRAM по мере роста диалога, например черезwatch -n1 nvidia-smiилиnvtopво время реальной сессии. Отдельно об этом подходе — как мониторить нагрузку локальной LLM. - Учитывайте параллельные запросы. Если сервер обслуживает не один диалог, а несколько параллельных сессий (несколько пользователей чат-бота, несколько вкладок), кеш нужен под каждую сессию отдельно — суммарный расход умножается на число одновременно активных диалогов, и именно это часто недооценивают при расчёте, сколько VRAM «хватит на всех».
Итог практического наблюдения простой: паспортная фраза «модель помещается в 16 ГБ» почти всегда означает «веса модели плюс небольшой запас под короткий контекст помещаются в 16 ГБ» — а не «в 16 ГБ можно вести диалог любой длины». Это разные утверждения, и путать их — самая частая причина внезапного OOM после часов стабильной работы.
Как снизить давление KV-кеша на память
Если карта тесновата для нужной вам длины диалога, есть несколько рабочих направлений — часто их комбинируют:
- Уменьшить лимит контекста осознанно, а не «на всякий случай по максимуму». Если реальные диалоги редко превышают 8k токенов, выставлять
num_ctxв 128k «про запас» не даёт пользы, зато резервирует память под кеш, которым вы не пользуетесь (особенно критично для vLLM, который резервирует бюджет заранее). - Квантовать сам кеш, а не только веса модели. Часть движков инференса (в том числе vLLM через флаг
--kv-cache-dtype) умеет хранить key/value в fp8 вместо fp16/bf16 — это примерно вдвое сокращает объём кеша при том же числе токенов ценой небольшой потери точности. Квантование весов модели — отдельная и более известная тема, разобранная в статье квантование моделей: Q4, Q5, Q8 — что выбрать, но квантование кеша решает именно проблему длинного диалога, а не размера самих весов. - Выбирать модели с GQA/MQA, если архитектура важна больше, чем конкретный бренд модели — при прочих равных они дают заметно более компактный кеш на тот же контекст, чем модели с классическим multi-head attention.
- Резать историю диалога программно: суммаризировать старые сообщения, обрезать историю скользящим окном, не отправлять модели документы целиком там, где хватит релевантного фрагмента (это же снижает и расход на prefill, а не только на кеш).
- Ограничивать число параллельных сессий на карту или явно закладывать под это память при расчёте, если сервер обслуживает не один диалог одновременно — иначе бюджет, рассчитанный на одну сессию, лопнет при второй.
- Пересчитать бюджет VRAM под реальную длину контекста, а не только под веса, прежде чем брать конкретную видеокарту под задачу. Если для нужной вам длины диалога расчёт показывает, что штатной карты не хватает, разумнее сразу смотреть на конфигурацию с большим объёмом VRAM — например, через GPU-сервер в Великобритании для инференса LLM, чем упираться в OOM на проде и разбираться постфактум.
- Если сервер уже падает на старте с нехваткой памяти именно на этапе профилирования (характерно для vLLM), а не посреди долгого диалога — это отдельная, более узкая история про резервирование бюджета движком, она разобрана в статье vLLM не запускается из-за памяти: причины и решение.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Развернуть ИИ на сервереНужны сами нейросети для контента?
Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.
Частые вопросы
KV-кеш — это то же самое, что контекстное окно модели?
Нет, это разные вещи. Контекстное окно — это архитектурный лимит модели на максимальное число токенов, которые она вообще способна обработать за раз (задаётся при обучении). KV-кеш — это память, которая физически расходуется под уже накопленный на данный момент контекст в пределах этого лимита. Модель с окном в 128k токенов не обязана тратить память как для полных 128k, если реальный диалог короче — если только движок не резервирует бюджет заранее под максимум.
Можно ли вообще отключить KV-кеш, чтобы не тратить на него VRAM?
Технически да, но это резко замедлит генерацию — придётся пересчитывать key/value для всей истории на каждом новом токене, и на сколько-нибудь длинном диалоге ответ будет формироваться на порядки дольше. На практике это делается разве что в узких исследовательских сценариях, а не на рабочем сервере.
Почему у одной и той же модели на Ollama и на vLLM разное поведение с памятью при длинном диалоге?
Потому что это разные движки инференса с разной стратегией управления памятью — Ollama и llama.cpp выделяют её более гибко, по мере роста диалога в пределах заданного лимита, а vLLM резервирует бюджет под кеш заранее, при старте, исходя из паспортной максимальной длины контекста. Сама механика KV-кеша при этом одна и та же, разница в том, когда и как движок под неё резервирует память.
Помогает ли добавление оперативной памяти (не видеопамяти), если не хватает именно под KV-кеш?
Не напрямую. KV-кеш при инференсе на GPU по умолчанию живёт в видеопамяти, а не в системной RAM, потому что к нему нужен быстрый доступ на каждом шаге генерации. Некоторые связки (например, offloading части кеша на CPU) существуют, но обычно ощутимо снижают скорость генерации — это компромисс, а не бесплатное расширение VRAM.
Есть ли способ заранее прикинуть, хватит ли карты на нужную длину диалога?
Да — но точный расчёт требует знать конкретную архитектуру модели (число слоёв, голов key/value, их размерность, формат хранения) из её конфига, а не усреднённое правило «на глаз». Проще всего ориентироваться на официальные рекомендации по VRAM для конкретной модели под нужную вам длину контекста и проверять реальный расход через мониторинг видеопамяти в процессе, а не полагаться на разовый замер при пустом диалоге.
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →