Перейти к содержимому
Ступень 77.1 Как работает инференс

Prefill и decode, узкие места

Почему первый токен ответа появляется с паузой, а остальные «печатаются» с постоянной скоростью, и как по пропускной способности памяти предсказать токены в секунду любой модели на любом железе.

УРОКstage-7/7-1-inference/01-prefill-decode.mdx1 ч 50 минУрок 1 из 2
После урока вы сможете
  • Объяснить, чем фазы prefill и decode отличаются по вычислениям и обращениям к памяти и какие метрики (TTFT, TPOT, throughput) они задают
  • Посчитать арифметическую интенсивность операции и по roofline-модели определить, во что она упирается — в память или в вычисления
  • Оценить скорость генерации (токенов/с) модели на конкретном GPU или CPU и объяснить, почему батчинг умножает суммарную скорость

Отправьте модели длинный документ с просьбой пересказать. Сначала ничего не происходит: секунда, две. Потом текст начинает «печататься» с ровной скоростью, слово за словом. В уроке 8.1.1 вы увидите и вторую странность: провайдеры берут за токены ответа в несколько раз больше, чем за токены вопроса, хотя вопрос обычно длиннее.

Обе странности объясняет одно: инференсинференс, вывод моделиEN inferenceСм. «reasoning»: в китайском одно и то же слово. LLM состоит из двух фаз с совершенно разной физикой. Первая упирается в скорость вычислений, вторая — в скорость памяти. Поймёте это — и сможете без замеров, с карандашом в руках сказать:

  • сколько токенов в секунду выдаст Qwen3-8B на вашей видеокарте, а сколько — на ноутбуке без неё;
  • почему 4-битное квантование (урок 7.2.1) ускоряет генерацию в 3–4 раза;
  • почему сервер, обслуживающий сотню пользователей сразу, выдаёт в десятки раз больше токенов, чем для одного, на том же железе (следующий урок);
  • почему DeepSeek ради уменьшения KV-кэшаKV-кэшEN KV cache придумала MLA, а prefill и decode держит на разных машинах.

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

Фаза 1 — prefillprefill (предзаполнение)EN prefillПервая фаза генерации: весь промпт проходит через модель одним параллельным проходом, заполняется KV-кэш и получается первый токен. Упирается в вычисления.. Весь промпт из TT токенов проходит через модель одним проходом. Все TT позиций считаются параллельно: каждый слой умножает матрицу активаций T×dT \times d на матрицу весов. По пути ключи и значения всех TT токенов записываются в KV-кэш, а из логитов последней позиции сэмплируется первый токен ответа.

Фаза 2 — decodeдекодирование (фаза decode)EN decode phaseВторая фаза: один токен за один проход модели. На каждый токен заново читаются все (активные) веса и KV-кэш, поэтому фаза упирается в пропускную способность памяти.. Дальше модель работает в цикле: подаёт на вход один новый токен, читает KV-кэш, выдаёт следующий токен, дописывает его ключ и значение в кэш. И так до токена конца или до max_tokens. Каждый шаг — полный проход через все слои ради одного токена.

flowchart LR
P["Промпт: T токенов"] --> PF["Prefill<br/>один проход по всем T<br/>(матрица × матрица)"]
PF --> KV[("KV-кэш")]
PF --> T1["Токен 1"]
T1 --> D["Шаг decode<br/>1 токен за проход<br/>(вектор × матрица)"]
KV <--> D
D --> TN["Токен 2, 3, …"]
TN -->|"ещё не конец"| D
Prefill один раз обрабатывает весь промпт; decode много раз прогоняет модель ради одного токена
PrefillDecode
Токенов за проходвесь промпт (TT = сотни–тысячи)1 на каждую последовательность
Главная операцияумножение матрицы на матрицу (GEMM, General Matrix Multiply)умножение вектора на матрицу (GEMV, General Matrix–Vector)
Что ограничиваетвычисления (FLOP/с)пропускная способность памяти (байт/с)
Какую метрику задаётвремя до первого токенаскорость «печати»
Сколько раз выполняетсяодин раз на запросстолько раз, сколько токенов в ответе

