The first ten parts measured throughput, concurrency, and a full datacenter loop on one Genoa + R9700 box. This part asks a sharper question: when an agent is working, which piece of hardware is actually busy — and which is just waiting. We profiled the same agentic pipeline end to end across three different workloads, swept concurrency from 1 to 32, and ran every sweep twice: once on a thread pool, once on a process pool. The answer is not one number. It is three completely different stories.
Every run executes the same shape of work — fetch a source, parse it, chunk it, retrieve against it, and (for the agentic case) ask the model. We changed only what the source is, because the bottleneck moves with the data, not the code.
| Case | Workload | Where the time should go |
|---|---|---|
| A | Local PDF books — parse, chunk, embed | On-core CPU. No network, no model. |
| B | Web articles — fetch + extract + chunk | Remote web servers. CPU only touches what arrives. |
| C | Agentic transcript Q&A — retrieve + reason | Remote LLM generation. CPU orchestrates, model thinks. |
Each case was swept at concurrency 1, 2, 4, 8, 16, 24, 32 on a Python ThreadPoolExecutor and again on a ProcessPoolExecutor. The thread-vs-process delta is the cleanest in-situ test for the GIL there is: if going to processes buys you throughput, you were CPU-bound and the GIL was the wall; if it changes nothing, you were waiting on something off-core.
Parsing and chunking local PDFs is pure on-core work. On threads, the GIL serializes it — throughput flatlines around 12 docs/s no matter how many workers you add. On processes, the same sweep climbs to ~21 docs/s and the EPYC saturates. This is the one case in the whole study where buying cores buys you results.
The latency view makes the GIL impossible to miss. As concurrency rises, thread-pool p50 index latency explodes from 4 ms to over 1000 ms — the threads are stacked up behind one interpreter lock. The process pool stays nearly flat, ending at ~22 ms because each worker owns its own interpreter.
Swap local PDFs for live web articles and the bottleneck jumps off the chip entirely. Now the work is dominated by waiting on remote servers. Threads and processes track each other almost exactly — both climb with concurrency as more requests overlap in flight, and the gap between them is statistical noise.
The real agentic workload — retrieve context, then ask a remote gateway model to reason over it — is dominated by one thing so completely that everything else disappears. Split the per-request latency into local CPU time versus remote LLM time and the local share is a rounding error: tens of milliseconds of CPU against five-to-twenty-five thousand milliseconds of generation.
| Concurrency | Throughput (it/s) | Local CPU (ms) | Remote LLM (ms) |
|---|---|---|---|
| 1 | 0.18 | 12.2 | 5,586 |
| 4 | 0.43 | 26.6 | 8,673 |
| 8 | 0.74 | 31.3 | 9,641 |
| 16 | 0.84 | 66.7 | 16,106 |
| 24 | 1.13 | 139.8 | 17,776 |
| 32 | 1.02 | 119.5 | 25,281 |
Overlay the EPYC utilization of each case's best configuration and the whole study collapses into one picture. Case A drives the cores to saturation. Case B nudges them as overlap grows. Case C barely registers — the CPU does a sliver of orchestration and then waits on a model that lives somewhere else.
| Case | Bottleneck | Thread vs process | GIL matters? | EPYC role |
|---|---|---|---|---|
| A · PDF books | Local CPU | Process ~1.8× faster | Yes — it is the wall | Saturates (~97%) |
| B · web articles | Network | Thread ≈ process | No | Mostly idle |
| C · agentic Q&A | Remote LLM | Thread ≈ process | No | Idle, then waits |
Across the realistic agentic workload, the dominant cost is remote generation latency — the same finding that shaped the datacenter blueprint in Part 10. The local silicon is sized correctly: the GPU has headroom to spare, the CPU saturates only on the one parse-heavy ingestion path, and the network and model are what you actually tune against. Profiling end to end is what tells you that, and it is cheaper than guessing.