欢迎光临
我们一直在努力

多GPU训练模式深度调研分享

目录

一、基础心智模型

二、并行模式详解

(一)数据并行

(二)ZeRO 与 FSDP

(三)张量并行

(四)流水并行

(五)参数服务器与异步训练

(六)混合并行与框架生态

三、性能模型与对比表

(一)估算公式

(二)参考算例

四、实验复现建议

(一)最小复现实验

(二)关键 API 示例

五、优化实践与排查

(一)何时使用哪些技巧

(二)常见问题与排查

六、结论与建议

主要参考链接


干货分享,感谢您的阅读!

对大模型训练,DP/DDP最容易上手但几乎不降显存;TP按层内张量切分,适合“大 hidden”的宽层;PP按层深切分,适合超深模型;ZeRO/FSDP通过分片参数、梯度和优化器状态,已经成为 PyTorch/DeepSpeed 训练 10B+ 模型的主线;参数服务器/异步训练则更适合稀疏、异构或弹性场景。现代主流方案通常是 TP/PP + DP 或 ZeRO/FSDP 的混合并行。 

一、基础心智模型

大模型分布式训练,本质上是在同时优化三件事:让模型装得下、让每步训练更快、让优化仍然稳定收敛。因此几类并行方式其实各自“购买”的能力不同:DP 购买的是吞吐扩展,TP/PP 购买的是模型可容纳规模,ZeRO/FSDP 购买的是“把重复保存的训练状态去掉”,参数服务器/异步训练购买的是弹性与弱同步能力。PyTorch 官方把 DDP、FSDP2、TP、PP 和 DeviceMesh/DTensor 归为一组原生可组合的并行能力;Megatron Core 则明确把 TP、PP、DP、EP、CP 设计成可组合策略;JAX 则把“如何切分”上推到 Mesh / PartitionSpec 与编译器自动分片。 

下面这张图,是理解所有模式最有效的总图:训练一步 = 计算 + 通信,性能瓶颈不是“有没有通信”,而是“通信发生在什么时候、传多大、跨什么拓扑、能否和计算重叠”。NCCL 官方定义了训练中最常用的集体通信原语:AllReduce、AllGather、ReduceScatter 以及点对点发送/接收;FSDP 官方则直接指出,它可以视为把 DDP 的 AllReduce 分解成了 AllGather 与 ReduceScatter。 

工程上最常见的估算模型是 α–β 模型:T_comm ≈ α × steps + bytes / B。 这里 α 代表每轮通信固定延迟,B 代表有效带宽。对 DDP 常见的 ring all-reduce,可以把它近似理解成 ReduceScatter + AllGather 两段,因此大消息时更常是带宽受限,小消息时更常是延迟受限。这也是为什么层内频繁同步的 TP 更怕高延迟网络,而每步只同步一次大梯度的 DDP/FSDP 更怕总带宽不足。NCCL 明确指出 ReduceScatter + AllGather 在语义上等价于 AllReduce,这正是很多工程公式的基础。

如果你只记住一条经验法则,那就是:宽层难题先想 TP,深层难题先想 PP,纯显存难题先想 ZeRO/FSDP,强基线和排错先用 DDP。Megatron Core 的建议几乎就是这句话的正式版本:先从 DP 开始,模型放不下再加 TP,模型非常大再加 PP,长上下文再加 CP,同时在用 TP 时建议打开 Sequence Parallel。 

二、并行模式详解

先给出一张总览表,后面再逐项拆开。表格是基于 PyTorch、DeepSpeed、Megatron、JAX 官方文档与经典论文的工程归纳。 

模式一句话原理典型通信显存变化扩展性一致性与收敛实现复杂度最适场景
DP / DDP 每卡一份完整模型,数据切分 backward 中梯度 AllReduce 模型状态几乎不降 弱扩展强,强扩展中等 同步,最稳定 小中模型,强基线,故障定位
ZeRO / FSDP 训练状态按 DP 组分片 AllGather + ReduceScatter + 部分 broadcast 显著下降 弱扩展强,强扩展受延迟影响 数学上仍是同步训练 1B–70B+ 稠密模型
TP 同一层内部切张量 逐层 AllReduce / AllGather 参数与部分激活约按 TP 度缩减 节点内好,跨节点谨慎 同步,近似数学等价 中高 大 hidden、大矩阵层
PP 按层深切模型,micro-batch 流水 P2P 传激活与激活梯度 参数约按 PP 度缩减 深模型好,但有 pipeline bubble GPipe 同步,PipeDream 需处理版本一致性 超深模型、慢网络跨主机
参数服务器 / 异步 worker push/pull;server 聚合 星型 push/pull 灵活,依实现而定 容忍异构,但中心瓶颈明显 有陈旧梯度风险 中高 稀疏、异构、弹性训练

(一)数据并行

DDP 的核心最简单:每个 rank 持有完整模型副本,各自跑自己的数据 shard,反向传播时把梯度同步起来,再各自执行相同的优化器更新。PyTorch 官方强调,DDP 应按 一进程一 GPU 的方式使用,并且它在单机多 GPU 上通常也显著快于 torch.nn.DataParallel。它的性能优势主要来自两点:多进程避免 GIL 争用,以及梯度桶(bucket)让 AllReduce 能与反向计算重叠。 

PyTorch 还特别说明:DDP 不会自动切分输入,输入分片通常由 DistributedSampler 完成;参数在每步里不是反复广播,而是通过梯度 AllReduce 后,依赖每个 rank 执行同样的优化器更新来保持参数一致;缓冲区(如 BatchNorm buffer)则会从 rank 0 广播到其他副本。对“它到底什么时候同步”的直觉可以理解为:forward 是各做各的,backward 中 bucket ready 就开始同步。

DDP 的带宽瓶颈集中在一次迭代中对完整梯度张量做 AllReduce。消息很大时,瓶颈主要是网络吞吐;消息很小时,瓶颈变成 bucket 数太多、collective 次数太频繁。PyTorch 提供 bucket_cap_mb 来控制 bucket 大小,并且 TORCH_DISTRIBUTED_DEBUG=DETAIL 可以直接打印平均 forward、backward、通信时间和通信/计算重叠时间,适合快速判断“是不是 bucket 太碎或 overlap 没打出来”。 

