大模型推理全链路技术详解
从用户按下 Enter 到屏幕上出现第一个 Token 的完整旅程
🧠 Transformer 架构 ⚡ 自回归生成 🔢 Token 级推理 🚀 技术流深度解析

🗺️ 全景概览

类比:互联网时代的 "输入URL后发生了什么"

互联网时代,输入URL后经历了:DNS解析 → TCP握手 → TLS握手 → HTTP请求 → 服务器处理 → 返回HTML → 浏览器渲染。

大模型时代,输入Prompt后经历了:Tokenization → Embedding → 位置编码 → 多层Transformer推理 → 概率采样 → Token解码 → 流式输出。每一步都涉及复杂的数学运算和工程优化。

┌─────────────────────────────────────────────────────────────────┐ │ 大模型推理全链路 Pipeline │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ 用户输入 ──► Tokenize ──► Embed ──► PosEncode ──► Transformer │ │ │ │ │ │ │ │ │ │ ▼ ▼ ▼ ▼ │ │ │ [分词器] [嵌入层] [位置编码] [N层注意力] │ │ │ │ │ │ │ │ │ │ └───────────┴────────────┴──────────────┘ │ │ │ │ │ │ │ ▼ │ │ │ [自回归生成循环] │ │ │ ┌─────────────────┐ │ │ │ │ for i in range: │ │ │ │ │ 1. 前向传播 │ │ │ │ │ 2. 计算logits │ │ │ │ │ 3. 采样next_token│ │ │ │ │ 4. 拼接上下文 │ │ │ │ │ 5. 输出Token │ │ │ │ └─────────────────┘ │ │ │ │ │ │ ▼ ▼ │ │ [流式显示] ◄───────────────── [Detokenize] │ │ │ └─────────────────────────────────────────────────────────────────┘

📦 Phase 1:输入处理层

Step 1:文本接收与预处理

1

用户输入捕获

前端通过 WebSocket/HTTP 接收用户输入,进行基础清洗(去除控制字符、统一换行符、截断超长输入)。

# 前端接收示例 user_input = "请解释一下量子计算的原理" max_length = 4096 # 上下文长度限制 # 基础清洗 cleaned = user_input.strip().replace("\r\n", "\n")[:max_length]

Step 2:Tokenization(分词)

2

将文本转换为 Token ID 序列

使用 BPE(Byte-Pair Encoding)或 SentencePiece 算法,将字符串拆分为模型词汇表中的整数索引。

🔤 BPE 分词核心逻辑

BPE 通过合并高频字符对来构建词汇表。训练时从字符级开始,迭代合并最频繁的相邻 token 对。

# GPT-4 风格 BPE 分词示例 vocab = {"请": 10001, "解释": 5234, "一下": 891, "量子": 3402, "计算": 2105, "的": 101, "原理": 4567} # "请解释一下量子计算的原理" → Token IDs token_ids = [10001, 5234, 891, 3402, 2105, 101, 4567] # 实际中可能更细粒度,如 "量子" 拆为 "量" + "子"
分词算法 代表模型 特点
BPE GPT-2/3/4, LLaMA 字节级,处理未登录词好,词汇表~50K-100K
SentencePiece T5, PaLM, DeepSeek 语言无关,直接训练raw text,支持BPE/Unigram
BBPE RoBERTa Byte-level BPE,完全避免未登录词

💡 为什么分词很重要?

  • 词汇表大小决定嵌入矩阵维度,直接影响模型参数量
  • 分词粒度影响序列长度:中文一个字可能=1个token或2-3个token
  • 特殊Token:<s>开始、</s>结束、<pad>填充、<unk>未知

Step 3:Token Embedding(词嵌入)

3

查表获取稠密向量表示

每个 Token ID 通过查嵌入矩阵 E ∈ ℝ^(V×d) 映射为 d 维向量,V 是词汇表大小,d 是模型维度。

xembed = E[token_id] ∈ ℝd
例如:GPT-3 d=12288, LLaMA-2 d=4096, DeepSeek-V3 d=7168
# Embedding 层本质是一个查找表 embedding_matrix = torch.randn(vocab_size, hidden_dim) # V × d # 输入: [batch_size, seq_len] = [1, 7] input_ids = torch.tensor([[10001, 5234, 891, 3402, 2105, 101, 4567]]) # 输出: [batch_size, seq_len, hidden_dim] = [1, 7, 4096] embeddings = embedding_matrix[input_ids] # 通过索引查表

Step 4:Positional Encoding(位置编码)

4

注入序列位置信息

Transformer 本身是无序的,必须通过位置编码让模型感知 token 的先后顺序。

