Protect CP shared-KV cache-hit correctness under batched FP8 reuse

Cache-hit GSM8K regressions only appeared after the second pass reused request-specific suffix pages, so this change adds fail-fast transfer validation, masks stale rectangular page-table tails, and extends CUDA/unit coverage across FP8 CP shared-KV write, load, top-k, and materialization paths. The temporary ledger records eliminated hypotheses to prevent re-debugging the same L2 and persistent-cache paths.\n\nConstraint: CP shared KV stores physical pages but scheduler-visible semantics must remain valid-token/page-bounded.\nConstraint: bs>1 FP8 prefill must preserve existing CP shared-KV fast paths without silent fallback.\nRejected: Blame raw HiCache L2 load without tests | L2 KV and index backup/load/materialize roundtrips pass on remote CUDA.\nRejected: Disable current/partial reuse broadly | hides the cache-hit contract regression and costs performance.\nConfidence: medium\nScope-risk: moderate\nDirective: Do not weaken CP shared-KV fail-fast or rectangular-tail masking without rerunning second-pass cache-hit accuracy tests.\nTested: remote CUDA pytest for fused FP8 MLA store, fused persistent index store, L2-loaded FP8 KV materialize, L2-loaded index materialize, ragged top-k offset, TAI batched index MQA prepare.\nTested: local py_compile for touched test files and git diff --check.\nNot-tested: full second-pass GSM8K accuracy after these diagnostic tests; root cause remains under investigation.
This commit is contained in:
laoyao0822
2026-06-07 13:26:49 +08:00
parent b17976b60d
commit f75ffff8d9
10 changed files with 2645 additions and 1 deletions

View File

