:WQE與原子操作驅(qū)動開發(fā)指南)
簡介Mellanox Adapters Programmers Reference ManualPRM第4部分面向RDMA網(wǎng)卡驅(qū)動開發(fā)、固件調(diào)試與底層協(xié)議棧實現(xiàn)的中高級工程師用于查閱Mellanox HCA命令參考與寄存器定義。內(nèi)容聚焦擴(kuò)展原子操作、WQE格式與RDMA寫原子性等關(guān)鍵機(jī)制涵蓋小于4字節(jié)原子參數(shù)的掩碼處理、信號量長度解析、比較與交換及取加操作的掩碼表以及寫操作與原子操作間原子性所需滿足的接收QP使能、max_atomic_size配置與自然對齊邊界等條件并延伸至調(diào)試增強(qiáng)相關(guān)寄存器說明。資源為1個PDF文件壓縮包約6.14MB便于離線檢索與隨查隨用。已有44人學(xué)習(xí)適合需要對照官方手冊實現(xiàn)原子語義、排查原子寫一致性問題的開發(fā)者參考。1. Mellanox PRM 第 4 版從 WQE 到原子操作一線工程師怎么啃這本手冊如果你手里有一張 ConnectX 系列網(wǎng)卡想繞過上層封裝直接和硬件對話那 Mellanox Adapters Programmers Reference ManualPRM就是繞不開的一本手冊。第 4 版把 Queue Pair、WQE、Completion Queue 以及 Atomic Operations 的寄存器級行為寫得比前幾版更細(xì)但它的寫法是給驅(qū)動開發(fā)者看的不是給應(yīng)用層程序員看的。我第一次翻的時候滿屏的 bit 偏移和 reserved 字段直接把人勸退。后來做 RDMA 用戶態(tài)驅(qū)動調(diào)試被逼著把 PRM 第 4 版里 WQE 格式和原子操作那幾章反復(fù)啃了三遍才慢慢摸到門道。這篇筆記不講空泛概念只講怎么把 PRM 第 4 版里的描述翻譯成能跑通的代碼和能排查的問題。適合已經(jīng)會用 ibverbs 但想往下鉆一層的人也適合被 WQE 格式和原子操作語義卡住的驅(qū)動調(diào)試者。2. PRM 第 4 版里 WQE 和原子操作到底定義了什么2.1 為什么 WQE 格式是理解一切操作的起點(diǎn)在 PRM 第 4 版的語境里Work Queue ElementWQE是軟件遞給硬件的唯一憑據(jù)。你發(fā)一個 RDMA Write、一個 Atomic Compare and Swap或者一個普通的 Send最終都是往 Send Queue 里填一個 WQE然后敲一下 doorbell硬件自己去取。PRM 第 4 版把 WQE 拆成 Control Segment 和 Data Segment 兩大部分Control Segment 里又細(xì)分 opcode、flags、transport 相關(guān)字段。很多人用 ibverbs 的 post_send 覺得很簡單但一旦要自己構(gòu)造 WQE 做用戶態(tài)旁路或者要解析硬件返回的 CQE 里的 error syndrome就必須回到 PRM 第 4 版的字段定義。第 4 版相比早期版本對 Atomic Operations 的 WQE 布局做了更明確的約束。Atomic 操作在 RC 連接下要求對端 VA 必須 8 字節(jié)對齊且長度只能是 8 字節(jié)。PRM 第 4 版在 Atomic 章節(jié)里用表格列出了 opcode 編碼0x0A 對應(yīng) Atomic Compare and Swap0x0B 對應(yīng) Atomic Fetch and Add0x0C 對應(yīng) Atomic Masked Compare and Swap。這些編碼在 WQE 的 Control Segment 第一個 32 位字的 bit 0-7 里。如果你自己拼 WQEopcode 填錯硬件不會報非法 opcode而是直接產(chǎn)生一個 completion errorCQE 的 syndrome 字段會告訴你具體原因。我見過有人把 Fetch and Add 的 opcode 寫成 0x0A結(jié)果對端內(nèi)存被改得面目全非查了兩天才定位到是 opcode 寫錯。2.2 Atomic Operations 在 PRM 第 4 版里的語義邊界PRM 第 4 版對 Atomic Operations 的語義描述有幾個關(guān)鍵點(diǎn)容易被忽略。第一Atomic 操作只保證對單個 8 字節(jié)目標(biāo)的原子性不保證跨多個目標(biāo)的原子性。第二Atomic 操作在 RC 和 UC 連接下的行為不同UC 下沒有重傳Atomic 失敗后不會自動重試。第三PRM 第 4 版明確指出Atomic 操作的響應(yīng)是一個 Atomic ACK它和普通 ACK 在 CQE 里的 opcode 字段不同。如果你在輪詢 CQ 時只判斷 CQE 的 opcode 是否為 RDMA_WRITE就會漏掉 Atomic 的完成事件。還有一個容易翻車的點(diǎn)PRM 第 4 版里寫得很清楚Atomic 操作的目標(biāo)內(nèi)存必須已經(jīng)注冊為 MR且 MR 的 access flags 必須包含 IBV_ACCESS_REMOTE_ATOMIC。很多人注冊 MR 時只給了 REMOTE_WRITE 和 REMOTE_READ結(jié)果 post_atomic 直接返回失敗但 ibverbs 的錯誤碼不夠具體最后還是得翻 PRM 第 4 版的 MR 權(quán)限表才能確認(rèn)。這個表在第 4 版里被挪到了 Memory Registration 章節(jié)的末尾不在 Atomic 章節(jié)里找的時候要留意。2.3 從 PRM 描述到可執(zhí)行代碼的最小路徑要把 PRM 第 4 版的 WQE 定義變成能跑的代碼最直接的方式是用 ibverbs 的 post_send 配合一個自己構(gòu)造的 WQE buffer然后通過 UAR 門鈴?fù)ㄖ布O旅孢@段代碼展示了如何為一個 Atomic Fetch and Add 操作準(zhǔn)備 WQE 并提交。注意這里用的是用戶態(tài)直接操作 WQE 的方式需要先拿到 QP 的 send queue buffer 地址和 doorbell 寄存器地址。// 假設(shè)已經(jīng)通過 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: 設(shè)置 Atomic 操作需要的標(biāo)志位bit 7 表示 solicited event ctrl[0] | (1 7); // 目標(biāo) 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; // 交換數(shù)據(jù)放在 Data Segment 的第一個 8 字節(jié) uint64_t *data (uint64_t *)(wqe_buf 64); // Data Segment 偏移 64 字節(jié) *data add_value; // 要加的值 // 寫 doorbell通知硬件取 WQE // uar_page 是通過 mmap 得到的 UAR 頁地址 uint64_t *doorbell (uint64_t *)(uar_page 0x10); // 具體偏移看 PRM 第 4 版 UAR 章節(jié) *doorbell (uint64_t)wqe_index 8;這段代碼里最關(guān)鍵的是 ctrl[0] 的 opcode 字段和 Data Segment 的偏移。PRM 第 4 版規(guī)定 Control Segment 固定 64 字節(jié)Data Segment 從第 64 字節(jié)開始。doorbell 的偏移在不同型號的 ConnectX 上可能不同第 4 版手冊里給了一個通用公式但實際調(diào)試時最好用 ibv_query_qp 返回的 uar 地址加上一個已知偏移去試。如果 doorbell 寫錯硬件不會取 WQE你會看到 CQ 一直空輪詢沒有任何 completion。這時候別懷疑 Atomic 語義先檢查 doorbell 地址和 wqe_index 的計算。3. 用 PRM 第 4 版定義構(gòu)造 WQE 的實操步驟3.1 拿到 QP 的 Send Queue 和 UAR 映射在用戶態(tài)直接操作 WQE 之前必須先把 QP 的 Send Queue buffer 和 UAR 頁映射到進(jìn)程地址空間。ibverbs 提供了 ibv_query_qp 來獲取 QP 的屬性但 send queue 的虛擬地址不在這個接口里。常見做法是通過 ibv_create_qp 時傳入的 qp_init_attr 里的 send_cq 和 recv_cq再結(jié)合 ibv_query_qp 返回的 qp_num用 ioctl 或者 sysfs 去拿 mmap 的偏移。更直接的方式是用 mlx5 驅(qū)動提供的 DV 接口ibv_create_qp_ex 可以拿到 mlx5_qp 結(jié)構(gòu)里面直接有 sq.buf 和 sq.dbrec 的地址。// 使用 mlx5 DV 接口創(chuàng)建 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 記錄寫入它即可通知硬件這段代碼的關(guān)鍵是 mlx5dv_qp_get 返回的 dv_qp 結(jié)構(gòu)。dv_qp-sq.buf 指向 Send Queue 的第一個 WQE 槽位每個槽位大小由 QP 創(chuàng)建時的 max_send_wr 和硬件規(guī)格決定通常是 64 字節(jié)的整數(shù)倍。dv_qp-sq.dbrec 是一個 64 位指針直接寫這個地址就能觸發(fā) doorbell不需要自己算 UAR 偏移。PRM 第 4 版里描述的 doorbell 格式是 bit 0-7 保留bit 8-31 是 WQE indexbit 32-63 是 QP number 的某種哈希。用 DV 接口的好處是驅(qū)動幫你處理了這些細(xì)節(jié)你只需要把 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 字節(jié)里前 16 字節(jié)是 opcode、flags、VA、rkey后面 48 字節(jié)是保留或者用于其他傳輸類型。Data Segment 從第 64 字節(jié)開始Atomic Fetch and Add 只需要 8 字節(jié)的 add_value。// 假設(shè) wqe_idx 是當(dāng)前可用的 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 字節(jié)避免殘留數(shù)據(jù)干擾 memset(ctrl, 0, 16); // opcode: Atomic Fetch and Add 0x0B ctrl[0] 0x0B; // 設(shè)置 Atomic 操作需要的 flagbit 7 是 solicited ctrl[0] | (1 7); // 目標(biāo) 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;這里有幾個參數(shù)需要特別注意。remote_va 必須 8 字節(jié)對齊否則硬件會返回 local protection error。remote_rkey 必須是對端 MR 的 rkey且對端 MR 的 access flags 必須包含 IBV_ACCESS_REMOTE_ATOMIC。add_value 是你想加到目標(biāo)內(nèi)存上的值硬件會先讀取目標(biāo)內(nèi)存的舊值加上 add_value寫回新值然后把舊值通過 Atomic ACK 返回給你。返回的舊值會出現(xiàn)在 CQE 的 64 位 immediate 字段里或者通過 recv queue 的 WQE 返回具體取決于 QP 的配置。PRM 第 4 版在 Atomic 章節(jié)的末尾有一張表列出了不同 QP 類型下 Atomic 響應(yīng)的返回路徑建議對照確認(rèn)。3.3 輪詢 CQ 并解析 Atomic 完成事件提交 WQE 之后需要輪詢 Completion Queue 來確認(rèn)操作完成。Atomic 操作的 CQE 和普通 RDMA Write 的 CQE 在 opcode 字段上有區(qū)別。PRM 第 4 版規(guī)定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 的原始字節(jié)就要按 PRM 第 4 版的編碼來解析。// 直接讀 CQE 原始數(shù)據(jù)的方式 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 失敗的關(guān)鍵。PRM 第 4 版在 Completion Queue 章節(jié)里有一張 syndrome 編碼表常見的錯誤包括0x01 表示 local protection error通常是 rkey 無效或 VA 未對齊0x02 表示 remote protection error對端 MR 權(quán)限不足0x04 表示 remote access error對端 VA 無效。如果你看到 syndrome 是 0x01先檢查 remote_va 是否 8 字節(jié)對齊再檢查 rkey 是否過期。rkey 在 QP 狀態(tài)遷移或者 MR 銷毀后會失效如果對端重新注冊了 MR你手里的 rkey 就作廢了。4. 避坑與排查Atomic WQE 和 CQE 的五個血淚教訓(xùn)4.1 現(xiàn)象post_atomic 返回成功但 CQ 永遠(yuǎn)收不到完成事件原因doorbell 寫入了錯誤的 WQE index或者 doorbell 地址不對。PRM 第 4 版里 doorbell 的格式是 bit 8-31 放 WQE index但有些驅(qū)動版本要求 bit 0-7 也參與編碼。如果你用 DV 接口的 dbrec驅(qū)動會幫你處理但如果你自己 mmap UAR 頁偏移算錯就會導(dǎo)致硬件不取 WQE。解決先用 ibv_poll_cq 輪詢?nèi)绻瑫r檢查 dbrec 的值是否和 wqe_idx 匹配。用 mlx5dv_qp_get 拿到的 dbrec 是經(jīng)過驅(qū)動驗證的優(yōu)先用這個。如果必須自己算 UAR 偏移參考 PRM 第 4 版 UAR 章節(jié)的公式但不同固件版本可能有差異建議用已知能工作的 QP 做對照。4.2 現(xiàn)象Atomic 操作返回 syndrome 0x01local protection error原因remote_va 沒有 8 字節(jié)對齊或者 rkey 無效。PRM 第 4 版明確規(guī)定 Atomic 操作的 VA 必須 8 字節(jié)對齊長度固定 8 字節(jié)。很多人從對端拿到的 VA 是 malloc 返回的地址不保證 8 字節(jié)對齊。另外 rkey 在 MR 銷毀后失效如果對端重新注冊了 MR舊 rkey 就不能用了。解決對端注冊 MR 時用 posix_memalign 保證 8 字節(jié)對齊。rkey 通過 RDMA CM 或者帶外通道交換每次對端重新注冊 MR 后都要更新 rkey。調(diào)試時可以在對端用 ibv_query_mr 確認(rèn) rkey 和 access flags。4.3 現(xiàn)象Atomic Fetch and Add 執(zhí)行后目標(biāo)內(nèi)存的值不對原因add_value 的字節(jié)序搞反了。PRM 第 4 版里 Data Segment 的 8 字節(jié)是主機(jī)字節(jié)序但如果你在 x86 和 ARM 之間做跨平臺測試字節(jié)序差異會導(dǎo)致加出來的值完全錯誤。另外如果目標(biāo)內(nèi)存之前被其他操作修改過你讀到的舊值可能不是預(yù)期的。解決確認(rèn)兩端都是小端或者都是大端??缙脚_時用 htole64 或者 le64toh 轉(zhuǎn)換。調(diào)試時先在對端用普通 RDMA Read 讀一下目標(biāo)內(nèi)存的當(dāng)前值再發(fā) Atomic對比結(jié)果。4.4 現(xiàn)象CQE 的 opcode 顯示為 0x0B 但 immediate 字段里的舊值是 0原因Atomic 操作的響應(yīng)路徑配置錯了。PRM 第 4 版里Atomic 的舊值可以通過 CQE 的 immediate 字段返回也可以通過 recv queue 的 WQE 返回取決于 QP 的 create 參數(shù)。如果你在創(chuàng)建 QP 時沒有設(shè)置 IBV_QP_EX_WITH_ATOMIC_FETCH_ADD或者 recv queue 沒有 post 足夠的 recv WQE舊值可能被丟棄。解決創(chuàng)建 QP 時在 send_ops_flags 里加上 IBV_QP_EX_WITH_ATOMIC_FETCH_ADD。如果希望舊值通過 recv queue 返回確保 recv queue 里有足夠的 recv WQE并且 recv WQE 的 sg_list 指向有效的內(nèi)存。用 ibv_poll_cq 時檢查 wc.opcode 是否為 IBV_WC_FETCH_ADD如果是wc.imm_data 里就是舊值。4.5 現(xiàn)象Atomic Compare and Swap 總是失敗syndrome 0x02原因?qū)Χ?MR 的 access flags 沒有包含 IBV_ACCESS_REMOTE_ATOMIC。PRM 第 4 版在 Memory Registration 章節(jié)里明確列出Atomic 操作要求 MR 同時具有 REMOTE_WRITE 和 REMOTE_ATOMIC 權(quán)限。很多人只給了 REMOTE_WRITE結(jié)果 Compare and Swap 的 compare 階段就失敗了。解決對端注冊 MR 時access flags 設(shè)為 IBV_ACCESS_LOCAL_WRITE | IBV_ACCESS_REMOTE_WRITE | IBV_ACCESS_REMOTE_READ | IBV_ACCESS_REMOTE_ATOMIC。如果對端是 GPU 內(nèi)存還要確認(rèn) GPU 驅(qū)動是否支持 Atomic 操作有些老款 GPU 的 BAR 空間不支持 PCIe Atomic需要走系統(tǒng)內(nèi)存中轉(zhuǎn)。5. 用 PRM 第 4 版的 Atomic 語義做無鎖隊列驗證PRM 第 4 版里 Atomic Operations 的語義定義最直接的落地場景就是做跨節(jié)點(diǎn)的無鎖隊列。我一般會用一個簡單的 Fetch and Add 來分配隊列槽位再用 Compare and Swap 來更新隊列頭尾指針。驗證的時候不要一上來就寫完整隊列先寫一個最小測試兩個節(jié)點(diǎn)一個節(jié)點(diǎn)往固定地址做 Fetch and Add另一個節(jié)點(diǎn)輪詢這個地址的值看是否單調(diào)遞增。這個測試能跑通說明 Atomic 的 WQE 構(gòu)造、doorbell 通知、CQE 解析、rkey 交換這一整條鏈路都是通的。下面這個表格是我在調(diào)試 Atomic 操作時常用的參數(shù)對照基于 PRM 第 4 版的定義整理實際使用時建議用 ibv_query_device 確認(rèn)硬件支持的能力。參數(shù)典型值PRM 第 4 版對應(yīng)章節(jié)備注Atomic opcode0x0A / 0x0B / 0x0CAtomic Operations0x0ACAS, 0x0BFAA, 0x0CMasked CASVA 對齊8 字節(jié)Atomic Operations不滿足會返回 syndrome 0x01操作長度8 字節(jié)Atomic Operations固定值不可變MR access flagREMOTE_ATOMICMemory Registration必須同時有 REMOTE_WRITECQE opcode0x0A / 0x0BCompletion Queue與 WQE opcode 對應(yīng)syndrome 0x01local protectionCompletion QueueVA 未對齊或 rkey 無效syndrome 0x02remote protectionCompletion Queue對端 MR 權(quán)限不足驗證無鎖隊列時還有一個容易忽略的點(diǎn)PRM 第 4 版里 Atomic 操作的響應(yīng)是保序的但不同 QP 之間的 Atomic 操作沒有順序保證。如果你用多個 QP 做 Fetch and Add拿到的舊值順序可能和提交順序不一致。要保證嚴(yán)格順序所有 Atomic 操作必須走同一個 QP。這個結(jié)論在 PRM 第 4 版的 Ordering 章節(jié)里有明確說明但很多人第一次看會漏掉。我自己的習(xí)慣是每次改完 WQE 構(gòu)造代碼先用一個固定的 remote_va 和 add_value 跑 1000 次 Fetch and Add然后在對端用 RDMA Read 讀回目標(biāo)內(nèi)存確認(rèn)值等于初始值加上 1000 倍的 add_value。如果不對就抓 CQE 的 syndrome 和 immediate 字段對照 PRM 第 4 版的編碼表逐項排查。這個笨辦法幫我省了很多抓包的時間。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取