Инференс: KV-cache, квантизация, спекулятивный декодинг
Почему генерация токенов стоит дорого, куда уходит память GPU и за счёт каких трюков — KV-cache, квантизация, спекулятивный декодинг, батчинг — инференс LLM ускоряют в разы. Обязательная тема для инженера, который возит модели в прод.
Интуиция: почему это дорого
LLM авторегрессивна: чтобы написать ответ из 500 токенов, нужно 500 последовательных прогонов модели — каждый следующий токен зависит от предыдущих, распараллелить по времени нельзя. При этом у запроса две очень разные фазы:
- Prefill — обработка промпта. Все токены промпта известны заранее, их можно прогнать через модель параллельно одной матричной операцией. GPU загружен вычислениями (compute-bound), фаза быстрая.
- Decode — генерация. По одному токену за шаг: ради каждого токена приходится прокачать через чип все веса модели и весь кэш. Арифметики мало, трафика памяти много — фаза memory-bound, и именно она определяет скорость генерации.
Отсюда две главные метрики сервинга: TTFT (time to first token — в основном цена prefill) и tokens/s (скорость decode).
KV-cache: не считать одно и то же
Внимание (attention) на каждом шаге смотрит на все прошлые токены — ему нужны их ключи K и значения V. Наивно их можно пересчитывать заново на каждом шаге, но они не меняются! KV-cache (кэш ключей и значений) сохраняет K и V всех обработанных токенов, и на новом шаге считаются K/V только одного нового токена. Стоимость шага падает с квадратичной по длине до линейной.
Платим памятью. Размер кэша растёт линейно с длиной контекста:
Для 7B-модели (32 слоя, 32 головы по 128 измерений, fp16) это ≈ 0.5 МБ на токен: контекст в 32k токенов съедает ~16 ГБ — сопоставимо с самими весами. Поэтому «большое контекстное окно» — это не бесплатная фича, а прямые затраты памяти и трафика; архитектуры вроде GQA (общие KV-головы) придуманы как раз чтобы уменьшить этот кэш.
Квантизация: жмём веса
Раз decode упирается в трафик памяти, очевидный ход — сделать веса меньше. Квантизация (quantization) переводит веса из fp16 в int8 или int4: модель занимает в 2–4 раза меньше памяти, и на каждый токен через чип гоняется в 2–4 раза меньше байтов — генерация ускоряется почти пропорционально. Цена — небольшая потеря качества; методы вроде GPTQ и AWQ подбирают квантизацию аккуратно (калибровка на данных, защита важных весов), так что для int4 деградация часто едва заметна. Квантизуют и KV-cache — экономия та же по смыслу.
Спекулятивный декодинг и батчинг
Спекулятивный декодинг (speculative decoding) обходит последовательность decode хитростью: маленькая быстрая draft-модель предлагает сразу несколько токенов вперёд, а большая модель проверяет их все одним параллельным прогоном (как prefill). Совпавший префикс принимается, первый несовпавший токен большая модель ставит своим. Специальное правило принятия гарантирует, что итоговое распределение — в точности распределение большой модели: ускорение в 2–3 раза без потери качества, если draft часто угадывает.
На сервере считают не латентность одного запроса, а пропускную способность. Батчинг объединяет запросы: веса читаются из памяти один раз на весь батч, стоимость на запрос падает. Continuous batching добавляет и убирает запросы из батча на каждом шаге, не дожидаясь, пока все закончат. vLLM с PagedAttention хранит KV-cache постранично, как виртуальную память ОС, убирая фрагментацию и позволяя набивать в GPU намного больше параллельных запросов.
- «Почему decode медленнее prefill?» — prefill параллелен по токенам и compute-bound; decode — по токену за шаг, упирается в трафик памяти (веса + кэш на каждый токен).
- «Что хранит KV-cache и сколько он весит?» — ключи и значения всех прошлых токенов по всем слоям и головам; размер линеен по длине контекста (≈0.5 МБ/токен у 7B в fp16).
- «Спекулятивный декодинг портит качество?» — нет: правило принятия сохраняет распределение большой модели, ускорение берётся из параллельной проверки.
- «Как оценивать сервинг?» — TTFT, tokens/s на запрос и суммарный throughput; continuous batching и PagedAttention повышают именно throughput.