Кэширование
Большая часть того, что языковая модель считает для запроса, уже когда-то была посчитана. Системный промпт (постоянные инструкции, которые приложение ставит перед каждым разговором) одинаков для всех пользователей. Чат на каждом шаге заново отправляет всю переписку. Агент — модель, которая в цикле вызывает инструменты, пока не решит задачу, — сотни раз перечитывает одни и те же описания инструментов. Кэширование — это искусство не делать одну и ту же работу дважды, и наряду с батчингом это самый сильный рычаг, влияющий на цену и скорость работы LLM.
Напоминание: KV-кэш
В главе Быстрый инференс мы познакомились с KV-кэшем: при генерации модель хранит ключи и значения всех предыдущих токенов (только их и читают более поздние токены), поэтому каждый новый токен считает только свои. Вся эта глава держится на трёх фактах оттуда:
- Ключи и значения токена зависят только от него самого и от токенов перед ним, потому что внимание причинное. Один и тот же префикс всегда даёт один и тот же кэш — неважно, какому запросу он принадлежит и что идёт после него.
- Построить кэш для промпта — это и есть работа префилла, а для длинного промпта префилл — основная часть работы до первого токена.
- Кэш большой: около 128 КБ на токен для модели размера Llama 3 8B, гигабайты для длинного диалога, и всё это в памяти GPU, объём которой — десятки гигабайт.
Первый факт делает переиспользование возможным: уже виденный префикс может пропустить свой префилл. Два других делают его выгодным.
Кэширование префиксов: общее у запросов храним один раз
Раз запись в кэше зависит только от своего префикса, два запроса с одинаковым началом могут делить эти записи. «Одинаковым» значит совпадающим токен в токен с самого первого: если два промпта расходятся на 10-м токене, общими могут быть только токены с 1-го по 9-й. Серверы инференса хранят готовые кэши и индексируют их префиксным деревом (radix tree — дерево, в котором запросы с одинаковым началом идут по общей ветке, как RadixAttention в SGLang или автоматическое кэширование префиксов в vLLM). Новый запрос спускается по дереву, пока его токены совпадают, переиспользует всё на этом пути и запускает префилл только для остатка.
Кэш пуст. Все запросы будут начинаться с одного и того же системного промпта и описаний инструментов: 3500 токенов.
Обратите внимание на продолжения диалогов. Ответ на запрос 1 попал в кэш прямо во время генерации, поэтому, когда пользователь отвечает, попаданием оказывается весь диалог, а считается только новое сообщение. Вот почему длинный чат замедляется от шага к шагу не так сильно, как можно было бы ожидать.
Когда память заканчивается, сервер первыми вытесняет (удаляет) дольше всего не использованные листья — концевые ветки дерева. Общие «стволы» вроде системного промпта используются постоянно и остаются.
Кэширование промптов в API
Облачные API моделей предлагают ту же идею под названием кэширование промптов (prompt caching). Одни включают его автоматически для любого повторяющегося префикса, другие позволяют явно отметить точку кэширования. Устройство цен у всех похоже: для закэшированных токенов провайдер пропускает префилл и отдаёт заметную часть экономии клиенту, так что они стоят намного дешевле (обычно на 50–90%). Некоторые провайдеры берут небольшую наценку за первую запись в кэш. Кэш живёт от минут до часов, в зависимости от провайдера, но не днями, поэтому окупается, когда один и тот же префикс используется часто.
Кэш сравнивает префиксы токен за токеном. Один изменившийся токен делает недействительным всё, что после него. Отсюда практическое правило:
В начало промпта ставьте то, что не меняется (инструкции, описания инструментов, справочные документы), а то, что меняется при каждом вызове (время, вопрос пользователя), — в конец.
Цены и скорости условные ($3 за миллион входных токенов, префилл 5000 токенов/с).
Кэш экономит не только деньги. Если пропустить префилл 25 000 токенов, время до первого токена сокращается с секунд до долей секунды — именно это делает длинные системные промпты и большие каталоги инструментов пригодными для интерактивных приложений.
PagedAttention: память как в операционной системе
Чтобы переиспользовать кэш, его нужно дёшево делить и наращивать. Ранние системы выдавали каждому запросу один непрерывный кусок памяти GPU. Заранее никто не знает, каким длинным будет ответ, поэтому кусок брали под максимальную длину. Большая его часть пустовала, а одинаковые префиксы хранились снова и снова. Потерянная под кэш память — это меньше последовательностей на GPU, значит батчи меньше и каждый пользователь ждёт дольше (батчинг).
PagedAttention (появился вместе с vLLM) заимствует идею виртуальной памяти: так операционная система выдаёт разбросанные по памяти страницы за один непрерывный кусок. Кэш нарезается на блоки фиксированного размера, скажем, по 16 токенов. У каждой последовательности есть таблица блоков: какие физические блоки хранят её токены и в каком порядке. Блоки выделяются по одному по мере роста последовательности, могут лежать где угодно в памяти и могут быть общими: две таблицы могут указывать на один блок, у которого есть счётчик ссылок (сколько последовательностей им пользуется). Если последовательности нужно записать в общий блок, она сначала получает собственную копию (копирование при записи, copy-on-write).
Память GPU под KV-кэш разбита на 16 одинаковых блоков по 4 места для токенов. Пока ничего не выделено.
При страничной организации пустует только незаполненная часть последнего блока каждой последовательности, поэтому на одном GPU одновременно помещается гораздо больше последовательностей. Больше последовательностей в батче — выше пропускная способность. Поэтому так устроены практически все современные серверы инференса, и поэтому кэширование префиксов сводится к общим блокам.
Как уменьшить кэш
Когда даже плотно упакованный кэш слишком велик, есть три семейства решений.
Хранить меньше голов. Групповое внимание (GQA) и внимание с общими ключами (MQA) (Быстрый инференс) делят ключи и значения между головами запросов. Многоголовое латентное внимание (MLA) идёт дальше и хранит на каждый токен один маленький сжатый вектор, из которого ключи и значения восстанавливаются; см. За пределами классического трансформера.
Хранить меньше бит. Ключи и значения можно квантизовать до 8 или даже 4 бит, уменьшив кэш в 2–4 раза ценой небольшой потери качества — та же идея, что и квантизация весов в главе 13.
Хранить меньше токенов. Некоторые токены можно выбросить из кэша совсем. И очень важно, какие именно:
Цвет — внимание от самого нового токена (условно). Бледные — вытеснены.
«Боб» всё ещё в кэше: ответ можно найти.
H2O: половина бюджета — последним токенам, половина — старым токенам, которые до сих пор получали больше всего внимания. Важные факты обычно выживают.
- Скользящее окно хранит последние несколько тысяч токенов. Неожиданно, но оно сильно ломается, как только из кэша выпадают самые первые токены. Softmax должен куда-то девать свой вес, и модели учатся сбрасывать «лишнее» внимание на первый токен, так что он работает как сток (глава 5). Уберите его — и этот вес достанется другим токенам, путая выход каждого слоя. StreamingLLM просто сохраняет несколько токенов-стоков плюс окно и стабильно работает на бесконечных потоках.
- Методы тяжёлых токенов (heavy hitters), например H2O, сохраняют токены, набравшие больше всего внимания, плюс последние. Более поздние методы (SnapKV и другие) выбирают отдельно для каждой головы и слоя.
Вытеснение необратимо: выброшенный токен уже не посмотреть. Оно подходит для потоковой обработки и очень длинной генерации; для вопросов по документу надёжнее поиск или кэш побольше.
Кэширование готовых ответов
На верхнем уровне приложение может кэшировать ответы целиком. Кэш точных совпадений возвращает сохранённый ответ на идентичный запрос. Семантический кэш превращает каждый вопрос в эмбеддинг (глава 3) и отдаёт сохранённый ответ, если новый вопрос достаточно похож — например, косинусное сходство выше 0,95. Это быстро и дёшево, но порог — компромисс: слишком низкий, и пользователи получат ответы на чуть другие вопросы.
Итоги
- Ключи и значения зависят только от префикса, поэтому любой общий префикс может делить свой кэш.
- Кэширование префиксов (префиксные деревья на сервере, кэширование промптов в API) пропускает префилл повторяющихся префиксов: ставьте статичное вперёд.
- PagedAttention хранит кэш блоками с таблицами блоков, общими блоками и копированием при записи, поэтому в память помещается гораздо больше запросов.
- Сам кэш уменьшают меньшим числом KV-голов (GQA, MLA), меньшим числом бит (квантизация KV) или меньшим числом токенов (стоки, скользящие окна, тяжёлые токены).
- Приложения могут кэшировать и готовые ответы — по точному совпадению или по близости эмбеддингов.
Кэш растёт с каждым токеном контекста. Следующая глава о том, что происходит, когда контекст становится по-настоящему длинным: Длинный контекст.