ToS, DSCP, PCP, SL: where marking gets lost
S2·E1The capture that ends the argument · Hotel lobby, 06:40, five days before the acceptance test
Builds on: RoCEv1 vs RoCEv2: the encapsulation, 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
- Trace one RoCE flow through the full marking chain from rdma_set_option ToS to the switch trust state and name the field that carries the marking at each hop.
- Compute ToS from DSCP and SL and PCP from ToS using NVIDIA's stated arithmetic and check the result against a capture.
- Analyze a customer symptom and decide which of the three loss points applies: no ToS set, no VLAN tag, or a trust-state mismatch.
- Apply the Force DSCP sysfs override to re-mark an unmodified application and roll it back.
Episode 1 — The capture that ends the argument
Six-forty in a hotel lobby, five days before the customer’s 64-node pod in Building D goes to acceptance, and the overnight dry run failed exactly as it did on Friday. Their network lead sent the capture with one sentence: we configured DSCP 26 everywhere and RoCE still lands in the default queue. Across the table the Dell SE is drafting a mail blaming the leaf switches, beside an airport coffee he has not touched and a spreadsheet whose row 41 reads “lossless: yes”.
You open the capture and filter UDP port 4791. The outer IP ToS byte reads zero. Trust state, the dscp2prio map, the leaf’s queues — all beside the point, because the application never marked anything: NVIDIA states that if a TOS is not set, the default value (0) is used.[1] Their jobs run under NCCL, whose NCCL_IB_TC defaults to 0, so an unset variable is an unmarked training run.[7]
This is why the marking chain exists at all. QoS on Ethernet is not a property a packet has; it is a value one layer hands to the next — application ToS, socket priority, user priority, traffic class — until a field on the wire reaches a switch that has been told which field to read.[2] Each handoff belongs to a different team, which is how all of them report success while the mark is gone.
A chain reports success at every link that still works. You have ninety minutes and one sysfs file that re-marks the flows without touching their binary.[1] Start where the chain starts.
1Marking is a chain, and every link can break
NVIDIA documents RoCE QoS as a four-step chain, not a setting. The application sets a ToS with rdma_set_option(..., RDMA_OPTION_ID_TOS, value); a fixed kernel table converts that ToS into a socket priority; tc (or the VLAN egress map) converts the socket priority into a user priority; and mlnx_qos or lldpad converts the user priority into a traffic class on the NIC.[2] The plain-Ethernet chain is the same shape and differs only at step 1, where the application calls setsockopt(IP_TOS, value), and at step 3, where the VLAN egress_map does the work.[2]
The first link is the one that fails most often, and it fails silently: “If a TOS is not set, the default value (0) is used”.[1] A flow with ToS 0 is DSCP 0, priority 0, and it will sit in the default queue on a fabric where every switch was configured perfectly for priority 3. NCCL behaves the same way — NCCL_IB_TC defaults to 0 and NVIDIA’s guidance is only to “set NCCL_IB_TC per vendor documentation”, so an unset variable is an unmarked training job.[7]
The second link has a hard ceiling that surprises people. The ToS-to-socket-priority table is fixed: TOS 0 <=> sk_prio 0, TOS 8 <=> sk_prio 2, TOS 24 <=> sk_prio 4, TOS 16 <=> sk_prio 6.[2] Because that table is fixed, “With RoCE, there can only be 4 predefined ToS values for the purpose of QoS mapping.”[2] The documented escape hatch is to skip the ToS entirely: “Socket applications can use setsockopt (SK_PRIO, value) to directly set the sk_prio of the socket. In this case, the ToS to sk_prio fixed mapping is not needed.”[2]
Diagram source (Mermaid)
flowchart LR; A["1. App: rdma_set_option ID_TOS - default 0"] --> B["2. Fixed table: ToS 0/8/24/16 to sk_prio 0/2/4/6"]; B --> C["3. sk_prio to UP: tc filter or VLAN egress_map"]; C --> D["4. UP to TC: mlnx_qos -p or lldpad"]; D --> E["Wire: DSCP always present - PCP only if VLAN tagged"]; E --> F["Switch trust state picks which field it reads"]; A -. "setsockopt SK_PRIO skips the four-value limit" .-> C; G["Force DSCP: sysfs traffic_class"] -. "re-marks without an app change" .-> E
2The arithmetic, quoted exactly
Three formulas carry the whole chain, and all three are on NVIDIA’s RoCE page. DSCP to ToS: “The ToS field is 8 bits, while the DSCP field is 6 bits. To set a DSCP value of X, you need to multiply this value by 4 (SHIFT 2). For example, to set DSCP value of 24, set the ToS bit to 96 (24x4=96).”[1][3] ToS to SL: “SL = TOS >> 5 (e.g., take the 3 most significant bits of the TOS field)”.[1] SL to PCP: “PCP = SL & 7 (take the 3 least significant bits of the TOS field)”.[1]
Run the numbers for the three DSCP values you will meet on an AI fabric. DSCP 26 is the Cumulus default for RoCE data and gives ToS 104; DSCP 48 is the default for CNP and gives ToS 192; DSCP 24 is what the Spectrum-X host profile uses and gives ToS 96.[5][8][1] Neither ToS 96 nor ToS 104 is an entry in the fixed table - its only entries are ToS 0, 8, 24 and 16 - so on the host neither DSCP 24 nor DSCP 26 reaches a socket priority through the ToS-to-sk_prio path at all.[2] Both depend instead on DSCP trust plus the NIC’s dscp2prio map, which is why the Spectrum-X host profile sets Trust: dscp alongside its ToS of 96; the profile states the values and gives no rationale for choosing 24 over 26.[8]
The last formula only matters on tagged frames. NVIDIA states it twice: “SL affects the PCP only when the traffic goes over tagged VLAN frames”, and separately, “VLAN tagged Ethernet frames carry a 3-bit priority field. The value of this field is derived from the IB SL field by taking the 3 least significant bits of the SL field”.[1] On an untagged routed fabric — which is what a rail-optimized AI network normally is — PCP is not a weak signal, it is an absent one.
1. App sets a ToS
Carried by: nothing on the wire yet — this lives in the address vector
RDMA-CM apps set it per connection with rdma_set_option(..., RDMA_OPTION_ID_TOS, value); "If a TOS is not set, the default value (0) is used." The CM default can also be moved without touching code: cma_roce_tos -d <ib_device> -t <TOS>, or the RDMA-CM configfs entry. An app that calls rdma_set_option still overrides both.
ib_write_bw -T 104 -R # CM path ib_write_bw --tclass=104 # AV path, no CM # no code change at all: echo "tclass=104" > /sys/class/infiniband/mlx5_0/tc/1/traffic_class echo "tclass=16,src_ip=1.1.1.2,dst_ip=1.1.1.0/24" > /sys/class/infiniband/mlx5_0/tc/1/traffic_class echo "tclass=-1" > /sys/class/infiniband/mlx5_0/tc/1/traffic_class # delete
Failure mode #1 of three: the app never set a ToS, so the traffic is DSCP 0 / priority 0 no matter how good the switch config is. Force DSCP via .../tc/<port>/traffic_class proves it in one command — global wins over rules, negative values disable, valid range 0–255. The sibling file is ttl.
trust dscp with DSCP 26 on the wire: the NIC classifies on the IP header and the default dscp2prio table puts it on priority 3. This is the configuration NVUE expects on the switch side.
Counter that proves it: ethtool -S <netdev> | grep prio3 climbing, and rx_prio3_pause / tx_prio3_pause telling you which side is pausing.
⚠ 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.
3The three places the mark disappears
Every “the switch is not putting my RoCE in the right queue” ticket lands on one of three causes. First, the application never set a ToS, so the flow is DSCP 0 and priority 0 no matter what the fabric does.[1] Second, the host or the switch is in trust pcp while the fabric is DSCP-based, so the DSCP the application set is ignored on the way in — PCP is the default trust state on the NIC, so this is the state you get by not choosing.[2] Third, there is no VLAN tag, so PCP never appears on the wire at all and a PCP-trust device has nothing to read.[1]
Those three are distinguishable with one capture and one command. A capture of UDP port 4791 shows the DSCP the frame actually carries, which separates cause one from causes two and three; mlnx_qos -i <interface> prints the host’s Priority trust state line, which separates cause two from cause three.[2] Note the ordering: the capture is the cheap test and it is decisive for the most common cause, so it goes first.
The default DSCP-to-priority table on the NIC matters here because it is already sensible and people re-invent it. Reading it off the canonical mlnx_qos output: DSCP 0-7 map to priority 0, 8-15 to 1, 16-23 to 2, 24-31 to 3, 32-39 to 4, 40-47 to 5, 48-55 to 6, and 56-63 to 7.[2] That is why DSCP 26 lands on priority 3 and DSCP 48 lands on priority 6 with no --dscp2prio configuration at all, and it is exactly the split Cumulus uses on the switch side: RoCE data on switch priority 3 with DSCP 26, CNP on switch priority 6 with DSCP 48.[5]
4Re-marking without touching the application
When the application cannot be changed — a customer binary, a container image under change control, a benchmark you did not write — NVIDIA exposes a driver-level override. A global traffic class is written to /sys/class/infiniband/<dev>/tc/<port>/traffic_class; “Negative values indicate that the feature is disabled” and “Valid values range between 0 - 255”.[1] The rule form takes source and destination IPs, verbatim from the page:[1]
echo "tclass=16,src_ip=1.1.1.2,dst_ip=1.1.1.0/24" > /sys/class/infiniband/mlx5_0/tc/1/traffic_classThe tclass= prefix is optional, a mask may be given on the destination IPv4, and "tclass=-1" removes a rule from the database.[1] Precedence is stated and is not what most engineers assume: a global traffic class wins over all rules; otherwise a rule matching both source and destination IP applies; rules matching only one of the two “have no defined precedence”; “The rule precedence is not affected by the order in which rules are inserted”; and “Overlapping rules are entirely up to the administrator.”[1] A sibling file, .../tc/<port>/ttl, forces the TTL on RC QPs with the same 0-255 range.[1]
For test traffic you have two marking paths in perftest and they are not interchangeable. -T, --tos=<tos value> is documented as “Set <tos_value> to RDMA-CM QPs. available only with -R flag”, so it exercises the CM path; --tclass=<value> is “Set the Traffic Class in GRH (if GRH is in use)” and marks on the address-vector path without the CM.[4] Running both against the same peer is a clean way to show a customer that their application’s problem is the CM’s default ToS and not the fabric.
Symptom: a customer’s inference service pushes RoCE at line rate but the leaf’s priority-3 queue counters never move. You have SSH on both hosts and read access to the switch.
- Capture on the sender and read the outer IP ToS byte. A RoCEv2 frame is UDP with destination port 4791, so the filter is unambiguous:[1]
Expected on a correctly marked flow:sudo tcpdump -i ens1f0 -nn -v -c 20 udp port 4791tos 0x68in the IP header, which is 104 decimal, which is DSCP 26.[1] If you seetos 0x0, the application never marked and you can skip every switch question. - Read the host trust state and the DSCP map:[2]
Expected: amlnx_qos -i ens1f0Priority trust state:line readingdscporpcp, and the eight-linedscp2prio mapping:block. On defaults, DSCP 24-31 maps to priority 3.[2] - Decide from the two artifacts. ToS 0 on the wire means cause one - the application. Correct ToS with
trust pcpon an untagged frame means causes two and three together - the mark is present but nothing is reading it. Correct ToS withtrust dscpmeans the host is clean and the investigation moves to the switch’s own DSCP map.[2][5] - Prove it without a code change. Set a global traffic class of 104 on the device and re-run the same unmodified workload:[1]
Expected: the capture in step 1 now showscat /sys/class/infiniband/mlx5_0/tc/1/traffic_class # record the before value first echo 104 | sudo tee /sys/class/infiniband/mlx5_0/tc/1/traffic_classtos 0x68for the same application. That single line converts an argument into a measurement. - Roll back. Restore the recorded value, or disable the feature entirely by writing a negative value, which NVIDIA defines as “disabled”:[1]
echo -1 | sudo tee /sys/class/infiniband/mlx5_0/tc/1/traffic_class - Report the fix in the customer’s own terms: either the application must call
rdma_set_optionwith ToS 104 (or setNCCL_IB_TCif it is NCCL), or the override stays as a documented host setting.[1][7]
- Capture:
sudo tcpdump -i ens1f0 -nn -v -c 20 udp port ____. The hex ToS you want for DSCP 26 is0x____because 26 x ____ = 104. - Host state:
mlnx_qos -i ens1f0. Read the____ ____ ____:line and thedscp2prio mapping:block. By default DSCP 24-31 maps to priority ____. - Decide: ToS 0 on the wire implicates the ____; a correct ToS with trust
____on an untagged frame implicates trust state; a correct ToS with trustdscpsends you to the ____. - Override without a code change: record the current value, then
echo ____ | sudo tee /sys/class/infiniband/mlx5_0/tc/1/traffic_class, and re-run the identical workload. - Roll back by writing ____ , which NVIDIA defines as disabling the feature.
- Name the permanent fix:
rdma_set_optionwith ToS ____ , or____for a NCCL job.
A Dell customer runs two workloads on the same pair of hosts. Workload A is a ib_write_bw run started with -R -T 104; workload B is a containerized NCCL job with no NCCL variables set. On the wire, A shows tos 0x68 and B shows tos 0x0. The host reports Priority trust state: dscp. The customer concludes “RoCE marking is broken for containers”.
Write the diagnosis and the change plan. Acceptance criteria: (a) you name which link in the four-step chain differs between A and B and why the container has nothing to do with it; (b) you state the exact variable and value that fixes B, and quote its documented default; (c) you give the no-code-change override command that would let the customer prove the fix tonight, including where you read the before value and the exact rollback write; (d) you state one reason the override is a diagnostic and not the recommended permanent fix; (e) you name the one capture line you would attach to the ticket as evidence.
Episode 1 — Ninety minutes later
You write a global traffic class into /sys/class/infiniband/<dev>/tc/<port>/traffic_class on both hosts, re-run the capture, and the outer ToS byte reads 104 — DSCP 26, which the default host map puts on priority 3.[1][2] The leaf’s priority-3 counters move for the first time; no switch changed and their binary was never touched. What you say in the room: the fabric was configured correctly, the application never marked, and the mark belongs in the code or in NCCL_IB_TC before acceptance.[7] The network lead writes one line in her grid-paper notebook, then asks why one direction of that same pair still runs at half rate.
Lab
Goal: on the Dell-lab ConnectX or BlueField-3 pair, prove the override path end to end. Pre-flight first; every mutating step names its rollback.
- Pre-flight inventory, read-only. Record all of this into the ticket before changing anything:
Expected:ibdev2netdev ibv_devinfo -d mlx5_0 | grep -E "link_layer|active_mtu|fw_ver" mlnx_qos -i <if> > /tmp/qos-before.txt cat /sys/class/infiniband/mlx5_0/tc/1/traffic_class cat /sys/class/infiniband/mlx5_0/tc/1/ttllink_layer: Ethernet, and a traffic_class value that is negative (feature disabled) on a stock host.[1] - Baseline capture with an unmarked run: server
ib_write_bw -d mlx5_0, clientib_write_bw -d mlx5_0 <server-ip>, andsudo tcpdump -i <if> -nn -v -c 10 udp port 4791alongside. Expectedtos 0x0.[1] - Mutating - global Force DSCP. Rollback: rewrite the value recorded in step 1, or
-1if it was negative.[1]
Expected:echo 104 | sudo tee /sys/class/infiniband/mlx5_0/tc/1/traffic_class # re-run the SAME unmodified ib_write_bw command and capture again # rollback: echo -1 | sudo tee /sys/class/infiniband/mlx5_0/tc/1/traffic_classtos 0x68on an application you did not change. If it does not change, check that you wrote to the port that carries the traffic — the path istc/<port>/, and port 1 is not always the one in use.[1] - Mutating - rule-based Force DSCP, scoped to one peer. Rollback is the same rule with
tclass=-1:[1]
Expected: only traffic to that peer is re-marked. Confirm the documented precedence by leaving the global value disabled during this step — a global value would win over the rule.[1]echo "tclass=104,src_ip=<local-ip>,dst_ip=<peer-ip>/32" | sudo tee /sys/class/infiniband/mlx5_0/tc/1/traffic_class # rollback: echo "tclass=-1,src_ip=<local-ip>,dst_ip=<peer-ip>/32" | sudo tee /sys/class/infiniband/mlx5_0/tc/1/traffic_class - Compare the two marking paths on the same pair, read-only:
ib_write_bw -R -T 104 <server-ip>(CM path) againstib_write_bw --tclass=104 <server-ip>(AV path).[4] Expected: the same ToS on the wire by two different mechanisms. Note which one your customer’s application actually uses — an application on RDMA CM inherits the CM’s ToS, not the AV’s.[1] - Close out: confirm
cat /sys/class/infiniband/mlx5_0/tc/1/traffic_classis back to the step-1 value and thatmlnx_qos -i <if>still matches/tmp/qos-before.txt.
Goal: see every field of the chain on a Soft-RoCE pair with no NVIDIA hardware. Steps 1-4 are read-only; step 5 adds and removes a VLAN sub-interface.
- Bring up Soft-RoCE on both containers or hosts and confirm the device exists:
Expected:sudo modprobe rdma_rxe sudo rdma link add rxe0 type rxe netdev eth1 ibv_devinfo -d rxe0 | grep -E "link_layer|active_mtu"link_layer: Ethernet. If the device does not appear, check thatrdma_rxeloaded and thateth1is up. Note that the course research flags Soft-RoCE counter fidelity as unconfirmed, so treat rxe as a wire-format lab, not a counter lab. - Run an unmarked transfer and capture it. Server:
ib_write_bw -d rxe0. Client:ib_write_bw -d rxe0 <server-ip>. While it runs, on the server:sudo tcpdump -i eth1 -nn -v -c 10 udp port 4791. Expected:tos 0x0in the IP header — this is the DSCP-0 baseline.[1] - Repeat with an explicit traffic class:
ib_write_bw -d rxe0 --tclass=104 <server-ip>.[4] Expected:tos 0x68. If the value does not change, confirm the GRH is in use on this path —--tclassis documented as setting the traffic class in the GRH “if GRH is in use”.[4] - Do the arithmetic by hand and verify each on the wire: DSCP 24, 26 and 48 give ToS 96, 104 and 192.[1] Compute
SL = TOS >> 5for each (3, 3 and 6) and predict the PCP you would see if the frame were tagged.[1] - Add a VLAN and show PCP appear (mutating on a lab interface only; rollback in the same step):
Expected: the capture now shows an 802.1Q tag with a priority field; without the VLAN interface there is no PCP to show, exactly as NVIDIA states.[1] If the traffic still leaves untagged, you are not sourcing from the VLAN IP — NVIDIA’s own note is that a run whose IP does not belong to a VLAN interface goes without PCP or VLAN.[1]sudo ip link add link eth1 name eth1.7 type vlan id 7 sudo ip addr add 10.7.0.1/24 dev eth1.7 && sudo ip link set eth1.7 up # run the client against the VLAN IP, capture with: sudo tcpdump -i eth1 -nn -e -c 10 vlan # rollback: sudo ip link del eth1.7
Retrieval check
10 questions from memory. Answer before looking anything up; misses become flashcards.
Explain it to a Dell SE
Explain to a Dell platform engineer, in five sentences, why a RoCE application that 'set QoS' can still land in the default switch queue, and name the one command you would run first to prove which link in the chain broke.
Sources
Facts in this lesson were checked against DOCA-Host RoCE and Ethernet QoS pages (HTML + generated PDF), perftest master man page, Cumulus Linux 5.18 RoCE page, NCCL 2.31.2 env reference, 2026-09-07. Dates are when each page was fetched.
- RDMA over Converged Ethernet (DOCA-Host) - generated PDF · fetched 2026-09-07 · DOCA 3.5.0
- Ethernet QoS (DOCA-Host) - generated PDF · fetched 2026-09-07 · DOCA 3.5.0
- RDMA over Converged Ethernet (DOCA-Host, current) · fetched 2026-09-07 · DOCA 3.5.0
- perftest(1) man page (linux-rdma/perftest master) · fetched 2026-09-07
- RDMA over Converged Ethernet - RoCE (Cumulus Linux 5.18) · fetched 2026-09-07
- Quality of Service (QoS) - MLNX_OFED 5.9-0.5.6.0 · fetched 2026-09-07
- Environment Variables - NCCL 2.31.2 documentation · fetched 2026-09-07
- Spectrum-X Configuration - NVIDIA NIC Configuration Operator · fetched 2026-09-07
The same idea elsewhere
Other lessons that cover this ground, sometimes from another course's angle.
- How a RoCE packet gets classifiedSpectrum-X course · Same ground: dscp, sysfs and tos
- RoCE, GID tables and the NCCL knobs that read themK8sNet course · Same ground: sysfs, NCCL_IB_TC and triage
- GIDs: how a connection picks its RoCE versionElsewhere in this course · Same ground: sysfs, perftest and NCCL_IB_TC