DDP 的显存特征同样直白:每卡都要保存完整参数、完整梯度和完整优化器状态。所以它最适合两类情况:一类是模型本来就能放进单卡,目标只是把吞吐做上去;另一类是你要建立一个“最可信”的同步基线,用来比较 TP、PP、FSDP、ZeRO 之后到底哪里赚到了、哪里亏掉了。它不适合“模型本身放不下单卡”的场景。 

DDP 在收敛层面通常最稳定,因为它等价于同步 SGD。真正会引入优化问题的,往往不是 DDP 本身,而是全球 batch 变大。Facebook 的大 batch 训练工作给出了非常经典的经验:当 batch 放大 k 倍时,学习率按线性规则近似乘 k,并在训练初期做 warmup;而对 BERT 这类大型 Transformer,LAMB 则是公认适合更大 batch 的优化器之一。 

PyTorch DDP 最小骨架如下。API 行为与官方文档一致。 

import os
import torch
import torch.distributed as dist
from torch.nn.parallel import DistributedDataParallel as DDP
from torch.utils.data import DataLoader, DistributedSampler

def main():
local_rank = int(os.environ["LOCAL_RANK"])
dist.init_process_group(backend="nccl")
torch.cuda.set_device(local_rank)

model = MyTransformer().cuda(local_rank)
model = DDP(model, device_ids=[local_rank], bucket_cap_mb=25)

dataset = MyDataset()
sampler = DistributedSampler(dataset, shuffle=True)
loader = DataLoader(dataset, batch_size=4, sampler=sampler, num_workers=4, pin_memory=True)

optim = torch.optim.AdamW(model.parameters(), lr=3e-4)

model.train()
for epoch in range(10):
sampler.set_epoch(epoch)
for batch in loader:
batch = {k: v.cuda(local_rank, non_blocking=True) for k, v in batch.items()}
loss = model(**batch)["loss"]
optim.zero_grad(set_to_none=True)
loss.backward()
optim.step()

dist.destroy_process_group()

(二)ZeRO 与 FSDP

ZeRO 和 FSDP 是“大模型时代最实用的一类数据并行改造”。DeepSpeed 对 ZeRO 的定义非常清晰:它不是改变数据并行的“外壳”,而是把原本在每个 DP rank 上重复保存的训练状态拆开分片。ZeRO 分三阶段推进:Stage 1 分片优化器状态,Stage 2 再分片梯度,Stage 3 再把参数也分片;ZeRO-Infinity 还能继续把状态卸到 CPU / NVMe。 

PyTorch FSDP2 的心智模型则是:平时参数是分片状态;forward / backward 之前把当前需要的参数 all-gather 成完整视图;backward 内把本地完整梯度 reduce-scatter 回分片梯度。官方甚至直接写明,FSDP 可以看作是对 DDP AllReduce 的分解:AllReduce = ReduceScatter + AllGather。这句话非常关键,因为它解释了为什么 FSDP 往往更省显存,但更依赖按模块/按层的通信调度质量。

显存角度看,ZeRO/FSDP 的价值极大。Megatron Core 给出的一个非常实用的官方公式是:对 fp16 参数与 fp16 梯度,非分布式优化器需要约 20 字节/参数;而其 distributed optimizer(ZeRO 风格的优化器分片)则可降到 4 + 16/d 字节/参数,其中 d 是 data parallel size。这个公式有一个极重要的工程含义:仅做优化器分片,显存不会无限下降,它会收敛到一个“参数副本仍在”的下界。因此,真正想把 7B、30B、70B 这类模型装进 A100 40GB,常常必须走到 FSDP / ZeRO-3,而不只是 ZeRO-1。 

DeepSpeed 的 ZeRO 教程还给了一个很直观的例子:1.5B GPT-2 的 Adam 优化器状态要吃掉约 18GB GPU 显存,而 ZeRO Stage 1 把它在 8 个 DP rank 上均分后,可把这部分降到约 2.25GB/卡,从而把“本来放不下”的训练变成“至少有机会放下”。这正是 ZeRO 的价值:不是先想怎么切模型代码,而是先把训练状态里的重复副本去掉。 

ZeRO/FSDP 的通信模式和 DDP 明显不同。DDP 一般是一轮大梯度 AllReduce;FSDP / ZeRO-3 则更像“很多次逐层参数 AllGather + 梯度 ReduceScatter”。这让它在低带宽集群上经常更延迟敏感。ZeRO++ 的论文正是针对这个痛点:它指出 ZeRO-3 的 forward、backward 和梯度平均里,会出现较高的权重 gather 与梯度通信开销,并通过量化 AllGather、数据重映射、量化梯度平均等方法,把 ZeRO 的通信量降了约 4 倍,在 384 GPU 规模上实现了最高 2.16× 吞吐提升。 

PyTorch FSDP 还有两个非常实用但常被忽略的工程点。第一,limit_all_gathers=True 被官方称为一个“rate limiter”,目的是抑制过度抢跑的 AllGather 导致 GPU 内存峰值不可控;第二,做 HYBRID_SHARD 时,如果采用节点内分片、节点间复制,官方建议可以尝试 NCCL_CROSS_NIC=1 来改善跨节点 AllReduce 时间。 

如果你用的是 PyTorch 原生栈,FSDP2 是当下最顺手的路线;如果你需要成熟的 ZeRO-3、CPU/NVMe offload、通信日志、和较完整的大模型训练生态,DeepSpeed 仍然非常强。FairScale 的 FSDP 在历史上很重要,但官方仓库已经明确说明,它现在更多是历史参考与实验场,核心能力已上游进入 PyTorch。 

PyTorch FSDP2 风格的最小骨架如下。 

import torch
import torch.distributed as dist
from torch.distributed.fsdp import fully_shard

def main():
dist.init_process_group("nccl")
rank = dist.get_rank()
torch.cuda.set_device(rank)

model = MyTransformer().cuda()
# FSDP2 风格:对模块或子模块进行 fully_shard
for block in model.layers:
fully_shard(block)
fully_shard(model)

optim = torch.optim.AdamW(model.parameters(), lr=3e-4)

for batch in loader():
loss = model(**batch)["loss"]
optim.zero_grad(set_to_none=True)
loss.backward()
optim.step()

DeepSpeed ZeRO-3 配置则常常只需要改 JSON。DeepSpeed 官方明确说,启用 ZeRO 往往不需要改模型代码。 

