Кеш KV — це невидима база даних, яка споживає пам’ять GPU під час декодування LLM.
Кеш KV тихо споживає пам’ять GPU під час інференсу LLM: попереднє заповнення проти декодування, математика KV, GQA та MQA, PagedAttention, політики квантування та видалення.
Джерела: The KV Cache: AI’s Unseen Database Dominating GPU Memory
Справжній бутель у інференсі LLM
Під час інференсу обчислювальні ресурси GPU часто залишаються незайнятими, тоді як пропускна здатність пам’яті живить постійно зростаючу кеш-пам’ять типу key–value. Кожен новий токен додає тензори K/V, які необхідно знову читати на наступних етапах. Системи, які ігнорують зростання обсягу кеш-пам’яті, досягають меж щодо обсягу контексту та затримки задовго до того, як закінчаться FLOPs.
Як це працює: механізм кеш-пам’яті типу KV
Механізм уваги потребує ключів та значень кожного попереднього токена. Їх кешування дозволяє уникнути повторного обчислення всього префікса на кожному кроці. Однак це призводить до зростання обсягу пам’яті пропорційно довжині послідовності × кількості шарів × кількості голів × розміру вектора.
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-голів, тож для однакової довжини послідовності потрібна менша кеш-пам’ять.
PagedAttention
Ставіться до KV як до сторінкованої віртуальної пам’яті: виділяйте блоки за потребою, зменшуйте фрагментацію та дозволяйте більшу групування даних.
Квантація KV
Зберігайте записи кешу з меншою точністю, якщо це дозволяють обмеження щодо якості.
Слайдинг-вікно / видалення
Видаляйте або стискайте віддалені токени, якщо завдання допускає обмежений проміжок уваги.
Матриця рішень для продакшну
Вибирайте моделі GQA/MQA, коли є обмеження пам’яті; увімкніть сторінковану увагу для високої конкурентності; квантуйте KV лише після оцінки; використовуйте вікна для чату з потребою у локальному контексті.
Заключення
Кеш KV — це невидима база даних для авторегресивного декодування. Оптимізуйте його прямо — архітектуру, сторінкування, точність, механізм видалення — інакше доведеться платити за це GPU та затримками.
Вимірюйте кількість токенів на секунду та кількість байтів 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 на токен одночасно. „Швидший“ ядро, яке подвоює обсяг кешу, може виявитися менш ефективним під навантаженням кількох користувачів.
Описуйте максимальний обсяг контексту для кожного SKU, щоб продукт не міг таємно запитувати 128 кБ на картці, призначеній для 8 кБ.
Під час батчування пам’ятайте, що кроки декодування мають синхронізуватися з найдовшою послідовністю; політика заповнення та безперервне батчування мають таке ж значення, як і пропускна здатність каналу.
Вимірюйте одночасно кількість токенів на секунду та кількість байтів KV на токен. „Швидший“ ядро, яке подвоює обсяг кешу, може погіршити продуктивність під навантаженням кількох користувачів.
Асиметрія у процесах попереднього заповнення та декодування також впливає на автомасштабування: різке збільшення довжини запиту навантажує процес попереднього заповнення, тоді як довге генерування — пропускну здатність каналу. Необхідно створювати пули ресурсів для обох сценаріїв.
Порівнюйте твердження виробників щодо „довгого контексту“, запитуючи кількість байтів KV при максимальній довжині з урахуванням вашого типу даних та розміру батчу — а не кількість маркетингових токенів.
Політики видалення даних мають враховувати особливості продукту: для співробітників підтримки, які посилаються на попередні кроки, краще узагальнювати вилучені фрагменти, ніж просто ігнорувати їх.
Скрейдлери безперервного батчування, які ігнорують фрагментацію даних KV, все одно призводять до нестачі пам’яті; структури з використанням сторінок та механізми дефрагментації мають бути включені до одного пакету оновлень.
Асиметрія попереднього заповнення/декодування також впливає на автомасштабування: різкі збільшення довжини запиту навантажують ресурси попереднього заповнення, тоді як довге генерування навантажує пропускну здатність. Для обох форм потрібні пули розмірів.
Порівняйте твердження постачальників щодо „довгого контексту“, запитавши кількість байтів KV при максимальній довжині з урахуванням вашого типу даних та розміру пакета — а не маркетингові цифри.
Політики видалення мають враховувати особливості продукту: узагальнення вилучених фрагментів краще, ніж мовчазне забування, для співробітників підтримки, які посилаються на попередні кроки.
Скриптори безперервного батчування, які ігнорують фрагментацію KV, все одно призводять до нестачі пам’яті; структури з сторінками та механізми дефрагментації мають бути включені до одного оновлення.
Асиметрія попереднього заповнення/декодування також впливає на автомасштабування: різкі збільшення довжини запиту навантажують ресурси попереднього заповнення, тоді як довге генерування навантажує пропускну здатність. Для обох форм потрібні пули розмірів.
Порівняйте твердження постачальників щодо «довгого контексту», запитавши кількість байтів KV при максимальній довжині з урахуванням вашого типу даних та розміру пакета — а не маркетингових токенів.
Політики видалення мають бути обґрунтовані характеристиками продукту: узагальнення втрачених фрагментів краще за мовчазне забування для співробітників підтримки, які посилаються на попередні запити.
Скрейдлери для безперервного оброблення пакетів, які ігнорують фрагментацію KV, все одно спричиняють проблеми з пам’яттю; структури з сторінками та механізми дефрагментації мають бути включені до одного оновлення.
Асиметрія у заповненні даних та їх декодуванні також впливає на автомасштабування: різкий зростання довжини запиту навантажує процес заповнення даних, тоді як довге генерування — пропускну здатність. Необхідно мати окремі пули місця для обох форматів.
Порівняйте твердження постачальників щодо «довгого контексту», запитавши кількість байтів KV при максимальній довжині з урахуванням вашого типу даних та розміру пакета — а не маркетингових токенів.
Політики видалення мають бути обґрунтовані характеристиками продукту: узагальнення втрачених фрагментів краще за мовчазне забування для співробітників підтримки, які посилаються на попередні запити.