Task Failed Successfully: Saturating NIC and Disk Bandwidth
Task Failed Successfully: Saturating NIC and Disk Bandwidth
A journey from wondering "how" to wondering "why."
By Yao Meng | June 21, 2026 | 21 min read
#System #TLB #RDMA #io_uring
0. Task Failed Successfully
The era of Artificial Intelligence has arrived with a velocity that caught most of us off guard. Specifically, agentic coding has fundamentally overhauled my daily professional workflow.
To be perfectly candid, I haven't manually authored a single line of production code in quite some time. Despite this, the software I oversee continues to execute across clusters comprising hundreds of HPC servers, operating at peak efficiency.
Of course, abstaining from writing code (or even skipping a granular review) doesn't mean I'm simply randomly poking around like a "monkey typing" at a keyboard. The engineering rigor remains:
- Analyze complex requirements
- Refine architectural designs with the AI agent
- Develop prototype demos
- Execute mock experiments
- Study results from small-scale deployments
- Iterate on discovered bottlenecks
- Maintain a rigorous, comprehensive testing pipeline
The "Monkey Typing" Paradox: With AI and agentic coding, the velocity of development has accelerated. Often, the code is generated faster than a human can fully comprehend its internal logic.
Recently, I prompted my agent to optimize a system's performance. The AI rapidly pivoted the system from roughly 50% throughput to total saturation. This article isn't about why the AI "failed successfully," but rather a detailed walkthrough of the analysis and debugging process used to achieve this optimization.
Optimizing a Demo: 1 NIC and 8 Disks
To isolate the performance variables, let's abstract the system into a simplified model. We want to focus on the optimization rather than business logic.
The Architecture
The system consists of a single thread performing the following sequence:
- Issue 1 MiB random direct I/O reads across 8 NVMe drives.
- Transfer that data to a remote host using RDMA WRITE.
Hardware Specifications:
- Storage: 8 NVMe drives (Max throughput: per drive).
- Network: 1 NIC providing of bandwidth.
- IOMMU: Configured in passthrough mode (no translation for I/O devices).
The Implementation
The AI agent constructed a basic event loop:
Client Request Server polls RDMA CQ Submit reads via io_uring Poll CQEs RDMA WRITE back to client.
Baseline Performance
This setup is designed to eliminate interference. Every component—except the NIC we are testing—has significant headroom. The theoretical maximum for the NIC is (calculated as approx).
Performance Metrics Table:
| Inflight | GiB/s | avg | p50 | p90 | p99 | p99.9 |
|---|---|---|---|---|---|---|
| 4 | 8.87 | 440 | 430 | 519 | 632 | 759 |
| 8 | 15.56 | 501 | 486 | 616 | 784 | 955 |
| 16 | 22.69 | 688 | 670 | 850 | 1118 | 1436 |
| 32 | 22.53 | 1386 | 1384 | 1696 | 1934 | 2272 |
| 64 | 22.15 | 2821 | 2819 | 3133 | 3366 | 3691 |
Surprisingly, the system hits a ceiling at an I/O depth of 16, reaching only about half of the NIC's potential bandwidth.
The Deep Dive: Profiling the Bottleneck
To understand why we stalled at , I used perf to profile the system at an I/O depth of 16.
The flamegraph revealed that 81.62% of the total CPU cost was spent within io_submit_sqes.
The Cost of Direct I/O
Since the demo utilizes Direct I/O, the kernel must construct DMA metadata from the user-space buffer for the block device. The most expensive operations in this path are:
__bio_iov_iter_get_pages: Converts theiovintobiopages.pin_user_pages_fast: This is the critical step. It translates a virtual address range into an array ofstruct pagepointers and pins them to prevent the VM from reclaiming, migrating, or swapping them during DMA.folio_*updates: Because the NVMe device DMA-writes directly into user-space pages, those pages must be marked dirty. The kernel updates thefolio(a unified abstraction for physically contiguous pages), managing reference counts, locking, and mapping.
The Mathematical Burden
The overhead is staggering when scaled:
- Per Read: A read using pages involves pages.
- Per Second: At to reads/sec, the system processes:
If physical memory is fragmented, this triggers massive amounts of folio metadata updates and atomic refcount increments, leading to expensive cross-core cache-line ownership transfers.
The Solution: Registered Buffers
To eliminate the per-I/O cost of processing user-space buffers, we can use io_uring_register_buffers(3). This function allows us to register buffers in advance, shifting the metadata preparation out of the critical path.
What io_uring_register_buffers(3) does upfront:
- Validation: Checks address ranges, alignment, and length limits for the
iovecs. - Pre-Pinning: Performs the GUP (Get User Pages) process and pins the pages for the entire duration of the registration.
- Metadata Caching: Constructs and stores
io_mapped_ubufstructures on the kernel side.
By doing this, we bypass the expensive page-table walks and folio updates during every single I/O submission.