第23章 LLaMA 系列训推实战

第23章 LLaMA 系列训推实战

LLaMA 不是第一个开源大语言模型,但它是第一个让整个行业重新思考”开源 AI 边界”的模型。从 LLaMA 1 的”意外泄漏”到 LLaMA 3 的正式开放,Meta 用一个模型系列定义了开源 LLM 的事实标准。

23.1 章节导入

2023 年 2 月,Meta 发布 LLaMA 1,最初仅限学术审批访问。但模型权重在一周内通过 4chan 和 GitHub 泄露到全网。这个”意外”反而催生了开源 LLM 生态的爆发——Alpaca、Vicuna、WizardLM 等基于 LLaMA 的微调模型如雨后春笋般出现。

到 2024 年,LLaMA 3.1 系列已经覆盖 1B 到 405B 参数规模,405B 版本在多项基准上与 GPT-4o 正面对决。Meta 用接近 40 万张 H100 GPU 的训练集群,向世界展示了开源模型能够达到闭源模型的天花板。

本章将深入拆解 LLaMA 系列的架构设计、训练技术、推理部署和微调实践。无论你是想部署一个私有 LLM 服务,还是想在自己的数据上微调,本章都提供了从理论到代码的完整路径。

23.2 架构拆解

23.2.1 整体架构

LLaMA 本质上是一个标准的 Decoder-only Transformer,但它在三个关键位置做了非标准选择:RoPE 位置编码SwiGLU 激活函数RMSNorm 归一化。这三个选择后来几乎成为所有开源模型的标配。

graph TD
    A["Input Tokens"] --> B["Embedding Layer<br/>(无位置编码添加)"]
    B --> C["Transformer Block × N"]
    C --> D["RMSNorm"]
    D --> E["Linear Head<br/>(与 Embedding 共享权重)"]
    E --> F["Logits → Softmax"]
    
    subgraph "Transformer Block"
        G["RMSNorm"] --> H["Multi-Head Attention<br/>+ RoPE"]
        H --> I["Residual Connection"]
        I --> J["RMSNorm"]
        J --> K["SwiGLU FFN"]
        K --> L["Residual Connection"]
    end
    
    C --> G

23.2.2 RoPE:旋转位置编码

传统的可学习位置编码(如 GPT-2)或正弦位置编码(如原始 Transformer)各有局限:前者不能外推到训练长度之外,后者外推效果也不好。

RoPE(Rotary Position Embedding)的思路很优雅:通过旋转矩阵将位置信息编码到 Query 和 Key 中,使得注意力分数自然地包含了相对位置信息。

数学上,对于位置 \(m\) 的向量 \(\mathbf{q}\),RoPE 将其按维度两两分组,每组旋转角度 \(m\theta_i\)

\[ \text{RoPE}(\mathbf{q}, m) = \mathbf{q} \odot \cos(m\boldsymbol{\theta}) + \text{rotate}(\mathbf{q}) \odot \sin(m\boldsymbol{\theta}) \]

其中 \(\theta_i = 10000^{-2i/d}\)

RoPE 的关键优势在于长度外推——通过 NTK-aware scaling 或 YaRN 等方法,可以将 4K 训练长度的模型扩展到 128K+ 上下文。

# RoPE 的简洁实现
import torch
import torch.nn as nn

class RotaryEmbedding(nn.Module):
    def __init__(self, dim, max_position_embeddings=2048, base=10000):
        super().__init__()
        inv_freq = 1.0 / (base ** (torch.arange(0, dim, 2).float() / dim))
        self.register_buffer("inv_freq", inv_freq)
        self.max_seq_len_cached = max_position_embeddings

    def forward(self, x, seq_len=None):
        seq_len = seq_len or x.shape[-2]
        t = torch.arange(seq_len, device=x.device, dtype=torch.float32)
        freqs = torch.einsum("i,j->ij", t, self.inv_freq)
        # cos 和 sin 缓存
        emb = torch.cat((freqs, freqs), dim=-1)
        return emb.cos()[None, :, None, :], emb.sin()[None, :, None, :]

