01

GPTModel 总览

gpt_model.py GPTModel model_provider

Megatron-LM 中的 GPT 模型由 GPTModel 类实现,定义在 megatron/core/models/gpt/gpt_model.py 中。整个模型的结构非常清晰,由三个核心组件纵向堆叠而成:

  1. Embedding -- 将 token ID 和位置信息映射到连续的隐藏空间向量,输出形状为 [s, b, h]
  2. TransformerBlock (又称 decoder) -- 由 L 个 TransformerLayer 堆叠而成,是模型的主体计算部分,参数量占比超过 95%。
  3. Output Layer -- 一个线性投影层,将隐藏向量映射回词表维度,产生 logits,形状为 [s, b, V]

Megatron 采用 model provider 模式:用户代码(如 pretrain_gpt.py)定义一个 model_provider 函数,传入一个 TransformerConfig 和一个 layer spec,框架负责根据 spec 构建完整模型。这种设计将模型的声明(spec)与实例化(框架)解耦,使得切换底层实现(例如从纯 PyTorch 切换到 TransformerEngine)只需替换 spec,不改动模型代码。

flowchart TD A["Token IDs
[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
gpt_model.py -- GPTModel.__init__ 三大组件初始化 L151-257
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,
        )
Pipeline Parallelism 与 pre_process / post_process
当使用流水线并行(PP)时,模型会被切分到多个 stage 上。只有第一个 stage 拥有 Embedding(pre_process=True),只有最后一个 stage 拥有 Output Layer(post_process=True)。中间的 stage 只包含若干 TransformerLayer,不会创建 Embedding 和 Output Layer,从而节省显存。
PP=4 时各 stage 拥有的组件 Stage 0 (pre_process=True, post_process=False) +-- Embedding (word + position) +-- TransformerLayer 0..7 Stage 1 (pre_process=False, post_process=False) +-- TransformerLayer 8..15 Stage 2 (pre_process=False, post_process=False) +-- TransformerLayer 16..23 Stage 3 (pre_process=False, post_process=True) +-- TransformerLayer 24..31 +-- Final LayerNorm +-- Output Layer
Embedding 权重共享
许多模型(如 GPT-2)将 Embedding 矩阵和 Output Layer 的权重绑定(tied weights),即 output_layer.weight = embedding.word_embeddings.weight。在 PP 模式下,由于 Embedding 在 Stage 0 而 Output Layer 在最后一个 Stage,权重共享需要跨 stage 同步,Megatron 通过在训练开始前和每步更新后做一次 P2P 通信来保持两端的权重一致。
02

Layer Spec 模式

gpt_layer_specs.py ModuleSpec 声明式定义

Megatron 使用 ModuleSpec 数据类实现声明式模型定义。核心思想是:用一棵 spec 树来描述模型的层级结构,框架在运行时根据 spec 递归地实例化各个子模块。

这种设计带来的最大好处是实现可替换。例如,get_gpt_layer_local_spec() 返回基于纯 Megatron 实现的 spec,而 get_gpt_layer_te_spec() 则返回基于 TransformerEngine 的 spec。两者对外暴露相同的接口,上层 GPTModel 代码不需要做任何修改。

Layer Spec 树结构:spec 到实例的映射 ModuleSpec(module=TransformerLayer) | +-- self_attention: ModuleSpec(module=SelfAttention) | | | +-- core_attention: ModuleSpec(module=DotProductAttention) | +-- linear_qkv: ColumnParallelLinear | +-- linear_proj: RowParallelLinear | +-- self_attn_bda: bias_dropout_add (function) | +-- pre_mlp_layernorm: ModuleSpec(module=LNImpl) | +-- mlp: ModuleSpec(module=MLP) | | | +-- linear_fc1: ColumnParallelLinear | +-- linear_fc2: RowParallelLinear | +-- mlp_bda: bias_dropout_add (function)

每个 ModuleSpec 包含四个字段:module(要实例化的类)、submodules(子模块的 spec)、params(额外构造参数)和 metainfo(元信息)。框架在构建时通过 build_module() 工厂函数遍历这棵 spec 树,逐层实例化各子模块。具体机制见下方递归构建机制一节。

gpt_layer_specs.py -- get_gpt_layer_local_spec 关键结构 L40-95
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,
        ),
    )
