Skip to content

Commit 91670a6

Browse files
committed
p2p-findings: UC2 paired A/B does not reproduce the blog's Use Case 2
Four counterbalanced ladders (A1 B1 B2 A2), fresh cold rig and mechanism gate per run, arms differing by exactly one plugin. 22,080 requests, zero failures. load+P2P is worth +2.0% achieved throughput at saturation, which is smaller than the treatment arm's own run-to-run spread - individual stages swing both directions across repeats, so any single-run headline number is erased by its own repeat. Latency p50 is consistently better for the pull at all four saturation rates (-13.2% to -1.2%), a real but modest effect. More importantly the premise does not reproduce: the no-pull control does not collapse. It saturates cleanly at ~8.2 req/s across two full ladders with zero failures, against a working set that genuinely exceeds per-pod cache. Both arms hit the same prefill-bound ceiling. Also records two silent methodology traps found while running this: a ConfigMap-only helm upgrade does not restart the EPP, so the arm never actually switches; and a gate prefix already cached fleet-wide cannot demonstrate the pull, reading as a dead mechanism. Signed-off-by: nilig <nili.ifergan@gmail.com>
1 parent e702d00 commit 91670a6

10 files changed

Lines changed: 541 additions & 1 deletion

File tree

test/p2p-findings/RESULTS-4.md

