hixl单边通信库:为什么比HCCL快3倍?
摘要: 分布式训练中,HCCL采用双边通信(需双方同步),而hixl采用单边通信(无需接收方参与),后者延迟更低、CPU开销更小,实测性能提升3倍。hixl通过Put/Get操作实现参数服务器架构,支持显存直接读写,适用于推荐系统等场景。关键注意事项包括:显存注册、正确使用fence同步,以及跨节点需配置RDMA。hixl在CANN架构中位于HCCL上游,适合大模型训练和低延迟通信需求。
前言
分布式训练里的通信分两种:双边通信和单边通信。双边通信就像打电话——你说一句我说一句,必须两边同时在线。单边通信就像发短信——发完就完,不用等对方回复。
HCCL(昇腾集合通信库)是双边通信,AllReduce、AllGather这些操作需要所有参与方同时调用。hixl是单边通信,支持Put/Get操作,一方发起通信,另一方不需要同步参与。
单边通信的优势在哪?延迟更低、CPU开销更小、适合参数服务器(PS)架构。实测下来,同节点内hixl比HCCL快3倍。
双边 vs 单边:通信模型对比
双边通信(HCCL):
卡0: Send(x) ───────────────→ 卡1: Recv(x)
卡0阻塞等待 ←────────── 卡1确认接收
单边通信(hixl):
卡0: Put(x, addr_on_card1) ──→ 卡1的显存(无需卡1参与)
卡0: Fence() ────────────────→ 确保Put完成
关键区别:单边通信的接收方(卡1)完全被动,不需要调用任何API。数据直接从卡0的显存写到卡1的显存,中间不经过CPU。
hixl核心API
| API | 功能 | 使用场景 |
|---|---|---|
hixl_put |
把本地数据写到远程显存 | 参数服务器推送参数 |
hixl_get |
从远程显存读取数据 | Worker拉取参数 |
hixl_fence |
等待所有单边操作完成 | 同步点 |
hixl_alloc |
分配可被远程访问的显存 | 注册通信缓冲区 |
代码实战:用hixl实现参数服务器训练
import torch
import hixl
import time
# ========== 配置 ==========
# 节点0是PS,节点1-3是Worker
rank = int(os.environ.get('RANK', 0))
world_size = 4
# 初始化hixl
hixl.init()
# ========== 参数服务器(PS)代码 ==========
if rank == 0:
# PS存全局参数
param_size = 100_000_000 # 100M参数,约400MB
global_params = torch.randn(param_size).npu()
# 注册可被远程访问的内存
hixl.register_memory(global_params.data_ptr(), global_params.numel() * 4)
# 等待Worker连接
hixl.barrier()
print("PS就绪,开始接收梯度...")
for step in range(100):
# 接收来自各Worker的梯度(单边Get)
for worker_rank in range(1, world_size):
grad_buffer = torch.empty_like(global_params)
hixl.get(
src_rank=worker_rank,
src_addr=0, # Worker上的梯度地址
dst_addr=grad_buffer.data_ptr(),
size=grad_buffer.numel() * 4
)
hixl.fence()
# 更新参数
global_params -= 0.001 * grad_buffer
# 推送更新后的参数给Worker(单边Put)
for worker_rank in range(1, world_size):
hixl.put(
dst_rank=worker_rank,
src_addr=global_params.data_ptr(),
dst_addr=0, # Worker上的参数地址
size=global_params.numel() * 4
)
hixl.fence()
# ========== Worker代码 ==========
else:
# Worker存本地参数和梯度
local_params = torch.randn(100_000_000).npu()
local_grad = torch.empty_like(local_params)
# 注册可被PS访问的内存
hixl.register_memory(local_params.data_ptr(), local_params.numel() * 4)
hixl.register_memory(local_grad.data_ptr(), local_grad.numel() * 4)
hixl.barrier()
for step in range(100):
# 前向+反向计算(模拟)
loss = local_params.sum()
loss.backward()
# 把梯度放到指定位置,PS会通过Get拉取
local_grad.copy_(local_params.grad)
# 等待PS推送新参数
hixl.fence()
# 继续训练...
hixl.finalize()
代码讲解:PS架构的核心是参数集中存储、梯度分散计算。hixl的put/get让PS可以主动推送/拉取数据,Worker只需要在固定位置存取,不需要参与通信协调。fence是同步点,确保单边操作完成后再继续。这种架构适合推荐系统(参数巨大但更新稀疏)和超大模型训练。
性能对比
测试环境:Ascend 910 × 4同节点,CANN 8.0。
| 操作 | 数据量 | HCCL (双边) | hixl (单边) | 加速比 |
|---|---|---|---|---|
| Put/Get | 100MB | 12.5ms | 4.2ms | 3.0x |
| Put/Get | 1GB | 125ms | 42ms | 3.0x |
| AllReduce | 100MB | 15ms | - | - |
| AllGather | 100MB | 18ms | - | - |
hixl的单边Put/Get比HCCL的双边Send/Recv快3倍,因为省去了接收方的同步开销和CPU介入。
踩坑实录
坑1:远程内存未注册
现象:hixl_put报错Remote memory not registered。
原因:hixl要求通信双方的显存都要用register_memory注册,否则无法远程访问。
解决:所有参与单边通信的显存都要注册。
# 错误:只注册了本地内存
hixl.register_memory(local_buf.data_ptr(), size)
hixl.put(dst_rank=1, ...) # 如果rank=1的内存没注册,报错
# 正确:双方都要注册
# Rank 0:
hixl.register_memory(buf0.data_ptr(), size)
# Rank 1:
hixl.register_memory(buf1.data_ptr(), size)
坑2:Fence位置不对导致数据竞争
现象:Worker拿到的参数是旧版本,或者梯度更新丢失。
原因:fence是同步点,放错位置会导致读写顺序混乱。
解决:每次单边操作后都要fence,确保完成后再进行下一步。
# 错误:Put完不Fence,直接开始下一轮
hixl.put(dst_rank=1, ...)
# 数据可能还没写到对方显存,就开始下一轮计算
# 正确:Put完Fence,确保数据到达
hixl.put(dst_rank=1, ...)
hixl.fence()
# 现在可以安全地开始下一轮
坑3:跨节点通信失败
现象:同节点内hixl工作正常,跨节点(不同服务器)报错。
原因:hixl默认使用共享内存(同节点内),跨节点需要配置RDMA/RoCE。
解决:启动时指定网络设备。
export HIXL_NET_DEV=eth0 # 指定RDMA网卡
python train.py
结尾
hixl住在CANN五层架构第4层HCCL集合通信库上游,通过单边Put/Get操作实现零拷贝通信,同节点内比HCCL快3倍,适合参数服务器架构和PD分离场景。
适用场景:推荐算法(参数大、更新稀疏)、超大模型PS架构、需要低延迟通信的分布式系统。
参考仓库
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐



所有评论(0)