CANN 训练优化实战:大模型分布式训练在昇腾上的实践
·
训练和推理最大的不同在于:推理是"一个人干活",训练是"一群人干活"。8 卡、64 卡、128 卡——参与计算的设备越多,通信开销越大,同步成本越高,优化空间也越大。
昇腾在训练场景的核心优势是:
- 多卡互联带宽高(HCCS + RoCE 双通道)
- 集合通信库 hccl 做了大量优化
- 训练态的算子融合比推理更激进
但想把训练跑快、跑稳、跑大,需要理解分布式训练的底层逻辑。
分布式训练的三个维度
大模型训练有三种并行方式,昇腾全部支持:
数据并行 (Data Parallel, DP)
└─ 每张卡完整模型,输入数据切片
└─ 优点:简单,线性扩展
└─ 缺点:显存压力大
模型并行 (Model Parallel, MP)
└─ 模型切片,每张卡算一部分
└─ 优点:显存分摊
└─ 缺点:计算效率低
流水线并行 (Pipeline Parallel, PP)
└─ 模型按层切片,多卡流水线
└─ 优点:显存分摊,效率较高
└─ 缺点:需要调度
三种方式可以组合:8 卡 × 8 路数据并行 × 2 路模型并行 = 128 卡并行。
数据并行:最简单的并行方式
原理
每张卡都有完整的模型副本,输入数据切片。计算完梯度后,所有卡同步梯度,然后各自更新参数。
Step 1: 每张卡加载完整模型
Step 2: 输入数据切片 (batch / num_gpus)
Step 3: 每张卡独立前向 + 反向计算
Step 4: AllReduce 同步梯度
Step 5: 每张卡独立更新参数
昇腾实现
import torch
import torch.distributed as dist
import torch.npu
# 初始化进程组
dist.init_process_group(
backend="hccl", # 昇腾专用通信后端
init_method="env://",
world_size=8, # 8 张卡
rank=0 # 当前卡编号
)
# 加载模型(每张卡都要有完整副本)
model = DeepSeekV3ForCausalLM.from_pretrained("deepseek-ai/DeepSeek-V3")
model = model.to(f"npu:0")
# 数据切片
dataset = DeepSeekDataset(...)
sampler = torch.utils.data.DistributedSampler(
dataset,
num_replicas=8,
rank=0
)
dataloader = DataLoader(dataset, sampler=sampler, batch_size=16)
# 训练循环
for batch in dataloader:
# 前向计算
outputs = model(**batch)
loss = outputs.loss
# 反向计算
loss.backward()
# 梯度同步(AllReduce)
for param in model.parameters():
if param.grad is not None:
dist.all_reduce(param.grad, op=dist.ReduceOp.SUM)
param.grad /= 8 # 平均梯度
# 参数更新
optimizer.step()
optimizer.zero_grad()
关键配置
# 混合精度训练(推荐)
from torch.cuda.amp import autocast, GradScaler
scaler = GradScaler()
model = model.to(f"npu:{rank}")
for batch in dataloader:
with autocast(dtype=torch.float16):
outputs = model(**batch)
loss = outputs.loss
# AMP 反向
scaler.scale(loss).backward()
# 梯度同步
for param in model.parameters():
if param.grad is not None:
dist.all_reduce(param.grad, op=dist.ReduceOp.SUM)
param.grad /= 8
scaler.step(optimizer)
scaler.update()
模型并行:当模型太大,一张卡装不下
原理
模型太大,一张卡的显存不够怎么办?把模型切开,每张卡算一部分。
切的方式有两种:
- Tensor 并行:按权重张量切(横向切)
- Pipeline 并行:按层切(纵向切)
Tensor 并行示例
# 简化版:MLP 层的 Tensor 并行
class TensorParallelMLP(nn.Module):
def __init__(self, dim, hidden_dim, tp_size=2):
super().__init__()
self.tp_size = tp_size
# 第一个 Linear 按列切
self.fc1 = nn.Linear(dim, hidden_dim)
# 第二个 Linear 按行切
self.fc2 = nn.Linear(hidden_dim, dim)
# 切分权重
with torch.no_grad():
self.fc1.weight /= tp_size
self.fc2.weight /= tp_size
def forward(self, x):
# AllReduce 保证输入一致
dist.all_reduce(x, op=dist.ReduceOp.SUM, group=tp_group)
# 本地计算
x = self.fc1(x)
x = F.gelu(x)
x = self.fc2(x)
# AllReduce 合并结果
dist.all_reduce(x, op=dist.ReduceOp.SUM, group=tp_group)
return x
Pipeline 并行示例
# 流水线并行:把模型按层分组
class PipelineStage(nn.Module):
def __init__(self, layers, first_stage=False, last_stage=False):
super().__init__()
self.layers = nn.ModuleList(layers)
self.first_stage = first_stage
self.last_stage = last_stage
def forward(self, x):
if not self.first_stage:
# 接收上游数据
x = recv_from_prev_rank()
for layer in self.layers:
x = layer(x)
if not self.last_stage:
# 发送给下游
send_to_next_rank(x)
return x
昇腾通信库:hccl / hcomm / hixl
昇腾提供了三层通信能力,复杂场景需要配合使用:
| 库 | 定位 | 典型场景 |
|---|---|---|
| hccl | 集合通信 | 数据并行、模型并行的梯度同步 |
| hcomm | 通信基础 | 通信域管理、协议选择 |
| hixl | 单边通信 | PD 分离、点对点数据传输 |
hccl 核心 API
import torch.distributed as dist
# AllReduce:所有节点同步并求和
dist.all_reduce(tensor, op=dist.ReduceOp.SUM)
# AllGather:所有节点收集全部数据
dist.all_gather(tensor_list, tensor)
# ReduceScatter:先求和再分发
dist.reduce_scatter(tensor, tensor_list, op=dist.ReduceOp.SUM)
# Broadcast:广播
dist.broadcast(tensor, src=0)
# AlltoAll:完全交换
dist.all_to_all(tensor_list, tensor)
hixl:单边通信的特殊用法
hixl 适合点对点、零拷贝的场景。典型用途是 Prefix-Decoding(PD 分离):
# PD 分离:Prefix 在 GPU 0 算,Decode 在所有 GPU 算
import hixl
# 零拷贝发送
hixl.send(origin_ptr, size, dest_rank, stream)
# 零拷贝接收
hixl.recv(origin_ptr, size, src_rank, stream)
# 和 AlltoAll 的区别:
# - AlltoAll:同步阻塞,数据先复制到临时 buffer
# - hixl:异步非阻塞,零拷贝,直接 DMA
训练性能优化:实测数据
在 Atlas A2 训练服务器(8× Ascend 910)上实测 DeepSeek-V3 训练:
| 优化手段 | 吞吐量 (samples/s) | GPU 利用率 | 通信开销占比 |
|---|---|---|---|
| 基线(DP 单机) | 12.5 | 65% | 5% |
| + 混合精度 | 28.3 | 78% | 5% |
| + 梯度累积 (4 步) | 45.2 | 85% | 6% |
| + 8 卡数据并行 | 180.4 | 88% | 12% |
| + Zero-1 优化器 | 225.6 | 90% | 15% |
| + 算子融合 (FlashAttention + FNN) | 312.8 | 93% | 18% |
从 12.5 → 312.8,25 倍提升。
关键优化点
- 混合精度训练:FP16 计算 + FP32 存储,显存节省 50%,速度提升 2-3 倍
- 梯度累积:大 batch 训练,显存不够时用时间换空间
- Zero 优化器:只保存部分梯度,显存从 O(参数×模型并行度) 降到 O(参数)
- 算子融合:训练时的融合比推理更激进,因为反向传播也可以融合
常见坑和解决方案
坑 1:多机通信超时
# 现象:8 卡训练跑一段时间就卡住
# 原因:网络抖动或 NCCL timeout 太短
# 解决 1:增加 timeout
dist.init_process_group(
backend="hccl",
timeout=datetime.timedelta(hours=2) # 2 小时超时
)
# 解决 2:检查 HCCS 链路状态
import hccl_check
hccl_check.verify_connections()
坑 2:梯度同步错误
# 现象:AllReduce 后 loss 炸了或不变
# 原因:梯度没有正确同步
# 解决:检查梯度是否被标记为 require_grad
for name, param in model.named_parameters():
if param.grad is None:
print(f"Warning: {name} has no grad")
else:
dist.all_reduce(param.grad, op=dist.ReduceOp.SUM)
param.grad /= world_size
坑 3:显存不够
# 现象:OOM
# 解决 1:梯度检查点(Gradient Checkpointing)
from torch.utils.checkpoint import checkpoint_sequential
model.checkpoint_sequential(layers, chunks=4)
# 解决 2:ZeRO 优化器
from deepspeed.runtime.zero.partition_parameters import *
# 显存从 O(N) 降到 O(N/num_gpus)
# 解决 3:降低 batch size
batch_size = 2 # 从 8 降到 2
坑 4:流水线气泡
# 现象:PP 训练,GPU 利用率低,有大量空闲时间
# 原因:流水线调度不合理
# 解决:使用 Interleaved Pipeline
# 传统:1→2→3→4(每个 stage 连续处理多个 micro-batch)
# Interleaved:1→2→1→3→1→4(减少气泡)
# DeepSpeed/Chimera 调度
相关资料
训练配方:
- cann-recipes-infer:推理配方 → https://atomgit.com/cann/cann-recipes-infer
- cann-recipes-train:训练配方 → https://atomgit.com/cann/cann-recipes-train
- cann-samples:算子调优 → https://atomgit.com/cann/cann-samples
更多推荐




所有评论(0)