前言

在昇腾CANN软件栈的完整版图中,集合通信库hccl占据着一个既基础又关键的位置。当开发者基于MindSpore或PyTorch训练大模型时,跨卡、跨节点、跨交换机的梯度同步与张量聚合,全部依赖hccl在底层完成数据的搬运与归约。它的性能上限直接决定了分布式训练的扩展效率——一个通信实现不够高效的集合通信库,会让千卡集群的实际算力利用率大打折扣。hccl的完整源码与开发文档已在开源社区公开,理解hccl的算法选择与工程细节,是从"能跑分布式训练"走向"跑好分布式训练"的必经之路。

hccl的核心算法实现

集合通信操作看似只有AllReduce、AllGather、Broadcast、ReduceScatter等有限的几种原语,但每种原语在不同网络拓扑、不同数据规模下存在多种算法实现,它们之间的性能差异可以非常显著。hccl在算法层面的核心工作,就是为每种原语提供多种可切换的实现路径,并在运行时根据集群状态自动选择最优路径。

Ring AllReduce

Ring AllReduce是分布式训练中最为人熟知的集合通信算法。它的基本思路是将参与通信的N张卡组织成一个逻辑环,数据被切分为N个等大的chunk,每个通信步中每张卡只向环上的下一张卡发送一个chunk,同时从上一张卡接收一个chunk。完成ReduceScatter阶段后,每个卡持有一段已经完成规约的数据片段;再经过AllGather阶段,所有卡拿到完整的规约结果。

hccl对Ring AllReduce的实现有几个工程层面的细节值得展开。第一是chunk数量的选择。理论上chunk数量等于参与通信的卡数就能让流水线充分填满,但实际实现中hccl会根据消息大小和链路带宽动态调整chunk粒度——小消息场景下chunk过碎反而会因为协议开销拉低效率,hccl会在这种场景下合并chunk以减少通信步数。第二是环的构建策略。hccl支持根据物理拓扑自动排列环序,优先将同一NUMA节点、同一PCIe Switch下的卡排列在相邻位置,使环上相邻卡之间的跳数最小化,降低跨Switch通信的占比。

// hccl中Ring AllReduce的通信步计算逻辑(简化示意)
HcclResult RingAllReduce::CalcLoopCount(size_t dataBytes, size_t alignedRankSize) {
    // 每个chunk的大小 = 总数据量 / 参与卡数
    size_t chunkSize = dataBytes / alignedRankSize;
    // ReduceScatter阶段需要 rankSize-1 步完成部分规约
    // AllGather阶段同样需要 rankSize-1 步完成全量广播
    loopCount_ = (alignedRankSize - 1) * 2;
    // 当数据量过小时,合并chunk减少步数
    if (chunkSize < MIN_CHUNK_THRESHOLD) {
        loopCount_ = std::max(static_cast<size_t>(1), dataBytes / OPTIMAL_CHUNK_SIZE);
    }
    return HCCL_SUCCESS;
}

这段代码展示了hccl在Ring AllReduce中对通信步数的动态决策。如果严格按照rank数切分chunk,小消息场景下步数过多、每步传输量过小,协议开销占比就会急剧上升。通过设定MIN_CHUNK_THRESHOLD阈值并在低于阈值时重新计算步数,hccl在小消息场景下牺牲了一定的流水线并行度,但换来了更少的通信步和更高的带宽利用率。这是一种典型的以并行度换效率的工程权衡。

Tree AllReduce

Tree AllReduce采用了截然不同的数据流动方式。它将通信过程组织为两棵互补的树——一棵负责Reduce阶段,将数据从叶节点向根节点汇聚规约;另一棵负责Broadcast阶段,将根节点的规约结果向叶节点扩散。与Ring AllReduce相比,Tree AllReduce的通信步数与log(N)成正比,而非与N成正比,在卡数较多时理论上具有步数优势。

hccl实现的Tree AllReduce并非简单的二叉树。它支持多叉树结构,分支因子可根据网络拓扑动态调整。在典型配置中,hccl会使用二叉树或四叉树:二叉树的逻辑深度最小,但每步中父节点需要聚合的子节点数据量固定;四叉树增加了每步的聚合数据量,但减少了总步数。hccl在初始化阶段会根据集群规模和网络带宽积选择树叉数,避免在延迟敏感型场景中因为树深度过大而拖慢整体进度。

