进入 LLM 推理系统
Day 29 是阶段 3 的入口:从“把模型训出来”切换到“让模型稳定、低延迟、高吞吐地服务用户”。 今天只抓两个最关键的基础概念:prefill vs decode 和 KV Cache。 只要理解这两件事,后面的 PagedAttention、continuous batching、SGLang、量化部署和推理调度都会变得清楚很多。
思维导图
一次 LLM 请求怎么跑
训练时我们关心一个 batch 的 forward/backward/optimizer step。 推理时我们关心的是一条用户请求从进来到返回 token 的全过程: 它什么时候排队、什么时候拿到 GPU、什么时候返回第一个 token、后续 token 以多快速度流出。
Tokenize & enqueue
HTTP 请求进入服务,文本被 tokenizer 切成 token id,调度器根据长度、优先级和显存预算决定何时进 batch。
Prefill
模型一次性处理 prompt 的全部 token,计算首个 next-token logits,同时为每层 attention 写入历史 K/V。
Decode loop
每一轮只输入上一个新 token,读取已有 KV Cache,采样出下一个 token,再把新的 K/V 追加进 cache。
Stream & finish
服务把 token 增量返回给客户端,直到遇到 stop token、长度上限、用户取消或调度器超时。
Prefill vs Decode
LLM 推理不是一个均匀过程。prefill 阶段像“读完整段题目”, decode 阶段像“一个字一个字作答”。两者的计算形态、瓶颈和优化方向完全不同。
| 维度 | Prefill | Decode | 工程含义 |
|---|---|---|---|
| 输入形态 | 一次处理 prompt 的全部 token,序列长度可能很长。 | 每轮只处理一个新 token,循环多次直到结束。 | prefill batch 看 prompt 长度,decode batch 看活跃请求数。 |
| Attention | 新 token 之间也要互相计算 attention,近似有 S² 项。 | 新 token query 只对历史 K/V 做 attention,每步随上下文长度线性增长。 | prefill 适合大 GEMM;decode 常被 KV 读取和显存带宽限制。 |
| 性能指标 | TTFT: time to first token。 | TPOT: time per output token。 | 用户体感通常先看 TTFT,长输出体验还要看 TPOT。 |
| 优化方向 | prompt batching、prefix cache、chunked prefill、FlashAttention。 | continuous batching、PagedAttention、KV quantization、speculative decoding。 | 不要用同一套指标评估两个阶段。 |
KV Cache 为什么是显存大头
自回归生成时,每生成一个 token 都要看历史上下文。 如果每轮都重新计算所有历史 token 的 K/V,decode 会非常慢。 KV Cache 的做法是把每一层 attention 的 key/value 保存下来,下一轮直接复用。 它换来速度,代价是显存会随请求数、上下文长度、层数线性增长。
2 表示 K 和 V 各一份。普通 MHA 中 kv_heads = num_heads;GQA/MQA 会减少 KV head 数。
服务端能并发多少请求,常常不是由权重显存决定,而是由剩余空间能装多少活跃 KV Cache 决定。
上下文越长越贵
prompt token 和已生成 token 都要占 KV。长文档问答、多轮对话、代码生成都会迅速推高 cache 占用。
每条请求都有状态
训练 batch 的 activation 可以在 backward 后释放;推理请求只要还在生成,对应的 KV Cache 就要留在显存里。
GQA/MQA 能省 cache
Grouped-query attention 减少 K/V head 数,不改变 query head 数,因此常用于降低长上下文 decode 的 KV 压力。
一个粗略估算
def kv_cache_gib(batch, seq_len, layers, kv_heads, head_dim, bytes_per_elem=2): bytes_total = batch * seq_len * layers * 2 * kv_heads * head_dim * bytes_per_elem return bytes_total / (1024 ** 3) # 例: 32 层,32 个 KV heads,head_dim=128,bf16/fp16 print(kv_cache_gib(batch=16, seq_len=4096, layers=32, kv_heads=32, head_dim=128)) # 约 32 GiB,还没有计算模型权重和运行时 buffer
动手:最小 generation loop
今天的代码目标不是搭服务,而是看清楚 cache 在 generation loop 中如何流动:
第一次 forward 处理完整 prompt,后续 forward 只喂最后一个 token,并把 past_key_values 传回模型。
import time import torch from transformers import AutoModelForCausalLM, AutoTokenizer model_name = "Qwen/Qwen2.5-0.5B-Instruct" device = "cuda" tokenizer = AutoTokenizer.from_pretrained(model_name) model = AutoModelForCausalLM.from_pretrained( model_name, torch_dtype=torch.bfloat16, device_map=device, ).eval() prompt = "用三句话解释 LLM 推理中的 KV Cache。" input_ids = tokenizer(prompt, return_tensors="pt").input_ids.to(device) generated = input_ids past = None max_new_tokens = 64 first_token_time = None start = time.perf_counter() with torch.inference_mode(): for step in range(max_new_tokens): # 第 0 步:喂完整 prompt。之后:只喂上一个新 token。 cur_input = generated if past is None else generated[:, -1:] out = model( input_ids=cur_input, past_key_values=past, use_cache=True, ) past = out.past_key_values next_token = out.logits[:, -1, :].argmax(dim=-1, keepdim=True) generated = torch.cat([generated, next_token], dim=-1) if step == 0: first_token_time = time.perf_counter() - start if next_token.item() == tokenizer.eos_token_id: break elapsed = time.perf_counter() - start new_tokens = generated.shape[-1] - input_ids.shape[-1] print({ "prompt_tokens": input_ids.shape[-1], "new_tokens": new_tokens, "ttft_s": first_token_time, "tpot_s": (elapsed - first_token_time) / max(new_tokens - 1, 1), "tokens_per_s": new_tokens / elapsed, }) print(tokenizer.decode(generated[0], skip_special_tokens=True))
对比 use_cache
分别设置 use_cache=True/False,观察长输出时 decode 速度差异。
改变 prompt 长度
把 prompt 扩到 1K/4K token,观察 TTFT 如何上升。
改变输出长度
固定 prompt,改变 max_new_tokens,观察 TPOT 和 tokens/s。
打印 cache 形状
查看 past_key_values 每层 K/V tensor shape,把它代入显存公式。
推理指标怎么拆
训练里常看 step time、MFU、loss。推理服务里更常看用户体验和系统吞吐: 首 token 等多久、后续 token 多快、单卡每秒能吐多少 token、请求排队多久、显存还剩多少 cache 空间。
| 指标 | 含义 | 主要受什么影响 | 优化方向 |
|---|---|---|---|
| TTFT | Time To First Token,从请求进入到第一个 token 返回。 | 排队时间、prompt 长度、prefill batch、模型大小。 | prefix cache、chunked prefill、合理调度长 prompt。 |
| TPOT | Time Per Output Token,后续每个 token 的平均间隔。 | decode batch、KV Cache 读取、采样策略、显存带宽。 | continuous batching、PagedAttention、KV quant、speculative decoding。 |
| Throughput | 单位时间输出 token 数或完成请求数。 | batching 策略、并发数、请求长度分布、GPU 利用率。 | 提高 batch 利用率,但要守住延迟 SLO。 |
| Cache hit | prefix/KV 复用命中率。 | 系统 prompt、RAG 模板、会话复用、请求排序。 | prefix cache、RadixAttention、共享 prompt 调度。 |
吞吐和延迟通常互相拉扯
更大的 batch 往往能提高 tokens/s,但会让部分请求排队更久。 面向在线产品时不能只追求吞吐峰值,还要看 p50/p95/p99 latency 和用户取消率。
常见疑问
Q1 为什么不能每次 decode 都重新计算整个 prompt? +
可以,但会非常浪费。第 100 个输出 token 时,如果重新计算全部历史 token,前面 99 步的大部分 K/V 都被重复算了。KV Cache 保存历史 K/V,让每一步只处理新 token,用显存换时间。
Q2 prefill 和 decode 哪个更贵? +
看请求形态。长 prompt、短输出时,prefill 很显眼;短 prompt、长输出时,decode 循环更显眼。在线服务通常两者都要优化,因为用户先感受到 TTFT,长回答又会持续感受到 TPOT。
Q3 KV Cache 和模型权重哪个更占显存? +
模型权重是固定底座,KV Cache 是随并发和上下文动态增长的部分。小并发短上下文时权重占主导;高并发长上下文时,KV Cache 可能成为真正限制吞吐的显存大头。
Q4 为什么 GQA/MQA 能降低推理显存? +
KV Cache 保存的是 key/value head,不是 query head。GQA/MQA 让多个 query heads 共享较少的 K/V heads,所以 cache 公式中的 kv_heads 变小,显存和读取带宽都下降。
Q5 为什么 Day29 不直接上 vLLM? +
vLLM 是后面几天的重点。今天先把 prefill、decode、KV Cache 和基础指标学清楚,再看 PagedAttention 和 continuous batching 时就能知道它们到底在优化哪一段。
复盘问题
- 用自己的话画出一次 LLM 请求从 HTTP 进入到 streaming 返回的生命周期。
- prefill 和 decode 在输入形态、attention 计算、性能瓶颈上有什么不同?
- 写出 KV Cache 显存估算公式,解释每个变量的含义。
- 为什么 KV Cache 会随 batch、seq_len 和 layers 线性增长?
- TTFT、TPOT、throughput 分别衡量什么?它们之间有哪些常见权衡?
- 如果一个服务首 token 很慢但后续 token 很快,你会优先检查哪些环节?
今日检查清单
- 能解释 LLM 推理请求的 tokenize、enqueue、prefill、decode、streaming 流程。
- 能说清 prefill 为什么通常更 compute-heavy,decode 为什么更 memory/cache-heavy。
- 能解释 KV Cache 保存的是每层 attention 的 K/V,而不是完整 hidden states。
- 能写出 KV Cache 显存估算公式,并用一个模型配置做粗略计算。
- 能区分 MHA、GQA、MQA 对 KV Cache 大小的影响。
- 能写出一个使用
past_key_values的最小 generation loop。 - 能记录 TTFT、TPOT、tokens/s 三个基础推理指标。
- 知道高吞吐 batch 可能增加排队延迟,不能只看 tokens/s。
- 能解释为什么 PagedAttention 要管理 KV Cache 的块分配与碎片。
- 为 Day30 的 greedy、top-k、top-p、temperature 解码算法预留实验脚本。
推荐阅读
KV cache strategies
Hugging Face Transformers 关于 KV Cache 的文档。重点看 cache 的基本概念、动态/静态 cache 和生成时的使用方式。
LLM inference tutorial
Transformers 的 LLM 推理教程。结合今天的 generation loop,对照理解 batching、prompt 长度和生成配置。
解码算法
Day30 会进入 token 选择策略:greedy、beam、top-k、top-p、temperature,以及 speculative decoding 的直觉。
Day 30 预告
解码算法:从 greedy 到 speculative decoding
今天我们把推理过程拆成 prefill/decode 和 KV Cache。 明天会看“每一步 logits 出来后怎么选下一个 token”: greedy、beam search、top-k、top-p、temperature 会改变输出质量和延迟, speculative decoding 则试图用小模型提前猜 token 来加速大模型 decode。