大模型在线推理,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 分离这种对延迟和吞吐同时敏感的场景,这条路径上的每一个微秒都能直接转化成推理吞吐。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