Skip to content

Commit 53a85ba

Browse files
committed
findings(p2p): request-finalization crash fix, capacity provenance, series 3 (missing affinity+P2P arm)
Three related investigations from the guide-testing/blog-review session: - p2p-request-finalization-crash.md: vllm#49635 (bisected to #46284), both fix variants verified on this findings set's own crash reproducer plus fresh unit runs. - capacity-number-provenance.md: traces why the guide report and blog disagreed on GPU KV capacity (0.48M / 1.38M / a fresh 1.22M and 1.61M) - each number was genuinely measured, for a different config or vLLM nightly than the one it ended up describing, not a copy-paste error. - RESULTS-3.md: the previously-untested epp-affinity-p2p arm, run on both the guide's flagship scenarios. It loses the concurrency/ ownership-contention regime (document Q&A) to load-aware+P2P, and wins the capacity/replication regime (uniform pool) over it - no single arm wins both, extending series 1's own conclusion with the specific missing data point. Signed-off-by: nilig <nili.ifergan@gmail.com>
1 parent a1b008e commit 53a85ba

15 files changed

Lines changed: 3595 additions & 0 deletions

test/p2p-findings/RESULTS-3.md

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
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.
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
# P2P findings — GPU KV capacity number provenance (guide report + blog)
2+
3+
The published gpt-oss-120b capacity figures in the guide's benchmark
4+
report and the blog draft disagreed with each other and, in the P/D case,
5+
with a fresh measurement. Neither was a copy-paste error or careless
6+
mistake — each number was genuinely measured, but for a *different*
7+
config or vLLM nightly than the one it ended up describing. Tracing the
8+
actual provenance:
9+
10+
## The three numbers and where each one is really from
11+
12+
**~0.48M tokens/pod** — genuinely measured, but for a deliberately
13+
different config: `--gpu-memory-utilization=0.60` on the series-2
14+
"sessions exceed fleet GPU cache" scenario (`RESULTS.md` line ~235,
15+
`RESULTS-2.md` line 18), where the whole point was to force eviction
16+
under a smaller GPU budget. It was never the number for the guide's
17+
actual shipped aggregated config (`--gpu-memory-utilization=0.85`) — it
18+
got carried into `benchmark-results/gpt-oss-120b-h200.md` and the blog's
19+
Use Case 1 table as if it were.
20+
21+
**~1.38M tokens/pod** — also genuinely measured, at `0.85`/`65536` (the
22+
guide's real aggregated flags), but on an earlier vLLM nightly than the
23+
one now pinned (`RESULTS.md` line 48, no specific SHA recorded). A fresh
24+
read on the current pin (`nightly-4080263bb2c5d10deac17aaeb88e0823bc35bca9`,
25+
same `0.85`/`65536` flags, two independent pod starts) gives
26+
**1,218,315 tokens** instead — nightly-to-nightly drift in the memory
27+
profiler/CUDA-graph accounting, not a config difference. The 1.38M figure
28+
was then reused *again*, by reference rather than measurement, for the
29+
P/D topology's CPU-tier sizing rationale in Run L
30+
(`RESULTS-2.md` line 290: "128 GiB CPU tier both roles (~2.3x the
31+
~1.38M-token GPU KV at TP=1)") — the P/D topology's GPU KV was never
32+
independently measured. The blog's Use Case 3 table copied Run L's
33+
numbers directly, inheriting the un-verified-for-P/D figure.
34+
35+
**1,607,392 tokens/pod (P/D, fresh measurement, this session)** — a
36+
minimal 2-pod probe (1 prefill + 1 decode, TP=1 both legs, gpt-oss-120b,
37+
`MultiConnector[NixlConnector + OffloadingConnector]`, current pinned
38+
nightly, "as the pd-disaggregation guide ships" — no explicit
39+
`--gpu-memory-utilization` or `--max-model-len` override, since neither
40+
`guides/pd-disaggregation/modelserver/gpu/vllm/base/patch-{prefill,decode}.yaml`
41+
nor the recipes base sets them). vLLM's own defaults apply instead:
42+
`--gpu-memory-utilization=0.92` (logged explicitly) and
43+
`--max-model-len=131072` (gpt-oss-120b's native context, vs the
44+
aggregated config's explicit 65536 cap). Both legs identical, as
45+
expected for matched TP=1/config. Manifest:
46+
[configs/2026-07-capacity-and-scenarios/pd-capacity-probe.yaml](configs/2026-07-capacity-and-scenarios/pd-capacity-probe.yaml).
47+
First attempt OOMKilled at `memory: 32Gi` (too small for the 88 GiB
48+
`cpu_bytes_to_use` shm-backed tier); the guide's own proven value is
49+
`memory: 160Gi` (`modelserver/gpu/vllm/patch-vllm.yaml`) — used that on
50+
retry, 0 restarts.
51+
52+
## What this means for the published numbers
53+
54+
Neither the guide's report nor the blog can currently cite a P/D GPU-KV
55+
figure with real provenance — the true number depends on which
56+
`--gpu-memory-utilization` the P/D deployment actually runs, which
57+
neither document states. Two ways to close this:
58+
59+
1. Explicitly set `--gpu-memory-utilization=0.85` on the P/D legs (for
60+
consistency with the aggregated config) and re-measure — expect
61+
something below 1.61M, roughly `1.61M * 0.85/0.92 ≈ 1.49M` if the
62+
relationship is close to linear, but this needs a real measurement,
63+
not an extrapolation, before publishing.
64+
2. Leave the P/D legs at vLLM's defaults (as the guide currently ships
65+
them) and publish 1.61M as the actual figure, documenting the
66+
`--gpu-memory-utilization`/`--max-model-len` used so it's
67+
reproducible.
68+
69+
Either way, the aggregated topology's report and blog entries should be
70+
corrected to 1.22M (this session's twice-confirmed measurement on the
71+
current pin), and the P/D entry should stop being derived by reference
72+
to a number that was never independently checked for that topology.
73+
74+
## Downstream corrections already made (guide only, not yet the blog)
75+
76+
- `benchmark-results/gpt-oss-120b-h200.md` and `benchmarking/README.md`:
77+
aggregated GPU KV corrected to ~1.22M, CPU:GPU ratio corrected to
78+
~1.8x (was reported as 4.4x using the wrong 0.48M denominator), and the
79+
document-Q&A headline's "oversubscribes the fleet's aggregate GPU KV"
80+
claim removed — at the corrected capacity the 9.2M-token corpus fits
81+
inside the fleet's aggregate cache with room to spare, so the
82+
mechanism driving the headline's displaced-request pattern is
83+
per-pod queueing under concurrency, not capacity scarcity. See guide
84+
commits `97c45027`, `d4a5fb91` on `guides/p2p-kv-cache-sharing`.
85+
- Blog (`llm-d.github.io`, `blog/2026-08-15_p2p-kv-cache-sharing-llm-d.md`):
86+
Use Case 1's capacity figure and oversubscription framing were
87+
corrected the same way (commit `cc1dd76`). Use Case 3's P/D figure and
88+
the guide's placement-rule closing section were **not yet corrected**
89+
as of this writing — pending a decision on how to handle the
90+
unresolved P/D provenance above.
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
load:
2+
type: concurrent
3+
num_workers: 8
4+
worker_max_concurrency: 1000
5+
request_timeout: 180
6+
stages:
7+
# 192 conversations x 6 turns; 128 concurrent conversations in flight.
8+
- num_requests: 1152
9+
concurrency_level: 128
10+
api:
11+
type: completion
12+
streaming: true
13+
server:
14+
type: vllm
15+
model_name: REPLACE_ENV_LLMDBENCH_DEPLOY_CURRENT_MODEL
16+
base_url: REPLACE_ENV_LLMDBENCH_HARNESS_STACK_ENDPOINT_URL
17+
ignore_eos: true
18+
tokenizer:
19+
pretrained_model_name_or_path: REPLACE_ENV_LLMDBENCH_DEPLOY_CURRENT_MODEL
20+
data:
21+
type: conversation_replay
22+
conversation_replay:
23+
seed: 37
24+
max_model_len: 65536
25+
num_conversations: 192
26+
shared_system_prompt_len: 256
27+
# Each conversation carries a private ~48K-token "document" prefix;
28+
# 6 short questions with short answers make TTFT dominate. What drives
29+
# hit-vs-miss here is request placement under concurrency (128
30+
# sessions against a limited set of owner pods), not corpus size
31+
# versus aggregate GPU KV - scale num_conversations and concurrency_level
32+
# relative to your fleet's pod count, not its aggregate cache.
33+
dynamic_system_prompt_len:
34+
type: lognormal
35+
min: 49152
36+
max: 49152
37+
mean: 49152
38+
std_dev: 1
39+
turns_per_conversation:
40+
type: lognormal
41+
min: 6
42+
max: 6
43+
mean: 6
44+
std_dev: 1
45+
input_tokens_per_turn:
46+
type: lognormal
47+
min: 256
48+
max: 256
49+
mean: 256
50+
std_dev: 1
51+
output_tokens_per_turn:
52+
type: lognormal
53+
min: 256
54+
max: 256
55+
mean: 256
56+
std_dev: 1
57+
tool_call_latency_sec:
58+
type: lognormal
59+
min: 1
60+
max: 2
61+
mean: 1
62+
std_dev: 1
63+
report:
64+
request_lifecycle:
65+
summary: true
66+
per_stage: true
67+
per_request: true
68+
storage:
69+
local_storage:
70+
path: /workspace
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
load:
2+
type: constant
3+
stages:
4+
- rate: 4
5+
duration: 60
6+
- rate: 8
7+
duration: 60
8+
- rate: 12
9+
duration: 60
10+
- rate: 16
11+
duration: 60
12+
- rate: 20
13+
duration: 60
14+
- rate: 24
15+
duration: 60
16+
api:
17+
type: completion
18+
streaming: true
19+
server:
20+
type: vllm
21+
model_name: REPLACE_ENV_LLMDBENCH_DEPLOY_CURRENT_MODEL
22+
base_url: REPLACE_ENV_LLMDBENCH_HARNESS_STACK_ENDPOINT_URL
23+
ignore_eos: true
24+
tokenizer:
25+
pretrained_model_name_or_path: REPLACE_ENV_LLMDBENCH_DEPLOY_CURRENT_MODEL
26+
data:
27+
type: shared_prefix
28+
shared_prefix:
29+
num_groups: 128 # 128 shared prefixes (Scenario A: uniform pool)
30+
num_prompts_per_group: 32 # unique questions per shared prefix
31+
system_prompt_len: 48000 # ~48K-token shared prefix per group
32+
question_len: 256 # unique question length (tokens)
33+
output_len: 64 # target output length (tokens)
34+
report:
35+
request_lifecycle:
36+
summary: true
37+
per_stage: true
38+
per_request: true
39+
storage:
40+
local_storage:
41+
path: /workspace
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# TEST SCAFFOLDING (not for commit): overlays the request-finalization fix
2+
# (offloading connector scheduler) onto the engine. Currently Or's variant,
3+
# orozery/vllm@fa07027d, from the p2p-fix-or ConfigMap.
4+
apiVersion: apps/v1
5+
kind: Deployment
6+
metadata:
7+
name: decode
8+
spec:
9+
template:
10+
spec:
11+
containers:
12+
- name: modelserver
13+
volumeMounts:
14+
- mountPath: /usr/local/lib/python3.12/dist-packages/vllm/distributed/kv_transfer/kv_connector/v1/offloading/scheduler.py
15+
name: p2pfix
16+
subPath: offloading_scheduler.py
17+
volumes:
18+
- name: p2pfix
19+
configMap:
20+
name: p2p-fix-or

0 commit comments

Comments
 (0)