Кэш KV — это невидимая база данных, которая истощает память GPU во время декодирования LLM.
Кэш KV тихо потребляет память GPU во время инференса LLM: предзаполнение против декодирования, математика KV, GQA и MQA, PagedAttention, политики квантизации и выгонки.
Источники: The KV Cache: AI’s Unseen Database Dominating GPU Memory
Настоящий узкий место при инференсе LLM
Во время инференса вычислительные ресурсы GPU часто остаются неиспользованными, в то время как пропускная способность памяти обеспечивает питание постоянно растущей кэш-памяти типа ключ–значение. Каждый новый токен добавляет тензоры K/V, которые необходимо снова читать на последующих этапах. Решения, игнорирующие рост объема кэш-памяти, сталкиваются с ограничениями по объему контекста и задержкам задолго до того, как исчерпаются вычислительные операции FLOPs.
Как это работает: принцип действия кэш-памяти типа ключ–значение
Для вычисления механизма внимания требуются ключи и значения каждого предыдущего токена. Их кэширование позволяет избежать повторных вычислений всего префикса на каждом шаге. Однако это влечет за собой потребность в памяти, пропорциональной длине последовательности × количеству слоев × количеству голов × размеру вектора.
Prefill против декодирования
Prefill обрабатывает вводный текст и заполняет кэш-память. Декодирование добавляет по одному токену за раз, читая весь кэш на каждом шаге. Метод Prefill требует больших вычислительных ресурсов, тогда как декодирование зависит от пропускной способности памяти.
import torch
import torch.nn as nn
# Configuration representing a simplified LLM layer
batch_size = 1
num_heads = 8
head_dim = 64
hidden_dim = 512
# Projection layers for Keys and Values
W_k = nn.Linear(hidden_dim, num_heads * head_dim)
W_v = nn.Linear(hidden_dim, num_heads * head_dim)
# --- 1. PREFILL PHASE ---
# The prompt has 5 tokens. We process them simultaneously.
prompt_len = 5
prompt_embeddings = torch.randn(batch_size, prompt_len, hidden_dim)
# Project to K and V, then reshape to: [Batch, Heads, SeqLen, HeadDim]
kv_cache_k = W_k(prompt_embeddings).view(batch_size, prompt_len, num_heads, head_dim).transpose(1, 2)
kv_cache_v = W_v(prompt_embeddings).view(batch_size, prompt_len, num_heads, head_dim).transpose(1, 2)
print(f"Prefill complete. Cache K shape: {kv_cache_k.shape}")
# Output: [1, 8, 5, 64] (5 tokens cached)
# --- 2. DECODING PHASE ---
# We generate 3 subsequent tokens, one by one.
for step in range(3):
# Input is only the single, newly generated token (SeqLen = 1)
new_token_embedding = torch.randn(batch_size, 1, hidden_dim)
# Project only the new token's K and V
new_k = W_k(new_token_embedding).view(batch_size, 1, num_heads, head_dim).transpose(1, 2)
new_v = W_v(new_token_embedding).view(batch_size, 1, num_heads, head_dim).transpose(1, 2)
# Append the new token's representations to our historical cache
# This prevents recalculating the past prompt_len + step tokens
kv_cache_k = torch.cat([kv_cache_k, new_k], dim=2)
kv_cache_v = torch.cat([kv_cache_v, new_v], dim=2)
print(f"Decoding Step {step + 1} | Cache K shape: {kv_cache_k.shape}")
# Step 1 Output: [1, 8, 6, 64]
# Step 2 Output: [1, 8, 7, 64]
# Step 3 Output: [1, 8, 8, 64]
def calculate_kv_cache_gb(batch_size: int, seq_len: int, layers: int, hidden_dim: int, precision_bytes: int = 2) -> float:
# 2 represents the key and value states
total_bytes = batch_size * seq_len * layers * hidden_dim * 2 * precision_bytes
return total_bytes / (1024 ** 3) # Convert to Gigabytes
# Llama 2 70B architecture parameters (assuming standard MHA)
llama_70b_gb = calculate_kv_cache_gb(
batch_size=32,
seq_len=8192,
layers=80,
hidden_dim=8192
)
print(f"KV Cache Size: {llama_70b_gb:.2f} GB")
# Output: KV Cache Size: 320.00 GB
+---------------------------------------------------------------+
| GPU VRAM (80 GB) |
+------------------------------+--------------------------------+
| Static Model Weights (Fixed) | KV Cache (Dynamic / Variable) |
| [==========================] | [=======================> ] |
| FP16 Weights (e.g., ~35GB/GPU| Grows linearly with: |
| across split tensor setups) | Batch Size * Sequence Length |
+------------------------------+--------------------------------+
MHA (1:1 Ratio) GQA (Grouped Ratio) MQA (Many:1 Ratio)
[Q1][Q2][Q3][Q4] [Q1][Q2] [Q3][Q4] [Q1][Q2][Q3][Q4]
| | | | \ / \ / \ | / /
[K1][K2][K3][K4] [ K1 ] [ K2 ] [ K1 ]
[V1][V2][V3][V4] [ V1 ] [ V2 ] [ V1 ]
# A simple simulation of PagedAttention block mapping
class VirtualPageTable:
def __init__(self, block_size: int):
self.block_size = block_size
self.logical_to_physical = {} # Maps (request_id, block_index) -> physical_block_id
self.free_blocks = list(range(1000)) # Pool of physical memory blocks
def allocate_block_for_token(self, request_id: int, token_index: int) -> int:
# Determine which logical block the token falls into
logical_block_idx = token_index // self.block_size
key = (request_id, logical_block_idx)
# Allocate a new physical block only when the previous one is full
if key not in self.logical_to_physical:
physical_block = self.free_blocks.pop(0)
self.logical_to_physical[key] = physical_block
return self.logical_to_physical[key]
# Instantiating a table with a block size of 4 tokens
table = VirtualPageTable(block_size=4)
# Token 5 belongs to logical block 1 (tokens 4-7). We allocate physical space on demand.
physical_id = table.allocate_block_for_token(request_id=42, token_index=5)
print(f"Logical token mapped to physical block: {physical_id}")
FP16/BF16 Value: [0.12345678] (16 bits of precision)
↓ Quantization via Scaling Factor (e.g., scale = 127.0)
INT8 Value: [15] (8 bits of precision, 50% memory saved)
Sequence: [T1] [T2] [T3] [T4] [T5] [T6] [T7]
└───── Window Size (4) ─────┘
(T1 and T2 are evicted from active memory)
# Example: Initializing a vLLM engine with memory optimizations
from vllm import LLMEngine, EngineArgs
# Define engine arguments optimized for memory management
engine_args = EngineArgs(
model="meta-llama/Llama-3-8B-Instruct",
# 1. Quantization: Reduces KV cache footprint
quantization="fp8",
# 2. Paged KV Cache: Eliminates physical fragmentation
gpu_memory_utilization=0.90,
# 3. Dynamic Batching: Prevents static padding overhead
max_num_seqs=256
)
# Initialize the engine with built-in memory guardrails
engine = LLMEngine.from_engine_args(engine_args)
Механизмы добавления данных
В реализациях хранятся буферы K/V для каждого слоя, и на каждом шаге записывается новая строка. Ошибки в этой части незаметно портят работу механизма внимания — предпочтительнее использовать проверенные временем ядра.
Математика использования памяти
Количество байт ≈ 2 (K и V) × количество слоев × kv_heads × head_dim × seq_len × bytes_per_element. Длинные контексты в крупных моделях могут потреблять больше памяти, чем сами веса.
Техники оптимизации
Перепроектирование архитектуры: MHA, GQA, MQA
Механизмы групповых и множественных запросов сокращают количество элементов KV heads, что позволяет уменьшить объем кэша при одинаковой длине последовательности.
PagedAttention
Обрабатывайте данные KV как страницированную виртуальную память: выделяйте блоки по мере необходимости, снижайте фрагментацию и обеспечивайте более эффективную группировку данных.
Квантизация KV
Храните записи кэша с меньшей точностью, если это допускают ограничения по качеству.
Метод скользящего окна/исключения
Удаляйте или сжимайте менее важные токены, если задача позволяет ограниченный объем внимания.
Матрица решений для производственной среды
Используйте модели GQA/MQA при ограничениях памяти; включайте механизм пагинации внимания для высокой конкурентности; квантизируйте данные KV только после оценки; применяйте метод окон для чатов, требующих локального контекста.
Кэш KV представляет собой невидимую базу данных для авторегрессивного декодирования. Оптимизируйте его явно — архитектуру, механизм пагинации, точность хранения данных и процедуры удаления — иначе это повлечет за собой затраты в виде использования дополнительных GPU и увеличения задержек.
Измеряйте количество токенов в секунду и объем данных KV на токен одновременно. «Более быстрый» ядро, удваивающее объем кэша, может показать худшие результаты при работе с несколькими пользователями одновременно.
Задокументируйте максимальный объем контекста на единицу продукта, чтобы продукт не мог незаметно запросить 128 кб памяти на карте, предназначенной для 8 кб.
При группировке данных помните, что шаги декодирования должны синхронизироваться с самой длинной последовательностью; политика заполнения и непрерывная группировка имеют такое же значение, как и чистая пропускная способность.
Измеряйте количество токенов в секунду и количество байт KV на токен одновременно. «Более быстрый» ядро, которое удваивает объем кэша, может проигрывать при нагрузке от нескольких пользователей.
Задокументируйте максимальный объем контекста на каждый SKU, чтобы продукт не мог незаметно запрашивать 128 кб на карту, предназначенную для 8 кб.
При группировке данных помните, что шаги декодирования должны синхронизироваться с самой длинной последовательностью; политика заполнения и непрерывная группировка имеют такое же значение, как и чистая пропускная способность.
Измеряйте количество токенов в секунду и количество байт KV на токен одновременно. «Более быстрый» ядро, которое удваивает объем кэша, может проигрывать при нагрузке от нескольких пользователей.
Задокументируйте максимальный объем контекста на каждый SKU, чтобы продукт не мог незаметно запрашивать 128 кб на карту, предназначенную для 8 кб.
При группировке данных помните, что шаги декодирования должны синхронизироваться с самой длинной последовательностью; политика заполнения и непрерывная группировка имеют такое же значение, как и чистая пропускная способность.
Измеряйте количество токенов в секунду и количество байт KV на токен одновременно. «Более быстрый» ядро, которое удваивает объем кэша, может проигрывать при нагрузке от нескольких пользователей.
Задокументируйте максимальный объем контекста на каждый SKU, чтобы продукт не мог незаметно запрашивать 128 кб на карту, предназначенную для 8 кб.
При группировке данных помните, что шаги декодирования должны синхронизироваться с самой длинной последовательностью; политика заполнения и непрерывная группировка имеют такое же значение, как и чистая пропускная способность.
Измеряйте количество токенов в секунду и количество байт KV на токен одновременно. «Более быстрый» ядро, которое удваивает объем кэша, может проигрывать при нагрузке от нескольких пользователей.
Задокументируйте максимальный объем контекста на каждый SKU, чтобы продукт не мог незаметно запрашивать 128 кб на карту, предназначенную для 8 кб.
При группировке данных помните, что шаги декодирования должны синхронизироваться с самой длинной последовательностью; политика заполнения и непрерывная группировка имеют такое же значение, как и чистая пропускная способность.
Измеряйте количество токенов в секунду и количество байт KV на токен одновременно. «Более быстрый» ядро, которое удваивает объем кэша, может проигрывать при нагрузке от нескольких пользователей.
Задокументируйте максимальный объем контекста на каждый SKU, чтобы продукт не мог незаметно запрашивать 128 кб на карту, предназначенную для 8 кб.
При группировке данных помните, что шаги декодирования должны синхронизироваться с самой длинной последовательностью; политика заполнения и непрерывная группировка имеют такое же значение, как и чистая пропускная способность.
Измеряйте количество токенов в секунду и количество байт KV на токен одновременно. «Более быстрый» ядро, которое удваивает объем кэша, может проигрывать при нагрузке от нескольких пользователей.
Задокументируйте максимальный объем контекста на каждый SKU, чтобы продукт не мог незаметно запрашивать 128 кб на карту, предназначенную для 8 кб.
При группировке данных помните, что шаги декодирования должны синхронизироваться с самой длинной последовательностью; политика заполнения и непрерывная группировка имеют такое же значение, как и чистая пропускная способность.
Измеряйте количество токенов в секунду и количество байт KV на токен одновременно. «Более быстрый» ядро, которое удваивает объем кэша, может проигрывать при нагрузке от нескольких пользователей.
Задокументируйте максимальный объем контекста на каждый SKU, чтобы продукт не мог незаметно запросить 128 кб на карту, предназначенную для 8 кб.
При группировке данных помните, что шаги декодирования должны синхронизироваться с самой длинной последовательностью; политика заполнения и непрерывная группировка имеют такое же значение, как и чистая пропускная способность.
Измеряйте количество токенов в секунду и количество байт KV на токен одновременно. «Более быстрый» ядро, которое удваивает объем кэша, может проигрывать при нагрузке от нескольких пользователей.
Задокументируйте максимальный объем контекста на каждый SKU, чтобы продукт не мог незаметно запросить 128 кб на карту, предназначенную для 8 кб.
При группировке данных помните, что шаги декодирования должны синхронизироваться с самой длинной последовательностью; политика заполнения и непрерывная группировка имеют такое же значение, как и чистая пропускная способность.
Измеряйте количество токенов в секунду и количество байт KV на токен одновременно. «Более быстрый» ядро, которое удваивает объем кэша, может проигрывать при нагрузке от нескольких пользователей.
Задокументируйте максимальный объем контекста на каждый SKU, чтобы продукт не мог незаметно запрашивать 128 кб на карту, предназначенную для 8 кб.
При группировке данных помните, что шаги декодирования должны синхронизироваться с самой длинной последовательностью; политика заполнения и непрерывная группировка имеют такое же значение, как и чистая пропускная способность.
Измеряйте количество токенов в секунду и количество байт KV на токен одновременно. «Более быстрый» ядро, которое удваивает объем кэша, может проигрывать при нагрузке от нескольких пользователей.
Задокументируйте максимальный объем контекста на каждый SKU, чтобы продукт не мог незаметно запрашивать 128 кб на карту, предназначенную для 8 кб.
При группировке данных помните, что шаги декодирования должны синхронизироваться с самой длинной последовательностью; политика заполнения и непрерывная группировка имеют такое же значение, как и чистая пропускная способность.
Измеряйте количество токенов в секунду и количество байт KV на токен одновременно. «Более быстрый» ядро, которое удваивает объем кэша, может проигрывать при нагрузке от нескольких пользователей.
Задокументируйте максимальный объем контекста на каждый SKU, чтобы продукт не мог незаметно запрашивать 128 кб на карту, предназначенную для 8 кб.
При группировке данных помните, что шаги декодирования должны синхронизироваться с самой длинной последовательностью; политика заполнения и непрерывная группировка имеют такое же значение, как и чистая пропускная способность.
Измеряйте количество токенов в секунду и количество байт KV на токен одновременно. «Более быстрый» ядро, которое удваивает объем кэша, может проигрывать при нагрузке от нескольких пользователей.
Задокументируйте максимальный объем контекста на каждый SKU, чтобы продукт не мог незаметно запрашивать 128 кб на карту, предназначенную для 8 кб.
При группировке данных помните, что шаги декодирования должны синхронизироваться с самой длинной последовательностью; политика заполнения и непрерывная группировка имеют такое же значение, как и чистая пропускная способность.
Измеряйте количество токенов в секунду и объем данных KV в токене одновременно. «Более быстрый» ядро, удваивающее объем кэша, может проигрывать при нагрузке от нескольких пользователей.
Асимметрия между предварительным заполнением и декодированием также влияет на автомасштабирование: резкий рост длины запроса увеличивает нагрузку на предварительную обработку, в то время как длительная генерация данных снижает пропускную способность. Необходимо предусмотреть пулы памяти для обоих случаев.
Сравнивайте заявления поставщиков о «длинном контексте», запрашивая информацию об объеме данных KV при максимальной длине с учетом вашего типа данных и размера группы — а не просто количество токенов, указанное в маркетинговых материалах.
Политики вытеснения должны учитывать особенности продукта: для сотрудников поддержки, цитирующих предыдущие ответы, лучше подходит краткое резюме утраченных фрагментов, чем их полное игнорирование.
Расписания непрерывной группировки данных, игнорирующие фрагментацию данных KV, все равно приводят к нехватке памяти; страницированная структура и механизмы дефрагментации должны включаться в одну и ту же версию продукта.
Асимметрия предзаполнения/декодирования также влияет на автомасштабирование: резкий рост длины запроса сильно нагружает процесс предзаполнения, в то время как длительная генерация нагружает пропускную способность. Для обоих случаев необходимы пулы памяти.
Сравнивайте заявления поставщиков о «длинном контексте», запрашивая количество байт KV при максимальной длине с учётом типа данных и размера пакета — а не маркетинговые цифры.
Политики вытеснения должны учитывать особенности продукта: для сотрудников поддержки, цитирующих предыдущие этапы обработки, лучше подходит краткое изложение пропущенных данных, чем их молчаливое игнорирование.
Расписатели непрерывной обработки пакетов, игнорирующие фрагментацию KV, всё равно приводят к нехватке памяти; страницированная структура и механизмы дефрагментации должны входить в одну и ту же версию продукта.
Асимметрия предзаполнения/декодирования также влияет на автомасштабирование: резкий рост длины запроса сильно нагружает процесс предзаполнения, в то время как длительная генерация нагружает пропускную способность. Для обоих случаев необходимы пулы памяти.
Сравните заявления поставщиков о «длинном контексте», попросив количество байт KV при максимальной длине с учётом вашего типа данных и размера пакета — а не маркетинговых токенов.
Политики удаления должны учитывать особенности продукта: подведение итогов пропущенных фрагментов лучше, чем молчаливое забывание, для сотрудников поддержки, которые цитируют предыдущие запросы.
Расписания непрерывной группировки, игнорирующие фрагментацию KV, всё равно приводят к нехватке памяти; структуры с разделением на страницы и механизмы дефрагментации должны внедряться одновременно.
Асимметрия в заполнении данных и декодировании также влияет на автомасштабирование: резкий рост длины запроса нагружает процесс заполнения данных, а длительная генерация — пропускную способность. Необходимы пулы памяти для обоих случаев.
Сравните заявления поставщиков о «длинном контексте», попросив количество байт KV при максимальной длине с учётом вашего типа данных и размера пакета — а не маркетинговых токенов.
Политики удаления должны учитывать особенности продукта: подведение итогов пропущенных фрагментов лучше, чем молчаливое забывание, для сотрудников поддержки, которые цитируют предыдущие запросы.