<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[System Design : Deep Dive]]></title><description><![CDATA[System Design : Deep Dive]]></description><link>https://paper2architecture.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Sat, 14 Nov 2026 23:13:20 GMT</lastBuildDate><atom:link href="https://paper2architecture.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[My RoCE testbed passed. It was lying.]]></title><description><![CDATA[When a server in a GPU cluster dies, everyone knows within seconds. The failures that hurt more are the quiet ones: a link that still works but has become…]]></description><link>https://paper2architecture.hashnode.dev/my-roce-testbed-passed-it-was-lying</link><guid isPermaLink="true">https://paper2architecture.hashnode.dev/my-roce-testbed-passed-it-was-lying</guid><dc:creator><![CDATA[Nathanael FETUE]]></dc:creator><pubDate>Mon, 02 Nov 2026 08:35:49 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/692e0ed542c4fa5c5a28685d/c4c1e769-c7b0-4b6a-9872-6dcdee7bb926.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2>Why I'm building this</h2>
<p>When a server in a GPU cluster dies, everyone knows within seconds. The failures that hurt more are the quiet ones: a link that still works but has become a bit slow, a NIC that keeps retransmitting. Nothing is down, no alarm fires, and the whole job slows down, because a collective such as an all-reduce finishes only when its slowest participant does. Disaggregated inference has the same weakness: the KV cache must cross the network between prefill and decode workers, and a slow path there is user-visible latency.</p>
<img src="https://cdn.hashnode.com/uploads/covers/692e0ed542c4fa5c5a28685d/2295983a-573e-4359-b3bb-90a9b5f8abda.png" alt="" style="display:block;margin:0 auto" />

<p><em>One slow link, and every GPU spends most of each step waiting.</em></p>
<p>These are gray failures, and I'm building AgentXDP, a per-node agent that measures how long RDMA communications take and points to the link that slows them down.</p>
<p>One limit up front: on real RDMA NICs, RoCE is processed in hardware and never reaches the host's network stack, so XDP sees nothing there. Production measurement has to come from NIC counters and from the communication libraries. XDP is for mirror ports, and for a test environment. This post is about that test environment.</p>
<img src="https://cdn.hashnode.com/uploads/covers/692e0ed542c4fa5c5a28685d/e50ab516-3c99-4782-94c8-100472a39d0e.png" alt="" style="display:block;margin:0 auto" />

<p><em>Sensors feed a core library; results leave through Prometheus and OpenTelemetry. Dashed parts are planned.</em></p>
<h2>Wall 1: Soft-RoCE won't move into a namespace</h2>
<p>My VM has no RDMA hardware, so I use Soft-RoCE (<code>rdma_rxe</code>), which implements RoCE v2 in software on top of any Ethernet interface: real UDP 4791 packets, real transport headers, real ACKs. The plan was two network namespaces joined by a veth pair, one rxe device on each side.</p>
<p>Creating rxe from inside a namespace "worked", but the device was not there:</p>
<pre><code class="language-console">$ sudo ip netns exec ns-a rdma link add rxe-a type rxe netdev veth-a
$ sudo ip netns exec ns-a rdma link show rxe-a/1
Wrong device name
$ sudo rdma link show
link rxe-a/1 state ACTIVE physical_state LINK_UP
</code></pre>
<p>The network interface was in <code>ns-a</code>; the RDMA device had been registered in the initial namespace. Moving it explicitly failed:</p>
<pre><code class="language-console">$ sudo rdma dev set rxe-a netns ns-a
error: Operation not supported
</code></pre>
<p>The kernel source explains it (<code>device.c</code>, upstream v7.0): moving an RDMA device resets every client, including user-space contexts, so the core requires the driver to be able to disassociate them by force.</p>
<pre><code class="language-c">if (!dev-&gt;ops.disassociate_ucontext || ib_devices_shared_netns) {
        ret = -EOPNOTSUPP;
</code></pre>
<p>rxe doesn't implement that operation. Its UDP sockets are also created in the initial namespace (<code>rxe_net.c</code>, upstream v7.0), which <code>ss</code> confirms: one IPv4 and one IPv6 socket on port 4791, owned by no process, shared by every rxe device.</p>
<p>So both endpoints had to live in the initial namespace. The catch: both addresses are now local, and the kernel delivers 10.10.0.1 → 10.10.0.2 through loopback, invisible to XDP on the veth. Policy routing fixes that. Source-based rules, limited to locally generated packets (<code>iif lo</code>), send each address out of its own veth. The <code>local</code> table lookup moves below them, the new rule added before the old one is removed so that SSH never drops, and <code>accept_local</code> lets each side accept a packet whose source is local.</p>
<pre><code class="language-console">$ ip rule show
10:     from 10.10.0.1 to 10.10.0.2 iif lo lookup 101
11:     from 10.10.0.2 to 10.10.0.1 iif lo lookup 102
100:    from all lookup local
$ ip route get 10.10.0.2 from 10.10.0.1
10.10.0.2 from 10.10.0.1 dev veth-a table 101
</code></pre>
<img src="https://cdn.hashnode.com/uploads/covers/692e0ed542c4fa5c5a28685d/e2ab5403-01e0-43cf-a1c1-6b7d4235ee87.png" alt="" style="display:block;margin:0 auto" />

<p><em>Both Soft-RoCE endpoints in one namespace, with policy routing across the veth pair. The red INPUT gates are the subject of Wall 2.</em></p>
<h2>Wall 2: the PASS that lied</h2>
<p>My acceptance test was a capture: during a short <code>ib_write_bw</code> run, <code>tcpdump</code> on veth-b must show RoCE v2 packets. It passed. Meanwhile, <code>ib_write_bw</code> reported zero completed iterations.</p>
<img src="https://cdn.hashnode.com/uploads/covers/692e0ed542c4fa5c5a28685d/7a2e1375-621c-4e82-a1b2-338d1e0300d3.png" alt="" style="display:block;margin:0 auto" />

<p><em>Zero iterations, and a passing test.</em></p>
<pre><code class="language-console">$ sudo scripts/testbed.sh traffic bw -D 5
...
 #bytes     #iterations    BW peak[Gb/sec]    BW average[Gb/sec]   MsgRate[Mpps]
 65536      0           0.000000            0.000000            0.000000
$ sudo scripts/testbed.sh verify
...
07:16:35.642102 IP 10.10.0.1.49476 &gt; 10.10.0.2.4791: UDP, length 4128
07:16:35.642158 IP 10.10.0.1.49476 &gt; 10.10.0.2.4791: UDP, length 4112
...
== PASS: RoCE v2 crosses veth-b
</code></pre>
<p>The connection setup had worked, both sides had exchanged queue pair numbers and GIDs, and then nothing.</p>
<p>The device counters split the problem in two. The sender's rxe counted packets sent and then retry errors. The veth counted every packet on both ends, and the receiver's rxe counted nothing. The packets crossed the wire and died somewhere between veth-b and rxe.</p>
<p>Reading rxe's receive path showed four checks (size, destination GID, header and queue pair, CRC) that drop a packet silently. I had four suspects and no evidence, so I stopped guessing and asked the kernel where packets were being freed. Every dropped packet goes through <code>kfree_skb</code>, whose tracepoint carries the calling function and, on recent kernels, a drop reason.</p>
<img src="https://cdn.hashnode.com/uploads/covers/692e0ed542c4fa5c5a28685d/17e37734-a07f-429e-badd-937e1b2b1c51.png" alt="" style="display:block;margin:0 auto" />

<p><em>Counting dropped packets by kernel function and drop reason.</em></p>
<pre><code class="language-console">$ sudo bpftrace -e 'tracepoint:skb:kfree_skb {
    @drops[ksym(args-&gt;location), args-&gt;reason] = count(); }'