📐 绝对位置编码(原始Transformer)

PE(pos, 2i) = sin(pos / 10000^(2i/d)) PE(pos, 2i+1) = cos(pos / 10000^(2i/d))

使用不同频率的正弦/余弦函数,每个位置有唯一编码,可外推到训练时未见过的长度。

📏 RoPE(旋转位置编码)

RoPE: 将q,k向量旋转角度m·θ f(q,m) = q·e^(imθ)

LLaMA、DeepSeek等主流模型采用。通过旋转矩阵注入位置,相对位置关系更自然,长上下文外推性好。

📊 ALiBi

直接在注意力分数上添加基于距离的偏置项,无需显式位置编码。训练稳定,外推性强。

xinput = xembed + xpos (绝对位置编码)
或:在 Attention 的 Q·KT 中通过 RoPE 注入位置信息
# RoPE 简化示意 def apply_rope(x, positions, theta=10000.0): # x: [batch, seq, heads, head_dim] # 计算旋转角度 freqs = 1.0 / (theta ** (torch.arange(0, head_dim, 2) / head_dim)) angles = torch.outer(positions, freqs) # [seq, head_dim/2] # 应用旋转 cos, sin = torch.cos(angles), torch.sin(angles) x_rot = x.clone() x_rot[..., 0::2] = x[..., 0::2] * cos - x[..., 1::2] * sin x_rot[..., 1::2] = x[..., 0::2] * sin + x[..., 1::2] * cos return x_rot

⚡ Phase 2:Transformer 核心推理层

Step 5:多层 Transformer Block 前向传播

5

核心计算:自注意力 + 前馈网络

输入经过 N 层(如 LLaMA-3 有 80 层,GPT-4 有 120 层)Transformer Block,每层包含 Multi-Head Attention、Feed-Forward Network、LayerNorm 和残差连接。

🔵 单层 Transformer Block 计算流程

# 输入: x ∈ [batch, seq, d] # 1. LayerNorm x_norm = LayerNorm(x) # 2. Multi-Head Self-Attention q = W_q @ x_norm # [batch, seq, d] → [batch, seq, d] k = W_k @ x_norm # 同上 v = W_v @ x_norm # 同上 # 3. 计算注意力分数 scores = (q @ k.T) / sqrt(d_k) # [batch, seq, seq] scores = scores + causal_mask # 因果掩码(只看前面) attn_weights = softmax(scores, dim=-1) attn_output = attn_weights @ v # [batch, seq, d] # 4. 投影 + 残差 attn_out = W_o @ attn_output x = x + attn_out # 残差连接 # 5. Feed-Forward Network (SwiGLU) x_norm = LayerNorm(x) ffn_out = W_down(silu(W_gate @ x_norm) * (W_up @ x_norm)) x = x + ffn_out # 残差连接

Step 5.1:Multi-Head Attention 详解

Attention(Q, K, V) = softmax(QKT / √dk) · V

MultiHead(Q, K, V) = Concat(head1, ..., headh)WO
where headi = Attention(QWiQ, KWiK, VWiV)
组件 计算 复杂度
Q/K/V 投影 3 × (seq × d × d) O(seq · d²)
QKT 矩阵乘 seq × dk × seq O(seq² · d)
Softmax + 加权 seq × seq × dv O(seq² · d)
输出投影 seq × d × d O(seq · d²)

⚠️ 因果掩码(Causal Mask)的关键作用

自回归生成时,模型只能"看到"当前位置及之前的 token,不能偷看未来。通过将注意力矩阵的上三角设为 -∞(softmax后变为0)实现。

# 因果掩码示例 (seq=4) mask = [[0, -inf, -inf, -inf], # 位置0只能看自己 [0, 0, -inf, -inf], # 位置1能看0,1 [0, 0, 0, -inf], # 位置2能看0,1,2 [0, 0, 0, 0]] # 位置3能看0,1,2,3

Step 5.2:KV Cache 机制

5.2

避免重复计算的关键优化

自回归生成时,已生成的 token 的 K、V 向量不会变化,缓存起来避免每步重新计算,将复杂度从 O(seq²) 降到 O(seq)。

