Benchmarking USB4 performance on Strix Halo

I’ve been benchmarking USB4/TB networking on a pair of Framework Desktop Strix Halo machines. Focused initally on bandwith, but more recently on latency.

Anyway, the usable bandwidth per link seems rather disappointing relative to the published 40Gbps specs. On Strix Halo specifically, I’ve been unable to get much over 10-11Gbps per link, per direction (Tx or Rx). I’ll get into the settings and tests in a followup-post, and I sure hope someone can point out a solution - but for nowit seems closer to a 10Gbps link than a 40Gbps one.

The latency side, however, seems quite a bit more promising. I had heard that USB4/TB networking was far inferior to Ethernet due to latency, but I’m measuring it as slightly faster than a direct P2P ethernet link, and a lot faster than connecting over Ethernet going via a shared switch (let alone if it goes >1 network hops).

Anyway, all of this only applies to Strix Halo. And maybe also varies based on OS (I used Fedora) and even kernel (I used 6.17 for these tests). But figured it might be interesting to folk.

3 Likes

Reserved for bandwidth test methodology & results

TL;DR? USB4 < Ethernet P2P << Ethernet switched

Connection Optimization Avg Latency (µs) Min Latency (µs) 99th %tile (µs)
USB4 Base 20.25 15.15 97.80
USB4 Partial 17.35 14.13 27.85
USB4 Full 19.56 17.41 24.07
Ethernet (P2P) Base 22.75 20.49 35.09
Ethernet (P2P) Partial 20.78 19.86 25.38
Ethernet (P2P) Base 21.11 20.68 24.63
Ethernet (Switched) None 37.44 30.79 136.84
Ethernet (Switched) Partial 31.77 29.35 46.29
Ethernet (Switched) Full 40.28 32.30 45.13

Connection details:
The latency tests performed under 3 different connection scenarions:

  1. USB4 P2P
  • Used 2x short (<2ft) TB5 cables, rated for 80Gbps/120Gbps
  • Connected to the USB4 ports on the backplate of the FWD motherboard
  • Static IPv4 assignments on a private subnet w. routing table
  • Firewall disabled (ports in the trusted zone), MTU=9000
  • Each test: 2x warmups, then 3x tests and combed the results
  • Ran the tests on both USB4s and combined results
  1. Ethernet P2P
  • RJ45 P2P connection on the FWD’s built-in 5GbE networking
  • Static IPv4 assignments on a private subnet w. routing table
  • Firewall disabled (ports in the trusted zone), MTU=9000
  • Each test: 2x warmups, then 3x tests and combed the results
  1. Ethernet switched
  • RJ45 to a Unifi Pro XG 8 PoE switch dedicated to the test
  • DHCP IPv4 assignments on my home network
  • Firewall disabled (ports in the trusted zone), MTU=9000
  • Each test: 2x warmups, then 3x tests and combed the results

Optimization details:
The latency tests performed under 3 different levels of kernel optimizations:

  1. Base
  • Networking core settings:
    net.core.busy_read=50
    net.core.busy_poll=50
    net.core.netdev_budget=600
    net.core.netdev_budget_usecs=5000
    net.core.default_qdisc=fq
    net.ipv4.tcp_congestion_control=bbr
  • AI shared mem:
    amdgpu.gttsize=126976
    ttm.pages_limit=32505856
  • NVMe perf:
    nvme_core.default_ps_max_latency_us=0
    elevator=none
  • CPU & IO perf:
    cpufreq.default_governor=performance
    transparent_hugepage=madvise
  1. Partial
  • The bulk of the benefit came from this level of optimization
  • Everything in #1 bucket plus:
    processor.max_cstate=1
    pcie_aspm=off
    usb4_dma_protection=off
    iommu=pt
  • Lowest min/avg, slightly higher P95 but a very good overall balance
  1. Full
  • Everything in #2 bucket but also idle=poll
  • All cores pinned at 100% => less jitter but higher min/avg
  • Power consumption baseline went from ~9W → 60W
1 Like

Test scripts for latency
Apologies for posting “AI slop”, but this is what I used on both client and server.

user@client:~/$ python3 latency_test.py <server-ip>>
user@server:~/$ python3 latency_test.py
import socket
import time
import sys

PORT = 5005
ITERATIONS = 1000

def run_server():
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
        s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        # 50 matches your sysctl net.core.busy_read
        try:
            s.setsockopt(socket.SOL_SOCKET, socket.SO_BUSY_POLL, 50)
        except Exception: pass
        
        s.bind(('0.0.0.0', PORT))
        s.listen(1)
        print(f"Server listening on {PORT}...")
        conn, addr = s.accept()
        with conn:
            for _ in range(ITERATIONS):
                data = conn.recv(1)
                if not data: break
                conn.sendall(data)

