Compare commits
6 Commits
08530b3915
...
4722883903
| Author | SHA1 | Date | |
|---|---|---|---|
| 4722883903 | |||
| 0c3220cbb8 | |||
| b7902061d1 | |||
| b9f324f2e6 | |||
| df3249925b | |||
| 1d87082ca1 |
@@ -1,10 +1,24 @@
|
||||
"""Aggregate B2 microbench cells into a single interference-index sweep summary.
|
||||
"""Aggregate B2 microbench cells: same- vs different-worker prefill overlap.
|
||||
|
||||
Per cell (variant × prefill_size):
|
||||
- read metrics.jsonl + run_window.json
|
||||
- slice the shared engine_*.jsonl by run window
|
||||
- run interference_index() against the slice
|
||||
- record (variant, prefill_size, n_overlap, n_clean, tpot_p90_*, idx)
|
||||
For each (variant × prefill_size) cell we have:
|
||||
- 240 short-prompt decode requests at qps=4
|
||||
- 4 large-prompt one-token "prefill injections"
|
||||
|
||||
The interesting question is *not* "does any other request's prefill overlap
|
||||
this decode" (the answer is always yes — every decode begins with its own
|
||||
short prefill, and at qps=4 they overlap each other constantly). The
|
||||
interesting question is "does an injected large prefill on the *same* worker
|
||||
materially slow this decode down?".
|
||||
|
||||
So we:
|
||||
1) extract each cell's injection windows = [(t_dispatch, t_finish)
|
||||
for r in metrics if r.workload=="prefill"];
|
||||
2) label each decode request as overlap iff its
|
||||
[t_first_token, t_finish] intersects at least one injection window;
|
||||
3) compute TPOT p50/p90/p99 for overlap vs clean;
|
||||
4) the per-cell interference index = TPOT_p90(overlap) /
|
||||
TPOT_p90(clean). For "different" variant this should hover near 1.0;
|
||||
for "same" it should rise with prefill_size.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -13,71 +27,77 @@ import argparse
|
||||
import json
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from analysis.characterization.joined_analysis import (
|
||||
_percentile,
|
||||
_vllm_rid_matches,
|
||||
interference_index,
|
||||
load_engine_state,
|
||||
load_jsonl,
|
||||
write_json,
|
||||
)
|
||||
|
||||
|
||||
def _slice_engine_state(
|
||||
engine_state_by_worker: dict[str, list[dict]],
|
||||
t_start: float,
|
||||
t_end: float,
|
||||
) -> dict[str, list[dict]]:
|
||||
sliced: dict[str, list[dict]] = {}
|
||||
for worker, steps in engine_state_by_worker.items():
|
||||
sliced[worker] = [s for s in steps
|
||||
if t_start <= (s.get("t_unix") or 0.0) <= t_end]
|
||||
return sliced
|
||||
def _overlaps(a_start: float, a_end: float, b_start: float, b_end: float) -> bool:
|
||||
return a_start <= b_end and b_start <= a_end
|
||||
|
||||
|
||||
def _to_joined_shape(metrics_rows: list[dict], variant: str) -> list[dict]:
|
||||
"""Adapt B2 metric rows to what interference_index expects."""
|
||||
joined: list[dict] = []
|
||||
for r in metrics_rows:
|
||||
if r.get("workload") != "decode":
|
||||
def _analyze_cell(metrics_rows: list[dict]) -> dict:
|
||||
prefills = [r for r in metrics_rows if r.get("workload") == "prefill"
|
||||
and r.get("error") is None]
|
||||
decodes = [r for r in metrics_rows if r.get("workload") == "decode"
|
||||
and r.get("error") is None]
|
||||
|
||||
inj_windows: list[tuple[float, float]] = []
|
||||
for p in prefills:
|
||||
ts = p.get("t_dispatch_unix")
|
||||
te = p.get("t_finish_unix")
|
||||
if ts is None or te is None:
|
||||
continue
|
||||
joined.append({
|
||||
"request_id": r["request_id"],
|
||||
"tpot_s": r.get("tpot_s"),
|
||||
"ttft_s": r.get("ttft_s"),
|
||||
"latency_s": r.get("latency_s"),
|
||||
"endpoint_url": r.get("endpoint"),
|
||||
"routed_to": r.get("endpoint"),
|
||||
"t_first_token_unix": (
|
||||
(r["t_dispatch_unix"] + r["ttft_s"])
|
||||
if r.get("ttft_s") is not None
|
||||
and r.get("t_dispatch_unix") is not None else None
|
||||
),
|
||||
"t_finish_unix": r.get("t_finish_unix"),
|
||||
"error": r.get("error"),
|
||||
})
|
||||
return joined
|
||||
inj_windows.append((float(ts), float(te)))
|
||||
|
||||
overlap_tpots: list[float] = []
|
||||
clean_tpots: list[float] = []
|
||||
overlap_ttfts: list[float] = []
|
||||
clean_ttfts: list[float] = []
|
||||
for d in decodes:
|
||||
ts = d.get("t_dispatch_unix")
|
||||
te = d.get("t_finish_unix")
|
||||
if ts is None or te is None:
|
||||
continue
|
||||
is_overlap = any(_overlaps(ts, te, ws, we) for ws, we in inj_windows)
|
||||
tpot = d.get("tpot_s")
|
||||
ttft = d.get("ttft_s")
|
||||
if tpot is not None:
|
||||
(overlap_tpots if is_overlap else clean_tpots).append(float(tpot))
|
||||
if ttft is not None:
|
||||
(overlap_ttfts if is_overlap else clean_ttfts).append(float(ttft))
|
||||
|
||||
p90_overlap = _percentile(overlap_tpots, 0.90) if overlap_tpots else None
|
||||
p90_clean = _percentile(clean_tpots, 0.90) if clean_tpots else None
|
||||
idx = (p90_overlap / p90_clean) if (p90_overlap and p90_clean) else None
|
||||
return {
|
||||
"n_prefill_injections": len(prefills),
|
||||
"n_decode_total": len(decodes),
|
||||
"n_decode_overlap": len(overlap_tpots),
|
||||
"n_decode_clean": len(clean_tpots),
|
||||
"tpot_p50_overlap_s": _percentile(overlap_tpots, 0.50),
|
||||
"tpot_p90_overlap_s": p90_overlap,
|
||||
"tpot_p99_overlap_s": _percentile(overlap_tpots, 0.99),
|
||||
"tpot_p50_clean_s": _percentile(clean_tpots, 0.50),
|
||||
"tpot_p90_clean_s": p90_clean,
|
||||
"tpot_p99_clean_s": _percentile(clean_tpots, 0.99),
|
||||
"ttft_p90_overlap_s": _percentile(overlap_ttfts, 0.90)
|
||||
if overlap_ttfts else None,
|
||||
"ttft_p90_clean_s": _percentile(clean_ttfts, 0.90)
|
||||
if clean_ttfts else None,
|
||||
"interference_index": idx,
|
||||
}
|
||||
|
||||
|
||||
def main() -> None:
|
||||
p = argparse.ArgumentParser(description="B2 sweep aggregation")
|
||||
p.add_argument("--sweep-dir", type=Path, required=True,
|
||||
help="Top-level dir produced by scripts/b2_interference.py")
|
||||
p.add_argument("--engine-state-dir", type=Path, required=True)
|
||||
p.add_argument("--worker-map", type=str, required=True,
|
||||
help="URL=worker_id pairs, comma-separated")
|
||||
p = argparse.ArgumentParser(description="B2 sweep aggregation (window-overlap)")
|
||||
p.add_argument("--sweep-dir", type=Path, required=True)
|
||||
p.add_argument("--out", type=Path, default=None)
|
||||
args = p.parse_args()
|
||||
|
||||
worker_map = {}
|
||||
for entry in args.worker_map.split(","):
|
||||
url, _, wid = entry.strip().partition("=")
|
||||
if url and wid:
|
||||
worker_map[url] = wid
|
||||
|
||||
engine_state = load_engine_state(args.engine_state_dir)
|
||||
rows: list[dict] = []
|
||||
for variant_dir in sorted(args.sweep_dir.glob("*/")):
|
||||
if variant_dir.name in ("logs",):
|
||||
@@ -89,27 +109,16 @@ def main() -> None:
|
||||
continue
|
||||
window = json.loads(window_path.read_text())
|
||||
metrics_rows = load_jsonl(metrics_path)
|
||||
joined = _to_joined_shape(metrics_rows, variant_dir.name)
|
||||
sliced = _slice_engine_state(
|
||||
engine_state, window["t_start_unix"], window["t_end_unix"],
|
||||
)
|
||||
idx = interference_index(joined, sliced, worker_map)
|
||||
cell = _analyze_cell(metrics_rows)
|
||||
rows.append({
|
||||
"variant": variant_dir.name,
|
||||
"prefill_size": int(window["prefill_size"]),
|
||||
"decode_endpoint": window["decode_endpoint"],
|
||||
"prefill_endpoint": window["prefill_endpoint"],
|
||||
"n_decode_requests": sum(1 for r in metrics_rows
|
||||
if r.get("workload") == "decode"
|
||||
and r.get("error") is None),
|
||||
"n_prefill_injections": sum(1 for r in metrics_rows
|
||||
if r.get("workload") == "prefill"
|
||||
and r.get("error") is None),
|
||||
**idx,
|
||||
**cell,
|
||||
})
|
||||
summary = {"rows": rows}
|
||||
out_path = args.out or args.sweep_dir / "b2_sweep_summary.json"
|
||||
write_json(out_path, summary)
|
||||
write_json(out_path, {"rows": rows})
|
||||
print(json.dumps(rows, indent=2))
|
||||
|
||||
|
||||
|
||||
@@ -1,54 +1,29 @@
|
||||
# Figures Index
|
||||
|
||||
Generated by:
|
||||
|
||||
```bash
|
||||
.venv/bin/python analysis/characterization/plot_current_results.py
|
||||
```
|
||||
## Window 0 (pre-Window-1 audit, legacy runs)
|
||||
|
||||
| Figure | Intended Claim |
|
||||
|---|---|
|
||||
| [fig_full_trace_workload.png](figures/fig_full_trace_workload.png) | Full GLM-5.1 trace is long-input, short-output, and high input/output ratio. |
|
||||
| [fig_session_skew.png](figures/fig_session_skew.png) | Session input-token mass is highly skewed; top sessions dominate work. |
|
||||
| [fig_pdsep_vs_combined.png](figures/fig_pdsep_vs_combined.png) | Existing static PD-sep A/B regresses TTFT/E2E vs combined. |
|
||||
| [fig_pdsep_vs_combined.png](figures/fig_pdsep_vs_combined.png) | Static PD-sep regresses TTFT/E2E vs combined (legacy 200-req A/B). |
|
||||
| [fig_elastic_vs_baseline.png](figures/fig_elastic_vs_baseline.png) | Existing elastic transfer-based run does not improve TTFT/TPOT over high-contention baseline. |
|
||||
| [fig_gpu_balance.png](figures/fig_gpu_balance.png) | Existing runs show GPU-util imbalance, but more data is needed for hot-spot causality. |
|
||||
| [fig_claim_status.png](figures/fig_claim_status.png) | Current audit separates supported, partial, and unsupported claims. |
|
||||
| [fig_gpu_balance.png](figures/fig_gpu_balance.png) | Existing runs show GPU-util imbalance; not sufficient for hot-spot causal claim. |
|
||||
| [fig_claim_status.png](figures/fig_claim_status.png) | Audit separates supported / partial / unsupported claims. |
|
||||
|
||||
## Figure Previews
|
||||
## Window 1 (B1' + B3 + B2)
|
||||
|
||||
### Full Trace Workload
|
||||
Generated by `analysis/characterization/render_window1_figures.py`.
|
||||
Source data: `analysis/characterization/window_1_results/`.
|
||||
|
||||
Full GLM-5.1 trace is long-input, short-output, and high input/output ratio.
|
||||
|
||||

|
||||
|
||||
### Session Skew
|
||||
|
||||
Session input-token mass is highly skewed; top sessions dominate work.
|
||||
|
||||

|
||||
|
||||
### PD-Sep vs Combined
|
||||
|
||||
Existing static PD-sep A/B regresses TTFT/E2E vs combined.
|
||||
|
||||

|
||||
|
||||
### Elastic vs Baseline
|
||||
|
||||
Existing elastic transfer-based run does not improve TTFT/TPOT over high-contention baseline.
|
||||
|
||||

|
||||
|
||||
### GPU Balance
|
||||
|
||||
Existing runs show GPU-util imbalance, but more data is needed for hot-spot causality.
|
||||
|
||||

|
||||
|
||||
### Claim Status
|
||||
|
||||
Current audit separates supported, partial, and unsupported claims.
|
||||
|
||||