Attaching 1 probe...


@drops[sk_stream_kill_queues, 106]: 2
@drops[nft_do_chain, 12]: 7611
</code></pre>
<p>Not rxe. <code>nft_do_chain</code> is netfilter, and reason 12, decoded from the tracepoint's format file, is <code>NETFILTER_DROP</code>:</p>
<pre><code class="language-console">$ sudo grep -o '{ 12, "[A-Z_]*" }' /sys/kernel/tracing/events/skb/kfree_skb/format
{ 12, "NETFILTER_DROP" }
</code></pre>
<p>The Oracle Cloud Ubuntu image ships an iptables INPUT chain that accepts established traffic, ICMP, loopback and SSH, and rejects everything else. My earlier ping went through the ICMP exception; perftest's TCP setup likely went through loopback. RoCE arriving on veth-b matched nothing and was rejected. The routing was fine all along.</p>
<img src="https://cdn.hashnode.com/uploads/covers/692e0ed542c4fa5c5a28685d/eb599903-68a5-4e1b-afb0-9bec7af93ae4.png" alt="" style="display:block;margin:0 auto" />

<p><em>tcpdump sits before the firewall: it saw every packet the firewall then dropped.</em></p>
<p>The fix is two rules, scoped to the test interfaces and never persisted, inserted by <code>up</code> and removed by <code>down</code>:</p>
<pre><code class="language-bash">iptables -I INPUT 1 -i veth-a -j ACCEPT
iptables -I INPUT 1 -i veth-b -j ACCEPT
</code></pre>
<p>And the acceptance test changed: the capture is still required, but <code>ib_write_bw</code> must also complete at least one iteration. With both conditions, it passes, and <code>down</code> puts the firewall and the routing rules back exactly as they were.</p>
<img src="https://cdn.hashnode.com/uploads/covers/692e0ed542c4fa5c5a28685d/3e10e035-a49e-4f84-8fb1-89dbf7138597.png" alt="" style="display:block;margin:0 auto" />

<p><em>The stricter test: packets on veth-b and completed RDMA writes.</em></p>
<h2>What I learned</h2>
<ul>
<li><strong>An RDMA device and its network interface are separate objects</strong>, each with its own namespace membership. Check the right one.</li>
<li><strong>"Operation not supported" is an answer.</strong> Reading the kernel code took minutes; retrying flag combinations would have taken an evening.</li>
<li><strong>A capture proves arrival, not delivery.</strong> tcpdump sees packets at the interface, before the firewall and the socket. A test that checks only what you can see on the wire will happily pass a broken system.</li>
<li><strong>Ask the kernel who dropped the packet.</strong> One <code>kfree_skb</code> one-liner replaced four hypotheses. eBPF is the subject of this project, and the first useful eBPF program I ran on it was a debugging tool.</li>
<li><strong>Your environment has policies you didn't write.</strong> The firewall came with the image.</li>
</ul>
<p>Next: an XDP program on veth-b that parses those RoCE v2 headers.</p>
]]></content:encoded></item></channel></rss>