Перейти к содержимому
TelegramGitHub

AI Engineering, глава 9: Оптимизация inference

Главная мысль: Хорошая модель бесполезна, если она медленная или дорогая. У LLM inference два шага с разными узкими местами: prefill упирается в вычисления, decode упирается в память. Оптимизация идёт на трёх уровнях: модель, attention и KV cache, сервис.

Две фазы inference LLM: prefill (параллельно, compute-bound, TTFT) и decode (по одному токену, memory-bandwidth-bound, TPOT, KV cache), и три уровня оптимизации: модель, attention и KV cache, сервис

Если вы вызываете API OpenAI или Google, большую часть этих приёмов делает провайдер. Если вы сами хостите open-source модель, inference-сервис становится вашей задачей. Знать эти приёмы нужно в обоих случаях: так проще оценить провайдера или framework.

Training создаёт модель. Inference считает ответ для входа. Inference-сервер хранит модель, получает запросы и выделяет под них ресурсы. Остальные части сервиса принимают запросы, маршрутизируют их и обрабатывают входные данные.

Оптимизация начинается с поиска узкого места. Их два вида:

ТипЧто ограничивает скоростьЧто помогает
compute-boundЧисло вычисленийБольше FLOP/s, несколько чипов
memory-bandwidth-boundСкорость передачи данных между памятью и процессоромЧипы с быстрой памятью, меньше байтов на параметр

У LLM два шага:

  • Prefill. Модель читает весь prompt параллельно. Скорость ограничена вычислениями. Шаг заполняет первый KV cache.
  • Decode. Модель выдаёт по одному токену. На каждый токен нужно загрузить веса модели в вычислительные блоки. Скорость ограничена памятью.

Шаги разные, поэтому в production их часто разносят на разные машины. Длинный контекст сильнее нагружает prefill и память.

APIЦельКогда брать
OnlineНизкая latencyЧат-бот, подсказки в IDE
BatchНизкая ценаНочные отчёты, разметка тикетов, переиндексация базы знаний

Batch API у крупных провайдеров обычно стоит примерно вполовину дешевле, но отвечает часами. Для data engineer это удобный вариант: ночной job по классификации тикетов или генерации описаний колонок не требует быстрого ответа.

Не путайте с классическим ML. Там batch inference значит “предсказания посчитаны заранее”. Для LLM prompt открытый, заранее все запросы не посчитать.

Streaming отдаёт токены по мере генерации. Пользователь ждёт первый токен меньше. Минус: вы не успеваете проверить ответ до показа.

МетрикаЧто измеряет
TTFT (time to first token)Время до первого токена. Это время prefill. Растёт с длиной входа
TPOT (time per output token)Время на каждый следующий токен. Это время decode
LatencyTTFT + TPOT × число выходных токенов
ThroughputСколько выходных токенов в секунду выдаёт весь сервис
GoodputСколько запросов в секунду уложились в SLO
UtilizationКакую часть мощности железа вы реально используете

Важные детали:

  • Две системы с одинаковой latency могут ощущаться по-разному. Одной нужен быстрый первый токен, другой быстрый поток.
  • Для чтения глазами TPOT около 120 мс (6-8 токенов в секунду) обычно хватает.
  • Если модель сначала думает или вызывает tools, пользователь видит первый токен позже, чем модель его произвела. Считайте TTFT с точки зрения пользователя.
  • Latency это распределение. Среднее обманывает: один запрос на 3 секунды портит среднее у девяти по 100 мс. Смотрите перцентили p50, p90, p95, p99.

Throughput связан с ценой напрямую. Пример: железо стоит $2 в час и даёт 100 токенов в секунду. Это 360 000 токенов в час, то есть около $5,56 за 1 млн выходных токенов. Если ответ в среднем 200 токенов, 1000 ответов стоят около $1,11. Prefill считают так же и добавляют к decode.

Throughput и latency конфликтуют. Batching поднимает throughput, но может замедлить каждый запрос. Если смотреть только на throughput и цену, получится плохой UX. Поэтому есть goodput.