def run_client(server_ip):
    latencies = []
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
        # TCP_NODELAY is vital to avoid buffering delays
        s.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
        try:
            s.setsockopt(socket.SOL_SOCKET, socket.SO_BUSY_POLL, 50)
        except Exception: pass
        
        s.connect((server_ip, PORT))
        for i in range(ITERATIONS):
            start = time.perf_counter_ns()
            s.sendall(b'x')
            s.recv(1)
            end = time.perf_counter_ns()
            latencies.append((end - start) / 1000) # Convert to microseconds

    avg = sum(latencies) / len(latencies)
    print(f"\nResults for {server_ip}:")
    print(f"  Avg Latency: {avg:.2f} µs")
    print(f"  Min Latency: {min(latencies):.2f} µs")
    print(f"  99th Percentile: {sorted(latencies)[int(ITERATIONS*0.99)]:.2f} µs")

if __name__ == "__main__":
    if len(sys.argv) > 1:
        run_client(sys.argv[1])
    else:
        run_server()

Test scripts for throughput
This side is a bit more involved, I’ll share in a bit. The results aren’t that interesting - bigger pipe == better - the only really surprising thing was that AFAICT, Strix Halo USB4 seems unable to get over ~11Gbps in Tx or Rx.

I am expanding my test matric with 10GbE and 25GbE, just need to finish the testing.

1 Like

From observations of other systems and thunderbolt networking, try turning the MTU up to stupid levels, try 65536. There were some other changes needed too… Thunderbolt isn’t the best for TCP. Which is a bit strange considering.

Thanks for the reminder, yeah I did test MTUs of 1500 (default), 9000 (most common recommendation) and 65522 (max supported value). For each setting I validated the MTU had taken effect on both ends with a ping -s <<MTU-28>> -M do <<remote-IP>.

The larger MTU did give a consistent but very small increase in throughput (~1.4%). Ran warmup + 3 tests at each MTU, using Claude to collate and summarize:

MTU Aggregate (2 links) Per-Link Bidir Per Direction
1500 45.45 Gbps 22.72 Gbps ~11.36 Gbps
9000 45.77 Gbps 22.89 Gbps ~11.44 Gbps
65520 46.09 Gbps 23.05 Gbps ~11.52 Gbps

I realize that I neglected to mention the other tuning parameters in place for the tests apart from the kernel parameters mentioned above. I also have these settings active in all tests to date:

net.core.busy_poll=50
net.core.netdev_budget=600
net.core.netdev_budget_usecs=5000
net.core.default_qdisc=fq
net.ipv4.tcp_congestion_control=bbr

You mentioned other tweaks that can help TB performance, mind sharing more info on that?

1 Like

I really wish I remembered where I read on this recently. I think it was something about a CEO mini homelab or mini rack? There were three or four MS-01s and a mountain of tweaking to the thunderbolt mesh. I will say that if I’m remembering correctly he was getting 15gbit/link.

I am in the process of developing my own rdma driver for the Strix Halo - (GMKtec EVO-X2 128Gig) and I started out at about 7.5Gig transfer from one box to the next (I have 4 of them) using just the off the shelf software. I was planning on running them in a ring configuration.
I eventually got all the way down to the tunderbolt_net driver and came to the conclusion that it was the bottle neck. It looks like it only supported 4K frames. I hacked it up, tossed all the IP stuff, and started to make the frames bigger, then went to near zero copy.
It didnt take me long and I got to 41Gig? I couldn’t believe it? I was thinking maybe I was running in a 3 X 1 fat mode as I was only sending packets in one direction. I added a loop back mode so that the packets would come right back, was expecting the 1 lane to bottle neck everything it I was running in a 3 X 1. It didn’t. I got a full 41 Gig round trip.
I still didn’t believe it.
Long story short I now have a driver that is achieving 78Gb/s with a latency around 1.5uSec.
This PHY is USB4 V2 capable (I got to think it didn’t make specs somewhere).
Turns out that the firmware is reporting the wrong capabilities. If you ignore it and just let the phy go full tilt (at least to another Stix Halo box) it works great at 80 gig (or 40 gig a lane).
I am now at a point where I am trying to find somebody out there that is capable of confirming it or telling me where I am wrong?

Thanks!

3 Likes

If it is of any use, I am happy to help test this out in a couple configurations. I’m using an Infiniband link between my Strix Halos currently but have compared a pretty wide range of P2P connections including some not mentioned here including some more oddball ones like PCIe tunneling over USB4 and WiFiDirect. IB wins but its a giant PITA to get working especially on my Framework Desktops (their BIOS is flaky specifically w. Mellanox cards).

Your 78Gbps seems high given what PCIe 4.0x4 can do, and the USB4 ports only get 4 PCIe lanes each. Are you bonding the 2x USB4 ports, or is this counting traffic in both directions? 78Gbps in one direction would be stellar, that exceeds what 100Gbps IB/RoCEv2 can do, given the Gen4x4 limit.

1 Like

The PHY in the AMD Ryzen AI MAX+ 395 SOC? Who’s IP are they using since there aren’t many implementations of USB4 v2.0 besides Intel’s and Apple’s? :thinking:

Ah. That makes sense then. USB4 v1.0 is 40 gbps (nominal) in each direction.