Пример. Модель на 8 млрд параметров на RTX 4090, промпт 2000 токенов, ответ 500 токенов. Prefill займёт около 0.4 с, а decode — около 10 с (при реальных ~50 токенах/с). Промпт вчетверо длиннее ответа, но ответ занимает в 25 раз больше времени. Откуда эти числа — дальше.

Чтобы понять, во что упирается каждая фаза, посчитаем две вещи: сколько операций нужно на токен и сколько байт придётся прочитать из памяти.

Операции. Каждый вес модели участвует в обработке токена ровно один раз: одно умножение и одно сложение. Поэтому прямой проход стоит около 2N2N FLOPsFLOPs, число операций с плавающей точкойEN floating-point operations (FLOPs)Не путайте FLOPs (сколько операций нужно сделать) и FLOP/s, FLOPS (сколько операций железо делает в секунду). на токен, где NN — число параметров. Это та же двойка, что в формуле 6ND6ND из урока 5.1.1: 2 на прямой проход и 4 на обратный. Для Llama 3 8B это 2⋅8⋅109=162 \cdot 8 \cdot 10^9 = 16 GFLOP на каждый токен (приставка «гига» — миллиард, 10910^9).

К этому добавляется внимание к контексту: на позиции tt каждый слой считает tt скалярных произведений с ключами и взвешенную сумму tt значений — около 4⋅L⋅d⋅t4 \cdot L \cdot d \cdot t FLOPs (LL — число слоёв, dd — размер модели). Откуда четвёрка: скалярное произведение векторов длины dd — это dd умножений и dd сложений, 2d2d FLOPs; таких произведений tt, итого 2dt2dt. Взвешенная сумма tt векторов длины dd — ещё 2dt2dt. Вместе 4dt4dt на слой. У Llama 3 8B (L=32L = 32, d=4096d = 4096) на позиции 8192 это 4⋅32⋅4096⋅8192≈4.34 \cdot 32 \cdot 4096 \cdot 8192 \approx 4.3 GFLOP — заметно, но меньше 2N2N.

Байты. Чтобы обработать токен, нужно прочитать из видеопамяти все веса (в BF16 — по 2 байта на параметр, у Llama 3 8B это 16 ГБ) и весь KV-кэш контекста (128 КиБ на токен у Llama 3 8B, см. урок 5.2.1).

А теперь главное. При prefill веса читаются один раз на весь промпт: прочитали матрицу слоя — и сразу умножили её на все TT токенов. При decode веса читаются один раз на один токен. Работа на каждый прочитанный байт различается в TT раз.

У любого вычислителя два предела:

  • пиковая производительность PP — сколько операций в секунду он может сделать (FLOP/с);
  • пропускная способность памятипропускная способность памятиEN memory bandwidthСколько байт в секунду память отдаёт вычислителю. RTX 4080 — около 717 ГБ/с, H100 — 3.35 ТБ/с, двухканальная DDR5 — около 90 ГБ/с. Главное число для скорости decode. WW — сколько байт в секунду память успевает подвезти.

Операция, которой нужно FF операций и MM байт, займёт не меньше max⁡(F/P, M/W)\max(F / P,\ M / W) секунд: нельзя посчитать быстрее пика и нельзя получить данные быстрее, чем их отдаёт память. Отношение

I=FM[FLOP/байт]I = \frac{F}{M} \quad \text{[FLOP/байт]}

называется арифметической интенсивностьюарифметическая интенсивностьEN arithmetic intensityСколько операций приходится на один прочитанный из памяти байт (FLOP/байт). Decode с батчем 1 в BF16 — около 1, prefill на 1000 токенов — около 1000.. Сравним её с отношением P/WP / W — точкой перегиба (ridge point). Если I<P/WI < P / W, операция ограничено памятьюограничено памятью (memory-bound)EN memory-boundОперация, которая дольше ждёт данные из памяти, чем считает. Ускоряется, если читать меньше байт (квантование, GQA, MLA) или делать больше работы на каждый прочитанный байт (батчинг).: вычислитель простаивает в ожидании данных. Если I>P/WI > P / W — ограничено вычислениямиограничено вычислениями (compute-bound)EN compute-boundОперация, в которой вычислитель загружен полностью, а память успевает подвозить данные. Ускоряется только более быстрым железом или меньшим числом операций.: память успевает, вычислитель загружен.

