SERIES Part 1 Part 2 Part 3 Part 4 Part 5 Part 6 Part 7 Part 8 Part 9 Part 10 Part 11
AMD Field AI Enthusiast · by Pradeep Nallimelli · Part 11 of 11+ · June 2026

Where the silicon actually spends its time.

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.

Case A · CPU-bound1.8×
ProcessPool over threads on PDF-book ingestion. The EPYC saturates to 97% and the GIL is the wall — this is the one case where more cores pays off.
Case B · network-bound≈0%
Thread-vs-process gap on web-article ingestion. Remote servers set the pace, the EPYC mostly idles, and the GIL is irrelevant.
Case C · LLM-bound99.8%
Of agentic Q&A latency is remote token generation. The local CPU does single-digit-millisecond work; the R9700 in this loop is never touched.
HOW WE PROFILED IT

One pipeline, three workloads, two pool types, seven concurrency levels.

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.

CaseWorkloadWhere the time should go
ALocal PDF books — parse, chunk, embedOn-core CPU. No network, no model.
BWeb articles — fetch + extract + chunkRemote web servers. CPU only touches what arrives.
CAgentic transcript Q&A — retrieve + reasonRemote 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.

The thread-vs-process gap is the diagnostic. You do not need a flame graph to know where the time goes. Run the same work on threads and on processes; the size of the gap tells you whether the silicon was the bottleneck or the bystander.
CASE A · THE CPU-BOUND ONE

PDF books: this is the workload that actually wants more cores.

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.

Throughput vs concurrency — threads vs processes (PDF-book ingestion)

Source: prof_pipeline thread/proc sweeps, local PDF corpus, Genoa EPYC 9684X. docs/s at steady state.

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.

p50 index latency vs concurrency — log scale (lower is better)

Source: same sweeps, p50 per-document index latency in milliseconds. Note the logarithmic y axis — the thread curve spans three orders of magnitude.
CPU-bound work is where the GIL is a tax and cores are the cure. ProcessPool delivered ~1.8× the throughput of threads and pushed the EPYC from ~55% to ~97% utilization. If your ingestion is local and parse-heavy, pay the process-spawn cost — you get it back.
CASE B · THE NETWORK-BOUND ONE

Web articles: the cores have almost nothing to do.

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.

Throughput vs concurrency — threads vs processes (web-article ingestion)

Source: prof_articles thread/proc sweeps. articles/s at steady state. The two curves overlap because the CPU is not the constraint.
When the curves overlap, the GIL is irrelevant. There is no point spawning processes for I/O-bound work — you pay the spawn cost and get nothing, because the threads were never fighting over the lock in the first place. They were all parked waiting on sockets. The EPYC sat mostly idle through the entire sweep.
CASE C · THE LLM-BOUND ONE

Agentic Q&A: 99.8% of the wall-clock is the model thinking.

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.

Per-request latency split — local CPU vs remote LLM, by concurrency (log scale, ms)

Source: agentic transcript Q&A sweep. Local CPU = parse + retrieve; Remote LLM = gateway generation (summarize + answer). Logarithmic y axis; the bars differ by ~1000×.
ConcurrencyThroughput (it/s)Local CPU (ms)Remote LLM (ms)
10.1812.25,586
40.4326.68,673
80.7431.39,641
160.8466.716,106
241.13139.817,776
321.02119.525,281
Data-integrity note. The agentic corpus was built from pre-fetched transcripts because YouTube blocks transcript pulls from datacenter IP ranges. The profiling above measures the retrieve-and-reason loop over that fixed local corpus — so the remote-LLM time is genuinely model generation, not transcript download. The bottleneck is the gateway, and the local R9700 is not in this path at all.
THE THESIS, IN ONE CHART

Same code. Three workloads. Three different pieces of silicon waiting.

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.

Mean EPYC utilization vs concurrency — the three workloads on one axis

Source: per-case CPU sampling at each concurrency level. Case A = ProcessPool, Cases B and C = ThreadPool (their best configs). Higher means the local CPU is the busy one.
CaseBottleneckThread vs processGIL matters?EPYC role
A · PDF booksLocal CPUProcess ~1.8× fasterYes — it is the wallSaturates (~97%)
B · web articlesNetworkThread ≈ processNoMostly idle
C · agentic Q&ARemote LLMThread ≈ processNoIdle, then waits
Pick your pool by where the time goes, not by habit. Defaulting everything to a process pool taxes your I/O- and LLM-bound stages with spawn overhead for no gain. Defaulting everything to threads throttles your one genuinely CPU-bound stage behind the GIL. Profiling the thread-vs-process gap, per stage, tells you exactly which lever to pull.
WHAT THIS MEANS FOR THE BOX

The R9700 was never the bottleneck. Neither, mostly, was the CPU.

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.

CPU-bound stages1
Local PDF ingestion — the only place to spend cores and process pools.
Off-core stages2
Web fetch and LLM reasoning — bound by network and gateway, not the box.
GPU in the agentic loopidle
Remote gateway served generation; the local R9700 had room to do more.