# KV Cache 机制 # 第一次(Prefill/提示处理阶段) # 输入全部 prompt tokens,计算并缓存所有层的 K, V kv_cache = {} # layer_idx → (K, V) for layer in layers: k, v = layer.compute_kv(x) # [batch, seq, heads, head_dim] kv_cache[layer_idx] = (k, v) # 后续每一步(Decode/生成阶段) # 只输入最新生成的 1 个 token new_token_emb = embedding_matrix[new_token_id] # [1, 1, d] for layer in layers: k_new, v_new = layer.compute_kv(new_token_emb) # [1, 1, heads, head_dim] k_cached, v_cached = kv_cache[layer_idx] # 拼接缓存 + 新的 K, V k_all = torch.cat([k_cached, k_new], dim=1) # [1, seq+1, heads, head_dim] v_all = torch.cat([v_cached, v_new], dim=1) kv_cache[layer_idx] = (k_all, v_all) # 注意力计算:新 token 的 Q 与所有 K 做 attention q = layer.compute_q(new_token_emb) scores = (q @ k_all.transpose(-2, -1)) / sqrt(d_k) attn = softmax(scores, dim=-1) @ v_all

KV Cache 的内存开销

对于 batch_size=b, seq_len=s, n_layers=L, n_heads=h, head_dim=dh

Cache 大小 = 2 × b × s × L × h × dh × sizeof(float16)

例如 LLaMA-70B:b=1, s=4096, L=80, h=64, dh=128 → ~5.2 GB

这是长上下文推理的主要内存瓶颈。

Step 5.3:MoE(混合专家)架构(如 DeepSeek、Mixtral)

5.3

稀疏激活降低推理成本

总参数量很大(如 DeepSeek-V3 总参 671B),但每次只激活部分专家(如 37B),通过 Router 网络动态选择 Top-K 个专家处理每个 token。

# MoE 层简化示意 def moe_layer(x, n_experts=256, top_k=8): # Router: 决定每个 token 用哪些专家 router_logits = W_router @ x # [batch, seq, n_experts] weights, expert_indices = torch.topk( softmax(router_logits, dim=-1), k=top_k ) # weights: [batch, seq, top_k], indices: [batch, seq, top_k] # 只激活选中的专家 output = torch.zeros_like(x) for i in range(top_k): expert_idx = expert_indices[..., i] expert_weight = weights[..., i:i+1] for e in range(n_experts): mask = (expert_idx == e) if mask.any(): expert_output = experts[e](x[mask]) # 专家前馈网络 output[mask] += expert_weight[mask] * expert_output return output

Step 5.4:MLA(多头潜在注意力)与 KV Cache 压缩

5.4

DeepSeek 的 KV Cache 压缩方案

通过低秩压缩将 K、V 投影到更小的 latent 空间,大幅降低 Cache 内存占用。

ctKV = WDKV · ht (压缩后的联合 KV,维度 dc << d)
ktC = WUK · ctKV (解压缩得到 K)
vtC = WUV · ctKV (解压缩得到 V)

Cache 从 2 × nh × dh 降到 dc + nh × dh
DeepSeek-V2: dc=512, 相比传统 MHA 压缩约 93%

🎲 Phase 3:输出生成层

Step 6:LM Head → Logits

6

将隐藏状态映射到词汇表概率分布

最后一层 Transformer 的输出 h ∈ ℝd 通过线性层 Wlm ∈ ℝV×d 映射为词汇表大小的 logits 向量。

logits = Wlm · hfinal ∈ ℝV
probs = softmax(logits / temperature) ∈ ℝV
# LM Head 计算 lm_head_weight = embedding_matrix.T # 常与原嵌入矩阵共享/绑定: V × d # h_final: [batch, 1, d] (只取最后一个位置的输出) logits = h_final @ lm_head_weight.T # [batch, 1, V] # 应用 temperature 缩放 temperature = 0.7 # 越低越确定,越高越随机 logits_scaled = logits / temperature # Softmax 得到概率分布 probs = F.softmax(logits_scaled, dim=-1) # [batch, 1, V], 总和为1

Step 7:Sampling Strategy(采样策略)

7

从概率分布中选择下一个 Token

不同的采样策略在"确定性"与"创造性"之间做权衡。

采样方法 算法 特点
Greedy argmax(probs) 总是选概率最高的,确定性高但容易重复
Temperature softmax(logits / T) T→0 趋近贪心,T→∞ 趋近均匀分布
Top-k 只从概率最高的k个中采样 过滤低概率噪声,k=50是常见值
Top-p (Nucleus) 从累积概率≥p的最小集合中采样 动态调整候选集大小,p=0.9常用
Repetition Penalty 降低已生成token的概率 减少重复,penalty=1.2常用
# Top-p (Nucleus) 采样实现 def top_p_sampling(logits, p=0.9, temperature=1.0): # 1. 温度缩放 logits = logits / temperature # 2. 排序并计算累积概率 sorted_logits, sorted_indices = torch.sort(logits, descending=True) sorted_probs = F.softmax(sorted_logits, dim=-1) cumsum_probs = torch.cumsum(sorted_probs, dim=-1) # 3. 找到累积概率超过p的截断点 mask = cumsum_probs > p mask[..., 0] = False # 至少保留第一个 sorted_logits[mask] = -float('inf') # 4. 在截断后的分布上采样 probs = F.softmax(sorted_logits, dim=-1) sample_idx = torch.multinomial(probs, num_samples=1) # 5. 映射回原始索引 return sorted_indices.gather(-1, sample_idx)

