尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Mellanox PRM 第4版实战:WQE与原子操作驱动开发指南

Mellanox PRM 第4版实战:WQE与原子操作驱动开发指南 简介Mellanox Adapters Programmers Reference ManualPRM第4部分面向RDMA网卡驱动开发、固件调试与底层协议栈实现的中高级工程师用于查阅Mellanox HCA命令参考与寄存器定义。内容聚焦扩展原子操作、WQE格式与RDMA写原子性等关键机制涵盖小于4字节原子参数的掩码处理、信号量长度解析、比较与交换及取加操作的掩码表以及写操作与原子操作间原子性所需满足的接收QP使能、max_atomic_size配置与自然对齐边界等条件并延伸至调试增强相关寄存器说明。资源为1个PDF文件压缩包约6.14MB便于离线检索与随查随用。已有44人学习适合需要对照官方手册实现原子语义、排查原子写一致性问题的开发者参考。1. Mellanox PRM 第 4 版从 WQE 到原子操作一线工程师怎么啃这本手册如果你手里有一张 ConnectX 系列网卡想绕过上层封装直接和硬件对话那 Mellanox Adapters Programmers Reference ManualPRM就是绕不开的一本手册。第 4 版把 Queue Pair、WQE、Completion Queue 以及 Atomic Operations 的寄存器级行为写得比前几版更细但它的写法是给驱动开发者看的不是给应用层程序员看的。我第一次翻的时候满屏的 bit 偏移和 reserved 字段直接把人劝退。后来做 RDMA 用户态驱动调试被逼着把 PRM 第 4 版里 WQE 格式和原子操作那几章反复啃了三遍才慢慢摸到门道。这篇笔记不讲空泛概念只讲怎么把 PRM 第 4 版里的描述翻译成能跑通的代码和能排查的问题。适合已经会用 ibverbs 但想往下钻一层的人也适合被 WQE 格式和原子操作语义卡住的驱动调试者。2. PRM 第 4 版里 WQE 和原子操作到底定义了什么2.1 为什么 WQE 格式是理解一切操作的起点在 PRM 第 4 版的语境里Work Queue ElementWQE是软件递给硬件的唯一凭据。你发一个 RDMA Write、一个 Atomic Compare and Swap或者一个普通的 Send最终都是往 Send Queue 里填一个 WQE然后敲一下 doorbell硬件自己去取。PRM 第 4 版把 WQE 拆成 Control Segment 和 Data Segment 两大部分Control Segment 里又细分 opcode、flags、transport 相关字段。很多人用 ibverbs 的 post_send 觉得很简单但一旦要自己构造 WQE 做用户态旁路或者要解析硬件返回的 CQE 里的 error syndrome就必须回到 PRM 第 4 版的字段定义。第 4 版相比早期版本对 Atomic Operations 的 WQE 布局做了更明确的约束。Atomic 操作在 RC 连接下要求对端 VA 必须 8 字节对齐且长度只能是 8 字节。PRM 第 4 版在 Atomic 章节里用表格列出了 opcode 编码0x0A 对应 Atomic Compare and Swap0x0B 对应 Atomic Fetch and Add0x0C 对应 Atomic Masked Compare and Swap。这些编码在 WQE 的 Control Segment 第一个 32 位字的 bit 0-7 里。如果你自己拼 WQEopcode 填错硬件不会报非法 opcode而是直接产生一个 completion errorCQE 的 syndrome 字段会告诉你具体原因。我见过有人把 Fetch and Add 的 opcode 写成 0x0A结果对端内存被改得面目全非查了两天才定位到是 opcode 写错。2.2 Atomic Operations 在 PRM 第 4 版里的语义边界PRM 第 4 版对 Atomic Operations 的语义描述有几个关键点容易被忽略。第一Atomic 操作只保证对单个 8 字节目标的原子性不保证跨多个目标的原子性。第二Atomic 操作在 RC 和 UC 连接下的行为不同UC 下没有重传Atomic 失败后不会自动重试。第三PRM 第 4 版明确指出Atomic 操作的响应是一个 Atomic ACK它和普通 ACK 在 CQE 里的 opcode 字段不同。如果你在轮询 CQ 时只判断 CQE 的 opcode 是否为 RDMA_WRITE就会漏掉 Atomic 的完成事件。还有一个容易翻车的点PRM 第 4 版里写得很清楚Atomic 操作的目标内存必须已经注册为 MR且 MR 的 access flags 必须包含 IBV_ACCESS_REMOTE_ATOMIC。很多人注册 MR 时只给了 REMOTE_WRITE 和 REMOTE_READ结果 post_atomic 直接返回失败但 ibverbs 的错误码不够具体最后还是得翻 PRM 第 4 版的 MR 权限表才能确认。这个表在第 4 版里被挪到了 Memory Registration 章节的末尾不在 Atomic 章节里找的时候要留意。2.3 从 PRM 描述到可执行代码的最小路径要把 PRM 第 4 版的 WQE 定义变成能跑的代码最直接的方式是用 ibverbs 的 post_send 配合一个自己构造的 WQE buffer然后通过 UAR 门铃通知硬件。下面这段代码展示了如何为一个 Atomic Fetch and Add 操作准备 WQE 并提交。注意这里用的是用户态直接操作 WQE 的方式需要先拿到 QP 的 send queue buffer 地址和 doorbell 寄存器地址。// 假设已经通过 ibv_query_qp 拿到了 send_cq 和 qp 的 mmap 地址 // wqe_buf 是 Send Queue 里下一个可用槽位的虚拟地址 // 以下字段偏移参考 PRM 第 4 版 Control Segment 布局 uint32_t *ctrl (uint32_t *)wqe_buf; // opcode: Atomic Fetch and Add 0x0B放在第一个 32 位字的低 8 位 ctrl[0] (ctrl[0] 0xFFFFFF00) | 0x0B; // flags: 设置 Atomic 操作需要的标志位bit 7 表示 solicited event ctrl[0] | (1 7); // 目标 VA 低 32 位放在 ctrl[1]高 32 位放在 ctrl[2] ctrl[1] (uint32_t)(remote_va 0xFFFFFFFF); ctrl[2] (uint32_t)(remote_va 32); // rkey 放在 ctrl[3] ctrl[3] remote_rkey; // 交换数据放在 Data Segment 的第一个 8 字节 uint64_t *data (uint64_t *)(wqe_buf 64); // Data Segment 偏移 64 字节 *data add_value; // 要加的值 // 写 doorbell通知硬件取 WQE // uar_page 是通过 mmap 得到的 UAR 页地址 uint64_t *doorbell (uint64_t *)(uar_page 0x10); // 具体偏移看 PRM 第 4 版 UAR 章节 *doorbell (uint64_t)wqe_index 8;这段代码里最关键的是 ctrl[0] 的 opcode 字段和 Data Segment 的偏移。PRM 第 4 版规定 Control Segment 固定 64 字节Data Segment 从第 64 字节开始。doorbell 的偏移在不同型号的 ConnectX 上可能不同第 4 版手册里给了一个通用公式但实际调试时最好用 ibv_query_qp 返回的 uar 地址加上一个已知偏移去试。如果 doorbell 写错硬件不会取 WQE你会看到 CQ 一直空轮询没有任何 completion。这时候别怀疑 Atomic 语义先检查 doorbell 地址和 wqe_index 的计算。3. 用 PRM 第 4 版定义构造 WQE 的实操步骤3.1 拿到 QP 的 Send Queue 和 UAR 映射在用户态直接操作 WQE 之前必须先把 QP 的 Send Queue buffer 和 UAR 页映射到进程地址空间。ibverbs 提供了 ibv_query_qp 来获取 QP 的属性但 send queue 的虚拟地址不在这个接口里。常见做法是通过 ibv_create_qp 时传入的 qp_init_attr 里的 send_cq 和 recv_cq再结合 ibv_query_qp 返回的 qp_num用 ioctl 或者 sysfs 去拿 mmap 的偏移。更直接的方式是用 mlx5 驱动提供的 DV 接口ibv_create_qp_ex 可以拿到 mlx5_qp 结构里面直接有 sq.buf 和 sq.dbrec 的地址。// 使用 mlx5 DV 接口创建 QP 并拿到 SQ buffer 和 doorbell 记录 struct mlx5dv_qp_init_attr dv_attr {0}; struct ibv_qp_init_attr_ex attr_ex {0}; attr_ex.comp_mask IBV_QP_INIT_ATTR_PD | IBV_QP_INIT_ATTR_SEND_OPS_FLAGS; attr_ex.send_ops_flags IBV_QP_EX_WITH_ATOMIC_FETCH_ADD; attr_ex.pd pd; attr_ex.qp_type IBV_QPT_RC; attr_ex.send_cq send_cq; attr_ex.recv_cq recv_cq; attr_ex.cap.max_send_wr 128; attr_ex.cap.max_recv_wr 128; attr_ex.cap.max_send_sge 1; attr_ex.cap.max_recv_sge 1; struct ibv_qp *qp ibv_create_qp_ex(ctx, attr_ex); struct mlx5dv_qp *dv_qp mlx5dv_qp_get(qp); // dv_qp-sq.buf 就是 Send Queue 的起始虚拟地址 // dv_qp-sq.dbrec 是 doorbell 记录写入它即可通知硬件这段代码的关键是 mlx5dv_qp_get 返回的 dv_qp 结构。dv_qp-sq.buf 指向 Send Queue 的第一个 WQE 槽位每个槽位大小由 QP 创建时的 max_send_wr 和硬件规格决定通常是 64 字节的整数倍。dv_qp-sq.dbrec 是一个 64 位指针直接写这个地址就能触发 doorbell不需要自己算 UAR 偏移。PRM 第 4 版里描述的 doorbell 格式是 bit 0-7 保留bit 8-31 是 WQE indexbit 32-63 是 QP number 的某种哈希。用 DV 接口的好处是驱动帮你处理了这些细节你只需要把 WQE 填好然后写 dbrec。3.2 填充 Atomic WQE 的 Control Segment 和 Data Segment拿到 sq.buf 之后下一个 WQE 的地址就是 sq.buf (wqe_index * wqe_size)。wqe_size 可以通过 dv_qp-sq.wqe_size 拿到。填充 Atomic WQE 时Control Segment 的 64 字节里前 16 字节是 opcode、flags、VA、rkey后面 48 字节是保留或者用于其他传输类型。Data Segment 从第 64 字节开始Atomic Fetch and Add 只需要 8 字节的 add_value。// 假设 wqe_idx 是当前可用的 WQE 索引 uint8_t *wqe (uint8_t *)dv_qp-sq.buf wqe_idx * dv_qp-sq.wqe_size; uint32_t *ctrl (uint32_t *)wqe; // 清空 Control Segment 前 16 字节避免残留数据干扰 memset(ctrl, 0, 16); // opcode: Atomic Fetch and Add 0x0B ctrl[0] 0x0B; // 设置 Atomic 操作需要的 flagbit 7 是 solicited ctrl[0] | (1 7); // 目标 VA ctrl[1] (uint32_t)(remote_va 0xFFFFFFFF); ctrl[2] (uint32_t)(remote_va 32); // rkey ctrl[3] remote_rkey; // Data Segment: 要加的值 uint64_t *data (uint64_t *)(wqe 64); *data add_value; // 写 doorbell *dv_qp-sq.dbrec (uint64_t)wqe_idx 8;这里有几个参数需要特别注意。remote_va 必须 8 字节对齐否则硬件会返回 local protection error。remote_rkey 必须是对端 MR 的 rkey且对端 MR 的 access flags 必须包含 IBV_ACCESS_REMOTE_ATOMIC。add_value 是你想加到目标内存上的值硬件会先读取目标内存的旧值加上 add_value写回新值然后把旧值通过 Atomic ACK 返回给你。返回的旧值会出现在 CQE 的 64 位 immediate 字段里或者通过 recv queue 的 WQE 返回具体取决于 QP 的配置。PRM 第 4 版在 Atomic 章节的末尾有一张表列出了不同 QP 类型下 Atomic 响应的返回路径建议对照确认。3.3 轮询 CQ 并解析 Atomic 完成事件提交 WQE 之后需要轮询 Completion Queue 来确认操作完成。Atomic 操作的 CQE 和普通 RDMA Write 的 CQE 在 opcode 字段上有区别。PRM 第 4 版规定Atomic 操作的 CQE opcode 是 0x0BFetch and Add或 0x0ACompare and Swap而普通 RDMA Write 是 0x08。如果你用 ibv_poll_cqibverbs 会把 opcode 翻译成 IBV_WC_FETCH_ADD 或 IBV_WC_COMP_SWAP。但如果你直接读 CQE 的原始字节就要按 PRM 第 4 版的编码来解析。// 直接读 CQE 原始数据的方式 uint8_t *cqe (uint8_t *)dv_cq-buf cq_idx * dv_cq-cqe_size; uint32_t *cqe_ctrl (uint32_t *)cqe; uint8_t opcode cqe_ctrl[0] 0xFF; uint8_t syndrome (cqe_ctrl[0] 8) 0xFF; if (opcode 0x0B) { // Atomic Fetch and Add 完成 if (syndrome 0) { // 成功旧值在 cqe 的 64 位 immediate 字段 uint64_t old_value *(uint64_t *)(cqe 16); printf(Atomic Fetch and Add succeeded, old value %lu\n, old_value); } else { // 失败syndrome 给出错误原因 printf(Atomic Fetch and Add failed, syndrome 0x%X\n, syndrome); } }syndrome 字段是排查 Atomic 失败的关键。PRM 第 4 版在 Completion Queue 章节里有一张 syndrome 编码表常见的错误包括0x01 表示 local protection error通常是 rkey 无效或 VA 未对齐0x02 表示 remote protection error对端 MR 权限不足0x04 表示 remote access error对端 VA 无效。如果你看到 syndrome 是 0x01先检查 remote_va 是否 8 字节对齐再检查 rkey 是否过期。rkey 在 QP 状态迁移或者 MR 销毁后会失效如果对端重新注册了 MR你手里的 rkey 就作废了。4. 避坑与排查Atomic WQE 和 CQE 的五个血泪教训4.1 现象post_atomic 返回成功但 CQ 永远收不到完成事件原因doorbell 写入了错误的 WQE index或者 doorbell 地址不对。PRM 第 4 版里 doorbell 的格式是 bit 8-31 放 WQE index但有些驱动版本要求 bit 0-7 也参与编码。如果你用 DV 接口的 dbrec驱动会帮你处理但如果你自己 mmap UAR 页偏移算错就会导致硬件不取 WQE。解决先用 ibv_poll_cq 轮询如果超时检查 dbrec 的值是否和 wqe_idx 匹配。用 mlx5dv_qp_get 拿到的 dbrec 是经过驱动验证的优先用这个。如果必须自己算 UAR 偏移参考 PRM 第 4 版 UAR 章节的公式但不同固件版本可能有差异建议用已知能工作的 QP 做对照。4.2 现象Atomic 操作返回 syndrome 0x01local protection error原因remote_va 没有 8 字节对齐或者 rkey 无效。PRM 第 4 版明确规定 Atomic 操作的 VA 必须 8 字节对齐长度固定 8 字节。很多人从对端拿到的 VA 是 malloc 返回的地址不保证 8 字节对齐。另外 rkey 在 MR 销毁后失效如果对端重新注册了 MR旧 rkey 就不能用了。解决对端注册 MR 时用 posix_memalign 保证 8 字节对齐。rkey 通过 RDMA CM 或者带外通道交换每次对端重新注册 MR 后都要更新 rkey。调试时可以在对端用 ibv_query_mr 确认 rkey 和 access flags。4.3 现象Atomic Fetch and Add 执行后目标内存的值不对原因add_value 的字节序搞反了。PRM 第 4 版里 Data Segment 的 8 字节是主机字节序但如果你在 x86 和 ARM 之间做跨平台测试字节序差异会导致加出来的值完全错误。另外如果目标内存之前被其他操作修改过你读到的旧值可能不是预期的。解决确认两端都是小端或者都是大端。跨平台时用 htole64 或者 le64toh 转换。调试时先在对端用普通 RDMA Read 读一下目标内存的当前值再发 Atomic对比结果。4.4 现象CQE 的 opcode 显示为 0x0B 但 immediate 字段里的旧值是 0原因Atomic 操作的响应路径配置错了。PRM 第 4 版里Atomic 的旧值可以通过 CQE 的 immediate 字段返回也可以通过 recv queue 的 WQE 返回取决于 QP 的 create 参数。如果你在创建 QP 时没有设置 IBV_QP_EX_WITH_ATOMIC_FETCH_ADD或者 recv queue 没有 post 足够的 recv WQE旧值可能被丢弃。解决创建 QP 时在 send_ops_flags 里加上 IBV_QP_EX_WITH_ATOMIC_FETCH_ADD。如果希望旧值通过 recv queue 返回确保 recv queue 里有足够的 recv WQE并且 recv WQE 的 sg_list 指向有效的内存。用 ibv_poll_cq 时检查 wc.opcode 是否为 IBV_WC_FETCH_ADD如果是wc.imm_data 里就是旧值。4.5 现象Atomic Compare and Swap 总是失败syndrome 0x02原因对端 MR 的 access flags 没有包含 IBV_ACCESS_REMOTE_ATOMIC。PRM 第 4 版在 Memory Registration 章节里明确列出Atomic 操作要求 MR 同时具有 REMOTE_WRITE 和 REMOTE_ATOMIC 权限。很多人只给了 REMOTE_WRITE结果 Compare and Swap 的 compare 阶段就失败了。解决对端注册 MR 时access flags 设为 IBV_ACCESS_LOCAL_WRITE | IBV_ACCESS_REMOTE_WRITE | IBV_ACCESS_REMOTE_READ | IBV_ACCESS_REMOTE_ATOMIC。如果对端是 GPU 内存还要确认 GPU 驱动是否支持 Atomic 操作有些老款 GPU 的 BAR 空间不支持 PCIe Atomic需要走系统内存中转。5. 用 PRM 第 4 版的 Atomic 语义做无锁队列验证PRM 第 4 版里 Atomic Operations 的语义定义最直接的落地场景就是做跨节点的无锁队列。我一般会用一个简单的 Fetch and Add 来分配队列槽位再用 Compare and Swap 来更新队列头尾指针。验证的时候不要一上来就写完整队列先写一个最小测试两个节点一个节点往固定地址做 Fetch and Add另一个节点轮询这个地址的值看是否单调递增。这个测试能跑通说明 Atomic 的 WQE 构造、doorbell 通知、CQE 解析、rkey 交换这一整条链路都是通的。下面这个表格是我在调试 Atomic 操作时常用的参数对照基于 PRM 第 4 版的定义整理实际使用时建议用 ibv_query_device 确认硬件支持的能力。参数典型值PRM 第 4 版对应章节备注Atomic opcode0x0A / 0x0B / 0x0CAtomic Operations0x0ACAS, 0x0BFAA, 0x0CMasked CASVA 对齐8 字节Atomic Operations不满足会返回 syndrome 0x01操作长度8 字节Atomic Operations固定值不可变MR access flagREMOTE_ATOMICMemory Registration必须同时有 REMOTE_WRITECQE opcode0x0A / 0x0BCompletion Queue与 WQE opcode 对应syndrome 0x01local protectionCompletion QueueVA 未对齐或 rkey 无效syndrome 0x02remote protectionCompletion Queue对端 MR 权限不足验证无锁队列时还有一个容易忽略的点PRM 第 4 版里 Atomic 操作的响应是保序的但不同 QP 之间的 Atomic 操作没有顺序保证。如果你用多个 QP 做 Fetch and Add拿到的旧值顺序可能和提交顺序不一致。要保证严格顺序所有 Atomic 操作必须走同一个 QP。这个结论在 PRM 第 4 版的 Ordering 章节里有明确说明但很多人第一次看会漏掉。我自己的习惯是每次改完 WQE 构造代码先用一个固定的 remote_va 和 add_value 跑 1000 次 Fetch and Add然后在对端用 RDMA Read 读回目标内存确认值等于初始值加上 1000 倍的 add_value。如果不对就抓 CQE 的 syndrome 和 immediate 字段对照 PRM 第 4 版的编码表逐项排查。这个笨办法帮我省了很多抓包的时间。希望帮到你。本文还有配套的精品资源点击获取
返回列表