Перейти к содержанию

vLLM и PagedAttention: пагинация KV Cache

~6 минут чтения

Предварительно: KV Cache | MQA/GQA

Проблема: фрагментация памяти KV cache

При авторегрессивном инференсе LLM каждый запрос занимает KV cache -- блок GPU памяти для хранения Key/Value тензоров всех сгенерированных токенов. Проблема: длина ответа неизвестна заранее.

Наивный подход -- зарезервировать максимум (например, 4096 токенов для каждого запроса). Но реальные ответы в среднем 200-500 токенов. Результат:

  • 60-80% GPU памяти потрачено впустую на незаполненные резервации
  • Меньше параллельных запросов (batch size ограничен)
  • Throughput падает в 2-5x от возможного

Аналогия: представьте ресторан, где для каждого гостя резервируют 10-местный стол, потому что "вдруг придет большая компания". В итоге зал полупустой, а очередь на улице.

Ключевой инсайт: KV cache можно аллоцировать динамически, как операционная система управляет виртуальной памятью -- страницами (pages). Именно это делает PagedAttention.


PagedAttention: как это работает

Идея: виртуальная память для KV cache

PagedAttention (Kwon et al., UC Berkeley, 2023) заимствует концепцию из OS:

  1. KV cache делится на блоки фиксированного размера (например, 16 токенов на блок)
  2. Блоки аллоцируются по мере необходимости -- не заранее
  3. Блоки не обязаны быть смежными в физической памяти -- таблица маппинга (page table) связывает логические позиции с физическими блоками
  4. Освобожденные блоки переиспользуются другими запросами
graph LR
    subgraph "Логический KV Cache (Запрос A)"
        A1["Block 0<br/>tokens 0-15"]
        A2["Block 1<br/>tokens 16-31"]
        A3["Block 2<br/>tokens 32-40"]
    end

    subgraph "Физическая GPU память"
        P1["Физ. блок 7"]
        P2["Физ. блок 2"]
        P3["Физ. блок 12"]
        P4["Физ. блок 5<br/>(свободен)"]
        P5["Физ. блок 9<br/>(Запрос B)"]
    end

    A1 --> P1
    A2 --> P2
    A3 --> P3

    style A1 fill:#e8eaf6,stroke:#3f51b5
    style A2 fill:#e8eaf6,stroke:#3f51b5
    style A3 fill:#e8eaf6,stroke:#3f51b5
    style P1 fill:#e8f5e9,stroke:#4caf50
    style P2 fill:#e8f5e9,stroke:#4caf50
    style P3 fill:#e8f5e9,stroke:#4caf50
    style P4 fill:#fff3e0,stroke:#ef6c00
    style P5 fill:#f3e5f5,stroke:#9c27b0

Блоки запроса A разбросаны по физической памяти (блоки 7, 2, 12), но логически -- последовательны. Page table обеспечивает маппинг.

Почему это дает такой эффект

  • Нет внутренней фрагментации: последний блок может быть заполнен частично (40 токенов = 2 полных блока + 1 частичный). Потери: максимум 1 блок на запрос вместо тысяч токенов
  • Нет внешней фрагментации: блоки одного размера, любой свободный блок подходит любому запросу
  • Утилизация памяти ~98% вместо ~20-40%

Экономия памяти

Наивный подход: \(N_\text{requests} \times S_\text{max} \times \text{KV\_per\_token}\)

PagedAttention: \(N_\text{requests} \times S_\text{actual} \times \text{KV\_per\_token} + \epsilon\)

Где \(\epsilon\) -- overhead page table (~0.1%).

Пример: 100 запросов, \(S_\text{max} = 4096\), \(S_\text{actual} = 300\) (среднее):

  • Наивный: \(100 \times 4096 = 409{,}600\) слотов
  • Paged: \(100 \times 300 = 30{,}000\) слотов (13.6x экономия)

Prefix Caching

Многие запросы начинаются с одинакового префикса -- system prompt, few-shot примеры, общий контекст документа. PagedAttention позволяет шарить блоки между запросами:

Запрос 1: [system prompt][user question 1][response 1]
Запрос 2: [system prompt][user question 2][response 2]
                ^^^^^^^^^
                Одни и те же физические блоки!

Блоки system prompt аллоцируются один раз и маппятся в page tables обоих запросов (copy-on-write семантика). Для system prompt длиной 2000 токенов и 100 параллельных запросов: экономия ~200K слотов KV cache.

Automatic Prefix Caching (APC) в vLLM 0.4+ делает это автоматически: хэширует токены блока и переиспользует при совпадении.


vLLM: архитектура

vLLM -- продакшен serving framework, построенный вокруг PagedAttention:

Ключевые компоненты

Компонент Роль
Scheduler Выбирает какие запросы обрабатывать (preemption, priority)
KV Cache Manager Аллокация/деаллокация блоков, page table
Block Allocator Управление пулом физических блоков
Model Executor Запуск модели на GPU (tensor parallel, pipeline parallel)
Tokenizer Токенизация входа
Sampling Стратегии сэмплирования (top-p, top-k, beam search)

Continuous Batching

Традиционный batching: ждем пока все запросы в batch закончат генерацию. Короткие ответы простаивают, ожидая длинные.