Пример: SLO это TTFT ≤ 200 мс и TPOT ≤ 100 мс. Сервис обрабатывает 100 запросов в минуту, но SLO выполняют 30. Goodput равен 30 запросов в минуту.

  • nvidia-smi показывает долю времени, когда GPU чем-то занят. Это не доля используемой мощности. GPU может показывать 100 % и выполнять 1 % возможных операций.
  • MFU (Model FLOPs Utilization) равен фактическому throughput, делённому на теоретический максимум при пиковых FLOP/s.
  • MBU (Model Bandwidth Utilization) равен используемой пропускной способности памяти, делённой на пиковую.

Формула для LLM: параметры × байты на параметр × токены в секунду. Пример: модель 7B в FP16 (2 байта) при 100 токенах в секунду читает 700 ГБ/с. На чипе с памятью 2 ТБ/с это MBU 35 %. Если квантизовать веса до 8 бит, нужно вдвое меньше полосы.

Высокая utilization сама по себе не цель. Важно, чтобы задачи шли быстрее и дешевле.

Ускоритель это чип для одного типа вычислений. Главный тип для AI это GPU. CPU имеет мало мощных ядер для сложных последовательных задач. GPU имеет тысячи простых ядер для параллельной работы. Основа ML это умножение матриц, и оно хорошо параллелится.

Есть и другие чипы: TPU, AWS Inferentia, Gaudi и другие. Часть из них заточена под inference: ниже точность, быстрее память.

Три памяти, с которыми работает GPU:

ПамятьСкоростьРазмер
CPU DRAMСамая медленная (25-50 ГБ/с)От десятков ГБ до ТБ
GPU HBMБыстрая (от 256 ГБ/с до 1,5 ТБ/с и выше)Десятки ГБ
On-chip SRAMСамая быстрая (10 ТБ/с и выше)Около 40 МБ и меньше

Оптимизация GPU это в основном вопрос того, как использовать эту иерархию.

Как выбрать чип. Сначала определите узкое место. Для compute-bound нужны чипы с большими FLOP/s. Для memory-bound нужны чипы с широкой и большой памятью. Три вопроса: справится ли чип с нагрузкой, как быстро, сколько стоит. Первые два решают три числа: FLOP/s, размер памяти, пропускная способность памяти. В облаке цена почти у всех похожа и зависит от времени использования. Энергия тоже важна: H100 при полной нагрузке за год потребляет около 7000 кВт·ч.

Эти приёмы меняют саму модель. Поведение может немного измениться. Три причины тяжёлого inference: размер модели, авторегрессивный decode, attention.

  • Quantization. Меньше бит на параметр: память и нужная полоса падают. Самый популярный приём: простой, работает на многих моделях. Предел: меньше 1 бита не бывает.
  • Distillation. Маленькая модель учится копировать большую.
  • Pruning. Убираем узлы сети или обнуляем наименее полезные веса. В книге результаты обещающие, но на практике приём редкий: нужно знать архитектуру, выигрыш часто меньше, и не всё железо умеет использовать разреженные матрицы.

Выходной токен у API стоит в 2-4 раза дороже входного. По замеру Anyscale один выходной токен влияет на latency как около 100 входных.

Speculative decoding. Быстрая слабая draft-модель предлагает K токенов. Основная target-модель проверяет их все сразу, параллельно. Берётся самое длинное начало, с которым она согласна, плюс один свой токен. Почему это работает:

  • Проверка параллельна, генерация последовательна.
  • Часть токенов легко угадать, и слабая модель с ними справляется.
  • Decode ограничен памятью, поэтому свободные FLOP/s используются для проверки.

Качество ответа не меняется. Пример из книги: draft-модель 4B ускорила Chinchilla-70B больше чем вдвое. Приём встроен в vLLM, TensorRT-LLM, llama.cpp. Draft лучше работает на структурном тексте, например на коде. Он также должен иметь тот же tokenizer, что target.

Inference with reference. Вместо draft-модели токены копируются из входа. Полезно, когда ответ повторяет контекст: RAG, правка кода, диалоги. Книга показывает ускорение до 2 раз.

Parallel decoding. Модель выдаёт несколько будущих токенов сразу, не дожидаясь предыдущих, потом проверяет их. Примеры: lookahead decoding и Medusa (дополнительные “головы” предсказывают токены вперёд). Идея интересная, но реализовать сложно.

При генерации токена модель нужны key и value векторы всех прошлых токенов. Чтобы не считать их заново, их хранят в KV cache. Только inference использует его: при обучении все токены известны сразу.