|
||||
| Figure | Intended Claim |
|
||||
|---|---|
|
||||
| [fig_kv_footprint_cdf.png](../window_1_results/figures/fig_kv_footprint_cdf.png) | KV per request for Qwen3-Coder-30B-A3B: p50/p90/p99 = 1.83/8.04/11.49 GiB; p99 takes 12% of H20 HBM. |
|
||||
| [fig_reuse_decomposition.png](../window_1_results/figures/fig_reuse_decomposition.png) | Cached_tokens decompose 93.2% intra / 5.7% cross / 1.1% shared on w600 lmetric run. |
|
||||
| [fig_b3_apc_vs_upper.png](../window_1_results/figures/fig_b3_apc_vs_upper.png) | Per-policy APC achieved vs theoretical intra-session ceiling (79.6%). |
|
||||
| [fig_b3_apc_vs_hotspot.png](../window_1_results/figures/fig_b3_apc_vs_hotspot.png) | Locality-vs-hotspot tradeoff across policies; unified dominates the frontier. |
|
||||
| [fig_b3_latency_bars.png](../window_1_results/figures/fig_b3_latency_bars.png) | TTFT / TPOT / E2E p90 bars per policy. |
|
||||
| [fig_b3_per_worker_ttft_p90.png](../window_1_results/figures/fig_b3_per_worker_ttft_p90.png) | Per-worker TTFT p90 distribution per policy; sticky's engine_3 and unified's engine_4 are the hot workers. |
|
||||
| [fig_b3_failure_breakdown.png](../window_1_results/figures/fig_b3_failure_breakdown.png) | Slow-request cause stacked bar per policy. |
|
||||
| [fig_b2_tpot_vs_prefill.png](../window_1_results/figures/fig_b2_tpot_vs_prefill.png) | TPOT during decode under same-worker prefill injection scales with prefill size; different-worker control flat. |
|
||||
| [fig_b2_ttft_vs_prefill.png](../window_1_results/figures/fig_b2_ttft_vs_prefill.png) | TTFT shows the same monotone same-worker scaling, peaking at 218× for 65k injection. |
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
# Characterization Claim Matrix
|
||||
|
||||
Updated 2026-05-25 after Window 1 (B1' KV-footprint + reuse, B3 5-policy
|
||||
sweep, B2 PD-colo interference microbench).
|
||||
|
||||
| Claim | Status | Supporting Data | Needed Next | Reviewer Risk |
|
||||
|---|---|---|---|---|
|
||||
| Batch 0 substrate audit is only partially complete for existing runs. | `partially_supported` | metrics.jsonl lacks actual dispatch/finish timestamps in current artifacts. | Add request dispatch and finish/error timestamps to future replayer/proxy metrics. | Cannot use these runs to prove online per-session sequentiality. |
|
||||
| Batch 1 workload shape can be characterized from formatted traces and metrics. | `supported_for_trace_shape` | Full compact trace CPU summary in `full_trace_summary.json`: input p50/p90/p99 = 20k/87.9k/125.5k, output p50/p90/p99 = 80/811/6.6k, top 1% sessions hold 46.5% of input-token mass. | Add cache-hit joined records for actual reuse decomposition. | Actual cache reuse decomposition needs cached_tokens joined with hash_ids. |
|
||||
| Static PD separation is worse than combined in existing 200-request GPU A/B. | `supported_by_existing_artifact` | outputs/gpu_ab_combined vs outputs/gpu_ab_pdsep metrics.summary.json. | Refresh with PD matrix, multiple seeds, cudagraph-enabled methodology. | Legacy run has no per-stage TTFT breakdown and no step-level KV occupancy. |
|
||||
| Elastic transfer-based migration does not improve high-contention 500-request run. | `supported_by_existing_artifact` | outputs/contention_16s_ts10 vs outputs/contention_16s_elastic metrics.summary.json and gpu_util.csv. | Attribute whether failure is trigger quality, transfer overhead, or wrong load regime. | Existing metrics lack actual sequentiality proof and per-request transfer waterfall. |
|
||||
| PD-colo prefill/decode interference is not yet directly proven by step-level data in this package. | `not_yet_supported` | No decode-step and prefill-overlap timestamp artifact found in summarized runs. | Run Batch 2 controlled same-worker/different-worker injection with step timestamps. | Cannot claim interference as causal without Batch 2. |
|
||||
| Session hot-spot residual imbalance is suggested but not fully attributed. | `partially_supported` | gpu_util.csv shows per-GPU mean-util imbalance in existing runs. | Collect per-worker queue delay, session-to-worker map, and per-session token mass per worker. | GPU util imbalance alone is not enough to prove session hot-spot. |
|
||||
| SRR is not measured by existing fixed-request runs. | `not_yet_supported` | No arrival-rate sweep artifacts found. | Implement Batch 4 Poisson session-arrival SRR sweep. | Latency-at-one-load cannot support sustainable throughput claim. |
|
||||
| Per-session sequentiality is enforced when replayer + proxy carry the new join fields. | `supported` | A1 unix timestamps (t_dispatch/t_first_token/t_finish_unix) and X-Request-Id passthrough; smoke validation 2026-05-25 confirmed 30/30 join coverage. | Use this stack for all Window 2 B4/B5 SRR runs. | Legacy outputs/ without these fields still cannot be re-classified as `online_realistic`. |
|
||||
| Agentic workload is long-input / short-output / heavy-tail session mass. | `supported` | Full trace CPU summary (full_trace_summary.json): input p50/p90/p99 = 20k/87.9k/125.5k; top 1% sessions hold 46.5% of input mass. Full trace 2.11M requests, 1.31M sessions. | — | Sample trace (w600) percentiles inherit from this full trace but should not be extrapolated. |
|
||||
| KV per request for Qwen3-Coder-30B-A3B is 98304 B/token; p50/p90/p99 footprint = 1.83/8.04/11.49 GiB. | `supported` | window_1_results/kv_footprint_summary.json; computed from model config and full trace input lengths. | — | Assumes bf16; would scale for fp8/int8 quant. |
|
||||
| Workload reuse is overwhelmingly intra-session. | `supported` | Real reuse decomposition from w600 lmetric run: intra 93.2%, cross 5.7%, shared 1.1% (window_1_results/lmetric_reuse.json). Theoretical any-vs-intra ceiling gap 0.7 pp. | — | Trace-specific; ChatGPT-style workloads with long system prompts would shift toward shared-prefix. |
|
||||
| Theoretical APC ceiling on w600 trace is 79.6% (intra) / 80.3% (any-session). | `supported` | window_1_results/apc_upper_w600.json from block-level trie walk on `hash_ids`. | — | Assumes infinite per-worker cache (no eviction); achieved values must be read as fraction of this ceiling. |
|
||||
| Cache-aware LMetric leaves a measurable locality gap (22.7 pp). | `supported` | lmetric achieved 56.9% vs intra-session ceiling 79.6%; B3 sweep window_1_results/b3_policy_comparison.json. | — | sticky data shows the gap can be recovered by harder affinity. |
|
||||
| Hybrid affinity (`unified`) breaks the locality-vs-latency tradeoff. | `supported` | unified APC 79.4% (97% of intra ceiling) AND TTFT p90 7.24 s (lmetric is 15.6 s). | — | unified concentrates a single very hot worker (engine_4 at 37.7 s p90); hotspot_index 3.35. |
|
||||
| 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. |
|
||||
| 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. |
|
||||
| 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. |
|
||||
|
||||
@@ -1,66 +1,76 @@
|
||||
# Main-Claim Allowed Runs
|
||||
|
||||
Status: current audit gate
|
||||
Status: post-Window-1 audit gate
|
||||
Date: 2026-05-25
|
||||
|
||||
## Allowed For Workload-Shape Claims
|
||||
|
||||
These artifacts can support trace/workload characterization claims:
|
||||
|
||||
- `dash0:/home/admin/cpfs/wjh/ali-trace/trace-glm5.1-formatted/051315-051317.jsonl`
|
||||
- Compact formatted full trace.
|
||||
- CPU summary recorded in `full_trace_summary.json`.
|
||||
- Supports long-input/short-output and session token-mass skew claims.
|
||||
- Does not prove runtime cache hits or online sequentiality.
|
||||
- Compact formatted full trace (2.11M requests / 1.31M sessions).
|
||||
- CPU summary in `current_results/full_trace_summary.json` and
|
||||
Window 1 KV footprint in `window_1_results/kv_footprint_summary.json`.
|
||||
- Supports: long-input / short-output / heavy-tail token mass /
|
||||
KV per request distribution.
|
||||
|
||||
- `traces/w600_r0.0015_st30.jsonl`
|
||||
- Local sampled trace.
|
||||
- Useful for local dry runs and figure generation.
|
||||
- Not the canonical full-trace source.
|
||||
- 1214 requests / 274 sessions / 53.3 M tokens.
|
||||
- APC theoretical bounds in `window_1_results/apc_upper_w600.json`.
|
||||
- Routing-policy comparison trace used by B3.
|
||||
|
||||
## Allowed For Routing-Policy Comparison Claims
|
||||
|
||||
These five runs share an identical trace, model, and 8-instance topology;
|
||||
they support all per-policy claims about APC, hotspot, interference,
|
||||
latency, failure breakdown.
|
||||
|
||||
- `outputs/b3_sweep_20260525_095043/lmetric/` — main baseline
|
||||
- `outputs/b3_sweep_20260525_095043/load_only/` — control: no cache / no affinity
|
||||
- `outputs/b3_sweep_20260525_095043/sticky/` — control: hard affinity
|
||||
- `outputs/b3_sweep_20260525_095043/unified/` — hybrid (interference index
|
||||
unavailable; see note in claim matrix)
|
||||
- `outputs/b3_sweep_20260525_095043/capped/` — lmetric on cap-8 trace
|
||||
|
||||
Aggregated comparison: `outputs/b3_sweep_20260525_095043/b3_policy_comparison.json`.
|
||||
Rendered figures: `analysis/characterization/window_1_results/figures/fig_b3_*.png`.
|
||||
|
||||
## Allowed For PD-colo Interference Causal Claims
|
||||
|
||||
- `outputs/b2_microbench/sweep/{same,different}/p{2048,8192,16384,32768,65536}/`
|
||||
- Decode-load + prefill-injection microbench.
|
||||
- `b2_sweep_summary.json` aggregates per-cell TPOT and TTFT
|
||||
(overlap vs clean), indexed by `prefill_size × variant`.
|
||||
- Different-worker control idx ≈ 1.0 across 32× variation;
|
||||
same-worker idx scales monotonically.
|
||||
|
||||
## Allowed For Legacy Baseline Sanity Claims
|
||||
|
||||
These existing runs can support sanity-level comparisons, but not final
|
||||
paper-grade SRR claims:
|
||||
These older runs predate Window 1 instrumentation. They can still support
|
||||
"static PD-sep was worse than combined on this fixed-request workload"
|
||||
type claims, but **not** the new SRR or per-policy comparisons.
|
||||
|
||||
- `outputs/gpu_ab_combined`
|
||||
- `outputs/gpu_ab_pdsep`
|
||||
- `outputs/contention_16s_ts10`
|
||||
- `outputs/contention_16s_elastic`
|
||||
- `outputs/combined_1000req`
|
||||
- `outputs/exp3_pd_sep_tp1_mooncake`
|
||||
- `outputs/gpu_ab_combined`, `outputs/gpu_ab_pdsep`
|
||||
- `outputs/contention_16s_ts10`, `outputs/contention_16s_elastic`
|
||||
- `outputs/combined_1000req`, `outputs/exp3_pd_sep_tp1_mooncake`
|
||||
|
||||
Allowed claims:
|
||||
## NOT Allowed For Main Claims
|
||||
|
||||
- Static PD-sep was worse than combined in these existing fixed-request runs.
|
||||
- Elastic transfer-based migration did not improve the summarized 500-request
|
||||
high-contention run.
|
||||
- GPU-util imbalance exists in these artifacts.
|
||||
The following need new runs:
|
||||
|
||||
Disallowed claims:
|
||||
- **B4 SRR sweep**: arrival-rate sweep with open-loop Poisson session
|
||||
arrivals and per-class SLO. No data yet.
|
||||
- **B5 failure attribution near SRR boundary**: depends on B4.
|
||||
- **Production interference under cache_aware proxy**: B2 used direct
|
||||
endpoints; the production routing might shift the same-worker
|
||||
collision profile.
|
||||
|
||||
- Online SRR.
|
||||
- Per-session sequentiality.
|
||||
- Causal attribution of prefill/decode interference.
|
||||
- Causal attribution of session hot spots from GPU utilization alone.
|
||||
## Required Upgrade Path
|
||||
|
||||
## Not Yet Allowed For Main Claims
|
||||
For Window 2 (B4 + B5), the existing stack already meets the needs:
|
||||
- A1 unix timestamps on every metric row ✓
|
||||
- A2 worker_state snapshots ✓
|
||||
- A3 step-level engine_state (works in isolated runs since `df32499`) ✓
|
||||
- A4 open-loop Poisson loadgen ✓
|
||||
- A5 joined_analysis + failure labels ✓
|
||||
|
||||
The following need fresh instrumentation or fresh runs:
|
||||
|
||||
- Batch 2 prefill/decode interference.
|
||||
- Batch 3 session hot-spot root cause.
|
||||
- Batch 4 sustainable request rate.
|
||||
- Batch 5 failure attribution near SRR boundary.
|
||||
|
||||
## Required Upgrade Before Paper-Grade Claims
|
||||
|
||||
Future main-claim runs must include:
|
||||
|
||||
- per-request actual dispatch timestamp;
|
||||
- per-request finish/error timestamp;
|
||||
- route decision and selected worker;
|
||||
- per-worker queue delay;
|
||||
- per-worker KV occupancy;
|
||||
- per-worker APC/cache-hit snapshot;
|
||||
- attempted/completed/error/goodput counters;
|
||||
- session-causal load generation.
|
||||
No new instrumentation required. The only software gap is `b3_analyze.sh`
|
||||
must use per-policy engine_state when present (fixed at commit `df32499`).
|
||||
|
||||
@@ -1,17 +1,62 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Rebuild this current-results audit package.
|
||||
python3 analysis/characterization/summarize_runs.py --output-dir analysis/characterization/current_results --runs outputs/gpu_ab_combined outputs/gpu_ab_pdsep outputs/contention_16s_ts10 outputs/contention_16s_elastic outputs/combined_1000req outputs/exp3_pd_sep_tp1_mooncake
|
||||
# Window 0 audit refresh (legacy run summaries).
|
||||
python3 analysis/characterization/summarize_runs.py \
|
||||
--output-dir analysis/characterization/current_results \
|
||||
--runs outputs/gpu_ab_combined outputs/gpu_ab_pdsep \
|
||||
outputs/contention_16s_ts10 outputs/contention_16s_elastic \
|
||||
outputs/combined_1000req outputs/exp3_pd_sep_tp1_mooncake
|
||||
|
||||
# Example Batch 0/1 local trace analysis.
|
||||
# B1' Per-request KV footprint on the full trace (runs on dash0 directly,
|
||||
# CPU-only; the formatted full trace is hundreds of GiB).
|
||||
python3 analysis/characterization/analyze.py \
|
||||
--trace traces/w600_r0.0015_st30.jsonl \
|
||||
--kv-bytes-per-token 98304 \
|
||||
--task-name w600_local_full_trace \
|
||||
--overwrite
|
||||
--trace ~/ali-trace/trace-glm5.1-formatted/051315-051317.jsonl \
|
||||
--kv-bytes-per-token 98304 \
|
||||
--task-name full_trace_with_kv \
|
||||
--output-root outputs/characterization \
|
||||
--overwrite
|
||||
|
||||
# CPU-only full compact trace summary was computed on dash0 from:
|
||||
# /home/admin/cpfs/wjh/ali-trace/trace-glm5.1-formatted/051315-051317.jsonl
|
||||
# Recompute either by running analyze.py on dash0, or by copying that compact
|
||||
# formatted JSONL locally. Do not use the 487G raw file directly.
|
||||
# w600 trace APC theoretical bound.
|
||||
python3 scripts/compute_apc_upper_bound.py \
|
||||
--trace traces/w600_r0.0015_st30.jsonl \
|
||||
--out outputs/apc_upper_w600.json
|
||||
|
||||
# B3 5-policy routing sweep on dash0 (8 × TP1 instances).
|
||||
# First three policies share one vLLM lifecycle (hot-cache, fast):
|
||||
bash scripts/b3_sweep.sh # writes outputs/b3_sweep_<TS>/
|
||||
|
||||
# Last two run isolated with cold vLLM:
|
||||
bash scripts/b3_isolated_policy.sh unified \
|
||||
traces/w600_r0.0015_st30.jsonl \
|
||||
outputs/b3_sweep_<TS>/unified
|
||||
|
||||
python3 scripts/build_capped_trace.py \
|
||||
--input traces/w600_r0.0015_st30.jsonl \
|
||||
--output outputs/b3_sweep_<TS>/capped/trace.jsonl \
|
||||
--max-turns 8
|
||||
|
||||
bash scripts/b3_isolated_policy.sh lmetric \
|
||||
outputs/b3_sweep_<TS>/capped/trace.jsonl \
|
||||
outputs/b3_sweep_<TS>/capped
|
||||
|
||||
# B3 analysis (joined records + indices) and report.
|
||||
bash scripts/b3_analyze.sh outputs/b3_sweep_<TS>
|
||||
python3 scripts/render_b3_report.py --sweep-dir outputs/b3_sweep_<TS>
|
||||
|
||||
# B2 PD-colo interference microbench. Launch 2 vLLM instances on
|
||||
# ports 8100 and 8101 with --enable-prompt-tokens-details first, then:
|
||||
python3 scripts/b2_interference.py \
|
||||
--decode-endpoint http://127.0.0.1:8100 \
|
||||
--prefill-endpoint http://127.0.0.1:8101 \
|
||||
--model <model-path> \
|
||||
--out-dir outputs/b2_microbench/sweep \
|
||||
--prefill-sizes 2048,8192,16384,32768,65536 \
|
||||
--variants different,same
|
||||
python3 analysis/characterization/b2_sweep_analysis.py \
|
||||
--sweep-dir outputs/b2_microbench/sweep
|
||||
|
||||
# Window 1 figure rendering (CPU only).
|
||||
python3 analysis/characterization/render_window1_figures.py \
|
||||
--results-dir analysis/characterization/window_1_results \
|
||||
--out-dir analysis/characterization/window_1_results/figures
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
# Reviewer Risk Register
|
||||
|
||||
Updated 2026-05-25 after Window 1.
|
||||
|
||||
| Risk | Severity | Evidence | Mitigation |
|
||||
|---|---|---|---|
|
||||
| Session sequentiality not proven | `high` | Current metrics include trace timestamp and latency but not actual dispatch/finish wall-clock timestamps. | Add dispatch/finish timestamps and run Batch 0 before SRR claims. |
|
||||
| Legacy PD-sep data may not match final methodology | `medium` | PD matrix scaffold exists separately; some old runs used earlier flags/methodology. | Use fresh PD matrix for paper-grade claims. |
|
||||
| GPU util is not a sufficient hot-spot proof | `medium` | Existing artifacts have gpu_util.csv but lack per-worker queue and session ownership. | Add route-decision and per-worker queue logs for Batch 3. |
|
||||
| Cache reuse decomposition is incomplete without joined hash/cache-hit data | `medium` | Trace has hash_ids; metrics have cached_tokens; request IDs may not join across all artifacts. | Emit hash_ids/session_id/cached_tokens in the same per-request record. |
|
||||
| ~~Session sequentiality not proven~~ | resolved | A1 instrumentation lands per-request t_dispatch/t_first_token/t_finish unix timestamps + proxy_request_id. Smoke validation 2026-05-25 confirms 30/30 join coverage. | All Window 1 runs already use this; Window 2 inherits. |
|
||||
| ~~Cache reuse decomposition incomplete~~ | resolved | Real reuse decomposition computed in `window_1_results/lmetric_reuse.json` from joined records carrying session_id + hash_ids + cached_tokens. | — |
|
||||
| APC across hot-sweep policies may be contaminated by prior policy runs | low | First-turn cached_tokens distribution shows < 1% empirical contamination; load_only and sticky vLLMs were not restarted between policies. `unified` and `capped` are isolated cold-start. | Window 2 will isolate each policy launch by default; document in paper that lmetric/load_only/sticky reflect "warm-cache" condition. |
|
||||
| Unified missing `interference_index` due to analyzer truncate-write bug | medium | The original `b3_analyze.sh` unconditionally `slice_engine_state.py`'d each policy and used `open("w")`, overwriting unified's correctly-written engine_state with the empty-window slice from the (hot-sweep) shared dir. | Fixed in commit `df32499`. B2 microbench provides the cleaner same-vs-different interference proof, so we do not need to rerun unified. |
|
||||
| GPU 0 ghost memory after vLLM crash | low | EngineCore subprocess name is `VLLM::EngineCor`; `pkill -f "vllm serve"` misses it. Killed manually on 2026-05-25; cleanup logic in `b3_sweep.sh` and `b3_isolated_policy.sh` now also targets `EngineCore`. | — |
|
||||
| w600 trace is a 1k-request sample, not the full GLM-5.1 trace | low | All B3 + B2 percentiles are on this sample. Full-trace KV-footprint and reuse claims use the 2.11M-request full trace. | Window 2 SRR sweep uses w600; full-trace SRR would need a larger sample and more GPU budget. |
|
||||
| Trace-timestamp dispatch with strict session sequentiality stretches replay wall time | medium | lmetric's 600s trace dispatched over 49 min; system over-saturates and the dispatch window expands. | Window 2 uses A4 open-loop Poisson loadgen with explicit arrival rate, decoupling load level from trace structure. |
|
||||
| Capped cap=8 may be too soft | low | Reviewer might prefer cap=2 or cap=4 to test "no multi-turn" extreme. Cap=8 was chosen to sit between turns/session p90 (1) and p99 (18). | Re-run with a stricter cap if reviewer pushes back; underlying capped script is parameterized. |
|
||||
| B2 microbench uses synthetic short-prompt decode load (256 tokens) | low | This bounds the realism of the "decode" workload. Production decode tokens come from prior turns of long context. | The signal magnitude is robust enough that prompt length shouldn't qualitatively change conclusions; B3 sticky's failure breakdown is the production-trace confirmation. |
|
||||
|
||||
303
analysis/characterization/render_window1_figures.py
Normal file
@@ -0,0 +1,303 @@
|
||||
"""Render PNG figures for Window 1 results (B1', B2, B3).
|
||||
|
||||
Inputs (all expected under <results-dir>):
|
||||
- b3_policy_comparison.json (per-policy table)
|
||||
- b2_sweep_summary.json (per-cell B2 sweep)
|
||||
- apc_upper_w600.json (theoretical bounds)
|
||||
- lmetric_reuse.json (intra/cross/shared decomp)
|
||||
- kv_footprint_summary.json (full trace KV stats)
|
||||
|
||||
Outputs (under <out-dir>):
|
||||
- fig_b3_apc_vs_hotspot.png
|
||||
- fig_b3_latency_bars.png
|
||||
- fig_b3_apc_vs_upper.png
|
||||
- fig_b3_failure_breakdown.png
|
||||
- fig_b3_per_worker_ttft_p90.png
|
||||
- fig_b2_tpot_vs_prefill.png
|
||||
- fig_b2_ttft_vs_prefill.png
|
||||
- fig_reuse_decomposition.png
|
||||
- fig_kv_footprint_cdf.png
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import matplotlib
|
||||
matplotlib.use("Agg")
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
POLICY_ORDER = ["lmetric", "load_only", "sticky", "unified", "capped"]
|
||||
POLICY_COLOR = {
|
||||
"lmetric": "#1f77b4",
|
||||
"load_only": "#ff7f0e",
|
||||
"sticky": "#d62728",
|
||||
"unified": "#2ca02c",
|
||||
"capped": "#9467bd",
|
||||
}
|
||||
|
||||
|
||||
def _load(results_dir: Path, name: str) -> dict:
|
||||
return json.loads((results_dir / name).read_text())
|
||||
|
||||
|
||||
def fig_b3_apc_vs_hotspot(comp: dict, upper: dict, out: Path) -> None:
|
||||
upper_intra = upper["apc_upper_intra_session"]
|
||||
fig, ax = plt.subplots(figsize=(6, 4.5))
|
||||
for r in comp["rows"]:
|
||||
pol = r["policy"]
|
||||
ax.scatter(r["apc_ratio"] * 100, r["hotspot_index_ttft_p90"],
|
||||
s=180, color=POLICY_COLOR.get(pol, "gray"), label=pol,
|
||||
edgecolors="black", linewidths=0.5)
|
||||
ax.annotate(pol, (r["apc_ratio"] * 100, r["hotspot_index_ttft_p90"]),
|
||||
xytext=(7, 7), textcoords="offset points",
|
||||
fontsize=9)
|
||||
ax.axvline(upper_intra * 100, linestyle="--", color="gray", alpha=0.6,
|
||||
label=f"intra-session APC upper {upper_intra * 100:.1f}%")
|
||||
ax.set_xlabel("APC achieved (%)")
|
||||
ax.set_ylabel("hotspot_index = max(worker TTFT p90) / median")
|
||||
ax.set_title("B3: APC vs hot-spot tradeoff across policies")
|
||||
ax.grid(alpha=0.3)
|
||||
fig.tight_layout()
|
||||
fig.savefig(out, dpi=120)
|
||||
plt.close(fig)
|
||||
|
||||
|
||||
def fig_b3_latency_bars(comp: dict, out: Path) -> None:
|
||||
by = {r["policy"]: r for r in comp["rows"]}
|
||||
pols = [p for p in POLICY_ORDER if p in by]
|
||||
metrics = [("TTFT p90 (s)", "ttft_p90_s"),
|
||||
("TPOT p90 (ms)", "tpot_p90_s"),
|
||||
("E2E p90 (s)", "e2e_p90_s")]
|
||||
fig, axes = plt.subplots(1, 3, figsize=(12, 4))
|
||||
for ax, (label, key) in zip(axes, metrics):
|
||||
vals = [by[p][key] * (1000 if "TPOT" in label else 1) for p in pols]
|
||||
ax.bar(pols, vals, color=[POLICY_COLOR.get(p, "gray") for p in pols],
|
||||
edgecolor="black", linewidth=0.5)
|
||||
ax.set_title(label)
|
||||
ax.tick_params(axis="x", rotation=20)
|
||||
for i, v in enumerate(vals):
|
||||
ax.text(i, v, f"{v:.1f}", ha="center", va="bottom", fontsize=9)
|
||||
ax.grid(alpha=0.3, axis="y")
|
||||
fig.suptitle("B3 headline latencies per policy")
|
||||
fig.tight_layout()
|
||||
fig.savefig(out, dpi=120)
|
||||
plt.close(fig)
|
||||
|
||||
|
||||
def fig_b3_apc_vs_upper(comp: dict, upper: dict, out: Path) -> None:
|
||||
by = {r["policy"]: r for r in comp["rows"]}
|
||||
pols = [p for p in POLICY_ORDER if p in by]
|
||||
achieved = [by[p]["apc_ratio"] * 100 for p in pols]
|
||||
fig, ax = plt.subplots(figsize=(6.5, 4))
|
||||
bars = ax.bar(pols, achieved,
|
||||
color=[POLICY_COLOR.get(p, "gray") for p in pols],
|
||||
edgecolor="black", linewidth=0.5)
|
||||
ax.axhline(upper["apc_upper_intra_session"] * 100, linestyle="--",
|
||||
color="black", alpha=0.7,
|
||||
label=f"intra-session ceiling {upper['apc_upper_intra_session'] * 100:.1f}%")
|
||||
ax.axhline(upper["apc_upper_any_session"] * 100, linestyle=":",
|
||||
color="darkgray", alpha=0.7,
|
||||
label=f"any-session ceiling {upper['apc_upper_any_session'] * 100:.1f}%")
|
||||
for b, v in zip(bars, achieved):
|
||||
ax.text(b.get_x() + b.get_width() / 2, v + 1, f"{v:.1f}%",
|
||||
ha="center", fontsize=9)
|
||||
ax.set_ylim(0, 100)
|
||||
ax.set_ylabel("APC ratio (%)")
|
||||
ax.set_title("B3: APC achieved vs theoretical ceiling")
|
||||
ax.legend(loc="upper right", fontsize=9)
|
||||
ax.grid(alpha=0.3, axis="y")
|
||||
fig.tight_layout()
|
||||
fig.savefig(out, dpi=120)
|
||||
plt.close(fig)
|
||||
|
||||
|
||||
def fig_b3_failure_breakdown(comp: dict, out: Path) -> None:
|
||||
by = {r["policy"]: r for r in comp["rows"]}
|
||||
pols = [p for p in POLICY_ORDER if p in by]
|
||||
causes = ["same_worker_prefill_overlap", "hot_worker_queue",
|
||||
"cache_miss_large_append", "high_kv_occupancy", "unknown"]
|
||||
cause_color = {
|
||||
"same_worker_prefill_overlap": "#d62728",
|
||||
"hot_worker_queue": "#ff7f0e",
|
||||
"cache_miss_large_append": "#1f77b4",
|
||||
"high_kv_occupancy": "#8c564b",
|
||||
"unknown": "#7f7f7f",
|
||||
}
|
||||
fig, ax = plt.subplots(figsize=(7, 4.5))
|
||||
bottom = [0.0] * len(pols)
|
||||
for c in causes:
|
||||
vals = [(by[p].get("failure_counts") or {}).get(c, 0) for p in pols]
|
||||
ax.bar(pols, vals, bottom=bottom, label=c.replace("_", " "),
|
||||
color=cause_color[c], edgecolor="black", linewidth=0.3)
|
||||
bottom = [a + b for a, b in zip(bottom, vals)]
|
||||
for i, total in enumerate(bottom):
|
||||
ax.text(i, total + 3, f"n={int(total)}", ha="center", fontsize=9)
|
||||
ax.set_ylabel("slow request count (TTFT > 2× p90 threshold)")
|
||||
ax.set_title("B3: slow-request cause breakdown per policy")
|
||||
ax.legend(fontsize=8, loc="upper right")
|
||||
ax.grid(alpha=0.3, axis="y")
|
||||
fig.tight_layout()
|
||||
fig.savefig(out, dpi=120)
|
||||
plt.close(fig)
|
||||
|
||||
|
||||
def fig_b3_per_worker_ttft(results_dir: Path, comp: dict, out: Path) -> None:
|
||||
"""Per-worker TTFT p90 grouped bars; reads each policy's hotspot_index.json."""
|
||||
by = {r["policy"]: r for r in comp["rows"]}
|
||||
pols = [p for p in POLICY_ORDER if p in by]
|
||||
fig, axes = plt.subplots(1, len(pols), figsize=(3 * len(pols), 4),
|
||||
sharey=True)
|
||||
if len(pols) == 1:
|
||||
axes = [axes]
|
||||
for ax, pol in zip(axes, pols):
|
||||
path = results_dir / f"per_worker_{pol}.json"
|
||||
if not path.exists():
|
||||
ax.text(0.5, 0.5, f"{pol}: no data", ha="center", va="center",
|
||||
transform=ax.transAxes)
|
||||
continue
|
||||
per = json.loads(path.read_text()).get("per_worker_ttft_p90_s") or {}
|
||||
items = sorted(per.items(), key=lambda kv: int(kv[0].rsplit(":", 1)[1]))
|
||||
labels = [f"e{int(k.rsplit(':', 1)[1]) - 8000}" for k, _ in items]
|
||||
vals = [v for _, v in items]
|
||||
ax.bar(labels, vals, color=POLICY_COLOR.get(pol, "gray"),
|
||||
edgecolor="black", linewidth=0.5)
|
||||
for i, v in enumerate(vals):
|
||||
ax.text(i, v, f"{v:.1f}", ha="center", va="bottom", fontsize=8)
|
||||
ax.set_title(f"{pol}\nhotspot={by[pol]['hotspot_index_ttft_p90']:.2f}",
|
||||
fontsize=10)
|
||||
ax.tick_params(axis="x", labelsize=8)
|
||||
ax.grid(alpha=0.3, axis="y")
|
||||
axes[0].set_ylabel("worker TTFT p90 (s)")
|
||||
fig.suptitle("B3 per-worker TTFT p90 distribution")
|
||||
fig.tight_layout()
|
||||
fig.savefig(out, dpi=120)
|
||||
plt.close(fig)
|
||||
|
||||
|
||||
def fig_b2_curves(b2: dict, out_tpot: Path, out_ttft: Path) -> None:
|
||||
sizes = sorted({r["prefill_size"] for r in b2["rows"]})
|
||||
by_var = {"same": {}, "different": {}}
|
||||
for r in b2["rows"]:
|
||||
by_var[r["variant"]][r["prefill_size"]] = r
|
||||
|
||||
for name, key, ylabel, ymax_log, out in [
|
||||
("TPOT", "tpot_p90", "TPOT p90 (ms)", True, out_tpot),
|
||||
("TTFT", "ttft_p90", "TTFT p90 (s)", True, out_ttft),
|
||||
]:
|
||||
fig, axes = plt.subplots(1, 2, figsize=(11, 4))
|
||||
ax_abs, ax_idx = axes
|
||||
for variant in ("different", "same"):
|
||||
xs, ys_o, ys_c, idxs = [], [], [], []
|
||||
for sz in sizes:
|
||||
r = by_var[variant].get(sz)
|
||||
if not r: continue
|
||||
ov = r.get(f"{key}_overlap_s")
|
||||
cl = r.get(f"{key}_clean_s")
|
||||
if ov is None or cl is None: continue
|
||||
xs.append(sz)
|
||||
scale = 1000 if name == "TPOT" else 1.0
|
||||
ys_o.append(ov * scale)
|
||||
ys_c.append(cl * scale)
|
||||
idxs.append(ov / cl)
|
||||
color = "#d62728" if variant == "same" else "#1f77b4"
|
||||
ax_abs.plot(xs, ys_o, "o-", color=color,
|
||||
label=f"{variant} (overlap)")
|
||||
ax_abs.plot(xs, ys_c, "s--", color=color, alpha=0.5,
|
||||
label=f"{variant} (clean)")
|
||||
ax_idx.plot(xs, idxs, "o-", color=color, label=variant,
|
||||
linewidth=2)
|
||||
ax_abs.set_xscale("log", base=2)
|
||||
ax_abs.set_yscale("log")
|
||||
ax_abs.set_xlabel("prefill injection size (tokens)")
|
||||
ax_abs.set_ylabel(ylabel + " (log)")
|
||||
ax_abs.set_title(f"B2 {name} absolute (overlap vs clean)")
|
||||
ax_abs.legend(fontsize=8)
|
||||
ax_abs.grid(alpha=0.3, which="both")
|
||||
|
||||
ax_idx.set_xscale("log", base=2)
|
||||
if ymax_log:
|
||||
ax_idx.set_yscale("log")
|
||||
ax_idx.axhline(1.0, color="black", linestyle=":", alpha=0.5)
|
||||
ax_idx.set_xlabel("prefill injection size (tokens)")
|
||||
ax_idx.set_ylabel(f"{name} idx = overlap / clean")
|
||||
ax_idx.set_title(f"B2 {name} interference index (same vs different worker)")
|
||||
ax_idx.legend()
|
||||
ax_idx.grid(alpha=0.3, which="both")
|
||||
fig.tight_layout()
|
||||
fig.savefig(out, dpi=120)
|
||||
plt.close(fig)
|
||||
|
||||
|
||||
def fig_reuse_decomposition(reuse: dict, out: Path) -> None:
|
||||
fr = reuse.get("fractions") or {}
|
||||
labels = ["intra-session", "cross-session", "shared-prefix", "unclassified"]
|
||||
vals = [fr.get("intra", 0), fr.get("cross", 0),
|
||||
fr.get("shared", 0), fr.get("unclassified", 0)]
|
||||
colors = ["#2ca02c", "#ff7f0e", "#9467bd", "#7f7f7f"]
|
||||
fig, ax = plt.subplots(figsize=(6, 3))
|
||||
bottom = 0.0
|
||||
for label, v, c in zip(labels, vals, colors):
|
||||
ax.barh(["lmetric run"], [v], left=[bottom], color=c, edgecolor="black",
|
||||
linewidth=0.5, label=f"{label} ({v * 100:.1f}%)")
|
||||
bottom += v
|
||||
ax.set_xlabel("fraction of cached_tokens")
|
||||
ax.set_xlim(0, 1)
|
||||
ax.set_title("Real reuse decomposition (w600 lmetric run)")
|
||||
ax.legend(fontsize=9, loc="lower right")
|
||||
ax.grid(alpha=0.3, axis="x")
|
||||
fig.tight_layout()
|
||||
fig.savefig(out, dpi=120)
|
||||
plt.close(fig)
|
||||
|
||||
|
||||
def fig_kv_footprint_cdf(kv: dict, out: Path) -> None:
|
||||
s = kv.get("kv_mib_per_request") or {}
|
||||
vals = [s.get(k) for k in ("p50", "p90", "p95", "p99")]
|
||||
labels = ["p50", "p90", "p95", "p99"]
|
||||
fig, ax = plt.subplots(figsize=(6, 3.5))
|
||||
ax.bar(labels, vals, color="#1f77b4", edgecolor="black", linewidth=0.5)
|
||||
for i, v in enumerate(vals):
|
||||
ax.text(i, v, f"{v:.0f} MiB", ha="center", va="bottom", fontsize=9)
|
||||
ax.axhline(95 * 1024, color="red", linestyle="--", alpha=0.5,
|
||||
label="H20 ~95 GiB usable")
|
||||
ax.set_ylabel("KV bytes per request (MiB)")
|
||||
ax.set_title("B1' Per-request KV footprint (Qwen3-Coder-30B-A3B, 98304 B/token)")
|
||||
ax.legend()
|
||||
ax.grid(alpha=0.3, axis="y")
|
||||
fig.tight_layout()
|
||||
fig.savefig(out, dpi=120)
|
||||
plt.close(fig)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
p = argparse.ArgumentParser()
|
||||
p.add_argument("--results-dir", type=Path, required=True)
|
||||
p.add_argument("--out-dir", type=Path, required=True)
|
||||
args = p.parse_args()
|
||||
args.out_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
comp = _load(args.results_dir, "b3_policy_comparison.json")
|
||||
upper = _load(args.results_dir, "apc_upper_w600.json")
|
||||
b2 = _load(args.results_dir, "b2_sweep_summary.json")
|
||||
reuse = _load(args.results_dir, "lmetric_reuse.json")
|
||||
kv = _load(args.results_dir, "kv_footprint_summary.json")
|
||||
|
||||
fig_b3_apc_vs_hotspot(comp, upper, args.out_dir / "fig_b3_apc_vs_hotspot.png")
|
||||
fig_b3_latency_bars(comp, args.out_dir / "fig_b3_latency_bars.png")
|
||||
fig_b3_apc_vs_upper(comp, upper, args.out_dir / "fig_b3_apc_vs_upper.png")
|
||||
fig_b3_failure_breakdown(comp, args.out_dir / "fig_b3_failure_breakdown.png")
|
||||
fig_b3_per_worker_ttft(args.results_dir, comp,
|
||||
args.out_dir / "fig_b3_per_worker_ttft_p90.png")
|
||||
fig_b2_curves(b2,
|
||||
args.out_dir / "fig_b2_tpot_vs_prefill.png",
|
||||
args.out_dir / "fig_b2_ttft_vs_prefill.png")
|
||||
fig_reuse_decomposition(reuse, args.out_dir / "fig_reuse_decomposition.png")
|
||||
fig_kv_footprint_cdf(kv, args.out_dir / "fig_kv_footprint_cdf.png")
|
||||
print(f"wrote 8 figures to {args.out_dir}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
171
analysis/characterization/window_1_results.md
Normal file
@@ -0,0 +1,171 @@
|
||||
# Window 1 Results: B1' + B2 + B3
|
||||
|
||||
Status: Window 1 complete (CPU + 2 dash0 GPU windows on 2026-05-25)
|
||||
Sweep: `outputs/b3_sweep_20260525_095043` (B3) + `outputs/b2_microbench/` (B2)
|
||||
Trace: `traces/w600_r0.0015_st30.jsonl` (1214 requests / 274 sessions / 53.3 M input tokens)
|
||||
Model: Qwen3-Coder-30B-A3B-Instruct (TP1 × 8 instances on H20)
|
||||
|
||||
Per-policy artifacts under `window_1_results/`. Figures under `window_1_results/figures/`.
|
||||
|
||||
## Headline
|
||||
|
||||
| Claim | Status | Evidence |
|
||||
|---|---|---|
|
||||
| Agentic workload reuse is overwhelmingly intra-session | **supported** | 93.2% of cached_tokens are intra-session (real); theoretical any-session APC ceiling 80.3% vs intra-session ceiling 79.6% → < 1pp gap |
|
||||
| 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 |
|
||||
| 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) |
|
||||
|
||||
## B1' Workload characterization
|
||||
|
||||
### Per-request KV footprint (Qwen3-Coder-30B-A3B)
|
||||
|
||||
`kv_bytes_per_token = 2 × num_layers × num_kv_heads × head_dim × dtype_bytes = 2 × 48 × 4 × 128 × 2 = 98304 B`
|
||||
|
||||
Full GLM-5.1 trace (2.11 M requests, 1.31 M sessions):
|
||||
|
||||
| | p50 | p90 | p95 | p99 | max |
|
||||
|---|---:|---:|---:|---:|---:|
|
||||
| KV per request | 1.83 GiB | 8.04 GiB | 9.59 GiB | **11.49 GiB** | 18.5 GiB |
|
||||
|
||||
H20 has ~95 GiB usable per GPU. **A single p99 request occupies 12% of a single H20's HBM** purely for KV. Multi-request batching is bounded by this.
|
||||
|
||||
Figure: `figures/fig_kv_footprint_cdf.png`.
|
||||
|
||||
### Real reuse decomposition (from lmetric run on w600 trace)
|
||||
|
||||
| class | tokens | fraction |
|
||||
|---|---:|---:|
|
||||
| intra-session | 28.3 M | **93.2%** |
|
||||
| cross-session | 1.72 M | 5.7% |
|
||||
| shared / system-prefix | 0.34 M | 1.1% |
|
||||
| unclassified | 0 | 0.0% |
|
||||
|
||||
→ session-affinity routing covers >99% of the reuse signal. There is no meaningful "system prompt" in this trace.
|
||||
|
||||
Figure: `figures/fig_reuse_decomposition.png`.
|
||||
|
||||
### Theoretical APC ceilings on w600
|
||||
|
||||
Computed by building a block-level trie of `hash_ids` per session (intra-session) or globally (any-session), then walking each request's `hash_ids` to count its longest prefix-match against previously-seen prefixes.
|
||||
|
||||
| variant | upper bound | hit requests |
|
||||
|---|---:|---:|
|
||||
| any-session (perfect global cache) | **80.3%** | 961 / 1214 |
|
||||
| intra-session only | **79.6%** | 914 / 1214 |
|
||||
| shared-prefix only (pos 0, ≥8 sessions) | 0.10% | 107 / 1214 |
|
||||
|
||||
Gap "any − intra" is 0.7 pp → no meaningful cross-session sharing in this trace.
|
||||
|
||||
## B3 5-policy routing sweep
|
||||
|
||||
8 vLLM instances on TP1, w600 trace, `--enable-prompt-tokens-details` so `cached_tokens` is reported per request.
|
||||
|
||||
| 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 |
|
||||
|
||||
\*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.
|
||||
|
||||
**Mechanism indices**
|
||||
- `interference_index` = TPOT_p90(decode overlapping same-worker prefill) / TPOT_p90(clean)
|
||||
- `hotspot_index` = max(worker TTFT p90) / median(worker TTFT p90)
|
||||
|
||||
Figures: `fig_b3_latency_bars.png`, `fig_b3_apc_vs_upper.png`,
|
||||
`fig_b3_apc_vs_hotspot.png`, `fig_b3_per_worker_ttft_p90.png`,
|
||||
`fig_b3_failure_breakdown.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.
|
||||
- **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.
|
||||
|
||||
### Slow-request cause breakdown (from `joined_analysis.label_slow_requests`)
|
||||
|
||||
| policy | n_slow | same-worker overlap | hot worker queue | cache miss large append | unknown |
|
||||
|---|---:|---:|---:|---:|---:|
|
||||
| lmetric | 295 | 69 (23%) | 68 (23%) | 94 (32%) | 64 (22%) |
|
||||
| load_only | 379 | 108 (29%) | 33 (9%) | 151 (40%) | 87 (23%) |
|
||||
| sticky | 234 | **134 (57%)** | 51 (22%) | **20 (9%)** | 29 (12%) |
|
||||
| unified | 189 | 0 (no engine_state) | 116 (61%) | 18 (10%) | 55 (29%) |
|
||||
| capped | 185 | 45 (24%) | 66 (36%) | 60 (32%) | 14 (8%) |
|
||||
|
||||
PD-colo failures are mixed-mechanism: lmetric has no single dominant cause.
|
||||
Sticky concentrates failures into same-worker overlap (locality is on, cache misses are gone, but interference takes over).
|
||||
|
||||
## B2 PD-colo interference microbench
|
||||
|
||||
Setup: 2 vLLM instances on GPU 0 (decode endpoint) and GPU 1 (prefill endpoint). A continuous 4 req/s short-prompt decode load runs against GPU 0 for 60 s per cell. 4 large-prompt one-token "prefill injections" fire every 12 s, targeted at either the same instance (`same`) or the paired one (`different`). Decode requests are labeled overlap iff their `[t_first_token, t_finish]` intersects any injection window. We compare TPOT p90 (overlap vs clean) per cell.
|
||||
|
||||
| variant | prefill | n_overlap | n_clean | **TPOT idx** | **TTFT idx** |
|
||||
|---|---:|---:|---:|---:|---:|
|
||||
| different | 2k–65k | 12–126 | 114–228 | **0.92–1.02** | **0.96–1.00** |
|
||||
| same | 2k | 12 | 228 | 1.16 | 2.15 |
|
||||
| same | 8k | 19 | 221 | 1.90 | **12.1×** |
|
||||
| same | 16k | 37 | 203 | 3.37 | **30.8×** |
|
||||
| same | 32k | 67 | 173 | **7.89** | **94.6×** |
|
||||
| same | 65k | 130 | 110 | 2.26* | **218×** |
|
||||
|
||||
\*65k TPOT idx is suppressed because n_overlap > n_clean — by the time the 65k prefill is finishing, the 4-second gap to the next injection has already started decoding overlap. The "clean" decodes left are the ones that randomly hit the brief gaps between injections.
|
||||
|
||||
Figures: `fig_b2_tpot_vs_prefill.png`, `fig_b2_ttft_vs_prefill.png`.
|
||||
|
||||
**Why this matters**
|
||||
- The `different-worker` control sits at idx ≈ 1.0 across 32× variation in prefill size. This is the cleanest possible disproof of "any prefill anywhere hurts decode": prefill on a *different* worker is invisible to the decode worker.
|
||||
- The `same-worker` curve is monotone in prefill size for TTFT (218× at 65k) and monotone-up-to-32k for TPOT (7.89×). The two ablations together establish causation: prefill-decode interference is a same-worker phenomenon and scales sharply with prefill mass.
|
||||
- This is the mechanism behind the B3 sticky interference jump (13.65) and unified's single hot worker (engine_4 at 37.7 s TTFT p90).
|
||||
|
||||
## What Window 1 does *not* answer
|
||||
|
||||
These need Window 2 (B4 SRR sweep + B5 failure attribution near SRR boundary):
|
||||
|
||||
1. **Sustainable arrival rate (SRR) per policy under SLO**. B3 was driven by trace timestamps with strict session sequentiality; when 8 instances cannot keep up, requests pile up and the *effective* dispatch window stretches (lmetric: trace claims 600 s, actual replay 49 min). We measured *saturated* behavior but not the saturation point. B4 needs the A4 open-loop Poisson loadgen with per-class SLO thresholds.
|
||||
2. **Failure breakdown at the SRR boundary**. B5 will rerun each policy at 0.9× / 1.0× / 1.1× of its SRR_max and label each SLO-violating request — gives the paper its causal failure-attribution table.
|
||||
|
||||
Optional / paper-polish runs (not blocking the story):
|
||||
|
||||
3. unified isolated rerun to capture `interference_index` (B2 already provides cleaner causal proof; skip unless reviewer asks).
|
||||
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
|
||||
|
||||
- **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.
|
||||
|
||||
## Reproduction commands
|
||||
|
||||
```bash
|
||||
# B3 5-policy sweep
|
||||
bash scripts/b3_sweep.sh # lmetric, load_only, sticky (hot-cache)
|
||||
bash scripts/b3_isolated_policy.sh unified <trace> <dir> # isolated cold-start
|
||||
bash scripts/b3_isolated_policy.sh lmetric <capped> <dir> # capped variant
|
||||
|
||||
bash scripts/b3_analyze.sh outputs/b3_sweep_<TS>
|
||||
python3 scripts/render_b3_report.py --sweep-dir outputs/b3_sweep_<TS>
|
||||
|
||||
# B2 interference microbench
|
||||
# (launch 2 vLLM on ports 8100/8101 with --enable-prompt-tokens-details first)
|
||||
python3 scripts/b2_interference.py \
|
||||
--decode-endpoint http://127.0.0.1:8100 \
|
||||
--prefill-endpoint http://127.0.0.1:8101 \
|
||||
--model <model> \
|
||||
--out-dir outputs/b2_microbench/sweep
|
||||
python3 analysis/characterization/b2_sweep_analysis.py --sweep-dir outputs/b2_microbench/sweep
|
||||
|
||||
# Figures
|
||||
python3 analysis/characterization/render_window1_figures.py \
|
||||
--results-dir analysis/characterization/window_1_results \
|
||||
--out-dir analysis/characterization/window_1_results/figures
|
||||
```
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"trace": "/home/admin/cpfs/wjh/agentic-kv/traces/w600_r0.0015_st30.jsonl",
|
||||
"n_requests": 1214,
|
||||
"n_sessions": 274,
|
||||
"block_size": 512,
|
||||
"shared_prefix_min_sessions": 8,
|
||||
"total_input_tokens": 53335690,
|
||||
"apc_upper_any_session": 0.8030439654947747,
|
||||
"apc_upper_intra_session": 0.7956783534627564,
|
||||
"apc_upper_shared_prefix_only": 0.0010271546126055554,
|
||||
"cached_tokens_any_session": 42830904,
|
||||
"cached_tokens_intra_session": 42438054,
|
||||
"cached_tokens_shared_prefix_only": 54784,
|
||||
"n_requests_any_hit": 961,
|
||||
"n_requests_intra_hit": 914,
|
||||
"n_requests_shared_hit": 107,
|
||||
"n_shared_pos0_blocks": 1
|
||||
}
|
||||
194
analysis/characterization/window_1_results/b2_sweep_summary.json
Normal file
@@ -0,0 +1,194 @@
|
||||
{
|
||||
"rows": [
|
||||
{
|
||||
"decode_endpoint": "http://127.0.0.1:8100",
|
||||
"interference_index": 0.9868436853823819,
|
||||
"n_decode_clean": 207,
|
||||
"n_decode_overlap": 33,
|
||||
"n_decode_total": 240,
|
||||
"n_prefill_injections": 4,
|
||||
"prefill_endpoint": "http://127.0.0.1:8101",
|
||||
"prefill_size": 16384,
|
||||
"tpot_p50_clean_s": 0.0061757058808297825,
|
||||
"tpot_p50_overlap_s": 0.006127697048765241,
|
||||
"tpot_p90_clean_s": 0.006862485770023231,
|
||||
"tpot_p90_overlap_s": 0.006772200748173878,
|
||||
"tpot_p99_clean_s": 0.007128368820806946,
|
||||
"tpot_p99_overlap_s": 0.0070623818792478,
|
||||
"ttft_p90_clean_s": 0.043039703369140626,
|
||||
"ttft_p90_overlap_s": 0.04307723045349121,
|
||||
"variant": "different"
|
||||
},
|
||||
{
|
||||
"decode_endpoint": "http://127.0.0.1:8100",
|
||||
"interference_index": 1.0176125863449343,
|
||||
"n_decode_clean": 228,
|
||||
"n_decode_overlap": 12,
|
||||
"n_decode_total": 240,
|
||||
"n_prefill_injections": 4,
|
||||
"prefill_endpoint": "http://127.0.0.1:8101",
|
||||
"prefill_size": 2048,
|
||||
"tpot_p50_clean_s": 0.0062349300191860005,
|
||||
"tpot_p50_overlap_s": 0.006218204594621754,
|
||||
"tpot_p90_clean_s": 0.006892242576136734,
|
||||
"tpot_p90_overlap_s": 0.007013632793619174,
|
||||
"tpot_p99_clean_s": 0.007111345902837888,
|
||||
"tpot_p99_overlap_s": 0.007131954732567373,
|
||||
"ttft_p90_clean_s": 0.04290406703948975,
|
||||
"ttft_p90_overlap_s": 0.040976309776306154,
|
||||
"variant": "different"
|
||||
},
|
||||
{
|
||||
"decode_endpoint": "http://127.0.0.1:8100",
|
||||
"interference_index": 0.9221676118155049,
|
||||
"n_decode_clean": 176,
|
||||
"n_decode_overlap": 64,
|
||||
"n_decode_total": 240,
|
||||
"n_prefill_injections": 4,
|
||||
"prefill_endpoint": "http://127.0.0.1:8101",
|
||||
"prefill_size": 32768,
|
||||
"tpot_p50_clean_s": 0.00620933012528853,
|
||||
"tpot_p50_overlap_s": 0.005991364970351711,
|
||||
"tpot_p90_clean_s": 0.0069098352181791054,
|
||||
"tpot_p90_overlap_s": 0.006372026241186894,
|
||||
"tpot_p99_clean_s": 0.007242970394365715,
|
||||
"tpot_p99_overlap_s": 0.006935877366499467,
|
||||
"ttft_p90_clean_s": 0.04308474063873291,
|
||||
"ttft_p90_overlap_s": 0.04266033172607422,
|
||||
"variant": "different"
|
||||
},
|
||||
{
|
||||
"decode_endpoint": "http://127.0.0.1:8100",
|
||||
"interference_index": 1.0162810692345416,
|
||||
"n_decode_clean": 114,
|
||||
"n_decode_overlap": 126,
|
||||
"n_decode_total": 240,
|
||||
"n_prefill_injections": 4,
|
||||
"prefill_endpoint": "http://127.0.0.1:8101",
|
||||
"prefill_size": 65536,
|
||||
"tpot_p50_clean_s": 0.006080349286397299,
|
||||
"tpot_p50_overlap_s": 0.006312949488861392,
|
||||
"tpot_p90_clean_s": 0.0068880830148253785,
|
||||
"tpot_p90_overlap_s": 0.007000228371283021,
|
||||
"tpot_p99_clean_s": 0.007222196574162956,
|
||||
"tpot_p99_overlap_s": 0.00723441562267265,
|
||||
"ttft_p90_clean_s": 0.04367616176605225,
|
||||
"ttft_p90_overlap_s": 0.04332089424133301,
|
||||
"variant": "different"
|
||||
},
|
||||
{
|
||||
"decode_endpoint": "http://127.0.0.1:8100",
|
||||
"interference_index": 0.92169565663476,
|
||||
"n_decode_clean": 220,
|
||||
"n_decode_overlap": 20,
|
||||
"n_decode_total": 240,
|
||||
"n_prefill_injections": 4,
|
||||
"prefill_endpoint": "http://127.0.0.1:8101",
|
||||
"prefill_size": 8192,
|
||||
"tpot_p50_clean_s": 0.006260122915711066,
|
||||
"tpot_p50_overlap_s": 0.006120474651606396,
|
||||
"tpot_p90_clean_s": 0.006968991684191154,
|
||||
"tpot_p90_overlap_s": 0.006423289366442748,
|
||||
"tpot_p99_clean_s": 0.007601349209294174,
|
||||
"tpot_p99_overlap_s": 0.006715166592838788,
|
||||
"ttft_p90_clean_s": 0.04314079284667969,
|
||||
"ttft_p90_overlap_s": 0.042817187309265134,
|
||||
"variant": "different"
|
||||
},
|
||||
{
|
||||
"decode_endpoint": "http://127.0.0.1:8100",
|
||||
"interference_index": 3.3716068170318985,
|
||||
"n_decode_clean": 203,
|
||||
"n_decode_overlap": 37,
|
||||
"n_decode_total": 240,
|
||||
"n_prefill_injections": 4,
|
||||
"prefill_endpoint": "http://127.0.0.1:8100",
|
||||
"prefill_size": 16384,
|
||||
"tpot_p50_clean_s": 0.006435276281954062,
|
||||
"tpot_p50_overlap_s": 0.009116151116111061,
|
||||
"tpot_p90_clean_s": 0.0071605749804564195,
|
||||
"tpot_p90_overlap_s": 0.024142643417974917,
|
||||
"tpot_p99_clean_s": 0.008356584539317119,
|
||||
"tpot_p99_overlap_s": 0.024809808827409838,
|
||||
"ttft_p90_clean_s": 0.04402604103088379,
|
||||
"ttft_p90_overlap_s": 1.3574100017547606,
|
||||
"variant": "same"
|
||||
},
|
||||
{
|
||||
"decode_endpoint": "http://127.0.0.1:8100",
|
||||
"interference_index": 1.1589170446597312,
|
||||
"n_decode_clean": 228,
|
||||
"n_decode_overlap": 12,
|
||||
"n_decode_total": 240,
|
||||
"n_prefill_injections": 4,
|
||||
"prefill_endpoint": "http://127.0.0.1:8100",
|
||||
"prefill_size": 2048,
|
||||
"tpot_p50_clean_s": 0.006142637946388938,
|
||||
"tpot_p50_overlap_s": 0.007610858088791972,
|
||||
"tpot_p90_clean_s": 0.006933137142296993,
|
||||
"tpot_p90_overlap_s": 0.008034930807171445,
|
||||
"tpot_p99_clean_s": 0.007201877651792584,
|
||||
"tpot_p99_overlap_s": 0.0084272463153107,
|
||||
"ttft_p90_clean_s": 0.043091440200805665,
|
||||
"ttft_p90_overlap_s": 0.09247522354125978,
|
||||
"variant": "same"
|
||||
},
|
||||
{
|
||||
"decode_endpoint": "http://127.0.0.1:8100",
|
||||
"interference_index": 7.891276559921504,
|
||||
"n_decode_clean": 173,
|
||||
"n_decode_overlap": 67,
|
||||
"n_decode_total": 240,
|
||||
"n_prefill_injections": 4,
|
||||
"prefill_endpoint": "http://127.0.0.1:8100",
|
||||
"prefill_size": 32768,
|
||||
"tpot_p50_clean_s": 0.006226602226796776,
|
||||
"tpot_p50_overlap_s": 0.012180752224392362,
|
||||
"tpot_p90_clean_s": 0.00694006813897027,
|
||||
"tpot_p90_overlap_s": 0.054765997029314145,
|
||||
"tpot_p99_clean_s": 0.010443444107518053,
|
||||
"tpot_p99_overlap_s": 0.058983875428787386,
|
||||
"ttft_p90_clean_s": 0.04411859512329101,
|
||||
"ttft_p90_overlap_s": 4.174754428863525,
|
||||
"variant": "same"
|
||||
},
|
||||
{
|
||||
"decode_endpoint": "http://127.0.0.1:8100",
|
||||
"interference_index": 2.259323176730457,
|
||||
"n_decode_clean": 110,
|
||||
"n_decode_overlap": 130,
|
||||
"n_decode_total": 240,
|
||||
"n_prefill_injections": 4,
|
||||
"prefill_endpoint": "http://127.0.0.1:8100",
|
||||
"prefill_size": 65536,
|
||||
"tpot_p50_clean_s": 0.0064652375500611585,
|
||||
"tpot_p50_overlap_s": 0.020095128001588764,
|
||||
"tpot_p90_clean_s": 0.009607415488272014,
|
||||
"tpot_p90_overlap_s": 0.021706256481132124,
|
||||
"tpot_p99_clean_s": 0.016912007837584522,
|
||||
"tpot_p99_overlap_s": 0.16948255478733715,
|
||||
"ttft_p90_clean_s": 0.06447408199310305,
|
||||
"ttft_p90_overlap_s": 14.060086917877197,
|
||||
"variant": "same"
|
||||
},
|
||||
{
|
||||
"decode_endpoint": "http://127.0.0.1:8100",
|
||||
"interference_index": 1.8961314610807898,
|
||||
"n_decode_clean": 221,
|
||||
"n_decode_overlap": 19,
|
||||
"n_decode_total": 240,
|
||||
"n_prefill_injections": 4,
|
||||
"prefill_endpoint": "http://127.0.0.1:8100",
|
||||
"prefill_size": 8192,
|
||||
"tpot_p50_clean_s": 0.00617263052198622,
|
||||
"tpot_p50_overlap_s": 0.008303543533941712,
|
||||
"tpot_p90_clean_s": 0.007060385713673601,
|
||||
"tpot_p90_overlap_s": 0.013387419479061859,
|
||||
"tpot_p99_clean_s": 0.0076809098022152696,
|
||||
"tpot_p99_overlap_s": 0.013849472662415166,
|
||||
"ttft_p90_clean_s": 0.04307150840759277,
|
||||
"ttft_p90_overlap_s": 0.52073073387146,
|
||||
"variant": "same"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
{
|
||||
"rows": [
|
||||
{
|
||||
"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,
|
||||
"apc_ratio": 0.3158312503528108,
|
||||
"interference_index": 6.331064378362814,
|
||||
"hotspot_index_ttft_p90": 1.9366915542605314,
|
||||
"reuse_intra_frac": 0.9192657105586233,
|
||||
"reuse_cross_frac": 0.0602232594931501,
|
||||
"n_slow": 185,
|
||||
"failure_counts": {
|
||||
"cache_miss_large_append": 60,
|
||||
"hot_worker_queue": 66,
|
||||
"same_worker_prefill_overlap": 45,
|
||||
"unknown": 14
|
||||
}
|
||||
},
|
||||
{
|
||||
"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,
|
||||
"apc_ratio": 0.5694312382571595,
|
||||
"interference_index": 6.530231061794441,
|
||||
"hotspot_index_ttft_p90": 2.237981740718548,
|
||||
"reuse_intra_frac": 0.9321238805590836,
|
||||
"reuse_cross_frac": 0.05679481258506571,
|
||||
"n_slow": 295,
|
||||
"failure_counts": {
|
||||
"cache_miss_large_append": 94,
|
||||
"hot_worker_queue": 68,
|
||||
"same_worker_prefill_overlap": 69,
|
||||
"unknown": 64
|
||||
}
|
||||
},
|
||||
{
|
||||
"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,
|
||||
"apc_ratio": 0.5412093853102866,
|
||||
"interference_index": 9.16424627504275,
|
||||
"hotspot_index_ttft_p90": 1.1400531308102801,
|
||||
"reuse_intra_frac": 0.9353191550754928,
|
||||
"reuse_cross_frac": 0.053372184678592026,
|
||||
"n_slow": 379,
|
||||
"failure_counts": {
|
||||
"cache_miss_large_append": 151,
|
||||
"hot_worker_queue": 33,
|
||||
"same_worker_prefill_overlap": 108,
|
||||
"unknown": 87
|
||||
}
|
||||
},
|
||||
{
|
||||
"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,
|
||||
"apc_ratio": 0.7720092868396378,
|
||||
"interference_index": 13.651718321568111,
|
||||
"hotspot_index_ttft_p90": 2.3493858974059214,
|
||||
"reuse_intra_frac": 0.9327723488279339,
|
||||
"reuse_cross_frac": 0.05495149683864246,
|
||||
"n_slow": 234,
|
||||
"failure_counts": {
|
||||
"cache_miss_large_append": 20,
|
||||
"hot_worker_queue": 51,
|
||||
"same_worker_prefill_overlap": 134,
|
||||
"unknown": 29
|
||||
}
|
||||
},
|
||||
{
|
||||
"policy": "unified",
|
||||
"n_ok": 1213,
|
||||
"n_total": 1214,
|
||||
"ttft_p50_s": 0.4997710260213353,
|
||||
"ttft_p90_s": 7.239999514014926,
|
||||
"ttft_p99_s": 42.022206099005416,
|
||||
"tpot_p50_s": 0.008079791456705824,
|
||||
"tpot_p90_s": 0.017107906969874808,
|
||||
"tpot_p99_s": 0.11808861252148231,
|
||||
"e2e_p50_s": 1.7495028690318577,
|
||||
"e2e_p90_s": 17.893827292020433,
|
||||
"e2e_p99_s": 68.18008507299237,
|
||||
"apc_ratio": 0.794261466256467,
|
||||
"interference_index": null,
|
||||
"hotspot_index_ttft_p90": 3.3497107140827365,
|
||||
"reuse_intra_frac": 0.9311187350942534,
|
||||
"reuse_cross_frac": 0.056702150437367635,
|
||||
"n_slow": 189,
|
||||
"failure_counts": {
|
||||
"cache_miss_large_append": 18,
|
||||
"hot_worker_queue": 116,
|
||||
"unknown": 55
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
114
analysis/characterization/window_1_results/b3_report.md
Normal file
@@ -0,0 +1,114 @@
|
||||
# B3 Routing Sweep Report
|
||||
|
||||
Sweep dir: `b3_sweep_20260525_095043`
|
||||
Trace: w600_r0.0015_st30.jsonl (~1.2k reqs, 8 × TP1)
|
||||
Policies present: lmetric, load_only, sticky, unified, capped
|
||||
Policies pending: —
|
||||
|
||||
## Headline latencies + APC
|
||||
|
||||
| policy | ok/total | TTFT p50/p90/p99 (s) | TPOT p50/p90/p99 (ms) | E2E p50/p90/p99 (s) | APC |
|
||||
|---|---:|---|---|---|---:|
|
||||
| **lmetric** | 1214/1214 | 0.94/15.59/52.95 | 8.9/21.2/175.9 | 2.75/24.75/79.62 | 56.9% |
|
||||
| **load_only** | 1214/1214 | 1.25/20.15/52.65 | 9.2/26.7/320.7 | 3.58/33.43/93.92 | 54.1% |
|
||||
| **sticky** | 1214/1214 | 0.54/18.02/71.37 | 8.9/36.1/345.2 | 2.08/34.61/133.58 | 77.2% |
|
||||
| **unified** | 1213/1214 | 0.50/7.24/42.02 | 8.1/17.1/118.1 | 1.75/17.89/68.18 | 79.4% |
|
||||
| **capped** | 770/770 | 1.20/12.76/46.05 | 7.2/16.0/101.5 | 2.59/21.24/73.39 | 31.6% |
|
||||
|
||||
## Mechanism indices
|
||||
|
||||
| policy | interference_index | hotspot_index (TTFT p90) | intra-session reuse | cross-session reuse | n_slow |
|
||||
|---|---:|---:|---:|---:|---:|
|
||||
| **lmetric** | 6.53 | 2.24 | 93.2% | 5.7% | 295 |
|
||||
| **load_only** | 9.16 | 1.14 | 93.5% | 5.3% | 379 |
|
||||
| **sticky** | 13.65 | 2.35 | 93.3% | 5.5% | 234 |
|
||||
| **unified** | — | 3.35 | 93.1% | 5.7% | 189 |
|
||||
| **capped** | 6.33 | 1.94 | 91.9% | 6.0% | 185 |
|
||||
|
||||
- **interference_index** = TPOT_p90(decode overlapping same-worker prefill) / TPOT_p90(clean)
|
||||
- **hotspot_index** = max(worker TTFT_p90) / median(worker TTFT_p90)
|
||||
|
||||
## Slow-request cause breakdown
|
||||
|
||||
| policy | n_slow | same-worker overlap | hot worker queue | cache miss large append | high KV | unknown |
|
||||
|---|---:|---:|---:|---:|---:|---:|
|
||||
| **lmetric** | 295 | 69 | 68 | 94 | 0 | 64 |
|
||||
| **load_only** | 379 | 108 | 33 | 151 | 0 | 87 |
|
||||
| **sticky** | 234 | 134 | 51 | 20 | 0 | 29 |
|
||||
| **unified** | 189 | 0 | 116 | 18 | 0 | 55 |
|
||||
| **capped** | 185 | 45 | 66 | 60 | 0 | 14 |
|
||||
|
||||
## Policy notes
|
||||
|
||||
- **lmetric** — cache-aware P_tokens × BS (main baseline)
|
||||
- **load_only** — control: min(num_requests), no cache, no affinity
|
||||
- **sticky** — control: hard session affinity (never break)
|
||||
- **unified** — hybrid affinity + LMetric fallback
|
||||
- **capped** — lmetric on per-session turn-capped trace
|
||||
|
||||
## Per-policy per-worker TTFT p90 (s)
|
||||
|
||||
### lmetric
|
||||
|
||||
| worker | TTFT p90 (s) |
|
||||
|---|---:|
|
||||
| http://127.0.0.1:8000 | 28.18 |
|
||||
| http://127.0.0.1:8001 | 13.15 |
|
||||
| http://127.0.0.1:8002 | 13.82 |
|
||||
| http://127.0.0.1:8003 | 14.00 |
|
||||
| http://127.0.0.1:8004 | 31.34 |
|
||||
| http://127.0.0.1:8005 | 7.87 |
|
||||
| http://127.0.0.1:8006 | 14.15 |
|
||||
| http://127.0.0.1:8007 | 11.78 |
|
||||
|
||||
### load_only
|
||||
|
||||
| worker | TTFT p90 (s) |
|
||||
|---|---:|
|
||||
| http://127.0.0.1:8000 | 22.06 |
|
||||
| http://127.0.0.1:8001 | 16.43 |
|
||||
| http://127.0.0.1:8002 | 16.81 |
|
||||
| http://127.0.0.1:8003 | 23.58 |
|
||||
| http://127.0.0.1:8004 | 25.14 |
|
||||
| http://127.0.0.1:8005 | 16.08 |
|
||||
| http://127.0.0.1:8006 | 23.96 |
|
||||
| http://127.0.0.1:8007 | 13.95 |
|
||||
|
||||
### sticky
|
||||
|
||||
| worker | TTFT p90 (s) |
|
||||
|---|---:|
|
||||
| http://127.0.0.1:8000 | 12.28 |
|
||||
| http://127.0.0.1:8001 | 23.57 |
|
||||
| http://127.0.0.1:8002 | 5.20 |
|
||||
| http://127.0.0.1:8003 | 55.38 |
|
||||
| http://127.0.0.1:8004 | 17.03 |
|
||||
| http://127.0.0.1:8005 | 25.49 |
|
||||
| http://127.0.0.1:8006 | 36.31 |
|
||||
| http://127.0.0.1:8007 | 2.50 |
|
||||
|
||||
### unified
|
||||
|
||||
| worker | TTFT p90 (s) |
|
||||
|---|---:|
|
||||
| http://127.0.0.1:8000 | 11.26 |
|
||||
| http://127.0.0.1:8001 | 3.61 |
|
||||
| http://127.0.0.1:8002 | 16.18 |
|
||||
| http://127.0.0.1:8003 | 9.31 |
|
||||
| http://127.0.0.1:8004 | 37.73 |
|
||||
| http://127.0.0.1:8005 | 18.33 |
|
||||
| http://127.0.0.1:8006 | 3.63 |
|
||||
| http://127.0.0.1:8007 | 7.77 |
|
||||
|
||||
### capped
|
||||
|
||||
| worker | TTFT p90 (s) |
|
||||
|---|---:|
|
||||
| http://127.0.0.1:8000 | 19.77 |
|
||||
| http://127.0.0.1:8001 | 15.79 |
|
||||
| http://127.0.0.1:8002 | 20.40 |
|
||||
| http://127.0.0.1:8003 | 10.54 |
|
||||
| http://127.0.0.1:8004 | 9.52 |
|
||||
| http://127.0.0.1:8005 | 9.46 |
|
||||
| http://127.0.0.1:8006 | 7.38 |
|
||||
| http://127.0.0.1:8007 | 9.66 |
|
||||
|
After Width: | Height: | Size: 84 KiB |
|
After Width: | Height: | Size: 79 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 49 KiB |
|
After Width: | Height: | Size: 58 KiB |
|
After Width: | Height: | Size: 51 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 31 KiB |
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"formula": "kv_bytes_per_request = input_tokens * kv_bytes_per_token",
|
||||
"kv_bytes_per_request": {
|
||||
"count": 2114220,
|
||||
"max": 19893878784.0,
|
||||
"mean": 3306689367.3278427,
|
||||
"min": 0.0,
|
||||
"p50": 1969029120.0,
|
||||
"p90": 8636507750.40001,
|
||||
"p95": 10296164352.0,
|
||||
"p99": 12339806208.0
|
||||
},
|
||||
"kv_bytes_per_token": 98304.0,
|
||||
"kv_mib_per_request": {
|
||||
"count": 2114220,
|
||||
"max": 18972.28125,
|
||||
"mean": 3153.5047219541957,
|
||||
"min": 0.0,
|
||||
"p50": 1877.8125,
|
||||
"p90": 8236.415625000009,
|
||||
"p95": 9819.1875,
|
||||
"p99": 11768.15625
|
||||
},
|
||||
"status": "available",
|
||||
"total_kv_gib": 6510940.188720703
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"hotspot_index_ttft_p90": 2.237981740718548,
|
||||
"per_worker_latency_p90_s": {
|
||||
"http://127.0.0.1:8000": 34.71445541951107,
|
||||
"http://127.0.0.1:8001": 21.922988962882666,
|
||||
"http://127.0.0.1:8002": 23.936190764518685,
|
||||
"http://127.0.0.1:8003": 26.22220957049285,
|
||||
"http://127.0.0.1:8004": 40.318757307820505,
|
||||
"http://127.0.0.1:8005": 12.26559703698149,
|
||||
"http://127.0.0.1:8006": 27.904838753980588,
|
||||
"http://127.0.0.1:8007": 18.430557113309625
|
||||
},
|
||||
"per_worker_ttft_p90_s": {
|
||||
"http://127.0.0.1:8000": 28.18261351052206,
|
||||
"http://127.0.0.1:8001": 13.147308969072796,
|
||||
"http://127.0.0.1:8002": 13.818959677941162,
|
||||
"http://127.0.0.1:8003": 14.003642184572524,
|
||||
"http://127.0.0.1:8004": 31.339895512629305,
|
||||
"http://127.0.0.1:8005": 7.870992770011071,
|
||||
"http://127.0.0.1:8006": 14.149156623415186,
|
||||
"http://127.0.0.1:8007": 11.777357225219024
|
||||
},
|
||||
"status": "supported"
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"cross_session_tokens": 1723017,
|
||||
"fractions": {
|
||||
"cross": 0.05679481258506571,
|
||||
"intra": 0.9321238805590836,
|
||||
"shared": 0.011081306855850749,
|
||||
"unclassified": 0.0
|
||||
},
|
||||
"intra_session_tokens": 28278380,
|
||||
"shared_prefix_min_sessions": 8,
|
||||
"shared_prefix_tokens": 336180,
|
||||
"status": "supported",
|
||||
"total_cached_tokens": 30371008,
|
||||
"unclassified_tokens": 0
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"hotspot_index_ttft_p90": 1.9366915542605314,
|
||||
"per_worker_latency_p90_s": {
|
||||
"http://127.0.0.1:8000": 23.81083881931848,
|
||||
"http://127.0.0.1:8001": 18.139674991380897,
|
||||
"http://127.0.0.1:8002": 29.116712999995805,
|
||||
"http://127.0.0.1:8003": 19.245074290811324,
|
||||
"http://127.0.0.1:8004": 17.230851700413044,
|
||||
"http://127.0.0.1:8005": 15.86663371440958,
|
||||
"http://127.0.0.1:8006": 16.707309890014592,
|
||||
"http://127.0.0.1:8007": 23.93718611740042
|
||||
},
|
||||
"per_worker_ttft_p90_s": {
|
||||
"http://127.0.0.1:8000": 19.772570010094213,
|
||||
"http://127.0.0.1:8001": 15.786850639013576,
|
||||
"http://127.0.0.1:8002": 20.403525242628533,
|
||||
"http://127.0.0.1:8003": 10.535247699997853,
|
||||
"http://127.0.0.1:8004": 9.52290979558602,
|
||||
"http://127.0.0.1:8005": 9.455131393985376,
|
||||
"http://127.0.0.1:8006": 7.379608143202497,
|
||||
"http://127.0.0.1:8007": 9.661995008389932
|
||||
},
|
||||
"status": "supported"
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"hotspot_index_ttft_p90": 2.237981740718548,
|
||||
"per_worker_latency_p90_s": {
|
||||
"http://127.0.0.1:8000": 34.71445541951107,
|
||||
"http://127.0.0.1:8001": 21.922988962882666,
|
||||
"http://127.0.0.1:8002": 23.936190764518685,
|
||||
"http://127.0.0.1:8003": 26.22220957049285,
|
||||
"http://127.0.0.1:8004": 40.318757307820505,
|
||||
"http://127.0.0.1:8005": 12.26559703698149,
|
||||
"http://127.0.0.1:8006": 27.904838753980588,
|
||||
"http://127.0.0.1:8007": 18.430557113309625
|
||||
},
|
||||
"per_worker_ttft_p90_s": {
|
||||
"http://127.0.0.1:8000": 28.18261351052206,
|
||||
"http://127.0.0.1:8001": 13.147308969072796,
|
||||
"http://127.0.0.1:8002": 13.818959677941162,
|
||||
"http://127.0.0.1:8003": 14.003642184572524,
|
||||
"http://127.0.0.1:8004": 31.339895512629305,
|
||||
"http://127.0.0.1:8005": 7.870992770011071,
|
||||
"http://127.0.0.1:8006": 14.149156623415186,
|
||||
"http://127.0.0.1:8007": 11.777357225219024
|
||||
},
|
||||
"status": "supported"
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"hotspot_index_ttft_p90": 1.1400531308102801,
|
||||
"per_worker_latency_p90_s": {
|
||||
"http://127.0.0.1:8000": 33.51168999259829,
|
||||
"http://127.0.0.1:8001": 29.20308109278556,
|
||||
"http://127.0.0.1:8002": 27.126518827211115,
|
||||
"http://127.0.0.1:8003": 38.597240307606995,
|
||||
"http://127.0.0.1:8004": 36.607777832809376,
|
||||
"http://127.0.0.1:8005": 28.097025175404276,
|
||||
"http://127.0.0.1:8006": 49.29610514297965,
|
||||
"http://127.0.0.1:8007": 20.958507975534303
|
||||
},
|
||||
"per_worker_ttft_p90_s": {
|
||||
"http://127.0.0.1:8000": 22.055091864388675,
|
||||
"http://127.0.0.1:8001": 16.425856862741057,
|
||||
"http://127.0.0.1:8002": 16.806352904380766,
|
||||
"http://127.0.0.1:8003": 23.581166115606912,
|
||||
"http://127.0.0.1:8004": 25.14397653030465,
|
||||
"http://127.0.0.1:8005": 16.080231266201018,
|
||||
"http://127.0.0.1:8006": 23.960470345703648,
|
||||
"http://127.0.0.1:8007": 13.95184187250561
|
||||
},
|
||||
"status": "supported"
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"hotspot_index_ttft_p90": 2.3493858974059214,
|
||||
"per_worker_latency_p90_s": {
|
||||
"http://127.0.0.1:8000": 30.185792533413043,
|
||||
"http://127.0.0.1:8001": 47.49661003401852,
|
||||
"http://127.0.0.1:8002": 22.069474861002554,
|
||||
"http://127.0.0.1:8003": 83.73774532350944,
|
||||
"http://127.0.0.1:8004": 22.03310715127737,
|
||||
"http://127.0.0.1:8005": 33.024566102202556,
|
||||
"http://127.0.0.1:8006": 61.65600914339302,
|
||||
"http://127.0.0.1:8007": 6.077459598158019
|
||||
},
|
||||
"per_worker_ttft_p90_s": {
|
||||
"http://127.0.0.1:8000": 12.284569517592924,
|
||||
"http://127.0.0.1:8001": 23.570226482005094,
|
||||
"http://127.0.0.1:8002": 5.202772857400123,
|
||||
"http://127.0.0.1:8003": 55.37555769548635,
|
||||
"http://127.0.0.1:8004": 17.031311958114394,
|
||||
"http://127.0.0.1:8005": 25.48531596700202,
|
||||
"http://127.0.0.1:8006": 36.31029207323453,
|
||||
"http://127.0.0.1:8007": 2.4984901855932535
|
||||
},
|
||||
"status": "supported"
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"hotspot_index_ttft_p90": 3.3497107140827365,
|
||||
"per_worker_latency_p90_s": {
|
||||
"http://127.0.0.1:8000": 41.42001512600109,
|
||||
"http://127.0.0.1:8001": 12.4878579101933,
|
||||
"http://127.0.0.1:8002": 22.462878945574648,
|
||||
"http://127.0.0.1:8003": 15.501050900109117,
|
||||
"http://127.0.0.1:8004": 39.956250199786155,
|
||||
"http://127.0.0.1:8005": 36.69850301651168,
|
||||
"http://127.0.0.1:8006": 10.116177947795954,
|
||||
"http://127.0.0.1:8007": 20.35038618039107
|
||||
},
|
||||
"per_worker_ttft_p90_s": {
|
||||
"http://127.0.0.1:8000": 11.264844838529825,
|
||||
"http://127.0.0.1:8001": 3.6063860427122614,
|
||||
"http://127.0.0.1:8002": 16.175747957825664,
|
||||
"http://127.0.0.1:8003": 9.314684258581842,
|
||||
"http://127.0.0.1:8004": 37.73397144810297,
|
||||
"http://127.0.0.1:8005": 18.328030522551852,
|
||||
"http://127.0.0.1:8006": 3.6328767628350773,
|
||||
"http://127.0.0.1:8007": 7.772977900883419
|
||||
},
|
||||
"status": "supported"
|
||||
}
|
||||
136
analysis/characterization/window_1_results/summary.json
Normal file
@@ -0,0 +1,136 @@
|
||||
{
|
||||
"analyzed_records": 2114220,
|
||||
"batch0": {
|
||||
"attempted_requests": 2114220,
|
||||
"completed_requests": null,
|
||||
"error_requests": null,
|
||||
"max_inflight_per_session": null,
|
||||
"session_concurrency_status": "unavailable",
|
||||
"session_sequential": null
|
||||
},
|
||||
"batch1": {
|
||||
"append_status": "unavailable",
|
||||
"input_stats": {
|
||||
"count": 2114220,
|
||||
"max": 202371.0,
|
||||
"mean": 33637.38370084476,
|
||||
"min": 0.0,
|
||||
"p50": 20030.0,
|
||||
"p90": 87855.1000000001,
|
||||
"p95": 104738.0,
|
||||
"p99": 125527.0
|
||||
},
|
||||
"kv_footprint_status": "available",
|
||||
"output_stats": {
|
||||
"count": 2114220,
|
||||
"max": 132665.0,
|
||||
"mean": 444.97059624826176,
|
||||
"min": 0.0,
|
||||
"p50": 80.0,
|
||||
"p90": 811.0,
|
||||
"p95": 2213.0,
|
||||
"p99": 6614.810000000056
|
||||
},
|
||||
"reuse_status": "unavailable"
|
||||
},
|
||||
"classification": {
|
||||
"label": "invalid_for_online_claim",
|
||||
"reason": "actual dispatch/finish timestamps are unavailable, so online sequentiality cannot be proven",
|
||||
"source": "auto",
|
||||
"stress_indicators": []
|
||||
},
|
||||
"manifest": {
|
||||
"canonical_trace_data_sources": {
|
||||
"dash0_formatted_trace_dir": "~/ali-trace/trace-glm5.1-formatted/",
|
||||
"dash0_raw_trace_dir": "~/ali-trace/trace-glm5.1/",
|
||||
"usage_note": "Full trace analysis can be run CPU-only on dash0, or the needed JSONL files can be copied/rsynced from dash0 to this machine before running this analyzer."
|
||||
},
|
||||
"end_time": "2026-05-25T09:03:36.499002+00:00",
|
||||
"figure_status": {
|
||||
"reason": "matplotlib unavailable: ModuleNotFoundError(\"No module named 'matplotlib'\")",
|
||||
"status": "skipped"
|
||||
},
|
||||
"git_commit": "",
|
||||
"gpu_count": 0,
|
||||
"gpu_type": "",
|
||||
"host": "ds-6348bee4-1-765874c9c4-7zrvf",
|
||||
"input_requirements": {
|
||||
"actual_sequentiality_proof": [
|
||||
"per-request dispatch timestamp",
|
||||
"per-request finish or error/timeout timestamp",
|
||||
"request_id join to trace/metrics when timing source is separate"
|
||||
],
|
||||
"metrics_jsonl": [
|
||||
"request_id",
|
||||
"session_id",
|
||||
"trace_timestamp_s",
|
||||
"input_length",
|
||||
"output_length",
|
||||
"latency_s",
|
||||
"ttft_s",
|
||||
"tpot_s",
|
||||
"error",
|
||||
"optional cached_tokens"
|
||||
],
|
||||
"reuse_decomposition": [
|
||||
"cached_tokens or cache_hit",
|
||||
"hash_ids",
|
||||
"session_id"
|
||||
],
|
||||
"trace_jsonl": [
|
||||
"chat_id",
|
||||
"parent_chat_id",
|
||||
"timestamp",
|
||||
"input_length",
|
||||
"output_length",
|
||||
"turn",
|
||||
"hash_ids",
|
||||
"optional session_id"
|
||||
]
|
||||
},
|
||||
"input_status": {
|
||||
"analyzed_records": 2114220,
|
||||
"breakdown_records": 0,
|
||||
"merge_warnings": [],
|
||||
"metrics_records": 0,
|
||||
"trace_records": 2114220,
|
||||
"trace_warnings": [],
|
||||
"unmatched_breakdown": 0,
|
||||
"unmatched_metrics": 0
|
||||
},
|
||||
"launch_command": "analysis/characterization/analyze.py --trace /home/admin/cpfs/wjh/ali-trace/trace-glm5.1-formatted/051315-051317.jsonl --kv-bytes-per-token 98304 --task-name full_trace_with_kv --output-root outputs/characterization --overwrite",
|
||||
"output_dir": "outputs/characterization/2026-05-25/full_trace_with_kv",
|
||||
"policy": "",
|
||||
"request_limit": null,
|
||||
"session_sampling_method": "",
|
||||
"session_sequential": null,
|
||||
"start_time": "2026-05-25T08:59:11.618919+00:00",
|
||||
"time_scale": null,
|
||||
"trace_file_info": {
|
||||
"exists": true,
|
||||
"mtime_s": 1778772033.2788928,
|
||||
"path": "/home/admin/cpfs/wjh/ali-trace/trace-glm5.1-formatted/051315-051317.jsonl",
|
||||
"sha256": "",
|
||||
"sha256_status": "skipped_use_--hash-inputs",
|
||||
"size_bytes": 1561266372
|
||||
},
|
||||
"trace_path": "/home/admin/cpfs/wjh/ali-trace/trace-glm5.1-formatted/051315-051317.jsonl",
|
||||
"trace_sha256": ""
|
||||
},
|
||||
"outputs": [
|
||||
"append_delta_stats.json",
|
||||
"invalid_runs.md",
|
||||
"kv_footprint_summary.json",
|
||||
"manifest.json",
|
||||
"raw/merged_requests.jsonl",
|
||||
"raw/unmatched_breakdown.jsonl",
|
||||
"raw/unmatched_metrics.jsonl",
|
||||
"reuse_decomposition.json",
|
||||
"session_arrival_stats.json",
|
||||
"session_concurrency.json",
|
||||
"session_skew.json",
|
||||
"trace_profile.json",
|
||||
"turn_interval_stats.json",
|
||||
"workload_summary.json"
|
||||
]
|
||||
}
|
||||
@@ -4,17 +4,17 @@ Status: execution checklist for interns
|
||||
Date: 2026-05-25
|
||||
Last progress audit: 2026-05-25
|
||||
|
||||
## Progress Snapshot (2026-05-25)
|
||||
## Progress Snapshot (2026-05-25, post-Window-1)
|
||||
|
||||
| Batch | State | Evidence |
|
||||
|---|---|---|
|
||||
| B0 Substrate audit | tool DONE, legacy runs partial | `analysis/characterization/analyze.py` implements per-session concurrency/arrival/inter-turn analyzer; legacy `metrics.jsonl` lacks dispatch/finish timestamps so actual sequentiality cannot be proven on old runs (correctly labeled in `current_results/`) |
|
||||
| B1 Workload characterization | trace-shape DONE, reuse pending | `current_results/full_trace_summary.json` covers 2.11M req / 1.31M sessions from `051315-051317.jsonl`; KV-footprint and reuse decomposition still require `--kv-bytes-per-token` rerun and cached_tokens+hash_ids joined records |
|
||||
| B2 PD interference | protocol DONE, run pending | `analysis/characterization/protocols.md` Batch 2 section ready; needs fresh GPU run with decode-step + prefill-chunk timestamps |
|
||||
| B3 Hot-spot imbalance | partial; needs new instrumentation | Legacy `gpu_util.csv` shows imbalance but lacks per-worker queue delay and session→worker map |
|
||||
| B4 SRR sweep | NOT DONE | No arrival-rate sweep artifacts; depends on session-causal open-loop loadgen |
|
||||
| B5 Failure attribution | NOT DONE | Depends on B2/B4 outputs |
|
||||
| B6 Audit package | scaffold DONE | `current_results/{characterization_claim_matrix.md, all_figures_index.md, reviewer_risk_register.md, main_claim_allowed_runs.md, reproduction_commands.sh}` + 6 figures committed |
|
||||
| B0 Substrate audit | **DONE for new runs**, legacy still partial | A1+A2 instrumentation lands per-request unix timestamps and X-Request-Id passthrough; B3 sweep 2026-05-25 achieves 100% join coverage on all 5 policy runs |
|
||||
| B1 Workload characterization | **DONE** | `window_1_results/kv_footprint_summary.json` (98304 B/token, p99 = 11.49 GiB); real reuse decomposition (`lmetric_reuse.json`: 93.2% intra-session, 5.7% cross, 1.1% shared); theoretical APC ceilings (`apc_upper_w600.json`: 79.6% intra / 80.3% any) |
|
||||
| B2 PD interference | **DONE** | `outputs/b2_microbench/sweep/` 5 × 2 cells. Different-worker control idx 0.92-1.02 across 32× prefill size variation; same-worker TTFT idx scales 2.15× → 218×. Causal proof complete. |
|
||||
| B3 5-policy routing sweep | **DONE** | `outputs/b3_sweep_20260525_095043/` lmetric/load_only/sticky (warm-cache) + unified/capped (isolated cold-start). Aggregated in `b3_policy_comparison.json`. Unified hits APC 79.4% (97% of ceiling) AND TTFT p90 7.24 s. |
|
||||
| B4 SRR sweep | NOT DONE | Window 2 task. A4 loadgen + per-class SLO + λ binary search per policy. |
|
||||
| B5 Failure attribution | NOT DONE | Window 2 task. Depends on B4 SRR boundaries. |
|
||||
| B6 Audit package | **DONE for Window 1** | `current_results/{characterization_claim_matrix.md, all_figures_index.md, reviewer_risk_register.md, main_claim_allowed_runs.md, reproduction_commands.sh}` refreshed; Window 1 results aggregated in `window_1_results.md` + 8 PNG figures |
|
||||
|
||||
Reusable assets already in repo:
|
||||
|
||||
|
||||
@@ -51,6 +51,7 @@ async def _send(
|
||||
"max_tokens": max_tokens,
|
||||
"min_tokens": max_tokens,
|
||||
"temperature": 0,
|
||||
"return_token_ids": True,
|
||||
"stream": True,
|
||||
"stream_options": {"include_usage": True},
|
||||
}
|
||||
@@ -82,10 +83,15 @@ async def _send(
|
||||
if choices:
|
||||
now = time.time()
|
||||
token_ids = choices[0].get("token_ids")
|
||||
delta = choices[0].get("text", "")
|
||||
if isinstance(token_ids, list) and token_ids:
|
||||
if ttft is None:
|
||||
ttft = now - t_dispatch
|
||||
token_times.extend([now] * len(token_ids))
|
||||
elif delta:
|
||||
if ttft is None:
|
||||
ttft = now - t_dispatch
|
||||
token_times.append(now)
|
||||
usage = chunk.get("usage")
|
||||
if usage:
|
||||
n_output = usage.get("completion_tokens", n_output)
|
||||
|
||||
@@ -15,21 +15,38 @@ SWEEP_DIR="${1:?usage: $0 <sweep_dir>}"
|
||||
|
||||
WORKER_MAP="http://127.0.0.1:8000=engine_0,http://127.0.0.1:8001=engine_1,http://127.0.0.1:8002=engine_2,http://127.0.0.1:8003=engine_3,http://127.0.0.1:8004=engine_4,http://127.0.0.1:8005=engine_5,http://127.0.0.1:8006=engine_6,http://127.0.0.1:8007=engine_7"
|
||||
|
||||
_has_engine_data() {
|
||||
# Return 0 (true) if $1/*.jsonl contains any non-empty file.
|
||||
local dir="$1"
|
||||
[ -d "$dir" ] || return 1
|
||||
local f
|
||||
for f in "$dir"/engine_*.jsonl; do
|
||||
if [ -s "$f" ]; then return 0; fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
for policy_dir in "$SWEEP_DIR"/*/; do
|
||||
policy=$(basename "$policy_dir")
|
||||
case "$policy" in
|
||||
engine_state|logs|capped) ;;
|
||||
engine_state|logs) continue ;;
|
||||
esac
|
||||
if [ ! -f "$policy_dir/run_window.json" ]; then
|
||||
continue
|
||||
fi
|
||||
echo "=== $policy ==="
|
||||
|
||||
PYTHONPATH="$ROOT" "$VENV/python" \
|
||||
"$ROOT/scripts/slice_engine_state.py" \
|
||||
--input-dir "$SWEEP_DIR/engine_state" \
|
||||
--output-dir "$policy_dir/engine_state" \
|
||||
--window "$policy_dir/run_window.json"
|
||||
# Isolated policies write engine_state into their own dir; hot-sweep
|
||||
# policies share the sweep-root engine_state and need slicing.
|
||||
if _has_engine_data "$policy_dir/engine_state"; then
|
||||
echo " using policy-local engine_state ($(du -sh "$policy_dir/engine_state" | cut -f1))"
|
||||
else
|
||||
PYTHONPATH="$ROOT" "$VENV/python" \
|
||||
"$ROOT/scripts/slice_engine_state.py" \
|
||||
--input-dir "$SWEEP_DIR/engine_state" \
|
||||
--output-dir "$policy_dir/engine_state" \
|
||||
--window "$policy_dir/run_window.json"
|
||||
fi
|
||||
|
||||
PYTHONPATH="$ROOT" "$VENV/python" \
|
||||
"$ROOT/analysis/characterization/joined_analysis.py" \
|
||||
@@ -41,24 +58,6 @@ for policy_dir in "$SWEEP_DIR"/*/; do
|
||||
--out-dir "$policy_dir/joined"
|
||||
done
|
||||
|
||||
# Also handle capped/ which is nested
|
||||
if [ -f "$SWEEP_DIR/capped/run_window.json" ]; then
|
||||
echo "=== capped ==="
|
||||
PYTHONPATH="$ROOT" "$VENV/python" \
|
||||
"$ROOT/scripts/slice_engine_state.py" \
|
||||
--input-dir "$SWEEP_DIR/engine_state" \
|
||||
--output-dir "$SWEEP_DIR/capped/engine_state" \
|
||||
--window "$SWEEP_DIR/capped/run_window.json"
|
||||
PYTHONPATH="$ROOT" "$VENV/python" \
|
||||
"$ROOT/analysis/characterization/joined_analysis.py" \
|
||||
--metrics "$SWEEP_DIR/capped/metrics.jsonl" \
|
||||
--breakdown "$SWEEP_DIR/capped/breakdown.json" \
|
||||
--worker-state "$SWEEP_DIR/capped/worker_state.json" \
|
||||
--engine-state-dir "$SWEEP_DIR/capped/engine_state" \
|
||||
--worker-map "$WORKER_MAP" \
|
||||
--out-dir "$SWEEP_DIR/capped/joined"
|
||||
fi
|
||||
|
||||
# Aggregate per-policy summary
|
||||
"$VENV/python" - <<PY
|
||||
import json, os, statistics
|
||||
|
||||
125
scripts/b3_isolated_policy.sh
Executable file
@@ -0,0 +1,125 @@
|
||||
#!/usr/bin/env bash
|
||||
# Run a single B3 policy with a cold-start vLLM (clean APC).
|
||||
#
|
||||
# Usage:
|
||||
# bash scripts/b3_isolated_policy.sh <policy> <trace> <rundir>
|
||||
#
|
||||
# Launches 8 fresh vLLM instances, captures their engine_state into
|
||||
# <rundir>/engine_state/, runs the policy through the proxy on
|
||||
# <trace>, then kills everything. Distinct from b3_sweep.sh which
|
||||
# shares one vLLM-set across all five policies (faster but warm-cache).
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="${ROOT:-/home/admin/cpfs/wjh/agentic-kv}"
|
||||
VENV="$ROOT/.venv/bin"
|
||||
MODEL="${MODEL:-/home/admin/cpfs/wjh/models/Qwen/Qwen3-Coder-30B-A3B-Instruct}"
|
||||
PROXY_PORT="${PROXY_PORT:-9300}"
|
||||
BASE_PORT="${BASE_PORT:-8000}"
|
||||
GPU_INDICES="${GPU_INDICES:-0 1 2 3 4 5 6 7}"
|
||||
EXTRA_VLLM_ARGS="${EXTRA_VLLM_ARGS:---enable-prompt-tokens-details}"
|
||||
N_INSTANCES=$(echo $GPU_INDICES | wc -w)
|
||||
|
||||
POLICY="${1:?usage: $0 <policy> <trace> <rundir>}"
|
||||
TRACE="${2:?usage: $0 <policy> <trace> <rundir>}"
|
||||
RUNDIR="${3:?usage: $0 <policy> <trace> <rundir>}"
|
||||
|
||||
mkdir -p "$RUNDIR/engine_state" "$RUNDIR/logs"
|
||||
echo "[isolated] policy=$POLICY trace=$(basename $TRACE) rundir=$RUNDIR"
|
||||
|
||||
cleanup() {
|
||||
pkill -9 -f cache_aware_proxy 2>/dev/null || true
|
||||
pkill -9 -f "vllm serve" 2>/dev/null || true
|
||||
pkill -9 -f "EngineCore" 2>/dev/null || true
|
||||
sleep 3
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
# Hard reset first
|
||||
cleanup
|
||||
|
||||
echo "[isolated] launching $N_INSTANCES vLLM on GPUs $GPU_INDICES ..."
|
||||
i=0
|
||||
for gpu in $GPU_INDICES; do
|
||||
port=$((BASE_PORT + i))
|
||||
master=$((29500 + i))
|
||||
AGENTIC_STEP_LOG_PATH="$RUNDIR/engine_state/engine_${i}.jsonl" \
|
||||
AGENTIC_WORKER_ID="engine_${i}" \
|
||||
CUDA_VISIBLE_DEVICES=$gpu \
|
||||
MASTER_PORT=$master \
|
||||
nohup "$VENV/vllm" serve "$MODEL" \
|
||||
--host 0.0.0.0 --port "$port" \
|
||||
--tensor-parallel-size 1 \
|
||||
--trust-remote-code --enable-prefix-caching \
|
||||
--dtype auto --gpu-memory-utilization 0.9 \
|
||||
--max-model-len 200000 \
|
||||
$EXTRA_VLLM_ARGS \
|
||||
> "$RUNDIR/logs/vllm_inst_${i}_gpu${gpu}.log" 2>&1 &
|
||||
disown
|
||||
sleep 2
|
||||
i=$((i + 1))
|
||||
done
|
||||
|
||||
echo "[isolated] waiting for vLLM health ..."
|
||||
for i in $(seq 0 $((N_INSTANCES - 1))); do
|
||||
port=$((BASE_PORT + i))
|
||||
tries=0
|
||||
while ! curl -sf "http://127.0.0.1:$port/health" >/dev/null 2>&1; do
|
||||
tries=$((tries + 1))
|
||||
if [ $tries -gt 90 ]; then
|
||||
echo "[isolated] FATAL: inst_$i not healthy after 180s"
|
||||
exit 1
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
echo " inst_$i ready"
|
||||
done
|
||||
|
||||
echo "[isolated] launching proxy with --policy $POLICY ..."
|
||||
combined_args=""
|
||||
for i in $(seq 0 $((N_INSTANCES - 1))); do
|
||||
combined_args="$combined_args http://127.0.0.1:$((BASE_PORT + i))"
|
||||
done
|
||||
nohup "$VENV/python" "$ROOT/scripts/cache_aware_proxy.py" \
|
||||
--port "$PROXY_PORT" \
|
||||
--combined $combined_args \
|
||||
--policy "$POLICY" \
|
||||
> "$RUNDIR/proxy.log" 2>&1 &
|
||||
disown
|
||||
tries=0
|
||||
until curl -sf "http://127.0.0.1:$PROXY_PORT/stats" >/dev/null 2>&1; do
|
||||
tries=$((tries + 1))
|
||||
if [ $tries -gt 30 ]; then
|
||||
echo "[isolated] FATAL: proxy did not come up in 60s"
|
||||
tail -30 "$RUNDIR/proxy.log"
|
||||
exit 1
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
|
||||
t_start=$(date +%s.%N)
|
||||
echo "[isolated] running replayer ..."
|
||||
PYTHONPATH="$ROOT" "$VENV/python" -m replayer \
|
||||
--trace "$TRACE" \
|
||||
--output "$RUNDIR/metrics.jsonl" \
|
||||
--endpoint "http://127.0.0.1:$PROXY_PORT" \
|
||||
--model "$MODEL" \
|
||||
2>&1 | tee "$RUNDIR/replayer.log" | tail -3
|
||||
t_end=$(date +%s.%N)
|
||||
|
||||
python3 - "$RUNDIR" "$POLICY" "$TRACE" "$t_start" "$t_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),
|
||||
"isolated": True,
|
||||
}, f, indent=2)
|
||||
PY
|
||||
|
||||
curl -s "http://127.0.0.1:$PROXY_PORT/breakdown" > "$RUNDIR/breakdown.json"
|
||||
curl -s "http://127.0.0.1:$PROXY_PORT/worker_state" > "$RUNDIR/worker_state.json"
|
||||
curl -s "http://127.0.0.1:$PROXY_PORT/stats" > "$RUNDIR/stats.json"
|
||||
echo "[isolated] $POLICY done: $(wc -l < "$RUNDIR/metrics.jsonl") metric rows"
|
||||
159
scripts/compute_apc_upper_bound.py
Normal file
@@ -0,0 +1,159 @@
|
||||
"""Compute theoretical APC upper bound from a trace's hash_ids.
|
||||
|
||||
vLLM prefix caching is block-level (BLOCK_SIZE token chunks) and chain-
|
||||
aware: block i hits the cache iff hash_ids[0..i] matches some previously
|
||||
seen request's hash_ids[0..i]. The trace's `hash_ids` field is the same
|
||||
hash chain.
|
||||
|
||||
Three variants:
|
||||
- any_session : trie built across all previously seen requests
|
||||
- intra_session : trie scoped to the request's own session_id
|
||||
- shared_prefix : trie built from blocks at position 0, that appear
|
||||
across >= K distinct sessions (system-prompt proxy)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
|
||||
BLOCK_SIZE_DEFAULT = 512
|
||||
|
||||
|
||||
def _walk(trie: dict, hashes: list[int]) -> int:
|
||||
depth = 0
|
||||
node = trie
|
||||
for h in hashes:
|
||||
if h in node:
|
||||
depth += 1
|
||||
node = node[h]
|
||||
else:
|
||||
break
|
||||
return depth
|
||||
|
||||
|
||||
def _insert(trie: dict, hashes: list[int]) -> None:
|
||||
node = trie
|
||||
for h in hashes:
|
||||
node = node.setdefault(h, {})
|
||||
|
||||
|
||||
def _resolve_session(row: dict, chat_to_session: dict[int, str]) -> str:
|
||||
if "session_id" in row:
|
||||
return str(row["session_id"])
|
||||
cid = int(row["chat_id"])
|
||||
pcid = int(row["parent_chat_id"])
|
||||
if pcid < 0:
|
||||
sid = str(cid)
|
||||
else:
|
||||
sid = chat_to_session.get(pcid, str(pcid))
|
||||
chat_to_session[cid] = sid
|
||||
return sid
|
||||
|
||||
|
||||
def main() -> None:
|
||||
p = argparse.ArgumentParser()
|
||||
p.add_argument("--trace", type=Path, required=True)
|
||||
p.add_argument("--block-size", type=int, default=BLOCK_SIZE_DEFAULT)
|
||||
p.add_argument("--shared-prefix-min-sessions", type=int, default=8)
|
||||
p.add_argument("--out", type=Path, default=None)
|
||||
args = p.parse_args()
|
||||
|
||||
rows: list[dict] = []
|
||||
chat_to_session: dict[int, str] = {}
|
||||
with args.trace.open("r", encoding="utf-8") as fh:
|
||||
for line in fh:
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
r = json.loads(line)
|
||||
r["session_id"] = _resolve_session(r, chat_to_session)
|
||||
rows.append(r)
|
||||
rows.sort(key=lambda r: float(r.get("timestamp", 0.0)))
|
||||
|
||||
global_trie: dict = {}
|
||||
session_tries: dict[str, dict] = defaultdict(dict)
|
||||
# First-position block stats: how many sessions hit each top-level
|
||||
# hash. We approximate "system prefix" as a block seen at position 0
|
||||
# across many sessions.
|
||||
pos0_session_set: dict[int, set[str]] = defaultdict(set)
|
||||
for r in rows:
|
||||
hids = list(r.get("hash_ids") or [])
|
||||
if hids:
|
||||
pos0_session_set[hids[0]].add(r["session_id"])
|
||||
shared_pos0 = {h for h, s in pos0_session_set.items()
|
||||
if len(s) >= args.shared_prefix_min_sessions}
|
||||
|
||||
total_input = 0
|
||||
cache_any = 0
|
||||
cache_intra = 0
|
||||
cache_shared_only = 0
|
||||
per_session_input: dict[str, int] = defaultdict(int)
|
||||
per_session_intra: dict[str, int] = defaultdict(int)
|
||||
n_with_any_hit = 0
|
||||
n_with_intra_hit = 0
|
||||
n_with_shared_hit = 0
|
||||
|
||||
for r in rows:
|
||||
hids = list(r.get("hash_ids") or [])
|
||||
input_len = int(r.get("input_length") or 0)
|
||||
sid = r["session_id"]
|
||||
total_input += input_len
|
||||
per_session_input[sid] += input_len
|
||||
|
||||
g_depth = _walk(global_trie, hids)
|
||||
s_depth = _walk(session_tries[sid], hids)
|
||||
# Shared-prefix-only: greedy depth, but stop at first non-shared
|
||||
# pos0 block (because then no later blocks can be from system
|
||||
# prefix as a contiguous chain).
|
||||
sh_depth = 0
|
||||
if hids and hids[0] in shared_pos0:
|
||||
sh_depth = 1
|
||||
# subsequent blocks at deeper positions are NOT modeled as
|
||||
# "shared system" in this conservative bound.
|
||||
# accumulate
|
||||
g_tokens = min(g_depth * args.block_size, input_len)
|
||||
s_tokens = min(s_depth * args.block_size, input_len)
|
||||
sh_tokens = min(sh_depth * args.block_size, input_len)
|
||||
cache_any += g_tokens
|
||||
cache_intra += s_tokens
|
||||
cache_shared_only += sh_tokens
|
||||
per_session_intra[sid] += s_tokens
|
||||
if g_tokens > 0:
|
||||
n_with_any_hit += 1
|
||||
if s_tokens > 0:
|
||||
n_with_intra_hit += 1
|
||||
if sh_tokens > 0:
|
||||
n_with_shared_hit += 1
|
||||
# update tries
|
||||
_insert(global_trie, hids)
|
||||
_insert(session_tries[sid], hids)
|
||||
|
||||
out = {
|
||||
"trace": str(args.trace),
|
||||
"n_requests": len(rows),
|
||||
"n_sessions": len({r["session_id"] for r in rows}),
|
||||
"block_size": args.block_size,
|
||||
"shared_prefix_min_sessions": args.shared_prefix_min_sessions,
|
||||
"total_input_tokens": total_input,
|
||||
"apc_upper_any_session": cache_any / total_input,
|
||||
"apc_upper_intra_session": cache_intra / total_input,
|
||||
"apc_upper_shared_prefix_only": cache_shared_only / total_input,
|
||||
"cached_tokens_any_session": cache_any,
|
||||
"cached_tokens_intra_session": cache_intra,
|
||||
"cached_tokens_shared_prefix_only": cache_shared_only,
|
||||
"n_requests_any_hit": n_with_any_hit,
|
||||
"n_requests_intra_hit": n_with_intra_hit,
|
||||
"n_requests_shared_hit": n_with_shared_hit,
|
||||
"n_shared_pos0_blocks": len(shared_pos0),
|
||||
}
|
||||
text = json.dumps(out, indent=2)
|
||||
if args.out:
|
||||
args.out.write_text(text)
|
||||
print(text)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||