Это и есть модель rooflineмодель roofline («крыша»)EN roofline modelДостижимая производительность = min(пик FLOP/с, интенсивность × пропускная способность памяти). Точка перегиба (ridge point) = пик / пропускная способность; у H100 около 295 FLOP/байт.: достижимая производительность равна min⁡(P, I⋅W)\min(P,\ I \cdot W). График похож на крышу — наклонный скат (упор в память), затем ровный конёк (упор в вычисления). Для обучения вы уже применяли её в уроке 5.7.1; здесь — для инференса.

xychart-beta
title "Roofline: достижимые TFLOPS в зависимости от интенсивности"
x-axis "FLOP на байт" ["1", "2", "4", "8", "16", "32", "64", "128", "256", "512", "1024"]
y-axis "TFLOPS (BF16)" 0 --> 1000
line [3.4, 6.7, 13.4, 26.8, 53.6, 107.2, 214.4, 428.8, 857.6, 989, 989]
line [0.7, 1.4, 2.9, 5.7, 11.5, 22.9, 45.9, 91.8, 97.5, 97.5, 97.5]
Верхняя линия — H100 SXM, нижняя — RTX 4080. Decode с батчем 1 сидит в самой левой точке: используется меньше 1% вычислителя

Характеристики популярных устройств (плотные BF16-операции тензорных ядер, без «маркетинговой» разреженности):

УстройствоПамять, ГБ/сBF16, TFLOPSТочка перегиба, FLOP/байт
RTX 408071797.5136
RTX 40901008165164
A100 80GB2039312153
H100 SXM3350989295
ПК, DDR5 в 2 канала~90——
EPYC, DDR4-3200 в 8 каналов~205——
EPYC 9004, DDR5-4800 в 12 каналов~460——

Нижние три строки — не видеокарты, а обычная оперативная память: домашний ПК с двумя «каналами» памяти и серверные процессоры AMD EPYC, у которых каналов 8–12 (каждый канал — отдельная «дорога» к памяти, их пропускные способности складываются). Вычислительную мощность процессоров мы не указываем: для decode важна именно память. H100 SXM — серверная версия карты H100 для специальных плат.

Теперь посчитаем интенсивность фаз (веса в BF16, 2 байта на параметр):

Idecode=2N2N=1,Iprefill=2N⋅T2N=T.I_{\text{decode}} = \frac{2N}{2N} = 1, \qquad I_{\text{prefill}} = \frac{2N \cdot T}{2N} = T.

Decode с одним пользователем делает 1 операцию на байт при точке перегиба 136–295: тензорные ядра больше 99% времени ждут память. Prefill промпта из 1000 токенов делает 1000 операций на байт — выше точки перегиба любого GPU из таблицы, то есть упирается в вычисления. А вот короткий промпт из 100 токенов на H100 (точка перегиба 295) ещё memory-bound: граница между фазами проходит не по названию, а по интенсивности.

Раз decode упирается в память, его скорость при батче 1 оценивается одной формулой:

токенов/с  ≤  Wбайт весов+байт KV-кэша.\text{токенов/с} \;\le\; \frac{W}{\text{байт весов} + \text{байт KV-кэша}}.

Пример. Qwen3-8B (8.2 млрд параметров) в BF16 весит 16.4 ГБ. На RTX 4080 с 717 ГБ/с выйдет не больше 717/16.4≈44717 / 16.4 \approx 44 токенов/с. В 4-битном формате Q4_K_M (формат программы llama.cpp, разберём его в уроке 7.2.1; ≈4.9 бита на вес, файл около 5 ГБ) — 717/5≈143717 / 5 \approx 143 токена/с. Вот почему квантование ускоряет генерацию: байт меньше — шаг короче.

Потолок скорости decode при батче 1 (токенов/с, только веса, без KV-кэша):

