GIDs: how a connection picks its RoCE version
S1·E4The benchmark crosses the subnet and the application does not · Hotel lobby, 07:20, ninety minutes before the go/no-go
Builds on: RoCEv1 vs RoCEv2: the encapsulation
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
- Read a GID table from `show_gids` or from sysfs and say which entry is RoCEv2 on which netdev.
- Determine which RoCE version a given connection is using and name the mechanism that chose it.
- Apply `cma_roce_mode` and perftest `-x` correctly to force a version on each of the two connection paths.
- Explain why an application using RDMA-CM and a perftest run on the same host can disagree about the RoCE version.
Episode 4 — The benchmark crosses the subnet and the application does not
Lobby coffee, laptop on your knees, and the SE’s 03:40 message still open. On the same pair of hosts, ib_write_bw runs clean across the subnet boundary and the customer’s storage client will not. At nine the room decides whether the archive cutover holds, and the sentence they are already rehearsing is “turn on RoCEv2 for storage”.
There is nothing to turn on. For a connected queue pair the version is not a flag: moving an RC or UC queue pair from INIT to RTR requires an address vector, the address vector names an index into the port’s GID table, and the GID type at that index sets the RoCE type of the queue pair.[1] perftest without -R does its own out-of-band exchange and lets an engineer pin that index with -x.[7] The storage client does not: it uses RDMA-CM, which decides the source GID itself and breaks the tie between the two entries with a global value of the RDMA-CM module, read and written with cma_roce_mode.[1] One benchmark and one application on one host, legitimately speaking different versions.
The table looks the way it does because both versions ship at once: every IP address configured on the port creates a pair of GID entries with the same value and different types.[1] Something has to choose between them, and which mechanism chooses depends entirely on how the connection was made.
Nobody enables RoCEv2. Somebody picks a GID index — find out who.
Ninety minutes, two hosts, one GID table. Start with where the entries come from.
1Where GIDs come from
A GID table entry is not something an administrator creates directly. “GID table entries are created whenever an IP address is configured on one of the Ethernet devices of the NIC’s ports”, and each entry carries three things: a GID value, a GID type and a network device.[1] Because both RoCE versions are supported at once, “the GID table is occupied with two GIDs, both with the same GID value but with different types”.[1] Re-verified against the current NVIDIA page at write time: each RoCE-capable port receives two GID entries per address, differing only in type.[2]
Address family shows up in the value: “The GID format can be of 2 types; IPv4 and IPv6. IPv4 GID is an IPv4-mapped IPv6 address, while IPv6 GID is the IPv6 address itself.”[1] And the family plus the version together decide the layer-3 header: “Layer 3 header for packets associated with IPv4 GIDs will be IPv4 (for RoCEv2) and IPv6/GRH for packets associated with IPv6 GIDs and IPv4 GIDs for RoCEv1.”[1]
Two capacity facts belong together. Each IP produces a pair, because both versions are supported at once - so the entry count is twice the address count, not equal to it.[1][3] Older MLNX_OFED builds added that in RoCEv1/RoCEv2 mode each GID-table entry consumes two entries in hardware as well; the current NVIDIA page no longer carries that sentence, so treat hardware GID-table capacity as version-dependent and confirm it against the customer’s firmware rather than quoting a number.[3] A host with a base interface, three VLAN sub-interfaces and both address families still burns through the table faster than a naive count suggests.
IP header 20 B / 40 B
IPv4 (20 B) for IPv4 GIDs, IPv6 (40 B) for IPv6 GIDs: "Layer 3 header for packets associated with IPv4 GIDs will be IPv4 (for RoCEv2)." The ToS octet (IPv6: Traffic Class) is split 6 + 2: DSCP in the top six bits, ECN in "bits 6 and 7 of the IPv4 TOS octet" — Not-ECT 00, ECT(1) 01, ECT(0) 10, CE 11 (RFC 3168 §5).
This is the byte the fabric reads under trust dscp, and the byte the congested switch rewrites to CE. ToS = DSCP × 4 — "to set DSCP value of 24, set the ToS bit to 96 (24x4=96)".
ib_write_bw --tclass=104 # DSCP 26 on the AV path tcpdump -i <netdev> -nn -v udp port 4791 # read the DSCP back
The ECN bits here are the IP-layer signal. They are NOT the BTH FECN/BECN bits one header down — a distinction that decides which counter you go read.
Every IP on the port creates two GID entries — same value, types IB/RoCE v1 and RoCE v2 — and each occupies two hardware entries, so the table fills twice as fast as people expect. An IPv4 GID is an IPv4-mapped IPv6 address.
A connected QP has no “set RoCEv2” knob: the address vector names a GID index, and the GID type at that index sets the version. RDMA-CM instead reads a module-global. That is why ib_write_bw -x <index> and an app using the CM can disagree on the same host.
show_gids # DEV PORT INDEX GID IPv4 VER DEV, then n_gids_found=<count> cat /sys/class/infiniband/mlx5_0/ports/1/gid_attrs/types/3 cma_roce_mode -d mlx5_0 -p 1 # read the RDMA-CM global cma_roce_mode -d mlx5_0 -p 1 -m 2 # 1 = RoCEv1, 2 = RoCEv2 echo 0 > /sys/devices/<bdf>/roce_enable # no GID tables, Raw Ethernet QPs only
show_gids ships in mlnx-tools at sbin/show_gids — not the old ofed_scripts/ path.
⚠ marks a number no fetched NVIDIA or IETF page states: the GRH, BTH and ICRC byte sizes (the IBTA spec is members-only) and whether a switch classifies RoCEv1 on the GRH traffic-class byte.
2Reading the table by hand
show_gids is the convenience script, and it is worth knowing what it does because on a stripped host you will have to do it yourself. Re-verified against the current source: it walks /sys/class/infiniband/*/ports/*/gids/*, skips the all-zero GID and the bare fe80:0000:0000:0000:0000:0000:0000:0000 entry, reads the type from gid_attrs/types/<i> and the netdev from gid_attrs/ndevs/<i>, derives the dotted-quad from the IPv4-mapped bytes, prints the header DEV PORT INDEX GID IPv4 VER DEV, and finishes with n_gids_found=<count>.[4] It takes an optional device-name argument and enumerates everything under /sys/class/infiniband/ without one.[4] It ships in mlnx-tools at sbin/show_gids, not the ofed_scripts/ path older documentation still points at.[6][4]
The kernel ABI is the authority for what those files contain, and it is short enough to memorise. gid_attrs/types/<gid-index> is “either ‘IB/RoCE v1’ for IB and RoCE v1 based GIDs or ‘RoCE v2’ for RoCE v2 based GIDs”; gid_attrs/ndevs/<gid-index> is “The net-device’s name associated with the GID”.[5] That is the whole contract: value, type, netdev.
NVIDIA’s own published table for mlx5_0 port 1 shows the pattern: index 0 is a link-local value with type v1 on p4p1, index 1 is the same value with type v2, index 2 is ::ffff:0a0a:0a01 (that is 10.10.10.1) with type v1, index 3 the same value with type v2, and mlx5_1 then starts again at index 0.[1][3]
3Which version does this connection use?
For a connected queue pair there is no version switch. NVIDIA states the mechanism: “To modify an RC/UC QP (connected QP) from INIT to RTR, an Address Vector (AV) must be given. The AV, among other attributes, should specify the index of the port’s GID table for the source GID of the QP. The GID type in that index will be used to set the RoCE type of the QP.”[1] Re-verified at write time on the current HTML page: the GID type at the specified index determines the RoCE mode for the QP.[2] IBV_QP_AV is one of the required attributes for the INIT to RTR transition on RC and UC, and it does not exist for UD.[8] So the rule to teach is: you do not set RoCEv2 — you pick a GID index, and the index picks the version.
RDMA-CM works differently, and the difference is the source of an entire class of tickets. “RDMA_CM interface requires only the active side of the peer to pass the IP address of the passive side. The RDMA_CM decides upon the source GID to be used and obtains it from the GID table. Since more than one instance of the GID value is possible, the lookup should be also according to the GID type. The type to use for the lookup is defined as a global value of the RDMA_CM module.”[1] That global is read with cma_roce_mode -d <dev> -p <port> and written with cma_roce_mode -d <dev> -p <port> -m <1|2>, where 1 is RoCEv1 and 2 is RoCEv2 — re-verified on the current page at write time.[1][2]
perftest sits on both sides of that fence, which is what makes it a good teaching instrument. Without -R it does an out-of-band exchange and lets you pin the index with -x, --gid-index=<index>; with -R, --rdma_cm it “connect[s] QPs with rdma_cm and run[s] test on those QPs” and therefore inherits whatever the CM global says.[7] Two runs on one host, minutes apart, can legitimately use different RoCE versions.
The same split appears one layer up in NCCL. NCCL_IB_GID_INDEX “Defines Global ID index used in RoCE mode” with a default of -1, but NVIDIA’s current troubleshooting guidance is explicit: on NCCL 2.21 and later the GID index is selected dynamically and you should not set the variable; only earlier versions needed it set by hand after running show_gids.[9][10] A runbook that still hard-codes an index is a runbook written for an older NCCL.
Goal: determine, with evidence rather than configuration files, which RoCE version each of two connection paths is using on one pair of hosts.
- Read the table on both hosts:
show_gids. Expected: pairs of rows sharing a GID value, with theVERcolumn readingv1andv2, and the finaln_gids_found=<count>line. Record the index of the v2 entry whoseIPv4column matches the address you will connect over, and the index of its v1 twin.[4] - Cross-check one index by hand so you can do it on a host with no
mlnx-tools:cat /sys/class/infiniband/mlx5_0/ports/1/gid_attrs/types/3should printRoCE v2, andcat .../gid_attrs/ndevs/3should print the netdev.[5] - Read what RDMA-CM would choose:
cma_roce_mode -d mlx5_0 -p 1. Record the value.[1] - Start a capture on the mapped netdev in a second shell:
tcpdump -i <netdev> -nn -c 20 "udp port 4791 or ether proto 0x8915". This filter catches both versions, which is the point.[1] - Out-of-band path, pinned to the v2 index. Server:
ib_write_bw -d mlx5_0 -x <v2 index> -s 4096 -n 2000. Client: same flags plus the server IP. Expected in the capture: UDP packets to port 4791.[7] - Out-of-band path, pinned to the v1 index. Re-run with
-x <v1 index>on both sides. Expected in the capture: frames with ethertype0x8915and no UDP header.[1] - CM path. Run
ib_write_bw -d mlx5_0 -R -s 4096 -n 2000on both sides. Expected: whichever encapsulation matches thecma_roce_modevalue you recorded in step 3, and note that-xis not what decided it here.[7] - Write the three results as a table: path, index or CM mode, and observed encapsulation. That table is the artifact you would show a customer.
show_gidsprints the headerDEV PORT INDEX GID ____ VER ____and ends withn_gids_found=____.- By hand, the type of index 3 is at
/sys/class/infiniband/mlx5_0/ports/1/____/types/3and prints either____or____. - The RDMA-CM default is read with
____ -d mlx5_0 -p 1. - A capture filter that catches both versions at once is
"udp port ____ or ether proto ____". - To force version 2 on the out-of-band path, pass
____ <index>to perftest on ____ sides. - To make perftest use the connection manager instead, pass
____; the version then comes from ____ and not from the index.
A Dell customer has two application tiers on the same hosts. The storage client uses RDMA-CM. The AI training job uses a library that opens connections itself and names a GID index. The customer reports that storage traffic is not being routed between subnets while training traffic is, and asks you to “turn on RoCEv2 for storage”.
Produce: the diagnosis in terms of the two selection mechanisms; the exact read command you would run first on one host; the exact change command and where its effect stops; and one sentence on why changing it does not affect the training job. Acceptance criteria: your answer names cma_roce_mode with its -m argument and its meaning, names the mechanism the training job uses instead, and states which of the two mechanisms a show_gids index number applies to.
Nine o'clock, with evidence
You walk in with three artefacts instead of an opinion: show_gids from both hosts, the output of cma_roce_mode -d mlx5_0 -p 1, and a capture filtered to catch both encapsulations at once.[4][1] The CM global was set to 1, so every CM-based application on that device was speaking RoCEv1 and stopping at the first router, while the benchmark kept working because it names its own GID index.[1] One command changes it, and the change stops exactly there. What you say: “You do not turn RoCEv2 on. You choose a GID, and for this client the chooser is cma_roce_mode.” The archive gets its go. The fabric is handed over on Friday, and on Monday the training job starts dying after forty minutes.
Lab
Dell-lab ConnectX or BlueField-3 pair. Steps 1 to 5 are read-only; step 6 is mutating and names its rollback.
- Pre-flight inventory:
ibv_devinfo -l,ibdev2netdev,ip -br addr,show_gids, andcma_roce_mode -d mlx5_0 -p 1. Save the output; the last value is what you must restore later. - Verify one row by hand against sysfs:
cat /sys/class/infiniband/mlx5_0/ports/1/gid_attrs/types/<i>and.../gid_attrs/ndevs/<i>for the index you plan to use. Expected:RoCE v2and the netdev name from step 1. - Start the dual-version capture:
tcpdump -i <netdev> -nn -c 20 "udp port 4791 or ether proto 0x8915". - Out-of-band, pinned:
ib_write_bw -d mlx5_0 -x <v2 index> -s 4096 -n 2000on both sides. Expected: UDP/4791 in the capture. Repeat with the v1 index and expect ethertype0x8915. - CM path:
ib_write_bw -d mlx5_0 -R -s 4096 -n 2000on both sides. Expected: the encapsulation implied by thecma_roce_modevalue recorded in step 1, regardless of any index you used earlier. - Mutating step — lab hosts only. Change the CM default:
cma_roce_mode -d mlx5_0 -p 1 -m 1. Verify withcma_roce_mode -d mlx5_0 -p 1, re-run the-Rtest from step 5 and confirm the capture now shows ethertype0x8915. Rollback, mandatory:cma_roce_mode -d mlx5_0 -p 1 -m 2(or whatever value step 1 recorded), verify by reading it back, and re-run step 5 to confirm the original behaviour returns. Leaving a host on RoCEv1 breaks every CM-based application that crosses a subnet.
Soft-RoCE on two containerlab Linux nodes. Read-only apart from adding a VLAN sub-interface, whose removal is given.
- Bring up Soft-RoCE on both nodes:
modprobe rdma_rxe,rdma link add rxe0 type rxe netdev eth1, confirm withrdma link. - Read the GID table:
show_gids. Ifmlnx-toolsis not installed, walk it by hand:for i in /sys/class/infiniband/rxe0/ports/1/gids/*; do n=$(basename $i); echo "$n $(cat $i) $(cat /sys/class/infiniband/rxe0/ports/1/gid_attrs/types/$n 2>/dev/null) $(cat /sys/class/infiniband/rxe0/ports/1/gid_attrs/ndevs/$n 2>/dev/null)"; done. Expected: pairs sharing a value with different types. Record every index, value, type and netdev. - Add a VLAN sub-interface on one node:
ip link add link eth1 name eth1.100 type vlan id 100 && ip addr add 10.100.0.1/24 dev eth1.100 && ip link set eth1.100 up. Re-run step 2. Expected: new GID entries whose netdev column iseth1.100. Rollback when finished:ip link del eth1.100, then re-run step 2 and confirm the table matches what you recorded originally. - Start a capture that catches both versions:
tcpdump -i eth1 -nn -c 20 "udp port 4791 or ether proto 0x8915". - Run
ib_write_bw -d rxe0 -x <v2 index>on both sides, then repeat with the v1 index, and classify each capture. Expected: UDP/4791 for the v2 index. If the v1 run produces nothing, record it: Soft-RoCE implements RoCEv2 over UDP and its RoCEv1 behaviour is not established by these notes, so an absent v1 capture is a finding about rxe, not about RoCE. - Doc-reading exercise: open NVIDIA’s published GID-table example and list every way the table and its caption disagree. Write down what you would ask a customer for instead of quoting an index from a document.
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, why their benchmark and their application can be running different RoCE versions on the same card at the same time, and the two commands that show which is which.
Sources
Facts in this lesson were checked against NVIDIA DOCA-Host RoCE page, mlnx-tools show_gids (master) and the Linux sysfs-class-infiniband ABI, all re-fetched 2026-09-07. Dates are when each page was fetched.
- RDMA over Converged Ethernet (DOCA-Host) - generated PDF · fetched 2026-09-07
- RDMA over Converged Ethernet (DOCA-Host, HTML) - re-verified at write time · fetched 2026-09-07
- RDMA over Converged Ethernet (RoCE) - MLNX_OFED 24.10-5.1.6.1 LTS · fetched 2026-09-07
- mlnx-tools sbin/show_gids (source) - re-verified at write time · fetched 2026-09-07
- Linux kernel ABI: sysfs-class-infiniband (stable) - re-verified at write time · fetched 2026-09-07
- GitHub API - Mellanox/mlnx-tools master tree · fetched 2026-09-07
- perftest(1) man page (linux-rdma/perftest master) · fetched 2026-09-07
- ibv_modify_qp(3) - rdma-core man page · fetched 2026-09-07
- Environment Variables - NCCL 2.31.2 documentation · fetched 2026-09-07
- Networking Troubleshooting - 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.