Skip to content

Scenario: "the 400G NIC only does 180 Gb/s"

S5·E2Two numbers, six people, and no command line · Bridge call, 22:10, the night the 18:40 mail lands

S5·E2Evaluate~35 minsources checked todayverified against linux-rdma/perftest master (man page and README), nccl-tests PERFORMANCE.md and NCCL 2.31.2 docs, 2026-09-07

Builds on: What ib_write_bw actually measures, Triage in order: symptom to counter to cause

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

  • Evaluate a customer perftest result against the six checks that explain most 400G bandwidth complaints.
  • Request evidence in the order that costs the least credibility and rules out the most causes.
  • Judge whether a two-node NCCL busbw collapse is a fabric fault or a GPUDirect fault using the reported number alone.
  • Produce a written finding that separates what was measured from what was inferred and names one change.

Episode 2 — Two numbers, six people, and no command line

The situation · Bridge call, 22:10, the night the 18:40 mail lands

Six people are on the bridge and nobody has a command line. The customer’s platform lead reads the 18:40 mail out loud: 180 on a 400G link, and the NCCL busbw halving when the second node joins. The Dell SE says the word “fabric” twice and adds row 44 to his spreadsheet of promises. Someone offers a packet capture. The acceptance review is Friday at nine, and that number is the one you defend or retract.

Micro-benchmarks like perftest exist so an argument about an application can be preceded by a narrow, boring fact about the hardware. The tool says so itself: “The benchmarks generate a synthetic stream of operations… The benchmarks are not designed to emulate any real application traffic.”[1] The same README fixes who is even reporting the number - “On all unidirectional bandwidth benchmarks, the client measures the bandwidth” - and requires that “the same options must be passed to both server and client”, a rule the man page repeats.[1][2] None of that is on this call.

So you ask for the two command lines and the output header instead, and half the room hears a stall. It is not one. A run printing MiB/s where the ticket says 180 is a units problem, and --report_gbits decides it.[2] The network lead says the thing she always says - show me the counter - and tonight the honest answer is that counters are the second question.

A number without its command line is a rumour with decimals.

Start with what the ticket does not say.

1The ticket, and what it does not say

The mail says: “ib_write_bw between two nodes gives 180 on a 400G link, and our NCCL busbw halves when we add the second node.” Two numbers, no units, no command lines, no node names.

Before asking for anything, fix what perftest actually reports. “On all unidirectional bandwidth benchmarks, the client measures the bandwidth”, so the server-side terminal is not the number.[1] Latency tests “measure round-trip time but report half of that as one-way latency”, which “may not be accurate for asymmetrical configurations”.[1] And the sentence to keep in a snippet file for customers who treat a micro-benchmark as an application SLA: “The benchmarks generate a synthetic stream of operations… The benchmarks are not designed to emulate any real application traffic.”[1]

Two constraints decide whether the evidence is admissible at all. “The same options must be passed to both server and client” - stated in both the README and the man page.[1][2] And “Different versions of perftest may not be compatible with each other. Please use the same perftest version on both sides”, with the specific note that “Test version 5.3 and above won’t work with previous versions of perftest. As well as 5.70 and above.”[1] If the customer used --dont_xchg_versions to get past a mismatch, the run skipped the version and MTU exchange and the result describes two differently configured endpoints.[2]

SymptomBlueField-3 on a Dell Pow…SymptomDev / environment: build,…SymptomSample runs, exits cleanl…CheckAre hugepages allocated…
Symptom

Sample runs, exits cleanly, but zero throughput

Start at the throughput node and walk the branch the evidence supports. Each step names the command that decides it; do not skip a node because you already have a theory.

2The checklist, in the order that costs least

Six checks explain most 400G bandwidth complaints, and they are ordered by what the customer already has in hand.

One - units. --report_gbits reports “Max/Average BW of test in Gbit/sec (instead of MiB/sec). Relevant only for bandwidth.”[2] A run printing 22,400 MiB/s is about 188 Gb/s. That is the same “180” the ticket reported, and the fabric is fine.

Two - queue pairs. -q, --qp=<num of qp's> is “Num of qp’s (default 1). Relevant only for bandwidth.”[2] One QP is one flow, one entropy value on the ECMP hash, and one hardware scheduling context; ask for -q 4 to -q 8 before drawing conclusions.