def apply_rotary_pos_emb(q, k, cos, sin):
    """将 RoPE 应用到 Query 和 Key"""
    def rotate_half(x):
        x1, x2 = x[..., :x.shape[-1] // 2], x[..., x.shape[-1] // 2:]
        return torch.cat((-x2, x1), dim=-1)
    
    q_embed = (q * cos) + (rotate_half(q) * sin)
    k_embed = (k * cos) + (rotate_half(k) * sin)
    return q_embed, k_embed

23.2.3 GQA:分组查询注意力

LLaMA 2 引入了 GQA(Grouped-Query Attention),这是从标准 Multi-Head Attention(MHA)向 Multi-Query Attention(MQA)的折中。

  • MHA(标准):每个 Query head 对应一个 Key/Value head → \(O(d^2 \cdot n_{heads})\)
  • MQA(极端):所有 Query head 共享同一组 Key/Value → KV Cache 大幅减少,但质量下降
  • GQA(折中):将 Query heads 分组,每组共享一对 Key/Value heads

graph LR
    subgraph "MHA (LLaMA 1)"
        Q1[Q Head 1] --- K1[K Head 1]
        Q2[Q Head 2] --- K2[K Head 2]
        Q3[Q Head 3] --- K3[K Head 3]
        Q4[Q Head 4] --- K4[K Head 4]
    end
    
    subgraph "GQA (LLaMA 2)"
        Q5[Q Head 1] --- K5[KV Group A]
        Q6[Q Head 2] --- K5
        Q7[Q Head 3] --- K6[KV Group B]
        Q8[Q Head 4] --- K6
    end
    
    subgraph "MQA (极端)"
        Q9[Q Head 1] --- K7[Single KV]
        Q10[Q Head 2] --- K7
        Q11[Q Head 3] --- K7
        Q12[Q Head 4] --- K7
    end

LLaMA 2 70B 使用了 8 个 KV heads(而 Query heads 为 64),KV Cache 大小减少了 8 倍。这对推理吞吐至关重要——KV Cache 是长序列推理的主要内存瓶颈。

23.2.4 SwiGLU 激活函数

标准 Transformer 使用 ReLU 作为 FFN 的激活函数。LLaMA 选择了 SwiGLU(Swish-Gated Linear Unit):

\[ \text{SwiGLU}(x, W, V) = \text{Swish}(xW) \otimes xV \]

其中 \(\text{Swish}(x) = x \cdot \sigma(\beta x)\)

SwiGLU 引入了一个额外的门控机制,虽然增加了一个权重矩阵(FFN 参数从 \(2d^2\) 变为 \(3d^2\)),但实验表明同样参数预算下效果更好。这也是为什么 LLaMA 的 FFN hidden dim 通常是 \(\frac{2}{3} \times 4d\) 而非标准的 \(4d\)——为了保持总参数量不变。

23.2.5 主要模型规格对比

规格 LLaMA 2 7B LLaMA 2 70B LLaMA 3 8B LLaMA 3 70B LLaMA 3.1 405B
Layers 32 80 32 80 126
Heads 32 64 32 64 128
KV Heads 32 8 8 8 8
Dim 4096 8192 4096 8192 16384
Vocab 32000 32000 128256 128256 128256
Context 4096 4096 8192 8192 131072
训练 Tokens 2.0T 2.0T 15T 15T 15T+
Tip

LLaMA 3 的词表扩展:从 32K 扩展到 128K,这意味着同样的文本需要的 token 数更少。中文编码效率提升尤其明显——LLaMA 2 编码一个中文字符平均需要 2-3 个 token,LLaMA 3 只需 1-2 个。这不仅提升了推理速度,还降低了中文场景的 API 成本。

23.3 训练技术细节

23.3.1 预训练数据策略

Meta 在 LLaMA 3 的技术报告中透露了一个关键数字:15 万亿 token。这比 LLaMA 2 的 2 万亿多了 7.5 倍。但更重要的不是量,而是数据质量。

LLaMA 3 的数据配比大致如下(Meta 没有公开精确比例,这是社区推测):

网页数据:       ~50% (Common Crawl 清洗)
代码数据:       ~25% (GitHub + 代码相关网页)
学术文献:       ~10% (arXiv, 等)
书籍:          ~5%
数学:          ~5%
其他高质量文本:  ~5%

数据清洗是预训练中最耗工程的部分。Meta 使用了以下管线:

  1. 启发式过滤:去除重复 n-gram、低质量页面
  2. 模型质量分类:用一个高质量数据训练的分类器给网页打分
  3. 安全过滤:去除有害内容
  4. 去重:MinHash + LSH 近似去重
# 简化的数据去重示例
from datasketch import MinHash, MinHashLSH

def create_minhash(tokens, num_perm=128):
    m = MinHash(num_perm=num_perm)
    for token in tokens:
        m.update(token.encode('utf-8'))
    return m

# 构建 LSH 索引
lsh = MinHashLSH(threshold=0.8, num_perm=128)

for doc_id, document in enumerate(documents):
    tokens = tokenize(document)
    mh = create_minhash(tokens)
    # 查询近似重复
    duplicates = lsh.query(mh)
    if not duplicates:
        lsh.insert(str(doc_id), mh)
        keep_document(document)
    else:
        # 近似重复,跳过
        discard_document(document)

23.3.2 后训练:SFT + RLHF

LLaMA 的对齐流程遵循经典的 SFT → RLHF 范式,但 Meta 在 LLaMA 3 中做了重要改进:

第一阶段:SFT(Supervised Fine-Tuning)

使用高质量人工标注的对话数据。Meta 强调数据质量远比数量重要——他们发现用少量高质量数据(~10K 条)比大量中等质量数据效果更好。

# SFT 训练配置示例(基于 llama-recipes)
training:
  model_name: "meta-llama/Meta-Llama-3-8B"
  batch_size: 64
  micro_batch_size: 1
  gradient_accumulation_steps: 64
  learning_rate: 2e-5
  num_epochs: 3
  warmup_ratio: 0.03
  lr_scheduler: "cosine"
  
data:
  dataset: "meta-llama/Meta-Llama-3-8B-Instruct"  # 或自定义数据
  train_on_input: false  # 只在 response 上计算 loss
  max_seq_length: 4096
  
precision:
  bf16: true
  gradient_checkpointing: true

第二阶段:RLHF(Reinforcement Learning from Human Feedback)

SFT 模型 → 训练 Reward Model → PPO 优化 → 对齐模型
              ↑                        ↓
         人类偏好数据 ←───── Reject Sampling

Meta 在 LLaMA 2 中使用了 Rejection Sampling + PPO 的两轮迭代策略:先用 SFT 模型生成多个回答,人工选择最好的,用这些数据再做一轮 SFT,然后再跑 PPO。

LLaMA 3 进一步引入了 DPO(Direct Preference Optimization) 作为 PPO 的替代方案——DPO 不需要显式的 Reward Model,直接从偏好数据中优化策略。

Warning

RLHF 不是银弹:Meta 的技术报告指出,过度的 RLHF 会导致模型”过度安全”(refusing benign requests)和”模板化回答”。他们花了大量精力调整 RLHF 的强度,确保模型既安全又有用。在实践中,DPO 的温度参数 beta 需要仔细调节——太高会让模型过于保守,太低则对齐效果不足。

23.3.3 代码训练的秘密

LLaMA 系列在代码任务上的强表现不是偶然的。Meta 在预训练中混入了大量代码数据,但这背后有一个有趣的现象:代码训练提升了模型的推理能力

这不是 Meta 的独家发现——OpenAI 在 GPT-3.5 时代就观察到了类似现象。一种解释是代码的因果结构(condition → action)天然适合训练模型的逻辑推理链路。

23.4 推理部署

23.4.1 部署方案对比

方案 适用场景 GPU 需求 吞吐量 延迟 易用性
vLLM 生产环境高吞吐 必须 ⭐⭐⭐⭐⭐
TGI Hugging Face 生态 必须 ⭐⭐⭐⭐
llama.cpp CPU/Mac/边缘设备 可选 ⭐⭐
Ollama 本地开发 可选 ⭐⭐ ⭐⭐⭐⭐⭐
TensorRT-LLM 极致性能 必须(NVIDIA) ⭐⭐⭐⭐⭐ 极低
MLX Apple Silicon 不需要 ⭐⭐⭐

23.4.2 vLLM 部署

vLLM 是目前最流行的开源推理引擎,核心创新是 PagedAttention(详见第29章)。

# 基础部署:LLaMA 3.1 8B
vllm serve meta-llama/Meta-Llama-3.1-8B-Instruct \
  --port 8000 \
  --tensor-parallel-size 1 \
  --max-model-len 8192 \
  --gpu-memory-utilization 0.9

# 多 GPU 部署:LLaMA 3.1 70B
vllm serve meta-llama/Meta-Llama-3.1-70B-Instruct \
  --port 8000 \
  --tensor-parallel-size 4 \
  --max-model-len 32768 \
  --gpu-memory-utilization 0.9 \
  --enable-prefix-caching

# 405B 需要 8x H100
vllm serve meta-llama/Meta-Llama-3.1-405B-Instruct \
  --port 8000 \
  --tensor-parallel-size 8 \
  --max-model-len 8192 \
  --quantization fp8  # FP8 量化减少显存
# Python 客户端调用
from openai import OpenAI

client = OpenAI(
    base_url="http://localhost:8000/v1",
    api_key="dummy"  # vLLM 默认不校验
)

response = client.chat.completions.create(
    model="meta-llama/Meta-Llama-3.1-8B-Instruct",
    messages=[
        {"role": "system", "content": "你是一个有帮助的助手。"},
        {"role": "user", "content": "解释什么是 PagedAttention"}
    ],
    max_tokens=512,
    temperature=0.7,
    stream=True  # 支持流式输出
)

for chunk in response:
    if chunk.choices[0].delta.content:
        print(chunk.choices[0].delta.content, end="")

23.4.3 Ollama 本地部署

对于个人开发和测试,Ollama 是最简单的方案:

# 安装并运行(一行命令)
ollama run llama3.1:8b

# 自定义 Modelfile(类似 Dockerfile)
cat > Modelfile << 'EOF'
FROM llama3.1:8b

# 系统提示
SYSTEM """你是一个专业的 AI 工程师助手,擅长回答技术问题。"""

# 调整参数
PARAMETER temperature 0.3
PARAMETER top_p 0.9
PARAMETER num_ctx 8192
EOF

# 构建自定义模型
ollama create my-llama -f Modelfile
ollama run my-llama

23.4.4 llama.cpp 量化部署

llama.cpp 支持 GGUF 格式的量化模型,可以在纯 CPU 环境或 Apple Silicon 上运行:

# 下载量化模型(Q4_K_M 是推荐的平衡点)
huggingface-cli download \
  bartowski/Meta-Llama-3.1-8B-Instruct-GGUF \
  Meta-Llama-3.1-8B-Instruct-Q4_K_M.gguf

# 使用 llama.cpp 运行
./llama-cli \
  -m Meta-Llama-3.1-8B-Instruct-Q4_K_M.gguf \
  -c 8192 \
  -n -1 \
  --chat-format llama3

# Apple Silicon 上使用 Metal 加速
./llama-cli \
  -m Meta-Llama-3.1-8B-Instruct-Q4_K_M.gguf \
  -ngl 99 \  # 将所有层放到 GPU
  -c 8192

量化对质量的影响是可接受的——Q4_K_M(4-bit 量化)相比 FP16,在多数基准上质量下降不到 2%:

量化格式 文件大小 (8B) 相对质量 推理速度
FP16(原始) 16 GB 100% 1.0x
Q8_0 8.5 GB ~99% 1.3x
Q4_K_M 4.7 GB ~98% 1.6x
Q4_0 4.4 GB ~96% 1.8x
Q3_K_M 3.9 GB ~93% 2.0x
Q2_K 3.0 GB ~87% 2.2x

23.5 微调实践

23.5.1 LoRA:低秩适配

LoRA(Low-Rank Adaptation)是目前最流行的参数高效微调方法。核心思想:不修改原始权重,而是在旁边加一个低秩矩阵

\[ W' = W + \Delta W = W + BA \]

其中 \(B \in \mathbb{R}^{d \times r}\)\(A \in \mathbb{R}^{r \times d}\)\(r \ll d\)(通常 \(r = 8, 16, 32\))。

# 使用 PEFT 库进行 LoRA 微调
from peft import LoraConfig, get_peft_model, TaskType
from transformers import AutoModelForCausalLM, TrainingArguments
from trl import SFTTrainer

model = AutoModelForCausalLM.from_pretrained(
    "meta-llama/Meta-Llama-3-8B",
    torch_dtype=torch.bfloat16,
    device_map="auto"
)

lora_config = LoraConfig(
    task_type=TaskType.CAUSAL_LM,
    r=16,                    # LoRA 秩
    lora_alpha=32,           # 缩放因子,通常 = 2r
    lora_dropout=0.05,
    target_modules=[
        "q_proj", "k_proj", "v_proj", "o_proj",  # Attention
        "gate_proj", "up_proj", "down_proj"       # FFN
    ],
)

model = get_peft_model(model, lora_config)
model.print_trainable_parameters()
# Output: trainable params: 13,631,488 || all params: 8,072,204,288 || trainable%: 0.17%

trainer = SFTTrainer(
    model=model,
    train_dataset=dataset,
    args=TrainingArguments(
        output_dir="./lora-output",
        num_train_epochs=3,
        per_device_train_batch_size=4,
        gradient_accumulation_steps=4,
        learning_rate=2e-4,
        bf16=True,
        logging_steps=10,
        save_strategy="epoch",
    ),
    max_seq_length=2048,
)

trainer.train()

# 保存 LoRA adapter(只有几十 MB)
model.save_pretrained("./my-llama-lora")

23.5.2 QLoRA:量化 + LoRA

QLoRA 在 LoRA 基础上进一步量化基础模型到 4-bit,使得在单张消费级 GPU(如 RTX 4090 24GB)上微调 70B 模型成为可能。

from transformers import BitsAndBytesConfig

# 4-bit 量化配置
bnb_config = BitsAndBytesConfig(
    load_in_4bit=True,
    bnb_4bit_quant_type="nf4",       # NormalFloat 4-bit
    bnb_4bit_compute_dtype=torch.bfloat16,
    bnb_4bit_use_double_quant=True,   # 双重量化
)

model = AutoModelForCausalLM.from_pretrained(
    "meta-llama/Meta-Llama-3-70B",
    quantization_config=bnb_config,
    device_map="auto"
)

# 然后照常使用 LoRA
model = get_peft_model(model, lora_config)
Tip

QLoRA 的实用建议: 1. nf4 量化类型比 fp4 效果更好——它假设权重服从正态分布 2. double_quant 可以再省 0.4 bit/参数,虽然不多但白捡 3. LoRA rank 不要太低——对于领域微调,r=32 通常是性价比最高的选择 4. 学习率要比全量微调高 10 倍左右(2e-4 vs 2e-5)

23.5.3 Full Fine-tuning

当你有足够的 GPU 资源且追求最高质量时,全量微调仍然是最佳选择。

# DeepSpeed ZeRO-3 配置用于 LLaMA 70B 全量微调
{
  "bf16": {
    "enabled": true
  },
  "zero_optimization": {
    "stage": 3,
    "overlap_comm": true,
    "contiguous_gradients": true,
    "sub_group_size": 1e9,
    "reduce_bucket_size": "auto",
    "stage3_prefetch_bucket_size": "auto",
    "stage3_param_persistence_threshold": "auto",
    "stage3_max_live_parameters": 1e9,
    "stage3_max_reuse_distance": 1e9,
    "stage3_gather_16bit_weights_on_model_save": true
  },
  "gradient_accumulation_steps": "auto",
  "gradient_clipping": "auto",
  "train_batch_size": "auto",
  "train_micro_batch_size_per_gpu": "auto",
  "steps_per_print": 2000
}
# 启动全量微调(8x H100)
deepspeed --num_gpus 8 \
  train.py \
  --model_path meta-llama/Meta-Llama-3-70B \
  --dataset_path ./data/train.jsonl \
  --output_dir ./output \
  --deepspeed ds_config_zero3.json \
  --learning_rate 2e-5 \
  --num_train_epochs 3 \
  --per_device_train_batch_size 2 \
  --gradient_accumulation_steps 16

23.5.4 微调方法选型

全量微调
  ↑ 质量
  │     ↗ LoRA (r=64+)
  │   ↗
  │ ↗     ← QLoRA
  │
  └──────────────→ GPU 资源需求
  低               高
方法 GPU 需求 (7B) GPU 需求 (70B) 训练速度 推理质量 Adapter 大小
Full FT 2x A100 80G 8x H100 80G 1.0x 100% 全量
LoRA 1x RTX 4090 4x A100 80G 1.5x ~98% ~50MB
QLoRA 1x RTX 3090 1x A100 80G 0.8x ~96% ~50MB

23.6 实践建议

Tip

部署决策树

  1. 只是想本地玩玩? → Ollama(一行命令)
  2. Mac 上跑? → Ollama 或 MLX(Apple Silicon 原生加速)
  3. 生产环境、高吞吐? → vLLM(PagedAttention 是杀手锏)
  4. NVIDIA 集群、极致延迟? → TensorRT-LLM(需要编译,但最快)
  5. CPU 服务器? → llama.cpp + GGUF 量化
  6. 需要灵活部署(多模型切换)? → TGI
Warning

常见陷阱: 1. Tokenizer 陷阱:LLaMA 3 的 tokenizer 与 LLaMA 2 完全不同(128K vs 32K vocab),微调数据必须用对应版本的 tokenizer 重新编码 2. Chat template 陷阱:LLaMA 3 使用自定义的 <|begin_of_text|><|start_header_id|>...<|end_header_id|> 格式,推理时必须使用正确的 chat template,否则效果会大幅下降 3. RoPE 缩放陷阱:如果你想让 8B 模型支持 128K 上下文,仅靠 YaRN 缩放是不够的——模型需要在长上下文数据上继续预训练 4. 量化陷阱:QLoRA 微调的模型在推理时必须保持量化状态,不能反量化回 FP16 再推理——这会丢失微调效果

23.7 小结

LLaMA 系列的成功不在于某个单一的技术突破,而在于一系列工程选择的系统性优化:

  • RoPE 提供了长度外推的灵活性
  • GQA 在质量和效率之间找到了最佳平衡点
  • SwiGLU 用门控机制提升了 FFN 的表达能力
  • 高质量数据 + 大规模训练 是 LLaMA 3 性能飞跃的根本原因

在推理侧,vLLM + PagedAttention 已经成为事实标准,而 Ollama 让个人开发者也能轻松在本地运行大模型。微调方面,LoRA/QLoRA 让参数高效微调成为主流——你不需要一个集群来定制模型,一张消费级 GPU 就够了。

23.8 延伸阅读

  • Meta (2024). The LLaMA 3 Herd of Models. arXiv:2407.21783 — 目前最详细的技术报告
  • Meta (2023). LLaMA 2: Open Foundation and Fine-Tuned Chat Models. arXiv:2307.09288
  • Su et al. (2021). RoFormer: Enhanced Transformer with Rotary Position Embedding. arXiv:2104.09864
  • Ainslie et al. (2023). GQA: Training Generalized Multi-Query Transformer Models from Multi-Head Checkpoints. arXiv:2305.13245
  • Hu et al. (2021). LoRA: Low-Rank Adaptation of Large Language Models. arXiv:2106.09685
  • Dettmers et al. (2023). QLoRA: Efficient Finetuning of Quantized LLMs. arXiv:2305.14314
  • vLLM 项目:https://github.com/vllm-project/vllm
  • Ollama 项目:https://github.com/ollama/ollama