{
"train_micro_batch_size_per_gpu": 1,
"gradient_accumulation_steps": 16,
"bf16": { "enabled": false },
"fp16": { "enabled": true },
"zero_optimization": {
"stage": 3,
"overlap_comm": true,
"contiguous_gradients": true,
"reduce_bucket_size": 500000000
}
}

(三)张量并行

TP 的核心不是“把层拆给不同 GPU 分工串行跑”,而是把同一层的权重矩阵切开,让多张 GPU 一起完成一个大矩阵乘。Megatron-LM 论文把它定义为一种高效的层内模型并行;PyTorch 原生 TP 文档也明确把 parallelize_module() 作为入口,而 ParallelStyle 则定义了怎么分输入、输出与子模块。Megatron Core 的官方建议是:当 hidden size 很大、单层放不下单卡时,TP 非常合适,并且通常与 DP/PP 组合使用。

TP 的优势是参数与一部分激活会随 TP 度缩小,因而它尤其适合“宽层”模型,像多头注意力的 QKV 投影、大 MLP、超大 hidden 的 LLM block。Megatron Bridge 官方文档明确提到:TP 不仅减少模型状态显存,也会因为每卡 tensor 变小而减少一些激活内存;但代价是每卡工作粒度更小,会带来更多 CPU 调度开销与更小的 GEMM,从而出现“卡数加了,但核函数不再饱和”的问题。 

TP 的真正瓶颈在通信频率:它不是“一步同步一次大梯度”,而是几乎每层都可能同步。因此,TP 非常依赖低延迟、高带宽、稳定拓扑,通常优先放在单机节点内,让通信走 NVLink / NVSwitch,而把跨节点扩展性交给外层的 DP/FSDP。PyTorch 的 TP 教程也明确把一个很典型的实践写成:主机内做 TP,主机间做 FSDP,这样既能分掉超大层,又能把节点间通信降到较粗粒度。 

Megatron Core 现在给出的工程建议非常明确:用 TP 就尽量开 Sequence Parallel。官方文档直接写了“Always enable when using TP”,因为它能在 LayerNorm / Dropout 这类不适合标准 TP 的部位,沿序列维分片,从而减少激活内存。对应论文进一步给出了强结论:Sequence Parallel + selective activation recomputation 能让激活内存降约 5×,同时把纯重计算带来的额外执行开销减少 90%+。 

TP 的收敛一般不会比 DDP 更差,因为它本质还是同步训练;它真正影响的是step time 与核函数效率。如果你发现 TP 度越大反而越慢,先怀疑的通常不是优化器,而是:矩阵太碎、跨节点 TP、高频小消息 collectives、LayerNorm/Dropout 没做 sequence parallel、attention 实现没和 TP 对齐。Megatron Core 的并行策略指南也建议:先从 DP 开始,只有在模型层本身装不下时才加 TP。 

PyTorch 原生 TP 最小示例如下。API 入口与官方文档一致。 

from torch.distributed.device_mesh import init_device_mesh
from torch.distributed.tensor.parallel import (
parallelize_module, ColwiseParallel, RowwiseParallel
)

# 例如 1D TP mesh
tp_mesh = init_device_mesh("cuda", mesh_shape=(4,))

model = MyTransformer().cuda()
parallelize_module(
model,
device_mesh=tp_mesh,
parallelize_plan={
"attn.q_proj": ColwiseParallel(),
"attn.k_proj": ColwiseParallel(),
"attn.v_proj": ColwiseParallel(),
"attn.o_proj": RowwiseParallel(),
"mlp.fc1": ColwiseParallel(),
"mlp.fc2": RowwiseParallel(),
},
)

(四)流水并行

PP 的核心是按层深切模型。与 TP 的“同一层拆开一起算”不同,PP 是“不同 GPU 各自负责一段连续层,然后让 micro-batch 在这些 stage 之间流动”。PyTorch 官方文档把它定义为 primitive parallelism,并特别指出它对大规模训练、带宽受限集群和大模型推理都很有效,因为它把“逐层频繁全集体同步”的问题,换成了“stage 间较粗粒度的点对点激活传输”。 

GPipe 给了 PP 最经典的同步版本:把 mini-batch 再切成多个 micro-batch,用 batch-splitting 的方式填满流水线,论文中报告了接近线性的加速,并成功训练了 6B 参数、128 层的多语 Transformer。PipeDream 则走得更激进:它利用流水并行减少通信,并通过参数版本控制解决 forward/backward 看到不同权重版本的问题;论文报告称,它相对数据并行可把通信减少到最高 95%,并在 time-to-accuracy 维度实现最高 5.3× 提升。

PP 的主要通信是相邻 stage 之间的 point-to-point:forward 传激活,backward 传激活梯度。这让它有两个非常鲜明的工程特征。第一,它天然适合跨主机,因为点对点的流量模式比逐层 AllReduce/AllGather 更可控;PyTorch 现在的 torch.distributed.pipelining 也明确写了它提供 cross-host pipeline parallelism 的一等支持。第二,它非常吃负载均衡:如果 stage 切得不均匀,慢 stage 会让全流水线空泡(bubble)增大。 

PyTorch 的 PP API 在最近几年已经相当丰富,不只是 GPipe 和 1F1B,还支持 Interleaved 1F1B、Looped BFS、Interleaved Zero Bubble、ZBV、DualPipeV 等调度。这说明业界共识已经很明确:PP 的核心问题早就不是“能不能做”,而是“bubble 怎么压、负载怎么配、与 TP/FSDP 怎么组合”。Megatron Core 官方也建议,真正很大的模型通常是 TP + PP 组合,PP 负责层深切分,TP 负责宽层切分。 

PP 的显存效果主要来自参数沿深度切分,大致能把参数负担按 PP 度下降;但与此同时,流水线中会保留多份在途 micro-batch 的激活,因此常要配合激活检查点。GPipe 本身就强调了重计算来压显存;DeepSpeed 也提供了 activation checkpointing,并支持 activation partitioning、CPU checkpointing 等进一步节省显存的手段。 

PP 在一致性/收敛上的分界线很清楚:GPipe 是同步 fill-drain,数学上更接近普通同步训练; PipeDream 则是带参数版本控制的异步流水,它靠版本化维持 backward 正确性。 因此,对研究者来说,如果你特别在意“尽量贴近标准同步训练”,优先从 GPipe/1F1B 风格的同步流水开始;如果你追求 time-to-accuracy 与高设备利用率,再考虑 PipeDream 式版本化调度。 

