Keep CP HiCache free room from collapsing under cache reuse

Repeated GSM8K/cache-hit traffic can drain a CP owner lane while exact
allocation still succeeds. This changes L1/L2 free-room logic so the trigger
accounts for the pending allocation, then evicts a minimum target chunk or the
exact shortage, whichever is larger. CP load-back also performs best-effort
owner-lane free-room eviction before admitting tiny cache hits that would
otherwise skew one lane.

The commit also adds gated bs>1 prefill timing markers around
recv/bootstrap/batch/inflight polling so future hangs expose the scheduler
boundary instead of only surfacing as a watchdog shutdown. The post-fix repeat
GSM8K failure is recorded as an active regression to continue investigating,
not as old-process noise.

Constraint: Free-room policy must reduce repeated eviction and owner-lane starvation without reserving required+target pages after every allocation
Rejected: Evict to required+target availability | wastes L1/L2 residency under many short cache hits
Rejected: Treat free-room misses as fatal on load-back | exact capacity should remain the strict admission condition
Confidence: medium
Scope-risk: moderate
Directive: Do not remove the gated bs>1 timing markers until repeat-GSM8K hangs have a direct failing boundary
Tested: git diff --check
Tested: python -m py_compile for touched Python files
Tested: remote earlier test_cp_shared_kv_layout.py and test_cp_hicache_metadata.py passed 157 tests after this free-room formula
Not-tested: Two full repeat GSM8K runs after this commit; latest repeat still killed prefill and requires follow-up root-cause work
This commit is contained in:
laoyao0822
2026-06-07 01:23:15 +08:00
parent 68defa3923
commit b17976b60d
9 changed files with 642 additions and 24 deletions

View File

