简介:本资源为 InfiniBand 架构规范第 2 卷《物理规范》正式版文档,面向从事高性能计算、服务器与存储互连、RDMA 网络开发的工程师及协议研究者,用于解决高速链路电气接口设计与信号完整性验证中的标准依据问题。压缩包内仅含 1 个 PDF 文件,约 15.14MB,内容聚焦高速电气接口章节,涵盖 QDR 与 FDR 速率下主机驱动器输出特性、眼图掩模参数 X、Y1、Y2 的定义、确定性抖动与总抖动限值、单位间隔容差,以及三抽头 FIR 均衡器预游标与后游标权重调整、16 组预设均衡设置和 AMP 幅度配置等关键指标,并引用 IEEE 802.3、OIF-CEI 与 ANSI T11 FC-PI-5 相关规范。文档还给出 S 参数与测试点 TP6 的测量约定,便于读者对照表格与图示完成链路预算、均衡调优和合规性测试。目前已有 91 人学习,适合需要权威物理层参数支撑设计或排错的技术人员查阅。
1. InfiniBand 规范卷 2 第 3 分册到底管什么:从链路层到包格式的落地地图
如果你正在做 RDMA 网卡驱动、HCA 固件、或者高性能计算集群的底层调优,迟早会撞上 InfiniBand 规范文档。这套规范分两卷,卷 1 讲架构总览和软件接口,卷 2 才是真正管硬件行为的核心——链路层协议、包格式、流控、QoS、连接管理。而卷 2 又拆成多个分册,第 3 分册聚焦的是链路层包格式与传输机制,也就是一个 IB 包从发出到被正确解析,中间每个 bit 该长什么样、状态机该怎么跳。2025 年 7 月 31 日发布的 Release 2.0 Final 版本,意味着这一版已经冻结,后续实现者可以拿它当基准做一致性验证。这篇文章不讲泛泛的 IB 科普,而是把卷 2 第 3 分册里最影响落地的那几块——包格式、流控信用、VL 仲裁、连接状态机——拆成能对着寄存器手册和抓包工具复现的步骤。适合已经上手过 RDMA 编程、但被底层行为卡住的工程师,也适合刚拿到 HCA 手册、想搞清楚每个字段为什么这么设的固件开发者。
2. 链路层包格式拆解:从 LRH 到 ICRC 的逐字段落地
2.1 为什么先啃包格式:一个抓包案例引出的字段依赖
很多人调 RDMA 性能时习惯先看perfquery的计数器,发现port_rcv_errors涨了却不知道从哪查。我一般会先抓一段链路层包,用分析仪或者 HCA 自带的诊断模式 dump 原始字节,然后对着卷 2 第 3 分册的包格式图逐字段比对。IB 链路层的包不是以太网那种固定偏移,它由本地路由头 LRH、全局路由头 GRH(可选)、基传输头 BTH、扩展传输头 ETH(可选)、载荷和 ICRC 组成。每个头的长度和字段位置在规范里写得很死,但实际实现时最容易翻车的是 VL 字段和 LNH 字段的配合——VL 决定走哪个虚拟通道,LNH 决定后面跟的是 GRH 还是直接 BTH。如果 LNH 设错,接收端会把 GRH 当 BTH 解析,直接丢包并报local_length_error。所以第一步不是写代码,而是把包格式的字段依赖关系画成一张表,贴在工位上。
2.2 用 Python 构造一个最小合法 IB 包并验证字段
下面这段代码用 Python 的struct模块手工拼一个不带 GRH 的 IB 包,重点是把 LRH 和 BTH 的关键字段按规范填对。你可以把它跑在任意 Linux 机器上,输出十六进制字节流,再和抓包结果对比。
import struct def build_ib_packet(sl, vl, dlid, slid, opcode, pkey, dest_qp, psn): # LRH: 8 bytes # VL(4b) | reserved(4b) | LVer(4b) | SL(4b) | reserved(2b) | LNH(2b) vl_lver = (vl << 4) | 0x0 # LVer=0 for IB sl_res_lnh = (sl << 4) | (0x0 << 2) | 0x2 # LNH=2 means BTH follows, no GRH lrh = struct.pack('>BBHHH', vl_lver, sl_res_lnh, dlid, (0x0 << 12) | (slid & 0xFFFF), # reserved + SLID 0x0000) # reserved # BTH: 12 bytes # Opcode(8b) | SE(1b) | M(1b) | PadCnt(2b) | Tver(4b) | PKey(16b) | DestQP(24b) | A(1b) | PSN(24b) bth = struct.pack('>BBHII', opcode, (0x0 << 7) | (0x0 << 6) | (0x0 << 4) | 0x0, # SE=0, M=0, PadCnt=0, Tver=0 pkey, (dest_qp << 8) | (0x0 << 7) | ((psn >> 16) & 0xFF), (psn & 0xFFFF) << 16 | 0x0000) # 简化处理,实际PSN占24位 # 载荷:4字节对齐的测试数据 payload = b'\xde\xad\xbe\xef' # ICRC:实际需要按规范计算,这里填0占位 icrc = b'\x00\x00\x00\x00' return lrh + bth + payload + icrc pkt = build_ib_packet(sl=0, vl=3, dlid=0x0001, slid=0x0002, opcode=0x0A, pkey=0xFFFF, dest_qp=0x000018, psn=0x123456) print(pkt.hex())这段代码的逻辑说明:LRH 的第一个字节把 VL 和 LVer 打包,第二个字节把 SL、保留位和 LNH 打包。LNH 设为 2 表示后面直接跟 BTH,没有 GRH。BTH 里 opcode 用 0x0A 代表 SEND 请求的第一个包,PKey 用 0xFFFF 表示默认分区。参数上最容易错的是 PSN 的 24 位对齐——规范里 PSN 跨两个 32 位字,低 8 位在高字的高字节,剩下 16 位在低字的高 16 位。如果你用现成的 scapy 或者 dpkt 库,它们对 IB 的支持不完整,还是手工拼最可控。拼完之后用xxd或者 Wireshark 的“Import Hex Dump”功能导入,看解析出来的字段和你的预期是否一致。这一步过了,再谈流控和状态机才有意义。
2.3 参数速查:LRH 和 BTH 里最容易被固件写错的五个字段
| 字段 | 位置 | 常见错误值 | 正确做法 |
|---|---|---|---|
| VL | LRH byte0 高4位 | 固定填0 | 按 SL 到 VL 映射表查表,通常 SL=0 映射 VL=0 |
| LNH | LRH byte1 低2位 | 填1(以为有GRH) | 无GRH时填2,有GRH时填3 |
| PKey | BTH byte4-5 | 填0x0000 | 默认分区用0xFFFF,非默认按SM配置 |
| DestQP | BTH byte8-10 | 只填低16位 | 24位必须填满,高8位在byte8 |
| PSN | BTH byte11-14 | 当成32位整数 | 24位循环,回绕时按规范处理 |
这张表是我在调试 HCA 固件时血泪总结出来的。尤其是 LNH 字段,很多开源驱动示例里直接写死 0x2,但一旦启用 GRH 就会解析错位。DestQP 的 24 位对齐也是经典坑,因为大多数网络协议 QP 号不会超过 16 位,容易忽略高 8 位。PSN 的回绕行为在规范里有明确的状态机描述,如果固件里用简单的 32 位递增,跑到 0xFFFFFF 之后会跳到 0x1000000,而规范要求回绕到 0。这个错误在长时间压测时才会暴露,表现为突然大量丢包。
3. 流控信用与 VL 仲裁:让链路跑满不丢包的配置逻辑
3.1 信用机制的本质:接收端缓冲区如何反向控制发送端
IB 链路层的流控不是靠丢包重传,而是靠信用。每个 VL 独立维护一套信用计数器,接收端在初始化时通过交换FlowControl包告诉发送端“我这边每个 VL 有多少个 64 字节单元的缓冲”。发送端每发一个包,对应 VL 的信用减一;接收端处理完一个包,回一个信用包,发送端信用加一。信用减到零,发送端必须停发该 VL 的包。这套机制的好处是零丢包,坏处是如果信用配置太小,链路带宽利用率上不去;配置太大,接收端缓冲区溢出直接翻车。卷 2 第 3 分册里详细定义了信用包的类型(FlowControl的 opcode 是 0x0B)和信用更新的粒度。实际调优时,我一般先看 HCA 手册里每个 VL 的接收缓冲深度,然后按信用数 = 缓冲深度 / 64算一个保守值,再逐步加大到链路跑满且不报port_rcv_remote_physical_errors。
3.2 用 ibdiagnet 和 perfquery 验证信用配置是否生效
光看规范不够,得用工具验证。下面这组命令是我在集群上线前必跑的流程,用来确认信用配置和 VL 仲裁是否按预期工作。
# 1. 查看端口基础状态,确认链路已激活 ibstat mlx5_0 # 2. 用 perfquery 读取端口计数器,重点看 VL15 丢包和信用相关错误 perfquery -x -C mlx5_0 1 # 3. 用 ibdiagnet 做全链路诊断,生成报告 ibdiagnet -o /tmp/ibdiag_out -d 3 -v 3 # 4. 从报告里提取信用和流控相关告警 grep -i "flow_control\|credit\|vl15" /tmp/ibdiag_out/ibdiagnet.log逻辑说明:ibstat确认物理层和链路层已经 up,如果这里显示State: Down,后面的信用配置无从谈起。perfquery的-x参数输出扩展计数器,-C指定 CA 名称,最后的1是端口号。重点看port_xmit_discards和port_rcv_errors,如果这两个在压测时增长,说明信用或 VL 配置有问题。ibdiagnet是 Mellanox 工具集里的诊断利器,-d 3表示诊断深度,-v 3是冗余度。跑完之后在日志里搜flow_control,如果出现VL15 credit exhausted之类的告警,说明管理包占用了太多信用,需要调整 VL15 的信用分配。参数上,-o指定输出目录,-d和-v的范围通常是 1 到 5,生产环境用 3 足够。
3.3 VL 仲裁表怎么配:从 SL 到 VL 的映射与带宽分配
VL 仲裁是 IB 里最像“玄学”的部分。规范定义了两种仲裁方式:基于优先级的仲裁和基于权重的轮询。实际 HCA 实现里,通常用一张 SL 到 VL 的映射表,加上每个 VL 的权重寄存器。配置的时候,先确定业务流的 SL 值——比如 MPI 集合通信通常用 SL 0,存储流量用 SL 1,管理流量用 SL 15。然后查 HCA 手册里的映射表寄存器地址,把 SL 映射到不同的 VL。权重方面,如果两个 VL 共享同一个物理链路,权重比就是带宽比。比如 VL0 权重 4,VL1 权重 1,那么 VL0 能拿到 80% 的带宽。这里有个坑:VL15 是保留给管理包的,不能映射业务流量,而且它的信用是独立的。如果你把业务 SL 映射到 VL15,管理包会被饿死,链路直接进入VL15 credit exhausted状态,表现为ibstat显示链路 up 但所有业务流量超时。我一般会在配置完之后用iblinkinfo确认每个端口的 VL 能力,再用ibv_rc_pingpong打流测试不同 SL 的延迟和带宽。
4. 连接状态机与 QP 转换:从 RESET 到 RTS 的每一步验证
4.1 QP 状态机的六个状态和四个转换条件
IB 的 Queue Pair 不是建好就能发数据,它必须经过状态机转换。卷 2 第 3 分册里定义了 QP 的六个状态:RESET、INIT、RTR、RTS、SQD、SQE。实际使用中,最核心的是 RESET → INIT → RTR → RTS 这条路径。每一步转换都有严格的参数要求:RESET 到 INIT 需要设置 PKey 和端口号;INIT 到 RTR 需要设置远端 QP 号和远端 LID;RTR 到 RTS 需要设置发送 PSN 和重传超时。任何一步参数不对,转换就会失败,返回IBV_WC_REM_ACCESS_ERR或者更模糊的IBV_WC_GENERAL_ERR。我见过太多人卡在 RTR 到 RTS,原因是远端 QP 号填错或者 PSN 没对齐。规范里明确写了,RTR 状态下 QP 只能接收,不能发送;RTS 才能发。所以如果你在 RTR 就调ibv_post_send,返回的错误是IBV_WC_LOC_QP_OP_ERR,而不是超时。
4.2 用 rdma_cm 和 libibverbs 写一个状态转换验证程序
下面这段 C 代码用 libibverbs 手工做 QP 状态转换,每一步都打印返回值,方便定位卡在哪。
#include <infiniband/verbs.h> #include <stdio.h> #include <string.h> int modify_qp_to_init(struct ibv_qp *qp, uint8_t port, uint16_t pkey) { struct ibv_qp_attr attr; memset(&attr, 0, sizeof(attr)); attr.qp_state = IBV_QPS_INIT; attr.port_num = port; attr.pkey_index = 0; attr.qp_access_flags = IBV_ACCESS_LOCAL_WRITE | IBV_ACCESS_REMOTE_READ; int ret = ibv_modify_qp(qp, &attr, IBV_QP_STATE | IBV_QP_PKEY_INDEX | IBV_QP_PORT | IBV_QP_ACCESS_FLAGS); printf("INIT ret=%d\n", ret); return ret; } int modify_qp_to_rtr(struct ibv_qp *qp, uint32_t dest_qp, uint16_t dlid, uint8_t sgid_idx) { struct ibv_qp_attr attr; memset(&attr, 0, sizeof(attr)); attr.qp_state = IBV_QPS_RTR; attr.path_mtu = IBV_MTU_1024; attr.dest_qp_num = dest_qp; attr.rq_psn = 0; attr.max_dest_rd_atomic = 1; attr.min_rnr_timer = 12; attr.ah_attr.is_global = 0; attr.ah_attr.dlid = dlid; attr.ah_attr.sl = 0; attr.ah_attr.src_path_bits = 0; attr.ah_attr.port_num = 1; int ret = ibv_modify_qp(qp, &attr, IBV_QP_STATE | IBV_QP_AV | IBV_QP_PATH_MTU | IBV_QP_DEST_QPN | IBV_QP_RQ_PSN | IBV_QP_MAX_DEST_RD_ATOMIC | IBV_QP_MIN_RNR_TIMER); printf("RTR ret=%d\n", ret); return ret; } int modify_qp_to_rts(struct ibv_qp *qp, uint32_t sq_psn, uint8_t timeout, uint8_t retry_cnt) { struct ibv_qp_attr attr; memset(&attr, 0, sizeof(attr)); attr.qp_state = IBV_QPS_RTS; attr.timeout = timeout; attr.retry_cnt = retry_cnt; attr.rnr_retry = 7; attr.sq_psn = sq_psn; attr.max_rd_atomic = 1; int ret = ibv_modify_qp(qp, &attr, IBV_QP_STATE | IBV_QP_TIMEOUT | IBV_QP_RETRY_CNT | IBV_QP_RNR_RETRY | IBV_QP_SQ_PSN | IBV_QP_MAX_QP_RD_ATOMIC); printf("RTS ret=%d\n", ret); return ret; }逻辑说明:modify_qp_to_init设置端口和 PKey 索引,访问权限按需加。modify_qp_to_rtr里path_mtu要和远端一致,dest_qp_num是远端 QP 号,rq_psn是接收 PSN 起点,min_rnr_timer是接收端没缓冲时发送端等多久再重试。modify_qp_to_rts里timeout是重传超时指数,retry_cnt是重传次数,rnr_retry是 RNR 重试次数,sq_psn是发送 PSN 起点。参数上,timeout一般设 14 到 18,对应大约 1 秒到 4 秒;retry_cnt设 7 表示重试 7 次;rnr_retry设 7 表示无限重试。每一步的返回值必须为 0,如果返回 -1,用errno看具体错误。常见错误是EINVAL,说明某个属性没设或者值超出范围。这个程序跑通之后,再上ibv_post_send发数据,成功率会高很多。
4.3 状态转换失败的排查顺序:从本地 QP 到远端 ACK
状态转换失败时,不要瞎猜。按这个顺序查:第一,本地 QP 的属性是否完整,特别是IBV_QP_AV里的dlid和sl;第二,远端 QP 是否已经处于 RTR 或 RTS,如果远端还在 INIT,你转 RTR 会超时;第三,PKey 是否匹配,两端 PKey 不一致会导致 RTR 失败;第四,MTU 是否一致,一端 1024 一端 4096 会在 RTS 后第一个包就丢;第五,PSN 是否在窗口内,如果发送 PSN 和接收 PSN 差太远,接收端会静默丢弃。排查工具上,ibv_rc_pingpong是最小验证集,如果它跑不通,你的代码肯定有问题。另外,dmesg里会有 HCA 驱动的详细报错,比如mlx5_0: QP 0x18 state transition failed,后面跟原因码。原因码在卷 2 第 3 分册的附录里有完整列表,对着查就行。
5. 避坑与常见问题:链路层调试中那些让你加班到凌晨的坑
5.1 坑一:链路显示 ACTIVE 但所有业务流量超时
现象:ibstat显示State: Active,Physical state: LinkUp,但ibv_rc_pingpong超时,perfquery里port_xmit_wait持续增长。原因:VL15 信用耗尽。管理包(SMP)走 VL15,如果 VL15 的信用被大量管理查询占满,业务包虽然走其他 VL,但 QP 状态转换需要管理包交换,管理包发不出去,转换就卡住。解决:用ibdiagnet查 VL15 信用计数,如果接近零,减少管理查询频率,或者调整 HCA 的 VL15 信用寄存器(需要厂商工具)。另一个原因是 SL 到 VL 映射把业务流量映射到了 VL15,改映射表即可。
5.2 坑二:大包传输时随机丢包,小包正常
现象:MTU 设 4096 时,ib_send_bw跑几秒就报port_rcv_errors,换成 1024 就正常。原因:接收端缓冲区按 64 字节单元分配信用,MTU 4096 的一个包消耗 64 个信用,如果信用总数不够,发送端在发到一半时信用耗尽,但接收端还没处理完,导致超时重传,重传又消耗信用,恶性循环。解决:查 HCA 手册里每个 VL 的接收缓冲总深度,按信用数 = 深度 / 64算,确保信用数大于 MTU/64 的几倍。一般建议信用数至少是最大包消耗的 4 倍。
5.3 坑三:QP 转换到 RTS 成功,但第一个 SEND 就报 REM_ACCESS_ERR
现象:ibv_modify_qp返回 0,但ibv_post_send的完成队列里报IBV_WC_REM_ACCESS_ERR。原因:远端 QP 的访问权限没开。RTR 转换时,远端的qp_access_flags必须包含IBV_ACCESS_REMOTE_WRITE或IBV_ACCESS_REMOTE_READ,否则你的 SEND 到了远端,远端检查权限不通过,回一个 NAK。解决:在远端 QP 转 RTR 时,把qp_access_flags设全,至少包含IBV_ACCESS_LOCAL_WRITE | IBV_ACCESS_REMOTE_WRITE | IBV_ACCESS_REMOTE_READ。注意,IBV_ACCESS_REMOTE_ATOMIC如果不需要就别开,开了会增加安全风险。
5.4 坑四:PSN 回绕导致长时间压测后突然丢包
现象:压测跑 30 分钟后,port_rcv_errors突然暴涨,之前一直正常。原因:PSN 是 24 位,从 0xFFFFFF 回绕到 0 时,如果发送端和接收端的回绕处理不一致,接收端会认为 PSN 乱序,丢弃所有包。规范里要求回绕时按模 2^24 计算,但有些固件实现用 32 位比较,导致回绕后差值巨大。解决:在固件或驱动里显式处理回绕,用(psn - expected_psn) & 0xFFFFFF来判断顺序,而不是直接比较大小。测试时可以用脚本把 PSN 初始值设到 0xFFFF00,跑几分钟就能触发回绕。
5.5 坑五:ibdiagnet 报告正常但应用层延迟抖动大
现象:ibdiagnet所有检查项通过,但 MPI 程序延迟抖动超过 100 微秒。原因:VL 仲裁权重配置不合理,高优先级 VL 饿死低优先级 VL,导致低优先级流量突发时延迟飙升。解决:用perfquery看每个 VL 的发送字节数,如果某个 VL 的字节数远低于权重比例,说明仲裁器没按预期工作。调整权重寄存器,让权重比接近实际流量比。另外,检查 SL 到 VL 映射是否把延迟敏感的流量映射到了高优先级 VL。
6. 进阶技巧:用规范附录的伪代码验证你的状态机实现
卷 2 第 3 分册的附录里有一组状态机伪代码,很多人直接跳过,觉得是“参考实现”。我一开始也这么想,直到有一次 QP 转换在特定时序下失败,查了三天才发现是状态机里一个“先检查后执行”的顺序问题。规范伪代码里明确写了,在 RTR 到 RTS 转换时,必须先更新发送 PSN,再使能发送队列;如果反过来,第一个包的 PSN 会用旧值,导致接收端认为乱序。这个细节在正文里只是一句话,但在附录伪代码里是显式的两步。所以我的习惯是:任何状态机实现,先照着附录伪代码逐行翻译成 Python 或者 C 的断言,跑一遍单元测试,再移植到固件。下面这个表格是我整理的附录里最容易被忽略的三个时序点。
| 转换 | 规范要求顺序 | 常见错误顺序 | 后果 |
|---|---|---|---|
| INIT→RTR | 先设 RQ PSN,再设 dest_qp | 先设 dest_qp | RQ PSN 用默认值,接收窗口错位 |
| RTR→RTS | 先更新 SQ PSN,再使能 SQ | 先使能 SQ | 第一个包 PSN 旧值,远端丢包 |
| RTS→SQD | 先排空 SQ,再改状态 | 直接改状态 | 未完成的 WQE 丢失,完成队列无事件 |
验证方法上,我一般会写一个 Python 脚本,把附录伪代码里的每个状态和转换条件抽成字典,然后用随机事件序列跑 10000 次,看是否出现死锁或者非法状态。这个脚本不需要连硬件,纯逻辑验证,但能抓住 80% 的状态机实现错误。剩下的 20% 是硬件时序问题,只能上逻辑分析仪或者 HCA 的诊断模式抓波形。最后说一个我自己的教训:不要相信“这个版本和上一版差不多”这种话。Release 2.0 Final 和之前的草案之间,光流控信用包的格式就改了两次,如果你拿旧代码直接跑,会在信用更新时解析错位,表现为链路时断时续。每次升级规范版本,先把附录的变更列表过一遍,再动代码。希望帮到你。
本文还有配套的精品资源,点击获取