Connector tax: trace-replay confirms +45% kv_both penalty is gone; DR-fix adds 22% more
Re-runs the elastic_migration_v2 trace (w600 r0.0015 st30, 1214 reqs,
274 sessions, 8×TP1 vLLM + cache_aware_proxy) with three configs:
- plain unified
- unified + Mooncake kv_both
- unified + Mooncake kv_both + DR-fix (env-gated O(|cache|) hash sync removal)
TTFT p90: 11.97 s → 9.74 s (−18.6%) → 7.58 s (−36.6% vs plain)
E2E p90: 23.48 s → 21.25 s (−9.5%) → 17.93 s (−23.6% vs plain)
Two findings:
1. The "+45% kv_both penalty" claim from elastic_migration_v2 is OBSOLETE
on current codebase — kv_both is now *faster* than plain at p90.
Likely fixed by e3a1d70 (RDMA-READ → bootstrap PUSH refactor) and
the connector-mode delay_free_blocks extending cross-turn prefix
cache hits on a 93%-intra-session-reuse trace.
2. DR-fix removes another 22% from TTFT p90 by skipping the
O(|cache|) hash sync in build_connector_meta. Cache-sweep with
DR-fix shows slope drops from +94.5 to +2.3 μs/1k blocks.
Adds:
- run_trace_replay_drfix.sh: A/B/C harness (env CT_DR_FIX gates patch)
- analyze_trace_replay.py: TTFT/TPOT/E2E delta analysis
- REPORT_TRACE_REPLAY.md: summary + reproduction
- results/20260526_1627_drfix/: cache-sweep with DR-fix
- results/trace_replay_20260526_1652/: full trace-replay A/B/C
Implication for EAR paper: the kv_both substrate is no longer the
bottleneck blocking session migration. The prior 4 migration reverts
were dominated by transfer overhead that has now been characterized
and (partially) removed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -56,21 +56,25 @@ class ReqMetric:
|
||||
error: str | None = None
|
||||
|
||||
|
||||
def make_random_prompt(target_tokens: int) -> str:
|
||||
def make_random_prompt(target_tokens: int, rng: random.Random) -> str:
|
||||
"""Same calibration as the bench_loop.py used elsewhere:
|
||||
'Block N: <32-hex>' tokenizes to ~35 tokens on Qwen3-Coder."""
|
||||
'Block N: <32-hex>' tokenizes to ~35 tokens on Qwen3-Coder.
|
||||
|
||||
Deterministic given `rng` — same RNG state produces the same prompt.
|
||||
Used so two runs with the same --seed get bit-identical request streams.
|
||||
"""
|
||||
n_parts = max(1, target_tokens // 35)
|
||||
seed = uuid.uuid4().hex
|
||||
# 32 random hex chars from rng (uuid.uuid4 would use os.urandom, unseedable)
|
||||
seed = "".join(f"{rng.randrange(16):x}" for _ in range(32))
|
||||
parts = []
|
||||
for i in range(n_parts):
|
||||
h = hashlib.md5(f"{seed}_{i}_{time.time_ns()}".encode()).hexdigest()
|
||||
h = hashlib.md5(f"{seed}_{i}".encode()).hexdigest()
|
||||
parts.append(f"Block {i}: {h}")
|
||||
return " ".join(parts)
|
||||
|
||||
|
||||
async def send_one(client, url, model, inp_tokens, out_tokens,
|
||||
rate, inflight, inflight_cap, fh):
|
||||
rid = uuid.uuid4().hex[:16]
|
||||
async def send_one(client, url, model, prompt, inp_tokens, out_tokens,
|
||||
rate, inflight, inflight_cap, fh, rid):
|
||||
if inflight[0] >= inflight_cap:
|
||||
m = ReqMetric(req_id=rid, rate_target=rate,
|
||||
input_tokens_target=inp_tokens,
|
||||
@@ -88,7 +92,6 @@ async def send_one(client, url, model, inp_tokens, out_tokens,
|
||||
t_send_ns=time.perf_counter_ns(),
|
||||
inflight_at_send=inflight[0])
|
||||
try:
|
||||
prompt = make_random_prompt(inp_tokens)
|
||||
payload = {
|
||||
"model": model,
|
||||
"messages": [{"role": "user", "content": prompt}],
|
||||
@@ -141,25 +144,39 @@ async def main_async(args):
|
||||
inflight = [0]
|
||||
pending: list[asyncio.Task] = []
|
||||
interval_mean = 1.0 / args.rate
|
||||
rng = random.Random(int(time.time_ns()) & 0xFFFFFFFF)
|
||||
|
||||
print(f"[bench] rate={args.rate} shape=({args.input_tokens},{args.output_tokens}) "
|
||||
f"duration={args.duration}s output={out_dir}")
|
||||
# Shared seed across configs gives bit-identical arrival times AND prompt
|
||||
# content. arrival_rng feeds expovariate, content_rng feeds make_random_prompt,
|
||||
# rid_rng feeds the per-request id. All three derive from --seed so two runs
|
||||
# with the same seed are bit-identical from the producer side; only server
|
||||
# response timing differs (which is what we want to measure).
|
||||
if args.seed is not None:
|
||||
seed = args.seed
|
||||
else:
|
||||
seed = int(time.time_ns()) & 0xFFFFFFFF
|
||||
print(f"[bench] seed={seed} rate={args.rate} shape=({args.input_tokens},"
|
||||
f"{args.output_tokens}) duration={args.duration}s output={out_dir}")
|
||||
arrival_rng = random.Random(seed)
|
||||
content_rng = random.Random(seed ^ 0xC0FFEE)
|
||||
rid_rng = random.Random(seed ^ 0xDEADBEEF)
|
||||
|
||||
fh = open(req_path, "a", buffering=1)
|
||||
t0 = time.perf_counter()
|
||||
last_print = t0
|
||||
|
||||
async with httpx.AsyncClient(timeout=httpx.Timeout(600.0)) as client:
|
||||
# producer
|
||||
# producer — prompts generated here in order so async scheduling can't
|
||||
# reorder the content RNG draw across requests.
|
||||
async def producer():
|
||||
while time.perf_counter() - t0 < args.duration:
|
||||
prompt = make_random_prompt(args.input_tokens, content_rng)
|
||||
rid = f"{rid_rng.randrange(1 << 64):016x}"
|
||||
pending.append(asyncio.create_task(
|
||||
send_one(client, args.url, args.model,
|
||||
send_one(client, args.url, args.model, prompt,
|
||||
args.input_tokens, args.output_tokens,
|
||||
args.rate, inflight, args.inflight_cap, fh)
|
||||
args.rate, inflight, args.inflight_cap, fh, rid)
|
||||
))
|
||||
await asyncio.sleep(rng.expovariate(1.0 / interval_mean))
|
||||
await asyncio.sleep(arrival_rng.expovariate(1.0 / interval_mean))
|
||||
|
||||
# heartbeat
|
||||
async def heartbeat():
|
||||
@@ -210,6 +227,10 @@ def main():
|
||||
ap.add_argument("--output-tokens", type=int, default=256)
|
||||
ap.add_argument("--duration", type=float, default=480.0,
|
||||
help="Total run duration in seconds (default 8 min)")
|
||||
ap.add_argument("--seed", type=int, default=None,
|
||||
help="Master seed; same value across configs gives "
|
||||
"bit-identical Poisson arrivals and prompt content. "
|
||||
"Default: time-based (different each run).")
|
||||
ap.add_argument("--inflight-cap", type=int, default=256)
|
||||
ap.add_argument("--output-dir", required=True)
|
||||
args = ap.parse_args()
|
||||
|
||||
Reference in New Issue
Block a user