基于
meta-llama/llama仓库(Llama 2 官方参考实现)的全面架构源码分析。 本仓库为 Llama 2 的最小化参考实现 —— 代码极其精炼,总计仅约 900 行 Python,却完整展示了一个百亿~千亿参数级别大语言模型 (LLM) 的结构与推理流程。 官方 README 中已标注 deprecated,主线后续演进到 llama-models、PurpleLlama、llama-toolchain 等多仓库体系。但理解这份原始实现,是理解整个 Llama 生态(含 Llama 3/4 的 HF transformers、llama.cpp、vLLM、SGLang、TensorRT-LLM 等)架构演化的起点。
目录
- 仓库结构总览
- 核心架构:Decoder-Only Transformer
- [model.py 逐模块深度剖析](#三modelpy-逐模块深度剖析)
- [generation.py 推理引擎分析](#四generationpy-推理引擎分析)
- [tokenizer.py 分词器剖析](#五tokenizerpy-分词器剖析)
- 对话模板 (Prompt Format) 与安全护栏
- 张量并行 (Tensor Parallelism) 原理与 fairscale 集成
- KV Cache 缓存机制详解
- 旋转位置编码 (RoPE) 完整推导
- RMSNorm 与 SwiGLU 为什么有效
- 生产部署:从裸机到大规模集群
- 性能优化:吞吐、显存、延迟三角平衡
- 微调与 LoRA 实战
- 量化方案选型 (GPTQ/AWQ/GGUF/FP8)
- 从参考实现迁移到生产栈 (vLLM/TGI/TensorRT-LLM)
- 监控、评测与安全
- 典型问题与踩坑排查
- 生态演化路线图
一、仓库结构总览
llama/
├── llama/ # 核心包,仅三个 .py 文件
│ ├── __init__.py # 导出 Llama, Dialog, ModelArgs, Transformer, Tokenizer
│ ├── model.py (496行) # Transformer 全部层定义
│ ├── generation.py (422行) # 权重加载、文本生成、对话封装
│ └── tokenizer.py ( 69行) # SentencePiece BPE 包装
├── example_text_completion.py # 文本补全 demo
├── example_chat_completion.py # 对话 demo (含多轮与 system prompt)
├── download.sh # 签名 URL 下载模型权重
├── requirements.txt # torch / fairscale / fire / sentencepiece
├── setup.py
├── MODEL_CARD.md
├── Responsible-Use-Guide.pdf
├── USE_POLICY.md
└── README.md
设计取向
- 最小化:无训练代码、无量化、无 Flash-Attention、无 paged KV、无 continuous batching。刻意呈现"教科书级"的结构,便于研究者二次实现。
- 张量并行优先:直接使用
fairscale的ColumnParallelLinear/RowParallelLinear/ParallelEmbedding,支持 70B 在 8×A100 / 8×H100 上多卡切分。 - 推理专用:
forward装饰为@torch.inference_mode(),并内置静态 KV Cache。不能直接用于训练(静态 cache、无 dropout、无 autograd 友好路径)。
二、核心架构:Decoder-Only Transformer
Llama 2 属于标准 Decoder-Only 因果自回归 Transformer,相比原始 "Attention Is All You Need" 做了以下关键改造:
| 组件 | 原始 Transformer | Llama 2 |
|---|---|---|
| 归一化 | LayerNorm (后置) | RMSNorm (前置 Pre-Norm) |
| 激活函数 | ReLU / GELU | SwiGLU (带 gating) |
| 位置编码 | 绝对位置 / 正弦编码 | RoPE 旋转位置编码 |
| 注意力 | MHA (n_heads Q/K/V) | GQA (n_kv_heads ≤ n_heads,70B 使用) |
| 偏置项 | 有 bias | 全部 bias=False |
| 精度 | fp32 | 默认 fp16(torch.cuda.HalfTensor) |
模型尺寸参数(对照 download.sh 与典型 params.json)
| 模型 | n_layers | dim | n_heads | n_kv_heads | ffn_mult | MP (TP 分片数) |
|---|---|---|---|---|---|---|
| 7B | 32 | 4096 | 32 | 32 | — | 1 |
| 13B | 40 | 5120 | 40 | 40 | — | 2 |
| 70B | 80 | 8192 | 64 | 8 | 1.3 | 8 |
注意:70B 的
n_kv_heads=8是 Grouped-Query Attention (GQA),每 8 个 Q head 共享 1 组 KV,直接把 KV Cache 显存降到 MHA 的 1/8,这是 70B 能在推理时落地的关键设计。
整体前向数据流 (model.py:456-495)
tokens (B, T)
│
▼
ParallelEmbedding ─────────────► h (B, T, dim) [跨 MP rank 分片]
│
▼
for layer in layers: ── TransformerBlock x N_layers
│ ┌─────────────────────────────────────┐
│ │ h' = attention(RMSNorm(h), …) │ Pre-Norm + 残差
│ │ h = h + h' │
│ │ f' = feed_forward(RMSNorm(h)) │
│ │ h = h + f' │
│ └─────────────────────────────────────┘
│
▼
RMSNorm(h)
│
▼
ColumnParallelLinear (output head, dim → vocab_size)
│
▼
logits (B, T, vocab_size) [.float() 提升精度避免 softmax 溢出]
三、model.py 逐模块深度剖析
3.1 ModelArgs — 模型超参数 (model.py:19-31)
@dataclass
class ModelArgs:
dim: int = 4096
n_layers: int = 32
n_heads: int = 32
n_kv_heads: Optional[int] = None # None → 退化为 MHA
vocab_size: int = -1 # 由 tokenizer 填入
multiple_of: int = 256 # FFN 隐藏维度对齐(便于张量核心 & TP 分片)
ffn_dim_multiplier: Optional[float] = None
norm_eps: float = 1e-5
max_batch_size: int = 32 # KV Cache 预分配用
max_seq_len: int = 2048 # KV Cache 预分配用
关键陷阱:max_batch_size × max_seq_len 决定了 每层 KV Cache 的静态预分配大小:
cache_k shape = (max_batch_size, max_seq_len, n_local_kv_heads, head_dim) × fp16
以 70B (80 层,n_kv_heads=8,head_dim=128,fp16) 为例,若 max_batch_size=32, max_seq_len=4096:
每层 KV Cache = 32 × 4096 × 8 × 128 × 2 bytes × 2 (K 和 V) ≈ 512 MB
80 层 × 512 MB = 40 GB ← 这部分显存被静态占用,即使 batch=1 也全量分配
这就是为什么官方参考实现不适合生产:它的静态 KV Cache 会严重浪费显存。生产级 vLLM 的 PagedAttention 直接解决这个痛点。
3.2 RMSNorm (model.py:34-77)
def _norm(self, x):
return x * torch.rsqrt(x.pow(2).mean(-1, keepdim=True) + self.eps)
def forward(self, x):
output = self._norm(x.float()).type_as(x) # 关键:fp32 计算再转回原 dtype
return output * self.weight # 可学习 scale,初始化为 1
对比 LayerNorm:
LayerNorm : y = (x - μ) / sqrt(σ² + ε) · γ + β
RMSNorm : y = x / RMS(x) · γ (无减均值,无 β)
为什么 Llama 选 RMSNorm?
- 更快:省去减均值与偏置加法。RMSNorm 在 GPU 上约比 LayerNorm 快 7-15%。
- 经验上效果相当:Zhang & Sennrich (2019) 表明减均值对 Transformer 表现非关键。
- 数值更稳:少一个减法步骤,减少 fp16 下的精度损失。
实现细节:x.float() 把输入 upcast 到 fp32 计算归一化再 downcast,避免 fp16 下平方项溢出。这是非常容易被忽略的生产级数值稳定技巧。
3.3 precompute_freqs_cis + apply_rotary_emb — RoPE (model.py:80-161)
def precompute_freqs_cis(dim, end, theta=10000.0):
freqs = 1.0 / (theta ** (torch.arange(0, dim, 2)[: dim // 2].float() / dim))
t = torch.arange(end, device=freqs.device)
freqs = torch.outer(t, freqs).float() # (end, dim//2)
freqs_cis = torch.polar(torch.ones_like(freqs), freqs) # e^{iθ}
return freqs_cis
theta=10000是 RoPE 基频(Llama 3 起改用500000.0以支持长上下文 128k)。torch.polar(r=1, θ=freqs)构造复数cos(θ) + i·sin(θ),数据类型complex64。- 预计算一次,后续每次 forward 只切片
[start_pos : start_pos + seqlen],几乎零开销。
应用:
xq_ = torch.view_as_complex(xq.reshape(..., -1, 2)) # (..., head_dim/2) complex
xk_ = torch.view_as_complex(xk.reshape(..., -1, 2))
xq_out = torch.view_as_real(xq_ * freqs_cis).flatten(3)
xk_out = torch.view_as_real(xk_ * freqs_cis).flatten(3)
这是把每对相邻维度 (x_{2i}, x_{2i+1}) 当作一个 2D 向量旋转 m·θ_i 角度。详见第 9 节推导。
3.4 repeat_kv — GQA 支持 (model.py:164-173)
def repeat_kv(x, n_rep):
bs, slen, n_kv_heads, head_dim = x.shape
if n_rep == 1:
return x
return x[:, :, :, None, :].expand(bs, slen, n_kv_heads, n_rep, head_dim)\
.reshape(bs, slen, n_kv_heads * n_rep, head_dim)
将 n_kv_heads 个 K/V head 沿 head 维度广播到 n_heads,使得 MHA 的 scaled-dot-product 计算路径不用改。expand 是零拷贝视图操作,仅 reshape 会产生连续化开销。
3.5 Attention 模块 (model.py:176-304)
核心流程(逐步):
-
投影并拆头
xq, xk, xv = self.wq(x), self.wk(x), self.wv(x) xq = xq.view(bsz, seqlen, n_local_heads, head_dim) xk = xk.view(bsz, seqlen, n_local_kv_heads, head_dim) xv = xv.view(bsz, seqlen, n_local_kv_heads, head_dim)n_local_heads = n_heads / model_parallel_size:每个 rank 只算一部分 head。 -
RoPE 注入:
xq, xk = apply_rotary_emb(xq, xk, freqs_cis)V 不旋转。位置信息只需编码到 Q 和 K 中,attention score 自然带上相对位置偏置。
-
写入 KV Cache
self.cache_k[:bsz, start_pos : start_pos + seqlen] = xk self.cache_v[:bsz, start_pos : start_pos + seqlen] = xv keys = self.cache_k[:bsz, : start_pos + seqlen] values = self.cache_v[:bsz, : start_pos + seqlen]核心观察:prefill 阶段 seqlen=prompt_len,decode 阶段 seqlen=1。KV Cache 让第 N+1 个 token 的生成只需 O(N) 而不是 O(N²)。
-
GQA 广播:
keys = repeat_kv(keys, n_rep) -
Scaled Dot-Product Attention
scores = torch.matmul(xq, keys.transpose(2, 3)) / sqrt(head_dim) if mask is not None: scores = scores + mask scores = F.softmax(scores.float(), dim=-1).type_as(xq) # fp32 softmax output = torch.matmul(scores, values)注意:softmax 在 fp32 下计算,避免 fp16 溢出到 inf。这是 LLM 推理的标准做法。
-
输出投影:
self.wo(output)——RowParallelLinear,自动完成 all-reduce 合并不同 rank 的 head 结果。
3.6 FeedForward — SwiGLU (model.py:307-348)
hidden_dim = int(2 * hidden_dim / 3) # 标准化 FFN 维度
if ffn_dim_multiplier is not None:
hidden_dim = int(ffn_dim_multiplier * hidden_dim)
hidden_dim = multiple_of * ((hidden_dim + multiple_of - 1) // multiple_of) # 对齐 256
def forward(self, x):
return self.w2(F.silu(self.w1(x)) * self.w3(x))
公式:FFN(x) = W₂ · (SiLU(W₁x) ⊙ W₃x)
SiLU(x) = x · σ(x),也叫 Swish。⊙是逐元素乘,W₃x充当 门控 (gate)。- 相比标准
FFN(x) = W₂ · ReLU(W₁x),参数量增加 50% 但表达力更强,是 Llama 家族标配。
为什么 hidden_dim = 2·hidden_dim/3? 原公式 hidden_dim = 4·dim(GPT-2 标准),SwiGLU 多一个 w3 矩阵,为了参数量与原版接近,缩放到 2/3。对 dim=4096 → 2/3 × 4×4096 = 10922,对齐 256 后 = 11008。
3.7 TransformerBlock (model.py:351-410)
def forward(self, x, start_pos, freqs_cis, mask):
h = x + self.attention(self.attention_norm(x), start_pos, freqs_cis, mask)
out = h + self.feed_forward(self.ffn_norm(h))
return out
Pre-Norm 结构:Norm 在 Attention/FFN 之前,残差连接直通。相比 Post-Norm 训练更稳定,深层模型(80 层)几乎必须用 Pre-Norm。
3.8 Transformer 顶层 (model.py:413-495)
两个细节值得关注:
-
freqs_cis预分配到max_seq_len * 2:允许运行时比训练时更长的上下文(外推)。Llama 2 官方是 4096,所以这里预分配到 8192。 -
Causal mask 的高效构造 (model.py:474-489):
if seqlen > 1: mask = torch.triu(torch.full((seqlen, seqlen), -inf), diagonal=1) mask = torch.hstack([torch.zeros((seqlen, start_pos)), mask])- Prefill(seqlen > 1):三角下三角可见的因果掩码。
- Decode(seqlen = 1):
mask = None,跳过整个逻辑 —— 当前 token 本来就要看全部历史。 hstack前置一块全零的(seqlen, start_pos):历史 KV 全部可见,新 tokens 之间才应用因果约束。这是 带 KV Cache 的 Prefill-While-Resuming 场景(不常见,但代码完整支持)。
四、generation.py 推理引擎分析
4.1 Llama.build — 权重加载 (generation.py:52-123)
if not torch.distributed.is_initialized():
torch.distributed.init_process_group("nccl")
if not model_parallel_is_initialized():
initialize_model_parallel(model_parallel_size or int(os.environ["WORLD_SIZE"]))
local_rank = int(os.environ.get("LOCAL_RANK", 0))
torch.cuda.set_device(local_rank)
torch.manual_seed(seed) # 所有 rank 同种子,确保采样一致
if local_rank > 0:
sys.stdout = open(os.devnull, "w") # 只有 rank 0 打印
checkpoints = sorted(Path(ckpt_dir).glob("*.pth"))
assert model_parallel_size == len(checkpoints) # MP 必须等于分片数
ckpt_path = checkpoints[get_model_parallel_rank()] # 每个 rank 加载自己的分片
checkpoint = torch.load(ckpt_path, map_location="cpu")
torch.set_default_tensor_type(torch.cuda.HalfTensor) # 后续 tensor 默认 fp16 + GPU
model = Transformer(model_args)
model.load_state_dict(checkpoint, strict=False)
要点
- Llama 权重已经按 MP 预分片保存为
consolidated.00.pth~consolidated.NN.pth。你不能用 MP=1 加载 70B(它有 8 个分片)。 strict=False容忍缺失的freqs_cisbuffer(预计算生成,不从 ckpt 加载)。- 必须通过
torchrun --nproc_per_node={MP}启动,否则会在init_process_group卡死。
4.2 generate — 核心采样循环 (generation.py:129-231)
bsz = len(prompt_tokens)
min_prompt_len = min(len(t) for t in prompt_tokens)
max_prompt_len = max(len(t) for t in prompt_tokens)
total_len = min(params.max_seq_len, max_gen_len + max_prompt_len)
tokens = torch.full((bsz, total_len), pad_id, dtype=torch.long, device="cuda")
for k, t in enumerate(prompt_tokens):
tokens[k, : len(t)] = torch.tensor(t, dtype=torch.long)
prev_pos = 0
input_text_mask = tokens != pad_id
for cur_pos in range(min_prompt_len, total_len):
logits = self.model.forward(tokens[:, prev_pos:cur_pos], prev_pos)
if temperature > 0:
probs = torch.softmax(logits[:, -1] / temperature, dim=-1)
next_token = sample_top_p(probs, top_p)
else:
next_token = torch.argmax(logits[:, -1], dim=-1)
next_token = torch.where(
input_text_mask[:, cur_pos], tokens[:, cur_pos], next_token
) # prompt 区域保留原 token,仅填充 pad 区域
tokens[:, cur_pos] = next_token
eos_reached |= (~input_text_mask[:, cur_pos]) & (next_token == self.tokenizer.eos_id)
prev_pos = cur_pos
if all(eos_reached):
break
非常关键的设计:"Left-Padded Prefill + Shared Batch Decode"
- 所有 prompt 被统一 pad 到
total_len,同一个 batch 里短 prompt 后面是 pad。 - 第一次
forward(tokens[:, 0:min_prompt_len], 0)对齐所有 prompt 的共同前缀做 prefill。 - 随后每次 forward 只传一个 slot:
tokens[:, prev_pos:cur_pos](宽度=1),这是典型的 autoregressive decode。 torch.where(input_text_mask[:, cur_pos], ...)确保短 prompt "慢慢结束 prefill" 的阶段不会覆盖真实输入 token。
缺陷
- 无 continuous batching:一个 batch 中最慢的序列拖累所有其他序列。假设 batch 里 7 条已输出 EOS,剩 1 条还在生成,另外 7 个 slot 空转。
- 静态 KV Cache:无法中途 swap 新请求进来。
- 这些缺陷 vLLM 的 PagedAttention + continuous batching 完美解决。
4.3 sample_top_p — 核采样 (generation.py:398-421)
probs_sort, probs_idx = torch.sort(probs, dim=-1, descending=True)
probs_sum = torch.cumsum(probs_sort, dim=-1)
mask = probs_sum - probs_sort > p # 累积概率 > p 的位置被屏蔽(注意减去当前项)
probs_sort[mask] = 0.0
probs_sort.div_(probs_sort.sum(dim=-1, keepdim=True)) # 重归一化
next_token = torch.multinomial(probs_sort, num_samples=1)
next_token = torch.gather(probs_idx, -1, next_token)
陷阱:probs_sum - probs_sort > p,注意是减 probs_sort[i](当前项概率)再比较,保证至少一个 token 进入候选(否则 p 极小时可能全被屏蔽)。
4.4 text_completion 与 chat_completion 对比
text_completion | chat_completion | |
|---|---|---|
| 输入 | 纯文本 List[str] | List[Dialog](Dialog = List[Message]) |
| 编码 | encode(x, bos=True, eos=False) | 复杂多轮模板(见下节) |
| 返回 | {"generation": str} | {"generation": {"role": "assistant", "content": str}} |
| 不安全输入检查 | 无 | UNSAFE_ERROR 直接替换输出 |
五、tokenizer.py 分词器剖析
class Tokenizer:
def __init__(self, model_path):
self.sp_model = SentencePieceProcessor(model_file=model_path)
self.n_words = self.sp_model.vocab_size() # Llama 2: 32000
self.bos_id = self.sp_model.bos_id() # 1
self.eos_id = self.sp_model.eos_id() # 2
self.pad_id = self.sp_model.pad_id() # -1(通常未定义)
Llama 2 tokenizer 特征
- BPE + SentencePiece。
- vocab_size = 32000(Llama 3 起用 tiktoken,
vocab_size=128256)。 pad_id = -1:没有专门的 pad token,推理时用-1作 sentinel。代码中tokens = torch.full((bsz, total_len), pad_id, ...)会出问题吗?不会 ——torch.where(input_text_mask[:, cur_pos], ...)会保证 pad 位置被替换为真实生成。- 不支持 中文高效编码(一个汉字常被拆成 2-3 tokens)。这是 Llama 2 中文能力受限的底层原因之一。Llama 3 词表扩大到 128k 后大幅改善。
六、对话模板 (Prompt Format) 与安全护栏
6.1 模板常量 (generation.py:44-48)
B_INST, E_INST = "[INST]", "[/INST]"
B_SYS, E_SYS = "<<SYS>>\n", "\n<</SYS>>\n\n"
SPECIAL_TAGS = [B_INST, E_INST, "<<SYS>>", "<</SYS>>"]
UNSAFE_ERROR = "Error: special tags are not allowed as part of the prompt."
6.2 多轮拼接逻辑 (generation.py:318-361)
对于 dialog = [system?, user, assistant, user, ..., user]:
-
system 合并:若首条为 system,把
<<SYS>>\n{sys}\n<</SYS>>\n\n拼到下一条 user 前,删除 system 消息本身。 -
逐对 (user, assistant) 编码:每对都独立加 bos/eos。
dialog_tokens = sum([ encode(f"{B_INST} {user_i} {E_INST} {asst_i} ", bos=True, eos=True) for (user_i, asst_i) in zip(dialog[::2], dialog[1::2]) ], []) -
最后一条 user:只加 bos,不加 eos,让模型续写。
dialog_tokens += encode(f"{B_INST} {last_user} {E_INST}", bos=True, eos=False)
最终格式(单轮例子)
<s>[INST] <<SYS>>
You are helpful.
<</SYS>>
What is 2+2? [/INST]
6.3 Prompt Injection 防御
unsafe_requests.append(
any([tag in msg["content"] for tag in SPECIAL_TAGS for msg in dialog])
)
用户消息中若含 [INST]、[/INST]、<<SYS>>、<</SYS>>,整个生成结果被替换为 UNSAFE_ERROR。这是 最朴素的 prompt injection 防御 —— 防止用户伪装成系统或注入对话边界。生产环境应叠加 Llama Guard 等专门模型。
七、张量并行 (Tensor Parallelism) 原理与 fairscale 集成
7.1 三类并行原语
| 类型 | 作用 | 典型用法 |
|---|---|---|
ColumnParallelLinear | 按输出维度切分,本 rank 只产一部分输出 | Q/K/V 投影、FFN w1/w3、lm_head |
RowParallelLinear | 按输入维度切分,本 rank 只吃一部分输入,输出 all-reduce | attention.wo、FFN w2 |
ParallelEmbedding | 按 vocab 维度切分 embedding | tok_embeddings |
7.2 Attention 里的 TP 对称性
wq, wk, wv → ColumnParallelLinear (gather_output=False)
│
▼ 每个 rank 只产 n_local_heads 的 Q/K/V,独立计算 attention
▼
wo → RowParallelLinear (input_is_parallel=True)
│
▼ 自动 all-reduce 把分散的 head 输出合并回完整 hidden_state
这样每层只做 一次 all-reduce(在 wo 输出),通信开销最小化。FFN 同理:w1/w3 列切分 → 逐元素 SwiGLU → w2 行切分 all-reduce。
7.3 启动命令
# 7B (MP=1)
torchrun --nproc_per_node 1 example_chat_completion.py \
--ckpt_dir llama-2-7b-chat/ \
--tokenizer_path tokenizer.model \
--max_seq_len 512 --max_batch_size 6
# 13B (MP=2)
torchrun --nproc_per_node 2 example_chat_completion.py ...
# 70B (MP=8)
torchrun --nproc_per_node 8 example_chat_completion.py ...
--nproc_per_node 必须等于权重分片数(len(checkpoints))。
八、KV Cache 缓存机制详解
8.1 为什么需要 KV Cache
生成第 N+1 个 token 时,naive 做法是把 tokens[0:N+1] 全部传入 forward,做 N² 的 attention。这是 O(N²) 工作量 × 每一步 → 总复杂度 O(N³)。
KV Cache 做法:
- 第 N 步结束时,每层的 K[0:N], V[0:N] 已被缓存。
- 第 N+1 步只需对新 token 计算 Q, K, V,追加到 cache,做一次 O(N) 的 attention。
- 总复杂度降到 O(N²)。对长上下文(N=2000+)提速 10-100 倍。
8.2 源码实现
# 构造时
self.cache_k = torch.zeros((max_batch_size, max_seq_len, n_local_kv_heads, head_dim)).cuda()
self.cache_v = torch.zeros((max_batch_size, max_seq_len, n_local_kv_heads, head_dim)).cuda()
# forward 中
self.cache_k[:bsz, start_pos : start_pos + seqlen] = xk
keys = self.cache_k[:bsz, : start_pos + seqlen] # 读取历史 + 当前
8.3 显存占用公式
KV Cache = 2 × batch × seq_len × n_kv_heads × head_dim × dtype_bytes × n_layers
Llama 2 70B(n_kv_heads=8,head_dim=128,n_layers=80,fp16),seq_len=4096,batch=32:
2 × 32 × 4096 × 8 × 128 × 2 × 80 = 42.9 GB (MP=1)
= 5.4 GB / 每卡 (MP=8)
再加 70B × 2 bytes = 140 GB 权重 / 8 = 17.5 GB 权重 / 卡。总共每卡约 23 GB,可以装进 80GB A100/H100 且留足运行时 buffer。
8.4 生产化改进方向
- PagedAttention (vLLM):把 KV Cache 按 page 管理,实现动态 batch 与请求抢占。
- FlashAttention-2:融合 kernel,减少 KV Cache 读写带宽。
- Sliding Window:Mistral 风格,只保留最近 W tokens。
- 量化 KV Cache:int8/int4 存储,4× 显存节省。
九、旋转位置编码 (RoPE) 完整推导
9.1 直觉
RoPE 的目标:让两个 token 的 attention score 只依赖它们的 相对位置 m - n,而非绝对位置。
9.2 数学形式
对 query q_m(位置 m),把它每对相邻维度 (q_m^{2i}, q_m^{2i+1}) 看作二维向量,逆时针旋转 mθ_i:
R_m^(i) = [ cos(mθ_i) -sin(mθ_i) ]
[ sin(mθ_i) cos(mθ_i) ]
θ_i = 10000^(-2i/d) # 不同维度用不同频率
旋转后:q'_m = R_m · q_m,k'_n = R_n · k_n。
注意力分数:
<q'_m, k'_n> = q_m^T · R_m^T · R_n · k_n
= q_m^T · R_{n-m} · k_n (旋转矩阵性质)
只依赖相对位置 n-m!
9.3 复数表达(源码实现)
把每对 (a, b) 写成复数 a + bi,旋转 θ 就是乘 e^{iθ} = cos θ + i sin θ:
freqs_cis = torch.polar(torch.ones_like(freqs), freqs) # e^{iθ}
xq_ = torch.view_as_complex(xq.reshape(..., -1, 2)) # 把 2 个 float 合成一个 complex
xq_out = torch.view_as_real(xq_ * freqs_cis).flatten(3) # 复数乘法 = 2D 旋转
9.4 长上下文外推
默认 theta=10000,Llama 3 改用 theta=500000 配合 YaRN、NTK-scaling 等技巧,把上下文扩展到 128k-1M。
十、RMSNorm 与 SwiGLU 为什么有效
RMSNorm
- 去掉减均值:实证上对 Transformer 表现无害,训练更快。
- 去掉 bias
β:简化,避免一个可学习参数。 - 数值稳定:
x.float()upcast 后再做平方和,避免 fp16 溢出。
SwiGLU (Shazeer 2020, GLU Variants Improve Transformer)
- Gating 机制:
SiLU(W₁x) ⊙ W₃x的逐元素乘让网络学会 "选择性放大"。 - 平滑激活 (SiLU):
x · σ(x),在 x=0 处平滑过渡,梯度更好。 - 表达力:参数量增 50%,但相同 FLOP 下表现显著优于 ReLU/GELU。
- 在 PaLM、Llama、Mistral、Qwen 等主流模型中都是默认激活。
十一、生产部署:从裸机到大规模集群
11.1 最小可行部署(7B,单卡)
# 1. 硬件需求:≥ 16 GB VRAM (fp16) or ≥ 8 GB (int4)
# 2. 环境
conda create -n llama python=3.10 -y
conda activate llama
pip install torch --index-url https://download.pytorch.org/whl/cu121
git clone https://github.com/meta-llama/llama.git
cd llama && pip install -e .
# 3. 下载权重(需向 Meta 申请签名 URL)
./download.sh
# 4. 运行
torchrun --nproc_per_node 1 example_chat_completion.py \
--ckpt_dir llama-2-7b-chat/ \
--tokenizer_path tokenizer.model \
--max_seq_len 2048 --max_batch_size 4
11.2 70B 多卡部署(8×A100 80GB)
# 关键:MP=8,所以 --nproc_per_node=8,并且节点内所有 GPU 走 NVLink
torchrun --nproc_per_node 8 example_chat_completion.py \
--ckpt_dir llama-2-70b-chat/ \
--tokenizer_path tokenizer.model \
--max_seq_len 4096 --max_batch_size 8
确保:
- NCCL 后端:检查
NCCL_DEBUG=INFO输出,确认 NVLink 使用。 - 共享内存:
ulimit -l unlimited;Docker 用--shm-size=8g。 - PCIe / NVLink 拓扑:
nvidia-smi topo -m查看,跨 NUMA 性能急剧下降。
11.3 多节点部署(跨机 70B、175B+)
# Node 0 (master)
NODE_RANK=0 WORLD_SIZE=16 MASTER_ADDR=10.0.0.1 MASTER_PORT=29500 \
torchrun --nnodes=2 --nproc_per_node=8 --node_rank=0 \
--master_addr=10.0.0.1 --master_port=29500 \
example_chat_completion.py ...
# Node 1 (worker)
NODE_RANK=1 WORLD_SIZE=16 MASTER_ADDR=10.0.0.1 MASTER_PORT=29500 \
torchrun --nnodes=2 --nproc_per_node=8 --node_rank=1 \
--master_addr=10.0.0.1 --master_port=29500 \
example_chat_completion.py ...
节点间需 InfiniBand / RoCE 100Gbps+,否则 all-reduce 会成为瓶颈。
11.4 Docker + K8s 生产部署
# Dockerfile
FROM nvidia/cuda:12.1.0-runtime-ubuntu22.04
RUN apt-get update && apt-get install -y python3.10 python3-pip git
WORKDIR /app
COPY requirements.txt .
RUN pip install torch --index-url https://download.pytorch.org/whl/cu121 \
&& pip install -r requirements.txt
COPY . .
RUN pip install -e .
ENV NCCL_DEBUG=WARN
ENTRYPOINT ["torchrun"]
Kubernetes Job 用 Volcano 或 Kubeflow Training Operator 做 gang scheduling,确保所有 MP pod 同时启动。
11.5 推荐生产方案(不自己用参考实现)
这份代码是 教学用,生产强烈建议切换到:
- vLLM(吞吐之王,PagedAttention + continuous batching)
- TensorRT-LLM(NVIDIA 官方极致优化,FP8/INT4/Speculative)
- SGLang(新一代,RadixAttention 前缀缓存最优)
- Text Generation Inference (TGI)(HF 官方,生态友好)
- llama.cpp(纯 C++,GGUF 量化,CPU/边缘最佳)
十二、性能优化:吞吐、延迟、显存三角平衡
12.1 三大指标定义
| 指标 | 定义 | 优化方向 |
|---|---|---|
| TTFT (Time To First Token) | 从请求到首 token 的延迟 | Prefill 并行度、attention kernel |
| TPOT (Time Per Output Token) | 稳态单 token 延迟 | KV Cache 带宽、矩阵乘效率 |
| Throughput (tokens/sec) | 并发下总吞吐 | batch size、continuous batching |
12.2 显存优化对照表
| 手段 | 7B 显存 | 备注 |
|---|---|---|
| fp32 | 28 GB | 几乎不用 |
| fp16/bf16 | 14 GB | 官方默认 |
| int8 (LLM.int8, SmoothQuant) | 7 GB | 精度损失 < 1% |
| GPTQ 4bit | 3.5 GB | 精度损失 1-3% |
| AWQ 4bit | 3.5 GB | 比 GPTQ 更精细的显著权重保护 |
| GGUF Q4_K_M (llama.cpp) | 4 GB | 边缘/CPU 友好 |
| GGUF Q2_K | 2.7 GB | 极限压缩,精度明显损失 |
12.3 延迟优化(按影响排序)
- Flash-Attention 2/3:attention 从 HBM memory-bound 优化到 compute-bound,2-4× 加速。
- Continuous Batching:请求动态加入/退出 batch,GPU 利用率从 20% → 90%+。
- Speculative Decoding:小模型(Draft Model)生成多 token,大模型验证,2-3× TPOT 提升。
- Tensor Parallelism within node:NVLink 低延迟 all-reduce。
- Pipeline Parallelism across node:多机下用,吞吐友好但延迟略增。
- 量化:int4 / FP8 降低带宽。
12.4 吞吐优化(Benchmarks)
在 8×A100 80GB 上,Llama 2 70B-chat 典型吞吐:
| 推理框架 | tokens/sec(并发 32,seq=512) |
|---|---|
| 官方参考实现 | ~40 tok/s |
| HF Transformers | ~80 tok/s |
| vLLM | ~2000 tok/s (50×) |
| TensorRT-LLM (FP8) | ~3200 tok/s (80×) |
官方参考实现的绝对性能垫底,因为没有 Flash-Attn、没有 CUDA graph、没有 continuous batching。
十三、微调与 LoRA 实战
本仓库不含训练代码,微调请用社区方案:
13.1 LoRA(Low-Rank Adaptation)原理
冻结原权重 W,为每个线性层注入两个低秩矩阵 A ∈ R^{r×d}, B ∈ R^{d×r}:
W' · x = W · x + α/r · B · A · x
r=8~64,可学习参数量降到原模型 0.1-1%。- 70B LoRA 单卡 80GB 可微调,A100 就足够。
13.2 QLoRA(4bit + LoRA)
- 原权重
W用 NF4(Normal Float 4-bit)量化加载。 - 只 LoRA 权重 fp16 训练。
- 65B 可在单卡 48GB 微调。
13.3 用 peft + trl 实战(推荐)
from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
from peft import LoraConfig, get_peft_model
from trl import SFTTrainer
bnb = BitsAndBytesConfig(load_in_4bit=True, bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype="float16")
model = AutoModelForCausalLM.from_pretrained("meta-llama/Llama-2-7b-hf",
quantization_config=bnb)
tokenizer = AutoTokenizer.from_pretrained("meta-llama/Llama-2-7b-hf")
lora_cfg = LoraConfig(r=16, lora_alpha=32, target_modules=["q_proj","v_proj"],
lora_dropout=0.05, bias="none", task_type="CAUSAL_LM")
model = get_peft_model(model, lora_cfg)
trainer = SFTTrainer(model=model, tokenizer=tokenizer,
train_dataset=dataset, dataset_text_field="text",
max_seq_length=2048, ...)
trainer.train()
13.4 官方 checkpoint 格式 ↔ HuggingFace 转换
# 官方格式 → HF 格式
python transformers/src/transformers/models/llama/convert_llama_weights_to_hf.py \
--input_dir /path/to/llama-2-7b \
--model_size 7B \
--output_dir /path/to/llama-2-7b-hf
绝大多数生态(vLLM、TGI、TensorRT-LLM、SGLang)都消费 HF 格式,生产建议第一步就转换。
十四、量化方案选型 (GPTQ/AWQ/GGUF/FP8)
| 方案 | 压缩率 | 精度损失 | 推理框架 | 最适合场景 |
|---|---|---|---|---|
| GPTQ | 4× | 小 | vLLM / TGI / AutoGPTQ | GPU 通用 |
| AWQ | 4× | 极小 | vLLM / TGI / TensorRT-LLM | GPU,对精度敏感 |
| GGUF (llama.cpp) | 2-8× | 视阶数 | llama.cpp / Ollama | CPU / 边缘 / Mac M 系列 |
| FP8 (E4M3/E5M2) | 2× | 极小 | TensorRT-LLM / H100 | 最高性能生产 |
| SmoothQuant (int8 W+A) | 2× | 极小 | TensorRT-LLM | 长上下文 |
| HQQ (半量化) | 4-8× | 小 | 自定义 | 实验性 |
生产推荐路径
- 有 H100 → FP8 (TensorRT-LLM)
- 只有 A100/A10 → AWQ 4bit (vLLM)
- CPU/Mac/边缘 → GGUF Q4_K_M (llama.cpp / Ollama)
十五、从参考实现迁移到生产栈
15.1 vLLM(推荐首选)
pip install vllm
from vllm import LLM, SamplingParams
llm = LLM(model="meta-llama/Llama-2-70b-chat-hf", tensor_parallel_size=8,
gpu_memory_utilization=0.9, max_model_len=4096)
sampling = SamplingParams(temperature=0.6, top_p=0.9, max_tokens=512)
outputs = llm.generate(["你好,介绍一下你自己"], sampling)
print(outputs[0].outputs[0].text)
或启动 OpenAI 兼容 API:
vllm serve meta-llama/Llama-2-7b-chat-hf --tensor-parallel-size 1 --port 8000
15.2 Text Generation Inference (TGI)
docker run --gpus all --shm-size 1g -p 8080:80 \
-v /models:/data ghcr.io/huggingface/text-generation-inference:latest \
--model-id meta-llama/Llama-2-7b-chat-hf \
--num-shard 1 --max-input-length 2048 --max-total-tokens 4096
15.3 TensorRT-LLM(极致性能)
# 1. 转换 HF 权重到 TRT engine
python convert_checkpoint.py --model_dir llama-2-70b-hf \
--output_dir ./trt_ckpt --dtype float16 --tp_size 8
# 2. build engine(FP8)
trtllm-build --checkpoint_dir ./trt_ckpt --gemm_plugin float16 \
--output_dir ./engines --use_fp8
# 3. 运行
mpirun -n 8 python run.py --engine_dir ./engines --tokenizer_dir ./llama-2-70b-hf
15.4 llama.cpp(最轻量)
# 1. 转 GGUF
python convert.py /path/to/llama-2-7b-hf
./quantize /path/to/llama-2-7b.gguf /path/to/llama-2-7b-q4.gguf Q4_K_M
# 2. 运行(CPU or CUDA or Metal)
./server -m llama-2-7b-q4.gguf -c 4096 -np 4 --port 8080
十六、监控、评测与安全
16.1 关键指标监控
# Prometheus 暴露项(vLLM 内置支持 /metrics)
vllm:request_latency_seconds # p50/p95/p99 端到端延迟
vllm:time_to_first_token_seconds # TTFT
vllm:time_per_output_token_seconds # TPOT
vllm:request_queue_time_seconds # 排队
vllm:gpu_cache_usage_perc # KV cache 使用率
vllm:num_requests_running # 当前并发
vllm:num_requests_waiting # 排队深度
16.2 质量评测
| 数据集 | 评测能力 |
|---|---|
| MMLU | 5-shot 综合知识 |
| GSM8K | 数学推理 |
| HumanEval | 代码 |
| ARC | 常识 |
| TruthfulQA | 事实性 |
| HellaSwag | 常识补全 |
| C-Eval / CMMLU | 中文综合 |
16.3 安全护栏栈
用户输入
│
▼
┌──────────────────────┐
│ 1. 输入过滤 │ 关键词、正则、Prompt Injection 检测
│ 2. Llama Guard │ 专门分类模型,判断 unsafe 类别
│ 3. Rate Limit / Auth │ 防滥用
└──────────────────────┘
│
▼
Llama 推理
│
▼
┌──────────────────────┐
│ 4. 输出过滤 │ PII、暴力、色情、歧视
│ 5. Llama Guard 2 │ 输出再过一遍 classifier
│ 6. Hallucination 检测 │ RAG 对照事实
└──────────────────────┘
│
▼
返回用户
十七、典型问题与踩坑排查
17.1 AssertionError: Loading a checkpoint for MP=8 but world size is 1
--nproc_per_node 必须等于权重分片数。70B 必须 MP=8。
17.2 OOM,即使 batch=1
max_batch_size × max_seq_len 预分配 KV Cache 一次性吃掉大量显存。把这两个降下来。
17.3 NCCL error: unhandled cuda error
- 检查
NCCL_P2P_DISABLE=0,NCCL_IB_DISABLE=0。 - 检查所有 GPU 同代(混代 A100+H100 会有兼容问题)。
nvidia-smi topo -m确认 NVLink 拓扑。
17.4 中文输出乱码
Llama 2 的 tokenizer 对 UTF-8 多字节字符 streaming decode 不友好。生产要用 tokenizer.decode(tokens, skip_special_tokens=True) 并按完整 token 组解码,或直接用 Llama 3(tiktoken-based)。
17.5 生成结果重复循环
- 提高
temperature(0.6 → 0.8)。 - 加
repetition_penalty(官方实现无此参数,vLLM / HF 都支持)。 - 加
frequency_penalty/presence_penalty。
17.6 多轮对话生成越来越慢
KV Cache 线性增长,attention 时间 O(N)。超过 max_seq_len 会溢出(Assert)。生产做法:
- 滚动 window 裁剪历史。
- 外挂 RAG 取代长期记忆。
- Llama 3 长上下文 128k 配合 PagedAttention。
17.7 pad_id=-1 产生 CUDA index out of bounds
某些历史 tokenizer.model 不设置 pad_id,代码里 torch.full(..., pad_id) 填入 -1,在 embedding lookup 时越界。解决:
if tokenizer.pad_id < 0:
tokenizer.pad_id = 0
或更换更新的 tokenizer.model。
十八、生态演化路线图
Llama 1 (2023.02)
│ + RMSNorm, RoPE, SwiGLU 奠基
▼
Llama 2 (2023.07) ★ 本仓库 ★
│ + GQA (70B), + 4k 上下文, + RLHF-chat
▼
Code Llama (2023.08) — 代码专用
│
▼
Llama 3 (2024.04)
│ + 词表 128k (tiktoken-based)
│ + 8k 上下文(后扩到 128k)
│ + Grouped-Query Attention 默认开启
│
▼
Llama 3.1 / 3.2 (2024.07-09)
│ + 128k 长上下文
│ + Multi-modal (Vision)
│ + Edge 版本(1B/3B)
│ + 405B MoE-candidate
│
▼
Llama 3.3 / 4 (2024.12 - 2025)
+ 更大规模 MoE
+ 原生多模态
+ 工具调用(Tool Use)原生支持
主仓库已拆分到:
- meta-llama/llama-models — 基础模型 + 工具
- meta-llama/PurpleLlama — 安全(Llama Guard)
- meta-llama/llama-toolchain — 训练/微调/推理统一接口
- meta-llama/llama-agentic-system — Agentic 栈
- meta-llama/llama-recipes — 社区食谱
本仓库作为入门与架构教学,价值长存;生产请使用 llama-models + vLLM/TGI/TensorRT-LLM。
附录 A:完整最小可运行 Demo(单卡 7B)
# demo.py (放在仓库根目录,torchrun 启动)
import torch
from llama import Llama
generator = Llama.build(
ckpt_dir="llama-2-7b-chat/",
tokenizer_path="tokenizer.model",
max_seq_len=512,
max_batch_size=4,
)
dialogs = [
[{"role": "system", "content": "你是一个简洁的助手,回答不超过 2 句话。"},
{"role": "user", "content": "解释什么是 RoPE?"}],
]
results = generator.chat_completion(dialogs, temperature=0.6, top_p=0.9, max_gen_len=256)
for r in results:
print(r["generation"]["content"])
torchrun --nproc_per_node 1 demo.py
附录 B:核心组件调用关系图
┌───────────────────────────┐
│ example_*.py 入口 │
└────────────┬──────────────┘
│ torchrun
▼
┌───────────────────────────┐
│ Llama.build() │
│ - init_process_group │
│ - initialize_model_parallel│
│ - load *.pth per rank │
└────────────┬──────────────┘
│
┌────────────────────┼────────────────────┐
▼ ▼ ▼
┌────────────┐ ┌───────────────┐ ┌─────────────┐
│ Tokenizer │ │ Transformer │ │ ModelArgs │
│(SentencePie│ │ (80 Blocks) │ │ params.json │
│ ce) │ └───────┬───────┘ └─────────────┘
└────────────┘ │
▼
┌─────────────────────┐
│ TransformerBlock │
│ ┌───────────────┐ │
│ │ Attention │ │ GQA + KV Cache + RoPE
│ │ (wq/wk/wv/wo)│ │
│ └───────────────┘ │
│ ┌───────────────┐ │
│ │ FeedForward │ │ SwiGLU
│ │ (w1/w2/w3) │ │
│ └───────────────┘ │
│ RMSNorm ×2 │
└─────────────────────┘
Llama.chat_completion / text_completion
│
▼
构造 prompt_tokens (apply chat template)
│
▼
Llama.generate()
│
├─ prefill: model.forward(prompt, start_pos=0)
│
└─ decode loop:
├─ model.forward(last_token, start_pos=cur_pos)
├─ softmax / top_p 采样
└─ 写 tokens[:, cur_pos],检查 EOS
结语
meta-llama/llama 这 900 行代码浓缩了现代 LLM 的核心设计:
- 架构层面:RMSNorm + Pre-Norm + RoPE + SwiGLU + GQA 成为事实标准。
- 系统层面:张量并行 + 静态 KV Cache 演化到生产级 PagedAttention + Continuous Batching。
- 工程层面:参考实现的"干净"让它成为所有二次实现的基准(llama.cpp、transformers、mlx-lm 都参考了它)。
理解这份代码,等于掌握了整个开源 LLM 生态的语法。 生产部署的下一步是 vLLM / TensorRT-LLM / llama.cpp,但回头看,所有优化的起点都是这份参考实现里展示的那几个核心概念。
"Read the source, Luke." —— 给所有想入门 LLM 基础设施的工程师。