Spec 的替换能力
如需使用 NVIDIA TransformerEngine 的融合内核(FP8 训练等),只需将 spec 中的 ColumnParallelLinear 替换为 TE 版本的 TEColumnParallelLinear,以及将 DotProductAttention 替换为 TEDotProductAttention。上层 GPTModelTransformerBlock 的代码完全不需要修改。这是 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] 要实例化的类,如 MLPSelfAttention;或动态导入路径元组 (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(如 configlayer_number)合并。

Step 5: 实例化

调用 module(*args, **params, **kwargs) 创建模块实例并返回。被实例化的类如果在 __init__ 中对 submodules 的各字段再次调用 build_module,递归就此展开。

build_module 完整源码(带注释) spec_utils.py:68-117
spec_utils.py -- build_module L68-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 本身不是递归函数。递归是通过一种协作模式实现的:build_module 把 submodules 注入 kwargs 并调用构造函数 → 构造函数(如 MLP.__init__)在内部对 submodules 的每个字段再次调用 build_module。每一层的 __init__ 才是递归展开的驱动者。
协作递归模式 build_module(spec) │ ├── 从 spec 取出 class ── Step 2 ├── kwargs["submodules"] = spec.submodules ── Step 3 └── class.__init__(submodules=..., **kwargs) ── Step 5 │ │ __init__ 内部: ├── self.child_a = build_module(submodules.child_a, ...) ──┐ ├── self.child_b = build_module(submodules.child_b, ...) ──┤ 递归展开 └── self.child_c = build_module(submodules.child_c, ...) ──┘

递归在以下三种情况下自然终止

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 个中间节点(TransformerLayerSelfAttentionMLP)的 __init__ 驱动递归展开:

完整递归构建树 TransformerBlock._build_layers()transformer_block.py:370-375 │ for each layer_spec in layer_specs: │ build_module(layer_spec) │ └── TransformerLayer.__init__(submodules=TransformerLayerSubmodules) │ transformer_layer.py:264-376 │ ├── build_module(input_layernorm) │ └── FusedLayerNorm(...) ■ 叶子(裸类) │ ├── build_module(self_attention) │ └── SelfAttention.__init__(submodules=SelfAttentionSubmodules) │ │ attention.py │ │ │ ├── build_module(linear_qkv) │ │ └── ColumnParallelLinear(...) ■ 叶子 │ │ │ ├── build_module(core_attention) │ │ └── DotProductAttention(...) ■ 叶子 │ │ │ └── build_module(linear_proj) │ └── RowParallelLinear(...) ■ 叶子 │ ├── build_module(self_attn_bda) │ └── get_bias_dropout_add ■ 叶子(函数) │ ├── build_module(pre_mlp_layernorm) │ └── FusedLayerNorm(...) ■ 叶子 │ ├── build_module(mlp) │ └── MLP.__init__(submodules=MLPSubmodules) │ │ mlp.py:76-139 │ │ │ ├── build_module(linear_fc1) │ │ └── ColumnParallelLinear(...) ■ 叶子 │ │ │ └── build_module(linear_fc2) │ └── RowParallelLinear(...) ■ 叶子 │ └── build_module(mlp_bda) └── get_bias_dropout_add ■ 叶子(函数)
各层 __init__ 中的 build_module 调用详解 源码追踪

Layer 0: TransformerBlock._build_layers(入口点)

transformer_block.py:370-375 遍历 layer_specs 列表,对每个 spec 调用 build_module

transformer_block.py -- _build_layersL370-375
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

transformer_layer.py -- TransformerLayer.__init__ 关键片段L288-376
# [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(全部为裸类,递归终止):

attention.py -- SelfAttention 与 Attention.__init__
# 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-139MLPSubmodules 的两个线性层调用 build_module(全部为裸类,递归终止):

mlp.py -- MLP.__init__L108-139
# 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, ...
)
设计优势:最小化替换成本
这种 spec 树 + 协作递归的设计带来了极低的替换成本。例如要将 MLP 中的 ColumnParallelLinear 替换为 TransformerEngine 的 TEColumnParallelLinear,只需修改 spec 树中的一行:MLPSubmodules(linear_fc1=TEColumnParallelLinear, ...)build_moduleMLP.__init__TransformerLayer 的代码完全不需要改动。这就是声明式配置与命令式构建解耦的威力。
03

