# Research Findings: KV Cache Optimization for Agentic LLM Workloads

**Date**: 2026-05-23
**Author**: Gahow Wang

---

## 1. Agentic Workload Characteristics (vs Chatbot/API)

| Property | Chatbot/API | **Agentic (this work)** |
|----------|-------------|----------------------|
| Input length | 1-5k tokens | **avg 33.6k, p90 88k** |
| Output length | 100-500 tokens | **avg 445** (similar) |
| I/O ratio | 1-10x | **75.6x** |
| Prefill token share | 50-70% | **98%** |
| KV reuse | Low (independent requests) | **71% theoretical, 91% intra-session** |
| Session structure | Mostly single-turn | **Multi-turn chains (parent_chat_id)** |
| Request weight distribution | Uniform | **Bimodal: 49% WARM/MEDIUM, 51% HEAVY** |

These characteristics fundamentally change what optimizations matter.

## 2. What Doesn't Work (and Why)

### 2.1 PD Disaggregation (DistServe/Splitwise approach)

**Setup**: 4P + 4D instances (Mooncake RDMA KV transfer)
**Result**: TTFT +72%, TPOT +1%, APC -5pp vs combined

**Root cause**: KV cache memory wall on decode instances. With avg 33.6k input and dedicated decode instances:
- Decode KV cache fills to 97.1%
- GPU idle (Running: 0), but new requests queue for KV cache memory
- 87.7% of TTFT is spent waiting for KV cache space

**Why it's different from chatbot**: Chatbot has short context (1-5k), so decode KV cache rarely fills. Agentic has 33k+ context, requiring 4-8GB KV per request → 2-3 concurrent requests saturate a single GPU's KV cache.

### 2.2 LMetric (OSDI'26, P_tokens × BS multiplication routing)

**Setup**: 8 instances, LMetric vs linear routing
**Result**: TTFT +2.2%, TPOT -4.4%, E2E +2.6% — all within noise (±7% run-to-run)

**Root cause (updated)**: LMetric is not "neutralized by affinity
constraints" — pure `--policy lmetric` runs without session affinity at all.
The actual reason the LMetric vs linear comparison sits within noise is that
`P_tokens` already includes `new_uncached_tokens = input_length - cache_hit`,
which means later turns of a session naturally score lowest on the instance
that cached their prefix. This gives LMetric an **implicit soft affinity**
that competes with linear's explicit sticky affinity. The two arrive at
similar placements through different mechanisms.

This is also why explicit migration buys little on top of LMetric: the
first-order signal driving placement is already cache-derived. See
`docs/migration-policy-design.md` for how the current hybrid policy uses
this insight (LMetric base + explicit affinity only when `cache_ratio > 0.5`).

**Previous framing (incorrect)**: an earlier draft of this section attributed
the result to session affinity constraining LMetric's routing freedom. That
framing assumed `--policy lmetric` inherited the linear-mode session-sticky
behavior, which it does not (verified in `tests/test_proxy_pick.py`).

### 2.3 Elastic P2P RDMA Offload (Heavy prefill on different instance)

**Setup**: 8 instances (kv_both), HEAVY requests prefilled on different instance, KV transferred via Mooncake RDMA
**Result**: E2E +37%, TPOT +11.6% — significantly worse

**Root causes**:
1. **Mooncake lacks layerwise KV transfer**: All blocks transferred after prefill completes (sequential, not pipelined). Transfer p50=1.1s for 40k tokens, highly variable (R²=0.095 vs input length).
2. **92% of HEAVY are turn-1 cold**: No cache to exploit on the P instance → full cold prefill is always slower than co-located cached prefill.
3. **kv_both has non-zero overhead at high concurrency**: Zero overhead at idle (Phase 0), but +16% TPOT at 16 sessions (background RDMA threads compete for resources).

### 2.4 Dedicated Prefill Service

