Как устроена LLM · How LLMs work · урок 4 · 14 мин

Inference: context, sampling, KV cache и serving

Inference: context, sampling, KV cache, serving

ТемыLLM изнутриInference и servingContext

Зачем

После урока ты объяснишь, что происходит, когда модель отвечает: почему первый token приходит с задержкой, а дальше текст «течёт», что делают temperature и top-p, сколько памяти съедает KV cache и зачем нужен сервер вроде vLLM. На интервью это вопросы про latency, throughput и «why does the server run out of memory with long chats», а в работе — настройка своего inference на GPU и выбор параметров для агента.

Два этапа: prefill и decode

Inference (прогон модели для ответа) идёт в два этапа.

Prefill (обработка входа): весь prompt проходит через модель за один параллельный проход. GPU здесь загружена вычислениями, а от длины входа зависит TTFT (time to first token, время до первого token).

Decode (генерация): дальше модель выдаёт по одному token за шаг. На каждом шаге нужно прочитать из памяти все веса ради одного нового token. Поэтому decode упирается не в скорость вычислений, а в memory bandwidth (пропускную способность памяти). Если в batch (пакете) идут сразу десятки запросов, одно чтение весов обслуживает их всех — поэтому загруженный сервер выдаёт в сумме намного больше tokens, чем один поток.

Отсюда грубая оценка потолка скорости для одного запроса: пропускная способность ÷ объём весов. Qwen3-8B в bf16 весит 16,4 ГБ. На GPU с памятью 1,79 ТБ/с потолок ≈ 109 tokens/с, на устройстве с 273 ГБ/с ≈ 17 tokens/с. В 4 битах (Q4_K, ~4,6 ГБ) потолки вырастают примерно в 3,5 раза. Реальная скорость ниже потолка, но порядок оценка даёт верный, и она объясняет главное: квантизация при хороших kernels (программах вычислений на GPU) ускоряет генерацию, потому что читать приходится меньше байт.

Context window: бюджет, а не склад

Context window (окно context) — предел длины одного вызова в tokens, причём ответ модели тоже входит в этот предел. У Qwen3-8B по карточке — 32 768 tokens «natively» и до 131 072 с методом растяжения YaRN.

Длинный context стоит четырьмя способами: деньгами (API считает каждый token), временем (prefill растёт с длиной входа), памятью (KV cache, ниже) и качеством — с ростом context точность вспоминания падает, это называют context rot. Поэтому большая часть работы с агентами — решать, что положить в context, а что оставить за его пределами.

Sampling: как выбирается следующий token

Модель выдаёт вероятности для всего словаря; решение, какой token взять, принимает decoding strategy (стратегия выбора).

Пример с числами: три кандидата с logits 2,0; 1,0 и 0,1.

temperature вероятности
0,5 0,86 · 0,12 · 0,02
1,0 0,66 · 0,24 · 0,10
2,0 0,50 · 0,30 · 0,19

Производитель модели обычно пишет рекомендации. Карточка Qwen3-8B: в режиме thinking (рассуждение перед ответом) temperature 0,6, top_p 0,95, top_k 20; без него — 0,7, 0,8 и 20; и прямо: «DO NOT use greedy decoding, as it can lead to performance degradation and endless repetitions». Для извлечения данных и вызова tools берут низкую temperature, для текстов и идей — выше. И помни: даже temperature 0 на сервере не гарантирует одинаковых ответов — порядок вычислений в batch немного меняет числа. Поэтому evals прогоняют несколько раз.

KV cache: память, которая растёт с каждым token

Из-за causal mask token смотрит только назад, и keys и values прошлых tokens не меняются. Их считают один раз и хранят — это KV cache (кэш ключей и значений внимания). На каждом шаге decode модель считает query только нового token. Без кэша каждый шаг пересчитывал бы весь текст заново.

Размер на один token: 2 (key и value) × число слоёв × число KV heads × размер head × байт на число. Для Qwen3-8B из config.json: 2 × 36 × 8 × 128 × 2 байта = 147 456 байт, около 144 КиБ на token.

Обрати внимание на «8 KV heads» при 32 query heads. Это GQA (grouped-query attention): несколько query heads делят одну пару key и value. С 32 KV heads кэш был бы вчетверо больше. Документация Hugging Face поэтому прямо советует GQA или MQA для чатов с длинным context.

Вывод: на сервере KV cache, а не веса, ограничивает, сколько разговоров идёт одновременно и какой длины. Рычаги: квантованный кэш (int8 или int4), выгрузка в оперативную память, короче максимальный context, общий префикс (одинаковый system prompt считают один раз — prefix caching).

Serving: от скрипта к серверу

model.generate() годится для опытов. Для пользователей ставят inference server. Стандарт для GPU — vLLM. Его основа — PagedAttention (статья 2023): KV cache хранится блоками фиксированного размера, как страницы виртуальной памяти в операционной системе. Это убирает фрагментацию, даёт «near-zero waste in KV cache memory» и, по статье, в 2–4 раза больше throughput (пропускной способности), чем у предыдущих систем, при той же задержке. Ещё vLLM делает continuous batching (новые запросы вливаются в batch на каждом шаге, а не ждут, пока закончится весь пакет), prefix caching, квантизацию (FP8, INT8, INT4, GPTQ/AWQ), tensor parallelism на нескольких GPU и отдаёт OpenAI-compatible API — клиентский код не меняется. Для GGUF на ноутбуке и Mac — llama.cpp и Ollama.

