Эпоха 6 · Генеративка и системы · 2023

49 vLLM / PagedAttention

Efficient Memory Management for LLM Serving with PagedAttention · Kwon, Li, Zhuang и др. · UC Berkeley · SOSP
🟧 оригинал выборочно~1.5–2 чоригинал ↗
Суть за 20 секунд. Управление памятью KV-кэша по образцу виртуальной памяти ОС: режем кэш на блоки-«страницы», не обязанные лежать подряд. Уходит фрагментация → больше запросов в батче → пропускная способность LLM-сёрвинга ×2–4. Движок vLLM.

Контекст

При ОБСЛУЖИВАНИИ LLM основная память уходит на KV-кэш — сохранённые ключи/значения контекста каждого запроса; он растёт с каждым новым токеном и у запросов разной длины.

Идея и механизм

Наивное хранение (непрерывный буфер на макс. длину под каждый запрос) даёт огромную фрагментацию и over-reservation → мало запросов влезает, низкий throughput. Решение по аналогии с виртуальной памятью ОС: режем KV-кэш на блоки фиксированного размера («страницы»), не обязанные лежать подряд; «таблица страниц» маппит логические позиции токенов на физические блоки.

системы Пейджинг для KV-кэша: откуда берётся ×2–4

Размер KV-кэша одного запроса = 2 · L · nheads · dhead · ntokens (K и V на каждый слой и токен) — и растёт по мере генерации. Беда наивного подхода — два вида фрагментации:

  • Внутренняя: резервируем буфер на максимальную длину, но запрос короче → слоты простаивают.
  • Внешняя: запросы разной длины оставляют «дыры», в которые ничего не влезает.

На практике полезно использовалось лишь ~20–40% памяти. PagedAttention: кэш — это блоки фиксированного размера, а таблица блоков связывает логические позиции токенов с произвольными физическими блоками. Память выделяется по мере роста контекста, дыр почти нет → утилизация под ~100% → в ту же память влезает кратно больше запросов → больше батч → throughput ×2–4 при той же латентности.

Python Таблица блоков (идея пейджинга)
# логические позиции токенов → произвольные физические блоки
block_size = 16
block_table = {}                    # request_id → [phys_block, ...]

def append_token(req, kv, pool):
    blocks = block_table.setdefault(req, [])
    if len(blocks) * block_size <= req.len:   # нужен новый блок?
        blocks.append(pool.alloc())           # выделяем по мере роста
    write(blocks[-1], kv)                       # без непрерывности и over-reservation
наивно (непрерывно): резерв простаивает → paged (блоки): блоки где угодно, без дыр
Непрерывный буфер резервирует лишнее и фрагментируется; блочный (paged) кэш выделяет память по чуть-чуть и заполняет её плотно.
Аналогия. Та же история, что с памятью в операционных системах. Выдавать каждой программе непрерывный кусок RAM «с запасом» — расточительно и оставляет дыры. ОС режет память на страницы и раздаёт их вразнобой, ведя таблицу соответствий. PagedAttention буквально переносит этот приём ОС на KV-кэш LLM — отсюда и название.

Почему это важно

Один из самых используемых open-source движков инференса; показал, что системные идеи из ОС напрямую двигают эффективность LLM-серва (cost/latency в проде). Бонус: блоки можно ШЕРИТЬ между запросами (общий системный промпт/префикс) по copy-on-write.

Связи

← использует48. FlashAttention

vLLM считает само внимание FlashAttention-ядрами, а PagedAttention управляет памятью между запросами. Два уровня оптимизации инференса: внутри attention (Flash) и вокруг него (Paged).

← обслуживает32. Transformer

KV-кэш существует именно из-за авторегрессионного внимания трансформера: чтобы не пересчитывать прошлое на каждом шаге, K и V кэшируют. vLLM делает этот кэш дешёвым в управлении — прямое следствие архитектуры #32.

→ для open-моделей50. LLaMA

Открытые модели вроде LLaMA нужно где-то эффективно запускать — vLLM стал стандартным движком сёрвинга для них. Открытые веса + эффективный инференс = практичный self-hosting LLM.

Вопросы пытливого ума

Почему именно «системная» идея дала такой прирост, а не новая модель?

Потому что узкое место сёрвинга было не в качестве модели, а в утилизации памяти: при 20–40% полезного использования две трети дорогой VRAM простаивали. Убрав фрагментацию, vLLM не улучшил ни одну модель — он позволил уместить в ту же память кратно больше запросов. Урок: часть прогресса в ML — это классическая системная инженерия, а не новые архитектуры.

Что даёт шеринг префиксов и где он реально полезен?

Если у многих запросов общий длинный системный промпт (или few-shot примеры), его KV-блоки можно хранить один раз и ссылаться из всех запросов (copy-on-write, как разделяемые страницы ОС). Это экономит память и время на повторный prefill. Особенно полезно в продакшене, где тысячи запросов делят один большой системный промпт.

Есть ли цена у пейджинга — он бесплатен?

Почти, но не совсем: появляется накладной расход на таблицу блоков и косвенную адресацию, а attention-ядро должно уметь работать с несмежной памятью (специальное PagedAttention-ядро). Это усложняет реализацию по сравнению с непрерывным буфером. Но выигрыш в утилизации памяти многократно перекрывает эти издержки — классический системный размен «чуть сложнее код ради сильно лучшего ресурса».

Что читать в оригинале

Читать ключевое — аналогия пейджинга, фрагментация KV-кэша, шеринг префиксов.