How a RoCE packet gets classified
S3·E1The DSCP that cannot exist · Dell lab, Round Rock, day one of the eleven-day acceptance clock
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
- Trace one RoCEv2 packet from its UDP destination port and TOS octet through trust mode, switch priority, traffic class, traffic pool and egress queue.
- Separate the 6 DSCP bits from the 2 ECN bits in an IPv4 TOS octet and name the four RFC 3168 codepoints.
- State the Cumulus RoCE default mappings (DSCP 24-31 to switch priority 3, DSCP 48-55 to switch priority 6, SP3 to TC3, SP6 to TC6).
- Convert between a host-side TOS value and the DSCP the switch sees, and explain why doRoCE.sh's 106 lands in the RoCE range.
Episode 1 — The DSCP that cannot exist
Two GPU nodes on the bench, an all-reduce that should be filling the link, and a number the customer’s network lead has written in his notebook under “attempt one”. In eleven days that number decides whether the second order for the rest of pod A is signed. The Dell SE has it in his promise spreadsheet as row 41, “fabric ready”; his coffee went cold two meetings ago.
The network lead is certain the fabric is not at fault: RoCE is enabled on the leaf, the hosts were configured with NVIDIA’s own script, both sides say lossless.
You ask for one byte. RoCEv2 rides on UDP, and the switch advertises protocol 4791 in the LLDP application TLV — but that TLV is an advertisement, not a classifier, and the data-path decision is made on the marking.[1] The marking lives in the IPv4 TOS octet: six bits of DSCP, then two bits of ECN.[4] With RoCE enabled, DSCP 24-31 lands on switch priority 3 and DSCP 48-55 on switch priority 6.[1]
That agreement exists because classification has to happen at line rate at every hop, and nothing in a packet announces “I am RDMA”. Host and fabric agree on a number instead, and every switch on the path enforces it independently.[1][7]
You ask him to read the host’s configured value aloud. 106. Then what he did with it: he configured the leaf for DSCP 106. A marking nobody validates never errors — it forwards, in the wrong class. Segment 1 starts exactly where that mistake does, with the byte itself.
1RoCEv2 is UDP, and the switch does not classify on the port
RoCEv2 rides on UDP. On a Cumulus switch with RoCE enabled the LLDP application TLV advertises UDP protocol 4791 together with the RoCE priority, so an attached NIC can learn which priority this fabric expects.[1] That TLV is an advertisement, not a classifier: the switch’s own data-path decision is made on the packet’s marking, not on the UDP port.[1][2]
The marking lives in one byte. In IPv4 that is the TOS octet, in IPv6 the Traffic Class octet, and RFC 3168 splits it: the top six bits are DSCP, and “bits 6 and 7” are the ECN field.[4] The four ECN codepoints are 00 Not-ECT, 01 ECT(1), 10 ECT(0) and 11 CE, and the RFC is explicit about who sets the last one: “The CE codepoint ‘11’ is set by a router to indicate congestion to the end nodes”, and “For a router, the CE codepoint of an ECN-Capable packet SHOULD only be set if the router would otherwise have dropped the packet”.[4]
Those two fields are independent. A switch that marks CE on a congested queue rewrites the low two bits and leaves the six DSCP bits alone, so classification upstream and downstream is unchanged. Engineers who have only ever seen the byte as a single number reliably get this wrong and report that “ECN changed our QoS marking”.
The practical consequence for a fabric: RoCE classification is an agreement between the host and the switch about DSCP values, enforced independently at every hop. Nothing in the packet says “I am RDMA”; the DSCP says “I belong in the lossless class”, and every switch on the path must agree what that means.[1][7]
Classify — trust mode decides what the switch reads
DSCP values are grouped into 8 ranges that map to switch priorities 0-7 — DSCP 0-7 → SP0, 24-31 → SP3, 48-55 → SP6. The packet here is RoCEv2 on UDP destination port 4791 with DSCP 26. RFC 3168 puts the ECN field in bits 6-7 of the IPv4 TOS octet, so DSCP (bits 0-5) and ECN share one byte: codepoints Not-ECT 00, ECT(1) 01, ECT(0) 10, CE 11.
FAE angle The host and the switch talk in different units. The host script says 106 (ToS), the switch talks DSCP 26. Do the shift on the whiteboard before anyone opens a support case.
PFC on switch priority 3 plus ECN on TC3. The buffer must never overflow for RoCE: the switch pauses upstream before it would drop.
nv set qos roce state enabled nv set qos roce mode lossless nv config apply
nv set qos mapping default-global trust both nv set qos mapping default-global dscp 26 switch-priority 3 nv set qos mapping default-global port-default-sp 0
- DSCP values are grouped into 8 ranges that map to switch priorities 0-7 — DSCP 0-7 → SP0, 24-31 → SP3, 48-55 → SP6. source
- Trust mode with RoCE enabled is PCP + DSCP; the LLDP application TLV advertises UDP protocol 4791. source
- The ECN field is bits 6 and 7 of the IPv4 TOS octet; codepoints Not-ECT 00, ECT(1) 01, ECT(0) 10, CE 11. source
- doRoCE.sh defaults to TOS/DSCP 106 with trust mode dscp. ⚠ 106 is a ToS value; ToS = DSCP << 2, so it is DSCP 26 — this arithmetic is stated on no NVIDIA page. source
- PFC RX and TX are enabled in lossless mode and disabled in lossy mode. source
- One command sets buffer pools, classification and priority mappings, PFC and ECN thresholds, and scheduling for regular, RoCE and CNP traffic. source
- `nv set qos roce state` defaults to `disabled` — RoCE QoS is off out of the box. source
- Default mode is `lossless`; `nv set qos roce` alone is the same as `nv set qos roce mode lossless`. source
RoCE (CL 5.18) · Quality of Service · Packet Trimming · NVUE QoS set
2Trust: which marking the switch believes
Trust mode is the switch’s answer to “whose marking do I read?”. NVUE sets it per mapping profile: nv set qos mapping <profile-id> trust l2|l3|port|both, with pcp <value> switch-priority <sp>, dscp <value> switch-priority <sp> and port-default-sp <sp> to change the tables themselves.[3] The semantics are: 802.1p (L2) maps PCP 0-7 directly onto switch priorities 0-7; DSCP (L3) groups the 64 DSCP values into eight ranges that map to switch priorities 0-7, so DSCP 0-7 becomes switch priority 0; port ignores markings entirely and classifies every frame to the port’s default priority.[2]
The fourth value is the one that catches people. With both configured, the switch “prioritizes DSCP for IP traffic while applying 802.1p to non-IP frames” — verified on the 5.18 QoS page at write time.[2] That is exactly what you want on a mixed fabric, and exactly what makes a PCP-only lab test look like it did nothing.
Enabling RoCE sets trust for you, and it is worth being precise about what it sets. The RoCE defaults table lists “Port trust mode: YES” for both lossy and lossless mode - that row means trust is enabled on the port, not that the mode is port - and nv show qos roce reports the resulting setting as trust-mode pcp,dscp: the switch trusts DSCP for IP traffic and PCP for the rest, which is the opposite of the port mode described above.[1] The per-interface status view reports the same trust-mode field so you can read back what the one-liner did rather than infer it.[6]
3The RoCE default tables
When RoCE is enabled, Cumulus installs a specific set of mappings. The DSCP table sends DSCP 24-31 to switch priority 3 (RoCE data) and DSCP 48-55 to switch priority 6 (congestion notification packets).[1] The switch-priority table then sends SP3 to traffic class 3 and SP6 to traffic class 6, and every other switch priority to traffic class 0.[1]
The two RoCE classes are scheduled differently on purpose. Traffic class 3 gets WRR 50% and traffic class 6 gets strict priority, in both lossy and lossless mode.[1] Strict priority for CNP is the point: a congestion notification that queues behind the congestion it is reporting is useless, so it jumps every other class.
Outside RoCE the mapping is trivial: “By default switch priority values map directly to the matching egress queue”, so switch priority 0 leaves on egress queue 0.[2] The default egress scheduler outside RoCE gives 12% to queues 0, 2, 4 and 6 and 13% to queues 1, 3, 5 and 7.[2] You can move any of this with nv set qos egress-queue-mapping <profile> switch-priority <sp> traffic-class <tc> and nv set qos egress-scheduler <profile> traffic-class <tc> mode dwrr|strict.[3]
Two numbers are worth memorising as a pair, because they are the fastest classification check you can do in a customer’s terminal: 3 for data, 6 for CNP, DSCP ranges 24-31 and 48-55. NCP-AIN objective 2.1 is “configure Spectrum-X switches for RoCE”, and this table is the part of it you will be asked to reproduce from memory.[9]
4From traffic class to pool to queue
A traffic class is not yet buffer. Each class draws from a traffic pool, and pools are sized as a percentage of switch buffer memory: nv set qos traffic-pool <pool-id> memory-percent <value> and nv set qos traffic-pool <pool-id> switch-priority <sp>.[3] Out of the box, and this is the fact that surprises people, “Cumulus Linux allocates 100% of the buffer memory to the default-lossy traffic pool by default”.[2] A lossless class with no lossless pool behind it is a mapping with nowhere to store a burst.
Enabling RoCE creates the pools, and the documented tuning pattern splits them explicitly — 40% to default-lossy and 60% to roce-lossless — which lesson 3.2 runs as a change window.[1] The nv show interface <if> qos roce status view reports the pool map alongside the mode and the mappings, so the pipeline is inspectable end to end from one command.[6]
The last stage is the egress queue, where the scheduler decides service order and where ECN marking and WRED drops are counted. nv show interface <if> counters qos egress-queue-stats breaks out per-traffic-class transmitted frames and bytes, unicast buffer discards and WRED drops — the read that tells you whether the class you built is actually carrying the traffic you think it is.[6]
One scope warning for lab work: a virtual Cumulus switch does not implement QoS shaping, buffer management or packet marking.[8] In simulation you can build and read the mapping tables, and that is genuinely useful, but any “verification” of buffering or marking there is verifying nothing.[8]
5The host side, and the arithmetic that bites
The switch’s table is only half the agreement. NVIDIA’s own host-side script doRoCE.sh sets the NIC’s trust mode to dscp with mlxreg -y -d {bdf} --reg_name QPTS -i "local_port=1" --set "trust_state={trust_val}", and writes the verbs traffic class to /sys/class/infiniband/{dev}/tc/1/traffic_class, with RDMA-CM defaults under /sys/kernel/config/rdma_cm/ and default_roce_tos.[5]
Its documented defaults are TOS/DSCP 106, GID index 3, trust dscp.[5] Read that carefully. 106 is a TOS value, and TOS is DSCP shifted left by two bits to make room for the ECN field. So 106 >> 2 = DSCP 26 — which sits inside the switch’s default 24-31 range and therefore lands on switch priority 3.[5][1][4] The number is correct; the label on it is the trap. An engineer who reads “106” as a DSCP and configures the switch for DSCP 106 has configured an out-of-range value, and one who “fixes” the host to TOS 26 has just marked DSCP 6, which lands in traffic class 0 with the bulk traffic.
Field procedure: never trust either side’s configured value on its own. Capture the flow and read the byte. On the receiver, tcpdump -v on the UDP 4791 flow prints the TOS octet, and the DSCP is that value divided by four. That capture is the only artefact that proves host and switch agree.[5][1]
Case update — divide by four
The capture settles it. On the receiver, tcpdump -v on the UDP 4791 flow prints tos 0x6a — 106, which is DSCP 26 with ECT(0) underneath — inside the default 24-31 range, therefore switch priority 3.[5][1] The hosts were right; the leaf had been configured for a DSCP that cannot exist. You change the switch, not the servers; attempt two lands where it should.
What you say to the network lead: “106 is a TOS value, and TOS is the DSCP shifted up two bits — divide by four before you type it into a switch.”[4] He writes it down, then ruins the mood: the other three leaves in pod A have never had nv set qos roce run on them, and the change record says five minutes, low risk.
Lab
Goal: prove the DSCP that leaves your Dell-lab host is the DSCP the switch would classify. Read-only on the switch; the host changes are per-connection and revert on reboot.
Pre-flight inventory (record all of it before touching anything):
ibstat # device names and port state
ibdev2netdev # RDMA device to netdev mapping
cat /sys/class/infiniband/<dev>/tc/1/traffic_class
ip -br link show <ifname>- On both hosts confirm the RDMA device and its netdev with
ibdev2netdev. Expected:mlx5_0 port 1 ==> ens1f0np0 (Up). If a device shows Down, stop here; nothing below is meaningful. - Record the current verbs traffic class:
cat /sys/class/infiniband/<dev>/tc/1/traffic_class. Write this value down — it is your rollback. - Set the RoCE TOS to 106 on both hosts:
echo 106 | sudo tee /sys/class/infiniband/<dev>/tc/1/traffic_class. Expected: the file reads back 106. Rollback: write the value you recorded in step 2 (or reboot; this sysfs value is not persistent). - On the receiver start a capture on the netdev:
sudo tcpdump -v -n -i <ifname> udp port 4791 -c 20. - On the sender run a short RDMA write test against the receiver:
ib_write_bw -d <dev> -F <receiver-ip> --duration 10. On the receiver, startib_write_bw -d <dev> -Ffirst. Expected: a bandwidth table on both sides and packets in the capture. - Read the TOS octet in the capture. Expected:
tos 0x6a(106), i.e. DSCP 26, inside the switch’s default 24-31 range, so switch priority 3. If you seetos 0x0, the traffic class was not applied to this queue pair — check whether the application used RDMA-CM, in which casedefault_roce_tosunder/sys/kernel/config/rdma_cm/<dev>/ports/1/is the knob that applies. - Now do the negative control: set the traffic class to 24 (
echo 24 | sudo tee …), repeat the capture, and confirm the wire showstos 0x18= DSCP 6, which would land in traffic class 0. This is the “fixed the units and broke the fabric” mistake, reproduced deliberately. Rollback: restore the step-2 value.
Optional (customer lab, read-only on the switch): on the leaf facing these hosts run nv show interface <if> qos roce status and compare its trust-mode and DSCP mapping against what you just captured. No configuration change is required for this step.
Goal: read the TOS octet with your own eyes and build the mapping table by hand. No switch required.
- In
~/containerlab, bring up two plain Linux containers on one bridge (any recentlinuxkind image withiproute2,tcpdumpandsocatoriperf3). Verify withdocker exec -it clab-<lab>-h1 ip -br addr. Expected: both hosts have an address on the shared link. If not, check the topology file’s endpoint names. - On h2, capture:
tcpdump -v -n -i eth1 udp port 4791. Leave it running. - On h1, send UDP to port 4791 with a chosen TOS. With socat:
socat -u - UDP-DATAGRAM:<h2>:4791,ipv4-tos=104 <<< test. Expected on h2: a line containingtos 0x68.0x68is 104, so DSCP = 104 / 4 = 26, ECN bits = 0. - Repeat with TOS 106 (
0x6a). Expected:tos 0x6a, DSCP still 26, ECN bits now10= ECT(0). Confirm for yourself that the DSCP did not move when the ECN bits changed. If tcpdump prints notosfield, add-vvor read the raw byte with-XX. - Repeat with TOS 96 (DSCP 24) and TOS 192 (DSCP 48). Write down which switch priority each would land on under the Cumulus defaults.
- Build the table by hand before checking it: for DSCP 0, 6, 24, 26, 31, 32, 48, 55, 56 write the switch priority and the traffic class. Then check against this lesson’s Segment 3 and the FabricQosPipeline component above. Expected: 24-31 to SP3 to TC3; 48-55 to SP6 to TC6; everything else to TC0.
- If you have a DSX Air simulation available, run
nv show qos mapping default-globalandnv show qos roceto see the classification surface as NVUE presents it. Expected: the mapping tables render. Do not attempt to validate buffering or marking here — a virtual switch implements no QoS shaping, buffer management or packet marking, so anything you measure is the Linux kernel, not a Spectrum ASIC.
Deliverable: a five-row table (DSCP, TOS, switch priority, traffic class, scheduler) that you can reproduce from memory in an interview.
Retrieval check
9 questions from memory. Answer before looking anything up; misses become flashcards.
Explain it to a Dell SE
Explain to a Dell server engineer, in five sentences, how a switch decides that one particular UDP packet is RoCE traffic that deserves the lossless queue - and what the server has to do for that decision to come out right.
Sources
Facts in this lesson were checked against Cumulus Linux 5.18 RoCE and QoS pages, NVUE 5.x QoS reference, RFC 3168; re-fetched 2026-09-07. Dates are when each page was fetched.
- RDMA over Converged Ethernet - RoCE | Cumulus Linux 5.18 · fetched 2026-09-07
- Quality of Service | Cumulus Linux 5.18 · fetched 2026-09-07
- NVUE Reference - QoS set/unset commands · fetched 2026-09-07
- RFC 3168 - The Addition of Explicit Congestion Notification (ECN) to IP · fetched 2026-09-07
- NVIDIA/doroce-linux - doRoCE.sh host-side RoCE configuration script · fetched 2026-09-07
- NVUE Reference - QoS show commands · fetched 2026-09-07
- Cumulus Linux Configuration Guide for Ethernet Storage Fabrics · fetched 2026-09-07
- Cumulus Linux in a Virtual Environment | Cumulus Linux 5.18 · fetched 2026-09-07
- NVIDIA-Certified Professional: AI Networking (NCP-AIN) · fetched 2026-09-07
- Network Troubleshooting | Cumulus Linux 5.18 · fetched 2026-09-07
The same idea elsewhere
Other lessons that cover this ground, sometimes from another course's angle.