Source: KV-cache

← Rendered

markdown source:

---
description: Что такое KV-cache в больших языковых моделях и почему он ускоряет генерацию текста.
tags: llm attention cache inference transformers
title: KV-cache
---

KV-cache (key-value cache) — это техника, которая позволяет LLM переиспользовать уже вычисленные векторы ключей и значений для прошлых токенов вместо того, чтобы пересчитывать их заново на каждом шаге генерации.

Это одна из ключевых оптимизаций для инференса: без нее генерация текста становится заметно медленнее, особенно при длинных контекстах и больших моделях.

В этой заметке я собрал основные идеи из статьи Sebastian Raschka про KV-cache и перевел их в более практический, инженерный язык.

## Почему без кэша генерация медленная

Большинство LLM генерируют текст по одному токену за раз: модель принимает контекст, предсказывает следующий токен, добавляет его в последовательность и повторяет.

Пример:

- prompt: "Time"
- следующий токен: "flies"
- затем: "fast"

Если модель не использует кэш, то на каждом шаге она заново обрабатывает весь уже сгенерированный контекст:

- для "Time" вычисляет все необходимые промежуточные представления
- для "Time flies" снова всё пересчитывает
- для "Time flies fast" снова всё пересчитывает

То есть при генерации третьего токена модель уже дважды пересчитывает то, что было вычислено на предыдущих шагах.

С точки зрения attention это особенно заметно: каждую новую позицию модель сравнивает с предыдущими ключами и значениями. Но для уже увиденных токенов эти K/V-представления не меняются — они были рассчитаны раньше и не должны пересчитываться.

## Что такое key и value в attention

В self-attention каждый токен преобразуется в три вектора:

- Query (Q): то, что модель ищет в контексте
- Key (K): то, что модель "помечает" как доступный для поиска
- Value (V): содержимое, которое будет использоваться для агрегации

Для одного токена в каждом слое:

- Q, K, V получаются линейными проекциями из эмбеддинга токена
- далее модель вычисляет скоры attention между текущим query и всеми ключами
- потом весирует значения по этим скорям

Уже вычисленные K и V для прошлых токенов зависят только от самих токенов и параметров модели, а не от того, что генерируется дальше.

Именно поэтому их можно сохранить и переиспользовать.

## Идея KV-cache

KV-cache хранит уже вычисленные:

- ключи K для каждого токена
- значения V для каждого токена

на каждом слое и в каждом attention-head.

При генерации следующего токена модель:

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

Вместо пересчета всей последовательности целиком модель работает только с:

- новым токеном
- уже сохраненными K/V для прошлого контекста

Именно это резко снижает вычислительную нагрузку.

## Почему это важно в авторегрессионной генерации

LLM обычно генерируют текст автререгрессионно:

- исходный prompt обрабатывается один раз
- далее на каждом шаге выбирается один следующий токен
- новый токен добавляется в последовательность

Если не использовать cache, то на каждом step мы пересчитываем историю заново. Это дает квадратичную зависимость по длине последовательности на уровне внимания.

С KV-cache мы в сущности уменьшаем повторную работу: каждая пара K/V вычисляется один раз и дальше используется много раз.

Упрощенно:

- без кэша: O(n²) работы по накоплению
- с кэшем: в каждом шаге мы добавляем только новые элементы, а старые переиспользуем

Это не магия: в реальных моделях это превращается в значимый выигрыш в latency и throughput, особенно в продакшене и при длинных диалогах, код-интерфейсах, генерации длинного текста и RAG-пайплайнах.

## Идея на примере

Представим, что мы уже сгенерировали контекст:

"Time flies"

Теперь генерируем следующий токен "fast".

Без cache модель заново делает:

- вычисление key/value для "Time"
- вычисление key/value для "flies"
- затем для "fast"

С cache:

- "Time" и "flies" уже лежат в кэше
- вычисляется только key/value для "fast"
- дальше attention смотрит сразу на кэш + новый токен

Это тот же результат математики, но без лишних вычислений.

## Как выглядит реализация на уровне кода

Идея очень простая: хранить тензоры `cache_k` и `cache_v` и дополнять их новыми значениями.

Пример в духе кода из статьи:

```python
import torch

class MultiHeadAttention(torch.nn.Module):
    def __init__(self, d_in, d_out, num_heads):
        super().__init__()
        self.W_key = torch.nn.Linear(d_in, d_out)
        self.W_value = torch.nn.Linear(d_in, d_out)
        self.W_query = torch.nn.Linear(d_in, d_out)

        self.register_buffer("cache_k", None)
        self.register_buffer("cache_v", None)

    def reset_cache(self):
        self.cache_k = None
        self.cache_v = None

    def forward(self, x, use_cache=False):
        b, seq_len, d_in = x.shape

        keys_new = self.W_key(x)
        values_new = self.W_value(x)
        queries = self.W_query(x)

        if use_cache:
            if self.cache_k is None:
                self.cache_k = keys_new
                self.cache_v = values_new
            else:
                self.cache_k = torch.cat([self.cache_k, keys_new], dim=1)
                self.cache_v = torch.cat([self.cache_v, values_new], dim=1)

            keys = self.cache_k
            values = self.cache_v
        else:
            keys = keys_new
            values = values_new

        # далее обычный attention
        # scores = queries @ keys.transpose(-2, -1)
        # weights = softmax(scores, dim=-1)
        # output = weights @ values
        return keys, values
```

