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:
- KV cache делится на блоки фиксированного размера (например, 16 токенов на блок)
- Блоки аллоцируются по мере необходимости -- не заранее
- Блоки не обязаны быть смежными в физической памяти -- таблица маппинга (page table) связывает логические позиции с физическими блоками
- Освобожденные блоки переиспользуются другими запросами
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):
- На каждом шаге генерации: проверить есть ли завершенные запросы
- Завершенные -- убрать из batch, освободить блоки
- Из очереди -- добавить новые запросы на освободившееся место
- Результат: 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¶
- KV Cache Optimization -- RadixAttention (SGLang), KV eviction strategies (H2O, StreamingLLM)
- Inference Engines -- vLLM vs SGLang vs TensorRT-LLM: decision framework
- Speculative Decoding -- complementary: PagedAttention + speculative = latency + throughput
- Fine-Tuning (LoRA) -- vLLM PEFT serving: LoRA adapters в продакшене
- Production Deploy -- end-to-end deployment patterns