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

R9700 does synthesis.
EPYC runs the agents.

The split is the design. One GPU at the back of the room writing structured knowledge. One EPYC Genoa CPU at the front doing every other job — ingestion, orchestration, transcription, embedding cache, schedule, dispatch — with agent processes pinned to specific CCDs so they never collide. Here is how the workload is divided in practice.

GPU roleSynth
All LLM tokens: summarize, verify, route, decide
CPU roleOrchestrate
Ingest, schedule, transcribe, embed, act, persist
CPU rolealways on
Genoa EPYC drives ingestion, embedding, scheduling, orchestration, speech and document pre/post-processing. Host load measures only the steady-state slice (p50 0.3%, p95 12.9% during batches) — the rest is reserved capacity for bursts, not wasted silicon.
HOW WE BUILT IT

From notebook to 24x7 production in six steps.

The order matters. Get the model serving stable first. Then ingestion. Then the brain. Then the agents. Then the frontends. Then the verifier loop. Each step earned its way in.

StepWhat it addedLesson
1vLLM + Qwen3-30B-A3B-AWQ on R9700MoE first (Part 2). Dense 14B was 6.8x slower. Start with the right shape.
2Outlook + Teams + shared documents ingestorsGet the data in before you teach the model what to do with it.
3the retrieval store (RAG) + the synthesis store (synth)Two layers. RAG for retrieval; synth for compressed wisdom. Different write patterns.
410 background agentsSchedule first, intelligence second. A cron with an LLM behind it is still mostly cron.
5Telegram + Teams + Gradio frontendsMeet the user where they already are. Three frontends share one router (agent.py).
6verify_worker continuous fact-checkThe hard one. Every claim is re-scored. Weak rows go back through synth with the correction injected.
R9700: THE SYNTHESIS ENGINE

One card. One model. Three jobs.

The R9700 runs Qwen3-30B-A3B-AWQ via vLLM continuously. Three distinct flows hit the same endpoint, batched together by vLLM's scheduler. The card never knows it is doing three different things.

R9700 request mix — 4-day production window

Source: agent_runs.db + verify_worker logs + vLLM request_success_total. Bucket attribution by request prefix sampled across the window.
FlowCalls per dayAvg promptAvg decode
kb_synth (write to the synthesis store)~6,800~900 tok~280 tok
verify_worker (fact-check)~3,200~620 tok~140 tok
agent.py (user/agent Q&A, tool-calling)~2,300~520 tok~120 tok
One card. Many agents. Real throughput. Single-stream decode on this R9700 is ~50 tok/s. Under 23–24 concurrent agents firing through vLLM, aggregate decode has been observed at ~450–480 tok/s — nearly 10× the single-user rate. The card scales because the workload is multi-agent by design, and a multi-agent workload is what justifies a GPU on the desk in the first place.
The privacy boundary lives here. Customer-bucket rows route to R9700 local only. Competitive and spec-bucket rows can route to the AMD Enterprise gateway (public oracle, gpt-4.1). The router is one switch in verify_worker.py. Customer data never leaves the box.
EPYC: THE CONDUCTOR

Genoa CPU: the conductor and the burst engine.

96 cores. 12 CCDs. 755 GB DDR5. 2.5 days of telemetry says p50 CPU usage is 0.3% and p95 is 12.9%. The CPU does its work in short, intense bursts — not because it is overprovisioned, but because that is the shape of agentic orchestration when each component is pinned to its own CCD.

Host CPU usage by component class — p50 vs p95 across 92,768 samples

Source: /tmp/agentic_perf.csv on the host. 92,768 rows @ 2-second interval, 2026-05-31 18:38 to 2026-06-03 00:31 UTC.

The "agentic_pct" lane sums every Python process tied to the reference workflow: inference client, agents, ingestors, mounts, serving, dashboards. Its p95 is 13.4%, meaning the steady-state agentic stack is lean by design. The remaining capacity is not wasted — it is reserved for the bursts that hit when speech preprocessing, embedding builds, or shared document mount I/O fire. The CPU is constantly working; the steady-state number just measures the quiet between bursts.

WHEN THE CPU REALLY WORKS

Three workloads pin the cores hot.

Most of the day the CPU is at sub-percent load. Three things change that — and all three exist precisely because they are wrong to put on the GPU.

Speech preprocessor batch533%
meeting_ingest stt --threads 24: faster-stt transcription of meeting audio. CPU-only, peaks at >500% of one core.
verify_worker39.5%
24 parallel HTTP clients pipelining verification calls into vLLM. CPU-bound on JSON parse + retry logic.
shared documents ingest12 workers
doc_fs_ingest. Streams files from SP, hashes, dedups, queues for embedding. Memory-bandwidth bound.
rclone mount4.9%
doc_fs_mount FUSE mount with VFS cache. Steady-state I/O cost of having shared documents look like a local disk.
CCD AFFINITY: WHY THE CPU FEELS QUIET

