• Русский
  • Кэширование

    Большая часть того, что языковая модель считает для запроса, уже когда-то была посчитана. Системный промпт (постоянные инструкции, которые приложение ставит перед каждым разговором) одинаков для всех пользователей. Чат на каждом шаге заново отправляет всю переписку. Агент — модель, которая в цикле вызывает инструменты, пока не решит задачу, — сотни раз перечитывает одни и те же описания инструментов. Кэширование — это искусство не делать одну и ту же работу дважды, и наряду с батчингом это самый сильный рычаг, влияющий на цену и скорость работы LLM.

    Напоминание: KV-кэш

    В главе Быстрый инференс мы познакомились с KV-кэшем: при генерации модель хранит ключи и значения всех предыдущих токенов (только их и читают более поздние токены), поэтому каждый новый токен считает только свои. Вся эта глава держится на трёх фактах оттуда:

    • Ключи и значения токена зависят только от него самого и от токенов перед ним, потому что внимание причинное. Один и тот же префикс всегда даёт один и тот же кэш — неважно, какому запросу он принадлежит и что идёт после него.
    • Построить кэш для промпта — это и есть работа префилла, а для длинного промпта префилл — основная часть работы до первого токена.
    • Кэш большой: около 128 КБ на токен для модели размера Llama 3 8B, гигабайты для длинного диалога, и всё это в памяти GPU, объём которой — десятки гигабайт.

    Первый факт делает переиспользование возможным: уже виденный префикс может пропустить свой префилл. Два других делают его выгодным.

    Кэширование префиксов: общее у запросов храним один раз

    Раз запись в кэше зависит только от своего префикса, два запроса с одинаковым началом могут делить эти записи. «Одинаковым» значит совпадающим токен в токен с самого первого: если два промпта расходятся на 10-м токене, общими могут быть только токены с 1-го по 9-й. Серверы инференса хранят готовые кэши и индексируют их префиксным деревом (radix tree — дерево, в котором запросы с одинаковым началом идут по общей ветке, как RadixAttention в SGLang или автоматическое кэширование префиксов в vLLM). Новый запрос спускается по дереву, пока его токены совпадают, переиспользует всё на этом пути и запускает префилл только для остатка.

    Рисунок 11.1 · Кэширование префиксов в дереве
    попадание в кэшсчитается сейчассгенерированный ответв кэше
    0токенов взято из кэша0токенов посчитано0%префилла пропущено
    1/9

    Кэш пуст. Все запросы будут начинаться с одного и того же системного промпта и описаний инструментов: 3500 токенов.

    Пять запросов к чат-сервису. Все они начинаются с системного промпта и описаний инструментов; последующие сообщения делят ещё и весь предыдущий диалог вместе со сгенерированным ответом. Пройдите по запросам: зелёные узлы — попадания в кэш, оранжевые считаются сейчас.

    Обратите внимание на продолжения диалогов. Ответ на запрос 1 попал в кэш прямо во время генерации, поэтому, когда пользователь отвечает, попаданием оказывается весь диалог, а считается только новое сообщение. Вот почему длинный чат замедляется от шага к шагу не так сильно, как можно было бы ожидать.

    Когда память заканчивается, сервер первыми вытесняет (удаляет) дольше всего не использованные листья — концевые ветки дерева. Общие «стволы» вроде системного промпта используются постоянно и остаются.

    Кэширование промптов в API

    Облачные API моделей предлагают ту же идею под названием кэширование промптов (prompt caching). Одни включают его автоматически для любого повторяющегося префикса, другие позволяют явно отметить точку кэширования. Устройство цен у всех похоже: для закэшированных токенов провайдер пропускает префилл и отдаёт заметную часть экономии клиенту, так что они стоят намного дешевле (обычно на 50–90%). Некоторые провайдеры берут небольшую наценку за первую запись в кэш. Кэш живёт от минут до часов, в зависимости от провайдера, но не днями, поэтому окупается, когда один и тот же префикс используется часто.

    Кэш сравнивает префиксы токен за токеном. Один изменившийся токен делает недействительным всё, что после него. Отсюда практическое правило:

    Сначала статичное, изменчивое в конце

    В начало промпта ставьте то, что не меняется (инструкции, описания инструментов, справочные документы), а то, что меняется при каждом вызове (время, вопрос пользователя), — в конец.

    Рисунок 11.2 · Порядок в промпте и выгода от кэша
    Системный промпт2 тыс.
    Инструменты3 тыс.
    Документы20 тыс.
    Время: 14:03:2720
    Вопрос150
    из кэша считается каждый раз99% каждого промпта берётся из кэша
    Без кэша С кэшем
    Стоимость входа, за вызов (после первого)
    $0,076
    $0,008
    Стоимость входа, всего за 20 вызовов
    $1,51
    $0,25
    Время до первого токена
    5,23 с
    0,23 с

    Цены и скорости условные ($3 за миллион входных токенов, префилл 5000 токенов/с).

    Переключите, где стоит отметка времени. Если статичное идёт первым, почти весь промпт попадает в кэш; одна меняющаяся строка в начале заставляет каждый вызов начинать с нуля. Затем подвигайте ползунки: выгода растёт с длиной промпта и числом вызовов.

    Кэш экономит не только деньги. Если пропустить префилл 25 000 токенов, время до первого токена сокращается с секунд до долей секунды — именно это делает длинные системные промпты и большие каталоги инструментов пригодными для интерактивных приложений.

    PagedAttention: память как в операционной системе

    Чтобы переиспользовать кэш, его нужно дёшево делить и наращивать. Ранние системы выдавали каждому запросу один непрерывный кусок памяти GPU. Заранее никто не знает, каким длинным будет ответ, поэтому кусок брали под максимальную длину. Большая его часть пустовала, а одинаковые префиксы хранились снова и снова. Потерянная под кэш память — это меньше последовательностей на GPU, значит батчи меньше и каждый пользователь ждёт дольше (батчинг).

    PagedAttention (появился вместе с vLLM) заимствует идею виртуальной памяти: так операционная система выдаёт разбросанные по памяти страницы за один непрерывный кусок. Кэш нарезается на блоки фиксированного размера, скажем, по 16 токенов. У каждой последовательности есть таблица блоков: какие физические блоки хранят её токены и в каком порядке. Блоки выделяются по одному по мере роста последовательности, могут лежать где угодно в памяти и могут быть общими: две таблицы могут указывать на один блок, у которого есть счётчик ссылок (сколько последовательностей им пользуется). Если последовательности нужно записать в общий блок, она сначала получает собственную копию (копирование при записи, copy-on-write).

    Рисунок 11.3 · PagedAttention: KV-кэш блоками
    #0
    #1
    #2
    #3
    #4
    #5
    #6
    #7
    #8
    #9
    #10
    #11
    #12
    #13
    #14
    #15
    Таблицы блоков
    0мест занято токенами0мест выделено0впустую
    1/10

    Память GPU под KV-кэш разбита на 16 одинаковых блоков по 4 места для токенов. Пока ничего не выделено.

    Для наглядности в блоке 4 места (в реальных системах 16 и больше). Посмотрите, как два запроса делят системный промпт, два варианта ответа — один промпт, и как происходит копирование при записи. Переключитесь на «Непрерывное», чтобы увидеть тот же момент с резервированием под каждый запрос.

    При страничной организации пустует только незаполненная часть последнего блока каждой последовательности, поэтому на одном GPU одновременно помещается гораздо больше последовательностей. Больше последовательностей в батче — выше пропускная способность. Поэтому так устроены практически все современные серверы инференса, и поэтому кэширование префиксов сводится к общим блокам.

    Как уменьшить кэш

    Когда даже плотно упакованный кэш слишком велик, есть три семейства решений.

    Хранить меньше голов. Групповое внимание (GQA) и внимание с общими ключами (MQA) (Быстрый инференс) делят ключи и значения между головами запросов. Многоголовое латентное внимание (MLA) идёт дальше и хранит на каждый токен один маленький сжатый вектор, из которого ключи и значения восстанавливаются; см. За пределами классического трансформера.

    Хранить меньше бит. Ключи и значения можно квантизовать до 8 или даже 4 бит, уменьшив кэш в 2–4 раза ценой небольшой потери качества — та же идея, что и квантизация весов в главе 13.

    Хранить меньше токенов. Некоторые токены можно выбросить из кэша совсем. И очень важно, какие именно:

    Рисунок 11.4 · Вытеснение токенов из KV-кэша
    <s>Итоги·встречи:·Алиса·выпустит·v2·3·мая.·Боб·отвечает·за·бюджет·в·$40k.·Риски:·поставщик·может·задержать,·а·QA·нужно·ещё·две·недели.·Кто·отвечает·за·бюджет?

    Цвет — внимание от самого нового токена (условно). Бледные — вытеснены.

    14токенов осталось38%от полного кэша74%внимания сохранено

    «Боб» всё ещё в кэше: ответ можно найти.

    H2O: половина бюджета — последним токенам, половина — старым токенам, которые до сих пор получали больше всего внимания. Важные факты обычно выживают.

    Заметка в кэше и вопрос в конце. Выберите стратегию и сожмите бюджет. Первый токен собирает много внимания («сток внимания»), а простое скользящее окно его выбрасывает. Вытеснение по «тяжёлым» токенам сохраняет те, что получили больше всего внимания, — здесь среди них и ответ.
    • Скользящее окно хранит последние несколько тысяч токенов. Неожиданно, но оно сильно ломается, как только из кэша выпадают самые первые токены. Softmax должен куда-то девать свой вес, и модели учатся сбрасывать «лишнее» внимание на первый токен, так что он работает как сток (глава 5). Уберите его — и этот вес достанется другим токенам, путая выход каждого слоя. StreamingLLM просто сохраняет несколько токенов-стоков плюс окно и стабильно работает на бесконечных потоках.
    • Методы тяжёлых токенов (heavy hitters), например H2O, сохраняют токены, набравшие больше всего внимания, плюс последние. Более поздние методы (SnapKV и другие) выбирают отдельно для каждой головы и слоя.

    Вытеснение необратимо: выброшенный токен уже не посмотреть. Оно подходит для потоковой обработки и очень длинной генерации; для вопросов по документу надёжнее поиск или кэш побольше.

    Кэширование готовых ответов

    На верхнем уровне приложение может кэшировать ответы целиком. Кэш точных совпадений возвращает сохранённый ответ на идентичный запрос. Семантический кэш превращает каждый вопрос в эмбеддинг (глава 3) и отдаёт сохранённый ответ, если новый вопрос достаточно похож — например, косинусное сходство выше 0,95. Это быстро и дёшево, но порог — компромисс: слишком низкий, и пользователи получат ответы на чуть другие вопросы.

    Итоги

    • Ключи и значения зависят только от префикса, поэтому любой общий префикс может делить свой кэш.
    • Кэширование префиксов (префиксные деревья на сервере, кэширование промптов в API) пропускает префилл повторяющихся префиксов: ставьте статичное вперёд.
    • PagedAttention хранит кэш блоками с таблицами блоков, общими блоками и копированием при записи, поэтому в память помещается гораздо больше запросов.
    • Сам кэш уменьшают меньшим числом KV-голов (GQA, MLA), меньшим числом бит (квантизация KV) или меньшим числом токенов (стоки, скользящие окна, тяжёлые токены).
    • Приложения могут кэшировать и готовые ответы — по точному совпадению или по близости эмбеддингов.

    Кэш растёт с каждым токеном контекста. Следующая глава о том, что происходит, когда контекст становится по-настоящему длинным: Длинный контекст.