Three - message size. -s, --size defaults to “65536 for bw, for lat 2” in the man page while the README’s common-options block says the default is 1 - a genuine conflict worth quoting both ways.[2][1] -a “Run sizes from 2 till 2^23” and gives a curve instead of a point.[2] A small-message run is a message-rate test wearing a bandwidth test’s name.

Four - the NUMA line in the header. “Perftest automatically detects the NUMA node of the IB device and binds the benchmark thread and memory allocations to that node… The active NUMA node is reported in the test output header”, and “If the NIC’s NUMA node cannot be determined, perftest continues without binding.”[1] It also “respects pre-existing CPU affinity set by external tools (e.g. taskset or numactl)”, in which case automatic binding is skipped.[1] That gives a clean A/B for a suspected NUMA regression: --numa_node=<id> against --disable_numa, which restores “the original behavior (OS scheduler decides placement)”.[1]

Five - GID and traffic class. -x, --gid-index picks the GID, and --tclass=<value> sets “the Traffic Class in GRH (if GRH is in use)” - the non--R path to a DSCP marking.[2] With -R, -T, --tos applies instead and is “available only with -R flag. values 0-256 (default off)”.[2] A run on the wrong GID or with no traffic class is a run through the lossy queue, which is a QoS finding, not a fabric-speed finding.[13]

Six - versions on both sides, as above.[1]

Two flags make the re-run defensible rather than repeatable-by-luck: -D, --duration with -f, --margin (default 2 s excluded at each end) instead of an iteration count, and -W, --report-counters=<list> which diffs sysfs counters across the run, for example counters/port_xmit_data,hw_counters/out_of_buffer.[2] Ask for one command line, run identically on both hosts, with those two flags.

3Counters second, and only on both hosts

When the command line is clean and the number is still low, the counters decide where to look, and the read order matters more than the counter list.

Start on the two hosts with the per-priority Ethernet counters. rx_prio[p]_pause is “The number of pause packets received with priority p on a physical port. If this counter is increasing, it implies that the network is congested and cannot absorb the traffic coming from the adapter”, and it is “available only if PFC was enabled on priority p”.[5] Its mirror, tx_prio[p]_pause, means “the adapter is congested and cannot absorb the traffic coming from the network”.[5] Direction identifies the victim, which is why you read both hosts before either switch.

Three counters change the diagnosis outright. rx_pause_ctrl_phy and tx_pause_ctrl_phy count link-layer, non-PFC pause; movement there while the customer believes PFC is configured means global pause is on and PFC is not.[5] tx_pause_storm_error_events counts the times pause transmission was disabled by timeout, “on the period where pause frames were disabled, drop could have been” - a lossless fabric that became lossy.[5] And rx_pci_signal_integrity is the one that ends the QoS conversation: “If this counter is raising, try moving the adapter card to a different slot to rule out a bad PCI slot. Validate that you are running with the latest firmware available and latest server BIOS version.”[5] That is the Dell escalation script, printed by the driver.

On the RDMA side, packet_seq_err and out_of_buffer come from the in-tree mlx5 driver along with the DCQCN four - np_ecn_marked_roce_packets, np_cnp_sent, rp_cnp_handled, rp_cnp_ignored.[10] The wording usually quoted for np_cnp_sent and rp_cnp_handled comes from a blog whose page returns 403 to automated fetch rather than from an NVIDIA page, so quote the behaviour, not the sentence.[14] Two mechanical traps: hw_counters refresh is capped by lifespan, “in milliseconds and defaults to 10”, with values 0 to 10000 writable, so a fast polling loop can report a frozen counter that is only cached; and port_xmit_data is “Total number of data octets, divided by 4 (lanes)”, so it must be multiplied by four before it is compared with a perftest figure.[6]

RDMA data — RoCEv2 UDP/4791 · priority 3 / DSCP 26CNP return path — priority 6 / DSCP 48 · np_cnp_sent here → rp_cnp_handled thereSenderreaction point (RP)counters/IB port counters1hw_counters/RoCE hardware counters (mlx5_ib)ethtool -SEthernet per-priority, physical and PCIe1rdma statisticiproute2 rdma tool — the modern viewSwitchcongestion point (CP)PFC / RoCE port countersnv show interface <id> qos roce countersRoCE mode + DSCP mapprio 3 → TC3 → DSCP 26 · CNP prio 6 → DSCP 48Receivernotification point (NP)counters/IB port countershw_counters/RoCE hardware counters (mlx5_ib)1ethtool -SEthernet per-priority, physical and PCIerdma statisticiproute2 rdma tool — the modern view

