Compare commits
3 Commits
f784e49c07
...
0881942cf3
| Author | SHA1 | Date | |
|---|---|---|---|
| 0881942cf3 | |||
| 0e82612100 | |||
| 8ac41a8684 |
187
analysis/characterization/agentic_dispatch_coupling.md
Normal file
187
analysis/characterization/agentic_dispatch_coupling.md
Normal file
@@ -0,0 +1,187 @@
|
||||
# Agentic Dispatch Coupling: Why Session-Sequential Replay is the Realistic Mode
|
||||
|
||||
Date: 2026-05-26
|
||||
Owner: characterization
|
||||
Status: methodology note for paper framing
|
||||
|
||||
## The observation
|
||||
|
||||
In `replayer/replay.py:282-287`, turn N of a session fires at:
|
||||
|
||||
```
|
||||
t_fire(N) = max(
|
||||
turn_N_trace_timestamp, # what the trace asked for
|
||||
turn_{N-1}_finish_wall_clock, # but turn N-1 must complete first
|
||||
)
|
||||
```
|
||||
|
||||
When the system is fast, the second term loses → turn N fires at its trace
|
||||
timestamp → the replay matches the captured trace. When the system is slow,
|
||||
the second term dominates → turn N fires *immediately* after turn N-1
|
||||
completes, with the trace timestamp ignored. The session's "inter-turn
|
||||
think time" collapses to zero.
|
||||
|
||||
A first reading flags this as a benchmark concern: under saturation the
|
||||
arrival process becomes policy-dependent, so cross-policy latency
|
||||
comparisons are confounded by a feedback loop (slow policy → longer
|
||||
sessions → more concurrent in-flight → harder system → slower latency).
|
||||
|
||||
This note argues the opposite: **the trace-replayer's behavior is the
|
||||
correct model of agentic workloads, and the feedback loop is a real
|
||||
property of production systems, not a methodology artifact**.
|
||||
|
||||
## Why agentic workloads do not have user think-time
|
||||
|
||||
In chat workloads, the turn N+1 message is composed by a human reading the
|
||||
turn N response. The inter-turn gap is dominated by human reading + typing
|
||||
speed, which is independent of how fast the server replied. The trace's
|
||||
timestamp captures the human cadence and is a faithful arrival process.
|
||||
|
||||
In **agentic workloads**, turn N+1 is generated by:
|
||||
- A tool-call response feeding back into the model context
|
||||
- An autonomous loop deciding the next action
|
||||
- A planner / executor stepping to the next subgoal
|
||||
|
||||
None of these wait for a human. Turn N+1 fires as soon as the
|
||||
infrastructure can hand the previous turn's output back to the next
|
||||
inference step. There is no think-time floor.
|
||||
|
||||
This means: in a real agentic system, **the wall-clock time between turn N
|
||||
finish and turn N+1 dispatch is essentially zero**. If the model serving
|
||||
infrastructure slows down (high TTFT or E2E for turn N), turn N+1's
|
||||
dispatch slips by the same amount — exactly the behavior the replayer
|
||||
exhibits.
|
||||
|
||||
## What B3's session-sequential dispatch is actually measuring
|
||||
|
||||
B3's trace replayer drives a workload that:
|
||||
- preserves the *causal structure* of the original trace (which turns
|
||||
belong to which session and in what order),
|
||||
- uses the *original timestamps as a lower bound* (turn N+1 cannot fire
|
||||
before its trace timestamp),
|
||||
- *but* lets turn N+1 fire immediately when the system has fallen behind.
|
||||
|
||||
For an agentic workload, this is the right model:
|
||||
|
||||
1. The captured trace's timestamps reflect the **production system's
|
||||
actual response speed at capture time** — they already encode the
|
||||
round-trip time the model + tool stack delivered.
|
||||
2. When we replay against a *different* policy, what we want to measure
|
||||
is "what wall-clock would this session take under policy X" — which
|
||||
includes the same tool-call-driven cadence: each next turn fires as
|
||||
soon as it can.
|
||||
3. The "inter-turn gap" is not a fixed delay we should respect; it is an
|
||||
artifact of the captured system's speed that we are explicitly trying
|
||||
to replace.
|
||||
|
||||
So the replayer's behavior is not "broken under saturation"; it is
|
||||
modeling the agentic semantic correctly: **no think-time, sequential
|
||||
within session, fire-immediately when ready**.
|
||||
|
||||
## The feedback loop is a real production phenomenon
|
||||
|
||||
Once we accept the agentic semantic, the so-called "dispatch slip
|
||||
artifact" is not an artifact — it is a real system behavior:
|
||||
|
||||
```
|
||||
slow policy
|
||||
→ each turn takes longer
|
||||
→ each session lives in the system longer
|
||||
→ at any moment, more sessions are concurrently in-flight
|
||||
→ 8 workers' KV / queue pressure is higher
|
||||
→ each request gets less per-worker capacity
|
||||
→ each turn takes even longer
|
||||
→ ...
|
||||
```
|
||||
|
||||
By Little's Law: `N_concurrent ≈ session_arrival_rate × mean_session_lifetime`.
|
||||
|
||||
In our B3 data:
|
||||
- lmetric: mean session lifetime is much longer than the original
|
||||
trace's ~600 s span (lmetric's 1214-request replay took 49 min wall
|
||||
clock — sessions stayed alive ~8× longer than the trace captured).
|
||||
- unified: sessions drain ~3× faster than lmetric.
|
||||
|
||||
So under unified, the 8-worker pool sees fewer concurrent sessions than
|
||||
under lmetric — and this is **what production would also see** if the
|
||||
operator switched routing policies on the same incoming agentic load.
|
||||
|
||||
**This is not a fairness violation**. It is a faithful reflection of:
|
||||
"a faster routing policy is faster both because of its per-request
|
||||
behavior AND because it reduces the steady-state concurrent load it
|
||||
inflicts on itself".
|
||||
|
||||
A user running an agentic system *does* benefit from both effects when
|
||||
they pick a better policy. The combined "policy × system-feedback" gain
|
||||
is what the user actually experiences.
|
||||
|
||||
## What this means for B3 and B4 in the paper
|
||||
|
||||
| | B3 trace-driven replay | B4 open-loop Poisson |
|
||||
|---|---|---|
|
||||
| Arrival process | original trace timestamps with session-sequential "fire-on-finish" | Poisson session inter-arrival at fixed λ |
|
||||
| Inter-turn think-time | none (matches agentic) | none (matches agentic) |
|
||||
| Session lifetime under load | *grows* with policy slowness (feedback) | *fixed* by trace template plus per-turn latency |
|
||||
| What latency at p90 measures | end-user latency under agentic feedback amplification | per-request behavior at the operator-chosen load level |
|
||||
| What "fair across policies" means | same trace, same total session set; arrival process is policy-dependent **on purpose** | same λ, decoupled from policy throughput |
|
||||
| When to use it | "if we run this real captured load through our system, what does the user see" | "what is the max sustainable rate (SRR) before SLO breaks, per policy" |
|
||||
|
||||
The two are **complementary**, not "B3 is unfair and B4 fixes it":
|
||||
|
||||
- **B3 answers the "in-production replay" question** — including feedback amplification, which agentic users will actually experience.
|
||||
- **B4 answers the "saturation envelope" question** — what's the policy's intrinsic throughput at fixed load.
|
||||
|
||||
A paper that drops B3 in favor of B4 would understate how much the
|
||||
**combined** effect (policy + feedback) actually helps the user. A paper
|
||||
that drops B4 in favor of B3 would conflate the two effects and prevent
|
||||
a "policy X sustains higher λ" statement.
|
||||
|
||||
## Recommended paper framing
|
||||
|
||||
1. **B3 is the production-replay experiment**. Report latency percentiles
|
||||
as "end-to-end under captured agentic load with no-think-time
|
||||
sequencing". Acknowledge that the *combined* gap (e.g. unified TTFT
|
||||
p90 = 7.24 s vs lmetric 15.6 s) reflects both policy and feedback;
|
||||
call this out, do not hide it.
|
||||
|
||||
2. **B4 is the controlled-load experiment**. Report `SRR_max` per policy
|
||||
under per-class SLO. This is the experiment that decouples policy
|
||||
from feedback and gives a sustainable-rate ranking.
|
||||
|
||||
3. **The feedback amplification itself is a finding to call out**. It is
|
||||
the reason why a "marginally better" routing policy (e.g. unified
|
||||
over lmetric in microbenchmarks) can deliver a much bigger gap in
|
||||
production (here ~2×): the feedback halves the in-flight count which
|
||||
compounds on top of the per-request improvement.
|
||||
|
||||
4. **The contrast with chat workloads is a paper section** (or at least a
|
||||
paragraph). Chat workloads have human think-time bounded by reading
|
||||
speed, so the feedback loop is partially broken: even if the server
|
||||
slows down, the user-driven inter-turn delay still puts a floor on
|
||||
how concentrated the load can become. Agentic workloads remove that
|
||||
floor.
|
||||
|
||||
## Open questions
|
||||
|
||||
- **Is the feedback amplification quantifiable from B3 alone?** We have
|
||||
total wall-clock per policy and per-session lifetime distributions; we
|
||||
can in principle attribute the policy-vs-feedback split by comparing
|
||||
B3's saturated-replay p90 to B4's at-fixed-λ p90 (when B4 runs).
|
||||
- **Does it matter that the original trace was captured under one
|
||||
policy's behavior?** The trace's timestamps were the production
|
||||
system's output at capture time. When we replay against a slower
|
||||
policy, we are asking "what if this same set of session+turns ran on
|
||||
a worse policy" — and the answer is "the sessions would live longer".
|
||||
This is precisely the counterfactual we want.
|
||||
- **What happens if real tools have variable per-call latency?** Our
|
||||
replayer assumes turn N+1 fires the instant turn N finishes. Real
|
||||
agentic systems have some tool-execution time between turns. This is
|
||||
a quantitative correction (raises the floor on inter-turn gap), not a
|
||||
qualitative one — the feedback loop still applies, just with a higher
|
||||
baseline.
|
||||
|
||||
## Cross-reference
|
||||
|
||||
- `replayer/replay.py:282-287` — the dispatch rule
|
||||
- `analysis/characterization/window_1_results.md` §"What Window 1 does not answer" — current treatment as caveat
|
||||
- `analysis/claude_characterization_work_plan.md` §B4 — open-loop Poisson loadgen as the orthogonal measurement
|
||||
@@ -15,6 +15,7 @@ sweep, B2 PD-colo interference microbench).
|
||||
| Same-worker prefill-decode interference is causal, not correlation. | `supported` | B2 microbench: different-worker control idx 0.92-1.02 across 32× prefill-size variation; same-worker TTFT idx scales 2.15× (2k) → 218× (65k). window_1_results/b2_sweep_summary.json. | — | Synthetic decode load (256-token prompts at 4 req/s) bounds the realism; production behavior is layered on top of B3. |
|
||||
| The cost of same-worker prefill interference migrates from TPOT to TTFT as prefill size grows past the chunked-prefill horizon. | `supported` | B2 same-worker TPOT p90 idx peaks at 32k (7.89×) and *drops* at 65k (2.26×), while TTFT idx grows monotonically (94.6× → 218×) and TPOT p99 grows monotonically (59 → 169.5 ms). See window_1_results.md "TPOT idx peaks at 32k, not 65k". | — | SLO thresholds for TTFT and TPOT cannot be the same under PD-colo; this should be reflected in B4 SRR sweep design. |
|
||||
| Hard session affinity (`sticky`) inflates same-worker prefill-decode interference. | `supported` | sticky interference_index 13.65 vs lmetric 6.53; sticky's slow-request breakdown 57% same-worker overlap vs lmetric 23%. | — | Confirms the B2 causal claim observed at the system level. |
|
||||
| Heavy-tail sessions are a contributor to hot-spot but not the sole cause. | `supported` | Cap-8 trace (37% requests dropped) reduces hotspot_index only 13% (2.24 → 1.94). | Run capped under unified to see whether unified's hotspot also persists. | Reviewer might counter that cap=8 is too soft; a stricter cap could be tried. |
|
||||
| Heavy-tail sessions are a contributor to hot-spot but not the sole cause. | `supported` | Cap-8 trace (37% requests dropped) reduces hotspot_index only ~10% (2.253 → 2.020 after fixing the `joined_analysis.hotspot_index` median bug). | Run capped under unified to see whether unified's hotspot also persists. | Reviewer might counter that cap=8 is too soft; a stricter cap could be tried. |
|
||||
| B3 saturated-replay latency gaps include an agentic dispatch-coupling feedback term, which is intentional and matches production. | `supported, framed as feature` | `replayer/replay.py:282-287` fires turn N+1 immediately when turn N is behind schedule (no human think-time). Under saturation, slow policies have longer mean session lifetime, more concurrent in-flight, higher worker pressure — so B3 latency gaps reflect "policy + feedback amplification", which is what a production operator switching policies on agentic workload experiences. See `analysis/characterization/agentic_dispatch_coupling.md`. | Run B4 open-loop Poisson at fixed λ to get the orthogonal "controlled-load" measurement; both are needed, not "B4 fixes B3". | Some reviewers will read "non-Poisson arrivals" as benchmark crime; the rebuttal is the agentic-vs-chat workload distinction. |
|
||||
| SRR per policy under SLO is not yet measured. | `not_yet_supported` | B3 was driven by trace timestamps with strict session sequentiality; saturation is reached but not parameterized. | Run B4 with the A4 open-loop Poisson loadgen, per-class SLO, 5 policies × λ binary search. | Without B4 the paper cannot claim "policy X sustains higher load than Y". |
|
||||
| Failure attribution near SRR boundary is not yet measured. | `not_yet_supported` | B5 protocol exists; no runs. | After B4, rerun each policy at 0.9× / 1.0× / 1.1× of its SRR_max with the same instrumentation, label slow requests. | The current `joined_analysis.label_slow_requests` is the labeler; needs SRR boundaries to point at. |
|
||||
|
||||
@@ -352,7 +352,10 @@ def hotspot_index(joined: list[JsonDict]) -> JsonDict:
|
||||
p90s_q = sorted(worker_p90_q.values())
|
||||
idx = None
|
||||
if len(p90s_q) >= 2:
|
||||
median = p90s_q[len(p90s_q) // 2]
|
||||
# True median: average of two middle values for even-length lists.
|
||||
# Previously used sorted[n//2] which returns the ~60th percentile
|
||||
# for n=8 and systematically under-states hotspot_index.
|
||||
median = statistics.median(p90s_q)
|
||||
if median > 0:
|
||||
idx = max(p90s_q) / median
|
||||
|
||||
|
||||
@@ -15,9 +15,10 @@ Per-policy artifacts under `window_1_results/`. Figures under `window_1_results/
|
||||
| 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 |
|
||||
| Hybrid affinity (Unified) breaks the locality-vs-latency tradeoff | **supported** | unified hits 79.4% APC and TTFT p90 7.35 s (lmetric 15.67 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) |
|
||||
| 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 ~10% (2.253→2.020) |
|
||||
| The agentic dispatch coupling amplifies policy gaps under saturation | **supported, framed as feature** | Slow policy → longer session lifetime → more concurrent in-flight → harder system. B3 measures the combined policy + feedback effect, which is what an agentic operator experiences. See `agentic_dispatch_coupling.md`. |
|
||||
|
||||
## B1' Workload characterization
|
||||
|
||||
@@ -66,14 +67,26 @@ Gap "any − intra" is 0.7 pp → no meaningful cross-session sharing in this tr
|
||||
|
||||
| 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 |
|
||||
| lmetric | 0.94 / 15.67 / 53.57 | 8.9 / 21.2 / 176.9 | 2.75 / 24.82 / 79.83 | 56.9% | 6.53 | 2.253 | 295 |
|
||||
| load_only | 1.26 / 20.20 / 52.84 | 9.2 / 26.9 / 320.7 | 3.59 / 33.46 / 93.93 | 54.1% | 9.16 | **1.294** | 379 |
|
||||
| sticky | 0.54 / 18.02 / 74.09 | 8.9 / 36.4 / 357.2 | 2.08 / 34.63 / 134.36 | 77.2% | **13.65** | 2.728 | 234 |
|
||||
| **unified** | **0.50 / 7.35 / 42.34** | 8.1 / 17.1 / 118.3 | **1.75 / 18.03 / 68.43** | **79.4%** | n/a* | **3.667** | **189** |
|
||||
| capped | 1.20 / 12.83 / 46.62 | 7.2 / 16.0 / 101.7 | 2.59 / 21.25 / 73.79 | 31.6% | 6.33 | 2.020 | 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.
|
||||
|
||||
> **Methodology note (read before interpreting latency comparisons)**: B3 uses
|
||||
> session-sequential trace dispatch — turn N+1 fires the instant turn N
|
||||
> completes when the trace timestamp has already passed. This is the right
|
||||
> model of agentic workloads (tool-call driven, no user think-time), but it
|
||||
> means under saturation each policy's effective in-flight session count is
|
||||
> a function of its own per-turn latency (slower policy → longer mean
|
||||
> session lifetime → more concurrent in-flight). The reported gaps are
|
||||
> therefore "policy + agentic-feedback-amplification", which is what a
|
||||
> production agentic operator would experience when switching policies.
|
||||
> See `agentic_dispatch_coupling.md` for the full argument. B4 will report
|
||||
> the orthogonal "fixed-λ open-loop" measurement.
|
||||
|
||||
**Mechanism indices**
|
||||
- `interference_index` = TPOT_p90(decode overlapping same-worker prefill) / TPOT_p90(clean)
|
||||
- `hotspot_index` = max(worker TTFT p90) / median(worker TTFT p90)
|
||||
@@ -85,10 +98,10 @@ Figures: `fig_b3_latency_bars.png`, `fig_b3_apc_vs_upper.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.
|
||||
- **load_only** strips cache awareness. Hot-spot drops to 1.294 (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.
|
||||
- **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.667`, 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 ~10% (2.253 → 2.020). 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`)
|
||||
|
||||
@@ -168,11 +181,56 @@ Optional / paper-polish runs (not blocking the story):
|
||||
4. B2 with the proxy in path — measure whether the production cache_aware routing actually pushes prefill and decode onto different workers in practice.
|
||||
5. 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
|
||||
## Limitations (read this before quoting B3 numbers)
|
||||
|
||||
- **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.
|
||||
1. **Agentic dispatch coupling is by design**. B3 is the
|
||||
"production-replay under captured agentic load" experiment, not the
|
||||
"controlled-load envelope" experiment. Latency p90 reflects both
|
||||
per-request policy effect AND the agentic feedback amplification
|
||||
(slow policy → longer mean session lifetime → more concurrent
|
||||
in-flight). Both contributions are real and visible to a production
|
||||
operator; **the paper must report both, not subtract one**. See
|
||||
`agentic_dispatch_coupling.md`. The orthogonal "fixed-λ Poisson"
|
||||
measurement is B4.
|
||||
|
||||
2. **B3 `interference_index` is a binary indicator**. A decode is
|
||||
labeled "overlap" iff *any* other request's prefill exists on the
|
||||
chosen worker during `[t_first_token, t_finish]`, regardless of
|
||||
prefill size. B2's per-prefill-size cells (2k = 1.16×, 65k = 2.26×)
|
||||
cannot be directly read off B3's aggregate numbers (lmetric 6.53,
|
||||
sticky 13.65). The B3 numbers are size-weighted averages of the
|
||||
per-cell signal, valid for *within-B3 cross-policy* comparison but
|
||||
not for direct cross-batch numerical comparison with B2.
|
||||
|
||||
3. **Hot-sweep cache contamination (low)**: `lmetric` ran from cold;
|
||||
`load_only` and `sticky` ran on the same 8 vLLMs without restart.
|
||||
First-turn cached_tokens verification puts empirical contamination
|
||||
at < 1% APC, well below the cross-policy gaps. `unified` and
|
||||
`capped` were rerun cold-start specifically to remove any residual
|
||||
concern.
|
||||
|
||||
4. **Unified's `interference_index` is missing**. 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. **Implication**:
|
||||
unified's slow-request mechanism breakdown (rows 0 / 116 / 18 / 55
|
||||
for same-worker overlap / hot worker queue / cache miss / unknown)
|
||||
has the "same-worker overlap" label *unrecoverable* and forced into
|
||||
the catch-all buckets — do not read unified's failure attribution
|
||||
as causal.
|
||||
|
||||
5. **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.
|
||||
|
||||
6. **Reuse decomposition (intra/cross/shared/unclassified) is
|
||||
per-cached-token only in expectation** — `joined_analysis.py`
|
||||
distributes a request's `cached_tokens` count uniformly across its
|
||||
`hash_ids` and classifies block-by-block. For the w600 trace with
|
||||
<1% cross-session sharing the qualitative split is robust; for
|
||||
workloads with mixed-class hashes within a single request the
|
||||
classifier should be revisited.
|
||||
|
||||
## Reproduction commands
|
||||
|
||||
|
||||
@@ -4,18 +4,18 @@
|
||||
"policy": "capped",
|
||||
"n_ok": 770,
|
||||
"n_total": 770,
|
||||
"ttft_p50_s": 1.195636051998008,
|
||||
"ttft_p90_s": 12.762421467981767,
|
||||
"ttft_p99_s": 46.05476947501302,
|
||||
"tpot_p50_s": 0.007229394937166944,
|
||||
"tpot_p90_s": 0.015995440982929352,
|
||||
"tpot_p99_s": 0.10145225453431651,
|
||||
"e2e_p50_s": 2.5921602529706433,
|
||||
"e2e_p90_s": 21.238469071977306,
|
||||
"e2e_p99_s": 73.38509433099534,
|
||||
"ttft_p50_s": 1.1989156164927408,
|
||||
"ttft_p90_s": 12.827629912580612,
|
||||
"ttft_p99_s": 46.61752380923125,
|
||||
"tpot_p50_s": 0.007231239004497606,
|
||||
"tpot_p90_s": 0.015998617687440243,
|
||||
"tpot_p99_s": 0.11515370831539476,
|
||||
"e2e_p50_s": 2.598489043477457,
|
||||
"e2e_p90_s": 21.245602010778384,
|
||||
"e2e_p99_s": 74.60736650204846,
|
||||
"apc_ratio": 0.3158312503528108,
|
||||
"interference_index": 6.331064378362814,
|
||||
"hotspot_index_ttft_p90": 1.9366915542605314,
|
||||
"hotspot_index_ttft_p90": 2.0204268015410918,
|
||||
"reuse_intra_frac": 0.9192657105586233,
|
||||
"reuse_cross_frac": 0.0602232594931501,
|
||||
"n_slow": 185,
|
||||
@@ -30,18 +30,18 @@
|
||||
"policy": "lmetric",
|
||||
"n_ok": 1214,
|
||||
"n_total": 1214,
|
||||
"ttft_p50_s": 0.9369571270071901,
|
||||
"ttft_p90_s": 15.592678204004187,
|
||||
"ttft_p99_s": 52.95170431700535,
|
||||
"tpot_p50_s": 0.008851506907892485,
|
||||
"tpot_p90_s": 0.02120516549011311,
|
||||
"tpot_p99_s": 0.17592118933357093,
|
||||
"e2e_p50_s": 2.7527842019917443,
|
||||
"e2e_p90_s": 24.75416105298791,
|
||||
"e2e_p99_s": 79.61890332301846,
|
||||
"ttft_p50_s": 0.9387824369769078,
|
||||
"ttft_p90_s": 15.671339168207492,
|
||||
"ttft_p99_s": 53.56683189840049,
|
||||
"tpot_p50_s": 0.008854518407308914,
|
||||
"tpot_p90_s": 0.02122720699121469,
|
||||
"tpot_p99_s": 0.18280341184277568,
|
||||
"e2e_p50_s": 2.754255389008904,
|
||||
"e2e_p90_s": 24.8209177934099,
|
||||
"e2e_p99_s": 80.59924928059091,
|
||||
"apc_ratio": 0.5694312382571595,
|
||||
"interference_index": 6.530231061794441,
|
||||
"hotspot_index_ttft_p90": 2.237981740718548,
|
||||
"hotspot_index_ttft_p90": 2.252837147833725,
|
||||
"reuse_intra_frac": 0.9321238805590836,
|
||||
"reuse_cross_frac": 0.05679481258506571,
|
||||
"n_slow": 295,
|
||||
@@ -56,18 +56,18 @@
|
||||
"policy": "load_only",
|
||||
"n_ok": 1214,
|
||||
"n_total": 1214,
|
||||
"ttft_p50_s": 1.2542553890380077,
|
||||
"ttft_p90_s": 20.14692750602262,
|
||||
"ttft_p99_s": 52.64810254302574,
|
||||
"tpot_p50_s": 0.00923045912795929,
|
||||
"tpot_p90_s": 0.02672785480314115,
|
||||
"tpot_p99_s": 0.3207044094773148,
|
||||
"e2e_p50_s": 3.584156609023921,
|
||||
"e2e_p90_s": 33.42658680601744,
|
||||
"e2e_p99_s": 93.91839688795153,
|
||||
"ttft_p50_s": 1.2609447415161412,
|
||||
"ttft_p90_s": 20.197147866390882,
|
||||
"ttft_p99_s": 52.84285237012196,
|
||||
"tpot_p50_s": 0.009231464695980247,
|
||||
"tpot_p90_s": 0.026851662550158716,
|
||||
"tpot_p99_s": 0.3211630676943426,
|
||||
"e2e_p50_s": 3.58568156149704,
|
||||
"e2e_p90_s": 33.459180271782685,
|
||||
"e2e_p99_s": 93.95083751494239,
|
||||
"apc_ratio": 0.5412093853102866,
|
||||
"interference_index": 9.16424627504275,
|
||||
"hotspot_index_ttft_p90": 1.1400531308102801,
|
||||
"hotspot_index_ttft_p90": 1.2940319990630569,
|
||||
"reuse_intra_frac": 0.9353191550754928,
|
||||
"reuse_cross_frac": 0.053372184678592026,
|
||||
"n_slow": 379,
|
||||
@@ -82,18 +82,18 @@
|
||||
"policy": "sticky",
|
||||
"n_ok": 1214,
|
||||
"n_total": 1214,
|
||||
"ttft_p50_s": 0.540947844972834,
|
||||
"ttft_p90_s": 18.016640832996927,
|
||||
"ttft_p99_s": 71.37327494798228,
|
||||
"tpot_p50_s": 0.00894752275507555,
|
||||
"tpot_p90_s": 0.0360956137329512,
|
||||
"tpot_p99_s": 0.34523129428917954,
|
||||
"e2e_p50_s": 2.0788628259906545,
|
||||
"e2e_p90_s": 34.605129147996195,
|
||||
"e2e_p99_s": 133.5824547969969,
|
||||
"ttft_p50_s": 0.5415176274836995,
|
||||
"ttft_p90_s": 18.021296651283045,
|
||||
"ttft_p99_s": 74.09429564891524,
|
||||
"tpot_p50_s": 0.008952101894096181,
|
||||
"tpot_p90_s": 0.03641285916619554,
|
||||
"tpot_p99_s": 0.35152006935195085,
|
||||
"e2e_p50_s": 2.081947358994512,
|
||||
"e2e_p90_s": 34.62592205510591,
|
||||
"e2e_p99_s": 139.68334607904353,
|
||||
"apc_ratio": 0.7720092868396378,
|
||||
"interference_index": 13.651718321568111,
|
||||
"hotspot_index_ttft_p90": 2.3493858974059214,
|
||||
"hotspot_index_ttft_p90": 2.727756623171119,
|
||||
"reuse_intra_frac": 0.9327723488279339,
|
||||
"reuse_cross_frac": 0.05495149683864246,
|
||||
"n_slow": 234,
|
||||
@@ -109,17 +109,17 @@
|
||||
"n_ok": 1213,
|
||||
"n_total": 1214,
|
||||
"ttft_p50_s": 0.4997710260213353,
|
||||
"ttft_p90_s": 7.239999514014926,
|
||||
"ttft_p99_s": 42.022206099005416,
|
||||
"ttft_p90_s": 7.345769894809922,
|
||||
"ttft_p99_s": 42.34170345296613,
|
||||
"tpot_p50_s": 0.008079791456705824,
|
||||
"tpot_p90_s": 0.017107906969874808,
|
||||
"tpot_p99_s": 0.11808861252148231,
|
||||
"tpot_p90_s": 0.017110194704198407,
|
||||
"tpot_p99_s": 0.12655874612209597,
|
||||
"e2e_p50_s": 1.7495028690318577,
|
||||
"e2e_p90_s": 17.893827292020433,
|
||||
"e2e_p99_s": 68.18008507299237,
|
||||
"e2e_p90_s": 18.033410895219994,
|
||||
"e2e_p99_s": 68.80023987947489,
|
||||
"apc_ratio": 0.794261466256467,
|
||||
"interference_index": null,
|
||||
"hotspot_index_ttft_p90": 3.3497107140827365,
|
||||
"hotspot_index_ttft_p90": 3.667136528736114,
|
||||
"reuse_intra_frac": 0.9311187350942534,
|
||||
"reuse_cross_frac": 0.056702150437367635,
|
||||
"n_slow": 189,
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 40 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 58 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 52 KiB |
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"hotspot_index_ttft_p90": 1.9366915542605314,
|
||||
"hotspot_index_ttft_p90": 2.0204268015410918,
|
||||
"per_worker_latency_p90_s": {
|
||||
"http://127.0.0.1:8000": 23.81083881931848,
|
||||
"http://127.0.0.1:8001": 18.139674991380897,
|
||||
@@ -21,4 +21,4 @@
|
||||
"http://127.0.0.1:8007": 9.661995008389932
|
||||
},
|
||||
"status": "supported"
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"hotspot_index_ttft_p90": 2.237981740718548,
|
||||
"hotspot_index_ttft_p90": 2.252837147833725,
|
||||
"per_worker_latency_p90_s": {
|
||||
"http://127.0.0.1:8000": 34.71445541951107,
|
||||
"http://127.0.0.1:8001": 21.922988962882666,
|
||||
@@ -21,4 +21,4 @@
|
||||
"http://127.0.0.1:8007": 11.777357225219024
|
||||
},
|
||||
"status": "supported"
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"hotspot_index_ttft_p90": 1.1400531308102801,
|
||||
"hotspot_index_ttft_p90": 1.2940319990630569,
|
||||
"per_worker_latency_p90_s": {
|
||||
"http://127.0.0.1:8000": 33.51168999259829,
|
||||
"http://127.0.0.1:8001": 29.20308109278556,
|
||||
@@ -21,4 +21,4 @@
|
||||
"http://127.0.0.1:8007": 13.95184187250561
|
||||
},
|
||||
"status": "supported"
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"hotspot_index_ttft_p90": 2.3493858974059214,
|
||||
"hotspot_index_ttft_p90": 2.727756623171119,
|
||||
"per_worker_latency_p90_s": {
|
||||
"http://127.0.0.1:8000": 30.185792533413043,
|
||||
"http://127.0.0.1:8001": 47.49661003401852,
|
||||
@@ -21,4 +21,4 @@
|
||||
"http://127.0.0.1:8007": 2.4984901855932535
|
||||
},
|
||||
"status": "supported"
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"hotspot_index_ttft_p90": 3.3497107140827365,
|
||||
"hotspot_index_ttft_p90": 3.667136528736114,
|
||||
"per_worker_latency_p90_s": {
|
||||
"http://127.0.0.1:8000": 41.42001512600109,
|
||||
"http://127.0.0.1:8001": 12.4878579101933,
|
||||
@@ -21,4 +21,4 @@
|
||||
"http://127.0.0.1:8007": 7.772977900883419
|
||||
},
|
||||
"status": "supported"
|
||||
}
|
||||
}
|
||||
@@ -65,9 +65,17 @@ from pathlib import Path
|
||||
|
||||
sweep = Path("$SWEEP_DIR")
|
||||
def pct(vals, p):
|
||||
# Linear-interpolated percentile, matches metrics._percentile.
|
||||
# Previously used floor-indexed sorted[int(p*(n-1))] which is
|
||||
# inconsistent with how the same percentile is computed elsewhere.
|
||||
if not vals: return None
|
||||
vs = sorted(vals)
|
||||
return vs[int(p * (len(vs)-1))]
|
||||
if len(vs) == 1: return vs[0]
|
||||
rank = p * (len(vs) - 1)
|
||||
lo = int(rank)
|
||||
hi = min(lo + 1, len(vs) - 1)
|
||||
frac = rank - lo
|
||||
return vs[lo] * (1 - frac) + vs[hi] * frac
|
||||
|
||||
rows = []
|
||||
for sub in sorted(sweep.iterdir()):
|
||||
|
||||
@@ -161,7 +161,9 @@ for policy in $POLICIES; do
|
||||
run_policy "$policy" "$TRACE"
|
||||
done
|
||||
|
||||
# 3) Capped variant on lmetric
|
||||
# 3) Capped variant: lmetric picker on a per-session turn-capped trace.
|
||||
# The directory label is "capped" but the proxy must launch with
|
||||
# --policy lmetric (the proxy's argparse has no "capped" choice).
|
||||
echo "[b3_sweep] building capped trace (max_turns=$MAX_TURNS_CAP) ..."
|
||||
CAPPED_TRACE="$OUTDIR/capped/trace.jsonl"
|
||||
mkdir -p "$OUTDIR/capped"
|
||||
@@ -169,7 +171,34 @@ mkdir -p "$OUTDIR/capped"
|
||||
--input "$TRACE" \
|
||||
--output "$CAPPED_TRACE" \
|
||||
--max-turns "$MAX_TURNS_CAP" | tee "$OUTDIR/capped/build.log"
|
||||
run_policy "capped" "$CAPPED_TRACE"
|
||||
|
||||
# Inline equivalent of run_policy "capped" but using --policy lmetric.
|
||||
echo "[b3_sweep] === policy=capped (picker=lmetric) trace=$(basename "$CAPPED_TRACE") ==="
|
||||
pkill -9 -f cache_aware_proxy 2>/dev/null || true
|
||||
sleep 2
|
||||
launch_proxy lmetric "$OUTDIR/capped/proxy.log"
|
||||
t_cap_start=$(date +%s.%N)
|
||||
PYTHONPATH="$ROOT" "$VENV/python" -m replayer \
|
||||
--trace "$CAPPED_TRACE" \
|
||||
--output "$OUTDIR/capped/metrics.jsonl" \
|
||||
--endpoint "http://127.0.0.1:$PROXY_PORT" \
|
||||
--model "$MODEL" \
|
||||
2>&1 | tee "$OUTDIR/capped/replayer.log" | tail -3
|
||||
t_cap_end=$(date +%s.%N)
|
||||
python3 - "$OUTDIR/capped" capped "$CAPPED_TRACE" "$t_cap_start" "$t_cap_end" <<'PY'
|
||||
import json, sys
|
||||
rundir, policy, trace, t_start, t_end = sys.argv[1:]
|
||||
with open(f"{rundir}/run_window.json", "w") as f:
|
||||
json.dump({
|
||||
"policy": policy, "trace": trace,
|
||||
"t_start_unix": float(t_start),
|
||||
"t_end_unix": float(t_end),
|
||||
}, f, indent=2)
|
||||
PY
|
||||
curl -s "http://127.0.0.1:$PROXY_PORT/breakdown" > "$OUTDIR/capped/breakdown.json"
|
||||
curl -s "http://127.0.0.1:$PROXY_PORT/worker_state" > "$OUTDIR/capped/worker_state.json"
|
||||
curl -s "http://127.0.0.1:$PROXY_PORT/stats" > "$OUTDIR/capped/stats.json"
|
||||
echo "[b3_sweep] capped done: $(wc -l < "$OUTDIR/capped/metrics.jsonl") metric rows"
|
||||
|
||||
# 4) Snapshot final engine state file sizes for the analyzer
|
||||
ls -l "$OUTDIR/engine_state/" > "$OUTDIR/engine_state_files.txt"
|
||||
|
||||
@@ -131,6 +131,26 @@ def test_hotspot_index_max_over_median_p90():
|
||||
assert out["hotspot_index_ttft_p90"] > 5.0
|
||||
|
||||
|
||||
def test_hotspot_index_uses_true_median_for_even_n():
|
||||
"""8 workers, sorted TTFT p90 [1,2,3,4,5,6,7,80].
|
||||
True median = (4+5)/2 = 4.5; hotspot = 80/4.5 ≈ 17.78.
|
||||
Previous buggy implementation used sorted[4] = 5, giving 80/5 = 16.0.
|
||||
"""
|
||||
rows = []
|
||||
ttfts = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 80.0]
|
||||
for i, t in enumerate(ttfts):
|
||||
for _ in range(10):
|
||||
rows.append({
|
||||
"request_id": f"x{i}", "routed_to": f"http://h:800{i}",
|
||||
"endpoint_url": f"http://h:800{i}",
|
||||
"ttft_s": t, "latency_s": 1.0, "error": None,
|
||||
})
|
||||
out = hotspot_index(rows)
|
||||
assert out["status"] == "supported"
|
||||
idx = out["hotspot_index_ttft_p90"]
|
||||
assert abs(idx - 80.0 / 4.5) < 1e-6, f"expected ~17.78, got {idx}"
|
||||
|
||||
|
||||
def test_label_slow_requests_flags_overlap_and_hot_worker():
|
||||
metrics = [
|
||||
_mk_metric("slow_overlap", ttft_s=10.0,
|
||||
|
||||
Reference in New Issue
Block a user