@@ -1781,3 +1781,254 @@ SSH 恢复后重新检查远端状态:
- 本地 `py_compile` 通过:`decode.py``scheduler_output_processor_mixin.py``test_decode_queue_compaction.py`
- 远端 g0034 容器 targeted tests 通过:
`PYTHONPATH=python python -m pytest -q test/registered/unit/disaggregation/test_decode_queue_compaction.py test/registered/unit/managers/test_scheduler_health_check.py``17 passed`
## 2026-06-06 full GSM8K prefill health-fail investigation
Evidence from `/mnt/beegfs/cjy/log/sglang_cp_hicache_20260606_074842.log` during official GSM8K full run:
- Official 200-question run passed before this: `Accuracy=0.970`, `Invalid=0.000`, no CP fail-fast/fallback/traceback in prefill log.
- Full run reached about 87% (`1150/1319`) before service failure.
- Prefill did not throw a Python exception. First prefill-side terminal symptom was health check:
- `08:13:32 Health check failed. Server couldn't get a response from detokenizer... last_heartbeat time: 08:12:18`
- `08:13:35 ... 128 requests still pending ... aborted 128 requests with 503`
- Last normal prefill batch was at `08:12:18`:
- `#new-seq: 7`, `#new-token: 832`, `#cached-token: 4480`, `#queue-req: 104`, `#inflight-req: 9`.
- write-through CP backups for node IDs `6271..6277` reached `final ack` and all write locks were released with `remaining_ongoing=0`.
- Decode errors begin after prefill health failure:
- decode logs start reporting `Could not fetch prefill parallel info from g0034:18992` at `08:13:48`, so decode failure is downstream of prefill bootstrap shutdown, not the first cause.
- Process state after shutdown: no active prefill process/port `17100`; only zombie python children remained. No container OOM evidence was found.
Current hypothesis:
- The scheduler likely stopped progressing after the last normal batch, before producing more detokenizer output.
- Two candidate blocking sites in prefill event loop both use `poll_and_all_reduce_attn_cp_tp_group(...)` and currently lack timing/status logs:
1. `PrefillBootstrapQueue.pop_bootstrapped()` polls bootstrap queue.
2. `SchedulerDisaggregationPrefillMixin.process_disagg_prefill_inflight_queue()` polls inflight KV transfer queue.
- Existing logs cannot distinguish whether the stall is in `recv_requests`, `pop_bootstrapped`, `get_next_disagg_prefill_batch_to_run`, `run_batch/process_batch_result`, or `process_disagg_prefill_inflight_queue`.
Next diagnostic requirement:
- Add low-frequency / env-gated timing logs around the prefill event-loop boundaries and around bootstrap/inflight poll-all-reduce. Log only when elapsed time exceeds a threshold or `SGLANG_CP_SHARED_KV_BS_GT1_DEBUG=1` is set.
- Do not change scheduling or transfer semantics until the blocking boundary is confirmed.
Implemented diagnostic:
- `prefill.py` now has env-gated `[CP_SHARED_KV_BS_GT1_TIMING]` markers/timings around:
- normal prefill event loop: `recv_requests`, `pop_bootstrapped`, `get_next_disagg_prefill_batch_to_run`, `run_batch`, `process_batch_result`, `process_disagg_prefill_inflight_queue`;
- bootstrap queue poll: `PrefillBootstrapQueue.pop_bootstrapped()` around `poll_and_all_reduce_attn_cp_tp_group`;
- inflight transfer poll: `process_disagg_prefill_inflight_queue()` around `poll_and_all_reduce_attn_cp_tp_group`.
- The diagnostic is off by default and should not change scheduling or transfer semantics.
- For the next reproduction, use:
- `SGLANG_CP_SHARED_KV_BS_GT1_TIMING=1`
- `SGLANG_CP_SHARED_KV_BS_GT1_TIMING_LIMIT=0` so the full GSM8K run does not exhaust the per-event log cap before the failure point;
- `SGLANG_CP_SHARED_KV_BS_GT1_TIMING_SLOW_MS=0` for the first reproduction so `*_start` markers are available. The last `*_start` without matching `*_done` is the blocking boundary.
## 2026-06-06 restart with timing: current evidence
Remote log: `/mnt/beegfs/cjy/log/sglang_cp_hicache_20260606_124320.log`.
Observed state:
- Prefill process is still alive after the new traffic batch; no current Python traceback or scheduler fatal.
- No `Health check failed` in this run so far.
- No `CP_SHARED_KV_FAIL_FAST`, no `RuntimeError`, no `Scheduler hit an exception`.
- Timing env is active, but the current run uses `SGLANG_CP_SHARED_KV_BS_GT1_TIMING_LIMIT=512` and `SGLANG_CP_SHARED_KV_BS_GT1_TIMING_SLOW_MS=1`; many timing events have already hit the per-rank cap (`4096` total across 8 ranks). If the service later hangs, the boundary marker may be exhausted and not show the final blocking point. For hang reproduction, use `SGLANG_CP_SHARED_KV_BS_GT1_TIMING_LIMIT=0`.
Interesting non-fatal findings:
- `CP_SHARED_KV_FALLBACK` appears 64 times, all seen as `tai_ipc_materialize reason=paged_start_slot_nonzero`; this means some current/prefix materialize paths still fall back from TAI IPC to local materialize plus collective when page-slot range starts from a nonzero slot. This is performance-relevant, not the fatal cause in this run.
- One request abort was observed at 13:00:28 across all CP ranks:
- `rid=76658e8841dc4db394a4df3052419e56`
- `prefill_bootstrap=4`, `prefill_inflight=2`, `abort_message=None`
- The scheduler continued normally afterward, so this abort is not the fatal condition.
- Batch size distribution from CP0 in the parsed window shows bs>1 is active:
- `bs=1: 411`, `bs=2: 132`, `bs=3: 128`, `bs=4: 62`, `bs=5: 22`, `bs=6: 8`, `bs=7: 4`.
Timing hotspots:
- `event_loop_run_batch_done`: avg ~1031 ms, p95 ~2025 ms, max ~5690 ms. This includes GPU forward and expected model work.
- `scheduler_prefill_prepare`: avg ~73 ms, p95 ~318 ms, max ~920 ms.
- Slowest `scheduler_prefill_prepare` cases are dominated by `prepare_for_extend`, not by `scan_waiting_queue`:
- example: `elapsed_ms=920.309`, `bs=3`, `extend_lens=[270, 2186, 1113]`, `prefix_lens=[51776, 79296, 94464]`, `prepare_for_extend=870.576 ms`.
- another example: `bs=1`, `extend_len=17363`, `prefix_len=137728`, `hicache_ready_to_load=43 ms`, `prepare_for_extend=716 ms`.
- `scan_waiting_queue` can still reach about 100 ms when queue is nontrivial, but it is not the largest hotspot in these slow samples.
Current conclusion:
- This restart has not reproduced the previous prefill-death symptom yet.
- The strongest evidence from this run is performance/control-path overhead in `prepare_for_extend` under large cached-prefix / bs>1 cases, plus residual TAI IPC fallback for nonzero page-slot materialize.
- For a future fatal reproduction, the current timing cap is insufficient; restart with unlimited timing limit before relying on the last `*_start`/`*_done` boundary.
## 2026-06-06 full GSM8K accuracy verification
Command run on g0034 container (`/sgl-workspace/sglang-tai`) against router `g0034:17100`:
```bash
OPENAI_API_KEY=EMPTY PYTHONPATH=python python benchmark/gsm8k/bench_sglang.py \
--host g0034 --port 17100 --backend srt \
--data-path /mnt/beegfs/cjy/data/gsm8k_test.jsonl \
--num-questions 1319 --parallel 64 \
--temperature 0.0 --top-p 1.0 --max-new-tokens 512 \
--result-file /mnt/beegfs/cjy/gsm8k_bench_sglang_full_20260606_132651.jsonl \
--raw-result-file /mnt/beegfs/cjy/gsm8k_bench_sglang_full_raw_20260606_132651.jsonl
```
Result evidence:
- Progress reached `1319/1319`.
- `Accuracy: 0.955`.
- `Invalid: 0.000`.
- `Latency: 311.296 s`.
- `Output throughput: 451.072 token/s`.
- Summary file: `/mnt/beegfs/cjy/gsm8k_bench_sglang_full_20260606_132651.jsonl`.
- Raw file: `/mnt/beegfs/cjy/gsm8k_bench_sglang_full_raw_20260606_132651.jsonl`.
- Log file: `/mnt/beegfs/cjy/gsm8k_bench_sglang_full_20260606_132651.log`.
- Post-run router health: `http://127.0.0.1:17100/health` returned `OK`.
- Post-run latest prefill log `/mnt/beegfs/cjy/log/sglang_cp_hicache_20260606_124320.log` had no matches for:
`Health check failed`, `Traceback`, `RuntimeError`, `Scheduler hit an exception`, `CP_SHARED_KV_FAIL_FAST`, `KVTransferError`, `SIGQUIT`.
Interpretation:
- Full GSM8K correctness is back to the expected official number (`0.955`) under current bs>1 CP shared-KV/HiCache configuration.
- The previous full-run prefill death was not reproduced in this run.
- This only verifies GSM8K accuracy and basic post-run service health; it does not close remaining performance issues such as `prepare_for_extend` CPU overhead or `tai_ipc_materialize reason=paged_start_slot_nonzero` fallback.
Caveat:
- `wc -l` on the raw result file reported `1318`, while the benchmark progress reached `1319/1319` and the summary result records `num_requests=1319`. Treat the printed benchmark summary as the accuracy source of truth unless raw-record completeness is needed for per-question diagnosis.
## 2026-06-06 second full GSM8K cache-check prefill death
Remote run under test:
```bash
OPENAI_API_KEY=EMPTY PYTHONPATH=python python benchmark/gsm8k/bench_sglang.py \
--host g0034 --port 17100 --backend srt \
--data-path /mnt/beegfs/cjy/data/gsm8k_test.jsonl \
--num-questions 1319 --parallel 64 \
--temperature 0.0 --top-p 1.0 --max-new-tokens 512 \
--result-file /mnt/beegfs/cjy/gsm8k_bench_sglang_full_cachecheck_20260606_135051.jsonl \
--raw-result-file /mnt/beegfs/cjy/gsm8k_bench_sglang_full_cachecheck_raw_20260606_135051.jsonl
```
Observed failure:
- Router health was `OK` before starting the second run.
- The benchmark progressed to about `77%` and then stalled; the benchmark process had to be killed manually.
- Prefill port `50001` was gone after the stall while router port `17100` was still listening.
- Latest prefill log remained `/mnt/beegfs/cjy/log/sglang_cp_hicache_20260606_124320.log`.
- No prefill-side Python traceback or explicit CP fail-fast was found immediately before death.
- Last normal prefill batch before watchdog was at `13:54:35` with `#new-seq: 7`, `#new-token: 448`, `#cached-token: 4928`, `#queue-req: 42`, `#inflight-req: 12`.
- Watchdog symptom:
- `13:55:17 Health check failed. Server couldn't get a response from detokenizer ... last_heartbeat time: 13:54:35`.
- `13:55:20 ... 65 requests still pending ... aborted 65 requests with 503`.
- Last timing evidence before death shows bootstrap queue did not fully drain:
- `bootstrap_poll_done elapsed_ms≈1.0 queue=7 polls_head=[2,2,2,2,2,2,1]` on multiple ranks.
- `event_loop_pop_bootstrapped_done ... bootstrapped=6 ... bootstrap_remaining=1`.
- Right after that, CP write locks for nodes `9803..9809` were released, followed by a burst of HiCache `load_back CP` logs for small host hits. These load_back entries report `SUCCESS`, so this is evidence of activity near the stall, not yet proof of a failing load_back transfer.
- Stale rid to investigate: `76658e8841dc4db394a4df3052419e56` was aborted much earlier at `13:00:28` with `prefill_bootstrap=4 prefill_inflight=2`, but appears again in the final watchdog `remaining_rids` list at `13:55:20`.
Current investigation direction:
- Root cause is not established yet.
- Two high-priority code paths to inspect:
1. `PrefillBootstrapQueue.pop_bootstrapped()` ordering/cleanup behavior when one item remains pending while later items may be ready.
2. Abort cleanup for requests already present in prefill bootstrap/inflight queues and tokenizer pending state.
- Do not treat the final HiCache load_back burst as root cause until a missing `*_done` timing boundary or code path proves it blocks scheduler progress.
### 2026-06-06 root-cause finding: L1 owner-lane free-room was only diagnostic on success paths
Evidence added after code inspection:
- Final cache-check failure ended after a burst of CP HiCache load-back for tiny host hits:
- `host_hit_len=64` appeared `104` times in the final two-second window across CP ranks;
- `host_hit_len=128` appeared `8` times;
- owner-lane availability in those logs fell to `available_by_owner[0]=1` while `free_room_deficit_by_owner[0]≈443`.
- `hiradix_cache.load_back()` logged `free_room_deficit_by_owner`, but only evicted on `deficit_by_owner` (exact required pages), not on free-room deficit. Therefore successful tiny load-backs could drain owner lane 0 without replenishing L1 free room.
- `alloc_paged_token_slots_extend()` only called `_evict_for_compute_owner_lanes()` after the exact owner-lane allocation failed. Since `alloc_extend_compute_owner()` itself only enforces exact required pages, L1 free-room ratios were not maintained on successful extend allocations either.
- This explains why the second GSM8K run, unlike the first, can regress after cache is populated: many short cached prefixes map to the first CP owner lane, consume owner-0 pages, and leave the scheduler with little L1 room for subsequent cache-hit/extend work.
Fix direction:
- Preserve exact fail-fast semantics.
- Add proactive owner-lane free-room eviction before successful owner-aware extend allocation when L1 free-room trigger/target ratios are enabled.
- Add best-effort CP HiCache load-back owner-lane free-room eviction when exact capacity is sufficient but free room is below trigger. Failure to satisfy the free-room target should not reject the load-back; exact deficits still remain strict.
### 2026-06-06 L1 owner-lane free-room fix implemented
Implementation:
- `alloc_paged_token_slots_extend()` now proactively calls CP owner-lane eviction before the first successful owner-aware allocation when L1 free-room target/trigger ratios are enabled. This keeps the configured free room from being only a failure-path mechanism.
- CP HiCache `load_back()` now performs best-effort owner-lane free-room eviction when exact `deficit_by_owner` is zero but `free_room_deficit_by_owner` is positive. Exact deficits still remain strict; free-room misses do not reject load-back.
Regression coverage:
- Added `test_compute_owner_alloc_maintains_l1_free_room_before_successful_alloc` to lock the case where exact allocation succeeds but lane free-room trigger requires proactive eviction.
- Remote g0034 container verification:
- `PYTHONPATH=python python -m py_compile python/sglang/srt/mem_cache/common.py python/sglang/srt/mem_cache/hiradix_cache.py test/registered/unit/mem_cache/test_cp_shared_kv_layout.py` passed.
- Targeted owner-lane tests: `3 passed`.
- Full `test_cp_shared_kv_layout.py`: `38 passed`.
Open runtime validation:
- No new prefill process has been started from this fix yet. The currently latest remote log remains the old failed run (`sglang_cp_hicache_20260606_124320.log`) with prefill port `50001` absent and router port `17100` still listening.
### 2026-06-06 correction: free-room trigger/target semantics
User correction applied to both L1 owner-lane and L2 host admission semantics:
- Trigger condition must account for the pending allocation/reservation:
- trigger when `available < required + trigger_room`;
- equivalently, when `available - required < trigger_room`.
- Once triggered, `target_room` is the minimum eviction chunk. Evict
`max(required - available, target_room)`.
- Rationale: `required + target_room` leaves a full target watermark after the
current allocation and can waste too much L1/L2 cache residency under many
small cache-hit requests. Evicting only to `max(required, target_room)`
available is also wrong because it treats `target_room` as an available
watermark rather than a minimum eviction chunk.
Tests added/updated:
- L1: direct owner-lane helper test now covers `available=2, required=1, trigger=2`, which must trigger because the pending allocation would leave only one page.
- L2: `_free_room_deficit` and CP host admission tests now expect
`max(required - available, target_room)` after trigger.
## 2026-06-07 repeat GSM8K precision run blocked by prefill death
Remote run attempted for the requested first repeat:
```bash
OPENAI_API_KEY=EMPTY PYTHONPATH=python python benchmark/gsm8k/bench_sglang.py \
--host g0034 --port 17100 --backend srt \
--data-path /mnt/beegfs/cjy/data/gsm8k_test.jsonl \
--num-questions 1319 --parallel 64 \
--temperature 0.0 --top-p 1.0 --max-new-tokens 512 \
--result-file /mnt/beegfs/cjy/gsm8k_bench_sglang_full_repeat1_20260606_171214.jsonl \
--raw-result-file /mnt/beegfs/cjy/gsm8k_bench_sglang_full_repeat1_raw_20260606_171214.jsonl
```
Observed failure:
- Benchmark log: `/mnt/beegfs/cjy/gsm8k_bench_sglang_full_repeat1_20260606_171214.log`.
- Progress reached roughly the middle of the dataset, then failed with router/decode error:
`decode_internal_error: Could not fetch prefill parallel info from g0034:18992 after 20 attempts`.
- After failure, only router port `17100` was listening; prefill port `50001` and bootstrap port `18992` were gone.
- Prefill log: `/mnt/beegfs/cjy/log/sglang_cp_hicache_20260606_164817.log`.
- Last normal prefill batch before watchdog:
`17:14:54 #new-seq=5 #new-token=640 #cached-token=3200 #queue-req=47 #inflight-req=9`.
- CP write locks for nodes `5822..5826` were released immediately after that batch.
- Watchdog symptom:
- `17:15:37 Health check failed ... last_heartbeat time: 17:14:54`.
- `17:15:41 ... 65 requests still pending ... aborted 65 requests with 503`.
- No prefill-side Python traceback, explicit OOM, or CP fail-fast was found immediately before the watchdog shutdown.
Important correction:
- User confirmed this repeat was run on a new prefill process started after the free-room formula corrections. Therefore the failure is valid evidence that the latest free-room correction alone does not fully fix the repeat-GSM8K prefill death.
- The second requested repeat was not started because the first repeat took down prefill/bootstrap.
Current next validation requirement:
- Treat this as an active post-fix hang/regression, not an old-process artifact.
- If this hang needs deeper diagnosis, run with an uncapped or higher CP bs>1 timing log limit so the final scheduler/bootstrap boundary is visible; the current log reaches watchdog without a direct failing traceback.