DeepSpeed 的一个重要限制必须单独强调:其文档明确写明,Pipeline Parallelism 与 ZeRO-2 / ZeRO-3 不兼容。如果你在 DeepSpeed 里想同时获得“参数深度切分”和“所有状态分片”,就不能简单叠加它的 PipelineModule 与 ZeRO-3,而往往要转向其他组合方式,或采用它的 AutoTP/不同训练栈。 

PyTorch 原生 PP 的最小骨架大致如下。 

from torch.distributed.pipelining import pipeline, SplitPoint
from torch.distributed.pipelining.schedules import Schedule1F1B

# 假设把 Transformer 分成两个 stage
pipe = pipeline(
model,
mb_args=(example_input,),
split_spec={
"layers.11": SplitPoint.END, # 在第12层后切开
}
)

stage = pipe.build_stage(local_rank, device=torch.device(f"cuda:{local_rank}"))
schedule = Schedule1F1B(stage, n_microbatches=8, loss_fn=my_loss)

# schedule.step() 会自动把 whole batch 切成 micro-batches
out = schedule.step(input_batch, target=target_batch)

(五)参数服务器与异步训练

参数服务器(PS)是另一条经典路线。USENIX 的 Parameter Server 论文把它定义为:worker 负责计算,server 维护全局共享参数;系统支持异步数据通信、灵活一致性模型、弹性扩展和持续容错。Google DistBelief 则把这种思路用于深网络训练:它支持 model parallelism 与 data parallelism,并通过 Downpour SGD 实现异步 SGD,在当时训练了超过 10 亿参数的网络。 

PS/异步训练最重要的优点是不必等所有 worker 严格步步对齐,因此它在异构节点、易抖动网络、弹性扩缩容、稀疏更新场景中很有吸引力。Parameter Server 论文甚至专门讨论了三种不同的一致性模型,并指出 relaxed consistency 能在收敛速率与系统效率之间权衡。换句话说,PS 的核心价值并不是“更快”,而是“同步可以不是刚性的”。 

但这条路线对今天的稠密 GPU LLM 训练通常不是首选。原因也很简单:一方面,中心化或分层 server 会引入明显的 incast / hotspot 风险;另一方面,现代 GPU 互联与 NCCL 对 AllReduce / AllGather / ReduceScatter 已经做了高度优化,特别适合稠密张量的大吞吐集体通信。NCCL 官方明确说,它就是围绕 all-reduce、all-gather、reduce-scatter 和 send/recv 等标准通信模式,在 PCIe、NVLink、NVSwitch、InfiniBand、TCP/IP 上做高带宽优化的。对 dense LLM 来说,这类 collective 通常比“worker 频繁 push/pull 到 server”更自然。这个结论是基于 PS 论文与 NCCL 官方能力做出的工程推断。 

PS/异步训练在收敛上的代价就是陈旧梯度。Parameter Server 论文明确写到,放松一致性会导致节点之间的数据不一致,这可能拖慢收敛;DistBelief 则指出,Downpour SGD 在深网络上配合 Adagrad 工作得 surprisingly well。也就是说,它不是不能收敛,而是你需要更小心地看待:学习率、延迟窗口、容错、worker 漂移和稳定性。 

如果要给工程师一句直白建议:稠密大模型预训练,优先 collective;稀疏/异构/弹性系统,再认真考虑 PS/异步。 

参数服务器式伪代码如下:

# worker side
while True:
w_local = ps.pull(keys)
loss = model_forward_backward(w_local, batch)
grads = collect_gradients()
ps.push(keys, grads)

# server side
while True:
grads = recv_from_workers()
params = update(params, grads, consistency="bounded_delay")
send_params_back()

(六)混合并行与框架生态

现代稠密 LLM 训练,主流已经不是“选一种并行”,而是做多维混合并行。DeepSpeed 官方把 DP、MP、PP 及其组合称为 3D parallelism;Megatron Core 明确支持 TP、PP、DP、EP、CP 的组合;PyTorch 原生则通过 DeviceMesh 与 DTensor 让用户在一个 N 维进程网格上组合 DDP/FSDP2/TP/PP。也就是说,真正的大模型训练早已从“单一并行模式”进入“mesh 设计问题”。 

最常见的几种混合方式,可以直接理解成这几句:

  • DP + TP:最常见的大模型组合,尤其适合“单层太宽,单卡放不下”的 Transformer;PyTorch 官方 TP 教程就明确展示了“主机内 TP、主机间 FSDP”的组合。 
  • DP + PP:适合“层数很深”或集群网络更慢的情况;DeepSpeed 的 pipeline 教程就展示了两路 DP + 两路 PP + 多个 micro-batch 的工作方式。 
  • DP + TP + PP:真正 10B–1000B 级模型的主流做法,也是 Megatron / DeepSpeed 最经典的训练形态。 
  • ZeRO/FSDP + TP:当模型状态显存是第一瓶颈、层内宽度也是第二瓶颈时,这是非常实用的组合。PyTorch 官方 TP 教程就是围绕 TP + FSDP 展开的;DeepSpeed 也在最新教程里给出了 Automatic Tensor Parallelism for Training,明确强调 TP 可与 ZeRO 结合。 
  • ZeRO/FSDP + PP:理论上很吸引人,但要看框架实现。以 DeepSpeed 为例,官方明确写了 PP 不兼容 ZeRO-2/3,因此不能想当然叠加。 

下面这张框架表,给工程选型时非常有用:

框架 / 栈主要能力强项典型限制适合谁
PyTorch torch.distributed DDP、FSDP2、TP、PP、DeviceMesh、DTensor、ZeroRedundancyOptimizer  原生、可组合、便于研究与排错 有些高级组合仍需自己设计 mesh 与 wrap 策略 想保留最大灵活性的工程师/研究者
DeepSpeed ZeRO、Offload、Pipeline、3D parallelism、通信日志、AutoTP/AutoSP  大模型训练工程化程度高 Pipeline 与 ZeRO-2/3 不能直接混用 追求最快落地的大模型训练团队
Megatron-LM / Megatron Core TP、PP、DP、EP、CP、distributed optimizer、混合精度  Transformer 规模训练的性能基准与参考实现 更偏“大模型专门栈”,通用性不如纯 PyTorch 原生 做 LLM 预训练、追求极致性能的团队
FairScale OSS / SDP / FSDP 历史实现,许多 ZeRO 思想的早期工程化载体  学习 ZeRO/FSDP 演化很有价值 现阶段更多是历史参考,主线已上游到 PyTorch 研究扩展思想、读源码
JAX / TPU 栈 Mesh、PartitionSpec、自动/显式分片、GShard/GSPMD 路线  并行表达能力强,编译器自动化程度高 更依赖 XLA/TPU 生态与编译器思维 熟悉 JAX/TPU、做超大规模实验的团队

