前言

做分布式训练的人都知道,卡一多,通信就成了命门。模型参数同步慢一步,八张卡全等着,算力白白空转。这个问题在昇腾NPU上同样存在,而且因为昇腾的硬件拓扑结构跟GPU差异很大——HCCS高速互联、RoCE网络直连、PCIe交换芯片——通信路径选择比想象中复杂得多。CANN软件栈里专门负责这一层的组件叫HCCL,全称Huawei Collective Communication Library,昇腾集合通信库。它不是简单封装几个MPI接口就完事了,而是从算法选择、拓扑感知、链路调度三个维度做了大量底层工作。把HCCL搞明白,对理解整个CANN分布式训练链路至关重要。

我之前在调试一个多机AllReduce性能不及预期的问题时,发现瓶颈不在计算而在通信——HCCL选了RHD算法却没感知到集群的Fat-Tree拓扑,导致跨交换机流量远超预期。翻HCCL源码和文档才搞清楚它的算法选择器和拓扑感知模块是怎么协作的。这篇文章就把这些踩坑和理解写下来。

HCCL在CANN架构中的定位

CANN的五层架构里,HCCL同时出现在第二层昇腾计算服务层和第四层昇腾计算执行层。这个"跨层"的设计是有道理的:对上,HCCL通过AscendCL接口向PyTorch、MindSpore等框架暴露集合通信算子,属于服务层的算子库范畴;对下,HCCL要直接调度HCCS、RoCE、PCIe等物理链路完成数据搬运,属于执行层的运行时范畴。

这种跨层定位决定了HCCL的代码架构天然分成了两个子系统:HCCL集合通信库和HCOMM通信基础库。集合通信库负责实现AllReduce、Broadcast、AllGather、ReduceScatter、AlltoAll这些通信原语的语义逻辑——用哪种算法、数据怎么切分、归约怎么做。HCOMM负责把算法逻辑翻译成底层通信动作——控制面建立连接、数据面搬运报文。分开的好处是改算法不影响底层驱动,升级驱动不破坏上层语义。

从仓库依赖关系看,HCCL与HCOMM紧密绑定,而HCOMM又依赖driver和runtime提供的基础通信能力。整个调用链是:框架→AscendCL→HCCL→HCOMM→Runtime→Driver→硬件。任何一层出问题,分布式训练就可能卡死或出静默错误。

通信算法:Ring、Mesh、RHD的设计取舍

HCCL内置了三种核心通信算法,每种算法针对不同的集群规模和数据规模做了优化。理解这三种算法的内在逻辑,是调优分布式训练性能的基础。

Ring算法是HCCL的默认选择,尤其在中小规模集群上表现稳定。它把参与通信的NPU组织成一个逻辑环,数据沿着环的方向逐跳传递和归约。以AllReduce为例,数据被切成N份(N是参与通信的卡数),每张卡先把自己的那份沿环传递给下一张,经过N-1步Reduce和N-1步Broadcast完成全局归约。Ring的带宽利用率在理想拓扑下接近理论最优,因为每条链路的负载几乎均匀。但Ring有个致命弱点:延迟随卡数线性增长。128张卡的Ring AllReduce,一个数据块要跑127跳才能完成归约,尾部延迟极高。

Mesh算法把NPU看作二维网格,先在行方向做Reduce,再在列方向做Broadcast。两步完成,延迟只与网格维度的开方成正比。64张卡排成8x8网格,延迟只相当于8跳Ring。但Mesh的代价是中间状态的内存开销更大——行Reduce完成后每行的中间结果要单独存一份,且列Broadcast时同一列的卡要同时读这个中间结果,对链路带宽的瞬时压力比Ring大。在HCCS带宽充裕的单机场景下Mesh表现很好,跨机场景因为RoCE带宽有限反而不如Ring稳定。

RHD算法即Recursive Halving-Doubling,递归减半加倍算法。核心思路是模仿递归的归并排序:先把参与方对半分,一半发数据给另一半做归约;再四等分,四等分之间互相通信;依此类推直到只剩一个归约结果,再反向把结果广播回去。RHD的延迟是O(logN)级别的,128张卡只需7步完成Reduce阶段。但RHD要求参与方数量是2的幂次,非2的幂次时需要补哑节点或者回退到Ring。HCCL的算法选择器会自动处理这个边界条件。

// HCCL算法选择器的核心逻辑简化示意
// 真实实现在 src/ops/op_common/selector/ 目录下

HcclAlgorithm select_algo(int rank_num, size_t data_bytes, TopoType topo) {
    // 单机场景优先Mesh,HCCS带宽够用
    if (topo == TOPO_SINGLE_SERVER && rank_num <= 8) {
        return ALGO_MESH;
    }
    // 大数据量+2的幂次卡数,RHD延迟最低
    if (is_power_of_two(rank_num) && data_bytes > LARGE_DATA_THRESHOLD) {
        return ALGO_RHD;
    }
    // 默认走Ring,稳定兜底
    return ALGO_RING;
}

