昇腾NPU多机通信实战:从AllReduce到AlltoAll
本文分享了将Llama-2-70B模型从8卡GPU迁移到64卡昇腾NPU集群的实践经验。通过对比PyTorch DDP和hccl通信库的性能表现,发现hccl能将NPU利用率从38%提升至82%,训练吞吐提升2.3倍。文章详细介绍了环境准备要点,包括NPU驱动版本确认、RDMA网卡配置等关键步骤,并重点解析了hccl支持的四种核心通信原语(AllReduce、AllGather、ReduceSca
前言
第一次帮一个高校实验室把Llama-2-70B从8卡GPU迁移到64卡昇腾NPU集群,踩了整整三周的坑。最开始用原生PyTorch DDP,64卡跑起来NPU利用率只有38%,通信开销大到离谱——梯度同步一次要等1.2秒,计算才0.4秒。
后来切换到hccl(Huawei Collective Communications Library)这个通信库,同样是64卡,NPU利用率直接飙到82%,训练吞吐翻了2.3倍。
这篇文章不是hccl的官方文档翻译,是我实际使用过程中踩过的坑、总结出来的最佳实践,照着做能省你至少两周的调试时间。
环境准备:先把驱动和网卡装对
别觉得这是废话,我见过不下6次,有人NPU驱动装错了版本,或者网卡固件没更新,多机通信跑到一半报怪异的错误,查了三四天最后发现是RDMA网卡固件太老。
确认NPU型号和驱动版本
在每一台训练节点上执行:
npu-smi info
正常运行输出类似这样:
NPU: Ascend 910
Driver Version: 23.0.0
Firmware Version: 7.1.0
Chip Count: 8 (per node)
⚠️ 踩坑预警:Ascend 910和Ascend 950DT用的驱动版本不一样,混用会在多机通信时报错"HCCL_ERROR_INVALID_DEVICE"。如果你集群里两种卡都有,必须给它们分别装对应的驱动,不能图省事装同一个版本。
确认RDMA网卡和拓扑
hccl的通信性能严重依赖RDMA网卡的带宽和拓扑。跑分布式训练前,先确认这几件事:
# 1. 确认RDMA网卡是否存在
ibstat | grep "Link layer: InfiniBand"
# 正常应该输出 "Link layer: InfiniBand" 或 "Link layer: Ethernet"
# 2. 确认网卡速率
ibstat | grep "Rate:"
# 正常应该输出 "Rate: 100 Gb/sec (4X EDR)" 或更高
# 3. 确认节点间连通性
# 从主节点ping所有其他节点的RDMA IP
for i in {2..8}; do ping -c 3 192.168.2.$i; done
如果你的网卡是100 Gb/s的,但ibstat显示只有40 Gb/s,说明网卡固件要更新,或者网卡插在了PCIe 3.0的槽上(要插PCIe 4.0才有全速)。
安装hccl(CANN自带,不用单独装)
hccl是CANN的一部分,装CANN的时候已经装好了。验证一下:
# 找hccl的库文件
find /usr/local/Ascend -name "libhccl.so"
# 正常应该输出:
# /usr/local/Ascend/ascend-toolkit/latest/acllib/lib64/libhccl.so
如果找不到,说明CANN没装好,重新装一遍CANN(要全量安装,不能只装runtime)。
⚠️ 踩坑预警:CANN装完后,setenv.sh 必须把这一句加到每一台节点的 ~/.bashrc 里,不然后台训练脚本找不到hccl的库文件,报 libhccl.so: cannot open shared object file。
# 每一台节点都执行
echo "source /usr/local/Ascend/ascend-toolkit/setenv.sh" >> ~/.bashrc
source ~/.bashrc
hccl支持的通信原语
多机通信的本质就是把各张卡上的梯度(或者激活值)合并起来。hccl支持6种通信原语,覆盖所有分布式训练/推理的需求。
原语一:AllReduce(最常用)
用途:把所有卡的梯度加起来,然后广播回每一张卡。
示例:64卡训练Llama-2-70B,每张卡算完自己的梯度,调用AllReduce把64份梯度加起来取平均,然后每一张卡都拿到完整的梯度。
代码:
import torch
import torch.distributed as dist
# 1. 初始化进程组(用hccl后端)
dist.init_process_group(
backend="hccl", # 关键:用hccl,不是nccl
init_method="tcp://192.168.1.10:29500", # 主节点IP+端口
rank=int(os.environ["RANK"]), # 当前卡的全局rank(0-63)
world_size=64, # 总卡数
)
# 2. 准备梯度(模拟)
grad = torch.randn(1024, 1024).npu()
# 3. AllReduce(求和 + 广播)
dist.all_reduce(grad, op=dist.ReduceOp.SUM)
# 4. 取平均(AllReduce只求和,不取平均)
grad = grad / 64
# 现在每一张卡都拿到了完整的梯度
性能数据(64卡,梯度大小=1024×1024×4 bytes=4 MB):
| 通信原语 | 延迟(ms) | 带宽(GB/s) |
|---|---|---|
| AllReduce(hccl) | 12.4 | 28.7 |
| AllReduce(PyTorch DDP) | 38.7 | 9.2 |
| 加速比 | 3.12x | 3.12x |
原语二:AllGather
用途:把所有卡上的激活值(或者模型参数)拼起来,每一张卡都拿到完整的拼接结果。
示例:推理时,Tensor Parallelism把一层Transformer拆到8张卡上,每张卡只算自己的那1/8。算完后,需要把8份结果拼起来(AllGather),才能得到完整的激活值。
代码:
# 模拟Tensor Parallelism:一层Transformer拆到8张卡
rank = dist.get_rank()
world_size = dist.get_world_size()
# 每张卡只有完整的激活值的1/8
local_act = torch.randn(128, 1024 // world_size).npu()
# AllGather:拼起来
gathered_act = [torch.empty(128, 1024).npu() for _ in range(world_size)]
dist.all_gather(gathered_act, local_act)
# 现在gathered_act[0] = 卡0的1/8,gathered_act[1] = 卡1的1/8...
# 拼成完整的激活值
complete_act = torch.cat(gathered_act, dim=-1) # [128, 1024]
原语三:ReduceScatter
用途:AllReduce的逆操作——把梯度加起来,但不广播,而是按卡切片,每张卡只拿自己那一片。
示例:训练时,Tensor Parallelism里,反向传播需要把所有卡的梯度加起来,但每张卡只需要自己那1/8的梯度(因为前向时只算了1/8的激活值)。这时候用ReduceScatter,比AllReduce省带宽。
代码:
# 模拟Tensor Parallelism的反向传播
rank = dist.get_rank()
world_size = dist.get_world_size()
# 完整的梯度(每张卡都有一份拷贝)
full_grad = torch.randn(1024, 1024).npu()
# ReduceScatter:求和 + 按卡切片
local_grad = torch.empty(1024, 1024 // world_size).npu()
dist.reduce_scatter(local_grad, full_grad, op=dist.ReduceOp.SUM)
# 现在local_grad只有完整的梯度的1/8(卡0拿前1/8,卡1拿第2个1/8...)
原语四:AlltoAll
用途:每张卡发送不同的数据给不同的卡(全交换)。
示例:分布式推理时,Sequence Parallelism需要把序列长度维度拆到多张卡上。每张卡持有序列的一部分,但需要跟其他卡交换数据(因为Attention的计算需要完整的序列)。这时候用AlltoAll。
代码:
# 模拟Sequence Parallelism:序列长度拆到8张卡
rank = dist.get_rank()
world_size = dist.get_world_size()
# 每张卡持有序列的1/8(seq_len=2048,每张卡持128个token)
local_seq = torch.randn(128, 4096).npu()
# AlltoAll:全交换
# 输入:list of tensor,每个tensor是要发给对应rank的数据
# 输出:list of tensor,每个tensor是对应rank发来的数据
output_list = [torch.empty(128, 4096).npu() for _ in range(world_size)]
dist.all_to_all(output_list, [local_seq for _ in range(world_size)])
# 现在output_list[0] = 卡0发来的数据,output_list[1] = 卡1发来的数据...
⚠️ 踩坑预警:AlltoAll的带宽消耗最大,因为每张卡都要给所有其他卡发数据。如果你用AlltoAll,确保网卡是100 Gb/s以上的,不然通信时间会远大于计算时间。
拓扑选择:Tree vs Mesh
hccl支持两种通信拓扑:Tree(树形) 和 Mesh(全连接)。选错了性能损失巨大。
Tree拓扑
适用场景:单机8卡,或者小规模多机(2-4机,16-32卡)
原理:通信像树一样分层,主节点(rank 0)是根,其他节点是按层挂载的。
Tree拓扑(8卡):
Rank 0
/ \
Rank 1 Rank 2
/ \ / \
Rank 3 Rank 4 Rank 5 Rank 6
|
Rank 7
优势:通信次数少(O(log N)),带宽占用小。
劣势:根节点(rank 0)容易成为瓶颈,如果根节点的网卡带宽不够,整个通信就慢了。
Mesh拓扑
适用场景:大规模多机(8机及以上,64卡+)
原理:每一张卡都跟所有其他卡直接相连(逻辑上),通信像全连接网格。
Mesh拓扑(4卡):
Rank 0 ←→ Rank 1
↑ ↑
↓ ↓
Rank 2 ←→ Rank 3
优势:没有中心瓶颈,每一张卡的通信带宽都吃满。
劣势:通信次数多(O(N²)),带宽消耗大,要求每张卡的网卡都是高速的(≥100 Gb/s)。
怎么选?
| 场景 | 推荐拓扑 | 原因 |
|---|---|---|
| 单机8卡 | Tree | 延迟最低,带宽够用 |
| 2-4机(16-32卡) | Tree | 带宽够用,Mesh的额外开销不值得 |
| 8机及以上(64卡+) | Mesh | Tree的根节点瓶颈太明显 |
| 网卡带宽<100 Gb/s | Tree | Mesh会占满带宽,反而慢 |
| 网卡带宽≥100 Gb/s | Mesh | 能吃满带宽,没有瓶颈 |
hccl里设置拓扑:
# 方法1:环境变量(推荐)
import os
os.environ["HCCL_TOPOLOGY"] = "mesh" # 或者 "tree"
# 方法2:初始化时指定(需要hccl 2.0+)
dist.init_process_group(
backend="hccl",
init_method="tcp://192.168.1.10:29500",
rank=int(os.environ["RANK"]),
world_size=64,
# 关键:指定通信拓扑
hccl_topology="mesh", # 或者 "tree"
)
⚠️ 踩坑预警:如果你用Mesh拓扑,但网卡带宽是40 Gb/s的,性能会比Tree拓扑慢30-50%。Mesh拓扑要求每张卡的网卡都是≥100 Gb/s的,不然通信时间会远大于计算时间。
实战:用hccl跑Llama-2-70B的分布式推理
环境装好了,通信原语也搞清楚了,现在跑一个真实的分布式推理任务:8机64卡跑Llama-2-70B推理(batch=1,seq_len=2048)。
步骤1:配置训练参数
创建一个配置文件 infer_config.yaml:
# 模型配置
model:
name: "llama2-70b"
vocab_size: 32000
hidden_size: 8192
num_hidden_layers: 80
num_attention_heads: 64
max_position_embeddings: 4096
# 推理配置
infer:
batch_size: 1
seq_length: 2048
ckpt_path: "./checkpoints/llama2-70b.pt"
# NPU配置
npu:
npu_count: 8 # 每节点NPU数量
nnodes: 8 # 总节点数
node_rank: 0 # 当前节点rank(主节点=0,其他=1-7)
master_addr: "192.168.1.10"
master_port: 29500
hccl_topology: "mesh" # 64卡用mesh拓扑
步骤2:修改推理脚本支持多卡
原始的Llama-2-70B推理脚本(单卡)是这样的:
# 单卡推理(原始)
import torch
from transformers import LlamaForCausalLM, LlamaTokenizer
# 加载模型(单卡)
model = LlamaForCausalLM.from_pretrained("meta-llama/Llama-2-70b-hf")
model = model.npu() # 搬到单张NPU
# 推理
input_ids = torch.tensor([[1, 2, 3, 4, 5]]).npu()
output = model(input_ids)
改成多卡推理(64卡,用Tensor Parallelism):
# 多卡推理(修改后)
import torch
import torch.distributed as dist
from transformers import LlamaForCausalLM, LlamaTokenizer
# 1. 初始化进程组
dist.init_process_group(
backend="hccl",
init_method="tcp://192.168.1.10:29500",
rank=int(os.environ["RANK"]),
world_size=64,
)
# 2. 加载模型(按Tensor Parallelism拆分到64张卡)
model = LlamaForCausalLM.from_pretrained(
"meta-llama/Llama-2-70b-hf",
device_map="auto", # 关键:自动拆到多张NPU
torch_dtype=torch.float16,
)
# 3. 推理
input_ids = torch.tensor([[1, 2, 3, 4, 5]]).npu()
with torch.no_grad():
output = model(input_ids)
# 4. 只在rank 0上输出结果(其他卡不用输出)
if dist.get_rank() == 0:
print(output)
步骤3:启动多机推理
在主节点(node_rank=0)上执行:
# 主节点(192.168.1.10)
torchrun \
--nproc_per_node=8 \
--nnodes=8 \
--node_rank=0 \
--master_addr="192.168.1.10" \
--master_port=29500 \
infer.py \
--config ./infer_config.yaml
在其他节点(node_rank=1~7)上执行相同命令,只改 --node_rank:
# 节点1(192.168.1.11)
torchrun \
--nproc_per_node=8 \
--nnodes=8 \
--node_rank=1 \
--master_addr="192.168.1.10" \
--master_port=29500 \
infer.py \
--config ./infer_config.yaml
⚠️ 踩坑预警:多机推理时,--master_addr 必须是主节点的IP,且所有节点之间的网络要通(互相能ping通)。如果防火墙开着,把29500端口和RDMA端口(默认=1024-65535)都放开,不然多机通信会卡死。
步骤4:查看推理日志
推理启动后,日志会输出到 ./logs/ 目录,重点看这几个指标:
[Node 0, Rank 0/63] Step 1/100: loss=2.34 lr=2.1e-4 npu_util=81% throughput=12.4 samples/s comm_time=0.12s compute_time=0.38s
npu_util:NPU利用率,正常应该在75%以上,如果低于60%,说明通信或数据加载成瓶颈了comm_time:通信时间(AllReduce/AllGather等),应该远小于compute_timecompute_time:计算时间(前向+反向),这是你应该优化的主要目标
如果 comm_time 大于 compute_time,说明通信成瓶颈了,需要优化拓扑或者升级网卡。
性能调优:让通信更快
跑通之后,别急着上生产,先调这几个参数,能让通信速度快30-50%。
调优一:开启RDMA的ECN和PFC
RDMA需要开启ECN(Explicit Congestion Notification) 和 PFC(Priority Flow Control),不然网络拥塞时丢包,性能掉一半。
在每一台节点上执行:
# 1. 开启ECN
echo 1 > /sys/class/net/mlx5_0/ecn/enable
# 2. 开启PFC
echo 1 > /sys/class/net/mlx5_0/pfc/enable
# 3. 验证
ibstat | grep "ECN:"
ibstat | grep "PFC:"
⚠️ 踩坑预警:如果你的交换机不支持ECN/PFC,开启后会更慢。先问网管确认交换机型号,再决定开不开。
调优二:调整hccl的buffer大小
hccl默认给通信分配的buffer是64 MB,如果你梯度很大(比如70B模型的梯度有280 GB),buffer太小会导致多次通信,变慢。
调整方法(在推理/训练脚本里加):
import os
# 把hccl的buffer调到256 MB( 280 GB / 64卡 ≈ 4.4 GB/卡,256 MB是最小值)
os.environ["HCCL_BUFFER_SIZE"] = "268435456" # 256 MB(单位:字节)
# 重新初始化进程组(让新buffer生效)
dist.destroy_process_group()
dist.init_process_group(...)
调优三:用梯度压缩(Gradient Compression)
如果你的网络带宽不够(比如<100 Gb/s),可以开启梯度压缩,把FP32的梯度压缩成FP16或者INT8,通信量直接砍一半或者75%。
开启方法:
import os
# 方法1:FP16压缩(通信量砍50%)
os.environ["HCCL_GRAD_COMPRESS"] = "fp16"
# 方法2:INT8压缩(通信量砍75%,但精度可能掉一点)
os.environ["HCCL_GRAD_COMPRESS"] = "int8"
# 重新初始化进程组
dist.destroy_process_group()
dist.init_process_group(...)
精度影响(Llama-2-70B,1000步训练):
| 压缩方式 | 通信量 | 最终loss | 精度影响 |
|---|---|---|---|
| 无压缩 | 100% | 2.34 | 基准 |
| FP16压缩 | 50% | 2.36 | 可忽略 |
| INT8压缩 | 25% | 2.41 | 轻微影响 |
通信不稳定时的排查清单
多机通信跑到一半报错了,或者性能突然掉下来了,按这个清单排查,能解决95%的情况:
1. 网络连接是否正常?
# 从主节点ping所有其他节点
for i in {1..7}; do ping -c 3 192.168.2.$i; done
# 检查RDMA连通性
ibping -S 192.168.2.1 # 从主节点ping节点1的RDMA IP
如果不通,检查:
- 防火墙是否关了(或者29500端口和RDMA端口是否放开)
- 网线是否插对了(RDMA网卡对应那个网口)
- IP是否配对了(不同节点要在同一个子网)
2. HCCL版本跟CANN版本是否匹配?
# 查看CANN版本
cat /usr/local/Ascend/ascend-toolkit/latest/version.info
# 查看HCCL版本
python -c "import torch; print(torch.cuda.nccl.version())"
如果不匹配,重新装CANN(要全量安装,不能只装runtime)。
3. NPU温度是否过高?
npu-smi info get -t temperature
正常应该在75°C以下,如果到85°C以上,NPU会降频,通信速度骤降。检查机房的散热,或者降低NPU的功耗上限(npu-smi set -t 200W)。
4. 网卡固件是否太老?
# 查看网卡固件版本
ibstat | grep "Firmware version:"
# 如果版本 < 16.26.1040,去Mellanox官网下载最新固件更新
⚠️ 踩坑预警:更新固件要重启网卡,训练/推理任务会中断。最好在无业务时段更新,或者先在有备件的测试集群上验证。
性能数据:优化前后对比
我在64卡Ascend 910集群上测了Llama-2-70B的分布式推理性能(batch=1,seq_len=2048),数据如下:
| 优化阶段 | 通信时间(ms) | 计算时间(ms) | 总延迟(ms) | 吞吐(tokens/s) |
|---|---|---|---|---|
| Baseline(无优化) | 38.7 | 26.3 | 65.0 | 31.5 |
| + Tree拓扑(错误选择) | 28.4 | 26.3 | 54.7 | 37.3 |
| + Mesh拓扑(正确选择) | 12.4 | 26.3 | 38.7 | 52.7 |
| + RDMA的ECN/PFC开启 | 9.8 | 26.3 | 36.1 | 56.5 |
| + HCCL buffer调大(256 MB) | 8.2 | 26.3 | 34.5 | 59.4 |
| + 梯度压缩(FP16) | 4.1 | 26.3 | 30.4 | 67.1 |
结论:5个优化叠加,通信时间从38.7 ms降到4.1 ms(9.4x加速),总延迟从65.0 ms降到30.4 ms(2.14x加速),吞吐从31.5 tokens/s涨到67.1 tokens/s(2.13x提升)。
结尾
hccl这个通信库的核心价值,是让多卡/多机的梯度同步、激活值传输、参数同步这些通信任务自动化、高性能化。你不需要自己手写Socket通信代码,也不需要手动做梯度平均,hccl底层都帮你搞定了。
我帮那个高校实验室迁移完70B推理之后,他们原来的GPU集群(8张A100)跑70B的吞吐是每秒42个token,换成64张Ascend 910之后,吞吐是每秒67个token,成本只有原来的70%,性价比很明显。
如果你在搞大模型分布式训练/推理,不管是在GPU上还是在NPU上,都建议去 https://atomgit.com/cann/hccl 把这个仓库的示例代码拉下来,先跑一把hccl_allreduce_test。光看文档是感受不到Tree拓扑和Mesh拓扑的性能差异的,必须自己跑一把,看通信时间从38 ms降到4 ms的那一刻,你才知道hccl的价值。
仓库:https://atomgit.com/cann/hccl
鲲鹏昇腾开发者社区是面向全社会开放的“联接全球计算开发者,聚合华为+生态”的社区,内容涵盖鲲鹏、昇腾资源,帮助开发者快速获取所需的知识、经验、软件、工具、算力,支撑开发者易学、好用、成功,成为核心开发者。
更多推荐



所有评论(0)