Размер cache растёт линейно с длиной последовательности и с размером batch. Число вычислений attention растёт квадратично. Формула: 2 × batch × длина × слои × размерность × байты.

Пример по книге: Llama 2 13B (40 слоёв, размерность 5120), batch 32, длина 2048, 2 байта. Cache занимает около 54 ГБ. Это один cache без оптимизаций. Если batch равен 1, останется около 1,7 ГБ. Длинный контекст и большой batch быстро съедают память.

Три группы приёмов:

ГруппаПриёмыЧто делают
Перепроектировать attentionЛокальное окно, cross-layer attention, multi-query attention, grouped-query attentionМеньше пар key-value. Применяются только на этапе training или finetuning
Управлять KV cachePagedAttention (vLLM), квантизация cache, сжатие cacheCache режется на блоки, меньше фрагментации, память делится гибко
Быстрые kernelsFlashAttentionСливает несколько операций в один проход

Пример эффекта: Character.AI сообщает, что комбинация приёмов уменьшила KV cache более чем в 20 раз при диалогах в среднем по 180 сообщений.

Kernel это код, оптимизированный под конкретный чип. Пишут на CUDA, Triton, ROCm. Базовые техники:

  • vectorization: обрабатывать много элементов за раз;
  • parallelization: делить массив на независимые части;
  • loop tiling: подгонять порядок обращений к памяти под кэш чипа;
  • operator fusion: объединять операции в один проход, меньше обращений к памяти.

Для нового чипа kernels нужно писать заново. Поэтому FlashAttention имеет версии под A100 и H100. Compiler переводит код модели в операции для чипа и подставляет kernels. Примеры: torch.compile, XLA, TensorRT, Apache TVM.

В кейсе PyTorch для Llama-7B на A100 throughput росло шагами: torch.compile, потом INT8, потом INT4, потом speculative decoding. Влияние на качество ответов в кейсе не проверяли.

Эти приёмы меняют не модель, а способ обслуживания. Качество ответов не должно измениться. Цель: распределить ресурсы между запросами так, чтобы упали latency и цена.

Запросы обрабатываются группами. Аналогия: автобус вместо отдельных машин.

ВидКак работаетМинус
StaticЖдём полный batch фиксированного размераРанний запрос ждёт последний
DynamicЗапускаем при полном batch или по таймауту (например, 100 мс)Batch бывает неполным
Continuous (in-flight)Готовый ответ уходит сразу, на его место встаёт новый запросСложнее в реализации

Для LLM лучше всего continuous. Иначе короткий ответ в 10 токенов ждёт длинный в 1000.

Prefill и decode на одной GPU мешают друг другу: новый prefill отнимает вычисления у текущего decode и замедляет TPOT. Если развести их по разным GPU, throughput при тех же SLO заметно растёт. Цена: передача промежуточного состояния между машинами. Внутри узла с быстрой связью она невелика.

Соотношение prefill к decode зависит от нагрузки. Длинные входы и упор на TTFT: примерно от 2:1 до 4:1. Короткие входы и упор на TPOT: от 1:2 до 1:1.

Многие prompts имеют общую часть: system prompt, длинный документ, история диалога. Cache хранит её обработку, и повторно её не считают. Его также называют context cache или prefix cache.

  • Prompt в 1000 токенов и миллион вызовов в день: cache экономит около миллиарда обработанных токенов в сутки.
  • Cache занимает память, как KV cache. Собственная реализация требует усилий.
  • Провайдеры дают скидки. По данным книги на момент написания: у Gemini скидка 75 % на cached токены плюс плата за хранение. Anthropic обещает до 90 % экономии и до 75 % меньше latency.
  • Пример: чат с книгой в 100 000 токенов: TTFT 11,5 с без cache и 2,4 с с cache, цена ниже на 90 %.
ВидЧто делаетПлюсМинус
ReplicaНесколько копий моделиПросто, больше одновременных запросовНужно больше чипов
TensorДелит операцию (например, матрицу) между устройствамиМодель помещается, latency падаетЗатраты на связь
PipelineДелит модель на стадии по устройствамБольшие модели, больше throughputРастёт latency, для inference с жёсткой latency не любят
Context и sequenceДелят длинный вход между устройствамиДлинный контекстРедко применяют

Подбор копий моделей под GPU разного размера это задача упаковки (bin packing). Она усложняется с числом моделей и чипов.