JAX/TPU 路线值得单独强调。Mesh-TensorFlow 很早就提出了“把任意张量维映射到多维处理器网格”的思想,并在 512 TPU cores 上训练到 5B 参数;GShard 则用轻量注解 + XLA 扩展,把 MoE Transformer 做到 600B+ 并在 2048 TPU v3 上高效训练;GSPMD 更进一步,把并行规划推进到编译器层,在有限注解下推断全图分片,并在 2048 TPUv3 cores 上,对 1T 参数模型达到 50%–62% 计算利用率。今天 JAX 官方文档把其并行模式分成三类:compiler-based automatic sharding、explicit sharding、manual per-device programming。这是与 PyTorch / Megatron“手工指定并行策略”显著不同的设计哲学。 

JAX 的现代写法一般通过 Mesh 与 PartitionSpec 来声明设备轴和张量切分方式;而 jax.distributed.initialize() 在 TPU、Slurm、OpenMPI 环境下很多参数还能自动探测。这意味着在 TPU 生态里,“并行表达”通常更像“给编译器提示”,而不是像 PyTorch 那样直接操作 process group 和 wrapper。 

一个极简的 JAX 分片示意如下。API 形式与官方文档一致。 

import jax
import numpy as np
from jax.sharding import Mesh, NamedSharding, PartitionSpec as P

devices = np.array(jax.devices()).reshape(2, 4)
mesh = Mesh(devices, ('dp', 'tp'))

# 例:第一维按 dp 切,第二维按 tp 切
x = np.arange(16).reshape(8, 2)
x = jax.device_put(x, NamedSharding(mesh, P('dp', 'tp')))

三、性能模型与对比表

这一节先给估算方法,再给公式化样例。因为真正严格可比的“官方 benchmark × 同一模型 × 同一数据集 × 所有 1/2/4/8/16/32/64/128 GPU 点位”现实里非常少,所以我把结果分成两类:官方论文/文档给出的点状结果,以及基于统一假设的工程估算。凡属估算,下文都会明确标注。 

本节统一假设:网络 100 Gbps RDMA,理论带宽 12.5 GB/s;为便于工程估算,按 80% 有效带宽 ≈ 10 GB/s 计算下界。GPU NVIDIA A100 40GB。精度 FP16 混合精度。 若需要全球 batch,按 DeepSpeed 官方关系式: train_batch_size = train_micro_batch_size_per_gpu × gradient_accumulation_steps × number_of_GPUs。 

(一)估算公式

先定义符号:

  • P:参数量
  • d:数据并行度
  • t:张量并行度
  • p:流水并行 stage 数
  • G:梯度字节数
  • B:有效带宽
  • α:每次 collective 的固定延迟
  • T_step ≈ T_compute + T_comm – T_overlap
  • 吞吐量 ≈ (global_batch × seq_len) / T_step

对 DDP,如果采用 ring all-reduce 的工程近似,下界可以写成:

T_allreduce ≈ 2(d-1)α + 2((d-1)/d) × G / B

这里的推导基础是 NCCL 对 AllReduce、ReduceScatter、AllGather 的语义定义,以及 FSDP 对 “DDP all-reduce 被分解成 reduce-scatter 与 all-gather” 的官方说明。 

对显存,可以先抓住一个足够好用的定量直觉。Megatron Core 官方给出的 fp16 params + fp16 grads 理论模型状态开销是:

  • 普通优化器:20 B / param
  • distributed optimizer:4 + 16/d B / param

它已经非常适合作为工程估算上界。进一步做“理想 fully sharded 下界”时,可以把全部训练状态近似看成按 d 均分,即理论下界约 20/d B / param,但要额外记住:FSDP/ZeRO-3 在运行期还会有临时 AllGather 窗口、bucket、flat buffer 与 activation 占用,所以真实峰值会高于这个理想下界。前者是官方公式,后者是本文基于官方状态拆分所做的工程近似。 

流水并行可以用一个非常实用的泡泡估算式:均衡 stage、使用 fill-drain 时,流水利用率近似为 m / (m + p – 1),其中 m 是 micro-batch 数;因此 bubble 比例近似是 (p – 1) / (m + p – 1)。这不是框架 API 给出的显式公式,而是基于 GPipe / 1F1B 调度图的标准工程推导。它直接告诉你:PP 想高效,micro-batch 数不能太小。支撑这一点的官方背景是 GPipe 的 batch-splitting 思路与 PyTorch/DeepSpeed 对 micro-batch pipeline scheduling 的实现。 

(二)参考算例

下面用一个7B dense Transformer做最直观的算例。 在 FP16 下,参数和梯度各约 14 GB 量级;若按 20 B / param 粗估完整模型状态,单卡要吃掉约 140 GB,显然 DDP 无法在 A100 40GB 上直接训练完整 7B。这个结论来自 Megatron 的官方字节/参数公式。 

对 7B 模型,若只看 DDP 梯度 AllReduce 的理论通信下界,则每个 rank 的传输量约为 2((d-1)/d) × 14 GB。 按 10 GB/s 有效网络带宽估算,可得到下表。表格是本文按上述公式计算。

GPU 数每 rank AllReduce 字节数理论通信下界备注
1 0 GB 0.00 s 无通信
2 14.00 GB 1.40 s 已明显受网络影响
4 21.00 GB 2.10 s DDP 强扩展开始吃紧
8 24.50 GB 2.45 s 100Gbps 下很难完全隐藏
16 26.25 GB 2.63 s 通信继续趋近饱和
32 27.13 GB 2.71 s 字节数接近极限
64 27.56 GB 2.76 s 延迟与调度开始更显著
128 27.78 GB 2.78 s AllReduce 字节几乎不再增长

这张表说明了一个常被忽视的事实:更多 GPU 并不会让 DDP 的单步梯度同步字节无限下降;它只是逼近一个上限。因此,当模型很大、网络只有 100Gbps 时,DDP 往往很快从“计算受限”切换到“通信受限”。这也是为什么大模型训练会转向 TP/PP/FSDP/ZeRO 的组合。这个判断与 PipeDream 对“通信-计算比”瓶颈的描述,以及 DeepSpeed / Megatron 对 3D 并行的演进是一致的。 

