Files
agentic-kvc/analysis/characterization/window_1_results.md
Gahow Wang 559faa1e26 B2 finding: TPOT idx peaks at 32k, not 65k — cost migrates to TTFT
The B2 same-worker TPOT p90 idx is non-monotone: 7.89x at 32k drops
to 2.26x at 65k. The naive reading is "interference gets weaker for
huge prefills"; the actual mechanism is a regime shift, and reading
TPOT p90 alone is misleading.

Three superimposed effects:

1. Cost migration TPOT -> TTFT. A 32k prefill is short enough that
   chunked-prefill keeps interleaving decode steps, so overlapping
   decodes trickle tokens out at painful per-token rates. A 65k
   prefill is long enough that overlapping decodes are *fully*
   blocked for ~10s; once they break through, the injection is
   winding down and subsequent iterations run unobstructed. The
   cost lands on the TTFT clock (14s) instead of inflating TPOT.

2. Bimodal TPOT distribution. At 65k overlap, decodes split into
   "blocked entire prefill then normal rate" and "trickled slowly
   through prefill chunks". p99 sits on the second population and
   grows 59 -> 169.5 ms; p90 sits on the first and shrinks.

3. "Clean" stops being clean. With 4x ~10s injections in 60s, the
   110 "clean" decodes at 65k are squeezed into 2-3s recovery
   pockets. TPOT p90 clean rises 6.9 -> 9.6 ms (40%), shrinking
   the denominator of the ratio.

window_1_results.md adds a new B2 subsection laying out the
mechanism with the per-cell data table and the explicit reading
rule: headline interference metric is TTFT idx (monotone); TPOT
p99 is the right tail indicator; TPOT p90 alone is unsafe across
regime shifts. Direct implication: TTFT and TPOT need separate
SLO thresholds under PD-colo, because they measure costs from
different points in the request lifecycle and the cost migration
between them is workload-dependent.

current_results/characterization_claim_matrix.md adds a new
supported claim for the cost migration, listed against the existing
B2 evidence. current_results/reviewer_risk_register.md adds a
low-severity entry warning future readers off TPOT p90 alone.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 00:35:45 +08:00

14 KiB
Raw Blame History

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) drives hotspot_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 2k65k 12126 114228 0.921.02 0.961.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 non-monotone — see §"TPOT idx peaks at 32k, not 65k" below.

Figures: fig_b2_tpot_vs_prefill.png, fig_b2_ttft_vs_prefill.png.

Why this matters

  • The different-worker control 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-worker TTFT curve is monotone in prefill size all the way to 218× at 65k. TPOT p90 is monotone only up to 32k (7.89×), then drops at 65k — this is not "interference relaxing", it is the cost regime shifting from TPOT to TTFT (see below).
  • 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).

TPOT idx peaks at 32k, not 65k — regime shift, not relief

The naïve reading of the table is "interference gets worse up to 32k then drops at 65k". That is wrong; the cost is shifting from per-token rate (TPOT) to first-token wait (TTFT), and p90 / clean happens to compress the visible cost. Three superimposed effects.

Same-variant detail across the regime boundary:

                          32k         65k       change
n_overlap                  67         130       +94%  (most decodes now overlap)
n_clean                   173         110       -37%
TPOT p50 overlap (ms)    12.2        20.1       +1.6x
TPOT p90 overlap (ms)    54.8        21.7       -2.5x  <- "improves"
TPOT p99 overlap (ms)    59.0       169.5       +2.9x  <- tail explodes
TTFT p90 overlap  (s)    4.17       14.06       +3.4x
TPOT p90 clean   (ms)     6.9         9.6       +40%

Mechanism 1 — Cost shifts from TPOT to TTFT. TPOT is measured only after a request starts emitting tokens. A 32 k prefill (~5 s on H20) is short enough that vLLM's chunked-prefill scheduler keeps interleaving decode steps; overlapping decodes trickle tokens out at painfully slow per-token rates → p90 TPOT 54.8 ms. A 65 k prefill (~10 s) is long enough that many overlapping decodes get zero tokens for nearly the whole prefill window; when they finally break through, the injection is winding down so subsequent decode iterations are unobstructed. The cost goes onto the TTFT clock (14 s) instead of inflating TPOT.

Mechanism 2 — Bimodal TPOT distribution hides under p90. At 65 k overlap, two populations of decodes coexist:

  • decodes blocked the entire prefill (high TTFT, then normal per-token rate)
  • decodes that did trickle slowly through prefill chunks (low TTFT, high TPOT)
  • The p99 jump 59 → 169.5 ms shows the second population is worse at 65 k. p90 happens to fall on the first (fast-after-block) population.

Mechanism 3 — "Clean" stops being clean. With 4 × ~10 s injections spread across 60 s (40 s of injection time, 20 s of gaps), there are very few moments where the worker is truly idle. The 110 "clean" decodes at 65 k are squeezed into 2-3 s pockets where the system is recovering from the previous injection or about to be hit by the next. TPOT p90 clean rises 6.9 → 9.6 ms (the denominator of the idx ratio drifts up by 40%).

Reading rule for B2: TTFT idx is the headline interference metric — it is monotone and reflects user-visible "no tokens for N seconds" latency. TPOT p99 is the right tail-sensitivity indicator (also monotone). TPOT p90 is non-monotone across regime shifts and should not be used alone. This has direct implications for SLO design: TTFT and TPOT cannot share the same violation threshold under PD-colo interference, because they measure costs from different points in the request lifecycle and the cost migration between them is workload-dependent.

This is also a finding the paper should call out: once same-worker prefill grows beyond a TTFT-block threshold, overlapping decodes "give up" their per-token rate complaint and pay the cost in queueing instead. The system looks faster on per-token metrics; users experience longer waits.

What Window 1 does not answer

These need Window 2 (B4 SRR sweep + B5 failure attribution near SRR boundary):

  1. 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.
  2. 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):

  1. unified isolated rerun to capture interference_index (B2 already provides cleaner causal proof; skip unless reviewer asks).
  2. B2 with the proxy in path — measure whether the production cache_aware routing actually pushes prefill and decode onto different workers in practice.
  3. KV-occupancy timeline per worker — needs polling vllm:gpu_cache_usage during B3 reruns; useful for "KV pressure drives cache miss" subsection.

Caveats and known data hygiene issues

  • APC contamination across B3 hot-sweep: lmetric ran from cold; load_only and sticky ran on the same 8 vLLMs without restart. Empirical contamination is < 1% (verified by first-turn cached_tokens distribution), but unified and capped were rerun cold-start specifically to remove any residual concern.
  • Unified's interference_index is missing because the original b3_analyze.sh unconditionally truncate-wrote sliced engine_state files; isolated runs that wrote engine_state into their own per-policy directory were overwritten. Fixed in commit df32499; 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