昇腾CANN hixl:零拷贝单边通信如何撑起大模型 PD 分离推理
大模型在线推理,Prefill 和 Decode 两个阶段对硬件的要求完全不同。Prefill 吃算力——一次性处理几百上千个 token 的 prompt,Cube 单元满载运转。Decode 吃带宽——每步只生成一个 token,但要从 KV Cache 里反复读几十 KB 的历史数据,瓶颈全在 HBM 带宽和通信链路上。
把这两个阶段拆到不同 NPU 上跑,Prefill 用高算力卡,Decode 用高带宽卡——这叫 PD 分离。但分离之后带来一个新问题:Prefill 算出来的 KV Cache 需要搬到 Decode 的 NPU 上。这一搬,数据要跨物理机器走网络,延迟和带宽就变成了新瓶颈。
hixl 专为这个场景设计。它做的是单边通信——发送方把数据推到接收方的 HBM 里,接收方完全不用参与。全程零拷贝,不需要 CPU 中转,不需要接收方同步等待。
单边通信和双边通信的根本区别
日常使用的通信库(hccl、NCCL、MPI)都是双边通信。hcclAllReduce 发送方调 AllReduce 发送数据,接收方也要调 AllReduce 来接数据——两边要同步,谁先到谁等。
hixl 的单边通信是另一套逻辑。发送方调 hixlPut 把数据推到远程 NPU 的 HBM 里,结束了就是结束了。接收方什么时候来读、甚至要不要读,发送方不关心。接收方的 NPU 连一个函数都不用调——数据自己出现在 HBM 的指定地址上了。
这对于 PD 分离意味着什么:
Prefill NPU 算完一批 KV Cache 后,调 hixlPut 直接把 KV 张量推到 Decode NPU 的 HBM 里。Prefill 的算子继续跑下一批 prompt,Decode 的算子直接从本地 HBM 读 KV Cache——两边完全解耦,没有同步等待。
// Prefill NPU 侧:算完 KV Cache,推到 Decode NPU
#include "hixl/hixl.h"
hixlContext_t ctx;
hixlInit(&ctx, 0); // rank=0: Prefill NPU
hixlMemory_t remote_kv; // 指向 Decode NPU HBM 上预分配的内存
hixlGetRemoteMem(ctx, 1, &remote_kv, kv_size); // rank=1: Decode NPU
// 算完一批 KV Cache 后,直接推到远程
// 这个操作不经过 CPU,数据从 Prefill NPU 的 HBM → RDMA 网卡 → Decode NPU 的 HBM
hixlPut(ctx, remote_kv, local_kv, kv_size);
// 函数返回后数据已经到达对端 HBM,持续计算,不等确认
接收方什么都感知不到。Decode NPU 跑自回归生成时,像读本地 HBM 变量一样读取 KV Cache——它不知道这块数据是另一端推进来的。
零拷贝怎么实现的
三个技术底座:
RDMA 硬件直通。 昇腾NPU 的网卡支持 RDMA,hixl 绕过操作系统内核协议栈,直接操作 RDMA 网卡的队列对(Queue Pair)。数据从 Prefill NPU 的 HBM 出来后经过 PCIe 到网卡,网卡用 RDMA Write 直接把数据写到 Decode NPU 所在机器的内存——对端 CPU 全程不介入。
HBM 地址注册。 hixl 初始化时把本地 NPU 的 HBM 注册到 RDMA 网卡的内存翻译表里。注册完之后,远程可以直接通过 RDMA 访问这块 HBM 地址,不需要本机做地址转换。
// hixl 的 HBM 注册——只在初始化做一次
hixlContext_t ctx;
hixlInit(&ctx, rank);
// 预分配一块 HBM 给 KV Cache
void* kv_pool;
aclrtMalloc(&kv_pool, MAX_KV_POOL_SIZE, ACL_MEM_MALLOC_HUGE_FIRST);
// 注册到 RDMA:远程 NPU 可以直接用 RDMA 写这块地址
hixlMemDesc_t mem_desc = {
.ptr = kv_pool,
.size = MAX_KV_POOL_SIZE,
.type = HIXL_MEM_HBM,
.access = HIXL_ACCESS_REMOTE_WRITE // 允许远程写入
};
hixlMemRegister(ctx, &mem_desc);
内存窗口。 hixl 不是每次通信都重建连接。初始化时建立一个持久的内存窗口(Memory Window),后续所有的 hixlPut / hixlGet 操作复用同一个窗口,只更新偏移量和长度——单次通信的延迟从毫秒级压到微秒级。
PD 分离的完整流水
拿 LLaMA-70B 的 PD 分离部署为例,Prefill 和 Decode 各用多张 NPU,hixl 管跨组 KV Cache 传输:
# PD 分离部署脚本(简化)
# 一组 Prefill NPU(高算力),一组 Decode NPU(高带宽),hixl 连接两组
# Prefill 组初始化
import hixl
prefill_ctx = hixl.init(rank=0, group="prefill")
# Decode 组初始化
decode_ctx = hixl.init(rank=4, group="decode")
# Prefill 组预分配 KV Cache 池,并注册到 hixl
kv_cache_size = 32 * 1024 * 128 * 2 # 32K tokens, 128 heads, fp16
kv_prefill = hixl.allocate_registered(kv_cache_size, access="remote_write")
# Decode 组拿到 Prefill 组的远程地址
kv_remote = hixl.get_remote_addr(prefill_ctx, target=decode_ctx)
# --- 推理循环 ---
while True:
prompt = get_next_prompt()
# 1. Prefill 组算 KV Cache
with torch.no_grad():
kv = model.prefill(prompt.npu())
# 2. hixl 推送 KV Cache 到 Decode 组
# Decode 的算子在执行的时候,KV Cache 已经在 HBM 里了
hixl.put(prefill_ctx, kv_remote, kv.data_ptr(), kv.numel() * 2)
# 3. Prefill 组不等待,立刻处理下一个 prompt
# Decode 组异步读 KV Cache 开始自回归生成
和 hccl 的区别
| 维度 | hccl | hixl |
|---|---|---|
| 通信模型 | 双边(发送方+接收方都要调 API) | 单边(只发送方调 API) |
| 同步方式 | 集体同步(AllReduce 等全局 barrier) | 无同步(发送完就结束) |
| 数据流向 | 双向(AllReduce 需要回传) | 单向(推或拉) |
| 典型场景 | 训练梯度同步 | 推理 PD 分离的 KV Cache 传输 |
| 零拷贝 | 部分支持 | 原生零拷贝(RDMA 直写) |
| CPU 介入 | 有(同步逻辑依赖 CPU 调度) | 极少(RDMA 直通,不经过 CPU) |
两者不是替代关系。训练时梯度同步用 hccl——每张卡的梯度要全局求和再分回去,这是典型的集体通信场景。推理时 KV Cache 从 Prefill 推到 Decode 是单向的点对点传输,用 hixl 更自然。实际部署中两个库共存,各管各的场景。
适用边界
hixl 最佳发挥的场景是:单向、大块、低频的数据传输——每次推送的数据量大(几十 MB 到几百 MB),推送频率低(每次 Prefill 一次),接收方不需要响应。PD 分离恰好满足所有三个条件。
不适合的场景也很明确:需要全局同步的操作(梯度求均值),小批量高频通信(每次几 KB 的激活值传输),以及需要接收方确认的场景(checkpoint 写入确认)。这些场景 hccl 更合适——集体通信天然解决全局同步问题,开销也在高频小数据场景下更低。
单边通信不是新概念,MPI RMA(Remote Memory Access)做了很多年。hixl 的特殊之处在于从设计之初就对齐了 NPU 的 HBM 和 RDMA 混合链路——不做 CPU 中转,不做协议栈绕路,数据从一块 NPU HBM 到另一块 NPU HBM 走的全是硬件直通路径。对于 PD 分离这种对延迟和吞吐同时敏感的场景,这条路径上的每一个微秒都能直接转化成推理吞吐。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)