下面再看同一个 7B 模型的模型状态显存/卡。表中前两列可视为较可靠的工程估算,第三列是“理想 fully-sharded 下界”,真实峰值会更高。计算基于前述公式。

GPU 数DDP 模型状态/卡Distributed Optimizer 估算/卡Fully Sharded 理想下界/卡A100 40GB 结论
1 140.0 GB 140.0 GB 140.0 GB 全部 OOM
2 140.0 GB 84.0 GB 70.0 GB 全部 OOM
4 140.0 GB 56.0 GB 35.0 GB Fully-sharded 勉强接近可放下
8 140.0 GB 42.0 GB 17.5 GB ZeRO-3/FSDP2 + checkpoint 较现实
16 140.0 GB 35.0 GB 8.75 GB 较从容,但通信更关键
32 140.0 GB 31.5 GB 4.38 GB 显存轻松,网络/调度主导
64 140.0 GB 29.75 GB 2.19 GB 显存不再是首要瓶颈
128 140.0 GB 28.88 GB 1.09 GB 进入强扩展与调优区间

这张表有两个结论非常“值钱”:

  • 第一,只做优化器分片并不够:即使到 128 GPU,4 + 16/d 公式下 7B 仍约有 28.9GB 的持久模型状态,激活、bucket、workspace 一加就很危险。
  • 第二,fully-sharded / ZeRO-3 才真正把 7B 从“不可能”带到“可训练”。这与 ZeRO 教程、FSDP2 文档以及 Megatron distributed optimizer 的结论完全一致。 

把视角再拉大到不同模型规模,下面给出一个部署建议表。参数区间与建议模式是本文基于上述显存/通信模型得到的工程结论。

模型规模参数量参考DDP 是否现实优先模式常见起步 GPU 数说明
<1B,例如 0.3B 通常可行 DDP 基线 1 / 2 / 4 / 8 先求简单稳妥
1–10B,例如 3B / 7B 一般不现实 FSDP2 / ZeRO-3;必要时加 TP 4 / 8 / 16 显存是第一问题
10–100B,例如 30B 基本不现实 TP + PP + ZeRO/FSDP 16 / 32 / 64 进入 3D 并行主场
超大 >100B,例如 300B 不现实 3D/4D 并行 + offload / TPU GSPMD 64 / 128+ 工程与调度系统同样重要

下面给出一张官方结果表。这些数字不是统一基准,因此不能横向当作 apples-to-apples 的对决,但能很好反映各路线“在哪个规模段最有说服力”。

路线官方结果读法
Megatron-LM TP 8.3B 模型在 512 GPU 上达到 15.1 PFLOPS,较强单卡基线的扩展效率约 76%  说明 TP 在宽层 Transformer 上非常有效
GPipe PP 模型切分到多加速器时实现近线性加速,并成功训练 6B、128 层 Transformer  说明同步流水可把极深模型做起来
PipeDream PP 相对数据并行通信量最高下降 95%,time-to-accuracy 最高 5.3× 更快  说明版本化流水在慢网络下很有优势
ZeRO 100B+ 模型在 400 GPU 上训练,达 15 PFLOPS,并报告超线性提速现象  说明“状态分片”本身就能跨入超大模型
ZeRO-Offload 单卡可把可训练模型从约 1.4B 提到 13B,多卡到 128 GPU 近线性提速  说明异构内存换显存非常有效
ZeRO++ 在 384 GPU 上,对 ZeRO-3 最高 2.16× 吞吐提升,通信量降约 4×  说明 ZeRO-3 的瓶颈已转向通信优化
GSPMD 在 2048 TPUv3 cores、1T 参数模型上达到 50%–62% compute utilization  说明编译器分片路线在超大规模上极具竞争力

四、实验复现建议

如果你的目标不是“读懂概念”,而是两天内复现实验并知道瓶颈在哪,最小且可比较的实验设计应满足四个原则:同一模型;同一数据;同一精度;同一全局 batch。 否则比较的不是并行方式,而是在比较“模型、精度、batch、调度、实现细节”的混合物。DeepSpeed 官方对全局 batch 的公式、PyTorch 对 DDP/FSDP/TP/PP 的 API 切分方式,都是围绕这一点设计的。 

建议从一个合成数据的 GPT-like 小模型开始,避免真实数据管道先成为瓶颈;等确认并行栈跑通后,再引入真实 tokenizer、真实 dataloader 与 checkpoint。测量指标建议至少包括:

  • step_time
  • samples/s 或 tokens/s
  • torch.cuda.max_memory_allocated()
  • 通信时间 / 计算时间 / overlap 时间
  • 最终 loss 曲线或 ppl 曲线
  • 对 PP 还要记录每个 stage 的 busy/idle 占比

PyTorch Profiler 可以采集时间与显存;DeepSpeed 通信日志可打印 collective 的消息大小、延迟与吞吐;TORCH_DISTRIBUTED_DEBUG=DETAIL 能直接给出 DDP 的 forward/backward/comm 指标。 

(一)最小复现实验

先从 DDP 基线 开始。它应该是你的“真值参考系”。

TORCH_CPP_LOG_LEVEL=INFO \\
TORCH_DISTRIBUTED_DEBUG=DETAIL \\
NCCL_DEBUG=INFO \\
torchrun –standalone –nproc_per_node=8 bench_ddp.py \\
–model gpt_small –seq-len 2048 –global-batch 128 –precision fp16

这个实验的目标不是极致性能,而是先得到三样东西: 单步时间、峰值显存、DDP 的通信/计算重叠比。如果 DDP 基线都不稳定、经常 hang 或 loss 不对,后面加 TP/PP/ZeRO 只会把问题放大。PyTorch 官方文档明确支持用 TORCH_DISTRIBUTED_DEBUG=DETAIL 打印运行时统计,并提醒 NCCL 后端是 GPU 训练的首选。 

第二步是 FSDP2 / ZeRO-3。目标是回答“同样做同步训练,状态分片到底换来了多少显存,又多付出了多少通信延迟”。

torchrun –standalone –nproc_per_node=8 bench_fsdp.py \\
–model gpt_7b_like –seq-len 4096 –global-batch 64 –precision fp16

