前言昇腾NPU上的CANN生态里有一个hccl仓库。你训练一个大模型比如 GPT-7B用数据并行Data Parallelism把模型复制到 8 张 NPU 卡上每张卡跑不同的数据批次然后算梯度再让所有卡上的梯度求平均。这个让所有卡上的梯度求平均的操作就是集合通信Collective Communication。具体算法叫AllReduce。如果你用 CPU 做梯度聚合那就要把每张卡的梯度拷到 CPU 内存里求平均再拷回每张卡。这个拷来拷去的开销可能比计算梯度本身的开销还大。如果你用 NPU 原生的集合通信库比如 hccl那梯度聚合就在 NPU 之间直接做数据不用来回拷贝延迟低得多。hccl 是昇腾 CANN 生态里的集合通信库。它对应 NVIDIA 的 NCCLNVIDIA Collective Communications Library。如果你用过 NCCL那 hccl 的概念和接口跟它很像。一、集合通信的核心问题与算法分类为什么需要集合通信数据并行训练中的梯度同步假设你用数据并行训练一个模型。你有 8 张 NPU 卡每张卡上都有完整的模型副本。训练流程是每张卡算自己批次数据的梯度本地梯度。让所有卡上的梯度求平均全局梯度。每张卡用全局梯度更新模型参数。步骤 2 就是集合通信要解决的问题怎么高效地在多张卡之间求平均。最简单的方法是把所有卡的梯度都拷到 CPU 内存里求平均再拷回每张卡。但这个方法很慢——因为数据要在 CPU 和 NPU 之间来回拷贝。更高效的方法是让 NPU 卡之间直接通信不求助于 CPU。这就是集合通信库hccl / NCCL要做的事情。主流集合通信操作AllReduce / AllGather / ReduceScatter / Broadcast集合通信有很多种操作最常见的有AllReduce所有卡上的数据归约后结果广播给所有卡。归约操作可以是求和“求最大值”求最小值等。梯度同步用的是求和归约。AllGather每张卡上有一些数据AllGather 把所有卡上的数据收集起来拼接成一个更大的数据然后广播给所有卡。比如在张量并行Tensor Parallelism里你需要把多张卡上的局部张量拼接成完整张量就会用到 AllGather。ReduceScatter跟 AllGather 相反。每张卡上有一些数据ReduceScatter 先做归约比如求和然后把归约结果分散到不同的卡上每张卡只保留归约结果的一部分。比如在序列并行Sequence Parallelism里你会用到 ReduceScatter。Broadcast一张卡上的数据广播给所有其他卡。比如你要在所有卡上加载同一个模型权重就会用到 Broadcast。Ring 算法 vs Tree 算法各自适用场景和带宽利用率分析集合通信的算法决定了数据怎么在卡之间流动。两种最经典的算法是Ring和Tree。Ring 算法环形算法把所有卡排成一个环。数据沿着环流动每张卡只跟左右两张卡通信。优点每张卡只跟两张邻居卡通信通信链路短延迟低。缺点带宽利用率不高因为数据要沿着环一圈一圈地传。Tree 算法树形算法把所有卡排成一棵树。数据从根节点往下传或者从叶子节点往上传。优点带宽利用率高因为数据可以同时往多个方向传。缺点通信链路长延迟高尤其是树的深度很大时。适用场景小消息消息大小 某个阈值用 Ring 算法延迟低。大消息消息大小 某个阈值用 Tree 算法带宽利用率高。hccl 会自动根据消息大小选择 Ring 还是 Tree 算法。这个阈值是可以调的通过环境变量HCCL_RING_THRESHOLD和HCCL_TREE_THRESHOLD。关键点算法选择和硬件拓扑NVLink / RoCE 网络强相关上面说的Ring vs Tree选择还跟硬件拓扑强相关。如果你的 NPU 卡之间用的是NVLinkNVIDIA 的卡间高速互联技术带宽很高延迟很低那 Ring 算法的延迟优势就不明显了——因为 NVLink 的带宽很高Tree 算法的带宽利用率优势更能发挥。如果你的 NPU 卡之间用的是RoCERDMA over Converged Ethernet以太网 RDMA那 Tree 算法的延迟劣势就更明显了——因为以太网的延迟比 NVLink 高。hccl 会自动检测硬件拓扑然后选择合适的算法。但你也可以手动覆盖通过环境变量HCCL_ALGO。二、hccl 在 CANN 五层架构中的位置hccl 位于昇腾计算服务层AOL 算子库旁回顾 CANN 的五层架构第 1 层AscendCL昇腾计算语言层第 2 层昇腾计算服务层AOL 算子库、AOE 调优引擎、Framework Adapter第 3 层昇腾计算编译层Graph Compiler、BiSheng/ATC 编译器第 4 层昇腾计算执行层Runtime、Graph Executor、HCCL、DVPP、AIPP第 5 层昇腾计算基础驱动和底层管理组件hccl 在第 2 层昇腾计算服务层跟 AOL 算子库ops-nn、ops-math 等并列。但是——这里有一个容易混淆的点hccl 的高层 API比如hccl.AllReduce是在第 2 层暴露给应用开发者的。但 hccl 的底层实现比如 Ring AllReduce 的 chunk 传输会调用第 4 层的 Runtime 和第 5 层的驱动。所以hccl 是一个跨层的库——它的 API 在第 2 层但它的实现会深入到第 4 层和第 5 层。hccl 与 runtime、HCCL高层 API的关系这里有一个命名混淆hccl和HCCL是两个东西。hccl小写是仓库名atomgit.com/cann/hccl也是集合通信原语的 NPU 原生实现底层。HCCL大写是 hccl 仓库暴露的高层 API类似于 NCCL 的 API。你在 PyTorch DDP 里调用的dist.all_reduce()背后就是调的 HCCL 的HCCL_AllReduce()。所以调用链路是这样的PyTorch DDP (Python) ↓ torch.distributed (Python) ↓ HCCL 高层 API (C/C) ↓ hccl 底层实现 (CANN 第 2 层) ↓ Runtime (CANN 第 4 层) ↓ NPU 驱动 (CANN 第 5 层) ↓ NPU 硬件调用链路PyTorch DDP → CANN Adapter → hccl API → 底层通信后端更具体地如果你用 PyTorch 的DistributedDataParallelDDP做数据并行训练调用链路是这样的PyTorch DDP你在 Python 里写model DDP(model)。DDP 会在每次反向传播后自动做梯度同步。PyTorch CANN AdapterDDP 的梯度同步会调到torch.distributed.all_reduce()。这个函数的后端实现取决于你初始化ProcessGroup时指定的后端backendhccl。HCCL 高层 APItorch.distributed.all_reduce()会调到HCCL_AllReduce()。hccl 底层实现HCCL_AllReduce()会调用 hccl 仓库里的 AllReduce 实现Ring 或者 Tree 算法。底层通信后端hccl 的底层实现会通过 Runtime 调用 NPU 驱动最终在硬件上做数据传输通过 PCIe 或者 NVLink 或者 RoCE。关键点hccl 不是一个库是一组集合通信原语的 NPU 原生实现hccl 仓库里不是只有一个libhccl.so库。它是一组集合通信原语AllReduce、AllGather、ReduceScatter、Broadcast 等的 NPU 原生实现。每个原语都有多种算法实现Ring、Tree、等等。hccl 会根据消息大小、硬件拓扑、当前 NPU 的负载情况自动选择最合适的算法。三、Ring AllReduce 在昇腾 NPU 上的硬件加速Ring AllReduce 的经典流程scatter-reduce allgather 两个阶段Ring AllReduce 是 AllReduce 操作的一种经典算法。它分两个阶段阶段 1Scatter-Reduce分散归约把所有卡排成一个环。数据被切分成 N 个 chunkN 是卡的数量。每个卡持有一个 chunk然后沿着环传递。每传递一次相邻的两张卡就做一次归约比如求和。经过 N-1 次传递后每个卡上都持有一个归约后的 chunk但不同的卡持有不同的 chunk。阶段 2AllGather全收集把阶段 1 的结果每个卡持有一个归约后的 chunk沿着环传递。每传递一次每张卡就把收到的 chunk 拼接到自己持有的数据上。经过 N-1 次传递后每张卡上都持有完整的归约结果。这两个阶段合起来就是 Ring AllReduce。昇腾 NPU 的硬件 DMA 引擎如何加速 Ring 算法中的 chunk 传输在经典的 Ring AllReduce 实现里chunk 的传输是一跳一跳的——chunk 从卡 i 传到卡 i1再传到卡 i2等等。在昇腾 NPU 上这个 chunk 传输可以用硬件 DMA 引擎Direct Memory Access直接内存访问来加速。DMA 引擎的作用是让数据在卡之间传输时不需要 CPU或者 NPU 的 Scalar 单元的参与——DMA 引擎直接把数据从卡 i 的内存拷贝到卡 i1 的内存。这样CPU/NPU 就可以去干别的事情而数据传输在后台进行。hccl 的 Ring AllReduce 实现会利用 NPU 的硬件 DMA 引擎来传输 chunk。这比用软件CPU 拷贝快得多。与 NCCL 兼容层的差异HCCL_RDMA_SPLIT_THRESHOLD 参数的作用hccl 提供了一个NCCL 兼容层——如果你原来的代码是用 NCCL 写的你可以把import nccl改成import hccl然后代码基本不用改。但是hccl 和 NCCL 的底层实现有差异。其中一个差异是跨节点通信时的 RDMA 分块阈值。如果你在多节点比如 2 个服务器每个服务器有 8 张 NPU 卡上做集合通信那数据要通过 RDMARemote DMA在节点之间传输。RDMA 传输有一个分块问题太大的 RDMA 请求可能会被网络层拆分成多个包导致延迟增加。HCCL_RDMA_SPLIT_THRESHOLD这个参数就是用来控制多大的 RDMA 请求需要被拆分。如果你发现跨节点通信很慢可以尝试调小HCCL_RDMA_SPLIT_THRESHOLD让 RDMA 请求更小减少网络层拆分开销。但这个参数调得太小又会导致RDMA 请求数量太多反而增加开销。需要根据你的网络环境以太网 vs InfiniBand来调整。关键点跨节点场景的 DMA 链分段处理是性能陷阱上面讲了 DMA 引擎可以加速 chunk 传输。但是——在跨节点场景下DMA 链的分段处理可能会成为性能陷阱。具体来说如果你在多节点上做 Ring AllReduce那 chunk 的传输路径可能是卡 0节点 0 → 卡 1节点 0 → ... → 卡 7节点 0 ↓ 卡 0节点 1 → 卡 1节点 1 → ... → 卡 7节点 1这里chunk 要从节点 0 的卡 7传到节点 1 的卡 0。这个跨节点的传输要通过 RDMA。RDMA 的传输延迟比节点内通过 NVLink 或者 PCIe的传输延迟高一个数量级。所以跨节点场景的性能瓶颈通常不是DMA 引擎的带宽而是RDMA 链路的延迟。hccl 的 Ring AllReduce 实现会尽量让节点内的传输用 Ring 算法节点间的传输用 Tree 算法因为 Tree 算法的带宽利用率更高。但这个混合算法的实现很复杂可能会引入额外的开销。四、通信与计算重叠的实现机制为什么通信和计算重叠是分布式训练的刚需在分布式训练里“通信”梯度同步和计算前向反向传播是串行的前向传播计算反向传播计算梯度同步通信参数更新计算如果通信和计算能重叠overlap那训练时间就能缩短。具体来说在反向传播的最后几层算梯度的同时可以把已经算好的梯度前面几层的先做同步通信。这样计算和通信就重叠了。hccl 如何利用 NPU 的异步执行引擎实现通信-计算重叠NPU 的执行模型是异步的你调一个算子比如 MatMul它立刻返回但实际的矩阵乘法还在 NPU 上跑。hccl 利用这个异步执行模型来实现通信-计算重叠把梯度分成多份不是等所有梯度都算完再同步而是算好一份就同步一份。用不同的 Stream 做计算和通信NPU 支持多个 Stream执行流不同 Stream 上的算子可以并行执行。你可以把计算反向传播放在一个 Stream 上把通信梯度同步放在另一个 Stream 上。用 Event 做同步如果通信需要等计算的结果可以用 Event事件来同步两个 Stream。hccl 的 AllReduce 实现默认就是异步的立刻返回实际通信在后台进行。所以如果你在 PyTorch DDP 里用backendhccl那梯度同步和参数更新是重叠的PyTorch DDP 会用不同 Stream 做计算和通信。Stream 优先级反转问题技能文件已提到及其解决方案这里有一个坑Stream 优先级反转Stream Priority Inversion。NPU 的 Stream 有优先级高优先级 vs 低优先级。高优先级的 Stream 上的算子会先被执行。但如果你有多个 Stream比如一个做计算一个做通信而且它们的优先级设置不当就可能导致优先级反转低优先级的 Stream 上的算子反而先被执行。具体来说如果你把计算放在高优先级 Stream 上把通信放在低优先级 Stream 上那计算会先被执行通信会被饿死一直等不到执行。解决方案把通信放在高优先级Stream 上把计算放在低优先级Stream 上。这样通信就不会被计算饿死。hccl 的默认 Stream 优先级是高。但如果你手动创建了 Stream需要注意优先级设置。关键点不理解 Stream 模型分布式训练性能调优就是瞎调上面讲的通信-计算重叠和Stream 优先级都依赖于 NPU 的 Stream 模型。如果你不理解 Stream 模型什么是 Stream、什么是 Event、怎么用多个 Stream 做并行执行那分布式训练的性能调优就是瞎调——你不知道瓶颈在哪也不知道怎么解决。所以建议先读懂 NPU 的 Stream 模型可以参考 CANN 的官方文档《AscendCL 异步执行模型》再去做分布式训练的性能调优。五、与 hcomm 的分工边界hccl 负责集合通信原语AllReduce 等hccl 的职责是实现集合通信原语AllReduce、AllGather、ReduceScatter、Broadcast 等。这些原语的特点是所有参与的卡都要知道彼此的存在比如AllReduce 需要所有卡上的数据做归约。hccl 的 API 是集合通信层面的。比如HCCL_AllReduce()你需要指定参与归约的所有卡的 rank_id。hcomm 负责主机-设备通信和节点间点对点通信hcomm 是另一个仓库atomgit.com/cann/hcomm它的职责是实现点对点通信原语Send、Recv、等等。这些原语的特点是只有两个参与者发送方和接收方不需要所有卡都参与。hcomm 的 API 是点对点通信层面的。比如hcomm_send()你只需要指定发送给哪张卡。实际使用中的调用关系分布式训练框架先调 hccl 做梯度同步再通过 hcomm 做数据搬运在实际的分布式训练框架里hccl 和 hcomm 是协同工作的。举个例子你用 PyTorch DDP 做数据并行训练。梯度同步DDP 会调torch.distributed.all_reduce()背后是 hccl 的HCCL_AllReduce()。这是集合通信。数据搬运如果你的任务是把数据从 CPU 内存搬到 NPU 显存或者把 NPU 显存里的数据搬到另一张 NPU 卡上那就要用 hcomm 的hcomm_send()和hcomm_recv()。这是点对点通信。所以hccl 和 hcomm 的分工是hccl 负责多卡协作的通信hcomm 负责两卡之间的通信。关键点两个库都涉及通信但抽象层级不同hccl 和 hcomm 都涉及通信但它们的抽象层级不同hccl抽象层级高。你不需要关心数据怎么在卡之间传只需要调HCCL_AllReduce()hccl 会自动帮你选算法Ring 或者 Tree、自动做数据传输。hcomm抽象层级低。你需要自己指定发送给哪张卡“发多少数据”“数据格式是什么”。所以大多数应用开发者只需要用 hccl 就够了。只有当你需要精细控制通信过程的时候比如你要实现一个自定义的分布式训练算法才会用到 hcomm。使用前 vs 使用后效率对比表格假设你有一个分布式训练任务用数据并行在 8 张 NPU 卡上训练一个模型。你在两个环境下跑环境 A用 CPU 做梯度聚合或者用 NCCL但数据在 CPU 和 NPU 之间来回搬运。环境 B用 hcclNPU 原生集合通信库 NPU 硬件加速。下面是概括性描述的效率对比表格不捏造具体数字对比维度使用前CPU 梯度聚合或 NCCL 数据搬运使用后hccl NPU 硬件加速性能提升AllReduce 延迟小消息基线数据搬运开销大显著降低分布式训练关键路径通信-计算重叠不支持或支持很差完全支持NPU 异步执行引擎硬件加速核心优势跨节点扩展能力受限RDMA 链路带宽利用率低优秀hccl 自动选最优算法大规模集群必备为什么会有这个性能提升核心原因有三个数据全程在 NPU 之间传输省掉了搬运开销。用 CPU 做梯度聚合的话数据要在 NPU 和 CPU 之间来回拷贝。用 hccl 的话数据在 NPU 之间直接传不用来回拷贝。NPU 的硬件 DMA 引擎加速了数据传输。hccl 的 Ring AllReduce 实现会用 NPU 的硬件 DMA 引擎来传输 chunk。这比用软件CPU 拷贝快得多。通信-计算重叠减少了训练时间。hccl 的 AllReduce 是异步的可以跟计算重叠。这样训练时间就缩短了。代码段 1PyTorch DDP 中使用 hccl 的代码示例高层 APIimporttorchimporttorch.distributedasdistimporttorch.multiprocessingasmpdeftrain(rank,world_size):# 初始化进程组指定后端为 hccldist.init_process_group(backendhccl,rankrank,world_sizeworld_size)# 创建模型用 DDP 包装modelYourModel().to(rank)modeltorch.nn.parallel.DistributedDataParallel(model,device_ids[rank])# 损失函数和优化器criteriontorch.nn.CrossEntropyLoss()optimizertorch.optim.SGD(model.parameters(),lr0.001)# 训练循环forepochinrange(100):# 前向传播outputsmodel(inputs.to(rank))losscriterion(outputs,labels.to(rank))# 反向传播DDP 会自动做梯度同步背后调 hccl 的 AllReduceoptimizer.zero_grad()loss.backward()# 这里会触发梯度同步通信# 参数更新计算optimizer.step()# 这里跟梯度同步重叠如果用了异步 AllReduce# 销毁进程组dist.destroy_process_group()if__name____main__:world_size8# 8 张 NPU 卡mp.spawn(train,args(world_size,),nprocsworld_size,joinTrue)这段代码展示了在 PyTorch DDP 里使用 hccl的方法。关键点backendhccl初始化进程组时指定后端为hccl。这样DDP 的梯度同步就会调到 hccl 的HCCL_AllReduce()。loss.backward()会触发梯度同步DDP 在反向传播后会自动调用dist.all_reduce()做梯度同步。这个all_reduce()就是通过 hccl 做的。通信-计算重叠optimizer.step()参数更新和梯度同步通信是重叠的——因为 hccl 的 AllReduce 是异步的。代码段 2Ring AllReduce 的 chunk 切分逻辑示意代码# 这是 Ring AllReduce 的 chunk 切分逻辑示意不是完整代码defring_allreduce_scatter_reduce(data,rank,world_size): Ring AllReduce 的 scatter-reduce 阶段阶段 1 data: 本地的数据比如梯度张量 rank: 当前卡的 rank_id (0 ~ world_size-1) world_size: 总卡数 chunk_sizelen(data)//world_size chunks[data[i*chunk_size:(i1)*chunk_size]foriinrange(world_size)]# Scatter-Reduce 阶段沿着环传递 chunk做归约forstepinrange(world_size-1):send_chunkchunks[(rank-step)%world_size]recv_chunkchunks[(rank-step-1)%world_size]# 发送到邻居卡rank-1hcomm_send(send_chunk,dst(rank-1)%world_size)# 从邻居卡接收数据并做归约求和recv_chunkhcomm_recv(src(rank-1)%world_size)returnchunks# 每个卡上持有一个归约后的 chunkdefring_allreduce_allgather(chunks,rank,world_size): Ring AllReduce 的 allgather 阶段阶段 2 # AllGather 阶段沿着环传递 chunk拼接成完整结果forstepinrange(world_size-1):send_chunkchunks[(rank-step)%world_size]recv_chunkchunks[(rank-step-1)%world_size]# 发送到邻居卡rank-1hcomm_send(send_chunk,dst(rank-1)%world_size)# 从邻居卡接收数据并拼接recv_chunkhcomm_recv(src(rank-1)%world_size)# 现在每张卡上都持有完整的归约结果resultconcatenate(chunks)returnresult这段伪代码展示了 Ring AllReduce 的两个阶段scatter-reduce 和 allgather的 chunk 切分逻辑。关键点数据切分成world_size个 chunk每张卡持有一个 chunk。沿着环传递每张卡只跟左右邻居通信。Scatter-Reduce 阶段做归约每传递一次就把收到的 chunk 跟自己持有的 chunk 做归约求和。AllGather 阶段做拼接每传递一次就把收到的 chunk 拼接到自己持有的数据上。实际 hccl 的实现会比这个复杂得多要处理错误、要做流控、要利用 DMA 引擎加速等等。但核心思路是一样的。代码段 3Stream 优先级设置代码示例避免优先级反转importtorchimporttorch_npu# 创建两个 Stream一个做计算一个做通信compute_streamtorch_npu.npu.Stream(priority0)# 低优先级数值越大优先级越低comm_streamtorch_npu.npu.Stream(priority1)# 高优先级# 在 compute_stream 上做计算反向传播withtorch_npu.npu.stream(compute_stream):lossmodel(inputs)loss.backward()# 梯度算好后放到 comm_stream 上做 AllReduce# 在 comm_stream 上做通信梯度同步withtorch_npu.npu.stream(comm_stream):# 等待 compute_stream 的梯度算好用 Event 同步eventtorch_npu.npu.Event()event.record(compute_stream)event.wait(comm_stream)# 做 AllReduce梯度同步dist.all_reduce(gradients)# 背后调 hccl 的 HCCL_AllReduce()这段代码展示了怎么设置 Stream 优先级避免优先级反转。关键点priority0低优先级给计算 Stream计算可以慢一点但不要阻塞通信。priority1高优先级给通信 Stream通信要快不要被计算饿死。用 Event 做同步如果通信 Stream 需要等计算 Stream 的梯度算好可以用 Event 来同步。实际 PyTorch DDP 会自动处理 Stream 优先级和 Event 同步。但你如果手动写分布式训练代码需要注意这些问题。代码段 4hccl 与 hcomm 协同使用的代码示例importtorchimporttorch.distributedasdistfromhcommimporthcomm_send,hcomm_recv# 假设 hcomm 有 Python 绑定defcustom_distributed_train(rank,world_size):# 初始化进程组用 hccl 做集合通信dist.init_process_group(backendhccl,rankrank,world_sizeworld_size)# 阶段 1用 hccl 做梯度同步集合通信gradientscompute_gradients()dist.all_reduce(gradients)# 调 hccl 的 HCCL_AllReduce()# 阶段 2用 hcomm 做数据搬运点对点通信ifrank0:# 卡 0 把数据发送给卡 1hcomm_send(gradients,dst1)elifrank1:# 卡 1 从卡 0 接收数据recv_datahcomm_recv(src0)# 阶段 3继续训练...这段代码展示了hccl 和 hcomm 协同使用的方法。关键点阶段 1 用 hccl梯度同步是集合通信用 hccl 的all_reduce。阶段 2 用 hcomm数据搬运是点对点通信用 hcomm 的send和recv。分工明确hccl 负责多卡协作的通信hcomm 负责两卡之间的通信。总结这篇文章从集合通信的核心问题讲起到 hccl 在 CANN 五层架构里的位置、Ring AllReduce 在 NPU 上的硬件加速、通信与计算重叠的实现机制最后给出了 hccl 和 hcomm 的分工边界。核心要点回顾集合通信是分布式训练的基础hccl 是昇腾 NPU 上的集合通信库对应 NVIDIA 的 NCCL。hccl 位于 CANN 的第 2 层昇腾计算服务层但它的实现会深入到第 4 层和第 5 层。Ring AllReduce 分两个阶段scatter-reduce allgatherhccl 会用 NPU 的硬件 DMA 引擎加速 chunk 传输。通信-计算重叠是分布式训练的刚需hccl 利用 NPU 的异步执行引擎来实现重叠。hccl 负责集合通信原语AllReduce 等hcomm 负责点对点通信原语Send/Recv 等。仓库链接https://atomgit.com/cann/hccl