|
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). |
2 | 8 |
|
3 | 9 | This series closes the lookup-hang blocker that stalled the blog's UC2/3/4 |
4 | 10 | 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 |
298 | 304 | parity verdict — "a capacity story, not latency, at this model scale") — |
299 | 305 | that finding stands from the original investigation; this run only |
300 | 306 | 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). |
0 commit comments