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.
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.
| Step | What it added | Lesson |
|---|---|---|
| 1 | vLLM + Qwen3-30B-A3B-AWQ on R9700 | MoE first (Part 2). Dense 14B was 6.8x slower. Start with the right shape. |
| 2 | Outlook + Teams + shared documents ingestors | Get the data in before you teach the model what to do with it. |
| 3 | the retrieval store (RAG) + the synthesis store (synth) | Two layers. RAG for retrieval; synth for compressed wisdom. Different write patterns. |
| 4 | 10 background agents | Schedule first, intelligence second. A cron with an LLM behind it is still mostly cron. |
| 5 | Telegram + Teams + Gradio frontends | Meet the user where they already are. Three frontends share one router (agent.py). |
| 6 | verify_worker continuous fact-check | The hard one. Every claim is re-scored. Weak rows go back through synth with the correction injected. |
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.
| Flow | Calls per day | Avg prompt | Avg 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 |
verify_worker.py. Customer data never leaves the box.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.
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.
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.
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.
| Component | Core band | CCDs | Why this band |
|---|---|---|---|
| synthesis burst (kb_synth) | 0-31 | 0-3 | 4 CCDs = 384 MB sticky L3 for the big context windows fed to vLLM |
| ingest_teams | 32-39 | 4 | Isolated CCD so Teams polling never blocks synthesis worker |
| ingest_email | 40-47 | 5 | Outlook fetch + classification on its own L3, no contention |
| meeting_ingest (speech preprocessor) | 48-79 | 6-9 | 4 CCDs because faster-stt bursts to 533% of one core |
| doc_fs_ingest (documentation source) | 80-87 | 10 | FUSE + dedup workers, far from the synthesis worker pool |
| DigestAgent | 88-91 | 11 | Daily 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.
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.
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.
| Step | Where it runs | Why |
|---|---|---|
| 1. Pick stale row | CPU (SQLite query) | Lowest veracity, oldest verified_at, or never-verified |
| 2. Route by bucket | CPU (switch in verify_worker.py) | customer -> R9700 local; competitive/spec -> AMD gateway |
| 3. Fact-check call | R9700 or AMD gateway | Return JSON {veracity, verdict, correction, sources} |
| 4. Score + persist | CPU | UPDATE synth SET veracity_score, verified_at, evidence_json |
| 5. Requeue weak rows | CPU + R9700 | If veracity < threshold, re-synth from source with correction injected |
confidence * coalesce(veracity_score, confidence). The brain naturally surfaces well-verified rows first, regardless of when they were written.Any agentic workload you build, on any hardware, will live or die on this allocation. Here are the three rules the reference workflow follows.