TransformerBlock

transformer_block.py TransformerBlock PP / VPP 分层

TransformerBlock 的核心职责是将 L 个 TransformerLayer 堆叠在一起,并负责在 Pipeline Parallelism(PP)下正确分配每个 stage 应构建的层数。它在初始化时通过 get_num_layers_to_build() 计算本 rank 需要构建哪些层,只实例化属于自己的那部分。

PP 层分布:普通 PP 模式下,层按连续区间分配。例如 PP=4、总层数=32 时,每个 stage 分到 8 层:

PP 连续分配(PP=4, 总层数=32) Stage 0: layers [ 0, 1, 2, 3, 4, 5, 6, 7] Stage 1: layers [ 8, 9, 10, 11, 12, 13, 14, 15] Stage 2: layers [16, 17, 18, 19, 20, 21, 22, 23] Stage 3: layers [24, 25, 26, 27, 28, 29, 30, 31] 每个 stage 得到连续的 32/4 = 8 层

VPP(Virtual Pipeline Parallelism)引入了非连续层分配。VPP 将每个 stage 的层拆成多个"virtual chunk",交错分布在整个模型中。这样每个 micro-batch 在 stage 间的一次"旅行"只需穿过更少的层,从而减小 pipeline bubble。

VPP 非连续分配(PP=4, VPP=2, 总层数=32) 每个 stage 有 2 个 virtual chunk,每个 chunk 有 32/(4*2) = 4 层 Stage 0: chunk 0 = layers [ 0, 1, 2, 3] chunk 1 = layers [16, 17, 18, 19] Stage 1: chunk 0 = layers [ 4, 5, 6, 7] chunk 1 = layers [20, 21, 22, 23] Stage 2: chunk 0 = layers [ 8, 9, 10, 11] chunk 1 = layers [24, 25, 26, 27] Stage 3: chunk 0 = layers [12, 13, 14, 15] chunk 1 = layers [28, 29, 30, 31] 前向路径: S0.c0 -> S1.c0 -> S2.c0 -> S3.c0 -> S3.c1 -> S2.c1 -> S1.c1 -> S0.c1 折返路径使每个 micro-batch 穿过 4 层/stage 而非 8 层/stage
VPP 的代价与约束
VPP 减小了 pipeline bubble 比率(从 \(\frac{p-1}{m}\) 降到 \(\frac{p-1}{m \cdot v}\),其中 v = virtual stages),但代价是 P2P 通信量翻倍(每个 micro-batch 要经过 2v 个 stage 而非 v 个)。此外,总层数必须能被 PP * VPP 整除。
配置PPVPP总层数每 stage 层数Bubble 比率
无 VPP41328 (连续)\(\frac{3}{m}\)
VPP=242324+4 (两段)\(\frac{3}{2m}\)
VPP=444322+2+2+2 (四段)\(\frac{3}{4m}\)
transformer_block.py -- TransformerBlock.__init__ 核心逻辑 L67-120
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,
            )
04

TransformerLayer(核心)

transformer_layer.py Pre-LN BDA Residual

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 启动开销。

TransformerLayer 数据流(Pre-LN 架构) hidden_states [s, b, h] | +------------------------------+ (residual) v | LayerNorm | | | SelfAttention | | [TP: f/g 或 AG/RS] | v | bias_dropout_add <--------------+ | +------------------------------+ (residual) v | LayerNorm | | | MLP | | [TP: f/g 或 AG/RS] | v | bias_dropout_add <--------------+ | v output [s, b, h]

上图中的 [TP: f/g 或 AG/RS] 标记了 Tensor Parallelism 通信发生的位置。在不使用 Sequence Parallelism 时,通信原语是 f(前向 identity / 反向 AllReduce)和 g(前向 AllReduce / 反向 identity)。启用 SP 后则变为 AllGather / ReduceScatter。具体原理参见 Tensor Parallelism 文档。