Tree AllReduce的一个工程难点是根节点的负载集中问题。在Reduce阶段,根节点需要接收并规约所有子节点的数据,其网络带宽容易成为瓶颈。hccl通过构建双树结构来缓解这一问题——两棵树的根节点分别放在不同的卡上,让AllReduce的Reduce和Broadcast阶段分别由不同的根节点承担,避免单卡负载过重。

// hccl中Tree AllReduce的树构建逻辑(简化示意)
HcclResult TreeAllReduce::BuildTree(const TopoInfo &topoInfo) {
    // 根据集群规模选择分支因子
    uint32_t branchFactor = 2;  // 默认二叉树
    if (topoInfo.rankSize > 64) {
        branchFactor = 4;  // 大规模集群使用四叉树减少深度
    }
    // 构建Reduce树(根节点选在rank 0)
    reduceTree_ = BuildBalancedTree(topoInfo, branchFactor, ROOT_RANK_0);
    // 构建Broadcast树(根节点选在最后一个rank,分散负载)
    bcastTree_ = BuildBalancedTree(topoInfo, branchFactor, topoInfo.rankSize - 1);
    return HCCL_SUCCESS;
}

双树结构的核心目的是消除单点瓶颈。如果Reduce和Broadcast共用同一棵树、同一个根节点,根节点在Reduce阶段承受全部聚合流量,在Broadcast阶段又承受全部扩散流量,带宽占用集中且容易与其他计算任务争抢资源。将两棵树的根节点分别放置在不同的rank上,本质上是将一个节点的双倍负载拆分到两个节点上,使得每个节点的网络负载更加均匀。在大规模集群中,这种均衡策略带来的收益会随着规模增长而更加显著。

通信域划分与子群通信

在千卡集群中,并非所有通信操作都需要全卡参与。数据并行、张量并行、流水线并行往往同时存在,不同并行维度的通信范围不同。hccl支持通信域的灵活划分,允许开发者为每个并行维度创建独立的通信域,在同一物理集群上同时运行多种不同规模的集合通信操作。

通信域划分的工程价值在于减少不必要的通信参与方。例如,在8卡节点内进行张量并行时,AllReduce只需要同节点内的8张卡参与,不需要跨节点同步。如果错误地使用全局通信域,8卡节点内的通信也要走跨节点的流程,延迟会被跨节点链路拉高。hccl允许开发者为张量并行单独创建节点内通信域,将通信范围限定在同节点内。

import hccl

# 创建全局通信域(所有卡参与)
global_comm = hccl.HcclCommunicator(global_rank_size, global_rank_id)

# 创建节点内通信域(仅同节点卡参与)
local_group = [0, 1, 2, 3, 4, 5, 6, 7]
local_comm = hccl.HcclCommunicator(global_comm, local_group)

# 全局AllReduce(数据并行的梯度同步)
hccl.AllReduce(global_comm, gradient_buffer, hccl.HCCL_REDUCE_SUM)

# 节点内AllReduce(张量并行的激活值同步)
hccl.AllReduce(local_comm, activation_buffer, hccl.HCCL_REDUCE_SUM)

通信域划分直接影响通信延迟和带宽占用。全局通信域的AllReduce需要所有卡参与,其延迟取决于集群中最慢的链路。如果张量并行的通信只需要同节点内的8张卡参与,使用节点内通信域可以将延迟控制在节点内HCCS互联的水平,避免被跨节点链路的延迟拖累。同时,节点内通信和跨节点通信可以在不同通信域上并行执行,进一步提升通信效率。这种按需划定通信范围的思路,是大规模集群中多维度并行训练的基础设施支撑。

hccl在CANN多层架构中的协作关系

CANN软件栈是一个分层的体系结构,从下到上依次为硬件层(昇腾NPU)、计算层(算子库)、通信层(hccl)、框架适配层(MindSpore/PyTorch适配器)、应用层(训练脚本)。hccl处于通信层,它向上为框架适配层提供集合通信接口,向下调用硬件层的通信驱动和DMA引擎。