@@ -2032,3 +2032,178 @@ 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.
### 2026-06-07 repeat GSM8K post-fix death: current bounded finding
New evidence from `/mnt/beegfs/cjy/log/sglang_cp_hicache_20260606_164817.log`:
- The process did not die with a Python traceback, explicit OOM, CP fail-fast, or `Scheduler hit an exception`.
- Last normal prefill batch was `17:14:54` with `bs=5`, `new_token=640`, `cached_token=3200`, `queue_req=47`, `inflight_req=9` on all CP ranks.
- The final batch's node/rid write-through backup reached `submit_write_cp_layer final ack`, `attached prepared CP backup`, and `writing_check released 5 write locks` on all CP ranks.
- The final watchdog pending list still contained the final-batch rids (`3732391e...`, `17f4f067...`, `55a9e51c...`, `2827e296...` etc.), so the `POST /generate 200 OK` lines at `17:14:54` are not proof that the final batch completed; they may be earlier inflight completions.
- `GET /health` was healthy through `17:14:18`; the watchdog started at `17:15:17` and reported `last_heartbeat=17:14:54`.
- The timing diagnostics were ineffective at the actual failure point because the run had a malformed env assignment: `SGLANG_CP_SHARED_KV_BS_GT1_TIMING_LIMIT` parsed as `"-1SGLANG_CP_SHARED_KV_BS_GT1_DEBUG=1"` and fell back to default `256`, so common scheduler boundary events hit their per-event cap around `16:59`.
Current inference:
- The strongest current suspicion is a scheduler-side hang after the final batch, before any later detokenizer response. The likely boundaries are still:
1. `process_disagg_prefill_inflight_queue()` polling inflight KV transfer;
2. `PrefillBootstrapQueue.pop_bootstrapped()` / subsequent scheduler admission for the next batch;
3. `stream_output()` / ZMQ send if the detokenizer side stops consuming.
- Existing logs cannot distinguish these because the final boundary markers were capped before the failure.
- Do not keep re-reading this log expecting a hidden traceback; the current log does not contain one.
Diagnostic change added locally:
- Added an env-gated CP shared-KV poll queue consensus guard behind the existing `SGLANG_CP_SHARED_KV_BS_GT1_DEBUG=1`.
- The guard validates `(queue_len, rid_hash)` across attn-TP and attn-CP CPU groups before the old variable-length poll tensor all-reduce. If queue length or rid order diverges, it raises `[CP_SHARED_KV_FAIL_FAST][poll_queue]` instead of risking a silent collective hang.
- This intentionally adds extra scalar collectives only when the existing `SGLANG_CP_SHARED_KV_BS_GT1_DEBUG=1` is enabled; normal production and timing-only paths do not pay this cost.
- Regression test added: `test_poll_consensus_debug_fails_before_shape_mismatch_hang`.
Next reproduction requirements:
- Use correctly separated env assignments:
- `SGLANG_CP_SHARED_KV_BS_GT1_TIMING=1`
- `SGLANG_CP_SHARED_KV_BS_GT1_TIMING_LIMIT=0`
- `SGLANG_CP_SHARED_KV_BS_GT1_TIMING_SLOW_MS=0`
- optional: `SGLANG_CP_SHARED_KV_BS_GT1_DEBUG=1` for one diagnostic run only.
- If the next run reports `[CP_SHARED_KV_FAIL_FAST][poll_queue]`, root cause is queue length/order divergence across ranks.
- If it still watchdogs without poll fail-fast, the next suspect is a call-count mismatch between ranks or a non-poll blocking site (`stream_output`, admission/eviction planning, or receive loop); use the uncapped boundary markers to locate the last `*_start` without matching `*_done`.
## 2026-06-07 cache-hit GSM8K accuracy regression
New user-provided evidence on `g0034:17100` with full GSM8K (`1319` questions, parallel=64, temp=0):
```text
cold/first run: Accuracy: 0.955 Invalid: 0.000 Latency: 207.174 s Output throughput: 668.366 token/s
second run: Accuracy: 0.687 Invalid: 0.001 Latency: 211.220 s Output throughput: 660.263 token/s
```
Interpretation:
- The cold path can still reach the expected GLM-5.1 GSM8K accuracy, so the base model, decode EAGLE, FP8 decode, and non-hit prefill path are not globally broken.
- The immediate second run is a cache-hit path and drops accuracy sharply. This shifts the top suspect away from generic EAGLE/FP8 decode and toward cache-hit semantics: radix/HiCache match length, page-tail valid length, CP shared-KV load/materialize, or cached index/MLA reuse under page-aligned storage.
- Throughput remains similar between runs, so this is correctness corruption rather than a service-death or obvious slow-path-only issue.
Current top hypothesis to verify next:
- Some path exposes page-rounded/padded KV as valid cached prefix, or uses cached prefix metadata without flooring to the last valid page boundary. A later request can then skip recomputing tail tokens or consume stale/padded page data, which would be invisible on the cold run and show up only after cache hits.
Immediate code audit targets:
1. `hiradix_cache.py` CP page-floor/prune/match logic: cached match length returned to scheduler must be valid-token/page-floor length, never physical padded extent.
2. `radix_cache.py` / `schedule_batch.py`: `prefix_indices`, `host_hit_length`, `cache_protected_len`, and `extend_input_len` must not include padded tail tokens.
3. `nsa/utils.py` and current reuse paths: batch compute padding and physical page padding must not alter request logical lengths used for top-k/MLA attention.
4. HiCache load path: host backed CP node should obey the same page-minimum contract; if a node is backed only to page floor, cache hit must not round up to the next page.
## 2026-06-07 继续定位cache-hit 回退集中到 slot-remap 的矩形 page table 合同
新增证据:
- 两次 GSM8K 全量同进程复跑cold run `Accuracy=0.955`immediate cache-hit run `Accuracy=0.687`。cold 正确说明模型、decode 基本路径、FP8 非命中路径不是全局损坏;错误集中在 cache-hit 前缀被 prefill 重新消费后。
- 远端日志 `/mnt/beegfs/cjy/log/sglang_cp_hicache_20260606_180649.log` 第二轮出现稳定 cache-hit batch`#new-seq: 5`, `#new-token: 320/384`, `#cached-token: 3520/3712`,约等于每请求 704+ cached tokens。
- `hiradix_cache` 初查没有看到把 non-page-aligned hit 向上 round-up 的证据;当前仍是 page-floor 方向。因此下一主线不是 radix round-up而是 cache-hit prefix materialize/compose。
- `nsa_backend.py` 使用 `metadata.real_page_table` 构造 token/index slot-remap`real_page_table` 来自矩形 `req_to_token[:, :max_seq_len]` 按 page stride 取样。bs>1 且请求长度不同/slot 复用时,矩形尾部可能包含请求 valid page 之外的旧 loc。
- `cp_shared_kv_runtime.build_slot_page_inverse()` 对 duplicate logical page 使用 `scatter_`,同一 logical page 会映射到最后一次出现的 slot。若 valid prefix/current page 也出现在某个请求的矩形无效尾部page_inverse 可能把合法 topk loc remap 到一个未被 prefix/current spans materialize/reduce 的 stale slotcache-hit attention 会读到 0/错误 KV。该路径只在 cache-hit prefix materialize 时高概率触发,符合 cold 正常、hit 掉点的现象。
待验证/修复方向:
1.`metadata.real_page_table`/index logical page table 增加 batch-aware valid-page mask每个 request 只保留 `ceil(seq_len/page_size)``ceil((prefix_len+extend_len)/page_size)` 范围内的 page范围外置 0不参与 slot inverse。
2. 增加单测:构造 bs>1 矩形 logical_pages尾部 stale slot 重复了前面 valid logical page验证 slot remap 不能把 valid loc 映射到 stale slot。
3. 修复应优先在 slot-remap 输入处清洗 invalid rectangular tail而不是禁止 cache-hit/current reuse。
## 2026-06-07 修复:在 NSA metadata 边界清理 rectangular page-table tail
- 变更点:`NSAMetadata.real_page_table` 构建后立即调用 `mask_batch_logical_pages_to_valid_lengths()`,按每个 request 的有效 `seq_len` 保留 `ceil(seq_len/page_size)` 个 page后续矩形尾部统一置 0。
- 选择该位置的原因:`real_page_table` 同时喂给 MLA KV materialize、index materialize、prefetcher 与 slot-remap cache。集中清理比在每个 consume/materialize 调用点单独清理更不容易遗漏。
- 保留的合同:最后一个有效 partial page 仍保留;不会把 token 级 valid length 截成 compact token cache只清理 page-table 的 invalid row tail。
- 覆盖的风险:当 bs>1 不同 seq_len 混排、request slot 复用导致矩形尾部出现旧 logical page`build_slot_page_inverse`/TAI inverse 的 duplicate page 写入不再能把有效 page remap 到 stale slot。
- 验证:远端容器 `py_compile` 通过;`test_cp_shared_kv_runtime.py::TestCpSharedKVRuntimeHelpers::test_valid_page_mask_prevents_stale_rectangular_tail_remap` 通过;完整 `test_cp_shared_kv_runtime.py` 通过 `114 passed, 5 warnings, 2 subtests passed`
- 未验证:最新 ETE cache-hit GSM8K 二次全量精度仍需用户重启新代码后复测。
### 2026-06-07 澄清cuda graph replay 不是当前 prefill ETE 主线
- 用户确认当前 prefill 不走 cuda graph。
- 因此 `init_forward_metadata_replay_cuda_graph()` 中同类 rectangular-tail 复用风险不作为本次 GSM8K cache-hit 掉点的 active root cause。
- 当前主线收敛到非 cuda-graph `init_forward_metadata()` 构建的 `real_page_table`,以及其下游 MLA/index materialize、prefetch、partial/current reuse。
### 2026-06-07 继续审计:`forward_indexer()` 的 direct req_to_token page-table 路径
- 发现 `nsa_indexer.forward_indexer()` 仍直接从 `forward_batch.req_to_token_pool.req_to_token[:, ::page_size]` 构造 `block_tables`,再传入 `_maybe_materialize_shared_index_buffer()`
- 这条路径若在 CP shared-KV + cache-hit 下启用,会绕过 `nsa_backend.init_forward_metadata()` 中对 `real_page_table` 的 logical-tail mask存在同类 stale rectangular-tail 风险。
- 但当前 CUDA/Hopper prefill 的 `forward_cuda()``_is_cuda or _is_hip` 分支内走 `_get_topk_paged/_get_topk_in_seq_cp_pair/_get_topk_ragged`,只有非 CUDA/HIP 分支才调用 `forward_indexer()`
- 结论:这是潜在维护风险,不是当前 prefill ETE 的 active root cause当前继续审计 CUDA active path。
### 2026-06-07 继续审计CUDA graph replay / MTP precompute 的 direct req_to_token 路径
- 发现 `nsa_backend.init_forward_metadata_replay_cuda_graph()` 以及 `nsa_backend_mtp_precompute.py` 的 decode/target-verify/draft precompute 分支也直接从 `req_to_token` 构造 token/page table并在 `real_page_size > 1` 时 transform 成 `real_page_table`
- 这些路径当前未调用 `mask_batch_logical_pages_to_valid_lengths()`,因此在 decode/target-verify/draft CUDA graph replay 或 MTP precompute 场景中存在同类 stale rectangular-tail 风险。
- 用户已确认当前 prefill 不走 cuda graph因此它们不是本次 prefill GSM8K cache-hit 掉点 active root cause。
- 后续若要覆盖 decode graph / draft graph应在 precompute/replay 边界同样按 logical `seq_lens` mask real-page-table且需要专门测试不能把它混入当前 prefill 修复判断。
### 2026-06-07 再确认page 物理粒度不等于 token 可见长度向上取整
本轮代码复核结论:
- `ReqToTokenPool.alloc()` 复用 slot 时只从 `free_slots` 取回 `req_pool_idx``free()` 也只把 slot 放回队列并清空 `req.req_pool_idx``req_to_token` 行内容不会被清零。因此矩形 batch 读取到 `max_seq_len` 时,短请求 valid token 之后的 tail 可以携带旧请求残留。
- `write_cache_indices()` 只写 `[0, prefix_len)``[prefix_len, seq_len)`,不会清理 `[seq_len, max_seq_len)`。这进一步确认 stale tail 是真实输入状态,不是单测构造出来的假风险。
- CP HiCache 的合同是radix/scheduler 可见长度使用 `logical_len`/`valid_len`,物理 host/device reservation 使用 `padded_len`,且 `padded_len` 必须是 `page_size` 倍数。也就是说 cache 存储和释放以 page 为最小物理单位,但命中长度和 split 仍不能把 padding token 暴露为有效 token。
- CP HiCache split 强制 `split_len % page_size == 0`exact non-page-aligned hit 会 floor 到上一页stale tail child 会被 prunechunked prefill backup 也只 backup 到 `floor_to_page_len()`。这些都支持同一个合同:可以牺牲一个 sub-page tail 来换 page 管理一致性,但不能把 page padding 当成有效语义 token。
- 因此 `real_page_table` 的 tail mask 是正确方向:保留 `ceil(valid_seq_len / page_size)` 个 page包括最后一个 partial page只把每行 valid page 数之后的矩形尾部置 0避免 stale duplicate page 参与 `page_inverse.scatter_()`。这不会丢失以 page 存储的合法 cache只会阻断无效矩形尾部覆盖 slot inverse。
继续审计到的类似风险:
1. `metadata.page_table_1` 仍保留 token 粒度矩形表,未整体 mask。当前 CUDA prefill 的 CP shared-KV materialize/slot-remap 已经改用 masked `metadata.real_page_table`,但 `topk_transform()` 的 paged fused path 仍以 `page_table_1` 为输入。现有 debug validator 只检查输出是否在整个 page table 的 min/max 范围内,不检查“输出 loc 是否属于该 request 的 valid prefix”。如果 cache-hit 复测仍掉点,下一步应加一个 env/debug 级 per-row membership validator而不是继续禁用 current/partial reuse。
2. `nsa_indexer.forward_indexer()` 的非 CUDA/HIP 路径会直接从 `req_to_token[:, ::page_size]` 构造 `block_tables`,绕过 metadata 边界 mask。当前 Hopper CUDA prefill 不走这条路径,归类为维护风险。
3. CUDA graph replay / MTP precompute 路径仍存在 direct `req_to_token` page-table 构造。用户确认 prefill 无 cuda graph所以不是当前 active root cause后续若要覆盖 decode/draft graph应单独补 mask 和测试。
4. MHA FP8 的 `page_table_1_flattened` 是按每行 `indexer_seq_lens_cpu``page_table[i, :kv_len]` 的 valid slice concat不读矩形 tail当前看不是同类 stale-tail 风险。
5. prefill disaggregation transfer 当前读 `req_to_token[req_pool_idx, start:end]` / `[:seq_len]` 这类 valid token slice这类路径本身不消费矩形 tail。仍需保留已有 page-count fail-fast但它不是本轮 cache-hit 精度掉点的主嫌。
后续判据:
- 如果最新 ETE 二次 GSM8K 精度恢复,说明主要 root cause 是 `real_page_table` slot-remap tail。
- 如果仍掉点,优先打开/补强 topk per-request membership validator检查 `topk_indices` 是否引用了每个 request valid range 外或错误 request 的 loc不要先做大范围 fallback/禁用。
### 2026-06-07 二次 GSM8K 仍掉点:风险从“所有 cache-hit”收敛到 non-page-aligned CP HiCache 命中长度/内容
新远端证据来自 `/mnt/beegfs/cjy/log/sglang_cp_hicache_20260606_193039.log`
- 用户同进程连续两轮 GSM8K第一轮 `Accuracy=0.953`,第二轮 `Accuracy=0.692`,吞吐基本相同。
- 第一轮并不是完全无 cache`19:35:08` 起已经出现大量 `#new-seq: 5, #cached-token: 3200`,即约每请求 640 cached tokens第一轮仍正确。
- 第二轮主要变成 `#new-token: 320/384, #cached-token: 3520/3584/3648...`,即命中更长、更贴近每个 GSM8K 问题自身的缓存。
- 第一轮开始处 CP HiCache 写入了 `node_id=4 logical_len=698`;这是 non-page-aligned node物理 page 覆盖会到 704 tokens。后续第二轮的典型 `cached-token=3520` 正好对应 5 个请求各 704 tokens。
当前推断:
- “cache-hit 本身全部坏”不是最强解释,因为第一轮已有 640-token 共享前缀命中且精度正常。
- 更强嫌疑是:某个 CP HiCache/radix/load-back/scheduler-visible 前缀长度路径,把 `logical_len=698` 的 node 以物理 `padded_len=704` 暴露给 scheduler/attention或者 load-back/backup 的最后 partial page 内容/索引与 logical valid token 不一致。
- 这会让第二轮复用 per-question cache 时跳过 6 个应计算 token或消费 page padding/stale KV冷跑和只命中 640 共享前缀时不明显,复跑命中 question-specific non-page-aligned node 时掉点。
下一步审计目标:
1. `hiradix_cache.match_prefix()` / `_match_prefix_helper()` / `load_back()`:确认返回给 scheduler 的 `prefix_indices` 长度来自 logical valid length而不是 `padded_len`
2. `schedule_batch.py`:确认 `cache_protected_len``extend_input_len`、日志里的 `#cached-token` 是否可能为满足 page 对齐被向上取整。
3. CP HiCache backup/load path确认 host/device node 的 `value` 只包含 logical indices物理 page reservation/owned positions 不应改变 node visible length。
4. 若代码确认没有 length exposure再回到 top-k membership validator但当前日志优先级应先查 698→704 这条链。
### 2026-06-07 复测后收敛:第二轮掉点不是所有 cache-hit 坏,而是 first-run suffix page 持久化复用可疑
最新证据来自 `/mnt/beegfs/cjy/log/sglang_cp_hicache_20260606_193039.log`,对应用户同进程两轮 GSM8K第一轮 `Accuracy=0.953`,第二轮 `Accuracy=0.692`
日志事实:
- 该日志内没有 `CP_SHARED_KV_FAIL_FAST``CP_SHARED_KV_FALLBACK``RuntimeError``Traceback``Health check failed``HiCache-load``CacheCtrl-load``load_back`
- 第一轮已经有稳定 cache-hitCP0 典型 `#new-seq: 5, #new-token: 640/704/576, #cached-token: 3200`,约等于每请求 640 cached tokens第一轮仍正确。
- 第二轮主要变成 `#new-seq: 5, #new-token: 320/384, #cached-token: 3520/3584/...`,即每请求多复用了约 64 tokens 的 question-specific suffix page。
- 第一轮 CP0 写入大量 non-page-aligned suffix node例如 `logical_len=95/81/140/66/86`,且 `owned_positions=64`bs=5 时 CP0 常见 `valid_local_tokens=320`,即每个请求写入首个 owner page。
- 第二轮 CP0 `local_out_cache_loc` 多为小 suffix`split_tokens=165 out_cache_tokens=168 local_tokens=165``split_tokens=63 out_cache_tokens=64 local_tokens=63`,说明当前 extend/write 仍按 valid rows 截掉 physical padding。
修正当前 root-cause 优先级:
- 仅用“radix/scheduler 把 698 向上暴露成 704”解释不充分。代码中 CP exact hit 仍有 page-floor 方向,且日志也能解释为第二轮命中共享 640 page 后,再命中第一轮写入的每题 suffix 首个 64-token page。
- 因此当前更强嫌疑是 bs>1 下持久化 direct write 的 row-order/physical-loc contract第一轮正常使用 current K/V 参与计算并得到正确答案,但同一轮写入 device/HiCache 的 MLA KV 或 NSA index K 在第二轮作为 prefix 被复用时内容或行序错误。
- 需要优先审计 `get_cp_shared_kv_local_out_cache_loc()``select_cp_local_valid_rows_for_cache_write()`、MLA direct store、index direct store 之间的行序一致性,以及 prefix materialize 读取这些 page 时是否按相同 owner-lane order 解释。
下一步最小验证:
1. 构造/补强单测覆盖 bs=5、page_size=64、prefix=640、extend=[95,81,140,66,86] 这类 GSM8K 形态,验证 local valid row selector 输出顺序与 local physical out loc 顺序逐 request/page 对齐。
2. 若单测无法暴露,加入 env-gated、限频的 runtime validator在 first-run suffix direct write 后读取刚写入的 index/MLA buffer与 current local rows 做 checksum/row-id 对比。优先验证 index K因为它有较清晰的 `local_key -> physical_out_loc` 写入路径MLA FP8 packed store 需要额外确认量化/布局。
3. 不再通过大范围禁用 current/partial reuse 判断根因;否则会掩盖“第一轮 direct write 与第二轮 persistent reuse 不等价”的核心问题。