Read order: rping smoke test → ethtool -S per-priority on BOTH hosts → the switch's PFC/ECN counters → hw_counters → a packet capture, last.

Counters on sender — click one to open it; numbered ones belong to the symptom below.
Symptom — pick what the customer reported

The 400G NIC only does 180 Gb/s

“perftest reports 180 on a 400G link, and NCCL busbw halves when a second node joins.”

  1. 1Read the command line before any counterSender
    ib_write_bw -d mlx5_0 -q 1 -s 65536      # what the customer actually ran

    What would you expect to see? Commit your diagnosis to reveal.

  2. 2Smoke test before measuringSender
    rping -s -a <server_ip> -V -C 10
    rping -c -a <server_ip> -S <client_ip> -V -C 10

    What would you expect to see? Commit your diagnosis to reveal.

  3. 3Sender
    ethtool -S enp1s0f0 | grep rx_prio3_pause

    What would you expect to see? Commit your diagnosis to reveal.

  4. 4Sender
    cat /sys/class/infiniband/mlx5_0/ports/1/counters/port_xmit_data

    What would you expect to see? Commit your diagnosis to reveal.

  5. 5Receiver
    cat /sys/class/infiniband/mlx5_0/ports/1/hw_counters/np_ecn_marked_roce_packets

    What would you expect to see? Commit your diagnosis to reveal.

Senderreaction point (RP)

rp_cnp_handled / rp_cnp_ignored increment here. This is where the rate is reduced.

RoCE counters are read on both hosts in the same window, with timestamps. A single host's dump cannot separate “the receiver never reported” from “the sender never reacted”.

Symptom loaded: the 400G NIC only does 180 Gb/s. Read the counters in the order the board lights them, commit a diagnosis, and only then reveal.

4The second node: is it the fabric or is it GPUDirect?

The other half of the ticket is NCCL. The arithmetic is small and settles the argument: “algbw = S/t”, and bus bandwidth applies a per-collective correction because “the theoretical peak algorithm bandwidth is not equal to the hardware peak bandwidth, usually depending on the number of ranks”.[3] For AllReduce, t = (S/B) * (2*(n-1)/n) so B = algbw * (2*(n-1)/n); ReduceScatter, AllGather and AllToAll use (n-1)/n; Broadcast and Reduce use 1 “because all data has to get out of the root rank”.[3] The sentence that turns the number into a diagnosis: “The bus bandwidth should reflect the speed of the hardware bottleneck: NVLink, PCI, QPI, or network.”[3]

So a drop at exactly two nodes is expected in kind - the bottleneck moved from NVLink to the fabric - and the question is only whether the new busbw matches the per-GPU rail bandwidth. Ask for n, the size sweep (-b 8 -e 8G -f 2), and whether correctness checking was on: -c defaults to 1 and -c 0 is the clean timing run.[12]

If the two-node number is far below the rail, split the fault in one step: run ib_write_bw host-memory between the same two hosts, then the same run with --use_cuda. NVIDIA’s own reference line is ib_write_bw --use_cuda=0 --use_cuda_dmabuf -d mlx5_0 -a -F --report_gbits -q 1 on the server with -n 5000 and the server IP on the client.[11] If the GPU number is roughly the host number, GPUDirect is on; if it is a fraction, it is bouncing through host memory and the fault is not the fabric.

The GPUDirect list, in order: NIC-to-GPU distance from nvidia-smi topo -m;[9] the peer-memory path (lsmod | grep nvidia, expecting modules such as nvidia_fs and nvidia_peermem, and the driver-install order trap where a GPU driver installed before the network drivers must be reinstalled);[11] memlock, where NCCL WARN Call to ibv_create_qp failed or ibv_reg_mr failed means memlock is not unlimited and the container equivalent is securityContext.capabilities.add: ["IPC_LOCK"];[7][11] IOMMU, checked with cat /proc/cmdline and dmesg | grep -i -E "iommu|dmar|default domain", where “Default domain type: Translated” is the concerning output;[8] and PCIe ACS, found with sudo lspci -vvv | grep ACSCtl.[8] If perftest itself fails with “Couldn’t allocate MR” in CUDA mode, retry with MLX5_SCATTER_TO_CQE=0.[1]

