Перейти к содержанию
Публикация AiManual

llama.cpp и внутреннее устройство локального инференса: prefill, decode, память и speculative decoding

Разбираем внутреннее устройство локального инференса LLM в llama.cpp: чем отличаются prefill и decode, какие типы памяти задействуются, как speculative decoding

Коротко

Что будет в материале

  1. 01

    Что такое локальный инференс LLM и при чём тут llama.cpp

  2. 02

    Prefill и decode: две фазы генерации токенов в llama.cpp

  3. 03

    Типы памяти при локальном запуске LLM: VRAM, RAM и KV-кэш

  4. 04

    Speculative decoding: как ускорить генерацию без потери качества

Локальный инференс LLM распадается на две фазы с разным характером нагрузки: prefill обрабатывает весь промпт за один проход, а decode генерирует ответ по одному токену. Prefill является compute-bound, а decode — memory-bound. Это разделение объясняет большинство странностей при настройке llama.cpp: почему один и тот же параметр ускоряет работу в одном сценарии и заметно замедляет в другом.

Рамки разбора опираются на публичный доклад Merve из Hugging Face, представленный на конференции для разработчиков. В нём разбирается llama.cpp и базовые концепции локального инференса: prefill vs decode, типы памяти, speculative decoding. Материалы доклада доступны публично, автор просит указывать атрибуцию при использовании (анонс доклада). В анонсе нет конкретных замеров, цифр и сравнений железа, поэтому ниже опираемся на общие принципы и арифметику, без выдуманных бенчмарков.

Что такое локальный инференс LLM и при чём тут llama.cpp

Локальный инференс - это запуск языковой модели на собственном железе: домашнем ПК, рабочей станции или арендованном сервере. Запросы не уходят в облачный API, данные остаются у вас, плата за токены не начисляется. Платите вы другим: скоростью, электричеством и временем на настройку.

llama.cpp занимает в этой нише особое место. Проект изначально писался под запуск моделей на CPU, умеет работать с GPU, поддерживает много схем квантования весов и встраивается в десятки обёрток и приложений. Разбор внутреннего устройства инференса удобно вести именно на его примере: большинство параметров llama-server напрямую управляют тем, как движок проходит фазы prefill и decode.

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

Почему prefill и decode - ключ к пониманию локального инференса

Промпт обрабатывается не так, как ответ. Сначала модель прогоняет через себя всю входную последовательность и заполняет KV-кэш - это prefill. Затем начинается генерация: каждый следующий токен предсказывается отдельным проходом, который опирается на уже готовый KV-кэш. Это decode.

Разница не косметическая. Prefill хорошо распараллеливается, потому что все токены промпта известны заранее и обрабатываются одновременно. Decode по своей природе последовательный: токен N нельзя посчитать, не имея токена N-1. Отсюда два разных узких места, и почти все настройки llama.cpp стоит читать через призму вопроса: на какую из двух фаз я сейчас влияю?

Prefill и decode: две фазы генерации токенов в llama.cpp

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

Во время decode на вход подаётся последний сгенерированный токен. Модель снова проходит все слои, но теперь считает attention только для одной новой позиции, подтягивая готовые ключи и значения из KV-кэша. Результат - один новый токен, который тут же попадает обратно на вход. Цикл повторяется, пока не сработает стоп-условие.

Аналогия, которая хорошо держится: prefill - прочитать всю книгу от корки до корки, decode - писать по ней ответ, буква за буквой, каждый раз заново пролистывая закладки.

Фазы отвечают за разные пользовательские метрики. Prefill определяет время до первого токена (TTFT), decode - скорость генерации в токенах в секунду. Поэтому «движок медленный» без уточнения, какая фаза провисает, ничего не значит.
ПризнакPrefillDecode
Что обрабатываетсяВесь промпт сразуОдин токен за проход
ПараллелизмВысокийПоследовательный
Узкое местоВычислительная мощность GPUПропускная способность памяти
МетрикаВремя до первого токенаТокены в секунду
Зависимость от длины промптаСильная, близка к линейнойПочти нет
Что помогаетБыстрые ядра, батчинг, flash-attentionБыстрая память, меньший объём данных, speculative decoding

Почему prefill - это compute-bound, а decode - memory-bound

Ключевая величина - сколько арифметических операций приходится на каждый прочитанный из памяти байт. Во время prefill один и тот же набор весов используется для обработки всех токенов промпта, поэтому вычислений на байт много, GPU загружен работой, а не ожиданием данных. Это и называют compute-bound.

В decode всё наоборот. На каждый сгенерированный токен нужно прочитать из памяти практически все веса модели плюс весь KV-кэш, а полезной арифметики при этом - одна небольшая порция матричных умножений. Память отдаёт данные быстрее или медленнее, но именно её пропускная способность задаёт потолок скорости генерации. Это memory-bound.

