昇腾NPU多机通信实战:从AllReduce到AlltoAll 前言第一次帮一个高校实验室把Llama-2-70B从8卡GPU迁移到64卡昇腾NPU集群踩了整整三周的坑。最开始用原生PyTorch DDP64卡跑起来NPU利用率只有38%通信开销大到离谱——梯度同步一次要等1.2秒计算才0.4秒。后来切换到hcclHuawei 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|grepLink layer: InfiniBand# 正常应该输出 Link layer: InfiniBand 或 Link layer: Ethernet# 2. 确认网卡速率ibstat|grepRate:# 正常应该输出 Rate: 100 Gb/sec (4X EDR) 或更高# 3. 确认节点间连通性# 从主节点ping所有其他节点的RDMA IPforiin{2..8};doping-c3192.168.2.$i;done如果你的网卡是100 Gb/s的但ibstat显示只有40 Gb/s说明网卡固件要更新或者网卡插在了PCIe 3.0的槽上要插PCIe 4.0才有全速。安装hcclCANN自带不用单独装hccl是CANN的一部分装CANN的时候已经装好了。验证一下# 找hccl的库文件find/usr/local/Ascend-namelibhccl.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。# 每一台节点都执行echosource /usr/local/Ascend/ascend-toolkit/setenv.sh~/.bashrcsource~/.bashrchccl支持的通信原语多机通信的本质就是把各张卡上的梯度或者激活值合并起来。hccl支持6种通信原语覆盖所有分布式训练/推理的需求。原语一AllReduce最常用用途把所有卡的梯度加起来然后广播回每一张卡。示例64卡训练Llama-2-70B每张卡算完自己的梯度调用AllReduce把64份梯度加起来取平均然后每一张卡都拿到完整的梯度。代码importtorchimporttorch.distributedasdist# 1. 初始化进程组用hccl后端dist.init_process_group(backendhccl,# 关键用hccl不是ncclinit_methodtcp://192.168.1.10:29500,# 主节点IP端口rankint(os.environ[RANK]),# 当前卡的全局rank0-63world_size64,# 总卡数)# 2. 准备梯度模拟gradtorch.randn(1024,1024).npu()# 3. AllReduce求和 广播dist.all_reduce(grad,opdist.ReduceOp.SUM)# 4. 取平均AllReduce只求和不取平均gradgrad/64# 现在每一张卡都拿到了完整的梯度性能数据64卡梯度大小1024×1024×4 bytes4 MB通信原语延迟ms带宽GB/sAllReducehccl12.428.7AllReducePyTorch DDP38.79.2加速比3.12x3.12x原语二AllGather用途把所有卡上的激活值或者模型参数拼起来每一张卡都拿到完整的拼接结果。示例推理时Tensor Parallelism把一层Transformer拆到8张卡上每张卡只算自己的那1/8。算完后需要把8份结果拼起来AllGather才能得到完整的激活值。代码# 模拟Tensor Parallelism一层Transformer拆到8张卡rankdist.get_rank()world_sizedist.get_world_size()# 每张卡只有完整的激活值的1/8local_acttorch.randn(128,1024//world_size).npu()# AllGather拼起来gathered_act[torch.empty(128,1024).npu()for_inrange(world_size)]dist.all_gather(gathered_act,local_act)# 现在gathered_act[0] 卡0的1/8gathered_act[1] 卡1的1/8...# 拼成完整的激活值complete_acttorch.cat(gathered_act,dim-1)# [128, 1024]原语三ReduceScatter用途AllReduce的逆操作——把梯度加起来但不广播而是按卡切片每张卡只拿自己那一片。示例训练时Tensor Parallelism里反向传播需要把所有卡的梯度加起来但每张卡只需要自己那1/8的梯度因为前向时只算了1/8的激活值。这时候用ReduceScatter比AllReduce省带宽。代码# 模拟Tensor Parallelism的反向传播rankdist.get_rank()world_sizedist.get_world_size()# 完整的梯度每张卡都有一份拷贝full_gradtorch.randn(1024,1024).npu()# ReduceScatter求和 按卡切片local_gradtorch.empty(1024,1024//world_size).npu()dist.reduce_scatter(local_grad,full_grad,opdist.ReduceOp.SUM)# 现在local_grad只有完整的梯度的1/8卡0拿前1/8卡1拿第2个1/8...原语四AlltoAll用途每张卡发送不同的数据给不同的卡全交换。示例分布式推理时Sequence Parallelism需要把序列长度维度拆到多张卡上。每张卡持有序列的一部分但需要跟其他卡交换数据因为Attention的计算需要完整的序列。这时候用AlltoAll。代码# 模拟Sequence Parallelism序列长度拆到8张卡rankdist.get_rank()world_sizedist.get_world_size()# 每张卡持有序列的1/8seq_len2048每张卡持128个tokenlocal_seqtorch.randn(128,4096).npu()# AlltoAll全交换# 输入list of tensor每个tensor是要发给对应rank的数据# 输出list of tensor每个tensor是对应rank发来的数据output_list[torch.empty(128,4096).npu()for_inrange(world_size)]dist.all_to_all(output_list,[local_seqfor_inrange(world_size)])# 现在output_list[0] 卡0发来的数据output_list[1] 卡1发来的数据...踩坑预警AlltoAll的带宽消耗最大因为每张卡都要给所有其他卡发数据。如果你用AlltoAll确保网卡是100 Gb/s以上的不然通信时间会远大于计算时间。拓扑选择Tree vs Meshhccl支持两种通信拓扑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卡MeshTree的根节点瓶颈太明显网卡带宽100 Gb/sTreeMesh会占满带宽反而慢网卡带宽≥100 Gb/sMesh能吃满带宽没有瓶颈hccl里设置拓扑# 方法1环境变量推荐importos os.environ[HCCL_TOPOLOGY]mesh# 或者 tree# 方法2初始化时指定需要hccl 2.0dist.init_process_group(backendhccl,init_methodtcp://192.168.1.10:29500,rankint(os.environ[RANK]),world_size64,# 关键指定通信拓扑hccl_topologymesh,# 或者 tree)踩坑预警如果你用Mesh拓扑但网卡带宽是40 Gb/s的性能会比Tree拓扑慢30-50%。Mesh拓扑要求每张卡的网卡都是≥100 Gb/s的不然通信时间会远大于计算时间。实战用hccl跑Llama-2-70B的分布式推理环境装好了通信原语也搞清楚了现在跑一个真实的分布式推理任务8机64卡跑Llama-2-70B推理batch1seq_len2048。步骤1配置训练参数创建一个配置文件infer_config.yaml# 模型配置model:name:llama2-70bvocab_size:32000hidden_size:8192num_hidden_layers:80num_attention_heads:64max_position_embeddings:4096# 推理配置infer:batch_size:1seq_length:2048ckpt_path:./checkpoints/llama2-70b.pt# NPU配置npu:npu_count:8# 每节点NPU数量nnodes:8# 总节点数node_rank:0# 当前节点rank主节点0其他1-7master_addr:192.168.1.10master_port:29500hccl_topology:mesh# 64卡用mesh拓扑步骤2修改推理脚本支持多卡原始的Llama-2-70B推理脚本单卡是这样的# 单卡推理原始importtorchfromtransformersimportLlamaForCausalLM,LlamaTokenizer# 加载模型单卡modelLlamaForCausalLM.from_pretrained(meta-llama/Llama-2-70b-hf)modelmodel.npu()# 搬到单张NPU# 推理input_idstorch.tensor([[1,2,3,4,5]]).npu()outputmodel(input_ids)改成多卡推理64卡用Tensor Parallelism# 多卡推理修改后importtorchimporttorch.distributedasdistfromtransformersimportLlamaForCausalLM,LlamaTokenizer# 1. 初始化进程组dist.init_process_group(backendhccl,init_methodtcp://192.168.1.10:29500,rankint(os.environ[RANK]),world_size64,)# 2. 加载模型按Tensor Parallelism拆分到64张卡modelLlamaForCausalLM.from_pretrained(meta-llama/Llama-2-70b-hf,device_mapauto,# 关键自动拆到多张NPUtorch_dtypetorch.float16,)# 3. 推理input_idstorch.tensor([[1,2,3,4,5]]).npu()withtorch.no_grad():outputmodel(input_ids)# 4. 只在rank 0上输出结果其他卡不用输出ifdist.get_rank()0:print(output)步骤3启动多机推理在主节点node_rank0上执行# 主节点192.168.1.10torchrun\--nproc_per_node8\--nnodes8\--node_rank0\--master_addr192.168.1.10\--master_port29500\infer.py\--config./infer_config.yaml在其他节点node_rank1~7上执行相同命令只改--node_rank# 节点1192.168.1.11torchrun\--nproc_per_node8\--nnodes8\--node_rank1\--master_addr192.168.1.10\--master_port29500\infer.py\--config./infer_config.yaml踩坑预警多机推理时--master_addr必须是主节点的IP且所有节点之间的网络要通互相能ping通。如果防火墙开着把29500端口和RDMA端口默认1024-65535都放开不然多机通信会卡死。步骤4查看推理日志推理启动后日志会输出到./logs/目录重点看这几个指标[Node 0, Rank 0/63] Step 1/100: loss2.34 lr2.1e-4 npu_util81% throughput12.4 samples/s comm_time0.12s compute_time0.38snpu_utilNPU利用率正常应该在75%以上如果低于60%说明通信或数据加载成瓶颈了comm_time通信时间AllReduce/AllGather等应该远小于compute_timecompute_time计算时间前向反向这是你应该优化的主要目标如果comm_time大于compute_time说明通信成瓶颈了需要优化拓扑或者升级网卡。性能调优让通信更快跑通之后别急着上生产先调这几个参数能让通信速度快30-50%。调优一开启RDMA的ECN和PFCRDMA需要开启ECNExplicit Congestion Notification和PFCPriority Flow Control不然网络拥塞时丢包性能掉一半。在每一台节点上执行# 1. 开启ECNecho1/sys/class/net/mlx5_0/ecn/enable# 2. 开启PFCecho1/sys/class/net/mlx5_0/pfc/enable# 3. 验证ibstat|grepECN:ibstat|grepPFC:踩坑预警如果你的交换机不支持ECN/PFC开启后会更慢。先问网管确认交换机型号再决定开不开。调优二调整hccl的buffer大小hccl默认给通信分配的buffer是64 MB如果你梯度很大比如70B模型的梯度有280 GBbuffer太小会导致多次通信变慢。调整方法在推理/训练脚本里加importos# 把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%。开启方法importos# 方法1FP16压缩通信量砍50%os.environ[HCCL_GRAD_COMPRESS]fp16# 方法2INT8压缩通信量砍75%但精度可能掉一点os.environ[HCCL_GRAD_COMPRESS]int8# 重新初始化进程组dist.destroy_process_group()dist.init_process_group(...)精度影响Llama-2-70B1000步训练压缩方式通信量最终loss精度影响无压缩100%2.34基准FP16压缩50%2.36可忽略INT8压缩25%2.41轻微影响通信不稳定时的排查清单多机通信跑到一半报错了或者性能突然掉下来了按这个清单排查能解决95%的情况1. 网络连接是否正常# 从主节点ping所有其他节点foriin{1..7};doping-c3192.168.2.$i;done# 检查RDMA连通性ibping-S192.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-cimport torch; print(torch.cuda.nccl.version())如果不匹配重新装CANN要全量安装不能只装runtime。3. NPU温度是否过高npu-smi info get-ttemperature正常应该在75°C以下如果到85°C以上NPU会降频通信速度骤降。检查机房的散热或者降低NPU的功耗上限npu-smi set -t 200W。4. 网卡固件是否太老# 查看网卡固件版本ibstat|grepFirmware version:# 如果版本 16.26.1040去Mellanox官网下载最新固件更新踩坑预警更新固件要重启网卡训练/推理任务会中断。最好在无业务时段更新或者先在有备件的测试集群上验证。性能数据优化前后对比我在64卡Ascend 910集群上测了Llama-2-70B的分布式推理性能batch1seq_len2048数据如下优化阶段通信时间ms计算时间ms总延迟ms吞吐tokens/sBaseline无优化38.726.365.031.5 Tree拓扑错误选择28.426.354.737.3 Mesh拓扑正确选择12.426.338.752.7 RDMA的ECN/PFC开启9.826.336.156.5 HCCL buffer调大256 MB8.226.334.559.4 梯度压缩FP164.126.330.467.1结论5个优化叠加通信时间从38.7 ms降到4.1 ms9.4x加速总延迟从65.0 ms降到30.4 ms2.14x加速吞吐从31.5 tokens/s涨到67.1 tokens/s2.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