算法选择不是单纯看哪个延迟低就选哪个。Mesh在大卡数时中间状态吃内存,RHD在非2幂次时有额外开销,Ring延迟高但内存和带宽都可控。把拓扑信息传进选择器,让决策基于实际硬件条件而不是理论最优,这是HCCL跟很多通用通信库最大的不同。通用库往往只有一个默认算法,HCCL的选择器会根据单机/多机、卡数、数据量三个维度综合判断。

拓扑感知:让通信走最短路径

HCCL的拓扑感知模块在源码的src/ops/op_common/topo/目录下,负责获取和转换通信域的拓扑信息。这个模块的工作对通信性能影响极大,但很多人忽略了它。

在一个典型的昇腾集群里,NPU之间的互联方式至少有三种:同服务器内的HCCS互联,带宽高延迟低;跨服务器的RoCE互联,带宽中等延迟偏高;通过PCIe Switch共享的互联,带宽最低。当HCCL发起一次AllReduce时,它需要知道哪些卡在同一台服务器内、哪些卡需要跨交换机通信,才能决定数据先在机内归约还是直接全局归约。

拓扑感知的具体实现分两步。获取阶段,HCCL通过驱动接口读取每张NPU的物理位置信息——哪个Server、哪个Rank、连着哪个HCCS端口或RoCE网口。转换阶段,把这些物理位置映射成逻辑拓扑图,标记出每条链路的类型和带宽等级。算法选择器和执行器都依赖这张逻辑拓扑图做决策。

// 拓扑信息获取的简化流程
// 实际实现在 topo 模块中

void build_topo_map(HcclComm comm) {
    // 从驱动获取每张卡的物理位置
    for (int i = 0; i < comm->rank_num; i++) {
        DevLocation loc = get_device_location(comm->ranks[i]);
        // 按server分组,同server的卡HCCS直连
        comm->server_groups[loc.server_id].add_rank(i);
        // 标记跨server链路类型
        if (loc.has_roce_port) {
            comm->inter_links[i].type = LINK_ROCE;
            comm->inter_links[i].bw_class = BW_HIGH;
        } else {
            comm->inter_links[i].type = LINK_PCIE;
            comm->inter_links[i].bw_class = BW_LOW;
        }
    }
}

拓扑信息不只是一个"有哪些卡"的列表,而是要标注出每条链路的物理特性。没有这个信息,HCCL可能把跨交换机的RoCE链路和机内HCCS链路等同对待,导致Ring算法的数据流路径绕远路。标注了链路类型之后,执行器可以优先在机内完成部分归约,减少跨机流量。这就是所谓的层次化通信策略——先机内AllReduce,再把每台服务器的结果做跨机AllReduce,跨机流量直接降为原来的1/K(K是每台服务器的卡数)。

控制面与数据面分离:HCOMM的设计哲学

HCOMM是HCCL的底层通信基础库,采用了控制面与数据面分离的架构。这个设计在src/ops/op_common/executor/目录的执行器代码中有清晰体现。

控制面负责通信域的建立、连接管理、同步屏障。当HCCL初始化一个通信组时,控制面先通过TCP或共享内存建立所有参与方之间的信令通道,交换彼此的Rank信息和内存描述符。这个阶段不搬运训练数据,只传控制报文,开销很小但必须可靠——任何一张卡没完成握手,整个通信组就无法进入数据传输阶段。

数据面负责实际的数据搬运。HCCL支持两种数据面传输方式:DMA直传和RoCE动词操作。DMA直传用于同服务器内的HCCS通信,NPU之间可以直接读写对方的设备内存,不需要CPU中转。RoCE动词操作用于跨服务器通信,走RDMA协议,数据从NPU显存直接推到网卡再传输到对端NPU,绕过操作系统内核栈。

// 数据面传输的简化逻辑
void data_plane_transfer(void* src, void* dst, size_t len, LinkType link) {
    if (link == LINK_HCCS) {
        // 机内DMA直传,零拷贝
        dma_copy(src, dst, len);
        // WHY: 同机内HCCS直连,DMA拷贝延迟极低,不需要额外封装
    } else if (link == LINK_ROCE) {
        // 跨机RDMA写,注册内存区域后直接推送
        mr = register_mr(src, len);
        roce_write(mr, dst_remote_addr, remote_rkey, len);
        // WHY: RDMA绕过内核,CPU不参与数据搬运,延迟和吞吐都比TCP好一个量级
    }
}

控制面和数据面分离后,控制面的可靠性逻辑和数据面的性能逻辑互不干扰。控制面用TCP保证握手可靠性,哪怕多花几毫秒也无所谓——只初始化一次。数据面用RDMA追求极致吞吐,报文丢了由硬件重传,软件层不做确认。如果把控制和数据混在一起,要么控制面被数据面的流量冲击导致丢握手报文,要么数据面被控制面的确认逻辑拖慢速度。分开之后各自做各自最擅长的事。

