外观
Transformer 速通:注意力、位置编码与推理
这一章目标:
- 用“工程能用的直觉”理解 Transformer 在干什么
- 明白为什么它会吃内存、为什么长上下文会变慢
- 做一个不依赖 GPU的小实验:用几十行代码实现一次注意力计算
如果你是 RTX 3050:这一章默认按“能在 CPU 上跑通”来讲;后面涉及本地模型时,会优先选 7B 以内、4-bit 量化、短上下文。
Transformer 的核心
Transformer 的关键是 自注意力(Self-Attention):每个位置的表示,都可以“关注”序列中其它位置的信息,然后加权汇总。
你可以把它理解为:
- 输入是一串 Token
- 模型为每个 Token 生成一个向量表示
- 通过注意力把“与当前 Token 最相关的上下文”聚合过来
Transformer(用于语言模型时)通常由很多层堆叠,每一层大致都是:
- 自注意力(Self-Attention)
- 前馈网络(FFN / MLP)
- 残差连接(Residual) + LayerNorm
你不用一开始就把所有细节吃透,但你要抓住“信息怎么在序列里流动”:
- 注意力:负责“看哪里、聚合哪里的信息”
- FFN:负责“把聚合后的信息做非线性变换/表达增强”
注意力在做什么
- Q(Query):我当前想找什么信息
- K(Key):每个位置能提供什么信息
- V(Value):真正被聚合的内容
计算上会得到一个权重分布(softmax),再对 V 做加权求和。
更工程一点的记法:
注意力 = 用 QK 计算相关性 → softmax 得到权重 → 用权重加权求和 V。
经典的“缩放点积注意力(Scaled Dot-Product Attention)”可以写成:
$$ ext{Attention}(Q,K,V)=\text{softmax}(\frac{QK^T}{\sqrt{d_k}})V $$
其中 $d_k$ 是 Key 向量维度。除以 $\sqrt{d_k}$ 是为了让数值更稳定。
多头注意力(Multi-Head)在解决什么
如果只有一个注意力头,它可能只能学到“单一类型的关联”。多头注意力让模型可以在同一层里同时学到多种关联:
- 有的头关注语法关系
- 有的头关注指代关系
- 有的头关注主题一致性
工程上你只要记住:多头 = 多组 Q/K/V 投影并行做注意力,再拼起来。
为什么需要位置编码
注意力本身对顺序不敏感(置换不变),所以要注入位置信息:
- 绝对位置编码
- 相对位置编码
- RoPE(旋转位置编码)等
工程上,你只需要记住:不同的位置编码方案会影响长上下文能力与外推性。
再补一个你后面会频繁遇到的点:
- 很多现代 LLM 使用 RoPE 或相对位置编码
- 它们与“长上下文扩展”(比如通过缩放策略)强相关
你不需要现在就推导 RoPE,只要知道:位置编码决定了模型如何理解“顺序”和“距离”。
推理阶段发生了什么
- 模型一次输出一个 Token
- 每次输出都依赖前面所有 Token(上下文)
- 这也是为什么:
- 上下文越长,推理越慢(KV Cache 相关)
- Prompt 越长,成本越高
KV Cache:为什么能加速
生成第 $t$ 个 token 时,理论上注意力要看前面 $1..t-1$ 的所有 token。
- 不缓存:每一步都重新算一遍历史的 K/V,非常浪费
- KV Cache:把历史 token 的 K/V 存起来,后续只需要算“新 token 的 Q/K/V”,并用缓存的 K/V 做注意力
你会看到两类瓶颈:
- 计算瓶颈:模型本身算得慢
- 内存瓶颈:KV Cache 很占显存/内存(上下文越长越大)
这也是为什么:同一个模型,长上下文会明显更慢、更吃显存。
1. 你要先建立的两个直觉
1.1 “注意力是平方复杂度”直觉
对长度为 $n$ 的序列,注意力权重矩阵是 $n \times n$。
- $n$ 翻倍,注意力相关的开销大致变成 4 倍
这不是让你死记复杂度,而是帮助你理解:
- 为什么 RAG 要“只塞最相关的片段”
- 为什么摘要/裁剪历史对话很重要
1.2 “模型推理像流水线”直觉
一次生成包含很多步骤:
- 编码输入(tokenize)
- 逐 token 生成(decode)
- 采样策略决定输出(temperature/top_p)
工程上你优化体验通常从:
- Streaming(先把字吐出来)
- 缓存(prompt cache / KV cache)
- 控制输出长度(max_tokens)
2. 立刻动手:用 30 行代码算一次注意力
这个实验不需要 GPU,只需要 numpy。
2.1 安装依赖
在你的 venv 里执行:
bash
pip install numpy2.2 写一个最小注意力实现
新建 toy_attention.py:
python
import numpy as np
def softmax(x: np.ndarray) -> np.ndarray:
x = x - np.max(x, axis=-1, keepdims=True)
exp_x = np.exp(x)
return exp_x / np.sum(exp_x, axis=-1, keepdims=True)
def attention(Q: np.ndarray, K: np.ndarray, V: np.ndarray) -> np.ndarray:
d_k = Q.shape[-1]
scores = (Q @ K.T) / np.sqrt(d_k)
weights = softmax(scores)
return weights @ V
if __name__ == "__main__":
np.random.seed(0)
# 假设 4 个 token,每个 token 的向量维度是 3
X = np.random.randn(4, 3)
# 为了演示,直接把 X 当成 Q/K/V(真实模型里会先做线性投影)
Q, K, V = X, X, X
Y = attention(Q, K, V)
print("Input X:\n", X)
print("Output Y (after attention):\n", Y)运行:
bash
python toy_attention.py你会看到输出 Y 是每个 token 对其它 token 的加权汇总。
2.3 你应该观察什么
- 把
X的第 0 行改得很大/很小,看看输出如何被影响 - 把 token 数从 4 改成 256(例如
X = np.random.randn(256, 3)),体感一下时间变化
这能帮助你对“长上下文更慢”建立非常具体的直觉。
3. RTX 3050 相关:什么时候需要折腾 PyTorch + CUDA
本章不强制你装 CUDA。
你后面会遇到三类任务:
- 云端 API 调用:不需要 CUDA
- 本地推理(Ollama / llama.cpp / vLLM):通常不需要你手动装 PyTorch CUDA(取决于方案)
- LoRA 微调(PyTorch):才更可能需要 CUDA
对 RTX 3050 的建议(以“少折腾”为第一原则):
- 想本地推理:优先 Ollama(模型选 7B 以内、4-bit、短上下文)
- 想学训练/微调:等到“微调”章节再装 CUDA,并且以 LoRA 为主
如果你确实要装 PyTorch CUDA:请以 PyTorch 官网给出的安装命令为准(会随版本变化)。
工程视角:你最该关心的 3 件事
- 上下文窗口:决定你能塞多少对话/资料
- KV Cache:决定吞吐与延迟(是否能复用历史计算)
- 采样策略:temperature / top_p 会直接改变输出稳定性
补充一个“经常被忽略但非常重要”的点:
- 上下文内容质量:同样 2k token,上下文结构清晰 vs 混乱堆砌,效果差距很大。
本章验收
满足下面两条,你就可以进入下一章(Prompt 工程 / RAG)了:
- 你能用自己的话解释:Attention 为什么要 Q/K/V
- 你能跑通
toy_attention.py并理解输出是“加权汇总”