deepspeed –num_gpus 8 bench_ds_zero.py \\
–deepspeed ds_zero3.json \\
–model gpt_7b_like –seq-len 4096 –global-batch 64

这组实验最值得画的图是:显存峰值 vs step_time。 如果你看到显存大幅下降、step_time 有一定上升,这是正常现象;如果显存没有明显下降,说明 wrap 粒度、参数 flatten、bucket 或 offload 配置可能没打到位。DeepSpeed 的 ZeRO 教程与 PyTorch FSDP2 教程都清楚说明了“参数在 forward/backward 之前重新聚合”的机制,因此 step_time 不可能完全免费。 

第三步是 TP。建议先做节点内 TP,不要一上来跨节点。PyTorch 官方 TP 教程已经把“TP + FSDP”作为主线示例,这也是实践中最稳的一条路。

torchrun –standalone –nproc_per_node=8 bench_tp_fsdp.py \\
–tp 4 –dp 2 –model gpt_7b_like –seq-len 4096 –global-batch 64

你要重点观察两件事: 一是 显存是否近似按 TP 度下降 二是 GEMM 是否变碎导致计算效率反而下降 如果 TP=2 比 TP=4 更快,很可能不是“TP 没用”,而是 intra-layer collectives 和小矩阵导致你掉进了延迟区。Megatron Bridge 文档明确提到,TP 虽能降状态/激活显存,但更小的 per-GPU workload 会增加 CPU 开销。 

第四步是 PP。建议固定总 GPU 数不变,比较 PP=1、PP=2、PP=4,并同时扫描 micro_batches。

torchrun –standalone –nproc_per_node=8 bench_pp.py \\
–pp 4 –micro-batches 8 –schedule 1f1b \\
–model gpt_deep_like –seq-len 2048 –global-batch 64

如果 micro_batches 太小,你会看到 stage 空转很明显;如果层切分不均匀,你会看到某个 stage 的时间远高于其他 stage。PyTorch 现在提供 GPipe、1F1B、Interleaved 1F1B、Zero Bubble 等 schedule,正是为了让你在这一步系统地比较 bubble 与负载均衡问题。 

(二)关键 API 示例

一个最小 DeepSpeed 训练循环如下,来自官方 API 形态。 

import deepspeed

model_engine, optimizer, _, _ = deepspeed.initialize(
model=model,
model_parameters=model.parameters(),
config=ds_config,
)

for step, batch in enumerate(loader):
loss = model_engine(batch)
model_engine.backward(loss)
model_engine.step()

DeepSpeed 的 gradient_accumulation_steps 是最重要的对照变量之一,因为它同时影响全局 batch 与通信频率。官方配置文档明确指出,梯度累积可以减少梯度平均的频率,从而改善可扩展性,并帮助训练更大的 batch。 

如果你想单独测网络而不受训练框架干扰,nccl-tests 是最应该常备的工具。NVIDIA 官方仓库给出了 all_reduce_perf 等基准,既可以单机 8 卡跑,也能跨多节点跑。它能把“训练栈问题”与“纯通信硬件问题”干净分离。 

# 单机 8 卡 all-reduce 基准
./build/all_reduce_perf -b 8 -e 128M -f 2 -g 8

# 跨节点请用 MPI 版本
mpirun -np 64 ./build/all_reduce_perf_mpi -b 8 -e 256M -f 2 -g 1

最后,如果你要做正式性能分析,优先使用 Nsight Systems 与 PyTorch Profiler。NVIDIA 明确说明 nvprof 已弃用并将在后续 CUDA 版本移除;Nsight Systems 官方则强调应尽量只 profile 性能关键区间。 

nsys profile –trace=cuda,nvtx,osrt -o trace_report \\
torchrun –standalone –nproc_per_node=8 bench_ddp.py

五、优化实践与排查

(一)何时使用哪些技巧

梯度累积适合两种场景:一是你想保持较大的全局 batch 但单卡放不下;二是你想降低每次参数更新前的通信频率。DeepSpeed 官方文档明确把它定义为“在触发梯度归约与优化器 step 前累积多个 micro-batch”,并强调它可改善可扩展性。 

混合精度几乎是现代大模型训练默认选项。PyTorch 官方文档说明了 torch.autocast 与 GradScaler 的标准组合,指出 mixed precision 会根据 op 特征在 float16 / bfloat16 / float32 间分配计算精度,以获得更高速度同时尽量保持数值稳定;DeepSpeed 也同时支持原生 fp16/bf16 与 PyTorch AMP。 

激活检查点用来换显存:PyTorch 官方文档明确写到,checkpointing 是“以额外重计算换内存”;DeepSpeed 进一步把 activation partitioning、CPU checkpointing、contiguous memory 等也纳入了配置项。对于 FSDP、PP、长上下文训练,这往往是必须项。 

Sequence Parallel 在 TP 场景尤其值得单独打开。Megatron Core 官方直接建议“用 TP 就开 SP”,因为它能在 LayerNorm / Dropout 上沿序列维做分片以减激活显存;论文还给出了最高约 5× 的激活内存改善。 

通信压缩要慎用,但在低带宽集群上价值很大。典型代表包括 ZeRO++ 的量化 AllGather / 量化梯度平均,以及 DeepSpeed 的 1-bit Adam / 1-bit LAMB / 0/1 Adam 这类通信高效优化器。需要注意的是,这类方法通常有 warmup 或 freeze step,而且更适合在真正的网络瓶颈出现后再用。 

拓扑感知调度的原则非常简单: TP 尽量节点内; FSDP/DP 常做跨节点; PP 用来沿深度跨主机。 PyTorch 的 DeviceMesh 明确就是为了让用户更容易构建多维 process group;FSDP 官方甚至专门提到了 HYBRID_SHARD 与 NCCL_CROSS_NIC=1 这类拓扑相关调优点。 

(二)常见问题与排查

第一类问题:OOM。 排查顺序建议固定化: 先看模型状态还是激活占主导; 再区分是 forward 峰值还是 backward 峰值; 最后决定是上 checkpoint、换 FSDP/ZeRO-3、还是加 TP/PP。 如果你已经在 FSDP 上,优先检查 wrap 粒度、是否开启 limit_all_gathers、是否存在不必要的大模块一次性 all-gather;如果你在 TP 场景,优先看是否打开了 Sequence Parallel。PyTorch checkpoint 文档提醒,checkpoint 区域 forward/backward 若不一致,可能导致错误或静默错误梯度,所以不要把带有状态分支的代码随便塞进 checkpoint。 