Практический вывод: prefill ускоряет более мощный вычислитель, decode - более быстрая память и меньший объём данных, который через неё прогоняется. Квантование весов даёт прирост именно на генерации: читать 4 бита на параметр быстрее, чем 16. Подробнее о том, как на prefill влияют батчинг, планировщик и работа с KV-кэшем, разбираем в материале про prefill в vLLM против llama.cpp.

Как KV-кэш ускоряет decode и почему он растёт с контекстом

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

Плата за это - память. KV-кэш растёт линейно с длиной контекста: каждый новый токен добавляет один набор Key и Value векторов, удвоение длины контекста удваивает KV-кэш. Он занимает VRAM (или RAM, если слои выгружены на CPU). Чем больше окно контекста вы открываете, тем больше памяти уходит на кэш, а не на веса модели. В llama.cpp размер контекста задаётся при запуске, и его можно ограничить сознательно, вместо того чтобы упираться в нехватку памяти посреди сессии.

Типы памяти при локальном запуске LLM: VRAM, RAM и KV-кэш

При локальном запуске память расходуется на несколько категорий данных.

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

Веса и KV-кэш могут лежать в VRAM или в системной RAM. llama.cpp позволяет распределять слои между GPU и CPU, поэтому часть модели может работать на видеокарте, а часть - на процессоре. Чем больше слоёв на GPU, тем быстрее генерация, но тем выше требования к VRAM. Гибридные конфигурации спасают при нехватке памяти, но заметно снижают скорость, потому что decode становится чувствителен к пропускной способности системной шины.

Веса модели: где хранятся и как квантование экономит VRAM

Каждый параметр модели в FP16 занимает 2 байта, в INT8 - 1 байт, в 4-битных схемах - около половины байта. Арифметика простая: модель на 7 миллиардов параметров в FP16 весит примерно 14 ГБ, в 8-битном квантовании - около 7 ГБ, в 4-битном - порядка 4 ГБ.

ТочностьБайт на параметрМодель 7BМодель 70B
FP162~14 ГБ~140 ГБ
INT8 / Q81~7 ГБ~70 ГБ
4-бит (Q4)~0,5~4 ГБ~40 ГБ
2-бит (Q2)~0,25~2 ГБ~20 ГБ

Цифры приблизительные: схемы квантования добавляют служебные данные, а KV-кэш и активации считаются сверху. Практический смысл в том, что квантование решает, влезет модель в вашу VRAM или нет, и одновременно снижает требования к памяти и пропускной способности, что связано с I/O-узким местом при инференсе. Прямого подтверждения, что квантование весов ускоряет decode именно за счёт меньшего числа проходящих через память байтов, в доступных источниках нет: эмпирические бенчмарки есть только для форка ik_llama.cpp, и переносить их выводы на upstream llama.cpp напрямую нельзя.

KV-кэш: сколько VRAM съедает длинный контекст

KV-кэш масштабируется с числом слоёв, числом голов attention, размерностью головы и длиной контекста. Для модели с 32 слоями, 8 KV-головами, head_dim 128 и точностью FP16 при контексте 32K KV-кэш составляет около 4 ГБ, а при 128K — около 16 ГБ. На длинных контекстах KV-кэш может потреблять больше памяти, чем сами веса модели.

Отсюда практические приёмы: ограничивать размер контекста под реальные задачи, снижать точность KV-кэша отдельно от весов и выносить часть слоёв на CPU. Пример такой конфигурации с квантованным KV-кэшем и speculative decoding разбираем в статье про сборку стека для Qwen3.8 27B на двух RTX 3090.

Speculative decoding: как ускорить генерацию без потери качества

Идея speculative decoding простая. Рядом с большой target-моделью запускается маленькая draft-модель. Она быстро предлагает несколько следующих токенов. Target-модель проверяет всю пачку за один проход: для каждой позиции она считает своё распределение вероятностей и решает, принять предложенный токен или отклонить его.

Принятые токены засчитываются сразу пачкой. Если draft-модель угадала, вы получаете несколько токенов за цену одного прохода target-модели. Если не угадала, отклонённые варианты отбрасываются, и target-модель выдаёт свой токен. Метод математически точен: вероятность генерации любой последовательности при speculative decoding идентична прямому семплированию из большой target-модели, поэтому качество вывода не меняется. Меняется только скорость.

Экономика здесь честная, но не бесплатная. Проверка пачки стоит дороже одного обычного шага decode, а draft-модель занимает дополнительную память. Выигрыш появляется тогда, когда предложения принимаются достаточно часто. Поэтому speculative decoding заметно помогает на предсказуемом тексте и куда слабее работает там, где следующий токен плохо предсказуем.

Как выбрать draft-модель и когда speculative decoding не поможет

Draft-модель должна быть заметно меньше и быстрее target-модели, но при этом близка к ней по распределению токенов. Слишком слабый кандидат предлагает токены, которые target-модель почти всегда отклоняет: накладные расходы растут, а выигрыш исчезает. Слишком большая draft-модель сама начинает съедать время и память.