Four NCCL variables account for most of the rest. NCCL_IB_DISABLE “Prevents IB/RoCE transport; forces fallback to IP sockets” and defaults to 0.[4] NCCL_IB_TC defaults to 0, so unmarked traffic lands in the default queue no matter what priority 3 was configured for.[4] NCCL_IB_GID_INDEX defaults to -1, and current guidance is explicit: “NCCL 2.21+: GID index selected dynamically; do not set NCCL_IB_GID_INDEX.”[7] NCCL_IB_QPS_PER_CONNECTION defaults to 1 and is “Useful for multi-level fabrics requiring routing entropy” - the same single-flow problem as -q 1, one layer up.[4] For evidence, NCCL_DEBUG=INFO with NCCL_DEBUG_SUBSYS=INIT,NET,GRAPH is the right first shot, written to NCCL_DEBUG_FILE with %h and %p so ranks do not interleave.[4]

5The deliverable: measured, inferred, one change, one test

A finding is not a diagnosis - it is a document the customer can act on without you in the room. Four sections, under 300 words.

Measured. The exact command lines from both sides, the exact output including the NUMA header line, the counter deltas with the commands that read them, and the conversion arithmetic if units were involved. Anything you did not see with your own eyes goes in the next section instead.

Inferred. The chain of reasoning from the measurements to the cause, with each step attributable to a documented behaviour: -q 1 is documented as the default and is documented as bandwidth-only; rx_prio3_pause is documented as the network pausing the adapter.[2][5]

The one change. One. Not a tuning list. If the evidence supports two changes, the finding is not finished; run the A/B that separates them.

How the customer verifies it. The exact re-run, the number that means success, and the counter that must stay flat. This is what makes the finding survive contact with a team you will never meet.

Escalation: DPU disappears after BIOS update · decision 1/6A 0 · P 0 · S 0

Brief — Customer's R760 with a BlueField-3 B3220 (DPU mode) lost the DPU after a BIOS update. Host sees nothing. They are about to reflash the DPU firmware "to be safe".

Dell L2 support engineer, bridge call with the customer: We are about to reflash the DPU firmware from the host — go ahead?

Escalation drill: the conversation shape of an FAE escalation - triage before blame, evidence before commitment. The technical subject differs from this lesson; the ordering discipline is the transferable part.

Episode 2, closing — The 02:10 call

How it ended

The re-run arrives at 07:04 with both command lines attached: units in MiB/s, queue-pair count at its default of one. With --report_gbits and -q 8 the pair does what the link promises.[2] The two-node NCCL drop survives, and it is supposed to: busbw reflects whichever hardware is the bottleneck, and a second host moves that bottleneck from NVLink to the network.[3] The finding goes out in four sections, under 300 words.

What you say at the morning check-in: “Nothing was broken. Your first number was one queue pair reported in MiB/s.”

The next night, the 02:10 call is the real news: the colo team will not carry PFC on hops it shares with other tenants - and the SE has a slide promising no switch configuration.

From a two-line ticket to a finding

Ticket: “ib_write_bw = 180, 400G link. NCCL busbw halves at 2 nodes.”

Round 1 - ask for the exact command line and full output from both sides, nothing else. Received: server ib_write_bw -d mlx5_0, client ib_write_bw -d mlx5_0 -x 3 10.0.3.11. Two problems are already visible without any new data: no --report_gbits, so the number is MiB/sec; and -x 3 on the client with no -x on the server, which violates the rule that the same options go to both sides.

Round 2 - one re-run, both sides identical: ib_write_bw -d mlx5_0 -x 3 --tclass 104 -q 8 -s 65536 -D 30 -f 5 --report_gbits -F -W counters/port_xmit_data. Result: 372 Gb/s. The original 180 was 22,400 MiB/s with one QP.

Round 3 - the NCCL half. Ask for n, the sweep and -c. Received: 16 ranks, -b 8 -e 8G -f 2 -c 0. Two-node busbw at 8 GB sits near the per-GPU rail bandwidth, so this is the expected bottleneck move, not a regression. Confirm with the split test: ib_write_bw host memory versus --use_cuda=0 --use_cuda_dmabuf between the same pair - both within a few percent, so GPUDirect is active.

