你有一台 8 卡昇腾 910 服务器,花了大价钱配的。

模型也不大,LLaMA-2-7B,数据集也就几十GB。按理说,8 张卡并行训练,速度应该是单卡的 8 倍吧?

结果一跑,发现加速比只有 3 倍。

4 张卡的钱,只换了 3 张卡的性能。老板问起来,你怎么解释?

“这个…通信有开销…”

什么叫通信开销?具体是哪些通信?优化空间有多大?

这些问题,如果你答不上来,今天这篇文章就是为你写的。

冲突切入:算得快 ≠ 整体快

很多人有个误区:只要计算够快,训练就快。

错了。

分布式训练是个"木桶效应":最短的那块板子,决定了整体性能。

在昇腾NPU的 8 卡训练里,最短的板子,往往不是计算,而是通信

为什么?

假设你有 8 张卡,每张卡的计算能力是 100 TFLOPS。8 张卡加起来,理论算力 800 TFLOPS。

但这 8 张卡不是孤立的。它们之间需要传递梯度(Gradient)、激活值(Activation)。

数据从卡 1 到卡 2,靠的是通信带宽

昇腾 910 的卡间互联带宽是 PCIe Gen4 x16,约 32 GB/s。

32 GB/s vs 100 TFLOPS —— 差了 3 个数量级。

算得再快,数据传不过来,也是白搭。

设计理念:HCCL 是怎么解决通信瓶颈的?

HCCL(Huawei Collective Communication Library,昇腾集合通信库)是昇腾CANN的一部分,专门负责多卡、多节点之间的数据同步

它的设计理念有三个关键词:

🎯 关键词1:集合通信(Collective Communication)

HCCL 实现的是"集合通信",不是"点对点通信"。

区别是什么?

  • 点对点通信(P2P):卡 1 发给卡 2,只涉及两个节点
  • 集合通信(Collective):卡 1、卡 2、卡 3…卡 8 一起参与,所有节点都要同步

常见的集合通信操作有 4 种:

操作 英文 场景 类比
AllReduce 归约并广播 梯度同步 全班对答案,每个人都知道所有人的答案
Broadcast 广播 参数同步 老师发卷子,每个人收到完整的卷子
AllGather 收集并广播 张量并行 拼图,每个人把自己那块拼到公共区域
ReduceScatter 归约并分发 数据并行 收作业,组长收齐后分发给每个人一部分

🎯 关键词2:分层通信(Hierarchical Communication)

HCCL 的第二个设计理念是分层通信

在 8 卡服务器上,8 张卡不是全互联的。它们通过 PCIe 连接到 CPU,CPU 之间可能还有 RoCE 网卡。

HCCL 把通信分成三层:

第一层:卡内通信(intra-chip)

  • 同一个 AI Core 内的 Vector Core 和 Cube Core 共享缓存
  • 这层通信几乎没开销

第二层:卡间通信(intra-node)

  • 8 张卡在同一个服务器内
  • 走 PCIe 或 NVLink(如果有)
  • 延迟:1-10 微秒

第三层:节点间通信(inter-node)

  • 多个服务器之间
  • 走 RoCE(RDMA over Converged Ethernet)或 TCP
  • 延迟:10-100 微秒

HCCL 的优化策略是:尽量在低层完成通信,减少高层通信的参与。

类比:

就像你在公司内部开会:

  • 同一楼层的人 → 走过去叫一声(卡间通信)
  • 不同楼层的人 → 发企业微信(节点间通信)
  • 外地同事 → 发邮件(跨数据中心)

HCCL 的目标是:能当面说,就不发微信;能发微信,就不发邮件。

🎯 关键词3:拓扑感知(Topology-Aware)

HCCL 的第三个设计理念是感知硬件拓扑,选择最优通信路径

昇腾 910 服务器的硬件拓扑,通常是这样的:

 ┌─────────────────────────────────────────┐
 │ 服务器 │
 │ ┌────┐ ┌────┐ ┌────┐ ┌────┐ │
 │ │卡0 │ │卡1 │ │卡2 │ │卡3 │ → PCIe Switch
 │ └────┘ └────┘ └────┘ └────┘ │
 │ ┌────┐ ┌────┐ ┌────┐ ┌────┐ │
 │ │卡4 │ │卡5 │ │卡6 │ │卡7 │ → PCIe Switch
 │ └────┘ └────┘ └────┘ └────┘ │
 │ ↓ │
 │ CPU + RoCE │
 └─────────────────────────────────────────┘
 ↓
 ┌─────────────────┐
 │ 其他服务器 │
 └─────────────────┘

问题:AllReduce 怎么走?

有两种策略:

策略1:Ring-AllReduce

  • 把 8 张卡组成一个环
  • 卡 0 → 卡 1 → 卡 2 → … → 卡 7 → 卡 0
  • 每张卡只跟相邻的两张卡通信
  • 通信量:2 × (N-1) × 数据量 / N

策略2:Tree-AllReduce

  • 把 8 张卡组织成一棵树
  • 先从叶子节点往上归约,再从上往下广播
  • 通信量:2 × log₂(N) × 数据量