第二类问题:通信瓶颈。 先用 nccl-tests 跑出纯通信上限,再用训练框架看真实通信是否接近上限。如果偏差很大,再开日志。PyTorch 官方建议在 NCCL 故障时设置 NCCL_DEBUG=INFO,对 hang 尤其有帮助的还有 NCCL_DEBUG_SUBSYS=COLL;如果怀疑拓扑识别错误,可用 NCCL_DEBUG_SUBSYS=GRAPH。对于 socket 网络,官方还指出 NCCL_SOCKET_NTHREADS 与 NCCL_NSOCKS_PERTHREAD 有时能提升带宽。 

export NCCL_DEBUG=INFO
export NCCL_DEBUG_SUBSYS=COLL,GRAPH
export TORCH_CPP_LOG_LEVEL=INFO
export TORCH_DISTRIBUTED_DEBUG=DETAIL

第三类问题:作业卡死 / 某些 rank 不前进。 NCCL 官方文档强调,collective 必须在所有 rank 上以相同 count 与 datatype 调用,否则就可能 hang、崩溃或数据损坏。PyTorch 官方提供了 dist.monitored_barrier() 来帮助你直接定位“是哪个 rank 没到齐”;更近一步,还有 Flight Recorder 可在 NCCL watchdog timeout 时自动转储关键信息,帮助做事后分析。 

from datetime import timedelta
import torch.distributed as dist

group_gloo = dist.new_group(backend="gloo")
dist.monitored_barrier(group=group_gloo, timeout=timedelta(seconds=10))

第四类问题:收敛变慢。 如果你是同步派生模式(DDP/FSDP/TP/GPipe),第一怀疑对象通常不是并行逻辑本身,而是全球 batch 变大。应优先检查:学习率是否按 batch 放大、是否做 warmup、是否用到了适合大 batch 的优化器。Goyal 的 linear scaling rule 和 warmup 仍是最普适的第一手段;BERT 类超大 batch 则可考虑 LAMB。对 PP,如果你用了参数版本化或非常激进的流水调度,也要确认它是否影响了与你基线一致的“time-to-quality”。 

第五类问题:负载不均衡。 这在 PP 最多见。看两个指标: 每个 stage 的忙闲时间; 不同 stage 的参数量与激活量。 如果某个 stage 持有 embedding / 大 attention block / lm head,常会成为慢 stage。可用 virtual pipeline、interleaved 1F1B 或 zero-bubble 调度缓解;Megatron Core 也支持 virtual pipeline stage 用于平衡。 

第六类问题:你不知道时间究竟花在哪。 先做 focused profiling。Nsight Systems 官方特别强调,不要默认采整段作业,而是尽量只采关键阶段;PyTorch Profiler 则可直接采 CPU/CUDA 活动、输入形状和显存;DeepSpeed 的 communication logger 还可以直接输出每类 collective 的消息大小、平均延迟、平均带宽,并支持 show_straggler=True 观察慢 rank 拖累。 

# DeepSpeed 通信摘要
import deepspeed.comm as dist
dist.log_summary(show_straggler=True)

六、结论与建议

如果你的团队只有单机多卡,最务实的路线是这样的: 模型 <1B 时,先用 DDP 建基线; 模型 1B–10B 时,优先上 FSDP2 或 ZeRO-3,而不是先想 TP/PP; 只有当“单层本身放不下”或 hidden 特别大时,再在节点内加入 TP。这一路线与 PyTorch、DeepSpeed、Megatron 的官方建议高度一致。 

如果你是小型集群,比如 2–16 台机器,建议把总原则定死: 跨节点先不要用 TP,优先 DP/FSDP/ZeRO;TP 尽量留在节点内;模型真的很深再加 PP。 原因很实在:100Gbps RDMA 对 DDP/FSDP 外层还勉强够用,但对跨节点、逐层频繁同步的 TP 往往太紧。PyTorch pipeline 文档也明确把 PP 标成 bandwidth-limited cluster 的有效手段。 

如果你是大型超算或上百卡集群,主线就应当是 3D 并行: 节点内 TP; 跨节点按模型层深做 PP; 最外层用 DP/FSDP/ZeRO 扩 throughput 与分片状态。 如果是极长上下文,还要把 CP / Sequence Parallel / selective recompute 纳入基线;如果是 TPU/JAX,则优先考虑 GSPMD / explicit sharding 这类编译器辅助路线。 

最后给一份最可执行的优先级清单:

第一优先级是把基线立起来:DDP 能不能跑稳、loss 对不对、网络是不是正常。 第二优先级是解决显存:混合精度 → 激活检查点 → FSDP/ZeRO-3。 第三优先级是解决层太宽/模型太深:节点内 TP,必要时加 PP。 第四优先级才是高级调优:通信压缩、zero-bubble、1-bit optimizer、AutoSP/AutoTP、拓扑感知。 这也是为什么很多团队明明“知道很多并行名词”,但真正上线时还是先从 DDP/FSDP 开始:因为最贵的不是理论最优,而是工程可控。 

主要参考链接

  • PyTorch DistributedDataParallel 官方 API 与设计说明:DDP 语义、bucket、重叠通信与计算。 
  • PyTorch FSDP2 官方教程与 FSDP 文档:all-gather / reduce-scatter 工作流、hybrid shard。 
  • PyTorch Tensor Parallel / Pipeline Parallel / DeviceMesh / DTensor 官方文档。 
  • DeepSpeed ZeRO、ZeRO-Offload、ZeRO-Infinity、ZeRO++ 和通信日志教程。 
  • DeepSpeed Pipeline Parallelism 与 Training API。 
  • Megatron-LM 论文与 Megatron Core 并行策略指南、distributed optimizer 文档。 
  • GPipe 与 PipeDream 原始论文。 
  • Parameter Server 与 DistBelief / Downpour SGD 原始论文。 
  • JAX distributed / sharding 官方文档,及 Mesh-TensorFlow、GShard、GSPMD 论文。 
  • 大 batch 训练与优化器:linear scaling rule / warmup、LAMB。 
  • PyTorch AMP、checkpoint、Profiler、Flight Recorder 与 NCCL / Nsight Systems 文档。 

赞(0)
未经允许不得转载:171主机测评 » 多GPU训练模式深度调研分享
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址