Что работает чаще всего: quantization, tensor parallelism, replica parallelism и оптимизация attention. Выбор зависит от нагрузки: KV cache важнее для длинного контекста, prompt caching для длинных повторяющихся prompts и многоходовых диалогов. Нужна низкая latency: добавляйте replicas.

Книга написана в 2024 году. Вот что важно знать сейчас. Это наше дополнение, его нет в книге.

  • MoE изменил расчёт памяти и цены. Всю модель надо держать в памяти, но считается только часть экспертов на токен.
  • FP8 стал стандартом, FP4 приходит. Чипы Blackwell (B200) поддерживают FP4. Quantization из главы теперь часть обычного serving.
  • Disaggregated serving. Prefill и decode на разных GPU, как в главе, стал промышленной практикой (NVIDIA Dynamo, llm-d). Главные open-source движки: vLLM и SGLang.
  • KV cache стал меньше. Новые схемы attention (например, MLA у DeepSeek) сжимают cache. Расчёт размера cache из главы годится для классической attention.
  • Prompt caching есть у всех крупных API. Скидки и правила изменились с момента выхода книги. Проверяйте текущую документацию провайдера.
  • Reasoning модели меняют метрики. TTFT может быть большим из-за thinking-токенов. Смотрите на время до полного ответа и цену на задачу.
ТерминЗначение
inferenceРасчёт ответа обученной моделью
prefillПервый шаг: параллельная обработка входа. Compute-bound
decodeВторой шаг: генерация по одному токену. Memory-bandwidth-bound
TTFTВремя до первого токена
TPOTВремя на один следующий токен
throughputТокены (или запросы) в секунду по всему сервису
goodputЗапросы в секунду, которые уложились в SLO
MFU / MBUДоля использованных FLOP/s и пропускной способности памяти
HBMБыстрая память GPU
quantizationМеньше бит на параметр
speculative decodingDraft-модель предлагает токены, target проверяет
KV cacheСохранённые key и value векторы прошлых токенов
PagedAttentionХранение KV cache блоками, меньше фрагментации
FlashAttentionKernel, который сливает операции attention
continuous batchingBatch, в который новые запросы входят по мере выхода готовых
prompt cachingПовторное использование обработки общей части prompt
tensor parallelismДеление одной операции между устройствами
  • Выбирайте API по задаче. Ночные job (разметка тикетов, описания колонок, переиндексация) отправляйте в batch API: примерно вдвое дешевле. Интерактивные функции держите на online API.
  • Ставьте метрики с первого дня. Логируйте TTFT, TPOT, число входных и выходных токенов, перцентили p95 и p99. Одно среднее значение вводит в заблуждение.
  • Считайте цену на запрос. Ведите в warehouse таблицу вызовов: модель, токены на входе и выходе, cached токены, latency, цена. Из неё удобно строить модели в dbt и дашборды по стоимости.
  • Выходные токены дороже входных. Короткие ответы и ограничение длины вывода экономят деньги. Смотрите на структуру ответа (например, JSON вместо длинного текста).
  • Используйте prompt caching. Ставьте стабильную часть prompt (инструкции, схема таблиц, примеры) в начало, переменную часть в конец. Иначе общий префикс не сработает.
  • Следите за провайдерами. Одна и та же модель у разных провайдеров может давать чуть разное качество из-за их оптимизаций. Прогоняйте свой evaluation-набор при смене провайдера или версии quantization.
  • Не верьте GPU utilization. Высокий показатель в nvidia-smi не значит, что вы платите не зря. Считайте цену и goodput.
  • Свой хостинг. Проверьте, закрывает ли ваш framework (vLLM и аналоги) continuous batching, PagedAttention, prefix caching и tensor parallelism. Без них вы платите за простой железа.
  1. Почему prefill упирается в вычисления, а decode в память? Как это влияет на TTFT и TPOT?
  2. Сервис обрабатывает 200 запросов в минуту, но SLO выполняют 80. Чему равен goodput? Почему он полезнее throughput?
  3. Как speculative decoding ускоряет генерацию и почему качество ответа не меняется?
  4. Что такое KV cache и почему он ограничивает длину контекста и размер batch? Какие три группы приёмов его уменьшают?

Предыдущая: Dataset engineering. Следующая: Архитектура и обратная связь от пользователей.