第二章 大模型如何工作:从 Token 到 Transformer
"Transformer 不是魔法,而是一套精心设计的信息路由系统。"
------ 本书作者 _abab
2.1 整体流程概览:一条文本的旅程
当你向大模型输入一句 "你好,今天天气怎么样?",它经历了以下步骤:
|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [原始文本] ↓ [Tokenizer 分词] → ["你", "好", ",", "今", "天", "天", "气", "怎", "么", "样", "?"] ↓ [Embedding 层] → 每个 token 变成 4096 维向量(以 Qwen-7B 为例) ↓ [位置编码(Positional Encoding)] → 注入"第3个词是','"的位置信息 ↓ [多层 Transformer Block] → 重复 32 次(Qwen-7B 的层数) ↓ [输出层(LM Head)] → 预测下一个 token 的概率分布 ↓ [采样策略(如 Top-p)] → 选择"晴"作为下一个词 ↓ [循环生成] → 拼接"晴",继续预测,直到结束 |
✅ 开发者视角:整个过程可视为一个 stateful 的函数调用链 ,每一步都可监控、可干预。例如:
- 分词后可过滤敏感 token
- 生成中可中断不符合预期的输出
- 位置编码可自定义扩展上下文长度
2.2 Tokenizer:文本的 "原子化" 处理
为什么需要分词?
神经网络只能处理数字,不能直接处理汉字或英文单词。Tokenizer 就是文本到数字 ID 的映射器,核心目标是:
- 覆盖所有可能的输入(最小化 OOV 率)
- 平衡 token 数量(太少导致语义模糊,太多增加计算量)
主流分词算法对比
|----------------------------------|------------|------------------------------|-------------------------------------------|---------------|-------------------|
| 算法 | 原理 | 示例(中文) | 示例(英文) | 适用模型 | 开发者关注要点 |
| WordPiece | 贪心合并高频子词 | "你好" → ["你", "好"] | "unhappiness" → ["un", "happi", "ness"] | BERT, ChatGLM | 中文 OOV 率低,适合理解类任务 |
| Byte Pair Encoding (BPE) | 统计合并最常见字节对 | "天气" → ["天", "气"] | "apple" → ["app", "le"] | GPT, Llama | 英文压缩率高,生成速度快 |
| SentencePiece | 无监督,支持跨语言 | "AI 助手" → ["AI", "助", "手"] | 同上,支持混合语言 | T5, Qwen | 适配多语言场景,可自定义词表 |
Qwen 使用 基于 BPE 的 SentencePiece ,其 tokenizer 核心优势(开发者需知):
- 中文按字切分为主,生僻字自动拆分为 UTF-8 字节(OOV 率接近 0)
- 支持动态扩展词表(如添加领域专有名词)
- 内置特殊 token(如 <s> 起始符、符、填充符)
开发者实操(Hugging Face)
|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| from transformers import AutoTokenizer # 加载 Qwen 的 tokenizer(需信任远程代码) tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen-7B-Chat", trust_remote_code=True) # 基础分词与解码 inputs = "你好,世界!" tokens = tokenizer(inputs) print("Token ID 序列:", tokens["input_ids"]) # 输出: [151644, 872, 198, 108503, 108504, 32852, 151645] print("Token 文本:", tokenizer.convert_ids_to_tokens(tokens["input_ids"])) # 输出: ['>', '你', '好', ',', '世', '界', '!', 'print("解码还原:", tokenizer.decode(tokens["input_ids"])) # 输出: "你好,世界!" # 处理长文本(自动截断/填充) long_text = "这是一段超过模型最大上下文长度的文本..." tokens = tokenizer(long_text, max_length=10, truncation=True, padding="max_length") print("截断后长度:", len(tokens["input_ids"])) # 输出: 10 # 自定义特殊 token tokenizer.add_special_tokens({"additional_special_tokens": [" print("新增 token ID:", tokenizer("")["input_ids"]) # 输出新增 ID |
⚠️ 关键警告:不同模型的 tokenizer 完全不兼容 !
- 用 Llama 的 tokenizer 处理 Qwen 的输入会导致语义错乱
- 部署时需确保 tokenizer 与模型权重严格匹配(建议一起下载)
2.3 Embedding 与位置编码:赋予 token 意义和顺序
Embedding 层:从 ID 到语义向量
- 核心功能:将离散的 token ID 映射为连续的向量(如 4096 维)
- 学习机制:训练过程中,语义相近的词(如 "猫" 和 "狗")的向量在高维空间中距离更近
- 内存占用:Qwen-7B 的 Embedding 层 ≈ 7B × 4096 维 × 4 字节(FP16)≈ 112MB(可忽略不计)
✅ 开发者优化点:
- 量化时 Embedding 层建议保留 FP16(INT8 会导致语义损失)
- 领域适配时可冻结 Embedding 层(减少微调参数)
位置编码:解决 Transformer 的 "顺序失忆症"
Transformer 是并行计算架构,本身不感知 token 顺序 ------ 若没有位置编码,"我吃苹果" 和 "苹果吃我" 会被视为相同输入。
主流位置编码对比
|----------------------|-------------------|---------------|-------------|-----------------------|-------------------|
| 类型 | 原理 | 优势 | 劣势 | 适用场景 | 开发者实操建议 |
| 正弦 / 余弦编码 | 用不同频率的正余弦函数生成位置向量 | 理论支持无限长度 | 长距离位置关系衰减快 | 早期模型(GPT-2) | 不建议用于上下文扩展 |
| 学习型位置编码 | 位置向量作为可训练参数 | 适配特定任务 | 超出训练长度后性能骤降 | BERT | 微调时需同步扩展位置参数 |
| RoPE(旋转位置编码) | 对向量进行旋转操作,编码相对位置 | 支持长上下文外推、计算高效 | 需适配模型架构 | Llama, Qwen, ChatGLM4 | 推荐优先选择支持 RoPE 的模型 |
RoPE 核心优势(开发者必须掌握):
- 外推能力:训练时用 2K 上下文,推理时可扩展到 8K 甚至 32K(仅需调整旋转角度)
- 相对位置保持:编码的是 "词 A 与词 B 相距 k 个位置",而非绝对位置,更符合语言逻辑
- 显存友好:无需存储位置参数,实时计算生成
工程实践:扩展上下文长度(如 8K → 32K)
以 Qwen-7B 为例,扩展上下文到 32K(需修改 RoPE 旋转系数)
|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| from transformers import AutoModelForCausalLM model = AutoModelForCausalLM.from_pretrained ( "Qwen/Qwen-7B-Chat", trust_remote_code=True, rope_scaling={"type": "linear", "factor": 4.0} # 8K ×4 =32K ) tokenizer.pad_token = tokenizer.eos_token # 确保填充 token 正确 |
2.4 Transformer Block:大模型的"计算单元"
每个 Transformer 层(Decoder 层,大模型生成核心)包含两个核心子模块 + 归一化/残差连接,结构如下:
输入 X → LayerNorm → 多头自注意力 → 残差连接 → LayerNorm → FFN → 残差连接 → 输出
(1)多头注意力机制(Multi-Head Self-Attention)
"Attention is All You Need" ------ 2017 年那篇改变世界的论文
直观理解:
每个词会"关注"句子中其他词的重要性,例如:
-
句子"猫追老鼠,因为它饿了"中,"它"会重点关注"猫"
-
句子"苹果的价格比香蕉贵"中,"贵"会关注"苹果"和"香蕉"
数学简化(开发者友好版,忽略缩放和 dropout):
|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| def multi_head_attention(X, num_heads, d_model): # X: [batch_size, seq_len, d_model] 输入向量 d_k = d_model // num_heads # 每个头的维度 # 1. 线性变换得到 Q, K, V W_q = torch.randn(d_model, d_model) # 可学习权重 W_k = torch.randn(d_model, d_model) W_v = torch.randn(d_model, d_model) Q = X @ W_q # [batch_size, seq_len, d_model] K = X @ W_k V = X @ W_v # 2. 分割为多个头(并行计算) Q = Q.reshape(-1, seq_len, num_heads, d_k).transpose(1, 2) # [batch, heads, seq_len, d_k] K = K.reshape(-1, seq_len, num_heads, d_k).transpose(1, 2) V = V.reshape(-1, seq_len, num_heads, d_k).transpose(1, 2) # 3. 计算注意力分数(相似度) scores = Q @ K.transpose(-2, -1) / math.sqrt(d_k) # [batch, heads, seq_len, seq_len] attn_weights = torch.softmax(scores, dim=-1) # 归一化 # 4. 加权求和得到输出 output = attn_weights @ V # [batch, heads, seq_len, d_k] output = output.transpose(1, 2).reshape(-1, seq_len, d_model) # 合并头 return output |
多头(Multi-Head)的核心价值:
- 不同头关注不同关系:有的头关注语法(主谓宾),有的头关注语义(同义替换),有的头关注指代(代词指向)
- 例如 Qwen-7B 有 16 个头,相当于 16 个 "专家" 并行分析文本关系
开发者优化点:
- 注意力权重可可视化(如用 matplotlib 绘制热力图),辅助调试语义理解问题
- 对长文本可使用 "稀疏注意力"(如 Local Attention),减少计算量
(2)前馈神经网络(Feed-Forward Network, FFN)
- 结构:两层线性变换 + 非线性激活(GELU)
|-------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| def ffn(X, d_model=4096, d_ff=11008): d_ff 通常是 d_model 的 2~4 倍(Qwen-7B 用 11008=4096×2.6875) return torch.relu (X @ W1 + b1) @ W2 + b2 # 简化版,实际用 GELU |
- 作用:对注意力输出进行**非线性变换和特征增强**------注意力负责"关联信息",FFN 负责"提炼信息"
- 计算占比:FFN 占 Transformer 层计算量的 60%+,是推理速度优化的重点
(3)残差连接 + LayerNorm:深层网络的"稳定器"
- 残差连接(Residual Connection):`output = X + sublayer(X)`
- 解决梯度消失问题,支持深层网络(如 100+ 层)
- 开发者可通过监控残差连接的输出方差,判断模型是否训练稳定
- LayerNorm:对每个样本的特征维度归一化(`mean=0, std=1`)
- 加速训练收敛,提高模型鲁棒性
- 现代模型多使用"预归一化"(输入先归一化),而非原始论文的"后归一化"
✅ 开发者须知:Transformer 层是 GPU 显存和计算的主要消耗者
- 计算量:O(seq_len² × d_model)(注意力层) + O(seq_len × d_model × d_ff)(FFN 层)
- 显存:主要用于存储 Q/K/V 矩阵和中间激活值(可通过梯度检查点技术优化)
2.5 输出层与采样策略:决定生成的"质量与多样性"
(1)输出层(LM Head)
- 作用:将 Transformer 输出的高维向量(4096 维)映射为所有 token 的概率分布
- 结构:`Linear(d_model, vocab_size) + Softmax`
- Qwen-7B 的 vocab_size=151936,因此 LM Head 权重矩阵为 4096×151936 ≈ 6.2GB(FP16)
- 优化技巧:权重共享(Embedding 层与 LM Head 共享权重),减少显存占用
(2)采样策略:从概率分布中选 token
生成式模型的核心决策点------不同策略会导致输出风格天差地别,开发者需根据场景选择:
| 策略 | 原理 | 优点 | 缺点 | 适用场景 | 代码示例 |
|---|---|---|---|---|---|
| 贪心搜索(Greedy) | 每次选概率最高的 token | 速度快,确定性强 | 输出单调,易重复 | 简单问答、代码生成 | next_token = torch.argmax(logits, dim=-1) |
| 束搜索(Beam Search) | 保留 top-k 个候选路径,最终选最优 | 输出更连贯 | 计算量大(k倍),多样性差 | 摘要、翻译 | beam_search(logits, beam_size=3) |
| Top-k 采样 | 从概率最高的 k 个 token 中随机选 | 多样性强 | 可能选低概率无意义 token | 创意写作、对话 | top_k = 50; filtered = logits.topk(top_k); next_token = torch.multinomial(filtered.softmax(dim=-1), 1) |
| Top-p 采样(Nucleus) | 从概率和≥p 的最小 token 集合中随机选 | 平衡多样性和合理性 | 需调温度参数 | 大多数生成场景 | top_p = 0.9; sorted_probs = torch.sort(logits.softmax(dim=-1), descending=True); cumsum = sorted_probs.cumsum(dim=-1); keep = cumsum ≤ top_p |
| 温度调节(Temperature) | 对 logits 除以温度(T>1 增加多样性,T<1 增加确定性) | 灵活调整输出风格 | 需手动调参 | 所有场景 | logits = logits / T; next_token = torch.multinomial(logits.softmax(dim=-1), 1) |
开发者最佳实践:
对话场景:Top-p (0.9) + 温度 (0.7),平衡自然度和可控性
代码/逻辑场景:贪心搜索或 Top-k (20) + 温度 (0.3),保证准确性
创意场景:Top-p (0.95) + 温度 (1.2),增加多样性
(3)生成终止条件
- 必须显式设置,否则模型会无限生成:
|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| def generate(model, tokenizer, prompt, max_new_tokens=512, eos_token_id=151645): input_ids = tokenizer(prompt, return_tensors="pt").input_ids for _ in range(max_new_tokens): logits = model(input_ids).logits[:, -1, :] next_token = top_p_sampling(logits, top_p=0.9) input_ids = torch.cat([input_ids, next_token], dim=-1) # 终止条件:生成 eos token 或达到最大长度 if next_token.item() == eos_token_id: break return tokenizer.decode(input_ids[0], skip_special_tokens=True) |
2.6 自回归生成与 KV Cache:效率的核心
大模型生成文本采用 自回归(Autoregressive)方式------ 逐词生成,直到触发终止条件。但直接计算会有严重性能问题:
(1)无缓存的性能瓶颈
- 每次生成新 token 时,需重新计算整个输入序列的 Transformer 前向传播
- 生成 L 个 token 的计算量:O (L × seq_len² × d_model),seq_len 随生成过程不断增长
- 示例:生成 1024 个 token 时,总计算量相当于单次处理 1024×(1024+1)/2 ≈ 50 万 token
(2)KV Cache:生成效率的 "革命"
- 核心思想:缓存之前 token 的 K(Key)和 V(Value)向量,避免重复计算
- 原理:新 token 只需要与所有历史 token 计算注意力,而历史 token 之间的注意力已在之前计算完成
- 性能提升:生成 L 个 token 的计算量降至 O (L × seq_len × d_model),速度提升 10~100 倍
开发者实操:KV Cache 配置
|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| # Hugging Face 中启用 KV Cache(默认开启) model = AutoModelForCausalLM.from_pretrained( "Qwen/Qwen-7B-Chat", trust_remote_code=True, use_cache=True # 启用 KV Cache ) # 监控 KV Cache 显存占用 def get_kv_cache_memory(model, seq_len, batch_size=1): num_layers = model.config.num_hidden_layers d_model = model.config.hidden_size num_heads = model.config.num_attention_heads d_k = d_model // num_heads # K 和 V 各占 batch_size × num_layers × seq_len × num_heads × d_k 字节 kv_memory = batch_size * num_layers * seq_len * num_heads * d_k * 2 # ×2 因为 K 和 V return kv_memory / 1024 / 1024 / 1024 # 转换为 GB # 示例:Qwen-7B 生成 8K token 时的 KV Cache 占用 print(f"KV Cache 显存: {get_kv_cache_memory(model, seq_len=8192):.2f} GB") # 输出约 8GB |
⚠️ KV Cache 注意事项:
- 开启后会增加显存占用(如 8K 上下文约 8GB),但生成速度大幅提升
- 多轮对话中,需缓存整个对话历史的 KV 向量,避免重新计算
- 动态批处理(Dynamic Batching)场景下,KV Cache 会碎片化,需用 vLLM 等引擎优化
2.7 上下文长度与内存占用:工程化选型的核心依据
内存消耗拆解(以 Qwen-7B 为例,FP16 精度)
大模型推理时的显存占用 = 模型权重显存 + KV Cache 显存 + 中间激活显存 + 框架开销
|-------------|-------------------------------|----------------|----------|----------------------------------|
| 组件 | 计算方式 | 8K 上下文 | 32K 上下文 | 开发者优化建议 |
| 模型权重 | 参数量 × 2 字节(FP16) | 7B × 2B = 14GB | 14GB(不变) | 用 INT8 量化可降至 7GB,INT4 降至 3.5GB |
| KV Cache | 前文公式 | ~8GB | ~32GB | 用分组量化(如 GPTQ),或限制上下文长度 |
| 中间激活 | seq_len × d_model × 2 字节 × 层数 | ~2GB | ~8GB | 启用梯度检查点(牺牲速度换显存) |
| 框架开销 | 固定开销 | ~2GB | ~2GB | 用 TensorRT-LLM 等优化框架减少开销 |
| 总显存 | 求和 | ~26GB | ~56GB | 8K 场景用 32GB GPU,32K 场景用 80GB GPU |
上下文长度的工程化权衡
|-----------|-------------------|---------------------|--------------|--------------------------------|
| 上下文长度 | 适用场景 | 硬件要求 | 推理速度 | 开发者决策要点 |
| 2K~8K | 短对话、单轮问答、代码生成 | 消费级 GPU(16GB~32GB) | 快(TPOT ) | 大多数场景的默认选择,平衡成本和体验 |
| 16K~32K | 长文档分析、多轮对话、法律合同解读 | 企业级 GPU(40GB~80GB) | 中(TPOT 0ms) | 需开启 RoPE 外推,用 vLLM 优化 KV Cache |
| 64K~128K | 书籍摘要、论文解读、超长代码调试 | 多卡 GPU(80GB×2+) | 慢(TPOT 50ms) | 仅必要场景使用,需结合稀疏注意力优化 |
✅ 核心结论:8K 是当前平衡能力、成本和速度的 "甜点" ,除非有明确的长文本需求,否则不建议盲目追求超长上下文。
本章小结
- 大模型的核心流程:文本→Tokenizer→Embedding→位置编码→Transformer→LM Head→采样→生成,每一步都可工程化干预。
- Tokenizer 是 "输入入口":不同模型不兼容,需匹配权重使用,Qwen 的 SentencePiece 适配多语言场景。
- 位置编码决定上下文能力:RoPE 支持外推,是扩展上下文长度的关键,开发者可通过 rope_scaling 配置。
- Transformer Block 是 "计算核心":注意力负责关联信息,FFN 负责提炼特征,残差连接 + LayerNorm 保证稳定。
- 生成效率的关键:KV Cache 减少重复计算,采样策略决定输出质量,自回归循环控制生成流程。
- 内存占用的核心:模型权重(可量化优化)+ KV Cache(与上下文长度正相关),8K 上下文是工程化首选。
开发者落地建议:
- 先掌握基础流程(分词→生成),再优化细节(采样策略、KV Cache)
- 用 Qwen-7B 作为入门实践模型(文档全、生态成熟)
- 遇到显存不足:优先量化模型(INT8),再限制上下文长度,最后考虑多卡部署