**Setup**: 1 PS (no sessions) + 7 C (session-sticky)
**Result**: PS either gets 0 offloads (cost model correctly identifies it's more expensive) or gets too many (cascading timeouts)

**Root cause**: Without KV pull from C, PS does cold prefill (full input) which is always slower than C's cached prefill. With KV pull, double RDMA transfer overhead negates the benefit.

### 2.5 Chunk Size Tuning (max_num_batched_tokens)

**Setup**: 2048/4096/8192/16384 at 16 sessions
**Result**: Default 8192 is optimal; smaller chunks add scheduler overhead, larger chunks help HEAVY but hurt overall

### 2.6 OVERLOAD_FACTOR Tuning

**Setup**: 2.0/1.5/1.3/1.0 session affinity breaking threshold
**Result**: No effect — imbalance from workload skew, not routing

## 3. What DOES Work

### 3.1 Cache-Aware Session-Sticky Routing (the dominant optimization)

**Setup**: `score = ongoing_tokens - α × cache_hit_tokens`, session affinity for turn 2+
**Result vs round-robin**:

| Metric | Round-Robin | Cache-Aware | Delta |
|--------|------------|-------------|-------|
| TTFT p50 | 1.836s | **0.731s** | **-60%** |
| TPOT p90 | 0.086s | **0.073s** | **-15%** |
| APC | 20.8% | **44.7%** | **+24pp** |

**Why it works for agentic**: 91% of KV reuse is intra-session. Session-sticky routing ensures subsequent turns find their KV cache on the same instance. Cache-aware scoring steers turn-1 requests to instances with matching system prompt blocks (47% of blocks are shared across sessions).

### 3.2 GPU Balance → Latency Improvement (H4 evidence)

When GPU imbalance was reduced from 4.0x to 2.0x (via H4 cache-gate routing):
- HEAVY_COLO TTFT: 7.02s → **6.28s (-10.5%)**
- No TPOT regression

**Mechanism**: Hot GPU (63.3% util) causes queuing delays for co-located requests. Spreading load more evenly eliminates the queuing bottleneck.

**Limitation**: Only demonstrated for the 52/60 HEAVY requests that stayed co-located. The 8 offloaded requests had RDMA overhead. A routing-only approach to achieve balance (without RDMA) would be ideal.

## 4. System-Level Insights

### 4.1 Prefill-Decode Interference Threshold

| Concurrency | TPOT p90 | MEDIUM TPOT p90 | GPU Util |
|------------|----------|-----------------|----------|
| 8 sessions (1/GPU) | 0.073 | 0.075 | 30% |
| **16 sessions (2/GPU)** | **0.106 (+45%)** | **0.197 (+163%)** | 25% |

At >1 session per GPU, chunked prefill interference becomes significant. MEDIUM requests' TPOT degrades 2.5x because HEAVY prefill chunks block their decode steps.

### 4.2 Mooncake Transfer Engine Limitations

- **No layerwise transfer**: All KV blocks transferred after full prefill → pure sequential overhead
- **High variance**: R²=0.095 (transfer time uncorrelated with data size), bimodal distribution (0.6s or 18-30s)
- **Zero idle overhead**: kv_both mode has no cost when not transferring (Phase 0 validated)
- **Non-zero overhead at high concurrency**: +16% TPOT at 16 sessions (background threads)

### 4.3 KV Cache Reuse Structure

```
Total trace: 2.1M requests, 71% theoretical APC
Reuse breakdown:
  91% intra-session (same session, subsequent turns)
   4.8% cross-session (shared system prompts)
   4.2% unique (no reuse)

Effective APC achieved:
  Round-robin: 20.8% (destroys session locality)
  Cache-aware: 44.7% (preserves session locality)
  Theoretical max: 71% (infinite cache, single instance)
  Gap: 26pp from eviction + routing imperfection
```

### 4.4 Request Weight After Cache

A critical insight: the "weight" of a request for scheduling should be `new_tokens = input - cached`, not `total_input`.

| Request | Total Input | After 70% Cache | Effective Weight |
|---------|------------|-----------------|-----------------|
| 80k HEAVY | 80k tokens | **24k tokens** | **MEDIUM** |
| 30k MEDIUM | 30k tokens | 9k tokens | WARM |
| 5k WARM | 5k tokens | 5k tokens | WARM |

This changes the scheduling picture: most "HEAVY" requests in agentic workloads are effectively MEDIUM after cache — PD separation's premise (heavy prefill needs dedicated resources) doesn't apply.

## 5. Paper-Ready Summary

### Agentic Workload Characteristics (vs prior LLM serving work):
1. **Extreme I/O ratio** (75x) → 98% of compute is prefill
2. **High intra-session KV reuse** (91%) → session affinity is critical
3. **Bimodal request weight** (51% HEAVY by input, but only ~15% HEAVY by new_tokens after cache)
4. **Multi-turn session structure** → routing decisions have long-term consequences (session migration destroys cache)

### Why existing approaches don't work:
1. **PD-Sep** assumes decode needs dedicated resources → agentic has memory wall on decode
2. **LMetric** matches linear within noise because cache-hit appears in
   `P_tokens` itself, so it already routes later turns back to the cached
   instance via implicit soft affinity — explicit affinity buys little
3. **Elastic RDMA** assumes KV transfer is cheap → Mooncake lacks layerwise pipelining
4. **Size-based classification** assumes HEAVY = needs special handling → after cache, most HEAVY is MEDIUM

### Our insights:
1. **Cache-aware session-sticky routing** is the dominant optimization: -60% TTFT, +24pp APC
2. **Routing quality > PD separation > eviction policy** (simulation verified: routing gives 24pp, eviction gives 1.8pp)
3. **Effective request weight** (new_tokens, not total_input) should drive scheduling
4. **Prefill-decode interference** only matters at >1 session/GPU (rarely reached in production clusters)
5. **GPU balance improvement** directly improves HEAVY TTFT (-10.5% demonstrated)

### Quantitative improvements (this work vs vLLM default):
- TTFT p50: **-60%** (0.731s vs 1.836s) from cache-aware session-sticky routing
- APC: **+24pp** (44.7% vs 20.8%)
- TPOT p90: **-15%** (0.073s vs 0.086s) from reduced prefill-decode interference via better cache utilization