View File

@@ -91,7 +91,7 @@ The new design should not add a per-tick maintenance loop.
Add a **reactive host free-room watermark** to CP HiCache write admission.
When a reservation would fit, do nothing:
Initial draft, superseded below, only evicted when a reservation would not fit:
```text
if required <= available:
@@ -106,9 +106,9 @@ if required > available:
deficit = required + free_room_target - available
```
Because eviction happens before reservation, reserving `required` after this
eviction should leave approximately `free_room_target` host slots available on
the affected owner lanes.
This `required + free_room_target` target is now rejected because it can
over-reserve free room and waste cache residency under many small cache-hit
requests.
## Scope for the first implementation
@@ -214,12 +214,15 @@ continues:
if available >= required + trigger_room:
deficit = 0
else:
deficit = max(0, required + target_room - available)
deficit = max(max(0, required - available), target_room)
```
This keeps remaining cache usable when the current reservation fits, while still
making a triggered eviction release enough extra room to reduce repeated eviction
and give subsequent allocators a better chance of finding contiguous page runs.
The trigger is evaluated after accounting for the pending allocation
(`available - required < trigger_room`). Once triggered, `target_room` is the
minimum eviction chunk; if the current allocation needs more than that, evict the
larger exact deficit. It deliberately does **not** restore `required +
target_room`, because doing so would leave a full target room after the current
allocation and can evict useful cache too aggressively.
Implications:

View File

@@ -1005,7 +1005,7 @@ scheduler admission 不做 bf16/fp8 字节级估算。原因是实际 KV pool
打开 scheduler bs>1 gate 后CP HiCache write-through 的 radix node / host reservation 仍然必须保持 per request 独立;但 host 侧容量 admission 如果仍按 request 串行执行,会在同一个 prefill batch 内反复触发 L2 host free-room eviction
- correctness 没问题,因为 `reserve_write_cp()` 最终会消费真实 host allocator slot
- 性能和碎片化较差,因为每个 request 都可能单独计算一次 `required + free_room_target - available`,在 host 接近满载时容易多次小额 eviction
- 性能和碎片化较差,因为每个 request 都可能单独触发 free-room watermark admission正确公式应在 `available - required < trigger_room` 时触发,并 evict `max(required - available, target_room)`,其中 `target_room` 是最小 eviction chunk不是目标 available 水位
- 这与 L1/L2 free-room 的目标相反:应尽量一次 evict 到目标余量,降低 eviction 频率并给后续 allocate 更连续的空间。
### 实现原则