hccl与CANN其他组件的协作关系体现在几个关键路径上。第一,hccl与HCCS驱动层的交互。HCCS是昇腾NPU之间的高速互联总线,hccl通过HCCS驱动提供的API完成节点内卡间的数据传输。hccl会根据HCCS链路的带宽和延迟特性选择传输模式——对于小消息使用中断驱动的传输模式以降低CPU开销,对于大消息使用DMA直传模式以最大化带宽利用率。

第二,hccl与RDMA驱动层的交互。跨节点通信依赖RoCEv2协议,hccl通过RDMA驱动提供的Verbs接口完成跨节点的数据传输。hccl在初始化时会注册RDMA内存区域,预分配通信缓冲区并锁定物理内存,避免传输过程中的页面换出导致地址失效。

第三,hccl与算子库的协作。在某些融合算子中,计算与通信被合并为单个算子执行,hccl为这些融合算子提供通信能力注入。例如,在MatMul+AllReduce融合算子中,MatMul的分块计算结果可以直接送入hccl的通信通道,不需要经过中间缓冲区的拷贝。

这种分层协作的设计使得hccl既能被上层框架以标准接口调用,又能充分发掘底层硬件的通信能力。开发者在使用MindSpore或PyTorch进行分布式训练时,不需要直接操作hccl的API,框架适配层会自动将框架层面的通信操作映射到hccl的集合通信原语上。但当开发者需要做深度性能调优时,直接使用hccl的配置接口和诊断工具可以获取更精细的控制能力。

hccl的典型使用场景与配置方法

场景一:标准数据并行训练

这是hccl最基础的使用场景。每个训练步中,各卡独立完成前向和反向计算后,通过AllReduce同步梯度。hccl的默认配置就能很好地覆盖这一场景,开发者通常不需要做额外调优。唯一需要关注的是通信域的初始化——确保所有参与训练的卡都加入了同一个全局通信域,并且rank映射与物理拓扑一致。

场景二:混合并行训练

在千亿参数大模型的训练中,数据并行、张量并行、流水线并行往往组合使用。hccl需要为每个并行维度创建独立的通信域,并根据通信模式选择不同的算法。张量并行的AllReduce通常在同节点内完成,适合使用Ring模式;数据并行的AllReduce跨越整个集群,适合使用NHNA分层模式;流水线并行的点对点通信使用hccl的Send/Recv原语。

import hccl

# 初始化全局通信域
hccl.Init()

# 获取全局通信域
world_comm = hccl.get_world_communicator()
world_size = hccl.get_world_size()
world_rank = hccl.get_world_rank()

# 构建张量并行通信域(节点内)
tp_group = list(range(0, 8))  # 节点0内的8张卡
tp_comm = hccl.HcclCommunicator(world_comm, tp_group)

# 构建数据并行通信域(跨节点同位置卡)
dp_group = [i for i in range(world_rank % 8, world_size, 8)]
dp_comm = hccl.HcclCommunicator(world_comm, dp_group)

# 张量并行:节点内AllReduce
hccl.AllReduce(tp_comm, tensor_parallel_buffer, hccl.HCCL_REDUCE_SUM)

# 数据并行:跨节点AllReduce
hccl.AllReduce(dp_comm, data_parallel_buffer, hccl.HCCL_REDUCE_SUM)

混合并行训练中不同维度的通信特征差异巨大。张量并行的通信发生在每层Transformer的前向和反向传播中,频率高、数据量相对小、延迟敏感,必须限制在节点内以利用HCCS的低延迟特性。数据并行的通信发生在每个训练步的反向传播结束后,频率低、数据量大、带宽敏感,需要跨节点但可以通过NHNA分层减少跨节点流量。如果将两者的通信域混用,张量并行的延迟会被跨节点链路拖高,数据并行的带宽也会被不必要的节点内流量消耗。分开构建通信域,让每种通信操作在最合适的网络层上执行,是混合并行训练性能优化的核心手段之一。

场景三:长序列推理的KV Cache同步

在长序列推理场景中,当序列长度超过单卡显存容量时,KV Cache需要分片存储在多张卡上,推理过程中各卡需要通过AllGather同步各自的KV Cache片段。hccl的AllGather实现针对这种场景做了优化——它支持在通信过程中直接将数据写入推理引擎的输入缓冲区,避免一次额外的显存拷贝。

配置方法

hccl的关键配置参数包括:

HCCL_BUFF_SIZE:通信缓冲区大小,默认值为200MB。增大缓冲区可以支持更大的单次通信消息,减少大张量AllReduce的分片次数,但会占用更多NPU侧内存。