通信原语实现:AllReduce的完整生命周期

以AllReduce为例,看HCCL从接收框架请求到完成全局归约的完整流程。这个流程覆盖了HCCL源码中src/ops/all_reduce/目录的核心代码。

当PyTorch通过AscendCL调用HcclAllReduce接口时,HCCL的入口函数会做几件事:校验通信句柄有效性、检查数据类型是否支持、计算数据分块策略。分块策略取决于算法选择——Ring切N份,Mesh按行列切,RHD按递归深度切。

分块完成后,HCCL创建一个执行计划。执行计划是一张状态机图,每个节点代表一个通信步骤(发送、接收、归约、同步),边代表步骤之间的依赖关系。执行器按照依赖顺序调度这些步骤到具体的NPU和链路上。在Ring AllReduce中,执行计划包含N-1个Reduce步骤和N-1个Broadcast步骤,每个步骤绑定了源Rank、目标Rank、数据偏移量和归约操作类型。

执行计划生成后,HCOMM的控制面确保所有参与方就绪,数据面开始按计划执行。每个步骤完成后通过中断或轮询通知执行器,执行器再推进状态机到下一个状态。当所有步骤完成,HCCL回调通知框架层AllReduce结束。

这个流程中有几个关键的性能优化点。批量发送:HCCL会把连续的小消息合并成一个大的RDMA写操作,减少协议开销。流水线:Reduce阶段的第k步和第k+1步的数据块可以部分重叠执行,不需要等第k步完全结束再开始第k+步。内存预注册:HCOMM在初始化时就把通信缓冲区注册到RDMA网卡,运行时不需要反复做内存注册/注销操作。

单算子模式与图模式的差异

HCCL支持两种执行模式:单算子模式和图模式。这两种模式的区别不是简单的"调用一次"和"调用多次",而是涉及执行流程的根本性差异。

单算子模式下,每次调用通信原语时HCCL都要走完整流程——校验参数、选择算法、生成执行计划、调度执行、等待完成。这种模式适合交互式调试和小规模实验,开销透明,出问题好定位。但每次调用都有固定开销,在训练循环中反复调用同一个AllReduce时,这些固定开销会累积成不可忽视的延迟。

图模式下,HCCL把多个通信原语编译成一个静态执行图。这个图在编译阶段就确定了算法选择、分块策略和执行顺序,运行时只做数据面搬运,跳过了参数校验和计划生成阶段。图模式的理论加速取决于通信原语的调用频率——高频调用时固定开销的节省很可观,低频调用时差异不明显。

图模式还有一个隐藏的好处:编译器可以跨算子做优化。比如连续的AllReduce+Broadcast可以被合并成一个AllReduce操作(AllReduce本身就包含了Broadcast语义),连续的ReduceScatter+AllGather可以被识别为AllReduce的拆分形式。这种跨算子融合在单算子模式下无法实现,因为每次调用只看到一个孤立的算子。

从源码结构看,单算子模式的入口在src/ops/all_reduce/等算子目录下,图模式的编译逻辑在GE(Graph Engine)侧,HCCL只提供算子注册信息和执行接口。两套模式的代码路径完全不同,但底层共享HCOMM的数据面传输能力。

使用前vs使用后:效率对比

分布式训练中,通信方案的选择对整体性能影响巨大。下面从几个关键维度对比不使用HCCL和使用HCCL的差异。

对比维度 使用前(原始MPI/手写通信) 使用后(HCCL方案) 效率变化
AllReduce延迟 MPI默认算法,不感知昇腾硬件拓扑,跨机流量无优化 拓扑感知算法选择,机内先归约再跨机同步,跨机流量大幅降低 通信延迟降低至数分之一
带宽利用率 走TCP/IP协议栈,CPU参与数据拷贝,RoCE网卡带宽利用不充分 RDMA零拷贝,NPU显存直推网卡,带宽利用率接近硬件理论值 有效带宽成倍提升
扩展性 随卡数增加延迟线性增长,64卡以上通信开销占比极高 RHD算法延迟对数增长,层次化通信抑制跨机流量膨胀 大规模集群扩展性显著改善
框架集成 需要手写通信逻辑和同步屏障,代码量大且容易出bug 通过AscendCL统一接口调用,PyTorch/MindSpore原生支持 开发效率和代码可维护性大幅提升

不使用HCCL时,最常见的做法是直接调MPI的AllReduce。MPI的问题是它对昇腾NPU的硬件拓扑一无所知——它不知道哪些卡在同一台服务器里,不知道HCCS和RoCE的区别,更不知道怎么利用DMA直传做零拷贝。结果就是MPI把所有NPU当成均匀互联的节点,Ring算法的数据流可能绕着跨机链路跑,而本该走HCCS的机内通信反而没被优先利用。HCCL的核心价值就在于它理解昇腾硬件,能把通信调度到正确的物理路径上。


仓库链接:https://atomgit.com/cann/hccl

Logo

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

更多推荐