49 vLLM / PagedAttention
Контекст
При ОБСЛУЖИВАНИИ 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
Почему это важно
Один из самых используемых open-source движков инференса; показал, что системные идеи из ОС напрямую двигают эффективность LLM-серва (cost/latency в проде). Бонус: блоки можно ШЕРИТЬ между запросами (общий системный промпт/префикс) по copy-on-write.
Связи
vLLM считает само внимание FlashAttention-ядрами, а PagedAttention управляет памятью между запросами. Два уровня оптимизации инференса: внутри attention (Flash) и вокруг него (Paged).
KV-кэш существует именно из-за авторегрессионного внимания трансформера: чтобы не пересчитывать прошлое на каждом шаге, K и V кэшируют. vLLM делает этот кэш дешёвым в управлении — прямое следствие архитектуры #32.
Открытые модели вроде LLaMA нужно где-то эффективно запускать — vLLM стал стандартным движком сёрвинга для них. Открытые веса + эффективный инференс = практичный self-hosting LLM.
Вопросы пытливого ума
Почему именно «системная» идея дала такой прирост, а не новая модель?
Потому что узкое место сёрвинга было не в качестве модели, а в утилизации памяти: при 20–40% полезного использования две трети дорогой VRAM простаивали. Убрав фрагментацию, vLLM не улучшил ни одну модель — он позволил уместить в ту же память кратно больше запросов. Урок: часть прогресса в ML — это классическая системная инженерия, а не новые архитектуры.
Что даёт шеринг префиксов и где он реально полезен?
Если у многих запросов общий длинный системный промпт (или few-shot примеры), его KV-блоки можно хранить один раз и ссылаться из всех запросов (copy-on-write, как разделяемые страницы ОС). Это экономит память и время на повторный prefill. Особенно полезно в продакшене, где тысячи запросов делят один большой системный промпт.
Есть ли цена у пейджинга — он бесплатен?
Почти, но не совсем: появляется накладной расход на таблицу блоков и косвенную адресацию, а attention-ядро должно уметь работать с несмежной памятью (специальное PagedAttention-ядро). Это усложняет реализацию по сравнению с непрерывным буфером. Но выигрыш в утилизации памяти многократно перекрывает эти издержки — классический системный размен «чуть сложнее код ради сильно лучшего ресурса».
Что читать в оригинале
Читать ключевое — аналогия пейджинга, фрагментация KV-кэша, шеринг префиксов.