CANN hixl 高性能线性代数库架构原理剖析:昇腾NPU上零拷贝单边通信与大规模矩阵运算的协同设计类比解读
前言
分布式训练和推理场景里,跨设备的海量数据搬运是个绕不开的话题。不同团队对这个问题的处理方式差异极大——有些直接用现成的通信库,能跑就行;有些则愿意花时间研究底层的传输机制,就为了省掉那几次多余的内存拷贝。
在昇腾NPU生态中,昇腾CANN提供了两套定位迥异的通信组件。名气更大的那套负责集合通信,所有节点一起参与,步调高度一致,适合训练这种需要全员协同的场景。另一套知道的人少一些,但在大模型推理的PD分离架构里扮演着关键角色——它叫hixl,定位是单边通信库,主打零拷贝和低延迟。两者的设计哲学完全不同,选择错误的那套,轻则性能浪费,重则整个架构都要推倒重来。
这篇文章聊聊hixl是什么、它解决的是什么问题、与集合通信的定位差异在哪里,以及什么时候该用它、什么时候不该用它。类比和原理会交替出现,目标是让你对这个库建立起清晰的概念模型。
hixl解决的核心问题:传统通信模式的延迟瓶颈
分布式训练和推理里,跨设备通信的方案看似五花八门,但底层逻辑区分起来就两类。第一类叫做双边通信,发送方和接收方都要参与,发送方主动把数据推过去,接收方被动接收,整个过程需要双方协同才能完成。第二类叫做单边通信,发送方只需要把数据放在某个位置并通知对方,接收方自己来取,发送方在对方取数据的过程中可以继续做别的事情,不需要干等着。
传统通信库大多数走的是第一类路线,包括TCP Socket、gRPC,甚至HCCL里的集合通信,都属于双边模式。这类模式有个天然的问题:当发送方和接收方不在同一个设备上时,接收方必须等发送方把数据完整推过来才能开始干活。以PD分离的推理架构为例,预填充引擎(Prefill Engine)计算完一个token的KV Cache,要通过网络把它传给推理引擎(Decode Engine)。如果用传统的主动推送模式,预填充引擎必须把完整的数据包塞进网络缓冲区、等待网络传输完成、推理引擎收到数据后才能开始下一阶段的计算。这个链条里,接收方完全是被动的,没有任何主动权。
大模型的KV Cache体积很大,一个几十层的transformer输出的Key和Value矩阵加起来可能有好几个GB。发送方推一次完整的数据需要的时间跟数据量成正比,数据越大,等待时间越长。更要命的是,接收方在等待期间什么都干不了,设备资源白白闲置。对于追求高并发的推理服务来说,这种空闲是不可接受的。
hixl解决这个问题的思路是换一种交互方式。它提供的是单边通信能力:发送方只需要把数据放到一块接收方可访问的内存里,同时通知对方"数据好了"。接收方可以自己决定什么时候来读这块数据——可以等数据完全就绪后再读,也可以在数据部分就绪时就开始读,还可以在读的同时让发送方继续写下一批数据。两个操作在时间轴上高度重叠,延迟自然就下来了。
这听起来有点像操作系统里的共享内存机制,但hixl的零拷贝设计比传统共享内存更彻底,它直接利用了昇腾NPU的高速互联硬件,数据从发送方的显存到接收方的显存,中间不需要经过Host内存的周转。
零拷贝单边通信的原理类比
要理解零拷贝单边通信,有个生活化的类比非常贴切。想象一间办公室里有一块共享白板,上面写着大家正在合作的项目进度数据。
传统通信模式的做法是这样的:负责写数据的同事A,把白板上的内容抄到一张纸上,装进信封,封好,写上同事B的名字,交给行政前台。前台把信封拆开,把纸上的内容再抄到另一张纸上,送到B的工位上,B才能看到。这中间经历了两次抄写(A写一次,前台抄一次),而且B必须等前台把纸送来才能开始工作,如果A写的内容很长,B等待的时间也会很长。这还不是最糟的——如果A写的内容本身就很大(比如说是一张占了整面白板的数据图),行政前台可能还要把内容拆成好几份分批发,B收到不完整的内容完全没法用,只能等全部收到后才能开始干活。
单边通信加上零拷贝的模式下,同事A直接在共享白板上写数据,写完了在群里发一条消息说"白板第三块区域写完了,B你可以去看"。同事B收到通知后,直接走到白板前看那块区域。A在等B走过来的时候,已经在白板的另一块区域写下一个数据了。B在看第一块区域的时候,A正在写第二块——两人的操作在时间线上完全重叠。A不需要等B看完才写下一批,B也不需要等A写完才开始处理。A从写数据到通知B,中间的延迟极低,因为整个过程不涉及任何中间人,没有"谁先抄一遍再送过去"的环节。
把这个类比映射回技术实现。共享白板就是远端节点的内存区域,白板上的分区对应不同的内存窗口,通知消息对应HIXL的事件通知机制,而两个人操作的重叠就是通信与计算的重叠掩盖(overlap)。零拷贝体现在哪里呢?就是省掉了"前台抄一遍再送过去"这个多余的中间环节,数据直接从A的写入位置到了B的读取位置,中间没有中转站。
为什么零拷贝能降低CPU参与度?传统的主动推送模式下,数据从发送方到接收方要经过好几个阶段:发送方的GPU把数据拷到自己的Host内存,Host CPU做协议封装,网卡驱动把数据拷到网卡的DMA缓冲区,网卡硬件把数据发出去;接收方这边反过来,网卡DMA把数据拷到Host内存,Host CPU做协议解析,CPU再把数据拷到接收方的GPU显存。每个阶段都需要CPU指令的参与,CPU是整个数据搬运链条的调度中心。CPU参与得越多,它就越忙,其他任务就得等着。
零拷贝的本质是绕过CPU直接让硬件之间对传。HIXL利用RDMA或者HCCS这类高速互联协议,让发送方的NPU显存和接收方的NPU显存之间建立直接的数据通道。数据从发送方的HBM直接进入网络交换机的缓存,再进入接收方的HBM,整个过程CPU只需要负责初始化阶段的配置工作——设置好内存窗口、注册好内存页、写入传输描述符。一旦传输开始,CPU就可以去干别的了,数据通路完全由硬件接管。这种设计对于昇腾NPU集群特别有价值,因为Cube计算单元的算力极高,数据供给跟不上计算速度会成为瓶颈,而绕过CPU的数据通路能让Cube单元持续有数据可用,不会因为等待数据而空闲。
与hccl的定位差异:单边通信不是集合通信的替代品
昇腾CANN生态里提到通信库,很多人第一个想到的是hccl。hccl的全称是Huawei Collective Communication Library,负责集合通信,用在分布式训练场景里,所有参与节点步调一致地执行AllReduce、Broadcast、AllGather这类集体操作。hccl的名气大、生态成熟,是训练场景的标配通信库。这种先入为主的印象让很多人看到hixl的第一反应是"这是hccl的另一个版本吗"或者"用hixl能替代hccl吗"。答案是不能,两者解决的根本不是同一个问题。
从交互模型上看,hccl做的是集合通信,核心特征是所有节点同时参与、同步推进。比如做AllReduce操作,8个节点上的梯度要全部汇总求平均,8个节点必须同时把数据送到某个中间位置、计算、再分发回来,缺任何一方的数据都不行。这种模式适合数据并行训练:每个worker算完自己的梯度,用AllReduce汇成全局梯度,大家步调一致,逻辑清晰。但它的前提是所有节点同时在线、同时参与,通信的发起方和接收方都是全体成员。
hixl做的是点对点单边通信,只有两方参与:一方提供数据(Server),另一方消费数据(Client)。数据流动的方向是单向的,Server只需要自己准备好数据并通知Client,不需要关心其他节点在干什么。这种模式天然适合PD分离——Prefill节点和Decode节点各自独立运行,Prefill节点计算完KV Cache后单向地传给Decode节点,不需要所有节点同时协同。用集合通信的逻辑去实现PD分离,要么根本实现不了(因为集合通信要求全员参与,而PD分离的两个引擎通常不在同一个通信域里),要么实现出来性能极差(强行用点对点通信模拟集合操作,中间多了大量不必要的同步等待)。
从应用场景来看,hccl的强项是大规模分布式训练。数据并行、模型并行、流水线并行,这些训练范式里充满了梯度同步、参数同步、 activations 同步的需求,所有节点必须在同一时间点交换信息,hccl的AllReduce、AllGather、ReduceScatter等接口正好覆盖这些需求。hixl的强项是大模型推理的PD分离架构、参数服务器的参数查询,以及任何"一方产生数据、另一方消费数据、两者不需要同时协同"的场景。
这两者的选择逻辑其实很直接:如果你的系统里有多个节点需要同时看到同一份数据(比如训练时的梯度同步),用hccl;如果你的系统里是一个节点产生数据、另一个节点消费数据,两者可以异步运行(比如推理时的KV Cache跨设备传输),用hixl。很多实际部署的生产系统两个都会用——训练阶段用hccl做梯度同步,推理阶段用hixl做KV Cache分发。
大规模矩阵运算与通信的协同设计
PD分离架构中,Prefill引擎的核心计算单元是大规模矩阵乘法。输入token序列经过embedding后,进入Transformer层,每一层都会执行Self-Attention计算,涉及Query、Key、Value三个矩阵与输入的乘法,以及Attention Score与Value矩阵的乘法。这些矩阵运算的规模极大,对于一个70B参数的大模型,单个token经过Attention层可能涉及上百GB/s的矩阵运算量。Prefill引擎在昇腾NPU的Cube单元上高效地完成这些计算后,生成的Key Cache和Value Cache需要尽快传给Decode引擎,否则Decode引擎就会陷入等待。
传统的做法是:Prefill引擎在Cube单元上算完一层的Attention后,把结果(即KV Cache)拷贝到一段临时内存缓冲区,随即用网络协议把这段数据发送到Decode引擎。Decode引擎收到数据后,再把数据从临时缓冲区拷贝到自己的显存里供后续计算使用。这个过程有两个明显的浪费点。第一,每次发送前都要做一次显式的数据拷贝,把计算结果从Cube单元的直接寻址范围拷到网络发送缓冲区,这个拷贝操作本身要消耗内存带宽。第二,Decode引擎必须等Prefill引擎把完整的一层KV Cache全部发送过来才能开始使用,发送时间完全是一个串行的等待开销,无法与Prefill引擎的下一层计算重叠。
hixl改变了这个链条。Prefill引擎在Cube单元上完成矩阵运算后,生成的KV Cache直接留在HBM里,不需要立即拷贝到临时缓冲区。hixl在两个引擎的NPU之间建立一条直接的数据通道——通过远端内存窗口映射,Decode引擎可以直接读取Prefill引擎显存里KV Cache所在的那块区域。这样就实现了一个关键效果:Prefill引擎可以一边算下一层的矩阵乘法,一边让Decode引擎去读上一层的KV Cache,两者在时间线上完全重叠。
具体的技术细节是这样的:Prefill引擎在自己的显存里分配一块内存窗口,通过HIXL的注册接口把这块窗口标记为可被远端访问的共享区域,并把窗口的地址和大小信息传递给Decode引擎。Decode引擎拿到这些信息后,通过HIXL建立到这个窗口的映射,随后直接读取远端内存。Prefill引擎写完KV Cache后发一个完成事件通知,Decode引擎收到通知后开始读取。重要的是,Decode引擎可以在Prefill引擎还在写后续数据时就开始读已经就绪的那部分——这个行为叫做异步读取,是hixl支持通信计算重叠的核心机制。
这个设计对于大规模矩阵运算的效率有直接影响。矩阵运算的性能瓶颈往往不在计算本身,而在数据供给速度。Cube单元的算力极高,如果KV Cache的传输速度跟不上,Cube单元就会因为等数据而空闲,导致实际吞吐量远低于理论值。hixl通过让KV Cache的传输与矩阵运算在时间上重叠,确保了Cube单元始终有数据可算,消除了传统模式下的串行等待。
另外一个值得关注的是内存占用。传统模式下,每次跨设备传输都需要发送端和接收端各准备一块临时缓冲区来存放待传输的数据,传输完成后才能释放。对于大模型的KV Cache,一张70B参数的模型可能有几十GB的中间缓冲区需求。hixl的零拷贝机制省掉了这些临时缓冲区——数据不需要从Cube单元的运算区挪到临时缓冲区再发出去,直接走远端内存映射,数据在哪块显存里,就让对方直接读哪块,发送端和接收端都省掉了中间拷贝所需的内存空间。
适用场景与性能边界
hixl的设计目标很明确,它最适合的场景是数据流动方向单一、两端可以异步运行的跨设备通信任务。
最典型的就是大模型推理的PD分离架构。Prefill引擎负责处理用户输入的prompt,进行大计算量的预填充,生成KV Cache;Decode引擎负责自回归地生成每一个输出token。两者职责分离后,KV Cache的传输就成了系统性能的关键瓶颈。hixl在这个环节扮演的角色就是提供高速的KV Cache跨设备分发能力,让Decode引擎不需要长时间等待Prefill引擎的完整输出。
参数服务器是另一个典型场景。参数服务器保存了模型的完整权重,多个训练worker需要频繁查询和更新这些权重。传统的参数服务器采用RPC方式,worker请求参数,参数服务器推送数据,每次交互都附带协议栈开销和多次内存拷贝。用hixl实现参数服务器时,参数服务器把权重所在的内存窗口开放给worker,worker直接去读,不需要等待参数服务器的主动推送,效率大幅提升。
强化学习后训练中的参数切换也适合用hixl。这类场景里,Actor网络和Critic网络的参数在不同设备之间频繁切换,需要快速地把最新参数从训练节点传到推理节点。hixl的零拷贝单边通信正好满足这种"一方推送通知,另一方自己拉取"的模式,每次参数切换的延迟远低于传统RPC方案。
性能边界在哪里?hixl不适合的场景同样值得明确。
涉及集合通信的操作完全不适合hixl。AllReduce、AllGather、ReduceScatter这类需要全员同步的操作,用hixl来实现要么根本实现不了(因为hixl只有点对点能力,没有集合操作原语),要么实现出来的效果远不如hccl。分布式训练中的梯度同步必须用hccl,强行用hixl模拟集合通信会导致大量无效的同步点,性能会断崖式下降。
单节点内的高速互联场景里,hixl相比hccl的优势不明显。同一台机器上不同NPU之间的数据搬运延迟本身就很低,而且hccl在单节点场景下经过了充分优化,如果你的系统不涉及跨设备通信,用hccl更简单。如果确实需要跨NUMA节点或者跨卡,可以在评估后选择hixl或hccl,视具体延迟需求而定。
小数据量的高并发传输场景需要特别留意。hixl的每次传输都需要一定的握手开销(建立连接、注册内存窗口、发送元数据通知等),这个开销跟传输的数据量无关。对于大块数据的传输,握手开销可以被传输时间充分分摊,可以忽略不计;但对于极小的数据(比如只有几百字节的元数据),握手开销可能远大于实际传输时间,这时候应该考虑批量化传输,把多个小数据合并成一个大数据块后再用hixl发送。
关于带宽数据,官方基准测试在昇腾A3芯片上用128MB数据实测,通过HCCS链路的带宽可以达到很高的水平,通过RDMA链路也有不错的表现。需要记住的是,这些数字代表的是大数据块、长时间稳态传输的性能。实际业务里,KV Cache的传输通常是间歇性的、突发式的,而且每次传输的数据块大小可能不如基准测试那么均匀,所以实际达到的带宽通常会比标称值低一些。
API设计理念分析:极简接口背后的分层哲学
hixl的API数量精简到十几个核心调用,相比hccl庞大的接口体系,这个数字显得非常克制。这种极简风格不是偷工减料,而是深思熟虑的设计决策,背后有两条主线。
第一条主线是场景聚焦。hixl面向的是PD分离推理、参数服务器、强化学习参数切换这类场景,这些场景的核心需求都是"把A地的数据高效地送到B地"。围绕这个核心需求抽象出来的接口,自然不需要太多花样——建立一个通道、注册一块内存窗口、发起一次传输、等待传输完成,十几二十个接口就能完整覆盖。hccl则需要覆盖数据并行、模型并行、流水线并行等训练场景,AllReduce、Broadcast、AllGather、ReduceScatter、Scatter、Gather等操作各有各的语义和优化路径,接口自然就多了。
第二条主线是分层抽象。hixl提供了两个层次的接口。底层是HIXL Engine,暴露的是基础的传输能力:通道管理、内存窗口注册、数据传输、事件通知。用户拿到这些底层原语后,可以自己组合出各种通信模式——同步发送、异步发送、轮询接收、事件驱动接收,完全由用户决定。这种底层的直接控制能力,是高级库所无法替代的。什么情况下需要直接操作底层呢?比如在PD分离的自定义实现里,用户想在收到部分KV Cache数据后立即开始某些预处理操作,这就需要直接监听传输进度事件,而不是等待完整的数据块到达。上层是LLM-DataDist模块,封装了KV Cache语义的数据传输接口,提供的是开箱即用的体验——用户传入KV Cache的tensor、目标设备地址和传输参数,模块自动处理窗口映射、传输调度和完成通知。
// C++ API示例:HIXL Engine的基础传输
// 创建一个通信通道,连接到目标节点
HixlChannelDesc desc;
desc.SetLocalRank(0);
desc.SetPeerRank(1);
desc.SetTransportType(HixlTransportType::HCCS);
HixlChannelHandle ch = HixlCreateChannel(desc);
// HixlChannelHandle is a lightweight descriptor, not the actual data buffer
// 从昇腾NPU显存中读取数据后,用hixl发送到远端
void* local_buf = /* 从NPU分配器申请的显存指针 */;
size_t size = 1024 * 1024; // 1MB数据
HixlTransferHandle tx = HixlIsend(ch, local_buf, size, /* 异步发送,不阻塞 */ 0);
// Asynchronous send lets the Cube unit continue computing while data is in flight
HixlWait(tx); // 等待传输完成
// Explicit wait gives callers control over when to synchronize
# Python API示例:使用LLM-DataDist传输KV Cache
import torch
from llm_datadist import LLMDataDist
dd = LLMDataDist()
ch = dd.CreateChannel("prefill_to_decode", src_rank=0, dst_rank=1)
# Channel abstraction hides underlying HCCS/RDMA protocol selection
kv_cache = torch.randn(32, 512, 64) # (heads, seq_len, head_dim)
// LLM-DataDist understands KV Cache tensor layout, no manual buffer management
dd.Send(ch, kv_cache)
# Send is non-blocking by default, enabling compute-communication overlap
// C++示例:手动管理内存窗口,实现更精细的控制
HixlMemoryWindow win;
win.AddRange(local_base, size, /* 可读写权限 */ HIXL_MEM_READ_WRITE);
HixlPublishWindow(ch, win); // 向远端暴露这个内存窗口
// Publishing a window allows the peer to directly read this memory region
// Fine-grained window control enables partial KV Cache streaming
hixl的底层接口设计体现了"把控制权交给开发者"的原则。什么时候适合直接用底层接口呢?当用户需要精确控制传输时机、想要自己实现通信计算重叠、或者需要在同一个连接上复用多个并发的传输流时,底层接口的灵活性就体现出价值了。大多数情况下,LLM-DataDist这样的高层封装已经足够好用,不需要从零构建自己的通信层。
hccl的API设计哲学完全不同。hccl面向的是通用训练场景,用户可能是PyTorch或者MindSpore框架,框架本身已经封装了大量的通信原语,hccl需要提供足够丰富的接口供框架调用,同时还要处理各种硬件拓扑和通信优化。这就导致hccl的接口更多、更细粒度,但学习曲线也更陡峭。hixl的极简风格降低了接入门槛,十几二十个接口一周之内就能掌握,而hccl光是把AllReduce的各种配置参数摸清楚可能就要花不少时间。
两种设计哲学各有其合理性。hixl把复杂度留在了库的实现层,对外只暴露最简单的接口,让用户少操心通信细节、多关注自己的业务逻辑。hccl把复杂性暴露给了需要精细调优的高级用户,让他们在面对千变万化的训练拓扑时有足够的控制手段。选择哪个工具,要看自己的需求是在通信层还是在业务逻辑层。
hixl与hccl的使用对比
从效率维度对比两者在不同场景下的行为差异,能够更清晰地看出各自的优势区间。
在大模型PD分离推理的KV Cache传输场景中,传统方式依赖RPC或TCP协议进行跨设备推送,Prefill引擎在推送数据时必须占用CPU资源做协议封装,Decode引擎必须等待完整数据到达后才能开始处理,中间的多次内存拷贝消耗了可观的内存带宽。引入hixl后,KV Cache直接从Prefill引擎的NPU显存流向Decode引擎的NPU显存,零拷贝机制消除了中间缓冲区,Decode引擎可以在数据部分就绪时就开始读取,Prefill引擎在推送的同时继续进行下一层的矩阵运算。差异来源在于零拷贝消除了中间拷贝和CPU参与,单边通信支持了通信与计算的时间重叠。
在参数服务器的参数查询场景中,传统的RPC方式需要参数服务器接收请求后主动推送数据,每次查询都附带协议栈开销和内存拷贝开销,而且CPU必须等待推送完成才能处理下一个请求。hixl支持worker直接读取参数服务器开放的内存窗口,参数服务器只需要处理"窗口开放"和"数据就绪"两条轻量级消息,实际的数据搬运完全由硬件直接完成,CPU不再是瓶颈。差异来源在于单边通信省掉了参数服务器的主动推送开销和协议栈开销。
在大规模分布式训练中的梯度同步场景中,传统方式用HCCL AllReduce让所有worker同时参与,梯度数据在各节点之间高效地汇总平均,所有节点同步推进,逻辑简洁。换成hixl来实现的话,单边通信缺少AllReduce这样的集合原语,必须用多个点对点传输组合出集合操作的效果,效率大幅下降,还需要额外的同步机制来协调各节点的进度。
| 维度 | 使用前(hccl在训练场景) | 使用后(训练用hccl+推理用hixl) | 差异来源 |
|---|---|---|---|
| KV Cache跨设备延迟 | RPC推送模式,Decode引擎串行等待 | 单边直接读取,支持部分就绪异步消费 | 零拷贝+通信计算重叠 |
| 参数查询CPU参与度 | 参数服务器CPU深度参与每个查询 | 参数服务器仅处理元数据通知 | 单边让接收方主导读取节奏 |
| 梯度同步效率 | HCCL AllReduce,节点全员同步 | 保持hccl AllReduce,多设备异构最优 | 集合通信原语的专业实现 |
| 内存占用峰值 | 每个节点都需要传输缓冲区 | hixl零拷贝减少中间缓冲区需求 | 远端内存直接访问,减少副本 |
| 接口复杂度 | hccl数十个接口,配置项多 | hixl十余个核心接口,逻辑简洁 | 场景聚焦带来极简设计 |
这套组合策略在现代AI系统的实际部署中非常普遍。训练阶段用hccl处理分布式梯度同步,推理阶段用hixl处理KV Cache分发,各司其职。硬要用hixl替代hccl在训练场景下做集合通信,或者反过来用hccl处理PD分离的跨设备数据传输,效果都会远不如专用工具。
结尾
hixl的核心价值在于为零拷贝单边通信提供了轻量级的接口抽象,使昇腾NPU集群能够高效地支持PD分离推理、参数服务器和RL参数切换等场景。它不是hccl的替代品,两者的交互模型完全不同——hccl做的是全员同步的集合通信,hixl做的是点对点的远端内存直接访问。在需要跨设备分发KV Cache的推理场景里,hixl通过远端内存窗口映射实现了数据从一方显存到另一方显存的直接通路,零拷贝消除了中间缓冲区的开销,单边通信支持了接收方的主动拉取节奏,两者协同大幅降低了延迟并释放了CPU资源。
仓库地址:https://atomgit.com/cann/hixl
更多推荐




所有评论(0)