The finding. Measured: both command lines, both outputs, the 22,400 MiB/s to 188 Gb/s conversion, the 372 Gb/s re-run, the host-versus-CUDA pair. Inferred: the original figure was a single-QP run reported in MiB/sec; the NCCL drop is the documented move of the bottleneck from NVLink to the network. One change: standardise the benchmark command line - identical on both sides, -q 8, --report_gbits, -D 30 -f 5. Verification: the customer re-runs that line and expects a figure above 350 Gb/s with rx_prio3_pause flat on both hosts.

What is explicitly not claimed: that the fabric will sustain this under their training job, because perftest is documented as not emulating real application traffic.

Lab

Pre-flight inventory (read-only): ibv_devinfo -d mlx5_0, ibdev2netdev, ofed_info -s on both Dell-lab hosts, and show_gids to record the GID index you intend to use. Record perftest’s version on both sides. Nothing in this lab writes configuration; every difference is introduced with a command-line flag on a single run.

  1. Baseline. Run the identical line on both hosts: server ib_write_bw -d mlx5_0 -x <v2-index> --tclass 104 -q 8 -D 30 -f 5 --report_gbits -F, client the same plus the server IP. Expected: a stable Gb/s figure near line rate. If it is not stable, fix that before injecting anything.
  2. Inject fault A on one run only: drop --tclass and use a GID index belonging to the RoCEv1 entry. Expected: a lower or unstable number. Rollback: none needed - the change lives only in that command line; the next run without the flag restores the baseline.
  3. Hand the run to a colleague as a black box: they see only the output, not your command. Their job is to reach the cause from counters alone, using ethtool -S per-priority counters on both hosts and grep . /sys/class/infiniband/mlx5_0/ports/1/hw_counters/*. Expected: they identify direction from rx_prio3_pause versus tx_prio3_pause before they guess a cause.
  4. Reverse roles. Your colleague injects -q 1 or a MiB/s report and you triage blind. Expected: you reach “command line, not fabric” without asking for switch data.
  5. Capture the artifact: both command lines, both output headers including the NUMA line, and the counter deltas before and after each run - use -W counters/port_xmit_data,hw_counters/out_of_buffer so perftest diffs them for you.
  6. If a GPU is present, add the split test: ib_write_bw --use_cuda=0 --use_cuda_dmabuf against the host-memory run on the same pair. Expected: within a few percent of each other. If the CUDA run fails with “Couldn’t allocate MR”, retry the same line prefixed with MLX5_SCATTER_TO_CQE=0 and record that as a finding, not a fix.

Retrieval check

10 questions from memory. Answer before looking anything up; misses become flashcards.

Explain it to a Dell SE

Explain to a Dell account SE, in four sentences, why you asked the customer for their exact command line before you looked at a single switch counter.

14 flashcards for this lesson — 0 in deck. Spaced review lives at /review.

Sources

Facts in this lesson were checked against linux-rdma/perftest master (man page and README), nccl-tests PERFORMANCE.md and NCCL 2.31.2 docs, 2026-09-07. Dates are when each page was fetched.

  1. OFED Performance Tests README (linux-rdma/perftest master) · fetched 2026-09-07
  2. perftest(1) man page (linux-rdma/perftest master) · fetched 2026-09-07
  3. Performance reported by NCCL tests (nccl-tests doc/PERFORMANCE.md) · fetched 2026-09-07
  4. Environment Variables - NCCL 2.31.2 documentation · fetched 2026-09-07
  5. Linux kernel: mlx5 ethtool counters documentation · fetched 2026-09-07
  6. Linux kernel ABI: sysfs-class-infiniband (stable) · fetched 2026-09-07
  7. Networking Troubleshooting - NCCL 2.31.2 documentation · fetched 2026-09-07
  8. GPU Troubleshooting - NCCL 2.31.2 documentation · fetched 2026-09-07
  9. nvidia-smi documentation (topo subcommand) · fetched 2026-09-07
  10. Linux kernel - drivers/infiniband/hw/mlx5/counters.c · fetched 2026-09-07
  11. GPUDirect RDMA and GPUDirect Storage - NVIDIA GPU Operator (v26.7.0) · fetched 2026-09-07
  12. NVIDIA/nccl-tests README (master) · fetched 2026-09-07
  13. Ethernet QoS (DOCA SDK 3.5.0) · fetched 2026-09-07
  14. RoCEv2 Congestion Counters Explained (Oracle Linux blog) · fetched 2026-09-07

The same idea elsewhere

Other lessons that cover this ground, sometimes from another course's angle.