МодельРазмер весовDDR5, 2 каналаEPYC, 8 каналовRTX 4080RTX 4090H100
Qwen3-8B, BF1616.4 ГБ5124461204
Qwen3-8B, Q4_K_M5.0 ГБ1841143201667
Qwen3-32B, Q4_K_M20.1 ГБ4103650167
Llama 3 70B, 4.5 бита39.7 ГБ25182584
Qwen3-30B-A3B, Q4_K_M2.0 ГБ активных441013554991657

Реальные программы достигают 50–85% этого потолка: часть времени уходит на запуск ядер, синхронизацию и чтение KV-кэша. Но порядок величин формула даёт верно, и это лучший способ заранее понять, «потянет» ли железо модель. Как перейти от неё к числу карт под целый сервис (нагрузка, KV-кэш, батч, запас) — в «оценках на салфетке» урока 11.2.1; собрать такой калькулятор можно в домашней работе s11-hw04-capacity.

MoE читает только активных экспертов. Последняя строка таблицы — модель смесь экспертовсмесь экспертов (MoE)EN mixture of experts (MoE) на 30.5 млрд параметров, из которых на токен работают 3.3 млрд (активные параметрыактивные параметрыEN activated (active) parametersСколько параметров MoE-модели реально участвует в обработке одного токена. «A22B» в названии Qwen3-235B-A22B — 22 млрд активных из 235 млрд.). Памяти ей нужно как 30B-модели, а при батче 1 читается и считается только 3.3B — скорость как у 3B-модели. Поэтому MoE так хороши для локального запуска, если хватает памяти. DeepSeek-V3 устроена так же: 671 млрд параметров, 37 млрд активных (урок 5.3.1).

Длинный контекст замедляет decode. На каждом шаге читается ещё и KV-кэш всего контекста. У Llama 3 8B на 32 тысячах токенов это 4.3 ГБ сверху к 16 ГБ весов — генерация медленнее примерно на 20%. При 128 тысячах кэш весит уже столько же, сколько модель, и скорость падает вдвое. Поэтому GQA, MLA и FP8-кэш не только экономят память, но и ускоряют генерацию.

Пусть на сервере не один пользователь, а BB. На каждом шаге decode модель делает по одному токену для каждой последовательности: умножает не вектор, а матрицу B×dB \times d на те же веса. Веса читаются один раз и служат всем. Операций в BB раз больше, а байт весов — столько же:

Idecode(B)≈2N⋅BN⋅b=2Bb,I_{\text{decode}}(B) \approx \frac{2N \cdot B}{N \cdot b} = \frac{2B}{b},

где bb — байт на параметр. Пока II ниже точки перегиба, шаг длится почти столько же, сколько при B=1B = 1, а токенов выходит в BB раз больше. Если бы на каждом шаге читались только веса, батч из 64 пользователей дал бы почти бесплатное 64-кратное ускорение.

Но есть подвох: KV-кэш батчинг не делит. У каждой последовательности свой контекст и свой кэш, и его надо прочитать целиком. Посчитаем по roofline для Llama 3 8B в BF16 на H100, контекст 2048 токенов у каждой последовательности (0.27 ГБ кэша на последовательность). Каждая строка считается так: байты шага = веса + BB × кэш одной последовательности; длительность шага = байты / 3350 ГБ/с; токенов/с = BB / шаг. Например, при B=64B = 64: (16+64⋅0.27)/3350≈0.0099(16 + 64 \cdot 0.27) / 3350 \approx 0.0099 с, то есть 9.9 мс, и 64/0.0099≈645064 / 0.0099 \approx 6450 токенов/с.

БатчШаг, мсВсего токенов/сНа одного пользователяПамять: веса + кэш
14.920520516.3 ГБ
85.41 47218418.2 ГБ
327.44 34913624.6 ГБ
649.96 45010133.2 ГБ
12815.18 5056650.4 ГБ
25625.310 1164084.8 ГБ — не влезает в 80 ГБ