View File

@@ -0,0 +1,401 @@
# CP shared-KV bs>1 GSM8K cache-hit 掉点临时排查记录
> 目的:上下文压缩后先读本文件,避免重复扫描远端大日志与重复审计同一批路径。
## 当前复现
- 远端日志:`g0034:/mnt/beegfs/cjy/log/sglang_cp_hicache_20260606_193039.log`
- 用户连续两次全量 GSM8K同一 prefill 进程:
- 第 1 次:`Accuracy=0.953`, `Invalid=0.000`, `Latency=211.445s`, `Output throughput=654.008 token/s`
- 第 2 次:`Accuracy=0.692`, `Invalid=0.002`, `Latency=214.264s`, `Output throughput=654.376 token/s`
## 日志已确认事实
- 无显式错误:`CP_SHARED_KV_FAIL_FAST=0`, `CP_SHARED_KV_FALLBACK=0`, `RuntimeError=0`, `Traceback=0`, `Health check failed=0`
- 无明显 L2 load-back`HiCache-load=0`, `CacheCtrl-load=0`, `load_back=0`
- 第一轮并非全冷CP0 典型 batch 已有 `#new-seq=5, #new-token=640/704/576, #cached-token=3200`,约每请求 640 cached tokens仍然精度正常。
- 第二轮典型变为 `#new-seq=5, #new-token=320/384, #cached-token=3520/3584/...`,约每请求比第一轮多复用 64 tokens 的 question-specific suffix page。
- 第一轮 CP0 写入大量 non-page-aligned suffix node`logical_len=95/81/140/66/86`, `owned_positions=64`。bs=5 时 CP0 常见 `valid_local_tokens=320`,即每请求首个 owner page。
- 第二轮 CP0 `local_out_cache_loc` 仍按 valid rows 计算,例如 `split_tokens=63 out_cache_tokens=64 local_tokens=63 valid_local_tokens=63`,说明当前 write 侧没有直接把 padding token 当 valid row 写。
## 已排除 / 降低优先级
- 不是所有 cache-hit 都坏:第一轮已有共享 640-token cache-hit精度仍正常。
- 不是明显服务错误或 fallback 慢路径:日志无 fail-fast/fallback/crash。
- 不是当前 prefill CUDA graph replay用户确认 prefill 不走 cuda graph。
- 单纯 “radix 把 698 向上暴露成 704” 不是最强解释:代码仍有 CP exact hit page-floor 方向,日志也可解释为第二轮额外命中了第一轮写入的每题 suffix 首 page。
## 当前主嫌
bs>1 下 first-run suffix page 的持久化 direct write 与 second-run prefix reuse 不等价:
1. 第一轮当前请求使用 current K/V 或 current index 参与计算,答案正确。
2. 同一轮把 suffix 首 page 写入 device/HiCache 持久 cache。
3. 第二轮把这个 page 当 prefix 复用后精度掉点。
因此优先审计:
- `get_cp_shared_kv_local_out_cache_loc()`
- `select_cp_local_valid_rows_for_cache_write()`
- MLA direct store (`_maybe_write_cp_shared_local_mla_kv`)
- NSA index direct store (`_store_cp_shared_local_index_k_cache`)
- prefix materialize/slot-remap 读取持久 page 时是否按相同 owner-lane order 解释。
## 下一步验证方向
1. 先补/跑 CPU 单测bs=5, page_size=64, prefix=640, extend=[95,81,140,66,86],验证 local valid row selector 输出顺序与 local physical out loc 顺序逐 request/page 对齐。
2. 如果单测没暴露,加入 env-gated 限频 runtime validatorfirst-run suffix direct write 后读回刚写的 index/MLA buffer与 current local rows 做 checksum/row-id 对比。
3. 不先大范围禁用 current/partial reuse这会掩盖 direct write 与 persistent reuse 不等价的问题。
## 2026-06-07 增量发现CPU 侧 bs=5 row/loc 顺序探针
远端容器已跑过一个不依赖 CUDA 的顺序探针:
- 脚本:`/sgl-workspace/sglang-tai/tmp_cp_bs5_order_probe.py`
- 参数:`page_size=64`, `cp_size=8`, `prefix=[640]*5`, `extend=[95,81,140,66,86]`
- 结论Python planner / valid-row selector / local out loc 在该 GSM8K-like 形状下顺序一致。
输出摘要:
```text
rank0: compute=320 valid_rows=320 locs=320 per_req=[64,64,64,64,64]
rank1: compute=320 valid_rows=136 locs=136 per_req=[31,17,64,2,22]
rank2: compute=320 valid_rows=12 locs=12 per_req=[0,0,12,0,0]
rank3-7: valid_rows=0 locs=0
```
含义:
- rank0 写每个 request 的 suffix 首 page符合第二轮多命中约 64 tokens/request 的现象。
- rank1/rank2 写剩余 valid taildummy compute-padding rows 没进入 persistent write rows。
- 这降低了 “Python split/selector 把 valid rows 和 out loc 排错序” 的优先级。
- 不能排除TAI fused store、index fused store、或 persistent prefix materialize 对同一批 loc 的解释不一致。
若需复跑:
```bash
ssh g0034 "docker exec sglang-glm5-dev-2 bash -lc 'cd /sgl-workspace/sglang-tai && PYTHONPATH=python python /sgl-workspace/sglang-tai/tmp_cp_bs5_order_probe.py'"
```
## 2026-06-07 增量发现direct write 代码路径事实
MLA direct write
- 文件:`python/sglang/srt/models/deepseek_common/attention_forward_methods/forward_mla.py`
- 函数:`_maybe_write_cp_shared_local_mla_kv`
- 流程:
1. `get_cp_shared_kv_local_out_cache_loc(forward_batch)` 取得 local logical locs。
2. `select_cp_local_valid_rows_for_cache_write(...)` 从 compute-padded local rows 中只取 valid rows。
3. 优先走 `try_tai_fused_mla_store(...)`,把 logical locs 交给 TAI fused store。
4. fallback 才通过 `get_cp_shared_kv_local_physical_out_cache_loc()` 转 physical loc 后调用 pool setter。
Index direct write
- 文件:`python/sglang/srt/layers/attention/nsa/nsa_indexer.py`
- 函数:`_store_cp_shared_local_index_k_cache`
- 流程:
1. 取 local logical locs。
2. 用同一个 valid-row selector 取 `local_key`
3. 转 physical locs。
4. `_store_index_k_cache(... out_loc_override=physical_out_loc)`
当前判断:
- Python 侧 valid row / local loc 长度与顺序在探针形状下是自洽的。
- MLA TAI fused store 比 index store 更可疑:它接收 logical locs 并在 kernel 内自行解释 CP shared layoutindex store 当前传的是 physical locs。
- FP8 packed MLA buffer shape 需要重点确认。远端曾出现过 `kv_cache_dim=656` 相关问题TAI fused MLA store 必须按真实 packed layout 写入,而不能按旧 bf16/compact 假设写。
## 2026-06-07 增量发现:日志噪音与非根因
最新日志里的 `Invalid` 计数主要来自环境变量拼接错误:
```text
Invalid value for SGLANG_CP_SHARED_KV_BS_GT1_TIMING_LIMIT:
"-1SGLANG_CP_SHARED_KV_BS_GT1_DEBUG=1" is not a valid integer value, using default "256"
```
该问题会影响 timing limit/debug 配置,但不是 GSM8K 第二轮掉点的直接根因:同一进程第一轮准确率正常,第二轮 cache 命中更高后才掉点。
## 当前最小下一步
优先做一个 CUDA/remote 级验证,不继续靠日志猜:
1.`tai-kernel``fused_store_mla_kv` 补 GSM8K-like bs=5 非连续 logical loc + FP8 packed shape 的单测。
2. 对比:
- TAI fused MLA store 写入 raw layer buffer
- reference path`CpSharedKVLayout.logical_locs_to_physical()` + 现有 pool setter / torch reference。
3. 如果 MLA fused store 通过,再对 index fused store 做同形状验证。
该验证能直接回答:第一轮写入的 suffix page 是否已经在 persistent KV cache 中损坏。
## 2026-06-07 新假设:第二轮掉点可能来自 L2->L1 load而不是写入
用户指出一个关键区分:
- 第一轮的 cache-hit 可能主要来自同 prefix 的 radix/device cache仍然正确
- 第二轮多命中的 question-specific suffix page 可能来自第一轮 backup 到 L2/HiCache 后再 load 回 L1
- 如果 L2 host layout、page_first_direct load kernel、或 load-back page-slot remap 错,现象就是第二轮掉点且不一定有显式 fallback/error。
这解释了为什么日志里“第一轮已有 cached-token 但正确”不能完全排除 cache 路径问题:第一轮命中的 cache 类型可能与第二轮额外命中的 cache 类型不同。
需要把验证拆成两类:
1. **写入验证**direct write 后立刻从 L1 persistent buffer 读回,确认 first-run suffix page 写入无损。
2. **L2 roundtrip 验证**L1 -> L2 backup -> 释放/覆盖 L1 -> L2 -> L1 load 后再读回,确认 host page_first_direct/direct backend 的 layout 与 L1 一致。
当前日志曾统计 `HiCache-load=0/CacheCtrl-load=0/load_back=0`,但这只能说明没有这些字符串;不能证明没有发生 HiCache L2 load。下一步需要按实际代码路径找 load/restore 日志点或补 env-gated validator。
### L2 roundtrip 单测结果
已新增并在远端容器验证:
```bash
PYTHONPATH=python python -m pytest -q \
test/registered/unit/mem_cache/test_nsa_pool_host_unit.py::TestNSAHiCacheTransfer::test_fp8_page_first_direct_roundtrip_preserves_kv_and_indexer_pages -s
```
结果:
```text
1 passed, 3 warnings in 10.05s
```
该测试覆盖:
- `NSATokenToKVPool(dtype=torch.float8_e4m3fn, kv_cache_dim=656)`
- `NSATokenToKVPoolHost(layout=page_first_direct, io_backend=direct)`
- 多层 KV bytesL1 -> L2 backup -> L1 清零 -> L2 -> L1 load
- NSA indexer bytes 同步 roundtrip
- 非连续 page 映射:`src_pages=[1,2,5,8]`, `host_pages=[3,4,7,12]`, `dst_pages=[10,11,13,15]`
含义:
- 简单的 TAI `page_first_direct/direct` byte-copy kernel 不是当前最高优先级根因。
- 仍未排除控制路径错误HiCache metadata 中 host_indices/device_indices 选择、node split/page-floor 后 stale tail、或 prefix materialize 对 loaded pages 的解释仍可能出错。
## 2026-06-07 C: bs=5 valid-row FP8 L2 roundtrip test
新增并远端验证:
- `test/registered/unit/mem_cache/test_nsa_pool_host_unit.py::TestNSAHiCacheTransfer::test_fp8_page_first_direct_bs5_valid_rows_survive_l2_roundtrip`
- 形状:`cp_size=8,page_size=64,bs=5,prefix=[640]*5,extend=[95,81,140,66,86]`
- 覆盖compute padding、只写 valid rows、FP8 packed MLA KV、NSA indexer page row、`page_first_direct/direct`、L1→L2 backup、L2→L1 load。
- 远端命令:
`PYTHONPATH=python python -m pytest -q test/registered/unit/mem_cache/test_nsa_pool_host_unit.py::TestNSAHiCacheTransfer::test_fp8_page_first_direct_bs5_valid_rows_survive_l2_roundtrip -s`
- 结果:`1 passed, 3 warnings in 9.97s`
结论:
- 真实形状下valid-row direct-write 之后再做 padded owned-row L2 backup/loadKV 与 index valid rows 均可逐字节保持。
- 因此“TAI direct L2 raw copy 坏”以及“bs=5 valid-row/padded backup 基本顺序坏”目前不是最高优先级。
- 剩余更可疑方向:
1. 线上第二轮 GSM8K 是否真的触发了 `load_back`;此前日志没有 `[HiCache-load]`,需要用最新日志再确认。
2. radix/HiCache node metadata 在真实 split/page-floor/evict 后的 `visible_device_indices` 是否与 page table/current-reuse consume 侧一致。
3. cache-hit consume 侧page table、current/partial reuse、index topk offset、MLA materialize是否在第二轮复用时使用了错误 page/order而不是 L2 copy 本身损坏。
## 2026-06-07 D: latest log still does not show L2 load_back
复查 `/mnt/beegfs/cjy/log/sglang_cp_hicache_20260606_193039.log`
- `[HiCache-load]`: 0
- `load_back CP`: 0
- `load_cp`: 0
- `load_to_device`: 0
- `CP_SHARED_KV_FAIL_FAST`: 0
- `CP_SHARED_KV_FALLBACK`: 0
- `Prefill batch`: 4384
- `cached-token`: 4384
- `partial`: 2048
- `index_topk`: 5007
代码事实:`hiradix_cache.load_back()` 的 CP path 会打 `[HiCache-load] load_back CP...` info 日志。当前日志完全没有该前缀。
结论:这份 GSM8K 二轮掉点证据目前不支持“第二轮主要来自 L2 load-back 拷贝损坏”。更像是 device/radix cache-hit consume 侧,或 current/partial reuse/index/MLA materialize/page table 的复用语义问题。L2 仍不能完全排除,但优先级下降;如要继续验证,需要在新运行中显式制造 L1 evict 后再复测同一 prompts。
## 2026-06-07 E: actual TAI fused MLA store bs=5 test
新增并远端验证:
- `test/registered/unit/mem_cache/test_cp_shared_kv_runtime.py::TestCpSharedKVRuntimeHelpers::test_fp8_fused_mla_store_matches_fallback_for_bs5_compute_padding_rows`
- 覆盖:`bs=5,prefix=[640]*5,extend=[95,81,140,66,86]`compute padding 后通过 `get_cp_shared_kv_local_out_cache_loc()``select_cp_local_valid_rows_for_cache_write()` 得到真实 local valid rows再调用实际 `runtime.try_tai_fused_mla_store()` 写 FP8 MLA KV。
- 参考:`quantize_k_cache_separate()` + `set_mla_kv_buffer_triton()` fallback。
- 远端结果:`1 passed, 3 warnings in 8.76s`
结论:第一轮 prefill 的 TAI fused MLA direct-write 在该 bs=5/tiny/FP8 形状下与 fallback 字节一致。它不是当前最高优先级根因。
当前更可疑方向收敛为 cache-hit consume 侧:
1. page table / visible device indices 在 page-floored cache hit 下是否按 request 边界传递。
2. index topk 的 current/partial reuse 在 second-run cache-hit 下是否使用了错误 offset/layout。
3. MLA/index materialize shared cache 时是否用错 logical page order 或 dense output slot。
## 2026-06-07 F: CP=8 bs=5 index partial-current compose rank-merge 单测
新增并远端验证:
- `test/registered/unit/mem_cache/test_cp_shared_kv_runtime.py::TestCpSharedKVRuntimeHelpers::test_cp8_index_partial_current_compose_matches_rank_merged_reference_for_bs5`
- 形状:`cp_size=8,page_size=64,bs=5,prefix=[640]*5,extend=[95,81,140,66,86]`
- 前缀页按相同 logical page 在 5 个 request 中重复,模拟同 prefix cache hitsuffix/current 页按 owner lane 分配,模拟 CP shared KV direct-write/current-reuse。
- 测试方式:每个 CP rank 只 materialize 自己拥有的 pagepatch collective 为 identity然后在测试中手工把 8 个 rank 的输出相加,等价模拟 all-reduce 后的 dense page buffer。
- 远端结果:`1 passed`
结论:当前 CPU/reference index partial-current compose 在 CP=8、bs=5、重复 prefix + owner-lane current suffix 的情况下rank-merge 后等价完整 dense reference。该路径暂未暴露 index page materialize/order 错误。
## 2026-06-07 G: CP=8 KV partial-current remote-current visibility 单测
新增并远端验证:
- `test/registered/unit/mem_cache/test_cp_shared_kv_runtime.py::TestCpSharedKVRuntimeHelpers::test_cp8_kv_partial_current_keeps_remote_current_valid_locs_after_reduce`
- 目的:确认本 rank 在 current slot all-reduce 后不会把远端 owner 的 valid current loc 错误 mask 成 `-1`
- 远端结果:`1 passed`
结论:`build_current_page_mask()` 当前只 mask 本 rank `current_locs` 所在 page远端 owner current page 的 valid loc 会保留,符合 all-reduce 后所有 rank 可见 current page 的语义。仍需注意:远端 owner current page 的 tail slack 也不会由本 rank mask是否安全依赖 index/topk 侧正确使用 valid sequence length不把 tail slack 选入 `logical_locs`
## 2026-06-07 H: FP8/RAGGED MLA materialization domain mismatch suspect
代码审计发现一个比 L2 copy 更具体的风险:
- `get_topk_transform_method()` 在 FP8 + `flashmla_sparse` prefill 下返回 `RAGGED`
- RAGGED topk 的 `page_table_1/topk_indices` 是 ragged flattened request/KV 坐标,用于索引 `page_table_1_flattened`,不是 token_to_kv_pool 的 raw logical loc。
- 但当前 CP shared-KV MLA generic materialize/current-reuse block 在进入 `flashmla_sparse` attention 分支前,无条件把 `page_table_1``logical_locs` 传给 `materialize_prefix_and_reuse_current_kv_page_slots()` / `materialize_shared_token_kv_buffer()`
- 这在 cache-hit/prefix sharing 下会把“ragged 坐标”误当 “KV logical loc”非常符合第一轮部分 cache-hit 仍正常、第二轮更高 prefix/suffix cache-hit 后精度掉点。
下一步按 TDD 补 source/behavior guard 单测FP8 RAGGED 路径必须绕过 generic CP MLA materialize交给后面的 RAGGED `page_table_1_flattened` materialize/dequantize 路径处理;后续再做基于 flattened locs 的 partial-current 优化。
## 2026-06-07 I: RAGGED generic MLA materialize guard 已存在
继续审计后确认当前本地代码已经有 guard
```python
if (
forward_batch.uses_cp_shared_kv
and topk_transform_method == TopkTransformMethod.PAGED
):
```
因此 FP8/`flashmla_sparse` 的 RAGGED topk 不会进入 generic CP MLA materialize/current-reuse block避免了把 ragged flattened topk 坐标误当 raw KV logical loc。已补回归测试锁住这个 guard。
结论H 是合理风险,但在当前版本已被代码规避;它不是这次第二轮 GSM8K 掉点的剩余根因。继续排查应转向 RAGGED 分支内部的 `page_table_1_flattened` materialize/dequantize、radix visible loc、或 cache-hit 后的 req_to_token/page_table 内容。
## 2026-06-07 J: 用户提出的 L2 区分需要更高层单测
用户指出:第一轮的 cache-hit 可能主要是同 prefix/device radix cache第二轮额外 cache-hit 才可能来自 L2/HiCache load。因此仅有 raw L1↔L2 byte-copy roundtrip 还不够,需要补一个更贴近真实 consume 的测试:
1. first-run 形状写入 FP8 packed MLA KV + NSA index valid rows
2. 备份 full padded owned pages 到 page_first_direct/direct host
3. 清空/污染 L1 后从 host load 到新的 L1 pages
4. 用 CP shared-KV `materialize_shared_token_kv_buffer()` 按 RAGGED `page_table_1_flattened` 风格 logical locs 读取 loaded pages
5. 对比 materialize 后 dense loc 对应 bytes 与 first-run 写入 bytes。
目的直接验证“L2 load 本身正确,但 load 后被 RAGGED/MLA materialize 或 logical→physical remap 错读”的可能性。若该测试通过L2 load 作为 GSM8K 二轮掉点根因的优先级继续降低,后续应重点看真实运行中的 page_table/topk offset/current reuse 控制流。
### L2-loaded suffix + RAGGED materialize 单测结果
新增并在远端验证:
```bash
PYTHONPATH=python python -m pytest -q \
test/registered/unit/mem_cache/test_nsa_pool_host_unit.py::TestNSAHiCacheTransfer::test_fp8_l2_loaded_bs5_suffix_materializes_from_ragged_logical_locs -s
```
结果:`1 passed, 3 warnings in 9.82s`
覆盖内容:
- GSM8K-like `bs=5,prefix=[640]*5,extend=[95,81,140,66,86]`
- CP=8 owner-lane suffix valid rows
- FP8 packed MLA KV (`kv_cache_dim=656`)
- `page_first_direct/direct` L1→L2 backup再 L2→L1 load 到新的 logical pages
- 之后按 RAGGED `page_table_1_flattened` 风格的 token logical locs 调 `materialize_shared_token_kv_buffer()`
- 手工合并 8 个 rank 的 local materialize 输出,验证 dense loc 对应 bytes 与 first-run 写入 bytes 一致。
结论在该单测覆盖的控制面里L2-loaded suffix page 被 RAGGED MLA materialize 读取是逐字节正确的。当前 GSM8K 二轮掉点更不像 raw L2 copy 或 basic RAGGED logical-loc materialize 错;下一步应看真实运行中的 `page_table_1_flattened/topk_indices_offset` 是否在 bs>1/cache-hit 后按 request 正确构造,以及 decode transfer/FP8 dtype 协议是否一致。
## 2026-06-07 K: RAGGED fused topk offset 语义验证
新增并在远端验证:
```bash
PYTHONPATH=python python -m pytest -q \
test/registered/unit/layers/test_nsa_topk_transform.py::TestNSATopkTransform::test_ragged_topk_transform_offsets_are_request_relative_with_row_starts -s
```
结果:`1 passed, 5 warnings in 9.77s`
验证点:`fast_topk_transform_ragged_fused` 在传入 `row_starts` 时,会把选中的 compact column 转成:
```text
request_offset + (selected_compact_col - row_start)
```
而不是:
```text
request_offset + selected_compact_col
```
结论:当前 `_get_topk_ragged_with_cp()` 给 batch compact path 传 `topk_indices_offset_override=request_kv_base` 的方向是对的;不需要改成 `request_kv_base - k_base`。这降低了“RAGGED topk offset 基本语义错误”作为二轮 GSM8K 掉点根因的优先级。
## 2026-06-07 L: TAI batched index MQA prepare GSM8K-like 形状验证
新增并在远端验证:
```bash
PYTHONPATH=python python -m pytest -q \
test/registered/unit/mem_cache/test_cp_shared_kv_runtime.py::TestCpSharedKVRuntimeHelpers::test_tai_batched_index_mqa_prepare_matches_getk_gets_reference_gsm8k_bs5 -s
```
结果:`1 passed, 3 warnings in 10.00s`
覆盖内容:
- `bs=5`, 每请求 `seq_len=704`, `q_start=640`, `q_len=64`
- `page_size=64`, `index_head_dim=128`
- 实际调用 `try_tai_prepare_cp_mqa_index_batch()`
- 与 reference 路径逐项对比:`index_buf_accessor.GetK/GetS``ks``ke_offset`
结论:至少在 GSM8K-like cache-hit/tiny-extend 形状下TAI batched index prepare kernel 的 K/S compact 与 range descriptor 与 Python reference 一致。当前二轮掉点不应优先归因于该 kernel 的基本 gather/descriptor 错误。
## 2026-06-07 M: L2 与 persistent index cache 需要区分验证
用户指出第一轮 GSM8K 里的 cache hit 可能主要来自相同 prefix 的 L1/device radix cache而第二轮新增的更高命中率可能来自 L2/HiCache load因此不能只用第一轮正常来排除 L2/load 路径。
当前已覆盖并通过的单测降低了以下路径优先级raw page_first_direct L1↔L2 copy、L2-loaded FP8 MLA KV 经 RAGGED logical loc materialize、RAGGED topk offset、TAI batched index MQA prepare。下一步需要补更贴近第二轮 consume 的 persistent NSA index cache 写入/读取单测,尤其是 `fused_store_index_k_cache` 在 bs=5/tiny/FP8/physical loc direct-write 下是否与 fallback `act_quant + SetKAndS` 等价。
原因:第一轮 topk 可以使用 freshly computed `current_index_kv/local_key`,即使 persistent index cache 写坏也不一定立即影响;第二轮高 cache-hit 时会更依赖持久化 index cache这与“第一轮 0.95、第二轮 0.69”的症状吻合。
### M 追加验证结果persistent index fused store 基本路径通过
新增并在远端验证:
```bash
PYTHONPATH=python python -m pytest -q \
test/registered/unit/mem_cache/test_cp_shared_kv_runtime.py::TestCpSharedKVRuntimeHelpers::test_fp8_index_fused_store_persistent_pages_survive_bs5_materialize -s
```
结果:`1 passed, 3 warnings in 10.22s`
覆盖内容bs=5/tiny extend、CP=8、compute padding、`get_cp_shared_kv_local_out_cache_loc()``select_cp_local_valid_rows_for_cache_write()``logical_locs_to_physical()``fused_store_index_k_cache()``materialize_shared_paged_buffer()` rank merge、`GetK/GetS` consume。
结论:不经过 L2 的 persistent NSA index fused-store + paged materialize 基本路径未复现行顺序/page offset 错误。单测仍保留,因为它能锁住第一轮写 persistent index cache 的核心合同。
## 2026-06-07 N: L2-loaded persistent index cache 单测
新增并在远端验证:
```bash
PYTHONPATH=python python -m pytest -q \
test/registered/unit/mem_cache/test_nsa_pool_host_unit.py::TestNSAHiCacheTransfer::test_fp8_l2_loaded_index_pages_materialize_after_fused_store_bs5 -s
```
结果:`1 passed, 3 warnings in 10.33s`
覆盖内容:
- 第一轮形状用 `fused_store_index_k_cache()` 写入 persistent index pages
- `page_first_direct/direct` 备份到 L2 host
- 污染目标 L1 pages 后从 L2 load 到新 L1 pages
- 对 loaded index pages 走 `materialize_shared_paged_buffer()` + rank merge
- 通过 `GetK/GetS` 按 request page table 读取并反量化,对比原始 valid key rows。
结论:在单测覆盖的 GSM8K-like bs=5/tiny/FP8 形状中persistent index cache 经 fused store → L2 backup/load → paged materialize → GetK/GetS 后仍可还原 valid rows。用户提出的“第二轮来自 L2 cache load 的错误”目前没有被 KV 或 index 的直接单测支持;下一步应把排查焦点上移到真实运行中的 radix/req_to_token/page_table 可见范围、chunked/full prompt cache-hit 后 valid length 与 page table 的组合、以及 prefill→decode transfer 元数据。