vLLM использует continuous batching (также: iteration-level batching):

  1. На каждом шаге генерации: проверить есть ли завершенные запросы
  2. Завершенные -- убрать из batch, освободить блоки
  3. Из очереди -- добавить новые запросы на освободившееся место
  4. Результат: GPU всегда загружен на ~100%

Бенчмарки: vLLM vs конкуренты

Framework Throughput (tok/s) TTFT (ms) Concurrency support Особенности
vLLM High Low Excellent (100+) PagedAttention, prefix caching
SGLang Very High Very Low Excellent RadixAttention, compiler
TensorRT-LLM Highest Lowest (low concurrency) Degrades at 100+ NVIDIA-only, FP8
TGI Good Good Good HuggingFace, simple setup
LMDeploy High Medium Good TurboMind engine

vLLM не всегда быстрее

Распространенное заблуждение: "vLLM = самый быстрый". На деле:

  • TensorRT-LLM быстрее на NVIDIA при низкой concurrency (оптимизированные CUDA kernels)
  • SGLang обгоняет vLLM на 1.2-2x благодаря RadixAttention и compiler optimizations
  • vLLM выигрывает на высокой concurrency (100+ запросов) и простоте использования

Выбор зависит от workload, не от "лучший фреймворк".


Практика: запуск vLLM

Минимальный пример (Python)
# Установка: pip install vllm
from vllm import LLM, SamplingParams

# Загрузка модели (автоматически использует PagedAttention)
llm = LLM(
    model="meta-llama/Llama-3.1-8B-Instruct",
    tensor_parallel_size=1,      # количество GPU
    gpu_memory_utilization=0.9,   # сколько GPU RAM отдать под KV cache
    max_model_len=8192,           # максимальная длина контекста
    enable_prefix_caching=True,   # Automatic Prefix Caching
)

# Параметры генерации
params = SamplingParams(
    temperature=0.7,
    top_p=0.9,
    max_tokens=512,
)

# Batch inference (vLLM автоматически батчит)
prompts = [
    "Explain PagedAttention in simple terms.",
    "What is the capital of France?",
    "Write a Python function for binary search.",
]
outputs = llm.generate(prompts, params)

for output in outputs:
    print(f"Prompt: {output.prompt[:50]}...")
    print(f"Output: {output.outputs[0].text[:100]}...")
    print()
Запуск как API server
# OpenAI-compatible API server
python -m vllm.entrypoints.openai.api_server \
    --model meta-llama/Llama-3.1-8B-Instruct \
    --tensor-parallel-size 2 \
    --gpu-memory-utilization 0.9 \
    --max-model-len 8192 \
    --enable-prefix-caching \
    --port 8000

# Клиент (совместим с OpenAI SDK)
curl http://localhost:8000/v1/completions \
    -H "Content-Type: application/json" \
    -d '{
        "model": "meta-llama/Llama-3.1-8B-Instruct",
        "prompt": "Explain attention mechanism",
        "max_tokens": 256,
        "temperature": 0.7
    }'

Оптимизация производительности

Ключевые параметры vLLM

Параметр Что делает Рекомендация
gpu_memory_utilization Доля GPU RAM для KV cache 0.85-0.95 (выше = больше batch)
max_model_len Максимум токенов Минимальный необходимый
tensor_parallel_size Количество GPU По количеству доступных GPU
enable_prefix_caching APC Включать при общих system prompts
block_size Размер KV block 16 (default, менять редко)
swap_space CPU offload (GB) 4-8 для preemption
max_num_seqs Max batch size По throughput target

Когда что использовать

  • High throughput (API сервис): gpu_memory_utilization=0.95, max_num_seqs=256, prefix caching ON
  • Low latency (interactive): max_num_seqs=32, speculative decoding, quantized model
  • Long context (RAG): уменьшить max_num_seqs, увеличить max_model_len

Interview вопросы

Conceptual

Q: Как PagedAttention решает проблему фрагментации KV cache?

Strong: "KV cache разбивается на фиксированные блоки (как страницы в виртуальной памяти). Блоки аллоцируются по мере генерации токенов, не заранее. Page table маппит логические позиции в физические блоки. Нет внутренней фрагментации (максимум 1 блок waste на запрос), нет внешней (блоки одного размера). Утилизация памяти ~98% вместо ~30%."

Red flag: "vLLM просто быстрее работает с памятью" (не объясняет механизм).

Q: Что такое continuous batching и зачем он нужен?

Традиционный batching ждет завершения самого длинного запроса. Continuous batching на каждом шаге убирает завершенные запросы и добавляет новые из очереди. GPU всегда занят, throughput растет на 2-5x.

Design

Q: Как бы вы спроектировали LLM serving для 1000 RPS?

Strong: "vLLM или SGLang с tensor parallelism на нескольких GPU. Prefix caching для system prompt. Load balancer перед несколькими инстансами. Quantization (INT8) для уменьшения memory footprint. Мониторинг TTFT/TPS/queue depth. Auto-scaling по GPU utilization."

Coding

Q: Напишите Python клиент для vLLM OpenAI-compatible API с retry и rate limiting.

См. практические примеры выше.


See Also

Sources

  1. Efficient Memory Management for LLMs with PagedAttention (Kwon et al., 2023)
  2. vLLM Official Documentation
  3. vLLM Blog: PagedAttention
  4. SGLang: Efficient Execution of Structured LLM Programs (Zheng et al., 2024)