HCCL 03 ctypes:结构体、对齐与不透明字节块
三个结构体绑定涉及三个 C 结构体,各有绑法。
HcclRootInfo — 不透明字节块
Root 节点标识信息:存储和传递 Root 节点的 Rank 标识信息,主要包含 Device IP、Device ID 等关键信息 。
使用场景:在调用 HCCL 初始化接口(如
HcclCommInitRootInfo或HcclCommInitRootInfoConfig)之前,需仅在 Root 节点调用HcclGetRootInfo接口生成该结构体 。
数据流转:HcclRootInfo需广播至集群内的所有 Rank,以便所有节点使用相同的 Root 信息进行通信域初始化 。
C 侧定义是 char internal[4108],内部字段不公开。绑定照抄:
class HcclRootInfo(ctypes.Structure):
_fields_ = [("internal", ctypes.c_char * 4108)]
不做字段级解析,整体当 4108 字节的字节块。跨进程分发时序列化:
def root_info_to_bytes(info):
return ctypes.string_at(ctypes.addressof(info), ctypes.sizeof(info))
def root_info_from_bytes(data):
info = HcclRootInfo()
if len(data) != ctypes.sizeof(info):
raise ValueError(f"rootInfo size {len(data)} != {ctypes.sizeof(info)}")
ctypes.memmove(ctypes.addressof(info), data, len(data))
return info
必须用 string_at / memmove,不能用 bytes(info.internal)。原因见下文 NUL 截断坑。
HcclCommConfig — 逐字段定义
通信域配置项:定义 HCCL 通信域的具体运行参数和配置 。先调用
HcclCommConfigInit接口对其进行初始化,且同一通信域中所有 Rank 的配置项内容需保持一致。项目如:
Buffer 大小:共享数据的缓存区大小(单位 MB,默认值通常为 200) 。
确定性计算开关:控制归约类通信算子是否开启确定性计算(默认关闭) 。
通信域名称:用于标识通信域 。
通信算子展开模式:其他高级配置项 。
20 个字段,按 hccl_types.h 字段顺序逐字段定义,依赖 ctypes 默认对齐与 C 编译器一致:
class HcclCommConfig(ctypes.Structure):
_fields_ = [
("reserved", ctypes.c_char * 24),
("hcclBufferSize", ctypes.c_uint32),
("hcclDeterministic", ctypes.c_uint32),
("hcclCommName", ctypes.c_char * 128),
("hcclUdi", ctypes.c_char * 128),
("hcclOpExpansionMode", ctypes.c_uint32),
("hcclRdmaTrafficClass", ctypes.c_uint32),
("hcclRdmaServiceLevel", ctypes.c_uint32),
("hcclWorldRankID", ctypes.c_uint32),
("hcclJobID", ctypes.c_uint64),
("aclGraphZeroCopyEnable", ctypes.c_uint8),
("hcclExecTimeOut", ctypes.c_int32),
("hcclAlgo", ctypes.c_char * 1600),
("hcclRetryEnable", ctypes.c_char * 50),
("hcclRetryParams", ctypes.c_char * 128),
("hcclBufferName", ctypes.c_char * 128),
("hcclQos", ctypes.c_uint32),
("hcclSymWinMaxMemSizePerRank", ctypes.c_uint64),
("hcclChannelSqDepth", ctypes.c_uint32),
]
布局一致性靠 UT 断言 ctypes.sizeof(HcclCommConfig) 对照预期值校验。字段顺序不能乱——C 编译器按声明顺序排布,Python 侧必须一致。
HcclSendRecvItem — 混合类型
点对点通信任务定义:定义批量点对点通信操作中每一个具体任务的基本信息 。通常作为数组传入
HcclBatchSendRecv接口,以实现单次调用完成本 Rank 上的多个异步收发任务 。包含:
任务类型:标识当前任务是发送(Send)还是接收(Recv) 。
数据地址:数据发送或接收的 Buffer 地址 。
数据个数与类型:指定发送/接收数据的元素个数及数据类型(如 FP32、INT8 等) 。
对端 Rank:通信域内数据接收端或发送端的 Rank 编号 。
class HcclSendRecvItem(ctypes.Structure):
_fields_ = [
("sendRecvType", ctypes.c_int), # enum → int
("buf", ctypes.c_void_p), # device 内存指针
("count", ctypes.c_uint64),
("dataType", ctypes.c_int), # enum → int
("remoteRank", ctypes.c_uint32),
]
batch_send_recv 算子用这个结构体数组,调用时构造:
items = (HcclSendRecvItem * n)(*send_recv_items)
ret = hccl.HcclBatchSendRecv(items, ctypes.c_uint32(n), comm, stream)
NUL 截断坑
HcclCommConfig 的前 24 字节是头部(size/magicWord/version/reserved),想读出来校验:
# 错误写法 — bytes(c_char * 24) 遇到 NUL 字节就截断
header = bytes(cfg.reserved) # 如果第 5 字节是 \x00,只拿到 5 字节
# 正确写法 — 按原始内存取整 24 字节
header = ctypes.string_at(ctypes.addressof(cfg), 24)
ctypes.c_char * N 转 bytes 时,行为类似 C 字符串——遇到 \x00 就停。结构体头部里 magicWord、version 这些二进制数据中间有 NUL 字节是正常的,bytes() 会截断,拿到的是残缺数据。
规则:结构体序列化一律按原始内存 string_at / memmove,禁用 c_char * N 字段级读写。 这条规则写进了 _binding.py 的模块 docstring。
rootInfo 跨进程分发
2 卡建域时,rank0 调 get_root_info() 拿到 HcclRootInfo,需要传给 rank1。分发方式是用户代码(文件/socket),绑定提供辅助函数:
def save_root_info(info, path):
raw = root_info_to_bytes(info)
tmp = path + ".tmp"
with open(tmp, "wb") as f:
f.write(raw)
os.replace(tmp, path) # 原子替换,避免对端读到半截文件
os.replace 是原子操作——对端要么看到旧文件,要么看到完整新文件,不会读到写了一半的 4108 字节。这在 2 卡测试中很重要:rank1 轮询等待 rootInfo 文件出现,如果 rank0 用普通 write 非原子写入,rank1 可能读到不完整的结构体,memmove 后建域行为未定义。
下一篇:那个 ctypes 加载不了的 inline 函数。
- 点赞
- 收藏
- 关注作者
评论(0)