Each agent class lives on its own CCD.

Genoa-class EPYC is one socket, 96 cores, organized as 12 CCDs of 8 cores each, with 12 separate 96 MB L3 slices. The reference workflow pins each agent class to a specific core band — which lines up with CCD boundaries — so heavy loops keep their L3 hot and never thrash on a neighbor's cache.

ComponentCore bandCCDsWhy this band
synthesis burst (kb_synth)0-310-34 CCDs = 384 MB sticky L3 for the big context windows fed to vLLM
ingest_teams32-394Isolated CCD so Teams polling never blocks synthesis worker
ingest_email40-475Outlook fetch + classification on its own L3, no contention
meeting_ingest (speech preprocessor)48-796-94 CCDs because faster-stt bursts to 533% of one core
doc_fs_ingest (documentation source)80-8710FUSE + dedup workers, far from the synthesis worker pool
DigestAgent88-9111Daily 9am scheduled work, no need for a full CCD

The mapping lives in a small affinity helper as named bands (env-overridable, clamped to online CPUs). Every ingester calls pin("teams") or pin("email") at start, which does three things in one shot:

# a small affinity helper - one call per ingester at startup
os.sched_setaffinity(0, set(cores))     # pin process to band
os.environ.setdefault("OMP_NUM_THREADS",    str(len(cores)))
os.environ.setdefault("OPENBLAS_NUM_THREADS", str(len(cores)))
os.environ.setdefault("MKL_NUM_THREADS",    str(len(cores)))

Capping the BLAS thread env vars to the band width is the unsung optimization. Without it, OpenMP and OpenBLAS spray threads across the full 96-core socket, which destroys the L3 isolation the affinity pin just bought.

Single socket, no NUMA cross-talk. the Genoa EPYC used here is one NUMA node so every band sees the same 755 GB of DDR5 at equal latency. The CCD pins are purely about L3 stickiness and isolation, not memory locality. On a two-socket box you would also pin to a NUMA node; here we get the benefit for free.
DATA SYNTHESIS, IN DETAIL

The R9700's main job is writing the brain.

Every email, every meeting transcript, every Salesforce note becomes a row in the retrieval store (raw, retrievable). Then kb_synth pulls source rows in batches and asks the R9700 to compress them into a single high-signal row in the synthesis store — with confidence score, source links, and bucket tag. That second row is what answers questions.

# kb_synth single-writer loop (simplified)
while True:
    src = pending_sources(limit=24)
    for s in src:
        prompt = build_synth_prompt(s)  # ~900 tokens of context
        out = vllm.chat(prompt, max_tokens=400)
        brain.write(out, src_id=s.id, bucket=s.bucket,
                    confidence=score(out), source_url=s.url)
    sleep_if_idle()

Single-writer invariant: only kb_synth writes to the synthesis store. Everything else — meeting harvest, ingestors, verify — writes upstream (the retrieval store, meetings.db) and lets synth pull. No write contention. No race conditions on the SQLite WAL.

VERIFY, RANK, RE-SYNTHESIZE

Every claim is re-scored continuously.

An agentic system that writes but never re-reads is a confident liar. verify_worker runs forever: it pulls the lowest-veracity rows, fact-checks them, and pushes weak rows back through synth with the correction injected.

StepWhere it runsWhy
1. Pick stale rowCPU (SQLite query)Lowest veracity, oldest verified_at, or never-verified
2. Route by bucketCPU (switch in verify_worker.py)customer -> R9700 local; competitive/spec -> AMD gateway
3. Fact-check callR9700 or AMD gatewayReturn JSON {veracity, verdict, correction, sources}
4. Score + persistCPUUPDATE synth SET veracity_score, verified_at, evidence_json
5. Requeue weak rowsCPU + R9700If veracity < threshold, re-synth from source with correction injected
Rank, not just store. Every read query orders by confidence * coalesce(veracity_score, confidence). The brain naturally surfaces well-verified rows first, regardless of when they were written.
THE SPLIT THAT MAKES THIS BUILDABLE

Three rules for what goes where.

Any agentic workload you build, on any hardware, will live or die on this allocation. Here are the three rules the reference workflow follows.

Rule 1LLM only
If it needs reasoning, summarization, or natural-language judgment, it is on the R9700. Nothing else is.
Rule 2CPU-native
Embeddings, BM25, regex, SQL, JSON parsing, scheduling, HTTP, FUSE, the speech preprocessor. CPU is faster end-to-end than batching to GPU.
Rule 3Single writer
For every store, exactly one process writes. Everyone else reads. Eliminates 90% of "agentic AI is hard" complexity.