Tensor Shape 全程不变
TransformerLayer 的输入和输出形状都是 [s, b, h](sequence_length, batch_size, hidden_size)。这意味着所有 L 层可以串联堆叠,每层的 hidden_size 不变。改变 hidden_size 是 Attention 和 MLP 内部的事情 -- 它们内部会投影到不同维度,但最终输出都会投影回 h 维。
transformer_layer.py -- TransformerLayer.forward L210-280
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
bias_dropout_add 融合操作
BDA 将三步操作合成一个 CUDA kernel: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 vs Post-LN 的残差路径 Pre-LN(Megatron 使用): x ─────────────────────+───> output | ^ +─> LN ─> Attention ──-+ 残差路径上无 LN,梯度直通 Post-LN(原始 Transformer): x ──> + ──> LN ──> output ^ | Attention 残差路径上有 LN,梯度受 LN 调节

Pre-LN 的代价是模型最后一层的输出没有经过 LayerNorm,因此需要在 TransformerBlock 末尾额外加一个 Final LayerNorm。此外,一些研究表明 Post-LN 在充分调参后可能达到更好的最终精度,但 Pre-LN 的训练稳定性优势使其成为工业界的默认选择。

05

SelfAttention

attention.py QKV Projection GQA Core Attention

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 的维度。