HCCL_ALGO:算法选择策略,可配置为auto(自动选择)、ring(强制Ring)、tree(强制Tree)等。auto模式下hccl会根据消息大小和集群拓扑自动选择最优算法,通常不需要手动覆盖。

HCCL_WHITENING_DISABLE:关闭HCCL内部的梯度白化预处理。在部分训练场景中,框架层已经做了梯度归一化,hccl再做白化会引入不必要的精度损失,可以通过此配置关闭。

hccl的技术边界:什么场景不适合用它

hccl的设计目标是优化集合通信,它在以下场景中并不适用或者需要谨慎使用。

第一,点对点高频小消息通信。hccl的集合通信原语针对大批量数据传输做了优化,内部有缓冲区管理和协议封装开销。如果应用层的通信模式是高频率、小消息的点对点通信(例如参数服务器架构中的参数拉取),hccl的开销可能比直接使用RDMA Verb接口更高。这种场景下更适合使用hccl的Send/Recv原语,甚至绕过hccl直接操作RDMA接口。

第二,非规约型通信。hccl的核心优势在于AllReduce、ReduceScatter等规约型操作的算法优化。如果通信模式是简单的数据搬运(例如大规模数据分发或收集),不需要规约运算,hccl的算法优化空间较小,使用Broadcast或AllGather即可,但性能提升不如规约型操作明显。

第三,极度异构的集群。hccl的拓扑感知和算法选择基于集群拓扑的对称性假设——同节点内卡间带宽一致、节点间带宽一致。如果集群中存在异构链路(例如部分节点通过200G RoCE互联、部分节点通过100G以太网互联),hccl的自动算法选择可能不够精准,需要开发者手动指定算法和通信路径。

第四,超低延迟场景。集合通信的延迟下限受限于算法步数和网络传播延迟。对于延迟敏感型应用(例如在线推理中的参数同步),hccl的AllReduce延迟可能在毫秒级别,如果应用要求的同步延迟在微秒级别,hccl的协议开销会成为瓶颈,需要更轻量的通信机制。

使用前后的效率对比

在引入hccl的拓扑感知与算法自适应优化之前,分布式训练的通信效率往往受制于几个典型问题:算法选择不当导致通信步数过多、环序排列不考虑物理拓扑导致频繁跨越高延迟链路、通信域未划分导致小范围通信被拉入全局同步、缓冲区配置不当导致大张量被过度分片。这些问题叠加在一起,使得大规模集群的实际算力利用率远低于理论值。

引入hccl的优化配置后,通信效率的改善体现在多个维度。Ring AllReduce在拓扑感知环序下的通信延迟相比随机环序有显著下降,降幅随集群规模扩大而增加——原因在于跨Switch和跨节点的高延迟链路访问次数被有效压缩。Tree AllReduce在大规模场景下的步数优势从理论预期转化为实际收益,双树结构消除了根节点的带宽瓶颈,使得树算法在高卡数场景下的表现不再劣于Ring算法。NHNA分层算法将跨节点通信量压减到非分层方案的一个比例,这个比例取决于节点内卡数与集群总卡数的比值——节点内卡数越多,跨节点通信量的压缩比越显著。通信域划分让张量并行的节点内AllReduce摆脱了跨节点链路的延迟拖累,也避免了数据并行的跨节点AllReduce被节点内流量挤占带宽。

在通信与计算流水线重叠方面,未开启重叠时梯度AllReduce的时间完全叠加在训练步的耗时上,通信时间与计算时间是串行关系。开启重叠后,部分通信时间被隐藏在反向传播的剩余计算中,训练步耗时的增幅明显小于未重叠时的增幅。梯度压缩在带宽受限场景下的效果更为突出:量化压缩将通信数据量压缩到原始大小的一半或四分之一,稀疏通信在梯度稀疏度高的场景下压缩比更为激进,两者都能在收敛精度可接受的范围内降低通信耗时。

这些改善的综合效果是:在大规模集群上训练大模型时,每卡的有效算力利用率从偏低水平提升到更接近理论峰值的水平,训练总时间相应缩短。具体改善幅度因集群规模、网络拓扑、模型结构、并行策略的不同而有差异,但趋势是明确的——集群规模越大、并行维度越多,hccl的优化收益越显著。

