昇腾CANN单边通信库HiXL实战:PD分离架构中的零拷贝通信优化实践
前言
大语言模型推理系统正在经历从单体部署向分布式解耦架构的演进。Prefill-Decode(PD)分离架构将推理过程拆分为预填充和解码两个阶段,分别由不同计算节点承担,从而提升整体吞吐和资源利用率。这种架构的核心挑战在于跨节点KV Cache的高效传输——Prefill节点生成的KV Cache需要低延迟、高带宽地传输到Decode节点,任何通信瓶颈都会直接拖累端到端推理性能。
昇腾CANN提供了HiXL(Huawei Xfer Library)单边通信库,通过零拷贝(Zero-Copy)机制和单边通信(One-Sided Communication)能力,为PD分离架构提供了高效的跨节点数据传输方案。HiXL在昇腾NPU之间建立直接内存访问通道,避免传统通信模式中多次数据拷贝和CPU介入带来的延迟开销,充分释放底层高速互联链路(HCCS、RDMA)的带宽潜力。
PD分离架构的通信痛点
传统通信模式的开销来源
在典型的PD分离部署中,Prefill节点负责处理用户输入的Prompt,计算并缓存Key-Value向量;Decode节点负责逐Token生成输出,需要从Prefill节点拉取对应的KV Cache。这一过程涉及跨节点的内存数据传输,传统实现方式存在多个性能瓶颈。
基于Socket或gRPC的通信方案需要在发送端和接收端各进行一次内存拷贝:应用层Buffer到内核Socket Buffer,再从内核Socket Buffer到应用层Buffer。每次拷贝不仅消耗内存带宽,还引入额外的延迟。当KV Cache规模达到百MB级别时,这种开销变得不可忽视。
即使使用RDMA的send/recv语义,仍然需要在目标端准备接收Buffer并显式发起接收操作。这意味着Decode节点的CPU必须主动参与每次传输的生命周期管理,无法与NPU的计算任务形成有效的流水线重叠。
单边通信的技术优势
单边通信(One-Sided Communication)允许源端在无需对端CPU介入的情况下完成数据传输。发送端准备好数据后,直接写入接收端已预注册的内存区域,接收端无需执行任何recv类操作即可在本地内存中看到完整数据。这种模式将通信的主导权完全交给源端,目标端的计算任务可以持续运行,仅在需要时直接读取已就绪的远程写入数据。
零拷贝(Zero-Copy)进一步消除传输路径上的冗余数据移动。HiXL通过内存注册(Memory Registration)机制,使发送端NPU能够直接访问接收端NPU或Host的物理内存,数据从源Buffer经过网络链路直接写入目标Buffer,全程不经过中间临时Buffer。这不仅降低了内存带宽消耗,也减少了内存容量压力——在KV Cache规模持续增长的背景下,每一分内存节省都具有实际价值。
HiXL核心架构与关键概念
三层设计
HiXL采用分层架构设计,从底层到上层依次为:HiXL Engine、HiXL CS(Communication Service)、LLM-DataDist。每一层面向不同的使用场景,提供逐步抽象的接口。
HiXL Engine是底层传输引擎,提供最基础的点对点数据传输接口。它直接操作各类内存类型(Device内存、Host内存)和传输链路(HCCS、RDMA),支持D2D(Device-to-Device)、D2H(Device-to-Host)、H2D(Host-to-Device)等传输方向。Engine层接口最为灵活,适合需要精细控制传输行为的场景。
HiXL CS在Engine之上提供了面向通信服务的抽象,支持更复杂的通信模式和多链路聚合。该接口目前仅支持Ascend 950PR和Ascend 950DT超节点场景,利用UB(Unified Bus)协议实现超节点内高带宽传输。
LLM-DataDist是最上层的语义接口,携带KV Cache的业务语义,直接与vLLM、SGLang等推理引擎对接。它屏蔽了底层传输的细节,推理引擎通过简洁的接口完成KV Cache的发布和订阅,无需关心数据传输的具体实现。
内存模型与传输语义
HiXL的内存模型区分本地内存和远程内存。本地内存是发起传输的进程所拥有的内存区域,远程内存是目标进程预注册可供远程访问的内存区域。所有参与传输的内存区域都必须通过HiXL的内存注册接口进行注册,以获取全局唯一的内存句柄(Memory Handle)。
传输语义分为同步和异步两种模式。同步传输在数据写入远程内存后返回,调用线程阻塞直至传输完成。异步传输立即返回传输句柄,调用线程可以继续执行其他任务,通过轮询或回调方式获知传输完成事件。异步模式是实现通信-计算重叠的关键——Prefill节点可以在启动KV Cache传输的同时开始处理下一个请求,从而隐藏通信延迟。
FabricMem是HiXL在Atlas A3超节点上提供的增强模式。它基于CANN的Virtual Memory Manager机制,将超节点内所有计算节点的DRAM统一编址,NPU可以通过HCCS高速链路直接访问远程节点的内存,无需CPU介入。在128M数据传输场景下,HCCS链路的带宽可达119GB/s,RDMA链路可达22GB/s。
实战:基于HiXL构建PD分离通信层
环境准备与编译安装
在开始编写代码之前,需要确保HiXL库已正确编译并安装到CANN软件路径下。HiXL的编译依赖HDK、CANN和灵衢计算网络组件,版本要求如下:HDK 25.5以上,CANN 9.0以上,灵衢计算网络1.5.0以上。
# 克隆HiXL仓库
git clone https://atomgit.com/cann/hixl.git
cd hixl
# 配置CANN安装路径(根据实际安装位置调整)
export ASCEND_HOME=/usr/local/Ascend/cann
# 执行编译脚本
bash build.sh
# 编译成功后,库文件位于 build/output 目录
# 头文件路径:include/hixl/hixl.h
# 库文件路径:build/output/libcann_hixl.so
HiXL使用CMake构建系统,build.sh封装了完整的编译流程,包括依赖检查、编译选项配置和安装包生成。编译完成后,头文件hixl.h包含所有Engine层接口的声明,动态库libcann_hixl.so在运行时被链接。将ASCEND_HOME指向正确的CANN安装路径是确保编译找到正确依赖的关键步骤。
初始化HiXL传输引擎
任何HiXL程序的起点是初始化传输引擎。引擎初始化时需要配置本端和远端的通信地址、使用的传输链路类型,以及可选的高级参数(如FabricMem开关、内存池容量等)。
#include "hixl/hixl.h"
#include <iostream>
int main() {
// 创建HiXL引擎配置
HixlConfig config = nullptr;
hixlCreateConfig(&config);
// 设置本端IP和端口
hixlConfigSetString(config, HIXL_OPTION_LOCAL_IP, "192.168.1.10");
hixlConfigSetInt(config, HIXL_OPTION_LOCAL_PORT, 18000);
// 设置远端IP和端口(Prefill节点连接Decode节点)
hixlConfigSetString(config, HIXL_OPTION_REMOTE_IP, "192.168.1.20");
hixlConfigSetInt(config, HIXL_OPTION_REMOTE_PORT, 18000);
// 使用HCCS链路(超节点内);跨节点使用RDMA
hixlConfigSetString(config, HIXL_OPTION_LINK_TYPE, "HCCS");
// 启用FabricMem模式(仅Atlas A3)
hixlConfigSetString(config, HIXL_OPTION_ENABLE_USE_FABRIC_MEM, "1");
// 创建引擎实例
HixlEngine engine = nullptr;
HixlStatus status = hixlCreateEngine(config, &engine);
if (status != HIXL_SUCCESS) {
std::cerr << "Failed to create engine: " << hixlGetErrorString(status) << std::endl;
return -1;
}
std::cout << "HiXL engine initialized successfully" << std::endl;
// ... 传输操作 ...
// 销毁引擎
hixlDestroyEngine(engine);
hixlDestroyConfig(config);
return 0;
}
hixlCreateConfig创建一个配置对象,通过hixlConfigSetString和hixlConfigSetInt设置引擎参数。LOCAL_IP/PORT和REMOTE_IP/PORT定义了通信端点的网络地址。LINK_TYPE选择传输链路:HCCS用于超节点内高速互联,RDMA用于跨节点标准网络。ENABLE_USE_FABRIC_MEM开启FabricMem模式后,引擎将使用统一内存编址,允许直接访问远端Host内存,这对PD分离场景中Decode节点直接读取Prefill节点的KV Cache至关重要。
内存注册与数据传输
PD分离的核心数据流是Prefill节点将KV Cache写入Decode节点的预注册内存区域。以下代码展示完整的数据发送流程。
// Prefill节点:发送KV Cache到Decode节点
// 1. 在Prefill节点本地申请Device内存(KV Cache所在位置)
void* local_kv_cache = nullptr;
size_t kv_cache_size = 128 * 1024 * 1024; // 128MB
aclrtMalloc(&local_kv_cache, kv_cache_size, ACL_MEM_MALLOC_HUGE_FIRST);
// 2. 获取Decode节点远程内存的句柄(通过控制面协商得到)
// 实际部署中,Decode节点调用hixlRegisterMemory获取handle,通过控制面传给Prefill节点
HixlMemHandle remote_mem_handle = /* 从控制面获取 */;
// 3. 将本地KV Cache传输到远程内存(单边零拷贝写入)
HixlRequest request = nullptr;
status = hixlWriteRemote(
engine,
local_kv_cache, // 本地源地址
kv_cache_size, // 传输大小
remote_mem_handle, // 远程内存句柄
0, // 远程偏移量
&request // 返回的请求句柄
);
if (status != HIXL_SUCCESS) {
std::cerr << "hixlWriteRemote failed: " << hixlGetErrorString(status) << std::endl;
return -1;
}
// 4. 异步模式下,立即返回可做其他工作,需要时查询完成状态
// hixlWaitRequest阻塞等待完成;hixlPollRequest非阻塞查询
hixlWaitRequest(engine, request, HIXL_WAIT_FOREVER);
std::cout << "KV Cache transferred successfully via zero-copy write" << std::endl;
// 5. 释放资源
aclrtFree(local_kv_cache);
hixlDestroyRequest(request);
aclrtMalloc在昇腾NPU的Device内存上分配KV Cache缓冲区。HiXL的零拷贝写入通过hixlWriteRemote实现:发送端NPU直接将数据写入接收端预注册的内存区域,全过程无需接收端CPU参与。HixlMemHandle是内存区域的全局标识,包含目标内存的物理地址信息,由接收端通过hixlRegisterMemory注册后获得,并通过控制面(如Redis、etcd或gRPC)分发给发送端。hixlWriteRemote支持异步执行,返回HixlRequest句柄用于后续完成查询,这使得Prefill节点可以在传输进行的同时继续处理其他请求,实现通信与计算的流水线重叠。
Decode节点:内存注册与数据消费
Decode节点需要预注册一块内存区域供Prefill节点写入,并在写入完成后直接读取该区域获取KV Cache。
// Decode节点:注册远程可写内存区,接收Prefill节点的KV Cache
// 1. 在Decode节点申请Host或Device内存作为KV Cache接收缓冲区
void* recv_buffer = nullptr;
size_t recv_size = 128 * 1024 * 1024;
// 使用Host内存(DRAM)作为接收缓冲区,支持更大的缓存容量
// Atlas A3上FabricMem模式支持D2RH(Device-to-Remote-Host)
aclrtMallocHost(&recv_buffer, recv_size);
// 2. 注册内存区域,获取内存句柄供Prefill节点使用
HixlMemHandle mem_handle = nullptr;
status = hixlRegisterMemory(
engine,
recv_buffer,
recv_size,
HIXL_MEM_PERM_WRITE, // 允许远程写入
&mem_handle
);
if (status != HIXL_SUCCESS) {
std::cerr << "hixlRegisterMemory failed: " << hixlGetErrorString(status) << std::endl;
return -1;
}
// 3. 将mem_handle通过控制面发送给Prefill节点
// 实际代码中通过gRPC/Redis等控制面组件分发
// std::string handle_str = hixlMemHandleSerialize(mem_handle);
// control_plane->distributeHandle(handle_str);
std::cout << "Memory registered, handle distributed to Prefill nodes" << std::endl;
// 4. 等待Prefill节点写入完成(通过通知机制或轮询完成标记)
// 实际部署中可使用HiXL的完成通知机制
wait_for_transfer_complete();
// 5. 读取接收到的KV Cache(无需拷贝,直接访问)
// recv_buffer中已包含Prefill节点写入的KV Cache数据
KVCacheHeader* header = static_cast<KVCacheHeader*>(recv_buffer);
std::cout << "Received KV Cache: seq_len=" << header->seq_len
<< ", num_layers=" << header->num_layers << std::endl;
// 6. 将KV Cache注入本地推理引擎的解码过程
inject_kv_cache_to_decoder(recv_buffer, recv_size);
// 7. 使用完成后注销内存注册并释放
hixlUnregisterMemory(engine, mem_handle);
aclrtFreeHost(recv_buffer);
Decode节点调用aclrtMallocHost在Host侧分配接收缓冲区,这是因为Host DRAM容量通常远大于Device显存,可以缓存更多的KV Cache。hixlRegisterMemory将这块内存注册到HiXL引擎,生成HixlMemHandle,该句柄编码了内存的物理地址和访问权限(WRITE/READ)。HIXL_MEM_PERM_WRITE标志允许远程节点向该区域写入数据。注册完成后,内存句柄需要通过控制面分发给所有潜在的发送节点。Decode节点在消费数据时直接读取recv_buffer,无需额外的接收拷贝操作——这是零拷贝通信的第二层含义:接收端消费数据时也无需从内核Buffer或网络Buffer拷贝到应用Buffer。
异步流水线:通信计算重叠
PD分离架构的性能关键在于隐藏通信延迟。以下示例展示如何利用HiXL的异步接口实现Prefill阶段与KV Cache传输的流水线执行。
// 异步传输实现通信-计算重叠
class PDPrefillWorker {
private:
HixlEngine engine_;
std::vector<HixlRequest> inflight_requests_;
public:
void process_request(const InferenceRequest& req) {
// 阶段1:Prefill计算(生成KV Cache)
KVCache kv_cache = run_prefill(req.input_tokens);
// 阶段2:启动异步传输(不等待完成)
HixlRequest req_handle = nullptr;
HixlMemHandle remote_handle = get_remote_handle_for_request(req.request_id);
HixlStatus status = hixlWriteRemoteAsync(
engine_,
kv_cache.data(),
kv_cache.size(),
remote_handle,
0,
&req_handle
);
if (status == HIXL_SUCCESS) {
inflight_requests_.push_back(req_handle);
}
// 阶段3:立即继续处理下一个请求的Prefill
// 传输在后台进行,与下一个请求的计算并行
}
void poll_completions() {
// 周期性轮询完成状态,释放已完成请求的资源
auto it = inflight_requests_.begin();
while (it != inflight_requests_.end()) {
HixlStatus poll_status = hixlPollRequest(engine_, *it);
if (poll_status == HIXL_SUCCESS) {
// 传输完成,通知Decode节点可以开始Decode
notify_decode_complete((*it)->request_id);
hixlDestroyRequest(*it);
it = inflight_requests_.erase(it);
} else {
++it;
}
}
}
};
hixlWriteRemoteAsync是非阻塞版本的数据传输接口,调用后立即返回,传输在后台继续执行。通过将传输过程与后续请求的计算过程并行化,有效隐藏了跨节点通信延迟。在实际部署中,这通常可以将通信开销从关键路径上完全移除——当第N个请求还在传输KV Cache时,第N+1个请求的Prefill计算已经在进行。poll_completions周期性检查传输完成状态,完成后通过通知机制告知Decode节点启动对应请求的解码过程。这种生产者-消费者模式是PD分离架构中的标准范式。
对接推理引擎:LLM-DataDist接口
vLLM集成路径
HiXL通过LLM-DataDist层提供与vLLM的直接集成。vLLM在Prefill节点生成KV Cache后,通过LLM-DataDist接口将Cache发布到HiXL传输层;Decode节点的vLLM通过对应接口订阅并获取KV Cache,注入本地解码过程。
LLM-DataDist接口的设计目标是让推理引擎开发者无需理解底层传输细节。接口核心概念包括:KVCachePublisher(发布端)、KVCacheSubscriber(订阅端)、CacheDescriptor(Cache描述符,包含形状、数据类型、存储位置等元数据)。
# Decode节点:使用LLM-DataDist Python接口订阅KV Cache
import llm_datadist as ldd
# 初始化订阅端
subscriber = ldd.KVCacheSubscriber()
subscriber.init(rank_id=0, local_ip="192.168.1.20", local_port=18000)
# 注册本地接收缓冲区
recv_buffer = ldd.allocate_host_memory(128 * 1024 * 1024) # 128MB
subscriber.register_buffer(recv_buffer)
# 订阅来自特定Prefill节点的KV Cache
# 通过Cache ID建立发布-订阅映射
cache_id = ldd.CacheId(prompt_hash="abc123", seq_len=512)
result = subscriber.subscribe(cache_id, timeout_ms=5000)
if result.status == ldd.SUBSCRIBE_STATUS_SUCCESS:
# 直接访问接收到的KV Cache,无需拷贝
kv_cache_ptr = subscriber.get_cache_data(cache_id)
print(f"Received KV Cache with {result.seq_len} tokens")
# 注入vLLM的解码器
model.executor.add_kv_cache(cache_id, kv_cache_ptr)
Python接口降低了与vLLM等Python原生推理引擎的集成门槛。KVCacheSubscriber封装了HiXL Engine的初始化、内存注册、传输等待等底层操作。CacheId通过Prompt的哈希值和序列长度唯一标识一份KV Cache,使Decode节点能够准确匹配需要订阅的Cache。allocate_host_memory内部调用aclrtMallocHost,确保分配的内存可被HiXL用于远程写入。整个流程对vLLM透明——vLLM只需要处理标准的KV Cache数据结构,传输细节完全由LLM-DataDist处理。
使用前vs使用后
使用前的通信路径
在未使用HiXL的传统PD分离方案中,KV Cache的跨节点传输通常依赖以下路径之一:
基于gRPC/socket的传输:Prefill节点将KV Cache从Device内存拷贝到Host内存,序列化为字节流,通过TCP/gRPC发送到Decode节点,Decode节点接收后反序列化,再拷贝到Device内存。整个路径涉及至少3次数据拷贝(D2H、H2N、N2H、H2D中的多个阶段),每次拷贝消耗内存带宽和延迟。在128MB KV Cache场景下,仅拷贝开销就可能达到毫秒级。
基于传统RDMA的传输:虽然RDMA可以减少拷贝次数,但标准的send/recv语义仍然需要Decode节点的CPU主动参与接收操作。此外,RDMA内存注册的区域通常较小,大规模KV Cache需要分片传输,增加了调度复杂度。CPU介入导致Decode节点的计算资源被通信任务占用,降低了有效算力。
基于NCCL等集合通信库的传输:NCCL设计用于多GPU协同计算场景,不适合PD分离这种非对称的Producer-Consumer模式。NCCL需要所有参与节点协同调用,无法由Prefill节点独立主导传输。
使用后的通信路径
引入HiXL后,通信路径得到显著简化:
零拷贝单边写入:Prefill节点的NPU直接将数据写入Decode节点预注册的Device或Host内存,全程无需Decode节点CPU参与。数据从发送端Device内存经网络链路直接写入接收端内存,中间无任何临时Buffer。对于Decode节点,接收到的数据已经位于可直接用于解码计算的内存位置,无需额外整理或拷贝。
异步流水线与细粒度重叠:HiXL的异步传输接口允许Prefill节点在启动传输后立即切换至下一个请求的处理,通信延迟完全被后续请求的计算掩盖。实测表明,在Atlas A3超节点内部署时,这种重叠可以将端到端推理延迟降低20%以上。
统一内存编址(FabricMem):在Atlas A3超节点内,FabricMem模式将各节点的Host DRAM统一编址,Prefill节点的NPU可以直接访问Decode节点的Host内存(D2RH),无需经过Decode节点NPU的转发。这种直连路径的带宽达到119GB/s(HCCS链路),相比之下传统RoCE方案的带宽仅为20GB/s左右。
与推理引擎的无缝集成:通过LLM-DataDist接口,vLLM、SGLang等推理引擎可以以最小化代码改动接入HiXL传输层。开发者无需重写通信模块,只需在引擎初始化时配置KV Cache的发布/订阅策略即可。
性能优化实践要点
传输链路选择策略
HiXL支持多种传输链路,正确选择链路类型对性能至关重要。超节点内的节点间通信应优先使用HCCS链路,其带宽显著高于RDMA。跨机架或跨服务器的通信则必须使用RDMA链路。在混合部署场景中,可以通过HiXL的多链路支持同时配置多种链路,由HiXL引擎根据目标地址自动选择最优路径。
FabricMem模式仅适用于Atlas A3系列,开启后自动启用统一内存编址。如果部署环境包含A2和A3混合节点,需要关闭FabricMem以确保兼容性。HiXL支持A2/A3/A5异构互联,但部分高级特性仅在同代际节点间可用。
内存注册粒度
内存注册的粒度影响传输效率和管理开销。过小的注册粒度导致每个KV Cache传输都需要多次注册/注销操作,增加控制面开销;过大的注册粒度则造成内存浪费。推荐的实践是为每个Decode节点预注册若干固定大小的内存池(如每个池128MB),通过内存池内的偏移量管理多个KV Cache的存储位置。HiXL的OPTION_GLOBAL_RESOURCE_CONFIG参数可以配置Fabric虚拟内存池的容量和起始地址,建议在部署规划阶段根据典型KV Cache大小进行合理配置。
异步传输的完成通知
大规模部署中,Decode节点需要同时处理来自多个Prefill节点的KV Cache传输请求。使用轮询方式检查传输完成状态会消耗CPU资源,推荐的做法是结合HiXL的完成通知机制(Completion Notification),在传输完成后由HiXL主动通知目标节点。这需要在初始化引擎时配置通知通道,并在Decode节点注册回调函数处理传输完成事件。
总结
HiXL为昇腾CANN生态提供了生产级的单边零拷贝通信能力,其在PD分离架构中的价值可以归纳为三个维度:性能维度上,零拷贝和单边通信消除了传统传输路径上的冗余拷贝和CPU介入,FabricMem模式进一步将超节点内传输带宽提升至百GB/s级别;工程维度上,分层接口设计允许开发者根据场景选择合适的抽象层级,LLM-DataDist接口使主流推理引擎的集成成本降至最低;生态维度上,HiXL已对接Mooncake、DeepLink、vLLM、SGLang等开源项目,形成了从底层传输到上层应用的完整技术栈。
仓库地址:https://atomgit.com/cann/hixl
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐
所有评论(0)