The verbs object model: device, PD, QP, CQ, MR
S1·E1The card that says InfiniBand on an Ethernet cable · Dell lab, Round Rock, day two of a five-day PoC
Before you read: what do you already know?
3 quick questions. Wrong answers are fine and expected; trying first makes the lesson stick.
After this lesson you can
- Name every object an RDMA application creates and state which other object owns it.
- Read an `ibv_devinfo` port block on a RoCE port and explain why it reports `transport: InfiniBand (0)` with `link_layer: Ethernet` and `port_lid: 0`.
- Distinguish `lkey` from `rkey` and state the access-flag rule that governs one-sided writes.
- Predict which destroy calls fail while another object still references them.
Episode 1 — The card that says InfiniBand on an Ethernet cable
Sixty-four PowerEdge nodes, five days of lab time, and a medical-imaging archive that must leave its TCP storage tier before the customer’s quarter-end freeze. The Dell SE has a reheated coffee he never drinks and a spreadsheet of every promise he has made here. The network lead flew in with a thin notebook and no intention of believing anything twice.
The first command anyone runs is ibv_devinfo, and the port block comes back with transport: InfiniBand (0), link_layer: Ethernet, sm_lid: 0 and port_lid: 0. The network lead stops the meeting: he bought Ethernet switches, the card claims InfiniBand, and he wants to know what shipped.
Nothing is wrong. RoCE keeps the InfiniBand transport and swaps only the link, which is why transport reads InfiniBand while link_layer reports the medium.[2] The zeros are the same fact from another angle: the LID is a layer-2 attribute of the InfiniBand stack, displayed as zero on a RoCE port, because a RoCE fabric does not require a Subnet Manager at all.[2]
That is why the object model looks like this. Applications wanted the InfiniBand transport on Ethernet the customer already owns, so the verbs objects were left untouched: a device context, a protection domain inside it, queue pairs reporting into completion queues, and memory regions that return an lkey for local use and an rkey to hand the peer.[6][3]
Read link_layer for the cable, transport for the semantics: on a RoCE port they are supposed to disagree.
Ten minutes at the whiteboard before the storage test starts. It starts with the device.
1The device and its port, read two ways
Everything starts with a device context. ibv_devinfo is the tool that prints what the driver knows about it, with the synopsis ibv_devinfo [-d device] [-i port] [-l] [-v]; -d/--ib-dev=DEVICE selects one device, -i/--ib-port=PORT one port, -l/--list prints only the device names, and -v/--verbose prints “all available information about RDMA devices”.[1] On a RoCE port the block looks like this.[2]
hca_id: mlx5_0
transport: InfiniBand (0)
fw_ver: 16.28.0578
node_guid: ...
vendor_id: 0x02c9
vendor_part_id: 4121
phys_port_cnt: 1
port: 1
state: PORT_ACTIVE (4)
max_mtu: 4096 (5)
active_mtu: 1024 (3)
sm_lid: 0
port_lid: 0
port_lmc: 0x00
link_layer: EthernetTwo lines in that block cause most of the confusion in a first RoCE ticket. transport: InfiniBand (0) on an Ethernet port is normal — RoCE keeps the InfiniBand transport and only swaps the link; link_layer is the field that reports the medium.[2] And sm_lid/port_lid are zero because “LID is a layer 2 attribute of the InfiniBand protocol stack, it is not set for a port and is displayed as zero when querying the port”.[2] That follows from a bigger fact: for RoCE “the presence of a Subnet Manager (SM) is not required in the fabric”, which also means path queries are impossible and the path record must be filled in by hand or by RDMA-CM.[2]
The same facts are readable without the tool: cat /sys/class/infiniband/mlx5_0/ports/1/state returns ACTIVE and cat /sys/class/infiniband/mlx5_0/ports/1/link_layer returns Ethernet, while the firmware version sits one level up, at cat /sys/class/infiniband/mlx5_0/fw_ver.[2] Keep both routes in your hands — on a customer box where mlnx-tools is missing, sysfs still answers.
2Protection domain: the scope that makes a key mean something
A protection domain is not an object that does work — it is the boundary that makes the other objects safe. MRs, QPs, SRQs and address handles are all allocated inside a PD, and a key registered in one PD is meaningless to a QP in another.[6][3] The API is blunt about this in one place people trip over: ibv_advise_mr() takes the PD as its first argument, not the MR, because the advice applies to a scatter-gather list of keys that all live in that domain.[7]
Above the PD sits the extended device query, ibv_query_device_ex(), whose struct ibv_device_attr_ex carries orig_attr, comp_mask, odp_caps, completion_timestamp_mask, hca_core_clock, device_cap_flags_ex, tso_caps, rss_caps, max_wq_type_rq, packet_pacing_caps, raw_packet_caps, tm_caps, cq_mod_caps, max_dm_size, atomic_caps, xrc_odp_caps and phys_port_cnt_ex.[8] That structure is where you check whether a feature a customer is asking for exists on their firmware at all, rather than arguing about it.
Click a box (or Tab to it and press Enter) for its create call, its failure modes and the completion statuses it produces.
Protection domain
ibv_alloc_pd(context)
no create-time attributes — the PD is a scope, not a resource
- A PD scopes MRs, QPs, SRQs and AHs together: objects from different PDs cannot be used with each other.
- ibv_advise_mr() takes the PD as its first argument, not the MR — the classic API surprise.
- Access checks on one-sided traffic are PD-scoped: the rkey the peer sends must belong to an MR in the QP's PD.
- Using an MR's lkey on a QP from another PD → IBV_WC_LOC_PROT_ERR on the local side.
- IBV_WC_LOC_PROT_ERR (4) — local key/PD mismatch
- IBV_WC_REM_ACCESS_ERR (10) — rkey/access-flags problem on the peer
Sources: ibv_devinfo(1) · ibv_create_qp(3) · ibv_create_cq_ex(3) · ibv_create_srq(3) · ibv_reg_mr(3) · ibv_poll_cq(3) · ibv_query_device_ex(3) · NVIDIA Optimized Memory Access · perftest
3QP and CQ: two queues, one completion ring
A queue pair is a send queue plus a receive queue. It is created from struct ibv_qp_init_attr, which is qp_context, send_cq, recv_cq, srq, a cap block, qp_type and sq_sig_all.[3] The cap block is max_send_wr, max_recv_wr, max_send_sge, max_recv_sge and max_inline_data.[3] Upstream verbs offers IBV_QPT_RC, IBV_QPT_UC, IBV_QPT_UD, IBV_QPT_RAW_PACKET and the vendor-specific IBV_QPT_DRIVER; the next lesson is about what each of them can actually do.[3] perftest exposes the same choice as -c, --connection=<RC/UC/UD/XRC/DC/SRD> with a default of RC, which is the fastest way to demonstrate the matrix without writing code.[12]
sq_sig_all decides how noisy the CQ is: “If set, each Work Request (WR) submitted to the SQ generates a completion entry”; unset it and only WRs flagged IBV_SEND_SIGNALED complete.[3] Attach a shared receive queue and two rules bite: creation “fails if attempting a QP type other than RC or UD with SRQ attachment”, and max_recv_wr/max_recv_sge are ignored because the SRQ owns them.[3]
The completion queue has its own surprises. ibv_create_cq_ex() “may create a CQ with size greater than or equal to the requested size” — read cqe back from the returned CQ instead of assuming.[4] The channel may be NULL if you will not use completion events, and comp_vector “must be >= 0 and < context->num_comp_vectors”.[4] An SRQ is created from struct ibv_srq_init_attr holding max_wr, max_sge and srq_limit, where srq_limit “is irrelevant for ibv_create_srq” and is armed later with ibv_modify_srq; both SRQ and CQ may return larger values than requested.[5]
Teardown has dependencies you should be able to recite: ibv_destroy_srq() fails while any QP is still associated with it, and ibv_destroy_qp() fails if the QP is still in a multicast group.[5][3]
4Memory registration: lkey, rkey, and what registration costs
ibv_reg_mr(pd, addr, length, access) pins a range of memory and gives the HCA a translation for it; ibv_reg_mr_iova() is the same with an explicit HCA virtual address.[6] The access flags are IBV_ACCESS_LOCAL_WRITE, REMOTE_WRITE, REMOTE_READ, REMOTE_ATOMIC, FLUSH_GLOBAL, FLUSH_PERSISTENT, MW_BIND, ZERO_BASED, ON_DEMAND, HUGETLB and RELAXED_ORDERING.[6] Two rules matter more than the list: “If IBV_ACCESS_REMOTE_WRITE or IBV_ACCESS_REMOTE_ATOMIC is set, then IBV_ACCESS_LOCAL_WRITE must be set too”, and “Local read access is always enabled for the MR” — there is no IBV_ACCESS_LOCAL_READ.[6]
Registration returns two keys. The lkey goes in the local QP’s scatter-gather entries; the rkey is what you hand the peer so it can do one-sided reads, writes and atomics against that range.[6] ibv_dereg_mr() fails while a memory window is still bound to the MR.[6]
Registration cost is page-pinning cost, which is why on-demand paging exists. NVIDIA describes ODP as “a technique to alleviate much of the shortcomings of memory registration”: an explicit ODP MR “does not need to have valid mappings at registration time”, and an implicit one gives the application “a special memory key that represents their complete address space”.[10] The implicit recipe from the man page is IBV_ACCESS_ON_DEMAND with addr 0 and length SIZE_MAX.[6] ODP moves the cost to first touch, so ibv_advise_mr(pd, IBV_ADVISE_MR_ADVICE_PREFETCH, IBV_ADVISE_MR_FLAG_FLUSH, sg_list, num_sge) exists to pay it early; with IBV_ADVISE_MR_FLAG_FLUSH the call is synchronous and the pages are guaranteed present in the HCA on success, without it the call is best-effort.[7] NVIDIA notes one flat limitation: “ODP does not support contiguous pages.”[10]
Finally, keep the vocabulary aligned with the DOCA side of the house, because Dell customers will mix them in one sentence: DOCA RDMA supports InfiniBand and “Ethernet using RoCE”, and on the BlueField platform the doca_dev handed to DOCA RDMA must be a scalable function - with one exception, the DPA datapath, which currently supports PFs only.[11]
Click a box (or Tab to it and press Enter) for its create call, its failure modes and the completion statuses it produces.
Memory region
ibv_reg_mr(pd, addr, length, access) # or ibv_reg_mr_iova(pd, addr, length, hca_va, access)
access: IBV_ACCESS_LOCAL_WRITE · REMOTE_WRITE · REMOTE_READ · REMOTE_ATOMIC · MW_BIND · ZERO_BASED · ON_DEMAND · HUGETLB · RELAXED_ORDERING · FLUSH_GLOBAL · FLUSH_PERSISTENToutput: lkey (used locally in sg_list) and rkey (handed to the peer for one-sided ops)
- Hard rule: "If IBV_ACCESS_REMOTE_WRITE or IBV_ACCESS_REMOTE_ATOMIC is set, then IBV_ACCESS_LOCAL_WRITE must be set too."
- "Local read access is always enabled for the MR" — there is no IBV_ACCESS_LOCAL_READ.
- Implicit ODP: set IBV_ACCESS_ON_DEMAND, addr = 0, length = SIZE_MAX → one key for the whole address space.
- Prefetch with ibv_advise_mr(pd, IBV_ADVISE_MR_ADVICE_PREFETCH, IBV_ADVISE_MR_FLAG_FLUSH, sg_list, num_sge); without FLUSH it is best-effort.
- ibv_dereg_mr() "fails if any memory window is still bound to this MR" — unbind the MW first.
- NVIDIA states flatly: "ODP does not support contiguous pages."
- IBV_WC_LOC_PROT_ERR (4) — bad lkey or the local buffer is outside the MR
- IBV_WC_REM_ACCESS_ERR (10) — the peer's rkey is wrong or its MR lacks REMOTE_WRITE/REMOTE_READ
- IBV_WC_MW_BIND_ERR (6) — memory-window bind failed
Sources: ibv_devinfo(1) · ibv_create_qp(3) · ibv_create_cq_ex(3) · ibv_create_srq(3) · ibv_reg_mr(3) · ibv_poll_cq(3) · ibv_query_device_ex(3) · NVIDIA Optimized Memory Access · perftest
Ten minutes at the whiteboard
You put the port block on the screen and read it line by line: the transport really is the InfiniBand transport, link_layer is the only field that names the cable, and the zero LIDs are an artefact with no Subnet Manager behind them.[2] Then the same two values from sysfs, so nobody thinks the tool is being polite.[2] The network lead writes two words in the notebook and signs off the hardware. What you say: “The transport is InfiniBand on purpose. Only link_layer tells you what the cable is, and yours says Ethernet.” That night the storage architect circulates tomorrow’s requirement list. Line nine: a node that misses a value must be able to pull it from a peer — and the tier is drawn entirely on one-sided writes.
Lab
Read-only on the Dell-lab ConnectX or BlueField-3 host. No configuration changes, so no rollback is required.
- Pre-flight inventory:
ibv_devinfo -lto list device names,ibdev2netdevto map each device to its netdev, andofed_info -sfor the installed stack version. Record all three before touching anything. - Capture the full port block:
ibv_devinfo -v -d mlx5_0 > /tmp/devinfo-mlx5_0.txt. Expected:link_layer: Ethernet,state: PORT_ACTIVE (4),port_lid: 0. - Compare
max_mtuwithactive_mtuin that file and write both down. They are frequently different, and the difference is what perftest will use by default in module 3. - Repeat the sysfs cross-check:
cat /sys/class/infiniband/mlx5_0/ports/1/{state,link_layer}andcat /sys/class/infiniband/mlx5_0/fw_ver. Expected: values identical to the tool output. - Confirm the device-to-netdev mapping you will need in every later lab:
ibdev2netdevprints lines of the formmlx5_0 port 1 <===> eth2. If a device maps to no netdev, RoCE GIDs will not exist for it — that is the subject of lesson 4.
Soft-RoCE gives you a real verbs device on any Linux box, including two containerlab nodes or two network namespaces.
- Load the driver and create the device on both nodes:
modprobe rdma_rxethenrdma link add rxe0 type rxe netdev eth1. Expected: no output. If not:modprobefails on kernels withoutrdma_rxebuilt — checkmodinfo rdma_rxe. - Confirm the device exists:
rdma linkandibv_devices. Expected: arxe0/1link in state ACTIVE andrxe0in the device list. If not: the netdev name in step 1 was wrong;ip -br linkto find the right one. - Read the full port block:
ibv_devinfo -v -d rxe0. Expected: a block withtransport,link_layer,state,max_mtu,active_mtu,sm_lid,port_lid. If not: run as root; some fields are privileged. - Annotate the output against the mlx5 sample in Segment 1. Write down, field by field, which fields match and which are absent or zero on rxe. Expected:
port_lidandsm_lidare zero on both. Record the fields rxe does not populate — that list is the honest limit of this lab. - Cross-check with sysfs:
cat /sys/class/infiniband/rxe0/ports/1/stateand.../link_layer. Expected:ACTIVEandEthernet. If they disagree with the tool, the tool is reading a different device — re-run with-d. - Note for later modules: the rxe documentation warns that
mlx4_ib/mlx5_ibshould not be loaded on the same machine as Soft-RoCE, so do not run this on the Dell lab host while its ConnectX driver is up.
Retrieval check
10 questions from memory. Answer before looking anything up; misses become flashcards.
Explain it to a Dell SE
Explain to a Dell SE, in four sentences, what an RDMA application has to create before it can move a single byte, and why the RoCE port on their PowerEdge reports itself as an InfiniBand transport.
Sources
Facts in this lesson were checked against rdma-core man pages (master) and NVIDIA DOCA-Host RoCE documentation, 2026-09-07. Dates are when each page was fetched.
- ibv_devinfo(1) - rdma-core man page · fetched 2026-09-07
- RDMA over Converged Ethernet (DOCA-Host) - generated PDF · fetched 2026-09-07
- ibv_create_qp(3) - rdma-core man page · fetched 2026-09-07
- ibv_create_cq_ex(3) - rdma-core man page · fetched 2026-09-07
- ibv_create_srq(3) - rdma-core man page · fetched 2026-09-07
- ibv_reg_mr(3) - rdma-core man page · fetched 2026-09-07
- ibv_advise_mr(3) - rdma-core man page · fetched 2026-09-07
- ibv_query_device_ex(3) - rdma-core man page · fetched 2026-09-07
- ibv_poll_cq(3) - rdma-core man page · fetched 2026-09-07
- Optimized Memory Access (ODP UMR MW) - MLNX_OFED 24.10-1.1.4.0 LTS · fetched 2026-09-07
- DOCA RDMA - DOCA-Host · fetched 2026-09-07
- linux-rdma/perftest README · fetched 2026-09-07
The same idea elsewhere
Other lessons that cover this ground, sometimes from another course's angle.