Три вывода:

  1. Суммарная скорость (throughputthroughput (суммарная скорость сервиса)EN throughputСколько токенов в секунду сервер выдаёт всем пользователям вместе. Батчинг повышает throughput ценой роста TPOT у каждого пользователя.) выросла в 50 раз, а скорость для каждого пользователя упала всего в 5 раз. Именно так облачные провайдеры делают токены дешёвыми.
  2. Даже при батче 256 шаг упирается в память, а не в вычисления: растёт чтение KV-кэша. Для одних только весов переход в compute-bound случился бы при B≈295B \approx 295 (точка перегиба H100), но кэш его отодвигает.
  3. Батч ограничен объёмом памяти: при 256 пользователях кэш уже не помещается. Сократили кэш вдвое (FP8, GQA, MLA) — примерно вдвое больше пользователей на одной карте и заметно дешевле токен. Это экономика, на которой DeepSeek начала ценовую войну 2024 года (Китайская линза).

Батч ограничен не только вычислениями, но и памятью: у каждого запроса свой KV-кэш, и он растёт с длиной контекста. Выберите в калькуляторе модель, поставьте длину контекста 32 768 и батч 16 и сравните результат с памятью вашей карты (32 ГБ). Потом переключите точность на FP8 и посмотрите, сколько запросов теперь помещается. Именно поэтому серверы вроде vLLM управляют KV-кэшем как памятью операционной системы (урок 7.1.2).

Калькулятор KV-кэшаинтерактив
Архитектура
Нагрузка
Та же модель с разными схемами внимания (на токен)
MHA/GQA: на токен хранится 2 · слои · KV-головы · размер головы чисел (K и V). MLA (DeepSeek-V2/V3): на токен хранится только латент c_KV размера d_c и общий RoPE-ключ размера d_rope — (d_c + d_rope) · слои чисел.

Скорость инференса нельзя описать одним числом. Различают три:

  • время до первого токенавремя до первого токена (TTFT)EN time to first token (TTFT)Сколько пользователь ждёт первое слово ответа: очередь + prefill всего промпта. Растёт с длиной промпта. — время до первого токена: ожидание в очереди плюс prefill. Растёт с длиной промпта. Именно его вы ощущаете как «модель задумалась».
  • время на выходной токенвремя на выходной токен (TPOT)EN time per output token (TPOT)Средняя пауза между токенами ответа после первого — длительность шага decode. 1 / TPOT — скорость «печати» для одного пользователя. — средний интервал между следующими токенами, то есть длительность шага decode. Скорость «печати» для одного пользователя — 1/TPOT1 / \text{TPOT}.
  • throughput — сколько токенов в секунду сервер выдаёт всем пользователям вместе.

Полное время ответа из nn токенов:

время≈TTFT+(n−1)⋅TPOT.\text{время} \approx \text{TTFT} + (n - 1) \cdot \text{TPOT}.

Вернёмся к примеру из начала: 8B-модель на RTX 4090, промпт 2000 токенов. Prefill — около 2⋅8⋅109⋅2000=3.2⋅10132 \cdot 8 \cdot 10^9 \cdot 2000 = 3.2 \cdot 10^{13} FLOPs. При 165 TFLOPS и загрузке около 50% выходит 3.2⋅1013/8.3⋅1013≈0.43.2 \cdot 10^{13} / 8.3 \cdot 10^{13} \approx 0.4 с. TPOT — около 20 мс (50 токенов/с). Ответ из 500 токенов придёт за 0.4+499⋅0.02≈10.40.4 + 499 \cdot 0.02 \approx 10.4 с.

Метрики тянут в разные стороны. Больше батч — выше throughput, но выше и TPOT у каждого. Длинный prefill чужого запроса задерживает шаги decode всех остальных. Сервисы задают целевые уровни (SLO), например «TTFT меньше 2 с, TPOT меньше 50 мс», и ищут максимальный throughput, при котором эти уровни соблюдаются. Как именно — в следующем уроке, а нагрузочный тест собственного LLM-сервиса против SLO — в уроке 8.4.3.

Задание в браузере: калькулятор скорости генерации

Заголовок раздела «Задание в браузере: калькулятор скорости генерации»

Соберите калькулятор, который по паспортным данным GPU и модели предсказывает размер весов и кэша, точку перегиба, скорость decode при разном батче, время prefill и максимальный батч. Потом проверьте им таблицы этого урока.