QKV 投影与拆分 hidden_states [s, b, h] | v ColumnParallelLinear (linear_qkv) | v mixed_qkv [s, b, (n_q + 2*n_kv) * d_head] | split | +----> Q [s, b, n_q * d_head] --> reshape --> [s, b, n_q, d_head] | +----> K [s, b, n_kv * d_head] --> reshape --> [s, b, n_kv, d_head] | +----> V [s, b, n_kv * d_head] --> reshape --> [s, b, n_kv, d_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 倍。

GQA 在 TP 下的 Head 分配(n_q=32, n_kv=8, TP=4)
TP RankQ HeadsKV HeadsGroup 关系
Rank 0Q0-Q7 (8个)KV0, KV1 (2个)Q0-Q3 -> KV0, Q4-Q7 -> KV1
Rank 1Q8-Q15 (8个)KV2, KV3 (2个)Q8-Q11 -> KV2, Q12-Q15 -> KV3
Rank 2Q16-Q23 (8个)KV4, KV5 (2个)Q16-Q19 -> KV4, Q20-Q23 -> KV5
Rank 3Q24-Q31 (8个)KV6, KV7 (2个)Q24-Q27 -> KV6, Q28-Q31 -> KV7
每个 TP rank 分到 n_q/TP=8 个 Q head 和 n_kv/TP=2 个 KV head,GQA group 关系在每个 rank 内保持一致

Core Attention:标准的缩放点积注意力(Scaled Dot-Product Attention),计算公式为:

$$\text{Attention}(Q, K, V) = \text{softmax}\!\left(\frac{Q K^T}{\sqrt{d_{\text{head}}}}\right) V$$

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 的部分结果求和,得到完整输出。

attention.py -- SelfAttention: QKV 拆分逻辑 L1327-1366
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
TP 约束:n_kv 必须能被 TP 整除
由于 QKV 使用 ColumnParallelLinear 按 head 维度切分,KV head 数量必须能被 TP size 整除。例如 TP=8 时,n_kv 至少为 8。如果 n_kv 太小(比如 MQA 中 n_kv=1),就无法直接使用 TP,需要启用 KV head 的复制机制。
SelfAttention 完整数据流 hidden_states [s, b, h] | | [TP: f 或 AllGather] v ColumnParallelLinear (linear_qkv) | v [s, b, (n_q/TP + 2*n_kv/TP) * d_head] | split into Q, K, V | v DotProductAttention | Q [s, b, n_q/TP, d_head] | K [s, b, n_kv/TP, d_head] | V [s, b, n_kv/TP, d_head] | | score = Q @ K.T / sqrt(d_head) -> causal mask -> softmax -> @ V | v [s, b, n_q/TP * d_head] | RowParallelLinear (linear_proj) | | [TP: g 或 ReduceScatter] v output [s, b, h]
06

MLP

mlp.py SwiGLU ColumnParallel + RowParallel

现代 GPT 模型的 MLP 层普遍采用 SwiGLU(Swish-Gated Linear Unit)激活函数,这是 LLaMA、PaLM 等模型的标准选择。与传统的 GeLU MLP 相比,SwiGLU 引入了一个门控机制,在经验上显示出更好的训练效率。

SwiGLU 的核心思想是:fc1 产生两倍宽度的输出,然后将输出一分为二 -- 一半作为"gate"经过 SiLU 激活,另一半作为"value"直接使用,两者逐元素相乘后送入 fc2

SwiGLU MLP 数据流 Input [s, b, h] | ColumnParallelLinear (fc1) | [s, b, 2 * ffn_hidden] | chunk(2, dim=-1) | +--- gate [s, b, ffn_hidden] | | | silu() -- silu(x) = x * sigmoid(x) | | +------* (element-wise multiply) | [s, b, ffn_hidden] | RowParallelLinear (fc2) | Output [s, b, h]

维度关系:传统 MLP 的 FFN 隐藏维度通常是 4 * h。SwiGLU 由于拆分成两半,为了保持参数量一致,通常将 ffn_hidden 设为 8/3 * h(约 2.67h),经向上取整到 128 的倍数后再乘以 2 作为 fc1 的输出维度。例如 LLaMA-7B 中 h=4096ffn_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 模式完全一致。

mlp.py -- SwiGLU 激活与 MLP.forward L211-222
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
SwiGLU 的参数量影响
SwiGLU 的 fc1 输出是传统 MLP 的 2 倍宽度(因为要拆分成 gate 和 value),但 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。

MLP 的 TP 切分 TP Rank 0 TP Rank 1 ───────────── ───────────── input [s,b,h] ─── f ───> input (copy) input (copy) | | fc1 (列切分) fc1 (列切分) | | [s,b,2*ffn/TP] [s,b,2*ffn/TP] | | SwiGLU SwiGLU | | [s,b,ffn/TP] [s,b,ffn/TP] | | fc2 (行切分) fc2 (行切分) | | [s,b,h] [s,b,h] | | +────── g ──────────+ | AllReduce (求和) | output [s,b,h] f = 前向 identity / 反向 AllReduce g = 前向 AllReduce / 反向 identity
07

完整数据流

gpt_model.py 端到端 参数量计算

最后,我们将前面所有组件串联起来,展示从 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)
参数量分布的启示
从上表可以看到,MLP 参数量占比最大(每层 fc1 + fc2 = 135M,占单层的 67%),其次是 Attention 的 QKV 和 O_proj(每层 67M,占 33%),而 LayerNorm 参数量几乎可以忽略。这也解释了为什么 TP 切分的重点在 ColumnParallelLinear(fc1 和 QKV)和 RowParallelLinear(fc2 和 O_proj)-- 它们覆盖了 99% 以上的参数。
显存占用远大于参数量
7B 参数在 FP16 下只占 ~14GB 显存,但训练时还需要存储:(1) FP32 master weights = 28GB, (2) Adam momentum + variance = 56GB, (3) 梯度 = 14GB, (4) 激活值(取决于 micro-batch 和序列长度)= 数十 GB。总计训练一个 7B 模型至少需要 ~120GB 显存,这正是需要各种并行策略的原因。
7B 模型训练显存预算(FP16 + Adam) 参数(FP16): 7B * 2 bytes = 14 GB FP32 Master Weights: 7B * 4 bytes = 28 GB Adam Momentum (FP32): 7B * 4 bytes = 28 GB Adam Variance (FP32): 7B * 4 bytes = 28 GB 梯度(FP16): 7B * 2 bytes = 14 GB ───────────────────────────────────────────── 固定开销合计: 112 GB + 激活值 (activation memory): 取决于 batch_size * seq_len 例如 mbs=1, seq=4096, h=4096: ~8 GB / layer, 32 层 = ~256 GB 使用 activation checkpointing 后: ~2 GB / layer 的峰值 结论: 即使单卡有 80GB(A100),也至少需要 2 卡 TP 才能放下 7B 13B 以上则必须组合使用 TP + PP + 分布式优化器

完整前向代码路径

gpt_model.py -- GPTModel.forward 端到端前向传播 L260-310
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
相关文档
GPT 模型架构知识图谱 pretrain_gpt.py | model_provider() | v GPTModel / | \ v v v Embedding TransformerBlock OutputLayer | L x TransformerLayer / \ v v SelfAttention MLP / | \ / \ v v v v v QKV Core O_proj fc1 fc2 Attn (SwiGLU) 相关并行策略: TP -> QKV, O_proj, fc1, fc2 (按 head/column 切分) SP -> LayerNorm, Dropout 区域 (按序列切分) PP -> TransformerBlock 中的层 (按层分配到不同 stage) DP -> 多个 GPTModel 副本 (不同 mini-batch)