Hey just wanted to drop a quick note. I’m on vacation right now but when I get back I’ll put a package together for you.
I am running chachyOS and have it setup to stay off of one of the processors so I can use the Rama driver to that one processor with no Linux in the way.

Let me know if you need os settings and such.

I don’t have the numbers off the top of my head but I am at around 50gb/s hooking into the rocm and am writing an IP layer so that I can load the silly deepseek model. Currently it take like 17min across the 2.5 gig Ethernet and I am hoping to get down to a couple min.

1 Like

The 78 gig is one direction from one box to the next. No IP. It’s just set up a really big mtu (I think 64k) and shoot it accross using zero copy dma.

When you say makes sense, what do you mean? v1 is two lanes of 20gig in each direction? How do you get to 41 gig round trip (and that includes overhead)?

Even if you assume fat mode (3 lanes of 20gig) you would only get 1 lane back and should only get 20gig round trip?

Am I missing something?

You mean you are actually claiming USB4 v2 support in AMD’s USB4 implementation? Intel’s discrete chip would certainly be bottle-necked at 64 gbps (before even subtracting encoding overhead).

Isn’t PCIe fully bi-directional?

Gen4 is 16 Gb/s per Lane and Gen5 is 32 Gb/s per lane and direction.

The USBv2 implementation of Minisforum MS-S1 (AMD Strix Halo) and MS-02 (Intel 285HX) is using Intel JHL9480 TB5 Controller (Barlow Ridge). As it is connected to PCIe Gen4 x4 it is limited to 64 Gb/s in each direction concurrently. As this is a limitation of the controller 80 Gb/s will never be reached in real world TB5/USB4v2 connections. You can this also see when testing transfer bandwidth to and from TB5 eGPU docks.

This is promising! I’ve confirmed we can push latency down quite a bit just by disabling interrupt throttling:

--- linux-7.0.3/drivers/thunderbolt/nhi.c.orig  2026-05-01 20:50:17.495818388 +0200
+++ linux-7.0.3/drivers/thunderbolt/nhi.c       2026-05-01 21:12:11.697865660 +0200
@@ -51,6 +51,10 @@
 module_param(host_reset, bool, 0444);
 MODULE_PARM_DESC(host_reset, "reset USB4 host router (default: true)");
 
+static int throttle = DIV_ROUND_UP(3 * NSEC_PER_USEC, 256);
+module_param(throttle, int, 0444);
+MODULE_PARM_DESC(throttle, "Interrupt throttling in 256ns increments");
+
 static int ring_interrupt_index(const struct tb_ring *ring)
 {
        int bit = ring->hop;
@@ -1038,7 +1042,7 @@
 static void nhi_enable_int_throttling(struct tb_nhi *nhi)
 {
        /* Throttling is specified in 256ns increments */
-       u32 throttle = DIV_ROUND_UP(128 * NSEC_PER_USEC, 256);
+       //u32 throttle = DIV_ROUND_UP(3 * NSEC_PER_USEC, 256);
        unsigned int i;
 
        /*

After loading this version (modprobe thunderbolt throttle=0), ping -f reports:

93590 packets transmitted, 93588 received, 0.00213698% packet loss, time 2283ms
rtt min/avg/max/mdev = 0.018/0.019/0.684/0.003 ms, ipg/ewma 0.024/0.020 ms

That’s <20µs round trip time, still using the very inefficient thunderbolt_net (which copies all data, calculates checksums and fragments in software). RXE (software RoCE) runs on this.

4 Likes

I’ve found my 7840u (I only have one 395, a Bosgame M5, which keeps me from testing for that 80Gbps) had one core saturated during bandwidth tests with irq throttling disabled. Still, setting it to something moderate like 64 does pretty well, compared to the stock 500. Also, that 0.7ms wakeup time can be reduced to about 0.2ms with /sys/devices/system/cpu/cpu*/power/pm_qos_resume_latency_us (should have the same effect as processor.max_cstate=1 mentioned above). Clearly more latency sources are around, but making slight progress on variables to tweak.

My tests as yet have stayed around 10-11Gbps, which is below the 20Gbps the firmware reports and the 40Gbps it should be capable of. I haven’t yet deepened the queues (thunderbolt_net hardcodes 256 frame deep rings), tried multiple rings, removed copying, etc. But I have confirmed the IP layers are costly.

1 Like

Hey man, can I test the driver too? Granted, I only have a X870E + Framework 16, but I still want to try before I get the Strix/Gorgon Halo hardware

3 Likes

Looks useful, but at first glance like it doesn’t strip out the copy, holding device dedicated kernel side rings of DMA buffers. It would be possible to improve for e.g. io_uring_register_buffers, sendfile or p2pdma if you plan your frame capacity, i.e. page sized datagrams, not streams. Perhaps the best match would be io_uring_prep_read_multishot with 4KiB buffers.

PS. Thunderbolt has a mode called raw mode, which has less hardware assistance. For each transfer descriptor, frame mode supports a 4KiB frame in 16 packets, while raw mode supports 256 bytes of payload in one transport layer packet. It’s a bit like control or interrupt vs bulk endpoints.

1 Like