大模型基础扫盲------从文本到 Token、Embedding、QKV 与 Transformer(不定期优化修改)大模型处理文本的全流程解析(四)
大模型基础扫盲------从文本到 Token、Embedding、QKV 与 Transformer 大模型处理文本的全流程解析(一)-CSDN博客
大模型基础扫盲------从文本到 Token、Embedding、QKV 与 Transformer(不定期优化修改)大模型处理文本的全流程解析(二)-CSDN博客
大模型基础扫盲------从文本到 Token、Embedding、QKV 与 Transformer(不定期优化修改)大模型处理文本的全流程解析(三)-CSDN博客
17. 推理优化:KV Cache、Prefill、Decode
17.1 推理的两个阶段
自回归推理通常分为两个阶段。
阶段 | 做什么 | 特点 | 关键指标 |
|---|---|---|---|
Prefill | 用户 prompt 第一次进入模型,一次性处理所有输入 token | 计算密集,可并行,生成整段 KV Cache | TTFT,Time To First Token |
Decode | 逐 token 生成,每步只处理 1 个新 token | 串行依赖,显存带宽压力大,KV Cache 越来越长 | TPOT,Time Per Output Token,或 tokens/s |
MLOps 视角:
优化 Prefill 主要靠:
GPU 算力,FLOPS优化 Decode 主要靠:
显存带宽,memory bandwidth这也是为什么 Decode 阶段经常是推理服务瓶颈。
17.2 KV Cache 的原理
没有 KV Cache 时:
生成第 1000 个 token 时, 需要重新计算 token 1~999 的 K、V。有 KV Cache 时:
把前面 token 的 K、V 缓存起来。 生成第 1000 个 token 时, 只计算 token 1000 的 Q、K、V, 然后让新的 Q 去和缓存中的 K、V 做注意力。收益:
大幅减少重复计算,提高生成速度。代价:
占用额外显存。17.3 KV Cache 显存估算
通用公式:
KV Cache 显存 = 2 × num_layers × num_kv_heads × head_dim × seq_len × bytes_per_param × batch_size其中:
2 代表 K 和 V 两份。 num_kv_heads 是 KV 头数,不是 Q 头数。 bytes_per_param 通常是 2,对应 FP16/BF16。示例:Mistral-7B 风格配置
假设:
num_layers = 32 num_kv_heads = 8 head_dim = 128 seq_len = 4096 dtype = BF16,每参数 2 bytes单条序列:
2 × 32 × 8 × 128 × 4096 × 2 = 536,870,912 bytes ≈ 0.5 GB如果:
batch_size = 32则:
0.5 × 32 = 16 GB如果某个模型是 4 个 KV heads,那么同样条件下大约是:
0.25 GB / 单条序列注意:
不同模型的 KV 头数差异很大。
做容量规划时,务必以目标模型config.json中的:
num_key_value_heads为准,不要凭记忆估算。
17.4 vLLM 的 PagedAttention
传统方式:
为每个请求预分配最大长度的连续 KV Cache 显存。问题:
大量显存被浪费。PagedAttention:
把 KV Cache 切成固定大小的“页”,按需分配。类似操作系统的虚拟内存分页。
你做过容器,对“分页”概念不会陌生。
PagedAttention 就是把 OS 虚拟内存思想用到 GPU 显存管理上。
17.5 连续批处理,Continuous Batching
传统批处理:
等一批请求全到齐。 一起推理。 一起返回。问题:
短请求要等长请求,浪费吞吐。连续批处理:
某个请求生成完毕就立刻移出。 新请求立刻插入。好处:
GPU 利用率更高。 在线服务吞吐更好。17.6 KV Cache 的更精确估算方式
KV Cache 的通用估算公式是:
KV Cache 显存 = 2 × num_layers × num_kv_heads × head_dim × cached_tokens × bytes_per_element其中:
2 表示 K 和 V 两份。 num_layers 是 Transformer 层数。 num_kv_heads 是 KV 头数,不是 Q 头数。 head_dim 是每个注意力头的维度。 cached_tokens 是当前已经缓存的 token 数量。 bytes_per_element 是每个元素占用的字节数。常见精度:
FP16 / BF16:2 bytes FP8:1 byte INT8:1 byte,具体取决于实现如果是 batch 推理:
cached_tokens不应该简单理解成:
batch_size × max_seq_len更准确地说,应该是所有请求当前已缓存 token 数的总和:
cached_tokens = sum( prompt_len_i + generated_len_i )也就是:
KV Cache 显存 = 2 × num_layers × num_kv_heads × head_dim × sum(prompt_len_i + generated_len_i) × bytes_per_element举例:
假设:
num_layers = 32 num_kv_heads = 8 head_dim = 128 dtype = BF16,每个元素 2 bytes一条请求缓存 4096 个 token:
2 × 32 × 8 × 128 × 4096 × 2 = 536,870,912 bytes ≈ 0.5 GB如果有 32 条请求,每条都缓存 4096 个 token:
0.5 GB × 32 = 16 GB但如果这些请求共享相同 system prompt,并且推理框架支持 prefix caching,那么公共前缀的 KV Cache 可以共享。
例如 32 条请求都有 2048 个 token 的相同 system prompt。
这部分理论上可以只存一份,或者通过页表共享。
MLOps 视角:
KV Cache 是推理显存的大头之一。
尤其是在长上下文和高并发场景下,KV Cache 往往比模型权重更容易成为瓶颈。所以部署时要重点关注:
num_key_value_heads max_model_len batch size prompt 长度 输出长度 是否开启 prefix caching 是否使用 FP8 KV Cache 是否使用 PagedAttention
17.7 PagedAttention 和操作系统分页的关系
你做过容器和运维,对虚拟内存、分页、按需分配这些概念不会陌生。
传统 KV Cache 分配方式类似:
给每个请求预分配一段连续显存。 即使这个请求最后只用了一半长度, 剩下的显存也可能被浪费。这很像早期连续内存分配的问题:
内存碎片严重 利用率低 无法灵活扩容PagedAttention 的做法类似操作系统虚拟内存分页:
把 KV Cache 切成固定大小的 block。 每个 block 可以存固定数量 token 的 K、V。 请求需要多少 block,就分配多少 block。 不要求物理显存连续。 通过页表或 block table 管理逻辑块和物理块的映射。好处:
减少显存碎片。 提高 batch size。 提升 GPU 吞吐。 更容易实现 prefix caching。在 vLLM 中,你会经常看到类似概念:
block_size block table physical block logical block copy-on-write prefix caching这些本质上都是显存管理优化。
MLOps 视角:
如果你要部署高并发 LLM 服务,PagedAttention 几乎是必学内容。
因为它直接影响:吞吐 显存利用率 首 token 延迟 长文本支持能力
18. 显存估算:从原理到工程
18.1 推理显存估算公式
推理时的显存消耗通常包括四部分:
1. 模型权重 2. KV Cache 3. 激活值 4. 临时缓冲和碎片粗略公式:
推理显存 ≈ 模型权重 + KV Cache + 激活值 + 临时缓冲模型权重估算:
weights_memory ≈ param_count × bytes_per_param例如:
7B 模型,BF16: 7,000,000,000 × 2 bytes ≈ 14 GBKV Cache 估算:
kv_memory = 2 × num_layers × num_kv_heads × head_dim × cached_tokens × bytes_per_element激活值:
激活值和以下因素强相关:
batch_size seq_len hidden_size 推理框架的算子实现 是否使用 CUDA Graph 是否使用算子融合临时缓冲:
通常建议预留 10%~20% 的额外空间。所以在生产环境中,不能只算模型权重。
例如一张 24GB 显卡:
能装下 14GB 的 7B BF16 权重, 不代表能稳定跑 7B 长上下文服务。因为 KV Cache 和激活值还会继续吃显存。
18.2 训练显存估算
训练显存比推理复杂得多。
全参数训练时,通常要考虑:
1. 模型权重 2. 梯度 3. 优化器状态 4. 激活值 5. 通信缓冲区 6. 临时 buffer以常见的 BF16 训练 + AdamW 为例:
模型权重:BF16,约 2 bytes/param 梯度:BF16,约 2 bytes/param AdamW 优化器状态: FP32 master weights:4 bytes/param FP32 first moment:4 bytes/param FP32 second moment:4 bytes/param合计:
2 + 2 + 4 + 4 + 4 = 16 bytes/param所以:
全参数训练显存粗略估算 ≈ param_count × 16 bytes + 激活值 + 通信缓冲区 + 临时 buffer例如:
7B 模型: 7,000,000,000 × 16 bytes ≈ 112 GB这还没有算激活值。
所以朴素单卡全参数训练 7B,经常会超过 100GB 显存。
这就是为什么实际工程中经常使用:
ZeRO FSDP DeepSpeed activation checkpointing 8-bit Adam LoRA QLoRA 参数高效微调MLOps 视角:
如果你要做微调平台,不能只问用户“模型多大”。
还要问:是否全参数训练 是否使用 LoRA 序列长度多少 batch size 多少 是否开启 gradient checkpointing 优化器是什么 精度是什么 是否多机多卡 通信后端是 NCCL 还是其他这些都会直接影响显存和集群资源规划。
另外,
16 bytes/param是常见混合精度 AdamW 的粗略估算,具体框架和实现可能略有差异。
19. Padding Mask 与特殊 Token
19.1 Padding Mask
训练时多个句子组成 batch,长度可能不一样。
为了组成矩阵,需要补齐。
例如:
seq1: 我 吃 鱼 seq2: 你 看 [PAD]对应 attention mask 可能写成:
[ [1, 1, 1], [1, 1, 0] ]其中:
1 表示可以关注 0 表示屏蔽[PAD]位置不应被关注,所以需要 padding mask 屏蔽。
19.2 特殊 Token
常见特殊 token:
token | 作用 |
|---|---|
| 填充短序列 |
| 句子开始 |
| 句子结束,生成时遇到即停止 |
| 常用于分类任务,BERT 常见 |
| 分隔两个句子,BERT 常见 |
| 未知 token,现代字节级 tokenizer 中可能很少触发 |
chat 特殊 token | 用于系统、用户、助手消息模板 |
注意:
采用字节级分词的模型通常能表示任意 UTF-8 文本, 所以通常不会因为罕见字符直接失败。 但具体是否保留 <unk>,以及有哪些 special token, 要以模型 tokenizer 配置为准。19.3 chat template 必须正确
现在大量开源模型都是 chat 模型。
chat 模型通常有自己的对话模板。
例如:
system user assistant不同模型的模板可能完全不同。
正确做法是:
tokenizer.apply_chat_template()而不是自己手拼字符串。
例如伪代码:
messages = [ {"role": "system", "content": "你是一个运维专家。"}, {"role": "user", "content": "请解释 KV Cache。"} ] prompt = tokenizer.apply_chat_template( messages, tokenize=False, add_generation_prompt=True )MLOps 视角:
chat template 错误是非常常见的生产事故。可能导致:
模型角色混乱 输出格式错误 无法正常停止 重复输出 user prompt 工具调用失败 多轮对话上下文错乱部署前一定要检查:
tokenizer_config.json重点关注:
chat_template eos_token bos_token pad_token additional_special_tokens如果模型支持 tool calling,还要检查 tool 相关 token 和格式说明。
20. 常见误解
# | 误解 | 纠正 |
|---|---|---|
1 | token id 有语义 | id 只是编号,语义在 embedding 里 |
2 | token 一定是一个词 | 可以是字、子词、标点、字节、特殊符号 |
3 | embedding 是简单格式转换 | 是训练出来的语义表示 |
4 | 模型天然知道 token 顺序 | 注意力本身对顺序不敏感,需要位置编码 |
5 | 自回归模型可以看到整个句子 | GPT 类模型只能看到当前位置及之前 |
6 | Transformer 只有注意力 | 每层还包括 FFN、残差、归一化 |
7 | 模型真的懂主谓宾 | 模型从海量文本学到语言模式,不是显式语法分析 |
8 | Q、K、V 是唯一可能的结构 | 是 Transformer 的设计选择,也有 Mamba、线性注意力等替代方案 |
9 | 所有模型都用 LayerNorm | 许多现代大模型用 RMSNorm,但非绝对 |
10 | FFN 只有两个矩阵 | SwiGLU 风格 FFN 常见有三个矩阵 |
11 | FFN 只占模型参数的 1/3 | 单个 block 中 FFN 常占约 2/3,整模型占比视词表、embedding、MoE 而定 |
| 12 | Embedding 向量就是词义的最终表示 | 输入层 embedding 只是初始表示。 真正携带上下文语义的是 Transformer 各层输出的 hidden state。 |
| 13 | 一个多义词在 embedding table 中一定有多个向量 | 通常不是。 一个 token 通常只有一个初始 embedding。 多义区分主要靠后续 Attention 和上下文表示。 |
| 14 | 词表越大越好 | 词表大可以让中文、代码等文本更短,减少 token 数, 但也会增加 embedding 和 LM Head 参数量。 需要综合权衡。 |
| 15 | max_position_embeddings 就是模型一定稳定支持的长度 | 不一定。 还要看训练数据长度分布、RoPE scaling、推理框架配置和长文本实际评测结果。 |
| 16 | KV Cache 只和 batch_size 有关 | KV Cache 和层数、KV 头数、head_dim、缓存 token 总数、精度都有关。 长上下文场景下尤其明显。 |
| 17 | 推理显存主要就是模型权重 | 长上下文和高并发场景下, KV Cache、激活值、临时缓冲、显存碎片都可能成为主要瓶颈。 |
21. 全文总结
21.1 核心概念速查表
概念 | 通俗解释 |
|---|---|
文本 | 人类可读的字符串 |
分词器 | 把文本切成 token 的工具 |
token | 模型处理文本的基本单位 |
token id | token 在词表中的编号,本身无语义 |
embedding | 把 token id 变成语义向量 |
位置编码 | 告诉模型 token 的顺序 |
Q | 查询向量:我在找什么 |
K | 键向量:我能被什么找到 |
V | 值向量:找到我后我提供什么 |
| 计算 token 之间的相关性分数 |
softmax | 把分数变成权重 |
因果掩码 | 防止自回归模型偷看后文 |
多头注意力 | 从多个子空间做注意力 |
GQA | 共享 KV 头,减少 KV Cache |
| 把多头结果投影回模型主维度 |
LayerNorm / RMSNorm | 归一化,让训练更稳定 |
FFN | 对每个 token 做非线性加工 |
SwiGLU | 常见现代 FFN 结构,通常有三个矩阵 |
残差连接 | 帮助深层网络训练 |
多层 Transformer | 逐层提取更丰富的表示 |
Weight Tying | 输入 embedding 和输出 LM Head 可能共享权重 |
交叉熵损失 | 训练模型预测正确 token |
LM Head | 把最后一层向量映射到词表概率 |
Prefill | 推理时并行处理 prompt |
Decode | 推理时逐步生成 token |
KV Cache | 推理时缓存 K、V,提高生成速度 |
21.2 最简流程图
原始文本 ↓ Tokenizer ↓ tokens ↓ token ids ↓ Embedding ↓ 位置信息注入 ├── 绝对位置编码:加到 embedding └── RoPE:在 Attention 内旋转 Q/K ↓ Transformer Block × N ├── RMSNorm ├── Attention │ ├── Q/K/V 投影 │ ├── QK^T / sqrt(d_k) │ ├── causal mask │ ├── softmax │ └── 加权 V ├── 残差连接 ├── RMSNorm ├── FFN / SwiGLU └── 残差连接 ↓ 最后一个位置的 hidden state ↓ LM Head ↓ logits ↓ softmax ↓ 下一个 token 概率分布 ↓ 采样 / greedy / top-k / top-p ↓ 生成下一个 token21.3 一句话总结增强版
如果用一句话概括现代 LLM:
Tokenizer 把文本变成 token, Embedding 给 token 一个初始语义坐标, 位置编码告诉模型 token 的顺序, Transformer 通过 Attention 和 FFN 逐层构造上下文语义, LM Head 最终输出下一个 token 的概率分布。也就是说:
模型不是简单查表。 模型是在海量文本上训练出来的、 能够根据上下文预测下一个 token 的概率函数。22. 完整例子:从“我吃鱼”到模型输出
假设输入:
我吃鱼第一步:分词器切分
可能切成:
我 / 吃 / 鱼第二步:查 token id
token | token id |
|---|---|
我 | 2513 |
吃 | 1892 |
鱼 | 3765 |
得到:
[2513, 1892, 3765]注意:这些 id 只是示例。
第三步:查 embedding
token | embedding |
|---|---|
我 | 向量 A |
吃 | 向量 B |
鱼 | 向量 C |
得到:
X = [A, B, C]第四步:位置信息
如果是绝对位置编码:
我:A + position_1 吃:B + position_2 鱼:C + position_3如果是 RoPE:
此时不直接加位置向量。 后面 Attention 中会对 Q/K 做旋转。第五步:生成 Q、K、V
每个 token 向量都会经过三副“滤镜”:
Q = X W_Q K = X W_K V = X W_V得到:
token | Q | K | V |
|---|---|---|---|
我 |
|
|
|
吃 |
|
|
|
鱼 |
|
|
|
第六步:计算注意力分数
如果是自回归模型,需要加因果掩码。
处理“吃”时,它只能看到:
我、吃不能看到后面的:
鱼所以“吃”的 Query:
q^(2)只能和:
k^(1), k^(2)计算注意力,不能和:
k^(3)计算。
这一点很重要:
在自回归模型中, “吃”在这个位置还不知道后面是“鱼”。 它只知道前面是“我”。第七步:softmax 变权重
假设在自回归模型中,处理“吃”时得到:
目标 token | 权重 |
|---|---|
我 | 0.60 |
吃 | 0.40 |
鱼 | 0.00,因为被 mask |
第八步:加权 V
“吃”的新向量:
output^(2) = 0.60 · v^(1) + 0.40 · v^(2) + 0.00 · v^(3)这样,“吃”融合了前文信息:
谁在吃:我但此时它还不知道吃的是什么,因为在自回归模型中它不能看到后面的“鱼”。
第九步:多头输出经过 W_O
如果有多个头,每个头都会得到一个输出。
这些输出会被拼接,然后经过输出投影矩阵W_O映射回模型主维度。
例如:
[head_1, head_2, ..., head_32] → concat → W_O → 4096 维第十步:残差连接和归一化
注意力输出通常会和原始输入做残差连接,再进行归一化。
以 Pre-LN 风格为例:
X_1 = X + Attention(RMSNorm(X))以 Post-LN 风格为例:
X_1 = LayerNorm(X + Attention(X))具体顺序因模型而异。
第十一步:进入 FFN
然后进入 FFN:
X_2 = X_1 + FFN(RMSNorm(X_1))FFN 会对每个 token 的表示做进一步非线性加工。
可以理解为:
Attention 像查资料。 FFN 像自己消化总结。第十二步:经过多层 Transformer
注意力、FFN、残差、归一化组成一个 Transformer block。
实际模型会堆叠很多层,比如:
32 层 40 层 80 层经过多层之后,每个位置的向量都会包含更丰富的上下文信息。
第十三步:模型预测下一个 token
最后一层输出的向量经过 LM Head:
logits = h_last · W_LM_Head再经过 softmax:
probs = softmax(logits)如果输入是:
我 吃模型需要预测下一个 token。
它可能输出:
token | 概率 |
|---|---|
鱼 | 0.42 |
饭 | 0.25 |
面 | 0.12 |
苹果 | 0.03 |
... | ... |
如果生成策略选择概率最高的 token,就会输出:
鱼最终得到:
我吃鱼23. 给 MLOps 学习者的下一步
23.1 主流推理框架选型指南
学习原理之后,最终要落到部署和优化。
常见主流推理框架包括:
vLLM TGI,Text Generation Inference SGLang TensorRT-LLM它们的侧重点不同。
vLLM
核心优势:
PagedAttention 高吞吐 显存利用率高 适合通用在线推理服务适合场景:
高并发 chat 服务 通用文本生成 对吞吐要求高的场景TGI
核心优势:
生产功能完整 监控和部署生态成熟 支持多种量化和 LoRA 适合企业级部署适合场景:
企业生产环境 需要较完整服务能力 需要 HuggingFace 生态集成SGLang
核心优势:
结构化输出强 适合 Agent、tool calling、JSON schema RadixAttention 对共享前缀和多轮对话友好适合场景:
Agent 系统 工具调用 结构化生成 复杂 prompt 复用TensorRT-LLM
核心优势:
NVIDIA GPU 上深度优化 kernel 级别优化多 适合追求极致性能适合场景:
对延迟和吞吐要求非常高 团队具备较强 CUDA / 推理引擎能力MLOps 视角:
不要只问“哪个框架最快”。
要结合业务场景看:是否需要长上下文 是否需要高并发 是否需要结构化输出 是否需要 tool calling 是否需要多 LoRA 热切换 是否需要 prefix caching GPU 型号是什么 显存大小是多少 延迟指标是 TTFT 还是 TPOT 运维团队是否能维护复杂引擎选型不是技术炫技,而是业务约束下的工程折中。
另外,推理框架生态更新很快,最终以当前版本功能和实测 benchmark 为准。
23.2 实战实验室:从原理到工程
建议你按顺序做下面几个实验。
实验 1:读取模型配置,理解模型结构
目标:
学会看 config.json。任务:
找一个开源模型,例如 Qwen、LLaMA、Mistral 系列。
找出以下字段:
hidden_size num_hidden_layers num_attention_heads num_key_value_heads intermediate_size vocab_size max_position_embeddings rope_theta rope_scaling tie_word_embeddings torch_dtype然后回答:
这个模型有多少层? 主维度是多少? 每个 head 维度是多少? KV Cache 用几个 head? FFN 中间维度是多少? 词表大小是多少? 是否绑定 embedding 和 LM Head?实验 2:手工估算 KV Cache
目标:
掌握推理显存大头。任务:
假设:
num_layers = 32 num_kv_heads = 8 head_dim = 128 seq_len = 8192 dtype = BF16计算单条请求的 KV Cache 显存。
公式:
KV Cache = 2 × num_layers × num_kv_heads × head_dim × seq_len × bytes然后把seq_len改成:
4096 8192 32768 131072观察显存变化。
参考结果:
4096 ≈ 0.5 GB 8192 ≈ 1 GB 32768 ≈ 4 GB 131072 ≈ 16 GB实验 3:用 vLLM 部署一个模型
目标:
理解推理服务启动参数。重点观察:
max_model_len tensor_parallel_size gpu_memory_utilization max_num_seqs enable_prefix_caching dtype quantization记录:
启动显存占用 首 token 延迟 TTFT 生成速度 tokens/s 并发增加时吞吐变化实验 4:对比 Prefill 和 Decode
目标:
理解推理两阶段瓶颈。方法:
构造不同 prompt 长度:
短 prompt,长输出 长 prompt,短输出 长 prompt,长输出观察:
TTFT 是否明显变长? TPOT 是否稳定? GPU 利用率如何? 显存占用如何变化?实验 5:LoRA 微调显存对比
目标:
理解参数高效微调。任务:
用同一模型分别做:
全参数训练 LoRA QLoRA对比:
显存占用 训练速度 可训练参数量 checkpoint 大小 部署复杂度这些实验做完后,你会从“知道原理”进入“能做工程落地”。
24. MLOps 部署前检查清单
24.1 模型配置检查
hidden_size num_hidden_layers num_attention_heads num_key_value_heads head_dim intermediate_size vocab_size max_position_embeddings rope_scaling tie_word_embeddings24.2 显存检查
模型权重显存 KV Cache 显存 激活值显存 临时缓冲 并发请求下的峰值显存 长上下文下的峰值显存24.3 推理服务检查
TTFT TPOT tokens/s max_batch_size max_model_len gpu_memory_utilization 是否开启 prefix caching 是否开启 continuous batching 是否使用 PagedAttention 是否使用量化24.4 Tokenizer 检查
vocab_size special tokens chat_template stop tokens 是否支持 tool calling 是否支持多语言 是否支持代码24.5 长文本检查
训练长度是多少 推理长度是多少 是否配置 rope_scaling 是否做过长文本评测 是否存在注意力退化24.6 训练 / 微调检查
是否全参数训练 是否 LoRA 是否 QLoRA 优化器状态显存 激活值显存 梯度累积 序列长度 batch size 是否 gradient checkpointing只要每次部署模型前过一遍这个清单,很多显存 OOM、效果异常、无法停止、长文本退化问题,都可以提前发现。
最后一句话
大模型就是一个“预测下一个 token”的函数:
把文本变成向量, 经过几十层注意力和前馈网络加工, 输出词表上的概率分布, 再选出或采样出最可能的下一个 token。所有参数,包括:
embedding Q/K/V W_O FFN 归一化参数 LM Head都是通过海量文本上的“预测下一个 token”任务训练出来的。