Метрики сервера: TTFT, tokens/с на пользователя, общий throughput, p95 latency. Главный компромисс: больше batch — выше общий throughput, но каждый пользователь получает tokens медленнее. Инженер выбирает максимальную длину context, число одновременных запросов, квант и параметры sampling под задачу — и проверяет это нагрузочным тестом.

Схема

Термины · Key terms

inference
прогон обученной модели, чтобы получить ответ
prefill
параллельная обработка всего входа и заполнение KV cache
decode
генерация по одному token за шаг; упирается в чтение весов из памяти
TTFT (time to first token)
время от запроса до первого token ответа
context window
предел длины одного вызова в tokens, вместе с ответом
temperature
делитель logits: ниже — увереннее, выше — разнообразнее
top_p (nucleus sampling)
выбор среди самых вероятных tokens с суммарной вероятностью p
KV cache
сохранённые keys и values прошлых tokens, чтобы не пересчитывать их
GQA (grouped-query attention)
несколько query heads делят одни keys и values; кэш меньше
PagedAttention
хранение KV cache блоками, как страницы памяти; почти без потерь
continuous batching
пополнение batch новыми запросами на каждом шаге генерации
throughput
сколько tokens или запросов сервер выдаёт в секунду всем пользователям

Сделай сам

Напиши 10 строк Python без пакетов: функцию kv_bytes(layers, kv_heads, head_dim, bytes_per_value, tokens) и функцию softmax(logits, temperature). Посчитай KV cache Qwen3-8B (36 слоёв, 8 KV heads, head_dim 128, bf16) для 8 000, 32 768 и 131 072 tokens и для 20 пользователей по 8 000 tokens. Затем выведи вероятности для logits [2.0, 1.0, 0.1] при temperature 0,2; 1,0 и 3,0. Запиши: сколько пользователей по 8 000 tokens поместится на GPU 32 ГБ, если веса в 4 битах занимают 4,6 ГБ, а 2 ГБ уходит на буферы.

Обсуди с тьютором: покажи расчёт и спроси, что даст кэш в fp8 вместо bf16 и чем за это платят.

Проверь себя · ответ под вопросом

1Почему первый token приходит с заметной задержкой, а дальше текст идёт ровно?
Сначала идёт prefill: весь вход обрабатывается за один проход и заполняет KV cache, и его время растёт с длиной входа. Дальше decode выдаёт по одному token за шаг, и каждый шаг стоит примерно одинаково: прочитать веса и кэш. Поэтому TTFT зависит от длины prompt, а скорость потока — от пропускной способности памяти.
2Сколько займёт KV cache Qwen3-8B на 32 768 tokens и откуда формула?
Около 4,8 ГБ: 2 (key и value) × 36 слоёв × 8 KV heads × 128 × 2 байта = 147 456 байт на token, умножить на 32 768. Для каждого token и каждого слоя хранятся вектор key и вектор value для каждой KV head. Благодаря GQA (8 KV heads вместо 32) это вчетверо меньше, чем было бы иначе.
3Почему temperature 0 не гарантирует одинаковые ответы на сервере?
При temperature 0 выбор детерминирован только при одинаковых числах на входе выбора. На сервере запросы считаются в batch разного состава, и порядок операций с плавающей точкой слегка меняет logits; при близких кандидатах это меняет выбранный token. Поэтому качество меряют несколькими прогонами.

Interview questions · US / UK

1Why is LLM decoding memory-bandwidth bound, and what does that imply for hardware choice and quantization?
Strong answer: each decode step reads all active weights to produce one token per sequence, so arithmetic intensity is low; ceiling per stream is roughly bandwidth divided by model bytes; batching amortizes the weight read across users; quantization speeds decode because fewer bytes are read; prefill by contrast is compute bound and drives TTFT.
2Scenario: our self-hosted model handles short chats fine but runs out of GPU memory when many users have long conversations. What is happening and what do you change?
Strong answer: the KV cache grows linearly with tokens and concurrent sequences, roughly 2 × layers × KV heads × head dim × bytes per token; compute it for our model; cap max context and concurrent sequences; use PagedAttention-based serving such as vLLM; quantize the KV cache, use prefix caching for the shared system prompt, prefer GQA models; load-test and watch p95 latency.
3What do temperature and top-p do, and how would you set them for a data-extraction agent versus a copywriting tool?
Strong answer: temperature rescales logits before softmax, lower is sharper; top-p samples from the smallest set reaching cumulative probability p; extraction and tool calls use low temperature and validate output with code; copywriting uses higher temperature for variety; follow the model card, some models warn against greedy decoding; temperature 0 is still not perfectly deterministic on batched servers, so evals use repeated runs.

Ключевые фразы · Say it aloud

Материалы