profile(kvc): add D KV pool timeseries poller + analyzer for v6 root-cause

v5 dropped errors but pushed session-cap fallback to 46-51%. Before adding
v6 mitigations we need to attribute that capacity loss to one of:
  (a) active sessions — real footprint
  (b) idle-evictable sessions — LRU not aggressive enough
  (c) prefill backup blocks / in-flight / fragmentation — release timing

Without this it's all guessing. Plumb a 1Hz poller into replay that hits
each P/D worker's /server_info, captures session_cache + memory_usage, and
writes a per-worker time-series JSONL to <run_dir>/d-pool-timeseries.jsonl.
Off by default (--pool-poll-interval-s 0); v5+profile sweep enables it at
1.0s. Per-tick HTTP cost is ~8 parallel /server_info calls — negligible
relative to the 50min run.

Analyzer (scripts/analysis/analyze_pool_timeseries.py) decomposes each D's
capacity into active_held / idle_evictable / other (= cap-held-avail, the
backup-blocks bucket) / free, and reports session residency churn across
workers as a starvation/thrashing signal.

Mock-tested poller end-to-end (cancellation clean, file flushed, sessions
captured); analyzer validated against synthetic timeseries.

Next: run scripts/sweep_tp1_v5_optD_profile.sh on hardware (~90min), then
analyze results to pick a v6 direction.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
kzlin
2026-04-29 20:04:21 +08:00
parent 6572d7f3f4
commit 51f5386691
5 changed files with 587 additions and 0 deletions

View File

@@ -228,6 +228,23 @@ def main() -> None:
)
replay.add_argument("--kvcache-prefill-direct-priority", type=int, default=-100)
replay.add_argument("--kvcache-prefill-normal-priority", type=int, default=100)
replay.add_argument(
"--pool-poll-interval-s",
type=float,
default=0.0,
help=(
"Poll each P/D worker's /server_info every N seconds and write a "
"time-series snapshot to <run_dir>/d-pool-timeseries.jsonl. "
"0 disables polling."
),
)
replay.add_argument(
"--pool-poll-no-sessions",
action="store_true",
help=(
"Disable per-session detail in the pool timeseries (smaller files)."
),
)
sample = subparsers.add_parser(
"sample-sessions",
@@ -439,6 +456,23 @@ def main() -> None:
)
benchmark.add_argument("--kvcache-prefill-direct-priority", type=int, default=-100)
benchmark.add_argument("--kvcache-prefill-normal-priority", type=int, default=100)
benchmark.add_argument(
"--pool-poll-interval-s",
type=float,
default=0.0,
help=(
"Poll each P/D worker's /server_info every N seconds and write a "
"time-series snapshot to <run_dir>/d-pool-timeseries.jsonl. "
"0 disables polling."
),
)
benchmark.add_argument(
"--pool-poll-no-sessions",
action="store_true",
help=(
"Disable per-session detail in the pool timeseries (smaller files)."
),
)
benchmark.add_argument(
"--sample-profile",
choices=["default", "small-append"],
@@ -520,6 +554,8 @@ def main() -> None:
),
kvcache_prefill_direct_priority=args.kvcache_prefill_direct_priority,
kvcache_prefill_normal_priority=args.kvcache_prefill_normal_priority,
pool_poll_interval_s=args.pool_poll_interval_s,
pool_poll_include_sessions=not args.pool_poll_no_sessions,
)
results = asyncio.run(replay_trace(config))
print(
@@ -662,6 +698,8 @@ def main() -> None:
kvcache_prefill_normal_priority=(
args.kvcache_prefill_normal_priority
),
pool_poll_interval_s=args.pool_poll_interval_s,
pool_poll_include_sessions=not args.pool_poll_no_sessions,
sample_profile=args.sample_profile,
min_initial_input_tokens=args.min_initial_input_tokens,
max_initial_input_tokens=args.max_initial_input_tokens,