analysis/characterization/window_1_results.md is the headline write-up for Window 1: workload characterization (KV per request, real reuse decomposition, APC theoretical ceilings), B3 5-policy sweep with per-policy interpretation, B2 same-vs-different-worker interference microbench with causal reading, and an explicit list of what Window 1 does *not* answer (deferred to B4 SRR sweep + B5 attribution). Under window_1_results/: - 5 raw result JSONs from the B3 sweep, the B2 microbench, the APC upper bound, and the KV footprint - per-policy hotspot_index.json snapshots so render_window1_figures.py can plot per-worker TTFT p90 distributions - 8 PNG figures (figures/) covering the headline claims Three takeaways the figures pin down: 1) intra-session reuse dominates (93.2%), so session-affinity routing is the right primary lever 2) unified hybrid affinity hits 79.4% APC (97% of the 79.6% intra- session ceiling) AND cuts TTFT p90 from lmetric's 15.6s to 7.24s 3) B2 different-worker control sits at idx ≈ 1.0 across 32× prefill- size variation; same-worker TTFT idx scales 2.15× -> 218×, which is the cleanest causal evidence for same-worker prefill-decode interference Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
11 KiB
Window 1 Results: B1' + B2 + B3
Status: Window 1 complete (CPU + 2 dash0 GPU windows on 2026-05-25)
Sweep: outputs/b3_sweep_20260525_095043 (B3) + outputs/b2_microbench/ (B2)
Trace: traces/w600_r0.0015_st30.jsonl (1214 requests / 274 sessions / 53.3 M input tokens)
Model: Qwen3-Coder-30B-A3B-Instruct (TP1 × 8 instances on H20)
Per-policy artifacts under window_1_results/. Figures under window_1_results/figures/.
Headline
| Claim | Status | Evidence |
|---|---|---|
| Agentic workload reuse is overwhelmingly intra-session | supported | 93.2% of cached_tokens are intra-session (real); theoretical any-session APC ceiling 80.3% vs intra-session ceiling 79.6% → < 1pp gap |
| LMetric leaves 23 pp of APC on the table | supported | lmetric achieved 56.9% vs intra-session ceiling 79.6% (theoretical) |
| Hard session affinity recovers the locality lost by LMetric | supported | sticky APC 77.2% = 97% of theoretical ceiling |
| Hard affinity inflates same-worker prefill-decode interference | supported | sticky interference_index 13.65 vs lmetric 6.53 |
| Hybrid affinity (Unified) breaks the locality-vs-latency tradeoff | supported | unified hits 79.4% APC and TTFT p90 7.24 s (lmetric 15.6 s) simultaneously |
| Same-worker prefill-decode interference is causal, not correlation | supported | different-worker control idx≈1.0; same-worker idx scales monotonically with prefill size |
| Heavy-tail sessions are a contributor to hot-spot, not the sole cause | supported | cap=8 truncated trace cuts 37% of work; hotspot drops only 13% (2.24→1.94) |
B1' Workload characterization
Per-request KV footprint (Qwen3-Coder-30B-A3B)
kv_bytes_per_token = 2 × num_layers × num_kv_heads × head_dim × dtype_bytes = 2 × 48 × 4 × 128 × 2 = 98304 B
Full GLM-5.1 trace (2.11 M requests, 1.31 M sessions):
| p50 | p90 | p95 | p99 | max | |
|---|---|---|---|---|---|
| KV per request | 1.83 GiB | 8.04 GiB | 9.59 GiB | 11.49 GiB | 18.5 GiB |
H20 has ~95 GiB usable per GPU. A single p99 request occupies 12% of a single H20's HBM purely for KV. Multi-request batching is bounded by this.
Figure: figures/fig_kv_footprint_cdf.png.
Real reuse decomposition (from lmetric run on w600 trace)
| class | tokens | fraction |
|---|---|---|
| intra-session | 28.3 M | 93.2% |
| cross-session | 1.72 M | 5.7% |
| shared / system-prefix | 0.34 M | 1.1% |
| unclassified | 0 | 0.0% |
→ session-affinity routing covers >99% of the reuse signal. There is no meaningful "system prompt" in this trace.
Figure: figures/fig_reuse_decomposition.png.
Theoretical APC ceilings on w600
Computed by building a block-level trie of hash_ids per session (intra-session) or globally (any-session), then walking each request's hash_ids to count its longest prefix-match against previously-seen prefixes.
| variant | upper bound | hit requests |
|---|---|---|
| any-session (perfect global cache) | 80.3% | 961 / 1214 |
| intra-session only | 79.6% | 914 / 1214 |
| shared-prefix only (pos 0, ≥8 sessions) | 0.10% | 107 / 1214 |
Gap "any − intra" is 0.7 pp → no meaningful cross-session sharing in this trace.
B3 5-policy routing sweep
8 vLLM instances on TP1, w600 trace, --enable-prompt-tokens-details so cached_tokens is reported per request.
| policy | TTFT p50/p90/p99 | TPOT p50/p90/p99 ms | E2E p50/p90/p99 | APC | interference | hotspot | n_slow |
|---|---|---|---|---|---|---|---|
| lmetric | 0.94 / 15.59 / 52.95 | 8.9 / 21.2 / 175.9 | 2.75 / 24.75 / 79.62 | 56.9% | 6.53 | 2.24 | 295 |
| load_only | 1.25 / 20.15 / 52.65 | 9.2 / 26.7 / 320.7 | 3.58 / 33.43 / 93.92 | 54.1% | 9.16 | 1.14 | 379 |
| sticky | 0.54 / 18.02 / 71.37 | 8.9 / 36.1 / 345.2 | 2.08 / 34.61 / 133.58 | 77.2% | 13.65 | 2.35 | 234 |
| unified | 0.50 / 7.24 / 42.02 | 8.1 / 17.1 / 118.1 | 1.75 / 17.89 / 68.18 | 79.4% | n/a* | 3.35 | 189 |
| capped | 1.20 / 12.76 / 46.05 | 7.2 / 16.0 / 101.5 | 2.59 / 21.24 / 73.39 | 31.6% | 6.33 | 1.94 | 185 |
*unified engine_state was overwritten by my analyzer's slice step before the b3_analyze.sh fix landed; vLLM and the patch worked correctly. The B2 microbench provides a cleaner interference proof.
Mechanism indices
interference_index= TPOT_p90(decode overlapping same-worker prefill) / TPOT_p90(clean)hotspot_index= max(worker TTFT p90) / median(worker TTFT p90)
Figures: fig_b3_latency_bars.png, fig_b3_apc_vs_upper.png,
fig_b3_apc_vs_hotspot.png, fig_b3_per_worker_ttft_p90.png,
fig_b3_failure_breakdown.png.
Per-policy reading
- lmetric is the cache-aware baseline. APC 56.9% achieves only 71% of the intra-session ceiling — the missing 23 pp is the locality opportunity unified picks up.
- load_only strips cache awareness. Hot-spot drops to 1.14 (best), but APC only drops 3 pp because the picker's
min(num_requests)tie-break to instance 0 creates accidental stickiness at low concurrency. - sticky locks each session to one worker. APC climbs to 77.2% (97% of ceiling) but interference doubles to 13.65 and TPOT p99 hits 345 ms.
- unified is the hybrid — affinity gate
(cache_ratio>0.5 AND num_req ≤ 2×avg)keeps locality where it pays and drops it where it would hurt. The result is APC 79.4% and TTFT p90 cut in half from lmetric. The one bad worker (engine_4 at 37.7s p90) driveshotspot_index=3.35, but the other seven workers are all under 18 s. - capped runs lmetric on a turn-capped trace (max 8 turns/session). Removes 37% of requests but APC also crashes to 31.6% and hotspot only improves by 13%. This is the session-mass ablation: heavy sessions are a contributor to hot-spot but not the sole cause.
Slow-request cause breakdown (from joined_analysis.label_slow_requests)
| policy | n_slow | same-worker overlap | hot worker queue | cache miss large append | unknown |
|---|---|---|---|---|---|
| lmetric | 295 | 69 (23%) | 68 (23%) | 94 (32%) | 64 (22%) |
| load_only | 379 | 108 (29%) | 33 (9%) | 151 (40%) | 87 (23%) |
| sticky | 234 | 134 (57%) | 51 (22%) | 20 (9%) | 29 (12%) |
| unified | 189 | 0 (no engine_state) | 116 (61%) | 18 (10%) | 55 (29%) |
| capped | 185 | 45 (24%) | 66 (36%) | 60 (32%) | 14 (8%) |
PD-colo failures are mixed-mechanism: lmetric has no single dominant cause. Sticky concentrates failures into same-worker overlap (locality is on, cache misses are gone, but interference takes over).
B2 PD-colo interference microbench
Setup: 2 vLLM instances on GPU 0 (decode endpoint) and GPU 1 (prefill endpoint). A continuous 4 req/s short-prompt decode load runs against GPU 0 for 60 s per cell. 4 large-prompt one-token "prefill injections" fire every 12 s, targeted at either the same instance (same) or the paired one (different). Decode requests are labeled overlap iff their [t_first_token, t_finish] intersects any injection window. We compare TPOT p90 (overlap vs clean) per cell.
| variant | prefill | n_overlap | n_clean | TPOT idx | TTFT idx |
|---|---|---|---|---|---|
| different | 2k–65k | 12–126 | 114–228 | 0.92–1.02 | 0.96–1.00 |
| same | 2k | 12 | 228 | 1.16 | 2.15 |
| same | 8k | 19 | 221 | 1.90 | 12.1× |
| same | 16k | 37 | 203 | 3.37 | 30.8× |
| same | 32k | 67 | 173 | 7.89 | 94.6× |
| same | 65k | 130 | 110 | 2.26* | 218× |
*65k TPOT idx is suppressed because n_overlap > n_clean — by the time the 65k prefill is finishing, the 4-second gap to the next injection has already started decoding overlap. The "clean" decodes left are the ones that randomly hit the brief gaps between injections.
Figures: fig_b2_tpot_vs_prefill.png, fig_b2_ttft_vs_prefill.png.
Why this matters
- The
different-workercontrol sits at idx ≈ 1.0 across 32× variation in prefill size. This is the cleanest possible disproof of "any prefill anywhere hurts decode": prefill on a different worker is invisible to the decode worker. - The
same-workercurve is monotone in prefill size for TTFT (218× at 65k) and monotone-up-to-32k for TPOT (7.89×). The two ablations together establish causation: prefill-decode interference is a same-worker phenomenon and scales sharply with prefill mass. - This is the mechanism behind the B3 sticky interference jump (13.65) and unified's single hot worker (engine_4 at 37.7 s TTFT p90).
What Window 1 does not answer
These need Window 2 (B4 SRR sweep + B5 failure attribution near SRR boundary):
- Sustainable arrival rate (SRR) per policy under SLO. B3 was driven by trace timestamps with strict session sequentiality; when 8 instances cannot keep up, requests pile up and the effective dispatch window stretches (lmetric: trace claims 600 s, actual replay 49 min). We measured saturated behavior but not the saturation point. B4 needs the A4 open-loop Poisson loadgen with per-class SLO thresholds.
- Failure breakdown at the SRR boundary. B5 will rerun each policy at 0.9× / 1.0× / 1.1× of its SRR_max and label each SLO-violating request — gives the paper its causal failure-attribution table.
Optional / paper-polish runs (not blocking the story):
- unified isolated rerun to capture
interference_index(B2 already provides cleaner causal proof; skip unless reviewer asks). - B2 with the proxy in path — measure whether the production cache_aware routing actually pushes prefill and decode onto different workers in practice.
- KV-occupancy timeline per worker — needs polling
vllm:gpu_cache_usageduring B3 reruns; useful for "KV pressure drives cache miss" subsection.
Caveats and known data hygiene issues
- APC contamination across B3 hot-sweep:
lmetricran from cold;load_onlyandstickyran on the same 8 vLLMs without restart. Empirical contamination is < 1% (verified by first-turn cached_tokens distribution), butunifiedandcappedwere rerun cold-start specifically to remove any residual concern. - Unified's
interference_indexis missing because the originalb3_analyze.shunconditionally truncate-wrote sliced engine_state files; isolated runs that wrote engine_state into their own per-policy directory were overwritten. Fixed in commitdf32499; capped was the first run to benefit and survived with intact 86 MB engine_state. - w600 is not the full GLM-5.1 trace (1214 req vs 2.11 M). All B3/B2 percentiles are on the sample. The full-trace KV-footprint stats are on the full trace.
Reproduction commands
# B3 5-policy sweep
bash scripts/b3_sweep.sh # lmetric, load_only, sticky (hot-cache)
bash scripts/b3_isolated_policy.sh unified <trace> <dir> # isolated cold-start
bash scripts/b3_isolated_policy.sh lmetric <capped> <dir> # capped variant
bash scripts/b3_analyze.sh outputs/b3_sweep_<TS>
python3 scripts/render_b3_report.py --sweep-dir outputs/b3_sweep_<TS>
# B2 interference microbench
# (launch 2 vLLM on ports 8100/8101 with --enable-prompt-tokens-details first)
python3 scripts/b2_interference.py \
--decode-endpoint http://127.0.0.1:8100 \
--prefill-endpoint http://127.0.0.1:8101 \
--model <model> \
--out-dir outputs/b2_microbench/sweep
python3 analysis/characterization/b2_sweep_analysis.py --sweep-dir outputs/b2_microbench/sweep
# Figures
python3 analysis/characterization/render_window1_figures.py \
--results-dir analysis/characterization/window_1_results \
--out-dir analysis/characterization/window_1_results/figures