# Cache-size Sweep — testing H5 from connector_tax DESIGN.md ## Hypothesis under test **H5**: `MooncakeConnectorScheduler.build_connector_meta()` walks `set(self._block_pool.cached_block_hash_to_block._cache.keys())` every scheduler step, so `step_duration_us` and `build_meta_us` should grow **linearly with |cache|** (= the number of cached block-hash entries in the block pool). The +45 % trace-replay tax is hypothesised to come from running this O(|cache|) loop at APC ≈ 79 %, which the prior microbench never tested (random content → cache stays small). ## What we instrument The original `apply_step_timing.py` only recorded `step_duration_us` and `build_meta_us`. This sweep adds: | Field | Source | Why | |---|---|---| | `cache_size` | `len(scheduler.kv_cache_manager.block_pool.cached_block_hash_to_block._cache)` | The exact dict that `set(...)` walks | | `get_finished_us` | wraps `kv_connector.get_finished(...)` in worker mixin | The other suspected cost (two `run_coroutine_threadsafe(...).result()` blocking waits for `kv_both`) | | `start_load_kv_us` | wraps `kv_connector.start_load_kv(...)` in worker mixin | Mostly fast for `kv_both` w/o transfers, but include for completeness | Scheduler-side fields go to `engine_step.jsonl` (existing channel). Worker-side timings go to `worker_step.jsonl` (one file per worker process). ## Method For each config in {`plain`, `noop_connector`, `mooncake_both`}: 1. Launch one fresh vLLM (TP=1, H20, max_model_len=200000, gpu-memory-utilization=0.9, enable_prefix_caching). 2. Read /metrics once to record `kv_cache_max_blocks` (the dict ceiling). 3. Drive an open-loop stream: - shape = 4096 in / 256 out - rate = 2 req/s (kept below saturation to keep step duration dominated by scheduler-not-queueing) - content random per request (UUID + hash), zero prefix-cache hit ⇒ `|cache|` grows monotonically until hit by LRU eviction - duration = until cache fills (≤ 12 min) 4. Collect `engine_step.jsonl` + `worker_step.jsonl` + the per-request metrics from `bench_loop.py`. 5. Tear down vLLM, wait for GPU release. After all three configs: - Apply LWESS-style binning on (`cache_size`, `step_duration_us`) to show the curve per config. - Compute linear fit per config: `step_duration_us ≈ a + b · cache_size`. - Connector-attributable per-step tax at a given |cache|: `tax_us(cache_size) = mc_step(cache_size) − plain_step(cache_size)`. - Same decomposition for `build_meta_us` (only mooncake / noop have non-zero values; plain is 0 by construction). - For worker side: `get_finished_us` distribution per config; in `kv_both` mode this should be non-zero even when no transfer fires. ## What "passes" or "fails" H5 - **PASS**: `step_duration_us` (mooncake_both) grows roughly linearly with |cache|, with slope **> 5 μs / 1 000 blocks** so that at |cache| ≈ 200 k it is ≥ 1 ms of per-step overhead. `plain` shows no slope. This matches the source code reading. - **FAIL**: no measurable slope, or slope is similar for plain and mooncake_both → the O(|cache|) walk is not the actual cost driver and we should look elsewhere (e.g. `get_finished` blocking waits, delay_free overhead). Either outcome is informative. ## What this sweep does *not* answer - Multi-instance coupling (8 schedulers running the walk concurrently vs proxy load-balancing). - Agentic session structure (long prefix reuse + short uncached tail). - The 8-instance trace-replay 45 % figure can only be reconciled once we know the slope and combine with concurrency / coupling measurements. This sweep is a necessary input, not the full reconciliation. ## Files ``` cache_sweep/ ├── DESIGN.md # this file ├── apply_step_timing_v2.py # extends apply_step_timing.py with cache_size + worker timings ├── run_cache_sweep.py # bench driver: per-config continuous open-loop ├── analyze.py # join engine_step + worker_step, plot, fit ├── run_all.sh # orchestrator (apply patch → run 3 configs → revert → analyze) └── results// # one subdir per run └── / ├── engine_step.jsonl ├── worker_step.jsonl ├── requests.jsonl ├── summary.json ├── vllm_stdout.log └── vllm_stderr.log ```