HCCL 会自动感知你的硬件拓扑,选择最优的通信算法。

在 8 卡服务器上,Ring-AllReduce 通常比 Tree-AllReduce 快,因为 PCIe Switch 的带宽比 RoCE 高。

实战:怎么用 HCCL 写分布式训练代码?

PyTorch + HCCL 分布式训练

import os
import torch
import torch.distributed as dist
import torch_npu

# 1. 设置环境变量
os.environ["MASTER_ADDR"] = "127.0.0.1"
os.environ["MASTER_PORT"] = "29500"
os.environ["RANK"] = str(os.environ.get("RANK", "0"))
os.environ["WORLD_SIZE"] = str(os.environ.get("WORLD_SIZE", "8"))

# 2. 初始化分布式训练
dist.init_process_group(backend="hccl") # ← 关键:backend 是 hccl,不是 nccl

# 3. 设置当前进程的 rank
rank = dist.get_rank()
world_size = dist.get_world_size()

# 4. 你的模型和数据
model = YourModel().to(f"npu:{rank}")
optimizer = torch.optim.Adam(model.parameters())

# 5. 训练循环
for epoch in range(num_epochs):
 for batch in dataloader:
 # 前向传播
 output = model(batch.to(f"npu:{rank}"))
 loss = criterion(output, target)
 
 # 反向传播
 loss.backward()
 
 # ⚠️ 关键:梯度同步(AllReduce)
 # 每个 rank 都有自己的梯度,需要 AllReduce 求平均
 for param in model.parameters():
 dist.all_reduce(param.grad, op=dist.ReduceOp.SUM)
 param.grad /= world_size
 
 optimizer.step()
 optimizer.zero_grad()

踩坑提示:

⚠️ dist.init_process_group(backend="hccl") —— 这里的 backend 是 hccl,不是 nccl。如果你从 NVIDIA 迁移过来,要改这个参数。

⚠️ 梯度同步要在 optimizer.step() 之前,否则参数更新就不一致了。

手动调 AllReduce(进阶)

有时候,你想自己控制通信的时机和内容,可以手动调 HCCL:

#include "hccl/hccl.h"

// 创建一个 AllReduce 操作
HcclOpHandle handle;
hcclAllReduce(
 send_buf, // 发送缓冲区
 recv_buf, // 接收缓冲区
 data_size, // 数据大小
 HCCL_DATA_TYPE_FP32, // 数据类型
 HCCL_OP_SUM, // 操作:求和
 comm, // 通信域
 stream // 计算 stream
);

// 等待通信完成
hcclStreamSynchronize(stream);

性能调优:HCCL 有哪些调优技巧?

技巧1:选择合适的通信算法

import torch_npu

# 强制使用 Ring-AllReduce
torch.distributed.distributed_c10d._HCCL_AVAILABLE = True
os.environ["HCCL_ALGO"] = "Ring" # 可选:Ring / Tree / DoubleTree

# 强制使用 Tree-AllReduce
os.environ["HCCL_ALGO"] = "Tree"

技巧2:梯度累加(Gradient Accumulation)

如果你的 batch size 太小,通信开销占比就高。

梯度累加的意思是:多算几步,再同步一次。

accumulation_steps = 4
effective_batch_size = batch_size * accumulation_steps * world_size

for i, batch in enumerate(dataloader):
 loss = model(batch)
 loss = loss / accumulation_steps # 归一化
 loss.backward()
 
 if (i + 1) % accumulation_steps == 0:
 # 只在这里同步一次,通信次数减少到 1/4
 dist.all_reduce_grads(model)
 optimizer.step()
 optimizer.zero_grad()

技巧3:混合精度通信

如果你的梯度是 FP32,但 AllReduce 的带宽不够,可以先把梯度转成 FP16,再通信,最后在更新参数时转回 FP32。

# 梯度转 FP16
grad_fp16 = grad_fp32.to(torch.float16)

# AllReduce(FP16)
dist.all_reduce(grad_fp16, op=dist.ReduceOp.SUM)

# 转回 FP32,更新参数
grad_fp32.copy_(grad_fp16.to(torch.float32))

总结:HCCL 优化的本质是什么?

回到开头的问题:为什么你的 8 卡训练加速比只有 3 倍?

因为通信拖了后腿。

HCCL 优化的本质,是让计算和通信重叠(overlap),减少通信等待时间

具体来说:

  • 计算的同时,后台异步发起 AllReduce
  • AllReduce 完成后,立刻有梯度可用
  • 计算和通信交替进行,“无缝衔接”

一句话总结:

HCCL = 昇腾NPU的"交通指挥系统"。它决定数据走哪条路、什么时候走、跟谁一起走。优化 HCCL,就是让"堵车"变成"一路绿灯"。

HCCL 支持了新的通信算法(具体看 release notes),升级版本可能有惊喜

仓库链接(纯文本URL,不用Markdown):
https://atomgit.com/cann/hccl
https://atomgit.com/cann/cann-samples
https://atomgit.com/cann/cann-recipes-train(里面有分布式训练的高级教程)

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