QP state machine, retries, and connection setup
S1·E5Forty minutes, every time · Customer lab, Monday, first week after handover
Builds on: Transports and opcodes: what RC, UC and UD can actually do, GIDs: how a connection picks its RoCE version
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
- Reproduce the RESET to INIT to RTR to RTS sequence and name the attributes each transport requires at each transition.
- Explain how `timeout`, `retry_cnt`, `rnr_retry` and `min_rnr_timer` produce completion status 12 and status 13.
- Trace the RDMA-CM active and passive sequences and identify which event reports each failure mode.
- Contrast connection setup through RDMA-CM with an out-of-band exchange and justify which one a given application should use.
Episode 5 — Forty minutes, every time
The cluster was handed over Friday; the training job dies after roughly forty minutes, every attempt. The platform engineer has a screenshot that says only “RDMA errors”, the switch team is on the call, tired of being blamed, and nobody wants to burn forty minutes per experiment finding out who owns it. The network lead says what he has said once a week since Round Rock: show me the counter.
Two completion statuses decide the meeting. If the requester transmits and no acknowledgement arrives inside timeout, it retransmits up to retry_cnt times, and exhausting that budget completes the work request as IBV_WC_RETRY_EXC_ERR — status 12, rendered as “transport retry counter exceeded”.[1][8] If instead the responder answers with a receiver-not-ready NAK because it has no receive buffer posted, the requester waits min_rnr_timer and retries up to rnr_retry times, and exhaustion gives IBV_WC_RNR_RETRY_EXC_ERR, status 13.[8] The switch team stays or leaves on that single answer.
Those knobs exist because a reliable connection has to decide how long to keep believing in a packet that has not been acknowledged. They are installed as the queue pair walks INIT to RTR to RTS, and they are RC-only, because a transport that never acknowledges has nothing to retry.[1] You do not have to wait forty minutes either: perftest exposes the local ACK timeout as -u, four microseconds times two to the power of the value.[5]
Status 12 means nobody answered; status 13 means somebody answered with nowhere to put it.
Walk the states first: every knob in this argument is installed on a transition.
1Four states and the masks that separate the transports
A queue pair walks IBV_QPS_RESET to INIT to RTR to RTS, with SQD, SQE and ERR off the happy path.[1] Each transition is one ibv_modify_qp() call, and what makes the transports differ is not the states but the attribute masks each transition requires.
RESET to INIT is local-only. UD requires IBV_QP_STATE, IBV_QP_PKEY_INDEX, IBV_QP_PORT, IBV_QP_QKEY; RC and UC require the same three plus IBV_QP_ACCESS_FLAGS instead of the q_key.[1] That single difference is the whole security model appearing early: a datagram QP is authorised by a q_key, a connected QP declares up front what the peer will be allowed to do one-sided.
INIT to RTR is the connection step. UD requires only IBV_QP_STATE.[1] UC requires IBV_QP_STATE, IBV_QP_AV, IBV_QP_PATH_MTU, IBV_QP_DEST_QPN, IBV_QP_RQ_PSN.[1] RC requires all of those plus IBV_QP_MAX_DEST_RD_ATOMIC and IBV_QP_MIN_RNR_TIMER — the two attributes only a reliable responder needs.[1] Note what IBV_QP_AV implies after the previous lesson: this is where the GID index, and therefore the RoCE version, is fixed.[6]
RTR to RTS arms the requester. UC and UD need IBV_QP_STATE and IBV_QP_SQ_PSN only; RC adds IBV_QP_MAX_QP_RD_ATOMIC, IBV_QP_RETRY_CNT, IBV_QP_RNR_RETRY, IBV_QP_TIMEOUT.[1] And path_mtu, whose enum is IBV_MTU_256 through IBV_MTU_4096, is “valid for RC/UC QPs only” — which is why UD traffic behaves differently from RC under an MTU mismatch.[1]
RESET → INIT
ibv_modify_qp(qp, &attr, IBV_QP_STATE | …)
Local-only step: which physical port and which P_Key index this QP lives on. The single difference between the transports here is the last attribute — UD needs IBV_QP_QKEY (datagrams are authorised by a q_key), the connected transports need IBV_QP_ACCESS_FLAGS (what the peer will be allowed to do one-sided).
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
2The four knobs that manufacture errors 12 and 13
The RC-only attributes are four 8-bit values: min_rnr_timer (“Minimum RNR NAK timer (valid only for RC QPs)”), timeout (“Local ack timeout for primary path”), retry_cnt (“Retry count”) and rnr_retry (“RNR retry”).[1] They are not tuning trivia; they are the mechanism that turns a network condition into a specific completion status.
If the requester sends and no acknowledgement arrives inside timeout, it retransmits, up to retry_cnt times. When that budget is exhausted the work request completes as IBV_WC_RETRY_EXC_ERR, which is position 12 in the status enum.[8] If instead the responder answers with a receiver-not-ready NAK because it has no receive buffer posted, the requester waits min_rnr_timer and retries up to rnr_retry times, and exhaustion produces IBV_WC_RNR_RETRY_EXC_ERR, position 13.[8] ibv_wc_status_str() renders these as “transport retry counter exceeded” and “RNR retry counter exceeded”.[8]
The host counters corroborate the split. The in-tree mlx5_ib driver defines local_ack_timeout_err, req_transport_retries_exceeded, rnr_nak_retry_err, req_rnr_retries_exceeded and out_of_buffer, all surfacing under /sys/class/infiniband/<dev>/ports/<p>/hw_counters/.[9] The names carry the mapping and the lab below confirms it on real hardware: the req_ counters and local_ack_timeout_err move on the requester behind a status 12, the RNR pair behind a status 13, and out_of_buffer on the responder is why the RNRs were sent. NVIDIA publishes no fetchable page defining these counters, so verify the deltas on the card rather than quoting a definition.
perftest lets you make the failure happen on demand: -u, --qp-timeout=<timeout> is documented as “QP timeout, timeout value is 4 usec * 2^(timeout), default 14”.[5] Lowering it shortens the wait before a retry-exceeded appears, which is how you provoke a clean demonstration instead of waiting.
3Two ways to exchange the connection
Something must carry the queue pair number, the starting PSN and the address between the two sides. There are exactly two families of answer.
The RDMA connection manager is the first. “The RDMA CM is a communication manager used to setup reliable, connected and unreliable datagram data transfers”, giving “an RDMA transport neutral interface for establishing connections”, and it “can control both the QP and communication management portions of an RDMA API, or only the communication management piece”.[2] The active sequence is rdma_getaddrinfo, rdma_create_event_channel, rdma_create_id, rdma_resolve_addr (wait for RDMA_CM_EVENT_ADDR_RESOLVED), rdma_create_qp, rdma_resolve_route (wait for ROUTE_RESOLVED), rdma_connect (wait for ESTABLISHED), then data, rdma_disconnect, rdma_destroy_qp, rdma_destroy_id, rdma_destroy_event_channel.[2] The passive side is rdma_create_event_channel, rdma_create_id, rdma_bind_addr, rdma_listen, wait for RDMA_CM_EVENT_CONNECT_REQUEST — which arrives with a new id — then rdma_create_qp and rdma_accept.[2]
Diagram source (Mermaid)
flowchart LR; A[rdma_resolve_addr] --> B[ADDR_RESOLVED] --> C[rdma_create_qp] --> D[rdma_resolve_route] --> E[ROUTE_RESOLVED] --> F[rdma_connect] --> G[ESTABLISHED] --> H[data transfer] --> I[rdma_disconnect]
Events are how failures announce themselves. RDMA_CM_EVENT_UNREACHABLE is “generated on the active side to notify the user that the remote server is not reachable”; REJECTED means “a connection request or response was rejected by the remote end point”; ADDR_CHANGE fires when “the network device associated with this ID through address resolution changed its HW address”; TIMEWAIT_EXIT means “the QP associated with a connection has exited its timewait state”.[3] The discipline that beginners skip: “All events that are reported must be acknowledged by calling rdma_ack_cm_event”, and “Destruction of an rdma_cm_id will block until related events have been acknowledged” — the usual cause of a clean-shutdown hang.[3]
rdma_conn_param carries the negotiated depths and a small payload: private_data_len is capped at 56 bytes for RDMA_PS_TCP and 180 bytes for RDMA_PS_UDP; responder_resources is “the maximum number of outstanding RDMA read and atomic operations that the local side will accept from the remote side” and initiator_depth the same for operations it will issue; retry_count and rnr_retry_count are 3-bit values and RDMA_PS_TCP only.[4] Behaviour is port-space dependent: “for RDMA_PS_TCP, it initiates connection; for RDMA_PS_UDP, it initiates remote QP lookup”.[4]
The second family is out-of-band exchange: the application moves the same values over any channel it already trusts. perftest does this by default and only uses the CM when given -R, --rdma_cm.[5] DOCA offers both: doca_rdma_export() produces a connection blob that the application transports and the peer consumes with doca_rdma_connect(), with the warning that the exported data “contains sensitive information” and must be transmitted securely; or the built-in CM path, doca_rdma_start_listen_to_port() on the server and doca_rdma_addr_create() plus doca_rdma_connect_to_addr() on the client, addressed by IPv4, IPv6 or GID.[7] For an application that already runs its own rdma_cm event loop, the bridge functions doca_rdma_bridge_prepare_connection(), doca_rdma_bridge_accept() and doca_rdma_bridge_established() splice the two together.[7]
On RoCE specifically there is a reason to prefer the CM that has nothing to do with convenience: with no Subnet Manager “querying a path is impossible. Therefore, the path record structure must be filled with relevant values before establishing a connection. Hence, it is recommended working with RDMA-CM to establish a connection as it takes care of filling the path record structure.”[6]
Goal: provoke a transport-retry-exceeded completion deliberately, and confirm the diagnosis from the counters rather than from the log line.
- Pre-flight: record the baseline counters on the client.
for c in local_ack_timeout_err req_transport_retries_exceeded rnr_nak_retry_err out_of_buffer packet_seq_err; do echo "$c $(cat /sys/class/infiniband/mlx5_0/ports/1/hw_counters/$c)"; done.[9] - Establish a normal run first so you know the path works:
ib_write_bw -d mlx5_0 -x <v2 index> -s 4096 -D 20on both sides. Expected: a result table and no counter movement beyond noise.[5] - Shorten the retry budget so the failure is quick, and start a long run: add
-u 8to both sides. The local ACK timeout becomes 4 usec times 2 to the power 8 instead of 2 to the power 14.[5] - Interrupt the path mid-run: unplug the DAC on the server side, or administratively down the switch port. Expected: the client run terminates with a completion failure whose text is “transport retry counter exceeded”.[8]
- Re-read the counters. Expected:
local_ack_timeout_errandreq_transport_retries_exceededhave moved;out_of_bufferandrnr_nak_retry_errhave not.[9] That asymmetry is the evidence that this is a fabric event and not an application one. - Restore the link and re-run step 2 to confirm the path is healthy again.
- Now do the contrast case if you can arrange it: a responder that stops posting receives produces status 13 with
out_of_bufferandrnr_nak_retry_errmoving instead, andlocal_ack_timeout_errflat.[9][8]
- Baseline the counters under
/sys/class/infiniband/mlx5_0/ports/1/____/. - Prove the path is healthy with
ib_write_bw -d mlx5_0 -x <index> -s 4096 -D 20on ____ sides with the ____ flags. - Shorten the retry budget with
____ 8, because the local ACK timeout is ____ usec times 2 to the power of the value. - Break the path, and expect the completion string “____ retry counter exceeded”, which is status number ____.
- Confirm the diagnosis:
____andreq_transport_retries_exceededmove whileout_of_bufferstays flat. - The mirror-image failure is status ____, whose corroborating counters are ____ and ____.
A Dell customer’s training job dies after roughly forty minutes with completions your contact describes only as “RDMA errors”. You have SSH on both hosts and no switch access yet.
Produce: the two completion statuses you would ask them to distinguish and what each one would mean; the exact counter reads on each host that would corroborate each; one perftest command that would reproduce the failing condition faster than waiting forty minutes; and the single question that decides whether the switch team needs to be in the call at all. Acceptance criteria: your answer names both status codes with their numbers, names at least two counters per branch and says on which host each is read, and names the perftest flag that shortens the retry budget with its formula.
Case closed
It is status 13, and the counters agree: out_of_buffer and rnr_nak_retry_err move while local_ack_timeout_err and req_transport_retries_exceeded stay flat.[9] The switch team drops off, the application team goes looking at the loop that posts receives, and you leave a -u 8 reproduction so the next experiment costs minutes, not forty of them.[5] The archive is live before the freeze. The SE closes the last row of the spreadsheet and finally drinks the coffee, cold; the operator prints one more label, STATUS 13, ASK THE RECEIVER; the network lead’s notebook now carries a counter list in his own handwriting, the real deliverable. What you say: “Twelve means nobody answered; that one is ours. Thirteen means they answered and had nowhere to put it — that one lives in your receiver.”
Lab
Dell-lab ConnectX or BlueField-3 pair. Steps 1 to 4 are read-only. Step 5 deliberately flaps a link and names its rollback.
- Pre-flight inventory:
ibv_devinfo -l,ibdev2netdev,show_gids,cma_roce_mode -d mlx5_0 -p 1, and a baseline read oflocal_ack_timeout_err,req_transport_retries_exceeded,rnr_nak_retry_err,out_of_bufferandpacket_seq_errunder/sys/class/infiniband/mlx5_0/ports/1/hw_counters/. Save all of it. - Healthy baseline:
ib_write_bw -d mlx5_0 -x <v2 index> -s 4096 -D 20on both sides. Expected: a result table; counters essentially unchanged. - Contrast the connection families as in the no-hardware lab:
-x <index>versus-R, and record which GID each run used by capturing withtcpdump -i <netdev> -nn -c 10 "udp port 4791 or ether proto 0x8915". - Shorten the retry budget and observe the difference in time-to-failure without breaking anything yet:
ib_write_bw -d mlx5_0 -u 8 -s 4096 -D 20versus the default-u 14. Expected: identical results on a healthy path; the setting only changes how long the requester waits before retransmitting. - Mutating step — a deliberate link flap on a lab host only. With a
-u 8run in progress, down the port on the far side:ip link set <netdev> downon the server. Expected: the client completes with “transport retry counter exceeded” andlocal_ack_timeout_errplusreq_transport_retries_exceededmove whileout_of_bufferstays flat. Rollback, mandatory:ip link set <netdev> up, wait foribv_devinfo -d mlx5_0 | grep stateto reportPORT_ACTIVE (4), re-run step 2 and confirm the baseline result returns. If the port does not come back, check the switch side before reloading any driver. - Record the counter deltas next to the completion status. That pairing - status code plus corroborating counter, on the correct host - is the artifact worth keeping for module 4.
Soft-RoCE on two containerlab Linux nodes. Read-only: no configuration is changed, only processes started and stopped.
- Bring up Soft-RoCE on both nodes:
modprobe rdma_rxe,rdma link add rxe0 type rxe netdev eth1, confirm withrdma link. Expected: no output frommodprobeorrdma link add;rdma linkthen showslink rxe0/1 state ACTIVE physical_state LINK_UP netdev eth1on both nodes. If not:modinfo rdma_rxeandip -br linkto confirm the module and the netdev name. - Exercise the CM path end to end: on the server
rping -s -a <server_ip> -V -C 10, on the clientrping -c -a <server_ip> -S <client_ip> -V -C 10. Expected: ten ping-pong lines on both sides. If nothing happens, check that the server is listening on the address the client is using, not on a different interface. - Provoke a CM failure: stop the server and re-run the client. Record which event the client reports. Expected: a failure consistent with
RDMA_CM_EVENT_UNREACHABLErather thanREJECTED, because nothing answered at all. If your build prints only an errno, write that down too and map it to the event definitions in Segment 3. - Contrast the two connection families: run
ib_write_bw -d rxe0 -x <index>on both sides, thenib_write_bw -d rxe0 -Ron both sides. Note which of the two accepted a GID index and which one did not - that is the whole distinction, in one pair of commands. Expected: both runs produce a result table, but only the first accepts-x; the-Rrun connects through rdma_cm and the index plays no part in it. If not: if the-Rrun fails to connect, the librdmacm listener is not up on the address the client dialled - re-run the server bound to the same IP. - Order the CM call sequence from memory: write the active-side calls and the event each one waits for, then check yourself against Segment 3. Do the same for the passive side and mark where the new id appears.
- Limitation to record: whether Soft-RoCE surfaces the same
hw_countersnames as a ConnectX is not established here, so do not generalise counter behaviour from this lab to hardware.
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 has to be exchanged between two machines before an RDMA connection can move data, and why an RDMA job can fail with a retry error even though both machines are healthy.
Sources
Facts in this lesson were checked against rdma-core and librdmacm man pages (master) plus NVIDIA DOCA-Host RoCE and DOCA RDMA documentation, 2026-09-07. Dates are when each page was fetched.
- ibv_modify_qp(3) - rdma-core man page · fetched 2026-09-07
- rdma_cm(7) - librdmacm man page · fetched 2026-09-07
- rdma_get_cm_event(3) - librdmacm man page · fetched 2026-09-07
- rdma_connect(3) - librdmacm man page · fetched 2026-09-07
- perftest(1) man page (linux-rdma/perftest master) · fetched 2026-09-07
- RDMA over Converged Ethernet (DOCA-Host) - generated PDF · fetched 2026-09-07
- DOCA RDMA - DOCA-Host · fetched 2026-09-07
- rdma-core libibverbs/verbs.h (master) · fetched 2026-09-07
- Linux kernel - drivers/infiniband/hw/mlx5/counters.c · fetched 2026-09-07
- Environment Variables - NCCL 2.31.2 documentation · fetched 2026-09-07
The same idea elsewhere
Other lessons that cover this ground, sometimes from another course's angle.