Калькулятор скорости генерации7 тестов
# Калькулятор скорости генерации — оценка «на салфетке» по roofline-модели.
# Единицы: память — в ГБ (1 ГБ = 1e9 байт), пропускная способность — в ГБ/с,
# пиковая производительность — в TFLOPS (1e12 операций в секунду), параметры — в миллиардах.

GPUS = {
    # имя: (пропускная способность памяти, ГБ/с;  плотные BF16 TFLOPS тензорных ядер)
    "RTX 4080": (716.8, 97.5),
    "RTX 4090": (1008, 165.2),
    "A100 80GB": (2039, 312),
    "H100 SXM": (3350, 989),
}


def weights_gb(params_b, bits):
    """Сколько ГБ занимают веса модели.

    params_b — число параметров в миллиардах, bits — бит на параметр (16 для BF16, 4 для INT4).
    weights_gb(8, 16) == 16.0;  weights_gb(70, 4) == 35.0
    """
    # TODO: ваш код
    ...


def kv_cache_gb(layers, kv_heads, head_dim, context, batch=1, bytes_per_value=2):
    """Размер KV-кэша в ГБ: 2 (K и V) · слои · KV-головы · размер головы · длина · батч · байт на число.

    Llama 3 8B (32 слоя, 8 KV-голов, голова 128) на 8192 токенах в BF16 — около 1.074 ГБ.
    """
    # TODO: ваш код
    ...


def ridge_point(bandwidth_gbs, peak_tflops):
    """«Точка перегиба» roofline: сколько FLOP на байт нужно, чтобы упереться в вычисления.

    ridge = пиковые FLOP/с / байт/с. Для H100 SXM: 989e12 / 3350e9 ≈ 295.2 FLOP/байт.
    """
    # TODO: ваш код
    ...


def decode_throughput(params_b, bits, bandwidth_gbs, peak_tflops, batch=1, kv_gb_per_seq=0.0):
    """Скорость шага декодирования для батча из batch последовательностей.

    За один шаг каждая из batch последовательностей получает один новый токен. Для этого нужно:
      * прочитать из памяти все веса один раз (их делят все последовательности батча)
        и KV-кэш каждой последовательности: байты = веса + batch · kv_gb_per_seq;
      * выполнить 2 · N · batch операций (N — число параметров).
    Время шага = max(время чтения памяти, время вычислений) — roofline.

    Для MoE передавайте в params_b число АКТИВНЫХ параметров.
    Возвращает кортеж (токенов/с суммарно, токенов/с на одну последовательность).
    """
    # TODO: ваш код
    ...


def prefill_seconds(params_b, prompt_tokens, peak_tflops, mfu=0.5):
    """Время prefill: 2 · N · T операций при доле mfu от пиковой производительности.

    prefill_seconds(8, 4000, 989, 0.5) ≈ 0.129 с
    """
    # TODO: ваш код
    ...


def max_batch(memory_gb, params_b, bits, kv_gb_per_seq):
    """Сколько последовательностей поместится в память одновременно (целое число).

    Сначала в память кладём веса, всё остальное отдаём под KV-кэш.
    Если веса не помещаются — 0.
    """
    # TODO: ваш код
    ...


# Проверьте себя кнопкой «Запустить»:
print(weights_gb(8.03, 16), kv_cache_gb(32, 8, 128, 8192))
print(ridge_point(*GPUS["H100 SXM"]))
print(decode_throughput(8.03, 16, *GPUS["RTX 4090"]))
print(prefill_seconds(8, 4000, 989), max_batch(80, 8.03, 16, 0.268))

Локально: почувствуйте memory-bound своими руками

Заголовок раздела «Локально: почувствуйте memory-bound своими руками»

Возьмём один «слой» — матрицу 8192×81928192 \times 8192 — и будем умножать на неё батчи разного размера. При батче 1 это ровно шаг decode: прочитать все веса ради одного вектора. Код работает на CPU (нужен только PyTorch):