В llama.cpp speculative decoding настраивается, и один из практичных вариантов - задействовать в роли draft-модели дополнительные головы предсказания самой модели (multitoken prediction) вместо отдельной сети. В таком режиме флаг включает MTP-стиль speculative decoding, а второй флаг задаёт максимальное число draft-токенов (например, 3): модель пытается предложить до трёх будущих токенов до проверки. Отдельно стоит учитывать, что часть данных о преимуществах встроенного MTP получена на форке ik_llama.cpp, а не на upstream, поэтому выводы могут переноситься не полностью. Как это влияет на скорость сервера и какие параметры за это отвечают, разбираем в материале про настройку draft-mtp в llama-server.

Случаи, когда ускорения ждать не стоит: задачи с высокой энтропией (креативный текст, редкие языки, код с нестандартными конструкциями), длинные ответы с постоянной сменой темы, сценарии, где память и так на пределе и draft-модель просто некуда положить. Проверять эффект стоит на своих реальных промптах, а не на синтетическом тексте.

Как понимание prefill, decode и памяти помогает выбрать железо и настроить llama.cpp

Профиль нагрузки определяет, куда вкладывать деньги. Если вы часто гоняете длинные документы и ждёте быстрый первый ответ, вас ограничивает prefill, и приоритет - вычислительная мощность GPU. Если вы ведёте долгие диалоги и генерируете много текста, ограничитель - decode, и здесь важнее пропускная способность памяти и её объём под KV-кэш.

Объём VRAM решает, что вообще поместится. Считайте не только веса модели, но и KV-кэш под ваш типичный контекст, плюс запас на активации и буферы. Когда памяти не хватает, вариантов немного: более агрессивное квантование весов, квантование KV-кэша, сокращение контекста или выгрузка части слоёв на CPU с потерей скорости.

Практический чек-лист: что учесть перед запуском локальной LLM

  1. Определите задачу: короткий чат, генерация кода, обработка длинных документов. От неё зависит типичная длина и промпта, и ответа.
  2. Выберите модель и схему квантования. Прикиньте размер весов по таблице выше.
  3. Оцените рабочую длину контекста и добавьте к расчёту запас памяти под KV-кэш.
  4. Сравните сумму с доступной VRAM. Не помещается - уменьшайте контекст, берите более компактное квантование или оставляйте часть слоёв на CPU.
  5. Настройте llama-server: сколько слоёв выгружать на GPU, размер батча, параметры KV-кэша.
  6. Если decode медленный, а память позволяет, попробуйте speculative decoding с подходящей draft-моделью.
  7. Измеряйте время до первого токена и скорость генерации отдельно: эти метрики реагируют на разные настройки.

Итеративность здесь не недостаток, а суть процесса. Конфигурация под длинные документы и под быстрый чат отличается, и один универсальный набор параметров для всех задач не работает.

Ограничения и подводные камни локального инференса

У каждого механизма есть цена, и её лучше знать заранее.

  • Квантование. Экономит VRAM и снижает требования к пропускной способности, но агрессивные схемы (2-3 бита) заметно бьют по качеству. Компромисс между размером и точностью каждый выбирает под свою задачу.
  • KV-кэш. Растёт с контекстом и съедает память постепенно. Легко получить рабочий запуск на коротком промпте и упереться в лимит на длинном.
  • Speculative decoding. Ускорение не гарантировано. Требует подбора draft-модели и проверки на реальных задачах; на непредсказуемом тексте накладные расходы могут перекрыть выигрыш.
  • Выгрузка слоёв на CPU. Позволяет запустить модель, которая не влезает в VRAM, но снижает скорость из-за пропускной способности системной памяти.
  • Совместимость и зрелость. Не все архитектуры моделей одинаково хорошо поддержаны. Скорость также зависит от версии движка и драйверов.

Отдельная ловушка - замеры. Даже правдоподобные цифры ускорения от новых механизмов стоит принимать с оговоркой, пока их не подтвердили независимые тесты. Показательный пример - приёмы управления KV-кэшем во время генерации, где заявленное ускорение пока не подтверждено бенчмарками самого форка: об этом в разборе focus-llama и declarative attention.

Что дальше: материалы доклада и куда копать

Статья опирается на публичный доклад Merve из Hugging Face, прочитанный на конференции для разработчиков. Тема доклада - llama.cpp и базовые концепции локального инференса: prefill vs decode, типы памяти и speculative decoding. Материалы доступны для публичного использования, автор просит указывать атрибуцию; ссылка на них указана в комментариях к посту с анонсом доклада.

Для углубления логично двигаться в трёх направлениях: документация и параметры запуска самого llama.cpp; материалы по устройству attention и KV-кэша; практические сравнения движков на ваших задачах. Понимание prefill, decode и типов памяти даёт основу: дальше вы читаете описания параметров не как список заклинаний, а как рычаги, каждый из которых тянет за конкретную фазу.

Подписаться на канал