Step 8:自回归生成循环

8

重复"预测→采样→拼接"直到结束

每次生成一个新 token,将其加入上下文,继续预测下一个,直到生成 </s> 或达到最大长度。

# 完整的自回归生成循环 def generate(model, input_ids, max_new_tokens=512, temperature=0.7, top_p=0.9, eos_token_id=2): generated = input_ids.clone() # [batch, prompt_len] kv_cache = None # === Prefill Phase: 处理整个 prompt === logits, kv_cache = model.forward(generated, use_cache=True) # logits: [batch, prompt_len, vocab_size] # 只关心最后一个位置的预测 next_logits = logits[:, -1, :] # [batch, vocab_size] # === Decode Phase: 逐个生成新 token === for i in range(max_new_tokens): # 采样下一个 token next_token_id = top_p_sampling(next_logits, p=top_p, temperature=temperature) # 拼接到序列 generated = torch.cat([generated, next_token_id], dim=-1) # 检查是否生成结束符 if next_token_id.item() == eos_token_id: break # 只传入最新 token 进行下一步预测(利用 KV Cache) logits, kv_cache = model.forward( next_token_id, past_key_values=kv_cache, use_cache=True ) next_logits = logits[:, -1, :] return generated

💡 Prefill vs Decode 的差异

阶段 输入 计算特点 瓶颈
Prefill 整个 Prompt 并行计算,QKT 矩阵大但只做一次 计算密集型,FLOPs 高
Decode 每次 1 个 token 串行计算,无法并行 内存带宽(读 KV Cache)

Decode 阶段是推理延迟的主要来源,因为每步只能生成一个 token,且受内存带宽限制("内存墙"问题)。

📤 Phase 4:输出与渲染层

Step 9:Detokenization(反分词)

9

将 Token ID 序列还原为可读文本

通过词汇表的反向映射,将整数序列转换回字符串。需要处理字节级编码(如 UTF-8)的边界问题。

# Detokenization token_ids = [10001, 5234, 891, 3402, 2105, 101, 4567, ...new_tokens...] # 查表还原 tokens = [vocab_inv[id] for id in token_ids] # 拼接(BPE 需要处理子词拼接) text = "".join(tokens).replace("▁", " ") # SentencePiece 用 ▁ 表示词首 # "▁请▁解释▁一下▁量子▁计算▁的▁原理▁..." → "请解释一下量子计算的原理..."

Step 10:流式输出(Streaming)

10

Server-Sent Events (SSE) 实时推送

不需要等全部生成完毕再返回,每生成一个 token 就通过 SSE 推送给前端,实现"打字机效果"。

# 后端:FastAPI + SSE 流式输出 from fastapi import FastAPI from fastapi.responses import StreamingResponse app = FastAPI() async def token_generator(model, prompt): # 逐步生成并 yield 每个 token for token_text in model.generate_stream(prompt): yield f"data: {json.dumps({'token': token_text})}\n\n" @app.post("/chat") async def chat(request: ChatRequest): return StreamingResponse( token_generator(model, request.prompt), media_type="text/event-stream" ) # 前端:接收 SSE 并逐字渲染 const eventSource = new EventSource("/chat"); eventSource.onmessage = (event) => { const data = JSON.parse(event.data); appendToChatWindow(data.token); // 逐字追加到对话框 };

Step 11:前端渲染与交互

11

Markdown 渲染、代码高亮、LaTeX 公式

原始文本经过 Markdown 解析器、代码高亮库(如 Prism.js)、MathJax/KaTeX 渲染后呈现给用户。

🚀 工程优化层

推理加速技术

技术 原理 效果
量化 FP16 → INT8/INT4,降低精度减少内存 内存减半/四倍,轻微精度损失
Continuous Batching 动态批处理,新请求随时加入当前 batch 吞吐量提升 10-20 倍
PagedAttention 将 KV Cache 分页管理,减少内存碎片 vLLM 核心,支持更大 batch
Speculative Decoding 小模型草稿 + 大模型验证,并行接受多个 token 延迟降低 2-3 倍
Tensor Parallelism 将矩阵计算拆分到多 GPU 单请求跨卡并行,降低延迟
Pipeline Parallelism 不同层放在不同 GPU 支持超大模型跨机部署