# Почему батчинг ускоряет: один и тот же «слой» 8192×8192 на батчах разного размера (CPU)
import time
import torch
torch.manual_seed(0)
d = 8192
W = torch.randn(d, d) # 8192² чисел × 4 байта = 268 МБ — больше кэша любого процессора
print(f"Веса слоя: {W.numel() * 4 / 1e6:.0f} МБ, потоков: {torch.get_num_threads()}")
def bench(B, repeats=5):
x = torch.randn(B, d)
x @ W.T # прогрев
t0 = time.perf_counter()
for _ in range(repeats):
x @ W.T
return (time.perf_counter() - t0) / repeats
print(" батч время, мс токенов/с ГФЛОП/с ГБ/с (веса)")
for B in [1, 4, 16, 64, 256]:
t = bench(B)
flops = 2 * B * d * d # умножение + сложение на каждый вес и каждый токен
print(f"{B:5d} {t * 1e3:9.2f} {B / t:9.0f} {flops / t / 1e9:7.1f} {W.numel() * 4 / t / 1e9:6.1f}")

Пример вывода на слабой облачной машине с 4 потоками (у вас числа будут больше, но картина та же):

Веса слоя: 268 МБ, потоков: 4
батч время, мс токенов/с ГФЛОП/с ГБ/с (веса)
1 44.76 22 3.0 6.0
4 60.69 66 8.8 4.4
16 96.00 167 22.4 2.8
64 152.93 418 56.2 1.8
256 381.70 671 90.0 0.7

Как читать: при переходе от батча 1 к батчу 4 время выросло лишь в 1.4 раза, а «токенов» стало вчетверо больше. До батча 64 время растёт намного медленнее батча — это зона memory-bound. От 64 к 256 время выросло уже в 2.5 раза при вчетверо большем батче: вычислитель начинает загружаться, и «бесплатный» батч заканчивается. Столбец «ГБ/с» при батче 1 — это реальная пропускная способность памяти вашей машины на такой операции. Сравните её с паспортной.

Проверь себя6 вопросов
1 Какая фаза генерации при одном пользователе упирается в пропускную способность памяти?
2 Модель 14B в 4-битном формате занимает около 7 ГБ. Видеокарта отдаёт 700 ГБ/с. Какой потолок скорости decode (токенов/с) при батче 1, если не учитывать KV-кэш?
3 Что заметно ускорит decode одного пользователя на том же GPU? Выберите все верные.Можно выбрать несколько ответов
4 У ускорителя 1000 ГБ/с памяти и 200 TFLOPS. Чему равна точка перегиба roofline (FLOP на байт)?
5 В таблице для H100 при батче 256 скорость на одного пользователя упала до 40 токенов/с. Главная причина?
6 Пользователь вставил в чат 50-страничный договор. Первый токен ответа пришёл через 6 секунд, дальше текст идёт быстро. Что вероятнее всего?
  • Генерация состоит из двух фаз. Prefill обрабатывает промпт одним проходом и упирается в вычисления; decode выдаёт по токену за проход и упирается в пропускную способность памяти.
  • Арифметическая интенсивность (FLOP на байт) против точки перегиба железа (у H100 ≈ 295) показывает, что ограничивает операцию. Decode с батчем 1 — около 1 FLOP/байт.
  • Потолок скорости decode: токенов/с ≈ пропускная способность / байт за токен. Квантование, MoE, GQA и MLA уменьшают байты и потому ускоряют генерацию.
  • Батчинг делит чтение весов между пользователями и умножает throughput, но KV-кэш у каждого свой — он ограничивает и скорость, и число пользователей на GPU.
  • Метрики: TTFT (очередь + prefill), TPOT (шаг decode) и throughput. Сервер ищет баланс между ними.

Так об этом спрашивают на собеседовании: «Почему decode упирается в память? Сколько токенов в секунду даст Qwen3-8B на одной RTX 4080?» — разбор с батчами и KV-кэшем в задаче 2 урока 11.3.3.

Дальше: как vLLM и SGLang выжимают максимум из этой физики — непрерывный батчинг, страничный KV-кэш, префикс-кэш, спекулятивное декодирование и раздельные prefill и decode у Kimi и DeepSeek.