GPTModel 总览
Megatron-LM 中的 GPT 模型由 GPTModel 类实现,定义在 megatron/core/models/gpt/gpt_model.py 中。整个模型的结构非常清晰,由三个核心组件纵向堆叠而成:
- Embedding -- 将 token ID 和位置信息映射到连续的隐藏空间向量,输出形状为
[s, b, h]。 - TransformerBlock (又称 decoder) -- 由 L 个
TransformerLayer堆叠而成,是模型的主体计算部分,参数量占比超过 95%。 - Output Layer -- 一个线性投影层,将隐藏向量映射回词表维度,产生 logits,形状为
[s, b, V]。
Megatron 采用 model provider 模式:用户代码(如 pretrain_gpt.py)定义一个 model_provider 函数,传入一个 TransformerConfig 和一个 layer spec,框架负责根据 spec 构建完整模型。这种设计将模型的声明(spec)与实例化(框架)解耦,使得切换底层实现(例如从纯 PyTorch 切换到 TransformerEngine)只需替换 spec,不改动模型代码。
[s, b]"] --> B["Embedding
word_embed + pos_embed
[s, b, h]"] B --> C["TransformerBlock
L x TransformerLayer
[s, b, h]"] C --> D["Output Layer
Linear(h, V)
[s, b, V]"] D --> E["Logits / Loss"] style B fill:#1f2937,stroke:#58a6ff,color:#e6edf3 style C fill:#1f2937,stroke:#3fb950,color:#e6edf3 style D fill:#1f2937,stroke:#d2a8ff,color:#e6edf3
| 组件 | 类名 | 源文件 | 关键参数 |
|---|---|---|---|
| Embedding | LanguageModelEmbedding |
language_module.py |
vocab_size, hidden_size, max_sequence_length |
| TransformerBlock | TransformerBlock |
transformer_block.py |
num_layers, hidden_size, num_attention_heads |
| Output Layer | ColumnParallelLinear |
tensor_parallel/layers.py |
hidden_size, vocab_size, bias=False |
class GPTModel(LanguageModule):
def __init__(self, config, transformer_layer_spec, ...):
super().__init__(config)
# ---- 1. Embedding ----
self.embedding = LanguageModelEmbedding(
config=self.config,
vocab_size=vocab_size,
max_sequence_length=max_sequence_length,
position_embedding_type=position_embedding_type,
)
# ---- 2. TransformerBlock (Decoder) ----
self.decoder = TransformerBlock(
config=self.config,
spec=transformer_layer_spec, # Layer Spec 决定内部结构
pre_process=self.pre_process, # PP first stage?
post_process=self.post_process, # PP last stage?
)
# ---- 3. Output Layer ----
self.output_layer = tensor_parallel.ColumnParallelLinear(
config.hidden_size,
self.vocab_size,
config=config,
init_method=config.init_method,
bias=False,
skip_bias_add=False,
gather_output=not self.parallel_output,
)
pre_process=True),只有最后一个 stage 拥有 Output Layer(post_process=True)。中间的 stage 只包含若干 TransformerLayer,不会创建 Embedding 和 Output Layer,从而节省显存。
output_layer.weight = embedding.word_embeddings.weight。在 PP 模式下,由于 Embedding 在 Stage 0 而 Output Layer 在最后一个 Stage,权重共享需要跨 stage 同步,Megatron 通过在训练开始前和每步更新后做一次 P2P 通信来保持两端的权重一致。
Layer Spec 模式
Megatron 使用 ModuleSpec 数据类实现声明式模型定义。核心思想是:用一棵 spec 树来描述模型的层级结构,框架在运行时根据 spec 递归地实例化各个子模块。
这种设计带来的最大好处是实现可替换。例如,get_gpt_layer_local_spec() 返回基于纯 Megatron 实现的 spec,而 get_gpt_layer_te_spec() 则返回基于 TransformerEngine 的 spec。两者对外暴露相同的接口,上层 GPTModel 代码不需要做任何修改。
每个 ModuleSpec 包含四个字段:module(要实例化的类)、submodules(子模块的 spec)、params(额外构造参数)和 metainfo(元信息)。框架在构建时通过 build_module() 工厂函数遍历这棵 spec 树,逐层实例化各子模块。具体机制见下方递归构建机制一节。
def get_gpt_layer_local_spec(
num_experts=None, moe_grouped_gemm=False, ...
) -> ModuleSpec:
"""返回基于 Megatron 原生实现的 GPT Layer Spec."""
return ModuleSpec(
module=TransformerLayer,
submodules=TransformerLayerSubmodules(
# ---- Self Attention ----
self_attention=ModuleSpec(
module=SelfAttention,
params={"attn_mask_type": AttnMaskType.causal},
submodules=SelfAttentionSubmodules(
linear_qkv=ColumnParallelLinear, # QKV 投影
core_attention=DotProductAttention, # 注意力计算
linear_proj=RowParallelLinear, # 输出投影
),
),
self_attn_bda=get_bias_dropout_add,
# ---- MLP ----
pre_mlp_layernorm=FusedLayerNorm,
mlp=ModuleSpec(
module=MLP,
submodules=MLPSubmodules(
linear_fc1=ColumnParallelLinear,
linear_fc2=RowParallelLinear,
),
),
mlp_bda=get_bias_dropout_add,
),
)
ColumnParallelLinear 替换为 TE 版本的 TEColumnParallelLinear,以及将 DotProductAttention 替换为 TEDotProductAttention。上层 GPTModel 和 TransformerBlock 的代码完全不需要修改。这是 Megatron 框架设计中最优雅的部分之一。
| Spec 类型 | 函数名 | Linear 实现 | Attention 实现 | 适用场景 |
|---|---|---|---|---|
| Local | get_gpt_layer_local_spec() |
Megatron ColumnParallel / RowParallel | DotProductAttention |
纯 PyTorch,无额外依赖 |
| TE | get_gpt_layer_te_spec() |
TransformerEngine Linear | TEDotProductAttention |
FP8 训练、融合 kernel 加速 |
ModuleSpec 数据结构
ModuleSpec 是声明式构建模式的核心数据类,定义在 megatron/core/transformer/spec_utils.py 中:
| 字段 | 类型 | 含义 |
|---|---|---|
module |
Union[Tuple, type] |
要实例化的类,如 MLP、SelfAttention;或动态导入路径元组 (module_path, ClassName) |
params |
dict |
传给构造函数的额外参数,如 {"attn_mask_type": AttnMaskType.causal} |
submodules |
type | None |
子模块的 spec,是一个 dataclass 实例(如 MLPSubmodules);叶子节点为 None |
metainfo |
dict |
附加元信息,不参与构建过程 |
每一级的 submodules 都由一个专门的 dataclass 定义。其字段名对应父模块 __init__ 中的属性名,值可以是裸类、函数或嵌套的 ModuleSpec:
| Submodules Dataclass | 所属模块 | 关键字段 |
|---|---|---|
TransformerLayerSubmodules |
TransformerLayer |
input_layernorm, self_attention, self_attn_bda, pre_mlp_layernorm, mlp, mlp_bda |
SelfAttentionSubmodules |
SelfAttention |
linear_qkv, core_attention, linear_proj, q_layernorm, k_layernorm |
MLPSubmodules |
MLP |
linear_fc1(h→4h 升维), linear_fc2(4h→h 降维), activation_func |
build_module:通用工厂函数
build_module 定义在 spec_utils.py:68-117,负责将一个 ModuleSpec(或裸类/函数)转化为实际的 PyTorch 模块实例。它的逻辑分为五步:
Step 1: 函数检测
如果传入的是一个函数(如 get_bias_dropout_add),直接返回,不做实例化。这是递归终止条件之一。
Step 2: 解析出实际的类
如果是裸类(isinstance(spec, type)),直接用该类。如果是 ModuleSpec,取出 spec.module。如果 module 是元组,则通过 import_module() 动态导入。
Step 3: 注入 submodules(关键!)
如果 spec 有 submodules 字段且不为 None,将其注入到 kwargs 中:kwargs["submodules"] = spec.submodules。这一步是递归传递的枢纽 -- 子模块 spec 被作为构造函数参数传递下去。
Step 4: 合并 params
将 spec.params(如 {"attn_mask_type": causal})与外部传入的 **kwargs(如 config、layer_number)合并。
Step 5: 实例化
调用 module(*args, **params, **kwargs) 创建模块实例并返回。被实例化的类如果在 __init__ 中对 submodules 的各字段再次调用 build_module,递归就此展开。
build_module 完整源码(带注释) spec_utils.py:68-117
def build_module(spec_or_module, *args, **kwargs):
# ---- 终止条件 1: 传入的就是函数,直接返回 ----
if isinstance(spec_or_module, types.FunctionType):
return spec_or_module
# ---- 终止条件 2: ModuleSpec 的 module 字段是函数 ----
if isinstance(spec_or_module, ModuleSpec) and isinstance(
spec_or_module.module, types.FunctionType
):
return spec_or_module.module
# ---- 解析出实际的类 ----
if isinstance(spec_or_module, type):
module = spec_or_module # 裸类,如 ColumnParallelLinear
elif hasattr(spec_or_module, "module") and isinstance(spec_or_module.module, type):
module = spec_or_module.module # 从 ModuleSpec 中取出类
else:
module = import_module(spec_or_module.module) # 动态导入
if isinstance(module, types.FunctionType):
return module
# ---- 关键一步:注入 submodules 到 kwargs ----
if hasattr(spec_or_module, "submodules") and spec_or_module.submodules is not None:
kwargs["submodules"] = spec_or_module.submodules
# ---- 实例化:class(*args, **params, **kwargs) ----
return module(
*args,
**spec_or_module.params if hasattr(spec_or_module, "params") else {},
**kwargs
)
递归构建的协作模式
build_module 本身不是递归函数。递归是通过一种协作模式实现的:build_module 把 submodules 注入 kwargs 并调用构造函数 → 构造函数(如 MLP.__init__)在内部对 submodules 的每个字段再次调用 build_module。每一层的 __init__ 才是递归展开的驱动者。
递归在以下三种情况下自然终止:
| spec 类型 | build_module 行为 | 递归 |
|---|---|---|
函数(如 get_bias_dropout_add) |
直接返回函数本身,不做实例化 | 终止 |
裸类(如 ColumnParallelLinear) |
isinstance(spec, type) 为 True,无 submodules 可注入,直接 class(**kwargs) |
终止 -- 叶子节点 |
| ModuleSpec 带 submodules | 取出 class,注入 submodules → class.__init__(submodules=...) |
继续 -- __init__ 会再调 build_module |
以 get_gpt_layer_local_spec() 为例,完整的递归构建树如下。树的深度为 3 层,共 9 个叶子节点,3 个中间节点(TransformerLayer、SelfAttention、MLP)的 __init__ 驱动递归展开:
各层 __init__ 中的 build_module 调用详解 源码追踪
Layer 0: TransformerBlock._build_layers(入口点)
transformer_block.py:370-375 遍历 layer_specs 列表,对每个 spec 调用 build_module:
self.layers = torch.nn.ModuleList([
build_layer(layer_spec, i + 1)
for i, layer_spec in enumerate(self.submodules.layer_specs)
])
# build_layer 内部调用:
# build_module(layer_spec, config=..., layer_number=..., pg_collection=..., ...)
build_module 取出 module=TransformerLayer,注入 submodules=TransformerLayerSubmodules(...),调用构造函数。
Layer 1: TransformerLayer.__init__
transformer_layer.py:264-376 接收 submodules: TransformerLayerSubmodules,对其每个字段逐一调用 build_module:
# [Module 1] Input LayerNorm -- 裸类,终止
self.input_layernorm = build_module(
submodules.input_layernorm, # FusedLayerNorm
config=self.config, hidden_size=..., eps=...
)
# [Module 2] Self Attention -- ModuleSpec,触发下一层递归
self.self_attention = build_module(
submodules.self_attention, # ModuleSpec(module=SelfAttention, submodules=...)
config=self.config, layer_number=...,
)
# [Module 3] BDA -- 函数,终止
self.self_attn_bda = build_module(submodules.self_attn_bda)
# [Module 7] Pre-MLP LayerNorm -- 裸类,终止
self.pre_mlp_layernorm = build_module(
submodules.pre_mlp_layernorm, # FusedLayerNorm
config=self.config, hidden_size=..., eps=...
)
# [Module 8] MLP -- ModuleSpec,触发下一层递归
self.mlp = build_module(
submodules.mlp, # ModuleSpec(module=MLP, submodules=MLPSubmodules(...))
config=self.config, ...
)
# [Module 9] MLP BDA -- 函数,终止
self.mlp_bda = build_module(submodules.mlp_bda)
Layer 2a: SelfAttention.__init__
attention.py 中,SelfAttention 及其父类 Attention 的 __init__ 对 SelfAttentionSubmodules 的三个核心字段调用 build_module(全部为裸类,递归终止):
# QKV 投影 -- ColumnParallelLinear(裸类)→ 终止
self.linear_qkv = build_module(
submodules.linear_qkv, # ColumnParallelLinear
self.config.hidden_size, self.linear_qkv_out_dim, ...
)
# 核心注意力 -- DotProductAttention(裸类)→ 终止
self.core_attention = build_module(
submodules.core_attention, # DotProductAttention
config=self.config, layer_number=..., ...
)
# 输出投影 -- RowParallelLinear(裸类)→ 终止
self.linear_proj = build_module(
submodules.linear_proj, # RowParallelLinear
self.query_projection_size, self.config.hidden_size, ...
)
Layer 2b: MLP.__init__
mlp.py:108-139 对 MLPSubmodules 的两个线性层调用 build_module(全部为裸类,递归终止):
# fc1: h → ffn_hidden(升维)-- ColumnParallelLinear(裸类)→ 终止
self.linear_fc1 = build_module(
submodules.linear_fc1,
self.input_size, ffn_hidden_size,
config=self.config, init_method=..., gather_output=False, ...
)
# fc2: ffn_hidden → h(降维)-- RowParallelLinear(裸类)→ 终止
self.linear_fc2 = build_module(
submodules.linear_fc2,
self.config.ffn_hidden_size, self.config.hidden_size,
config=self.config, init_method=..., input_is_parallel=True, ...
)
ColumnParallelLinear 替换为 TransformerEngine 的 TEColumnParallelLinear,只需修改 spec 树中的一行:MLPSubmodules(linear_fc1=TEColumnParallelLinear, ...)。build_module、MLP.__init__、TransformerLayer 的代码完全不需要改动。这就是声明式配置与命令式构建解耦的威力。
TransformerBlock
TransformerBlock 的核心职责是将 L 个 TransformerLayer 堆叠在一起,并负责在 Pipeline Parallelism(PP)下正确分配每个 stage 应构建的层数。它在初始化时通过 get_num_layers_to_build() 计算本 rank 需要构建哪些层,只实例化属于自己的那部分。
PP 层分布:普通 PP 模式下,层按连续区间分配。例如 PP=4、总层数=32 时,每个 stage 分到 8 层:
VPP(Virtual Pipeline Parallelism)引入了非连续层分配。VPP 将每个 stage 的层拆成多个"virtual chunk",交错分布在整个模型中。这样每个 micro-batch 在 stage 间的一次"旅行"只需穿过更少的层,从而减小 pipeline bubble。
PP * VPP 整除。
| 配置 | PP | VPP | 总层数 | 每 stage 层数 | Bubble 比率 |
|---|---|---|---|---|---|
| 无 VPP | 4 | 1 | 32 | 8 (连续) | \(\frac{3}{m}\) |
| VPP=2 | 4 | 2 | 32 | 4+4 (两段) | \(\frac{3}{2m}\) |
| VPP=4 | 4 | 4 | 32 | 2+2+2+2 (四段) | \(\frac{3}{4m}\) |
class TransformerBlock(MegatronModule):
def __init__(self, config, spec, ...):
super().__init__(config=config)
# 计算本 stage 应构建多少层
self.num_layers_per_pipeline_rank = get_num_layers_to_build(config)
# 根据 spec 构建层列表
self._build_layers(spec)
# 最后一个 PP stage 的最后加一个 Final LayerNorm
if self.post_process and self.post_layer_norm:
self.final_layernorm = build_module(
spec.submodules.final_layernorm,
config=self.config,
hidden_size=self.config.hidden_size,
)
TransformerLayer(核心)
TransformerLayer 是 GPT 模型中最核心的计算单元。Megatron 使用 Pre-LayerNorm 架构(GPT-2 以来的标准做法):先做 LayerNorm,再做 Attention 或 MLP。每个子层(Attention 和 MLP)都被残差连接包裹,并通过一个融合的 BDA(bias_dropout_add) 操作完成残差相加。
每个 TransformerLayer 的计算流程可以表示为:
output = LN(x) -> SelfAttention -> BDA(output, residual=x)output = LN(output) -> MLP -> BDA(output, residual=prev_output)
其中 BDA 是一个融合 kernel,将 bias 加法、dropout 和残差加法合并为一次操作,减少显存读写和 kernel 启动开销。
上图中的 [TP: f/g 或 AG/RS] 标记了 Tensor Parallelism 通信发生的位置。在不使用 Sequence Parallelism 时,通信原语是 f(前向 identity / 反向 AllReduce)和 g(前向 AllReduce / 反向 identity)。启用 SP 后则变为 AllGather / ReduceScatter。具体原理参见 Tensor Parallelism 文档。
[s, b, h](sequence_length, batch_size, hidden_size)。这意味着所有 L 层可以串联堆叠,每层的 hidden_size 不变。改变 hidden_size 是 Attention 和 MLP 内部的事情 -- 它们内部会投影到不同维度,但最终输出都会投影回 h 维。
def forward(self, hidden_states, attention_mask, ...):
# ============ Self Attention Block ============
# 保存残差
residual = hidden_states
# Pre-LN: 先 LayerNorm
hidden_states = self.input_layernorm(hidden_states)
# Self Attention
attention_output_with_bias = self.self_attention(
hidden_states, attention_mask=attention_mask, ...
)
# BDA: bias + dropout + residual add
# training 时用融合 kernel,inference 时用普通实现
with self.bias_dropout_add_exec_handler():
hidden_states = self.self_attn_bda(
self.training, self.config.bias_dropout_fusion
)(attention_output_with_bias, residual, self.hidden_dropout)
# ============ MLP Block ============
residual = hidden_states
# Pre-LN
hidden_states = self.pre_mlp_layernorm(hidden_states)
# MLP
mlp_output_with_bias = self.mlp(hidden_states)
# BDA: bias + dropout + residual add
with self.bias_dropout_add_exec_handler():
hidden_states = self.mlp_bda(
self.training, self.config.bias_dropout_fusion
)(mlp_output_with_bias, residual, self.hidden_dropout)
return hidden_states
output = dropout(attention_output + bias) + residual。分开做的话需要三次显存读写(bias add、dropout、residual add),融合后只读写一次。在大模型训练中,这类"显存带宽受限"(memory-bound)的操作优化非常重要,因为 Attention 和 MLP 之间的连接操作看似简单,但显存吞吐量往往成为瓶颈。
Pre-LN vs Post-LN 架构对比 背景
原始 Transformer 论文(Vaswani et al., 2017)使用 Post-LN 架构:output = LN(x + Attention(x))。GPT-2 之后,大多数大语言模型改用 Pre-LN 架构:output = x + Attention(LN(x))。
Pre-LN 的优势在于训练稳定性。Post-LN 中残差连接在 LayerNorm 之前,梯度需要穿过 LayerNorm 才能沿残差路径传播,这在深层网络中容易导致梯度消失或爆炸。Pre-LN 将 LayerNorm 放在子层之前,残差路径上没有任何非线性操作,梯度可以直接"高速通过"残差连接,从输出层一路传到输入层。
Pre-LN 的代价是模型最后一层的输出没有经过 LayerNorm,因此需要在 TransformerBlock 末尾额外加一个 Final LayerNorm。此外,一些研究表明 Post-LN 在充分调参后可能达到更好的最终精度,但 Pre-LN 的训练稳定性优势使其成为工业界的默认选择。
SelfAttention
SelfAttention 模块实现了完整的多头注意力机制。Megatron 的实现有几个关键设计决策:
- QKV 合并投影:使用单个
ColumnParallelLinear同时生成 Q、K、V 三个张量(而非三个独立的线性层),减少 kernel 启动次数并提高 GPU 利用率。 - GQA(Grouped Query Attention)支持:允许 KV head 数量少于 Q head 数量,多个 Q head 共享同一组 KV head,显著减少 KV cache 的显存占用。
- TP 切分:QKV 投影使用
ColumnParallelLinear(按 head 维度切分),输出投影使用RowParallelLinear(按 head 维度切分输入,AllReduce 输出)。
QKV 投影:单个 ColumnParallelLinear 产生 Q、K、V 的拼接输出。输出形状为 [s, b, (n_q + 2 * n_kv) * d_head],其中 n_q 是 query head 数量,n_kv 是 key/value head 数量,d_head = h / n_q 是每个 head 的维度。
GQA(Grouped Query Attention):在 GQA 中,n_kv < n_q,多个 query head 共享同一对 key/value head。例如 LLaMA-2 70B 使用 64 个 query head 但只有 8 个 KV head(每 8 个 Q head 共享 1 个 KV head)。这将 KV cache 大小减少了 8 倍。
| TP Rank | Q Heads | KV Heads | Group 关系 |
|---|---|---|---|
| Rank 0 | Q0-Q7 (8个) | KV0, KV1 (2个) | Q0-Q3 -> KV0, Q4-Q7 -> KV1 |
| Rank 1 | Q8-Q15 (8个) | KV2, KV3 (2个) | Q8-Q11 -> KV2, Q12-Q15 -> KV3 |
| Rank 2 | Q16-Q23 (8个) | KV4, KV5 (2个) | Q16-Q19 -> KV4, Q20-Q23 -> KV5 |
| Rank 3 | Q24-Q31 (8个) | KV6, KV7 (2个) | Q24-Q27 -> KV6, Q28-Q31 -> KV7 |
Core Attention:标准的缩放点积注意力(Scaled Dot-Product Attention),计算公式为:
Megatron 的 DotProductAttention 支持多种后端实现:FlashAttention(默认,O(N) 显存)、fused attention、以及 unfused 的朴素实现。Causal mask 通过设置 attn_mask_type=causal 来启用,确保每个 token 只能 attend 到它之前的 token。
Output Projection:Attention 的最后一步是通过 RowParallelLinear 将拼接后的多头输出投影回 hidden_size 维度。在 TP 模式下,RowParallelLinear 的输入已经按 head 维度分片(每个 rank 只有部分 head 的输出),RowParallelLinear 在前向时做 AllReduce 将各 rank 的部分结果求和,得到完整输出。
def forward(self, hidden_states, attention_mask, ...):
# ---- QKV Projection ----
# 单个 ColumnParallelLinear 同时产生 Q, K, V
mixed_qkv, _ = self.linear_qkv(hidden_states) # [s, b, proj_size]
# 拆分 Q, K, V
# proj_size = (n_q_per_rank + 2 * n_kv_per_rank) * d_head
new_tensor_shape = mixed_qkv.size()[:-1] + (
self.num_query_groups_per_partition, # n_kv per rank
(self.num_attention_heads_per_partition # n_q per rank
// self.num_query_groups_per_partition + 2), # +2 for K, V
self.hidden_size_per_attention_head, # d_head
)
mixed_qkv = mixed_qkv.view(*new_tensor_shape)
# 沿 group 维度拆分
query, key, value = torch.split(
mixed_qkv,
[self.num_attention_heads_per_partition
// self.num_query_groups_per_partition, # Q heads per group
1, # 1 K head per group
1], # 1 V head per group
dim=3,
)
# ---- Core Attention ----
core_attn_out = self.core_attention(query, key, value, attention_mask)
# ---- Output Projection ----
output, bias = self.linear_proj(core_attn_out) # RowParallelLinear
return output, bias
ColumnParallelLinear 按 head 维度切分,KV head 数量必须能被 TP size 整除。例如 TP=8 时,n_kv 至少为 8。如果 n_kv 太小(比如 MQA 中 n_kv=1),就无法直接使用 TP,需要启用 KV head 的复制机制。
MLP
现代 GPT 模型的 MLP 层普遍采用 SwiGLU(Swish-Gated Linear Unit)激活函数,这是 LLaMA、PaLM 等模型的标准选择。与传统的 GeLU MLP 相比,SwiGLU 引入了一个门控机制,在经验上显示出更好的训练效率。
SwiGLU 的核心思想是:fc1 产生两倍宽度的输出,然后将输出一分为二 -- 一半作为"gate"经过 SiLU 激活,另一半作为"value"直接使用,两者逐元素相乘后送入 fc2。
维度关系:传统 MLP 的 FFN 隐藏维度通常是 4 * h。SwiGLU 由于拆分成两半,为了保持参数量一致,通常将 ffn_hidden 设为 8/3 * h(约 2.67h),经向上取整到 128 的倍数后再乘以 2 作为 fc1 的输出维度。例如 LLaMA-7B 中 h=4096,ffn_hidden=11008(接近 8/3 * 4096 = 10923,取整到 128 的倍数)。
TP 切分:fc1 使用 ColumnParallelLinear 按列切分(每个 rank 产生 2 * ffn_hidden / TP 的输出),fc2 使用 RowParallelLinear 按行切分(每个 rank 输入 ffn_hidden / TP,AllReduce 后得到完整输出 h)。这和 Attention 的 TP 模式完全一致。
def forward(self, hidden_states):
# fc1: [s, b, h] -> [s, b, 2 * ffn_hidden / TP]
intermediate_parallel, bias_parallel = self.linear_fc1(hidden_states)
# SwiGLU activation
if self.config.gated_linear_unit:
# 拆成 gate 和 value 两半
intermediate_parallel = self.activation_func(intermediate_parallel)
# activation_func 内部做:
# gate, value = chunk(intermediate, 2, dim=-1)
# return silu(gate) * value
# fc2: [s, b, ffn_hidden / TP] -> [s, b, h]
output, output_bias = self.linear_fc2(intermediate_parallel)
return output, output_bias
ffn_hidden 本身通常设为 8/3 * h 而非 4 * h,使得总参数量大致持平。具体而言,传统 MLP 参数量为 \(2 \times h \times 4h = 8h^2\),SwiGLU 参数量为 \(h \times 2 \times \frac{8}{3}h + \frac{8}{3}h \times h = 8h^2\),基本一致。
为什么 SwiGLU 比 GeLU 效果更好? 可选
GLU(Gated Linear Unit)家族的核心思想是引入乘法门控。在传统 MLP 中,激活函数(如 GeLU)直接作用于线性变换的输出:output = gelu(xW1) * W2。而 GLU 系列将线性输出一分为二,一半经过激活作为"门",另一半作为"值":output = activation(xW_gate) * (xW_value) * W2。
这种乘法门控允许模型学习哪些特征维度应该被激活、哪些应该被抑制,比单纯的逐元素激活函数表达能力更强。Shazeer (2020) 的实验表明,在固定参数量下,GLU 变体(特别是 SwiGLU 和 GeGLU)在各种 NLP 基准上都优于标准 GeLU/ReLU MLP。
完整数据流
最后,我们将前面所有组件串联起来,展示从 token ID 到 logits 的完整前向传播路径。理解这条数据流有助于在调试和性能分析时快速定位问题所在。
1. Token Embedding
Token IDs [s, b] 通过词表嵌入矩阵 W_embed [V, h] 查表,得到 [s, b, h] 的连续向量表示。在 TP 模式下,词表按列切分到各 rank(VocabParallelEmbedding),每个 rank 只存储 V/TP 行。
2. Position Encoding
位置信息通过可学习的 position embedding 加到 token embedding 上。Megatron 也支持 RoPE(Rotary Position Embedding),此时不在 Embedding 层加位置信息,而是在每个 Attention 层对 Q 和 K 做旋转编码。
3. L x TransformerLayer
隐藏向量 [s, b, h] 依次通过 L 个 TransformerLayer。每层执行 LN -> Attention -> BDA -> LN -> MLP -> BDA,输入输出形状保持不变。在 PP 模式下,L 层被分配到不同 stage;在 TP+SP 模式下,序列维度 s 也会被切分。
4. Final LayerNorm
最后一个 TransformerLayer 的输出经过一个额外的 LayerNorm。这是 Pre-LN 架构的特点 -- 由于每层内部的 LN 在 Attention/MLP 之前,最后一层的输出没有经过 LN,因此需要在 TransformerBlock 末尾补一个。
5. Output Linear
将 [s, b, h] 投影到词表维度 [s, b, V],得到每个位置的 logits。使用 ColumnParallelLinear 实现,可选与 Embedding 矩阵共享权重(tied weights)以减少参数量。
6. CrossEntropy Loss
Logits 与 label token IDs 计算交叉熵损失。Megatron 使用 VocabParallelCrossEntropyLoss,在 TP 模式下每个 rank 只计算自己负责的 V/TP 个类别的 softmax 分子,然后通过 AllReduce 得到完整分母,避免 gather 完整的 [s, b, V] 张量。
参数量计算:下表以 LLaMA-7B 的典型配置(h=4096, L=32, V=32000, n_q=32, n_kv=32, d_head=128, ffn_hidden=11008)为例,分解各组件的参数量。
| 组件 | 公式 | 7B 配置下的参数量 |
|---|---|---|
| Embedding | \(V \times h\) | 32000 × 4096 = 131M |
| QKV 投影 (每层) | \(h \times (n_q + 2 \times n_{kv}) \times d_{\text{head}}\) | 4096 × (32+64) × 128 / 4096 = 4096 × 3 × 4096 = 50.3M |
| O_proj (每层) | \(h \times h\) | 4096 × 4096 = 16.8M |
| fc1 (每层) | \(h \times 2 \times \text{ffn\_hidden}\) | 4096 × 2 × 11008 = 90.2M |
| fc2 (每层) | \(\text{ffn\_hidden} \times h\) | 11008 × 4096 = 45.1M |
| LayerNorm (每层) | \(2 \times 2 \times h\)(2 个 LN,各有 weight 和 bias) | 2 × 2 × 4096 = 16K |
| 每层合计 | ~202M | |
| 所有层合计 | \(L \times \text{per\_layer}\) | 32 × 202M = ~6.5B |
| Output Layer | \(h \times V\) | 4096 × 32000 = 131M |
| Final LN | \(2 \times h\) | 8K |
| 总计 | ~6.7B (若 Embedding 与 Output 共享权重则 ~6.6B) |
ColumnParallelLinear(fc1 和 QKV)和 RowParallelLinear(fc2 和 O_proj)-- 它们覆盖了 99% 以上的参数。
完整前向代码路径:
def forward(self, input_ids, position_ids, attention_mask, labels=None, ...):
# ---- Stage 1: Embedding (仅 PP first stage) ----
if self.pre_process:
decoder_input = self.embedding(input_ids, position_ids)
else:
decoder_input = None # 从上一个 PP stage 接收激活值
# ---- Stage 2: TransformerBlock ----
hidden_states = self.decoder(
hidden_states=decoder_input,
attention_mask=attention_mask,
...
)
# ---- Stage 3: Output (仅 PP last stage) ----
if self.post_process:
# 线性投影: [s, b, h] -> [s, b, V]
logits, _ = self.output_layer(hidden_states)
if labels is not None:
# 计算 cross-entropy loss
loss = self.compute_language_model_loss(labels, logits)
else:
loss = None
return loss
else:
return hidden_states # 中间 stage,传给下一个 PP stage
- Tensor Parallelism -- ColumnParallelLinear 和 RowParallelLinear 的切分原理
- 训练循环篇 -- GPTModel.forward 如何被训练循环调用
- 集合通信操作 -- AllReduce/AllGather/ReduceScatter 的通信原理
- Rank 与并行组 -- TP/PP/DP 组的划分方式
- pretrain_gpt.py -- model_provider 如何创建 GPTModel