// hccl拓扑感知环序构建逻辑(简化示意)
HcclResult TopoAwareRing::BuildRing(const TopoInfo &topoInfo) {
    std::vector<uint32_t> ringOrder;
    // 按NUMA节点分组
    for (auto &numaGroup : topoInfo.numaGroups) {
        // 按PCIe Switch分组
        for (auto &switchGroup : numaGroup.switchGroups) {
            // 同Switch下的卡排在相邻位置
            for (auto rank : switchGroup.ranks) {
                ringOrder.push_back(rank);
            }
        }
    }
    // 验证环的有效性:相邻rank之间的跳数总和最小
    size_t totalHops = CalcTotalHops(ringOrder, topoInfo);
    if (totalHops > OPTIMAL_HOP_THRESHOLD) {
        // 尝试交换组间顺序以减少跨Switch跳数
        ringOrder = OptimizeRingOrder(ringOrder, topoInfo);
    }
    ringOrder_ = ringOrder;
    return HCCL_SUCCESS;
}

拓扑感知环序构建的核心逻辑是"让物理上距离近的卡在逻辑环上也相邻"。在没有任何拓扑信息时,环序可能是随机排列的,每一步通信都可能跨越PCIe Switch甚至跨节点,这些高延迟链路会拖慢整个环的推进速度。按NUMA节点和PCIe Switch分组排列后,环上大部分相邻卡之间的通信都在低延迟链路上完成,只有少数几步需要跨越Switch或节点边界。totalHops的计算和优化是确保环序质量的兜底机制——当初步排列的跳数仍然偏高时,hccl会尝试调整组间的排列顺序,以进一步减少高延迟跳的次数。这种贪心式的优化不能保证全局最优,但在实际场景中已经能产生显著改善。

# hccl通信与计算重叠的典型配置
import hccl
import torch_npu

# 启用hccl的异步通信模式
hccl.Config.set_async_comm(True)

# 增加通信通道数以支持多流并行
hccl.Config.set_channel_count(4)

# 在训练循环中使用分块梯度同步
for i, (layer_grad_start, layer_grad_end) in enumerate(grad_segments):
    # 反向传播到当前层时,立即发起已计算完层的梯度AllReduce
    if i > 0:
        hccl.AllReduceAsync(global_comm, gradient_buffers[i-1], stream=comm_stream)
    # 继续反向传播计算
    loss.backward(retain_graph=True, up_to=layer_grad_end)

# 等待所有异步AllReduce完成
hccl.Synchronize(comm_stream)

异步通信与分块梯度同步是将通信时间隐藏在计算时间中的关键策略。同步AllReduce要求所有梯度计算完成后再统一发起通信,通信时间完全串行叠加在训练步中。异步模式允许在反向传播尚未完全结束时就开始传输已经计算完成的梯度块,通信与计算在不同的NPU流上并行推进。channel_count设置为4意味着hccl可以同时处理4路并行的通信请求,对应4个梯度块的并发传输。这种配置在梯度张量较大的场景下效果最为明显——梯度块足够大时,每块的传输时间足够长,能够与下一块的计算时间形成有效重叠;而梯度块过小时,每块的传输时间太短,重叠的效果会被通信启动开销稀释。channel_count的选取需要与梯度分块数量匹配,过多通道会增加缓冲区开销,过少通道则无法充分重叠。

收尾

hccl作为CANN生态中的集合通信基础设施,其价值远超一个通信库的范畴。它将分布式训练中的核心通信原语从简单的数据搬运提升为拓扑感知、算法自适应、多维度优化的系统工程。Ring AllReduce的动态chunk调整、Tree AllReduce的双树负载均衡、NHNA分层算法的跨节点流量压缩、通信域划分的按需范围限定、异步通信的计算重叠——每一项优化都不是孤立的技巧,而是针对特定通信瓶颈的系统性解法。


仓库地址:https://atomgit.com/cann/hccl

Logo

鲲鹏昇腾开发者社区是面向全社会开放的“联接全球计算开发者,聚合华为+生态”的社区,内容涵盖鲲鹏、昇腾资源,帮助开发者快速获取所需的知识、经验、软件、工具、算力,支撑开发者易学、好用、成功,成为核心开发者。

更多推荐