Главная мысль здесь в том, что мы не заменяем сам attention, а лишь изменяем способ, каким модель получает K и V.

## Обязательный паттерн: очистка кэша

Кэш нельзя просто держать между разными запросами или отдельными генерационными сессиями.

Если этого не делать, то новый запрос может начать "видеть" старый контекст, что приведет к неправильному поведению и запутанным результатам.

Поэтому обычно есть метод вроде:

```python
model.reset_kv_cache()
```

или на уровне блока attention:

```python
for block in model.trf_blocks:
    block.att.reset_cache()
```

Это особенно важно в продакшене, где один и тот же объект модели может использоваться для разных разговоров или запросов.

## Почему нужен offset / current_pos

В простом коде при каждом новом вызове нужно помнить, на какой позиции в контексте мы находимся.

Например, если кэш уже содержит 10 токенов, а новый токен должен пойти после них, то позиционный индекс новых токенов должен начинаться не с 0, а с 10.

Иначе модель "смешает" старые и новые позиции и будет ошибочно учитывать их как перекрывающиеся.

Типичный паттерн:

```python
self.current_pos = 0
```

и затем при генерации:

```python
pos_ids = torch.arange(self.current_pos, self.current_pos + seq_len)
self.current_pos += seq_len
```

Это важно для корректного position embedding и для того, чтобы модель не путала временные шаги.

## Почему кэш помогает в реальном инференсе

Кэш особенно полезен в системах, где:

- пользователь пишет длинный диалог
- модель должна быстро продолжать ответ
- используется streaming generation
- есть большие модели и длинный контекст

Если без кэша при каждом новом токене модель заново прогоняет весь контекст, то задержка быстро растет. С кэшем повторные вычисления исчезают, и скорость генерации становится заметно выше.

Это одна из причин, почему современные LLM-инференс-системы в продакшене используют кэширование K/V как стандартную практику.

## Трейд-оффы

У KV-cache есть плюсы и недостатки

### Плюсы

- заметно ускоряет генерацию текста
- уменьшает повторные вычисления
- особенно полезен при длинных последовательностях

### Минусы

- увеличивает потребление памяти
- с длинным контекстом кэш может значительно расти
- добавляет сложность в реализации
- кэш нужен только для inference, не для training

Для больших моделей с очень длинным контекстом память становится критичным фактором. Поэтому в продакшене часто используются оптимизации:

- предаллокейшн памяти
- sliding window для ограничения контекста
- отсечение старых токенов
- более специальные наборы K/V буферов

## Оптимизации в продакшене

В учебном примере кэш часто делается через `torch.cat`. Это удобно и понятно, но на больших моделях это может быть неэффективно, потому что:

- создаются новые тензоры на каждом шаге
- возникает лишнее выделение памяти
- возможна фрагментация памяти

Более продвинутые варианты:

1. Предаллоцировать буфер максимальной длины
2. Записывать новые значения в нужный слот вместо конкатенации
3. Использовать sliding window: держать только последние N токенов
4. Для длинных контекстов применять более сложные стратегии управления memory

То есть в реальных системах KV-cache — это не просто "один словарь с K и V", а обычно продуманная структура памяти, обученная под модель и устройство.

## Что важно запомнить

- в autoregressive LLM прошлые токены не нужно пересчитывать заново
- их K/V уже известны и стабильны
- кэш позволяет переиспользовать их на каждом следующем шаге

Это превращает генерацию текста из повторного пересчета всей истории в incremental inference, где каждый новый токен добавляет лишь небольшую дополнительную работу.

Именно эта идея лежит в основе производительного инференса современных больших языковых моделей.

## Итог

KV-cache — это один из самых важных инженерных приемов в LLM-инференсе.

Он решает одну и ту же проблему, с которой сталкивается любая генерация текста:

- почему модель заново считает уже известный контекст?
- почему скорость падает при длинных диалогах?

Ответ в том, что без кэша она фактически заново вычисляет старый контекст каждый раз.

KV-cache меняет ситуацию так, что:

- прошлые ключи и значения сохраняются
- новый токен обрабатывается отдельно
- attention обращается к уже готовому контексту

Именно поэтому кэш считается фундаментальной оптимизацией для эффективной работы LLM в production.

## Ссылки и рекомендуемое чтение

- статья: [Understanding and Coding the KV Cache in LLMs from Scratch](https://magazine.sebastianraschka.com/p/coding-the-kv-cache-in-llms)
- про attention: [Understanding and Coding Self-Attention, Multi-Head Attention, Causal-Attention, and Cross-Attention in LLMs](https://magazine.sebastianraschka.com/p/understanding-and-coding-self-attention)
- по книге: [Build a Large Language Model (From Scratch)](https://amzn.to/4fqvn0D)
- [[lists/llm]]

## AI-warning

✨ Данная статья сгенерирована с помощью MAI-Code-1.1-Flash. Текст проверен человеком.

[lists/llm]: ../lists/llm "LLM"