vLLM, SGLang и непрерывный батчинг
Как инференс-серверы выжимают из GPU максимум — непрерывный батчинг, страничный KV-кэш, префикс-кэш, спекулятивное декодирование и раздельные prefill и decode у Kimi и DeepSeek.
Зачем это нужно
Заголовок раздела «Зачем это нужно»В прошлом уроке мы выяснили физику инференса: decode упирается в память, батчинг делает
генерацию для десятков пользователей почти бесплатной, а размер батча ограничен памятью под KV-кэш. Наивный сервер,
который вызывает model.generate() для каждого запроса по очереди, загружает вычислитель видеокарты на доли процента.
Инференс-движокинференс-движок (сервер инференса)EN inference engine / serving engineПрограмма, которая держит модель в памяти и обслуживает запросы — планирует батчи, управляет KV-кэшем, отдаёт OpenAI-совместимый API. Примеры — vLLM, SGLang, LMDeploy, llama.cpp. — vLLM, SGLang, LMDeploy — решает четыре задачи:
- держать батч всегда полным (непрерывный батчинг);
- не тратить память KV-кэша впустую (PagedAttention);
- не считать одно и то же дважды (префикс-кэш, RadixAttention);
- выжимать больше одного токена из каждого чтения весов (спекулятивное декодирование).
На масштабе тысяч GPU добавляется пятая: развести prefill и decode по разным машинам. Здесь китайские команды — Moonshot (Kimi) и DeepSeek — сделали одни из самых заметных открытых работ.
flowchart LR U["Запросы пользователей<br/>(OpenAI API)"] --> S["Планировщик<br/>кого взять в батч"] S --> M["Менеджер KV-кэша<br/>блоки, префиксы"] S --> R["Исполнитель модели<br/>ядра на GPU"] M <--> R R --> D["Детокенизатор"] --> O["Поток токенов<br/>обратно (SSE)"]
Запросы приходят по HTTP в формате OpenAI-совместимого APIOpenAI-совместимый APIEN OpenAI-compatible APIHTTP-интерфейс в формате OpenAI Chat Completions (/v1/chat/completions). Его отдают Ollama, llama-server, vLLM, SGLang, а также DeepSeek и Qwen в облаке — код клиента не меняется. (подробно — в уроке 8.1.1), а ответ уходит обратно потокомпотоковая выдача (стриминг)EN streamingОтвет приходит по кусочкам через Server-Sent Events; уменьшает время до первого токена, но не число токенов.: SSE (server-sent events) — способ, которым сервер шлёт клиенту кусочки ответа по мере готовности, не дожидаясь конца.
Непрерывный батчинг
Заголовок раздела «Непрерывный батчинг»При статическом батчингестатический батчингEN static batchingБатч запросов стартует и заканчивается целиком: короткие ответы ждут самый длинный, их места простаивают. сервер набирает, скажем, 32 запроса и генерирует, пока не закончится самый длинный ответ. Ответы бывают по 10 токенов, бывают по 1000. Тот, кто закончил на десятом шаге, освобождает место, но оно простаивает до конца батча. Новые запросы ждут в очереди.
Система Orca (OSDI 2022, Сеульский национальный университет и FriendliAI) предложила планировать на уровне итераций: после каждого шага decode закончившиеся последовательности уходят, а на их место сразу встают новые. Это и есть непрерывный батчингнепрерывный батчингEN continuous batching (iteration-level scheduling)Состав батча пересматривается после каждого шага decode — закончившиеся запросы уходят, новые встают на их место. Идея системы Orca (2022), стандарт всех современных серверов. — сегодня его делает любой серьёзный сервер.
Проверим на игрушечной модели планировщика. Шаг decode при memory-bound длится примерно одинаково при любом батче (урок 7.1.1), поэтому считаем шаги:
# Статический и непрерывный батчинг: игрушечный симулятор планировщикаimport random
rng = random.Random(0)N_REQ, SLOTS = 200, 32 # 200 запросов, в батч помещается 32 последовательности# Длины ответов: много коротких, мало длинных (от 10 до 1000 токенов).# rng.random() даёт число от 0 до 1; куб «прижимает» его к нулю (0.5³ = 0.125), поэтому коротких ответов большеlengths = [int(10 + 990 * rng.random() ** 3) for _ in range(N_REQ)]useful = sum(lengths) # сколько токенов реально нужно сгенерировать
def static_batching(lengths, slots): """Батч стартует целиком и ждёт самую длинную последовательность.""" steps = 0 for i in range(0, len(lengths), slots): steps += max(lengths[i:i + slots]) return steps
def continuous_batching(lengths, slots): """После каждого шага закончившиеся уходят, а на их место сразу встают новые запросы.""" queue, running, steps = list(lengths), [], 0 while queue or running: while queue and len(running) < slots: # дозаполняем свободные места running.append(queue.pop(0)) running = [r - 1 for r in running] # один шаг decode: каждой — по токену running = [r for r in running if r > 0] steps += 1 return steps
print(f"Запросов: {N_REQ}, средняя длина ответа: {useful / N_REQ:.0f}, самая длинная: {max(lengths)}")for name, fn in [("статический ", static_batching), ("непрерывный", continuous_batching)]: steps = fn(lengths, SLOTS) print(f"{name}: шагов {steps:6d}, загрузка слотов {useful / (steps * SLOTS):6.1%}")Запросов: 200, средняя длина ответа: 270, самая длинная: 988статический : шагов 6292, загрузка слотов 26.8%непрерывный: шагов 2142, загрузка слотов 78.8%Та же работа — в 2.9 раза быстрее. Оставшиеся 21% простоя — «хвост», когда очередь опустела и последние длинные ответы дописываются в неполном батче.
Куда девать prefill новых запросов? Новичка надо сначала «прогреть» — сделать prefill его промпта. Если промпт на 30 тысяч токенов, этот prefill займёт около секунды даже у 8B-модели на H100, и все остальные пользователи на это время замрут: их TPOT подскочит. Решение — prefill по частямprefill по частям (chunked prefill)EN chunked prefillДлинный промпт обрабатывается кусками, которые перемешиваются с шагами decode других запросов, чтобы те не замирали на время большого prefill.: длинный промпт режут на куски (например, по 2048 токенов) и в каждый шаг кладут один кусок плюс по токену decode для всех остальных. Шаги становятся ровными, а GPU получает смесь compute-bound и memory-bound работы. Современные vLLM и SGLang делают это по умолчанию.
PagedAttention: KV-кэш страницами
Заголовок раздела «PagedAttention: KV-кэш страницами»Вторая беда — память. KV-кэш последовательности растёт на токен за шаг, и заранее неизвестно, сколько токенов будет. Ранние серверы выделяли каждому запросу сплошной кусок памяти под максимальную длину. Если предел 2048 токенов, а ответ занял 300, остальное пропадало. Авторы vLLM измерили: в таких системах реальные данные кэша занимали лишь 20.4–38.2% выделенной под него памяти.
Решение подсмотрено у операционных систем — виртуальная память со страницами. Операционная система раздаёт программам память кусками фиксированного размера (страницами) и ведёт таблицу «какая страница программы где лежит физически». Программа видит сплошную память, хотя её страницы разбросаны где угодно. PagedAttentionPagedAttention (страничный KV-кэш)EN PagedAttentionKV-кэш хранится блоками фиксированного размера (например, по 16 токенов), а таблица блоков связывает логические позиции с физическими — как страницы виртуальной памяти в ОС. Основа vLLM.:
- KV-кэш делится на блоки фиксированного размера (в vLLM по умолчанию 16 токенов);
- у каждой последовательности есть таблица блоков: логический блок № 0, 1, 2… → физический блок где угодно в памяти;
- новый блок выделяется, только когда заполнился предыдущий. Впустую теряется максимум один неполный блок;
- ядро внимания умеет читать K и V по таблице блоков — отсюда название.
flowchart LR subgraph A["Запрос A: 40 токенов"] A0["лог. блок 0"] A1["лог. блок 1"] A2["лог. блок 2<br/>(занято 8 из 16)"] end subgraph G["Физическая память KV-кэша (GPU)"] P1["блок 7<br/>(общий для A и B)"] P2["блок 1"] P3["блок 3"] end subgraph B["Запрос B: тот же промпт"] B0["лог. блок 0"] end A0 --> P1 A1 --> P2 A2 --> P3 B0 --> P1
Бонус — общие блоки. Если вы просите 8 вариантов ответа на один промпт (так делает GRPO в
уроке 6.4.1), блоки промпта хранятся один раз, а у каждого блока есть счётчик
ссылок. Когда последовательность хочет дописать в общий блок, он копируется (copy-on-write — «копировать только при
записи»: пока все лишь читают, копия не нужна; так же операционная система Linux экономит память при fork(), создании
копии процесса).
Посчитаем выигрыш для Llama 3 8B (128 КиБ кэша на токен) и бюджета 16 ГиБ под кэш:
# Сколько памяти KV-кэша тратится впустую: резерв «на максимум» против страниц по 16 токеновimport mathimport random
rng = random.Random(0)MAX_LEN, BLOCK = 2048, 16 # предел длины и размер блока (страницы) в токенахKV_PER_TOKEN = 128 * 1024 # Llama 3 8B: 128 КиБ на токен (урок 5.2.1)BUDGET = 16 * 2**30 # 16 ГиБ видеопамяти под KV-кэш
# 200 запросов: промпт 50–500 токенов, ответ 10–1000 токенов (много коротких)seqs = [int(50 + 450 * rng.random()) + int(10 + 990 * rng.random() ** 3) for _ in range(200)]used = sum(seqs)reserved_contig = MAX_LEN * len(seqs) # резерв под максимумreserved_paged = sum(math.ceil(n / BLOCK) * BLOCK for n in seqs) # только целые блоки
print(f"Средняя длина последовательности: {used / len(seqs):.0f} токенов")print(f"Резерв на максимум : полезно {used / reserved_contig:6.1%} памяти")print(f"Страницы по {BLOCK} ток.: полезно {used / reserved_paged:6.1%} памяти")avg = used / len(seqs)print(f"Помещается в 16 ГиБ: {BUDGET // (MAX_LEN * KV_PER_TOKEN)} запросов против " f"~{BUDGET / (math.ceil(avg / BLOCK) * BLOCK * KV_PER_TOKEN):.0f} со страницами")Средняя длина последовательности: 547 токеновРезерв на максимум : полезно 26.7% памятиСтраницы по 16 ток.: полезно 98.6% памятиПомещается в 16 ГиБ: 64 запросов против ~234 со страницамиНаша игрушка попала ровно в диапазон из статьи. Батч вырос почти вчетверо, а из прошлого урока вы знаете: больше батч — больше throughput. По данным авторов, vLLM поднимает throughput в 2–4 раза по сравнению с FasterTransformer и Orca при той же задержке.
Префикс-кэш и RadixAttention
Заголовок раздела «Префикс-кэш и RadixAttention»Огромная доля запросов начинается одинаково:
- системный промпт приложения одинаков у всех пользователей;
- в диалоге каждый новый запрос заново содержит всю историю (урок 8.1.1);
- агент (модуль 8.3) на каждом шаге шлёт те же описания инструментов и растущую историю действий;
- few-shot примеры и длинный документ, по которому задают много вопросов.
Префикс-кэшпрефикс-кэш (кэширование префиксов)EN prefix cachingЕсли начало нового запроса совпадает с уже обработанным (системный промпт, история диалога), его KV-кэш берут готовым и prefill делают только для «хвоста». не выбрасывает KV-кэш после окончания запроса. Если новый запрос начинается с тех же токенов, их кэш берётся готовым и prefill делается только для «хвоста». TTFT падает в разы, GPU освобождается для других.
vLLM ищет совпадения поблочно: у каждого полного блока есть хеш, вычисленный по его токенам и хешу предыдущего блока. Совпал хеш — блок уже в памяти. Свободные блоки вытесняются по принципу LRU (давно не использованные — первыми).
SGLang пошёл дальше. В RadixAttentionRadixAttentionEN RadixAttentionПрефикс-кэш SGLang — KV-кэш всех запросов хранится в префиксном (radix) дереве, общие начала переиспользуются автоматически, редко нужные ветви вытесняются по LRU. кэш всех запросов лежит в префиксном (radix) дереве. Дерево в программировании — структура, где от одного корня расходятся ветви, а от них — новые ветви; общее начало двух путей хранится один раз (см. схему ниже). Здесь рёбра — последовательности токенов, узлы — их KV-кэш. Новый запрос спускается по дереву и находит самый длинный общий префикс любой длины. Листья вытесняются по LRU, а планировщик в первую очередь берёт запросы с самым длинным совпадением, чтобы нужные ветви не успели вытесниться. По данным авторов, на нагрузках с общими префиксами (агенты, few-shot, многоходовые диалоги) SGLang даёт до 6.4 раза больший throughput, чем другие системы на момент публикации.
flowchart TD
R(("корень")) --> S1["«Ты — помощник курса ChinaAI.»<br/>(системный промпт)"]
R --> S2["«You are a helpful assistant.»"]
S1 --> Q1["«Что такое KV-кэш?»"]
S1 --> Q2["«Объясни MLA»"]
Q2 --> Q3["ответ + «А в DeepSeek-V3?»<br/>(следующий ход диалога)"]
S2 --> Q4["«What is quantization?»"]Спекулятивное декодирование
Заголовок раздела «Спекулятивное декодирование»Decode при малом батче использует вычислитель на доли процента: веса читаются ради одного токена. А что если проверять сразу несколько? Прогнать через большую модель токенов-кандидатов стоит почти столько же, сколько один: это маленький prefill, веса читаются один раз. Нужно лишь где-то быстро взять кандидатов.
Спекулятивное декодированиеспекулятивное декодированиеEN speculative decoding (Google, 2022; DeepMind, 2023) делает так:
- маленькая черновая модельчерновая модель (драфт)EN draft modelМаленькая быстрая модель, которая в спекулятивном декодировании предлагает несколько токенов вперёд; большая модель проверяет их за один проход. (например, Qwen3-0.6B для Qwen3-32B — у них общий токенизатор) быстро генерирует токенов: со своими вероятностями ;
- большая модель одним проходом считает свои вероятности во всех позициях;
- токены проверяются слева направо: принимается с вероятностью
- при первом отказе вместо сэмплируется токен из нормированного остатка , остальные кандидаты выбрасываются. Если приняты все , большая модель бесплатно добавляет ещё один токен из своего .
Главная теорема: итоговые токены распределены в точности как у большой модели. Это не приближение — качество не меняется, меняется только скорость. При жадной генерации правило проще: кандидат принимается, если совпал с argmax большой модели.
Почему так? Если черновик предлагает токен реже, чем нужно (), его всегда принимают. Если чаще (), лишнюю часть «срезают» отказом, а освободившуюся вероятность раздают через остаток тем токенам, которые черновик недооценил. То же одной строкой. Токен получается двумя путями: черновик предложил его и его приняли (вероятность ) или случился отказ и выпал из остатка. Отказ случается с вероятностью , где , а сумма остатка тоже равна . Поэтому
Проверим численно на словаре из пяти токенов — английских и русских:
# Спекулятивное сэмплирование: черновик q, проверка большой моделью p — итог распределён ровно как pimport numpy as np
rng = np.random.default_rng(0)tokens = ["the", "is", "в", "и", "AI"]p = np.array([0.40, 0.25, 0.15, 0.15, 0.05]) # большая (целевая) модельq = np.array([0.30, 0.40, 0.10, 0.10, 0.10]) # маленькая черновая модель
def speculative_step(p, q, rng): """Один токен: черновик предлагает x ~ q, большая модель принимает его с вероятностью min(1, p/q).""" x = rng.choice(len(q), p=q) if rng.random() < min(1.0, p[x] / q[x]): return x, True residual = np.maximum(p - q, 0) # отказ: сэмплируем из «недостающей» части p return rng.choice(len(p), p=residual / residual.sum()), False
n = 200_000draws = [speculative_step(p, q, rng) for _ in range(n)]counts = np.bincount([x for x, _ in draws], minlength=len(p)) / naccepted = np.mean([ok for _, ok in draws])
print("токен p (цель) q (черновик) итог")for t, pi, qi, ci in zip(tokens, p, q, counts): print(f"{t:>4} {pi:8.3f} {qi:12.3f} {ci:6.3f}")print(f"Доля принятых: {accepted:.3f}, теория Σ min(p, q) = {np.minimum(p, q).sum():.3f}")
print("\nСколько токенов даёт одна проверка большой моделью (k токенов черновика, доля принятия α):")print(" α k=1 k=2 k=4 k=8")for a in [0.6, 0.8, 0.9]: row = [(1 - a ** (k + 1)) / (1 - a) for k in (1, 2, 4, 8)] print(f"{a:4.1f} " + " ".join(f"{v:4.2f}" for v in row))токен p (цель) q (черновик) итог the 0.400 0.300 0.401 is 0.250 0.400 0.250 в 0.150 0.100 0.149 и 0.150 0.100 0.150 AI 0.050 0.100 0.050Доля принятых: 0.801, теория Σ min(p, q) = 0.800
Сколько токенов даёт одна проверка большой моделью (k токенов черновика, доля принятия α): α k=1 k=2 k=4 k=8 0.6 1.60 1.96 2.31 2.47 0.8 1.80 2.44 3.36 4.33 0.9 1.90 2.71 4.10 6.13Столбец «итог» совпал с , хотя предлагал токены черновик. Доля принятиядоля принятых черновых токенов (acceptance rate)EN acceptance rateВ спекулятивном декодировании — какая доля токенов-черновиков совпала с выбором основной модели. У MTP-модуля DeepSeek-V3 — 85–90%. — чем ближе черновик к большой модели, тем она выше. Если каждый токен черновика принимается независимо с вероятностью , то первый токен проходит с вероятностью , первые два — и так далее, а ещё один токен большая модель добавляет всегда (из остатка при отказе или бесплатный, если приняты все ). Одна проверка в среднем даёт
Ускорение меньше этого числа: черновик тоже тратит время. Если шаг черновика стоит долю от шага большой модели, выигрыш равен . Например, при , и выходит раза. В статье Google на T5-XXL получилось 2–3 раза.
Откуда брать черновик:
| Способ | Идея | Плюсы и минусы |
|---|---|---|
| Черновая модель | маленькая модель того же семейства | просто; нужна вторая модель в памяти |
| MTP (DeepSeek-V3) | модуль предсказание нескольких токеновпредсказание нескольких токенов (MTP)EN multi-token prediction (MTP), обученный вместе с моделью, предсказывает токен на шаг дальше обычного | принимается 85–90% вторых токенов, генерация ≈1.8× быстрее (урок 5.4.2) |
| EAGLE | лёгкая «голова» поверх скрытых состояний большой модели, кандидаты деревом | дешевле отдельной модели, высокая доля принятия; голову надо обучить |
| n-gram / prompt lookup | кандидаты копируются из промпта | бесплатно; хорошо работает на правке кода и RAGгенерация с дополнением извлечёнными данными (RAG)EN retrieval-augmented generation (RAG) (ответ по найденным документам, ступень 8), где ответ повторяет контекст |
Когда не помогает. При большом батче вычислитель и так загружен — лишние проверки только мешают. При высокой температуре в творческих задачах черновик угадывает редко. Спекулятивное декодирование — это прежде всего способ снизить TPOT для небольшого числа пользователей.
PD-дезагрегация: prefill и decode на разных машинах
Заголовок раздела «PD-дезагрегация: prefill и decode на разных машинах»На одной GPU фазы мешают друг другу. Длинный prefill (compute-bound) задерживает шаги decode соседей, а decode (memory-bound) хочет огромный батч и много памяти под кэш. Даже chunked prefill лишь сглаживает конфликт.
Радикальное решение — PD-дезагрегацияPD-дезагрегация (раздельные prefill и decode)EN prefill-decode disaggregationPrefill и decode выполняются на разных группах GPU, а KV-кэш передаётся между ними по сети. Так фазы не мешают друг другу. Mooncake (Kimi), DistServe, инференс DeepSeek-V3.: держать отдельные группы GPU для prefill и для decode. Prefill-узел обрабатывает промпт и передаёт готовый KV-кэш decode-узлу по быстрой сети (RDMA — remote direct memory access: сетевая карта пишет прямо в память другой машины, минуя её процессор). Теперь каждую группу можно настроить под свою фазу: другой параллелизм, другой батч, даже другое железо. Идею формализовали в 2024 году работы DistServe (Пекинский университет и UC San Diego) и Splitwise (Microsoft).
Mooncake — архитектура сервинга Kimi (Moonshot AI вместе с Университетом Цинхуа, best paper конференции FAST’25). Её центр — KV-кэш:
- prefill- и decode-кластеры разделены;
- простаивающие у GPU-серверов оперативная память и SSD объединены в распределённый пул KV-кэша, поэтому кэш префиксов можно хранить долго и в огромном объёме;
- глобальный планировщик (Conductor) отправляет запрос туда, где больше его префикса уже в кэше и меньше нагрузка;
- при перегрузке запросы, которые всё равно не уложатся в SLO, отклоняются сразу, а не после дорогого prefill.
Авторы сообщают, что на реальной нагрузке Kimi такая архитектура позволила обслуживать заметно больше запросов при тех же требованиях к задержке (в первой версии статьи — на 75% больше). Код открыт: kvcache-ai/Mooncake; его транспорт KV-кэша используют SGLang и vLLM.
DeepSeek разносит фазы так же (цифры — в «Китайском угле» прошлого урока): prefill идёт блоками по 4 узла, decode — блоками по 18 узлов с другим параллелизмом экспертов. StepFun в Step-3 пошла ещё дальше и разнесла по разным GPU даже внимание и FFN на decode (AFD, arXiv:2507.19427).
Ядра и связь: Open Source Week DeepSeek
Заголовок раздела «Ядра и связь: Open Source Week DeepSeek»В феврале 2025 года DeepSeek пять дней подряд открывала свою инфраструктуру (open-infra-index). Для инференса важны три вещи.
FlashMLA — ядро decode для MLA на GPU Hopper, со страничным KV-кэшем (блоки по 64 токена). По README на момент релиза — до 3000 ГБ/с в memory-bound режиме на H800 SXM5 при паспортных 3.35 ТБ/с. Вот что значит «хорошее ядро» в терминах roofline: около 90% пропускной способности памяти. FlashMLA вдохновлено FlashAttention — как устроены такие ядра, разобрано в уроке 5.7.2.
DeepEP — библиотека пересылок для параллелизма экспертов MoE: раздать токены по экспертам на разных GPU (dispatch) и собрать результаты (combine). Есть ядра на пропускную способность (обучение и prefill) и отдельные ядра на минимальную задержку для decode, поддержка FP8 и перекрытие связи с вычислениями без занятия вычислительных блоков GPU.
3FS — распределённая файловая система DeepSeek. В README среди её применений названо хранение KV-кэша для инференса на SSD — дешёвая альтернатива оперативной памяти. Это перекликается с дисковым кэшем контекста в API DeepSeek: по статистике из обзора инференс-системы V3/R1 (за сутки наблюдения), в него попадали 56.3% входных токенов.
Кто есть кто среди движков
Заголовок раздела «Кто есть кто среди движков»| Движок | Откуда | Сильные стороны | Когда брать |
|---|---|---|---|
| vLLM | UC Berkeley, огромное сообщество | PagedAttention, шире всех поддержка моделей и железа | универсальный сервер по умолчанию |
| SGLang | LMSYS, много китайских разработчиков | RadixAttention, поддержка DeepSeek, Qwen и GLM в день релиза, PD-дезагрегация | агенты, диалоги, большие MoE |
| LMDeploy | Shanghai AI Lab | движок TurboMind, 4-битные веса и квантованный KV-кэш | InternLM и Qwen в квантованном виде |
| TensorRT-LLM | NVIDIA | максимум скорости на NVIDIA, сложнее в настройке | продакшн на картах NVIDIA |
| llama.cpp, Ollama | сообщество | CPU + GPU, формат GGUF, простота | локально, один пользователь (урок 7.2.2) |
| KTransformers | Цинхуа (MADSys) и Approaching.AI | эксперты MoE на CPU, внимание на GPU | DeepSeek-V3/R1 на одной GPU и большой RAM |
Практика
Заголовок раздела «Практика»Запустите три симулятора из урока и поэкспериментируйте:
- В симуляторе батчинга поставьте
SLOTS = 64иSLOTS = 8. Как меняется выигрыш непрерывного батчинга? Сделайте все ответы одинаковой длины (lengths = [270] * N_REQ) — что осталось от выигрыша и почему? - В симуляторе памяти поставьте
BLOCK = 1иBLOCK = 256. Почему vLLM не берёт блок в 1 токен, хотя потери тогда нулевые? (Подсказка: таблица блоков и эффективность ядра внимания.) - В симуляторе спекулятивного сэмплирования сделайте
qравнымpи, наоборот, очень непохожим. Как меняется доля принятия? Убедитесь, что столбец «итог» всегда совпадает с .
Проверь себя
Заголовок раздела «Проверь себя»- Непрерывный батчинг держит батч полным: состав пересматривается после каждого шага, длинные промпты обрабатываются по частям (chunked prefill).
- PagedAttention хранит KV-кэш блоками по таблице, как страницы виртуальной памяти: вместо 20–40% полезной памяти — почти 100%, значит, батч в разы больше.
- Префикс-кэш и RadixAttention не пересчитывают общие начала запросов: системные промпты, историю диалога, контекст агента. Стабильное — в начало промпта.
- Спекулятивное декодирование проверяет несколько дешёвых кандидатов за один проход и не меняет распределение; черновиком служат маленькая модель, MTP или EAGLE.
- На масштабе фазы разносят по разным машинам (PD-дезагрегация): Mooncake у Kimi, раздельные prefill и decode у DeepSeek, ядра FlashMLA и связь DeepEP.
Дальше: квантование весов — как уменьшить байты, которые decode читает на каждом шаге, и уместить 70B-модель в одну-две видеокарты.