Lines changed: 145 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
# P2P benchmarks, series 4: UC2 lookup-hang resolved; UC3/UC4 re-validation
1+
# P2P benchmarks, series 4: UC2 lookup-hang resolved; UC3/UC4 re-validation; UC2 paired A/B
2+
3+
> **Headline for the A/B section below**: on this rig and stack, `load+P2P`
4+
> does **not** reproduce the blog's Use Case 2 result. The pull is worth
5+
> about **+2% achieved throughput** at saturation — smaller than the
6+
> run-to-run spread — and the control arm (`load`, no pull) **does not
7+
> collapse**. See [UC2 paired A/B](#uc2-paired-ab-load-vs-loadp2p-4-ladders-counterbalanced).
28
39
This series closes the lookup-hang blocker that stalled the blog's UC2/3/4
410
re-validation ([[project_p2p_guide_docs_pr]], "BLOG UC2/3/4 RE-VALIDATION
@@ -298,3 +304,141 @@ re-derived tonight: Run N's actual throughput/latency comparison (TTFT
298304
parity verdict — "a capacity story, not latency, at this model scale") —
299305
that finding stands from the original investigation; this run only
300306
confirms the underlying mechanism hasn't regressed.
307+
308+
---
309+
310+
## UC2 paired A/B: `load` vs `load+P2P` (4 ladders, counterbalanced)
311+
312+
The UC2 section above establishes only that the lookup-hang is fixed — it
313+
was a single-arm run. It does **not** test the blog's Use Case 2 *claim*,
314+
which is comparative. This section does, with a proper paired design.
315+
316+
### Why this was needed
317+
318+
The single-arm UC2 numbers landed suspiciously close to the `load`-no-pull
319+
baseline recorded 2026-07-25, which would contradict the blog. That
320+
comparison was invalid (different harness, different engine build), so it
321+
could not settle anything either way — but it was close enough to parity to
322+
require a real A/B before publishing behind the claim.
323+
324+
### Design
325+
326+
- **Arms differ by exactly one plugin.** `uc2-values-load.yaml` (control)
327+
vs `uc2-values-load-p2p.yaml` (treatment); identical placement policy
328+
(`queue-scorer` + `kv-cache-utilization-scorer` + `weighted-random-picker`),
329+
the only delta being `p2p-source-producer`.
330+
- **Counterbalanced ABBA**: A1, B1, B2, A2 — so any monotonic cluster drift
331+
cancels rather than loading onto one arm.
332+
- **Fresh cold rig per ladder**: all 4 engine pods deleted and recreated
333+
before every run, so no arm inherits a warm cache from its predecessor.
334+
- **Mechanism gate before every ladder**, with a *unique* prefix per gate
335+
(see trap 2 below). Treatment gates confirmed the exact pull signature —
336+
1 owner pod at `load=0`, 3 non-owner pods each pulling 2.20 GB. Control
337+
gates confirmed `load=0` on all 4 pods against a *fresh* prefix, so the
338+
zero means "pull disabled", not "nothing left to pull".
339+
- Workload identical to `uc2_llama_pool.yaml.in`: 64-group shared-prefix
340+
pool, 16K-token system prompt, 256 question/output tokens, stages
341+
2/4/6/8/12/16/20/24 req/s x 60s. 5,520 requests per ladder, **22,080
342+
total, zero failures across all four**.
343+
- Driver: [configs/uc2-ab-load-vs-loadp2p/uc2_ladder2.py](configs/uc2-ab-load-vs-loadp2p/uc2_ladder2.py),
344+
which adds the metric the first pass lacked — **achieved throughput**
345+
(completions / stage wall-clock including drain), the quantity the blog's
346+
claim actually rests on.
347+
348+
### Result: achieved throughput (req/s), saturation stages
349+
350+
| rate | A1 | A2 | **A mean** | B1 | B2 | **B mean** | delta |
351+
|---:|---:|---:|---:|---:|---:|---:|---:|
352+
| 12 | 8.04 | 8.34 | **8.19** | 8.73 | 7.86 | **8.29** | +1.3% |
353+
| 16 | 8.30 | 8.25 | **8.28** | 8.09 | 8.41 | **8.25** | −0.3% |
354+
| 20 | 8.07 | 8.16 | **8.12** | 8.44 | 8.81 | **8.62** | +6.3% |
355+
| 24 | 8.06 | 8.22 | **8.14** | 8.56 | 7.88 | **8.22** | +1.0% |
356+
357+
Overall saturation mean: **A = 8.18, B = 8.35 → +2.0% for the pull.**
358+
359+
**That +2.0% is not distinguishable from noise.** The between-arm
360+
difference is 0.168 req/s; the within-arm run-to-run spread is 0.125
361+
(A) and 0.215 (B) req/s. The effect is smaller than the treatment arm's own
362+
variance. Individual stages swing both directions across repeats — rate 12
363+
went +8.6% in run 1 and −2.2% in run 2; rate 24 went +6.2% then −2.2%.
364+
Reporting any single stage would have produced a headline number that the
365+
repeat erases.
366+
367+
### Result: latency p50, saturation stages
368+
369+
| rate | A mean | B mean | delta |
370+
|---:|---:|---:|---:|
371+
| 12 | 26.0s | 22.6s | −13.2% |
372+
| 16 | 41.5s | 39.2s | −5.6% |
373+
| 20 | 51.2s | 50.6s | −1.2% |
374+
| 24 | 67.0s | 65.6s | −2.1% |
375+
376+
Latency is the better-behaved signal: the pull is ahead at **all four**
377+
saturation rates. Direction is consistent (4/4), so a real effect is
378+
plausible, but the magnitude is small outside rate 12 and the design has
379+
n=2 per arm — enough to reject a large effect, not enough to size a small
380+
one precisely.
381+
382+
### The premise does not reproduce
383+
384+
The blog's UC2 narrative rests on load-balancing *without* the pull being
385+
catastrophic once the working set exceeds per-pod cache — the series-1
386+
observation that the no-pull arm "COLLAPSES", grinding to ~46s p50 at the
387+
top of the ladder.
388+
389+
**That did not happen here.** The control arm saturated cleanly at ~8.2
390+
req/s and completed 11,040 requests across two full ladders with **zero
391+
failures** and zero restarts. It degrades with offered load exactly as a
392+
saturated system should, and the pull arm saturates at essentially the same
393+
ceiling. Both arms are prefill-capacity-bound at the same point; the pull
394+
shifts where time is spent, not how much total work the fleet can do.
395+
396+
The working-set premise itself does hold on this rig (64 x 16K = ~1.02M
397+
tokens against a 16 GiB per-pod CPU tier, ~131K tokens at Llama-8B's ~128
398+
KB/token — so the pool genuinely does not fit per-pod). The regime is
399+
right; the dramatic outcome is not there.
400+
401+
### What this does and does not license
402+
403+
- It does **not** support publishing the blog's UC2 comparison as-is on
404+
this stack.
405+
- It does **not** prove the pull is worthless — latency is consistently
406+
better, and this is one workload shape at one scale on a 4-pod rig.
407+
Series-1's collapse was observed on a different engine build with a
408+
different harness; something real may have changed (the upstream fixes
409+
altered exactly this path), or the original may have been measuring a
410+
configuration that no longer exists.
411+
- It **does** mean the UC2 numbers need re-derivation before they back a
412+
public claim, and that any re-derivation must be paired, counterbalanced,
413+
and report achieved throughput with its run-to-run spread.
414+
415+
### Two methodology traps found while running this
416+
417+
Both are silent — they produce plausible-looking numbers rather than errors —
418+
and both would corrupt any A/B that hits them.
419+
420+
1. **Switching EPP arms by editing the ConfigMap does not switch the arm.**
421+
`helm upgrade` with only a `pluginsCustomConfig` change leaves the
422+
Deployment podspec untouched, so no rollout fires, and the EPP reads its
423+
plugin config once at startup. The first Arm B attempt ran with Arm A's
424+
config still loaded (verified: zero occurrences of `p2p-source-producer`
425+
in the running pod's log, pod creation timestamp predating the upgrade).
426+
It looked like "the pull mechanism is dead". **An explicit
427+
`kubectl rollout restart deploy/llm-d-router-epp` is required after every
428+
arm switch**, and the loaded config should be asserted before trusting a
429+
gate.
430+
2. **A gate prefix already cached on every pod cannot demonstrate the pull.**
431+
Re-gating with the same prefix warms it everywhere; subsequent requests
432+
are local hits, so `load` stays 0 and `store` stops growing. This is
433+
indistinguishable from a broken mechanism by metrics alone. Gates must
434+
use a **fresh, never-served prefix each time**
435+
([configs/uc2-ab-load-vs-loadp2p/gate_fresh.py](configs/uc2-ab-load-vs-loadp2p/gate_fresh.py)).
436+
The tell that distinguishes the two cases: a frozen `store` counter means
437+
"everything is already cached", a growing one means "computing locally
438+
instead of pulling".
439+
440+
Raw per-stage output for all four ladders:
441+
[ab_armA_run1.log](configs/uc2-ab-load-vs-loadp2p/ab_armA_run1.log),
442+
[ab_armB_run1.log](configs/uc2-ab-load-vs-loadp2p/ab_armB_run1.log),
443+
[ab_armB_run2.log](configs/uc2-ab-load-vs-loadp2p/ab_armB_run2.log),
444+
[ab_armA_run2.log](configs/uc2-ab-load-vs-loadp2p/ab_armA_run2.log).
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# arm=armA-load-nopull-run1 endpoint=http://localhost:18090
2+
rate sent ok fail wall_s achieved p50_ms p95_ms
3+
2 120 120 0 61.6 1.95 2864 4266
4+
4 240 240 0 62.5 3.84 3031 5039
5+
6 360 360 0 62.9 5.73 4804 9373
6+
8 480 480 0 64.5 7.44 11008 16776
7+
12 720 720 0 89.6 8.04 26099 35114
8+
16 960 960 0 115.7 8.30 40475 57135
9+
20 1200 1200 0 148.8 8.07 49907 84931
10+
24 1440 1440 0 178.7 8.06 67598 115042
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# arm=armA-load-nopull-run2 endpoint=http://localhost:18090
2+
rate sent ok fail wall_s achieved p50_ms p95_ms
3+
2 120 120 0 61.9 1.94 2672 4081
4+
4 240 240 0 63.0 3.81 3481 6465
5+
6 360 360 0 62.8 5.73 4481 9526
6+
8 480 480 0 65.3 7.35 9863 14846
7+
12 720 720 0 86.4 8.34 25946 33424
8+
16 960 960 0 116.3 8.25 42461 59775
9+
20 1200 1200 0 147.1 8.16 52463 87269
10+
24 1440 1440 0 175.2 8.22 66414 112482
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# arm=armB-load-p2p-run1 endpoint=http://localhost:18090
2+
rate sent ok fail wall_s achieved p50_ms p95_ms
3+
2 120 120 0 62.8 1.91 4101 6215
4+
4 240 240 0 62.3 3.85 3393 5776
5+
6 360 360 0 63.9 5.64 5618 9426
6+
8 480 480 0 64.5 7.44 9685 14837
7+
12 720 720 0 82.4 8.73 20552 29434
8+
16 960 960 0 118.7 8.09 41626 58129
9+
20 1200 1200 0 142.2 8.44 50630 82616
10+
24 1440 1440 0 168.2 8.56 64595 107870
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# arm=armB-load-p2p-run2 endpoint=http://localhost:18090
2+
rate sent ok fail wall_s achieved p50_ms p95_ms
3+
2 120 120 0 62.0 1.94 2856 4217
4+
4 240 240 0 63.0 3.81 3349 5435
5+
6 360 360 0 63.0 5.71 4848 8466
6+
8 480 480 0 65.0 7.38 11171 16038
7+
12 720 720 0 91.7 7.86 24641 34601
8+
16 960 960 0 114.2 8.41 36701 56691
9+
20 1200 1200 0 136.2 8.81 50542 77456
10+
24 1440 1440 0 182.7 7.88 66587 114357
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import json, sys, time, urllib.request
2+
from concurrent.futures import ThreadPoolExecutor
3+
EP, NONCE = sys.argv[1], sys.argv[2]
4+
W = "route cache block prefix decode tier pull peer session lookup offload tensor page score filter epoch batch stream token merge".split()
5+
PREFIX = f"gate-{NONCE} unique pool document: " + " ".join(W[(hash(NONCE)+i) % len(W)] for i in range(16000))
6+
def one(i):
7+
body = json.dumps({"model":"meta-llama/Llama-3.1-8B-Instruct","prompt":PREFIX+f" q{i}?","max_tokens":8,"temperature":0}).encode()
8+
r = urllib.request.Request(EP+"/v1/completions", data=body, headers={"Content-Type":"application/json"})
9+
try:
10+
with urllib.request.urlopen(r, timeout=90) as resp:
11+
return bool(json.loads(resp.read()).get("choices"))
12+
except Exception as e:
13+
return str(e)[:60]
14+
print("seed:", one(0)); time.sleep(12)
15+
with ThreadPoolExecutor(max_workers=8) as ex:
16+
res = list(ex.map(one, range(1, 41)))
17+
print("burst ok:", sum(1 for x in res if x is True), "/ 40")
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
---
2+
# UC2 lookup-hang ladder re-test: #48021(merged, base image predates it so
3+
# fully overlaid) + #49877 (round-scoping fix for #49820) + #49850
4+
# (HIT_PENDING deadline fix for #49829, its scheduler.py already carries
5+
# #49671's merged crash fix -- confirmed via finished_signaled marker diff
6+
# against the live p2p-fix-49671 ConfigMap, so that older ConfigMap is
7+
# dropped). No defect12 reimplementation: the disconnect-recovery chain
8+
# (ClientRole.close -> failed_req_ids -> manager._failed_req_ids -> next
9+
# lookup() returns MISS) was verified directly against this code and the
10+
# duplicate-fetch trigger that caused it is what #49877 removes. This run
11+
# is the empirical confirmation of whether the 16-24 req/s hang recurs.
12+
apiVersion: apps/v1
13+
kind: Deployment
14+
metadata:
15+
name: uc2-llama
16+
namespace: nilig-p2p
17+
spec:
18+
replicas: 4
19+
selector:
20+
matchLabels:
21+
app: uc2-llama
22+
template:
23+
metadata:
24+
labels:
25+
app: uc2-llama
26+
llm-d.ai/role: decode
27+
llm-d.ai/inferenceServing: "true"
28+
llm-d.ai/uc: uc2
29+
spec:
30+
initContainers:
31+
- name: routing-proxy
32+
image: quay.io/niliguy/llm-d-router-disagg-sidecar:p2p-rename-43aea48e
33+
args:
34+
- --port=8000
35+
- --vllm-port=8200
36+
- --kv-connector=offloading
37+
- --p2p-connector-port=7777
38+
- --secure-proxy=false
39+
env:
40+
- name: POD_IP
41+
valueFrom:
42+
fieldRef:
43+
fieldPath: status.podIP
44+
ports:
45+
- containerPort: 8000
46+
name: proxy
47+
restartPolicy: Always
48+
securityContext:
49+
allowPrivilegeEscalation: false
50+
runAsNonRoot: true
51+
containers:
52+
- name: modelserver
53+
image: vllm/vllm-openai:nightly-1240c74c0a47473449cf0c3a9c2d87a1e159f73b
54+
command: ["sh", "-c", "rm -f /dev/shm/vllm_offload_*.mmap 2>/dev/null; exec vllm serve \"$@\"", "sh"]
55+
args:
56+
- meta-llama/Llama-3.1-8B-Instruct
57+
- --port=8200
58+
- --disable-access-log-for-endpoints=/health,/metrics,/v1/models
59+
- --tensor-parallel-size=1
60+
- --block-size=64
61+
- --max-model-len=20000
62+
- --gpu-memory-utilization=0.6
63+
- --kv-transfer-config
64+
- '{"kv_connector":"OffloadingConnector","kv_role":"kv_both","kv_connector_extra_config":{"spec_name":"TieringOffloadingSpec","cpu_bytes_to_use":17179869184,"offload_prompt_only":false,"secondary_tiers":[{"type":"p2p","host":"$(POD_IP)","port":7777}]}}'
65+
- --kv-events-config
66+
- '{"enable_kv_cache_events":true,"publisher":"zmq","endpoint":"$(KV_EVENTS_ENDPOINT)","topic":"kv@$(POD_IP):$(POD_PORT)@meta-llama/Llama-3.1-8B-Instruct"}'
67+
env:
68+
- name: HF_TOKEN
69+
valueFrom:
70+
secretKeyRef:
71+
name: llm-d-hf-token
72+
key: HF_TOKEN
73+
- name: POD_IP
74+
valueFrom:
75+
fieldRef:
76+
fieldPath: status.podIP
77+
- name: VLLM_P2P_SIDE_CHANNEL_HOST
78+
valueFrom:
79+
fieldRef:
80+
fieldPath: status.podIP
81+
- name: PYTHONHASHSEED
82+
value: "0"
83+
- name: KV_EVENTS_ENDPOINT
84+
value: "tcp://*:5556"
85+
- name: POD_PORT
86+
value: "8000"
87+
- name: HOME
88+
value: /tmp
89+
- name: VLLM_LOGGING_LEVEL
90+
value: DEBUG
91+
ports:
92+
- containerPort: 8200
93+
name: http
94+
- containerPort: 7777
95+
name: p2p
96+
- containerPort: 5556
97+
name: kv-events
98+
resources:
99+
limits:
100+
cpu: "8"
101+
memory: 64Gi
102+
nvidia.com/gpu: "1"
103+
requests:
104+
cpu: "8"
105+
memory: 64Gi
106+
nvidia.com/gpu: "1"
107+
volumeMounts:
108+
- name: dshm
109+
mountPath: /dev/shm
110+
- { name: gp2p, mountPath: /usr/local/lib/python3.12/dist-packages/vllm/v1/kv_offload/base.py, subPath: base.py }
111+
- { name: gp2p, mountPath: /usr/local/lib/python3.12/dist-packages/vllm/v1/kv_offload/tiering/p2p/manager.py, subPath: manager.py }
112+
- { name: gp2p, mountPath: /usr/local/lib/python3.12/dist-packages/vllm/v1/kv_offload/tiering/p2p/session/__init__.py, subPath: session___init__.py }
113+
- { name: gp2p, mountPath: /usr/local/lib/python3.12/dist-packages/vllm/v1/kv_offload/tiering/p2p/session/client.py, subPath: session_client.py }
114+
- { name: gp2p, mountPath: /usr/local/lib/python3.12/dist-packages/vllm/v1/kv_offload/tiering/p2p/session/protocol.py, subPath: session_protocol.py }
115+
- { name: gp2p, mountPath: /usr/local/lib/python3.12/dist-packages/vllm/v1/kv_offload/tiering/p2p/session/server.py, subPath: session_server.py }
116+
- { name: gp2p, mountPath: /usr/local/lib/python3.12/dist-packages/vllm/v1/kv_offload/tiering/p2p/session/session.py, subPath: session_session.py }
117+
- { name: gp2p, mountPath: /usr/local/lib/python3.12/dist-packages/vllm/v1/kv_offload/tiering/p2p/control/zmq.py, subPath: control_zmq.py }
118+
- { name: gp2p, mountPath: /usr/local/lib/python3.12/dist-packages/vllm/v1/kv_offload/tiering/p2p/control/base.py, subPath: control_base.py }
119+
- { name: gp2p, mountPath: /usr/local/lib/python3.12/dist-packages/vllm/distributed/kv_transfer/kv_connector/v1/offloading/scheduler.py, subPath: offloading_scheduler.py }
120+
- { name: gp2p, mountPath: /usr/local/lib/python3.12/dist-packages/vllm/distributed/kv_transfer/kv_connector/v1/offloading/metrics.py, subPath: offloading_metrics.py }
121+
readinessProbe:
122+
httpGet: { path: /health, port: 8200 }
123+
initialDelaySeconds: 30
124+
periodSeconds: 10
125+
failureThreshold: 60
126+
volumes:
127+
- name: dshm
128+
emptyDir: { medium: Memory, sizeLimit: 24Gi }
129+
- name: gp2p
130+
configMap: { name: combined-overlay-uc2resume }
131+
---
132+
apiVersion: v1
133+
kind: Service
134+
metadata:
135+
name: uc2-llama
136+
namespace: nilig-p2p
137+
spec:
138+
selector:
139+
app: uc2-llama
140+
ports:
141+
- { name: proxy, port: 8000, targetPort: 8000 }
142+
- { name: kv-events, port: 5556, targetPort: 5556 }

0 commit comments

Comments
 (0)