|
| 1 | +# P2P benchmarks, series 3: the missing affinity+P2P arm, and where it wins |
| 2 | + |
| 3 | +Series 1 ([RESULTS.md](RESULTS.md)) and the llm-d guide's own benchmark |
| 4 | +report both leave a gap: `epp-affinity-p2p` — precise prefix-cache |
| 5 | +affinity *plus* the pull, the guide's shipped default — was never |
| 6 | +isolated on the guide's own two flagship scenarios. Every published table |
| 7 | +compares affinity-alone or load-alone against load+P2P; the arm actually |
| 8 | +recommended by default was untested. This series fills that gap on both |
| 9 | +scenarios and finds they disagree with each other. |
| 10 | + |
| 11 | +Run on the guide's own manifests (`guides/p2p-kv-cache-sharing`, commit |
| 12 | +`d4a5fb91`), the pinned nightly |
| 13 | +(`nightly-4080263bb2c5d10deac17aaeb88e0823bc35bca9`) with the |
| 14 | +`generic_p2p` overlay (`145a460c`) plus the request-finalization crash |
| 15 | +fix (`orozery/vllm@fa07027d` — see |
| 16 | +[p2p-request-finalization-crash.md](p2p-request-finalization-crash.md)) |
| 17 | +overlaid via ConfigMap. Fix overlay: |
| 18 | +[configs/2026-07-capacity-and-scenarios/patch-p2p-fix-scaffolding.yaml](configs/2026-07-capacity-and-scenarios/patch-p2p-fix-scaffolding.yaml), |
| 19 | +fix source: |
| 20 | +[configs/2026-07-capacity-and-scenarios/vllm-fix-orozery-scheduler.py](configs/2026-07-capacity-and-scenarios/vllm-fix-orozery-scheduler.py). |
| 21 | +Every deployment verified in-pod before running load: pinned image |
| 22 | +digest, `finished_signaled` present in the mounted `scheduler.py` (fix |
| 23 | +live), `remote_kv_source` present in the mounted `manager.py` (branch |
| 24 | +overlay live). |
| 25 | + |
| 26 | +## Scenario D: document Q&A (14 pods, session-ownership regime) |
| 27 | + |
| 28 | +Guide's own headline workload |
| 29 | +([guide_p2p-kv-cache-sharing_1.yaml.in](configs/2026-07-capacity-and-scenarios/guide_p2p-kv-cache-sharing_1.yaml.in) — |
| 30 | +also lives at `llm-d-benchmark#1656`): 192 conversations, private 48K-token |
| 31 | +document prefix, 6 turns of 256-token questions/answers, 128 concurrent. |
| 32 | +Two independent runs of the new arm (order-alternation doesn't apply with |
| 33 | +only one non-baseline arm running). |
| 34 | + |
| 35 | +| run | affinity (no pull) | **affinity + P2P** | load + P2P | |
| 36 | +|---|---|---|---| |
| 37 | +| 1 | 4.1 / 41.0 / 80.5s; 5.98 turns/s | 4.0 / 27.7 / 48.8s; 5.6 turns/s | 4.5 / 13.0 / 20.9s; 7.02 turns/s | |
| 38 | +| 2 | 4.2 / 17.3 / 37.2s; 7.66 turns/s | 2.6 / 33.6 / 67.2s; 5.7 turns/s | 3.9 / 12.5 / 26.7s; 7.76 turns/s | |
| 39 | + |
| 40 | +(TTFT p50/p95/p99; throughput. affinity+P2P's throughput is slightly |
| 41 | +undercounted by a sub-1% tail of gpt-oss requests that generated |
| 42 | +unusually long reasoning output under `ignore_eos:true` — median/p90/p95 |
| 43 | +output length all land on the intended 256-token target, only `p99.9` |
| 44 | +blows out. TTFT is unaffected, measured at first token before that |
| 45 | +generation happens.) |
| 46 | + |
| 47 | +**affinity+P2P sits between the other two arms, in both runs, not one.** |
| 48 | +It improves on affinity-alone's worst case (48.8-67.2s p99 vs 80.5s) but |
| 49 | +never reaches load+P2P's range (20.9-26.7s p99). Likely mechanism: |
| 50 | +affinity+P2P still queues a request behind a busy owner pod whenever the |
| 51 | +scorer's affinity term outweighs load, and only pulls once a peer already |
| 52 | +out-caches the scheduled pod — it recovers the *cache-locality* cost of a |
| 53 | +cross-pod placement but not the *queueing* cost of a placement decision |
| 54 | +that still prefers a busy owner. load+P2P never makes that tradeoff: |
| 55 | +placement always goes to the least-loaded pod. |
| 56 | + |
| 57 | +Zero restarts, zero failures, 1,152/1,152 turns completed on both runs of |
| 58 | +the new arm (2,304 turns total). |
| 59 | + |
| 60 | +## Scenario A: uniform shared-prefix pool (16 pods, capacity regime) |
| 61 | + |
| 62 | +128 shared 48K-token prefixes, 256-token questions, 64-token outputs, |
| 63 | +constant-rate stages 4-24 req/s. Profile built for this series: |
| 64 | +[guide_p2p-kv-cache-sharing_2.yaml.in](configs/2026-07-capacity-and-scenarios/guide_p2p-kv-cache-sharing_2.yaml.in) |
| 65 | +(now also `llm-d-benchmark#1656`). Raw per-stage results: |
| 66 | +[configs/2026-07-capacity-and-scenarios/scenario-a-raw/](configs/2026-07-capacity-and-scenarios/scenario-a-raw/). |
| 67 | + |
| 68 | +| offered | affinity | **affinity + P2P** | load, no P2P | load + P2P | |
| 69 | +|---|---|---|---|---| |
| 70 | +| 4 req/s | 3.8 / 2.4s | 3.9 / 2.4s | 3.8 / 4.2s | 3.9 / 2.4s | |
| 71 | +| 8 req/s | 7.9 / 0.7s | 7.9 / 0.73s | 6.7 / 6.5s | 7.7 / 1.6s | |
| 72 | +| 12 req/s | 11.9 / 0.7s | 11.9 / 0.75s | 9.0 / 24.9s | 11.4 / 2.3s | |
| 73 | +| 16 req/s | 15.8 / 0.7s | 15.8 / 0.76s | 8.8 / 37.3s | 15.1 / 3.6s | |
| 74 | +| 20 req/s | 19.8 / 0.7s | 19.8 / 0.77s | 9.4 / 48.8s | 15.4 / 15.6s | |
| 75 | +| 24 req/s | 23.7 / 0.8s | 23.6 / 0.82s | 9.4 / 63.5s | 16.7 / 30.0s | |
| 76 | + |
| 77 | +(achieved req/s / request latency p50) |
| 78 | + |
| 79 | +**Here the result reverses.** affinity+P2P matches affinity's near-ideal |
| 80 | +ceiling within noise at every offered rate, tracking offered to |
| 81 | +saturation at 24 req/s. load+P2P — the arm that won Scenario D — degrades |
| 82 | +sharply at the top of the ladder: p50 30.0s at 24 req/s, achieving only |
| 83 | +16.7 of 24 offered. Likely mechanism: load-aware placement scatters each |
| 84 | +prefix's traffic across more pods than affinity does; at saturation that |
| 85 | +costs more in pull/transfer contention than affinity+P2P ever pays, since |
| 86 | +affinity+P2P never scatters a prefix's traffic in the first place — the |
| 87 | +pull mostly sits idle here (placement rarely diverges from cache), so it |
| 88 | +costs nothing to have it available for the rare miss. |
| 89 | + |
| 90 | +Zero restarts, zero failures, 5,040/5,040 requests. |
| 91 | + |
| 92 | +## No single arm wins both regimes |
| 93 | + |
| 94 | +Consistent with series 1's own cross-regime conclusion ("no single |
| 95 | +configuration wins both regimes") — this series supplies the specific |
| 96 | +data point that was missing: **affinity+P2P**, not just affinity-alone, |
| 97 | +loses the concurrency/ownership-contention regime (Scenario D) and wins |
| 98 | +the capacity/replication regime (Scenario A). The two regimes: |
| 99 | + |
| 100 | +- **Capacity-driven** (working set spread wider than any pod's cache, |
| 101 | + traffic uniformly distributed across owners — Scenario A here, series |
| 102 | + 1's uniform pool, series 1's small-scale Llama pool): affinity-based |
| 103 | + placement concentrates each prefix's traffic tightly enough that pulls |
| 104 | + stay cheap and local; load-aware placement's scattering costs more at |
| 105 | + saturation than it saves in balance. |
| 106 | +- **Concurrency/ownership-driven** (many concurrent sessions each pinned |
| 107 | + to a fixed owner pod by a composite scorer, contention builds |
| 108 | + independent of aggregate capacity — Scenario D here): load-aware |
| 109 | + placement avoids the owner-pod queue by never preferring a busy owner; |
| 110 | + affinity-based placement still pays that queueing cost even with the |
| 111 | + pull covering the cache-locality half of it. |
| 112 | + |
| 113 | +The guide ships `epp-affinity-p2p` as the default (reverted back to it |
| 114 | +after this series' Scenario A result, having briefly flipped to |
| 115 | +`epp-load-p2p` on Scenario D's evidence alone) — the safer |
| 116 | +general-purpose choice: close to affinity's ceiling in the capacity-driven |
| 117 | +regime, and still a clear improvement over affinity-alone in the |
| 118 | +concurrency-driven one, even though it does not match load+P2P there. |
| 119 | +`epp-load-p2p` remains the documented recommendation specifically for |
| 120 | +workloads shaped like Scenario D. |
0 commit comments