🗺️ 全景概览
类比:互联网时代的 "输入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 对。
vocab = {"请": 10001, "解释": 5234, "一下": 891,
"量子": 3402, "计算": 2105, "的": 101,
"原理": 4567}
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_matrix = torch.randn(vocab_size, hidden_dim)
input_ids = torch.tensor([[10001, 5234, 891, 3402, 2105, 101, 4567]])
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 注入位置信息
def apply_rope(x, positions, theta=10000.0):
freqs = 1.0 / (theta ** (torch.arange(0, head_dim, 2) / head_dim))
angles = torch.outer(positions, freqs)
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_norm = LayerNorm(x)
q = W_q @ x_norm
k = W_k @ x_norm
v = W_v @ x_norm
scores = (q @ k.T) / sqrt(d_k)
scores = scores + causal_mask
attn_weights = softmax(scores, dim=-1)
attn_output = attn_weights @ v
attn_out = W_o @ attn_output
x = x + attn_out
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)实现。
mask = [[0, -inf, -inf, -inf],
[0, 0, -inf, -inf],
[0, 0, 0, -inf],
[0, 0, 0, 0]]
Step 5.2:KV Cache 机制
5.2
避免重复计算的关键优化
自回归生成时,已生成的 token 的 K、V 向量不会变化,缓存起来避免每步重新计算,将复杂度从 O(seq²) 降到 O(seq)。
kv_cache = {}
for layer in layers:
k, v = layer.compute_kv(x)
kv_cache[layer_idx] = (k, v)
new_token_emb = embedding_matrix[new_token_id]
for layer in layers:
k_new, v_new = layer.compute_kv(new_token_emb)
k_cached, v_cached = kv_cache[layer_idx]
k_all = torch.cat([k_cached, k_new], dim=1)
v_all = torch.cat([v_cached, v_new], dim=1)
kv_cache[layer_idx] = (k_all, v_all)
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。
def moe_layer(x, n_experts=256, top_k=8):
router_logits = W_router @ x
weights, expert_indices = torch.topk(
softmax(router_logits, dim=-1),
k=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_weight = embedding_matrix.T
logits = h_final @ lm_head_weight.T
temperature = 0.7
logits_scaled = logits / temperature
probs = F.softmax(logits_scaled, dim=-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常用 |
def top_p_sampling(logits, p=0.9, temperature=1.0):
logits = logits / temperature
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)
mask = cumsum_probs > p
mask[..., 0] = False
sorted_logits[mask] = -float('inf')
probs = F.softmax(sorted_logits, dim=-1)
sample_idx = torch.multinomial(probs, num_samples=1)
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()
kv_cache = None
logits, kv_cache = model.forward(generated, use_cache=True)
next_logits = logits[:, -1, :]
for i in range(max_new_tokens):
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
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)的边界问题。
token_ids = [10001, 5234, 891, 3402, 2105, 101, 4567, ...new_tokens...]
tokens = [vocab_inv[id] for id in token_ids]
text = "".join(tokens).replace("▁", " ")
Step 10:流式输出(Streaming)
10
Server-Sent Events (SSE) 实时推送
不需要等全部生成完毕再返回,每生成一个 token 就通过 SSE 推送给前端,实现"打字机效果"。
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
app = FastAPI()
async def token_generator(model, prompt):
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"
)
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 核心思想
🔄 完整数据流回顾
用户输入: "请解释量子计算"
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 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 的核心路径
- 输入处理:文本 → Token IDs(BPE/SentencePiece)
- 嵌入层:查表获取稠密向量 + 位置编码(RoPE)
- Transformer:N层注意力 + 前馈网络(自回归、因果掩码、KV Cache)
- 输出头:LM Head → Logits → Softmax → 概率分布
- 采样:Top-p/Temperature 策略选择下一个 Token
- 循环:重复直到 EOS,期间流式输出到前端
核心矛盾:自回归的串行本质(每步只能生成一个 token)与用户对低延迟的期望之间的张力。所有工程优化(KV Cache、量化、Continuous Batching、PagedAttention、Speculative Decoding)都是围绕这一矛盾的妥协与创新。