Головна / Статті

Кеш KV — це невидима база даних, яка споживає пам’ять GPU під час декодування LLM.

Кеш KV тихо споживає пам’ять GPU під час інференсу LLM: попереднє заповнення проти декодування, математика KV, GQA та MQA, PagedAttention, політики квантування та видалення.

Видавець: REACTAPP.TOP9 хв читання2109 слів

Джерела: 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 при максимальній довжині з урахуванням вашого типу даних та розміру пакета — а не маркетингових токенів.

Політики видалення мають бути обґрунтовані характеристиками продукту: узагальнення втрачених фрагментів краще за мовчазне забування для співробітників підтримки, які посилаються на попередні запити.