vLLM 的 PagedAttention 核心思想

# 传统 KV Cache: 预分配连续内存 [max_seq_len, head_dim] # 问题: 实际序列长度差异大,内存浪费严重 # PagedAttention: 将 KV Cache 分为固定大小的 block(如 16 tokens) # 类比操作系统虚拟内存的分页机制 # 逻辑视图 → 物理视图 # Seq A: [t0,t1,t2,t3,t4,t5] → [Block0, Block1] (Block0: t0-t3, Block1: t4-t5) # Seq B: [t0,t1] → [Block2] (Block2: t0-t1, 只占用部分) # Seq C: [t0,t1,t2,t3] → [Block3] (Block3: t0-t3) # 优势: # 1. 消除内存碎片(内部碎片只在 block 末尾) # 2. 共享 block(并行解码时共享 prompt 的 KV Cache) # 3. 动态分配,无需预先知道最大长度

🔄 完整数据流回顾

用户输入: "请解释量子计算" │ ▼ ┌─────────────────────────────────────────────────────────────┐ │ Phase 1: 输入处理层 │ ├─────────────────────────────────────────────────────────────┤ │ 1. 文本清洗 → "请解释量子计算" │ │ 2. Tokenize → [10001, 5234, 3402, 2105] (4个token) │ │ 3. Embedding → 4 × 4096 的稠密向量矩阵 │ │ 4. + Positional Encoding (RoPE) │ └─────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────┐ │ Phase 2: Transformer 推理层 (以 LLaMA-70B 为例) │ ├─────────────────────────────────────────────────────────────┤ │ 5. 80层 Transformer Block 前向传播 │ │ ├─ LayerNorm → MHA (64 heads, head_dim=128) │ │ │ ├─ Q/K/V 投影: [4, 4096] × [4096, 4096] │ │ │ ├─ RoPE 位置编码 │ │ │ ├─ Attention: softmax(QK^T/√128 + mask) × V │ │ │ └─ 输出投影 + 残差 │ │ ├─ LayerNorm → FFN (SwiGLU, 4×d) │ │ │ └─ gate = silu(xW_g) ⊙ (xW_u), out = gate × W_d │ │ └─ 残差连接 │ │ │ │ 总参数量: ~70B (Attention: 25%, FFN: 65%, Embed: 10%) │ │ 总计算量: ~2 × params × tokens FLOPs │ └─────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────┐ │ Phase 3: 输出生成层 │ ├─────────────────────────────────────────────────────────────┤ │ 6. LM Head: h_final × E^T → logits [1, 32000] │ │ 7. Temperature scaling + Top-p sampling │ │ 8. 自回归循环: 预测 → 采样 → 拼接 → 预测... │ │ 生成: "量子计算是一种利用量子力学..." │ │ 共生成 128 个新 token │ └─────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────┐ │ Phase 4: 输出渲染层 │ ├─────────────────────────────────────────────────────────────┤ │ 9. Detokenize: token IDs → UTF-8 字符串 │ │ 10. SSE 流式推送到前端 │ │ 11. Markdown 渲染 + 代码高亮 + 逐字显示 │ └─────────────────────────────────────────────────────────────┘ │ ▼ 用户看到: "量子计算是一种利用量子力学原理进行计算的技术..."

时间线估算(以 LLaMA-70B 在 A100 上为例)

阶段 时间 说明
网络传输 ~10-50ms 取决于用户与服务器距离
Tokenization ~1ms CPU 上快速完成
Prefill (Prompt处理) ~50-200ms 取决于 prompt 长度
Decode (每token) ~20-50ms 串行,是主要延迟来源
生成128个token ~2.5-6.4s 128 × 单token延迟
流式渲染 实时 用户感知为"打字机效果"

📝 总结

从 Enter 到第一个 Token 的核心路径

  1. 输入处理:文本 → Token IDs(BPE/SentencePiece)
  2. 嵌入层:查表获取稠密向量 + 位置编码(RoPE)
  3. Transformer:N层注意力 + 前馈网络(自回归、因果掩码、KV Cache)
  4. 输出头:LM Head → Logits → Softmax → 概率分布
  5. 采样:Top-p/Temperature 策略选择下一个 Token
  6. 循环:重复直到 EOS,期间流式输出到前端

核心矛盾:自回归的串行本质(每步只能生成一个 token)与用户对低延迟的期望之间的张力。所有工程优化(KV Cache、量化、Continuous Batching、PagedAttention、Speculative Decoding)都是围绕这一矛盾的妥协与创新。