Plan bs>1 CP shared-KV L1 prefetch safely

Record the staged design for batch-aware L1 shared-KV prefetch under CP + HiCache. The plan separates L2->L1 load two-layer lookahead from L1 prefetch one-layer lookahead, keeps target first and draft/EAGLE out of the first phase, and calls out that current suffix reuse remains synchronous until the request-aware span contract is extended.

Constraint: Existing CP shared-KV correctness failures came from batch-global remap/req-id ambiguity, so bs>1 prefetch must carry explicit per-request spans and ids.

Rejected: Reuse the bs=1 scalar prefix_pages prefetcher for bs>1 | it aliases request rows and cannot represent mixed prefix/current spans.

Rejected: Make draft/EAGLE prefetch part of the first phase | target path must be validated first before coupling draft lifecycle.

Confidence: medium

Scope-risk: narrow

Directive: Do not open bs>1 L1 prefetch until req-id fail-fast and per-request slot span tests cover both MLA and index paths.

Tested: git diff --cached --check

Not-tested: Runtime implementation; this is a design document only.
This commit is contained in:
laoyao0822
2026-06-10 07:04:39 +08:00
parent f78c414b64
commit ffff715f00

View File

@@ -0,0 +1,736 @@
# NSA Prefill CP Shared-KV bs>1 L1 Prefetch 与 0SM 传输计划
> 日期2026-06-10
> 分支:`cjy-cp-refactor`
> 当前基线:`c8c736e75`
> 范围target model 的 bs>1 CP shared-KV L1 prefetchMLA KV 与 NSA index 两条路径;后续 0SM/低 SM TAI kernel 设计。
## 0. 目标和非目标
### 0.1 目标
1.`--enable-cp-shared-kv-prefill-bs-gt1` 下恢复 target model 的 L1 shared-KV prefetch。
2. 保持两级流水线合同:
```text
L2 -> L1 HiCache load: 提前 2 个 layer
L1 shared-KV prefetch: 提前 1 个 layer
当前 layer consume/wait: 只等自己需要的 layer-ready event
```
3. bs>1 下 prefetch 必须使用 request-aware slot spans不能继续使用单个 scalar `prefix_pages` 把整个 flattened page table 切成 prefix/suffix。
4. 第一版先支持 target model
- MLA KV prefix L1 prefetch
- NSA index prefix L1 prefetch
- current suffix 仍沿用当前同步 compose/reduce 路径。
5. 后续为 prefix prefetch 提供 0SM/低 SM transport primitive避免 prefetch materialize 与当前 layer attention/MLA/MOE 抢 SM。
### 0.2 非目标
1. 第一版不打开 EAGLE/draft prefetch。draft 只有少量 executable layer且 draft KV 生命周期应跟随 target mirror不能简单套 target 的 next-layer prefetch。
2. 第一版不改变 decode 路径。
3. 第一版不改变 HiCache storage layout也不强制切换到 layer-page-first。
4. 第一版不把 current suffix 的跨 rank compose 改成 0SM。current suffix 是当前 layer 刚产生的数据peer-read ordering 比历史 prefix 更严格,先保留已有同步语义。
5. 不新增 all-reduce/all-gather 来同步 “prefetch plan 是否一致”。plan 必须由本地 deterministic metadata 推导。
---
## 1. 两级流水线合同
我们期望的 target layer 流水线如下:
```text
warmup / batch prepare:
为 layer 0、layer 1 至少启动 L2->L1 load
处理 layer L-2:
启动 layer L 的 L2->L1 load
处理 layer L-1:
在 prefetch stream 上等待 layer L 的 L2->L1 load event
启动 layer L 的 L1 shared-KV prefetch
处理 layer L:
当前 layer index/MLA consume 时等待 layer L 的 L1 prefetch event
current suffix 仍在当前 layer 生成后同步 fill/reduce
attention 使用 prefix-prefetched + current-filled dense view
```
对应窗口:
| 时间点 | 允许做的工作 | 不允许做的工作 |
| --- | --- | --- |
| `layer L-2` | 启动 `L2->L1(layer L)` | 阻塞 forward stream 等 H2D |
| `layer L-1` attention 前 | 启动 `L1 prefetch(layer L)` | 与当前 layer 必要 KV reduce 争抢 collective 顺序 |
| `layer L` consume | 等待 `L1 prefetch(layer L)` event | 重新 materialize 已经 prefetched 的 prefix |
关键点:
1. L2->L1 load 比 L1 prefetch 早一个 layer避免 L1 prefetch 读到尚未从 host load back 的 device pages。
2. L1 prefetch 的启动点应保持在当前 layer attention 前,即当前实现中的 `_maybe_start_cp_shared_kv_attention_prefetch()` 语义。
3. L1 prefetch 不应该提前到 MQA/index materialize 前,否则会与当前 layer 的必要 materialize/reduce 重叠,造成之前观察到的性能回退。
4. L2->L1 load event 与 L1 prefetch event 是两个不同层次的 readiness
- L2->L1 eventdevice pool 中的真实 physical pages 已经可读;
- L1 prefetch eventdense prefix buffer 已经可被 consumer 使用。
---
## 2. 当前代码事实
### C1. L1 prefetch hook 已经在 attention 前
相关文件:
- `python/sglang/srt/layers/attention/nsa_backend.py`
- `_maybe_start_cp_shared_kv_attention_prefetch(forward_batch, layer_id)`
当前 hook 做的事情:
```text
next_layer_id = layer_id + 1
index_prefetcher.start_next_layer_prefix(next_layer_id)
mla_prefetcher.start_next_layer_prefix(next_layer_id)
```
结论L1 prefetch 的 “提前 1 layer” 启动位置基本符合目标bs>1 工作重点不是移动 hook而是让 prefetcher 的 plan 和 materialize 支持 batch。
### C2. MLA/index prefetcher 仍强制 batch_size == 1
相关文件:
- `python/sglang/srt/layers/attention/nsa/cp_shared_kv_prefetch.py`
- `CpSharedKVMlaPrefetcher.maybe_create()`
- `CpSharedKVIndexPrefetcher.maybe_create()`
当前行为:
```text
if int(forward_batch.batch_size) != 1:
create_skip / fallback reason=batch_size
return None
```
结论:不能直接删除 guard。pre-batch 代码内部仍假设 prefix 是 flattened slot 的 `[0:prefix_pages)`
### C3. 当前 prefetcher 使用 scalar prefix_pages
当前 prefetcher 字段:
```text
prefix_pages: int
slot_logical_pages: Tensor
page_inverse: Tensor
total_slots = slot_logical_pages.numel()
```
当前 consume 逻辑:
```text
prefix slots: [0, prefix_pages)
suffix slots: [prefix_pages, total_slots)
```
bs=1 时成立bs>1 时不成立。
bs>1 的 row-major page table 形态是:
```text
req0: [req0 prefix pages][req0 current pages][req0 padding/empty slots]
req1: [req1 prefix pages][req1 current pages][req1 padding/empty slots]
...
```
如果仍用 `[0, prefix_pages)`
1. 只能覆盖 req0 prefix
2. 会把 req0 current / padding gap / req1 prefix 混在同一个 suffix bounding span 里;
3. 可能重复 reduce 无效 slot
4. 更严重时会把不同 request 的 logical page 映射压到同一 dense view 中,复现之前 bs>1 cache-hit 精度下降类问题。
### C4. sync compose 已经有 bs>1 span helper可复用语义
相关文件:
- `python/sglang/srt/layers/attention/nsa/cp_shared_kv_runtime.py`
- `build_batch_prefix_slot_spans(...)`
- `build_batch_current_slot_spans(...)`
已有语义:
```text
build_batch_prefix_slot_spans:
返回每个 request prefix 在 flattened real_page_table 里的精确 slot spans
build_batch_current_slot_spans:
返回每个 request current suffix 在 flattened real_page_table 里的精确 slot spans
```
结论bs>1 prefetch 第一版应复用这两个 helper。不要重新发明 bounding span也不要引入 batch-global scalar prefix。
### C5. 当前 TAI IPC materialize 是 SM kernel不是 0SM
相关文件:
- SGLang:
- `python/sglang/srt/layers/attention/nsa/cp_shared_kv_runtime.py`
- `_try_tai_ipc_materialize_token_kv_page_slots_into(...)`
- `_try_tai_ipc_materialize_paged_buffer_page_slots_into(...)`
- tai-kernel:
- `python/tai_kernel/nsa_prefill/ipc.py`
- `materialize_cuda_ipc_peer_pages_slot_dense(...)`
- `python/tai_kernel/nsa_prefill/csrc/kvcacheio_lf_pf.cu`
- `ipc_materialize_peer_pages_slot_dense_kernel`
- `ipc_materialize_peer_pages_slot_dense_multi_warp_kernel`
当前优势:
1. 避免原来的 local materialize + all-reduce 冗余通信;
2. 可以直接 peer-read owner rank 的 L1 page
3. 已支持 fp8 packed page 与 bf16 page 形态。
当前不足:
1. kernel 使用 SM copy不是 Copy Engine
2. `start_slot != 0` 会 fallback
3. 输入是一个连续 slot range不是 spans list
4. 用于 sync compose不是 prefetcher 的 prefix pipeline。
结论0SM 计划不能把现有 IPC kernel 直接称为 0SM。它是可用的 SM baseline0SM 需要新增 CE/batched-copy primitive。
### C6. index active-layer skip 已经改变 index prefetch 的 layer 集合
index skip 后index K/scale 只在 active index layers 存储。L1 index prefetch 必须满足:
```text
如果 next_layer_id 不是 index active layer:
不启动 index prefetch
不访问 compacted index cache slot
如果 next_layer_id 是 index active layer:
使用 state_layer_id / active index layer id 映射后的 index buffer
```
MLA KV prefetch 仍然每个 target layer 都可能需要。
### C7. draft/EAGLE prefetch 当前应保持关闭
之前已经确认draft KV 生命周期应是 target KV 的镜像draft 不应该维护独立的 page ownership / HiCache state。draft prefetch 若直接套 target 的 `next_layer_id=layer+1`,容易出现:
1. draft 只有一层或层数很少,`next_layer` 语义不稳定;
2. target/draft layer id 空间不同;
3. draft consume 时机在 target forward 之后,和 target prefetch window 不同。
结论bs>1 L1 prefetch 第一版只打开 target。draft 后续作为单独阶段处理。
---
## 3. bs>1 L1 prefetch 语义设计
### 3.1 新的 prefetch plan 形态
建议在 `cp_shared_kv_prefetch.py` 内部引入一个小的 batch-aware plan dataclass或者直接把同等字段放入两个 prefetcher
```python
@dataclass(frozen=True)
class CpSharedKVBatchPrefetchPlan:
batch_size: int
page_size: int
prefix_slot_spans: tuple[tuple[int, int], ...]
current_slot_spans: tuple[tuple[int, int], ...]
total_slots: int
dense_num_pages: int
prefix_slot_count: int
current_slot_count: int
```
字段合同:
- `prefix_slot_spans`:只包含历史 cache-hit prefix pages。
- `current_slot_spans`:只包含本次 extend 产生的 current pages。
- `total_slots``slot_logical_pages.numel()`
- `dense_num_pages`dense buffer 的 page 数,沿用现有 remap helper 结果。
- `prefix_slot_count/current_slot_count`用于日志、阈值、benchmark不用于切分 tensor。
构造方式:
```text
logical_pages = metadata.real_page_table
prefix_slot_spans = build_batch_prefix_slot_spans(
logical_pages=logical_pages,
prefix_lens_cpu=forward_batch.extend_prefix_lens_cpu,
page_size=page_size,
)
current_slot_spans = build_batch_current_slot_spans(
logical_pages=logical_pages,
prefix_lens_cpu=forward_batch.extend_prefix_lens_cpu,
extend_lens_cpu=forward_batch.extend_seq_lens_cpu,
page_size=page_size,
)
```
### 3.2 prefetch create gate
替换当前 `batch_size != 1` guard 后create gate 应变成:
```text
if batch_size < 1:
skip/fail
if no prefix_slot_spans:
skip, reason=no_prefix_pages
if sum(prefix span pages) < min_prefix_pages:
skip, reason=prefix_below_min
if sum(extend_seq_lens_cpu) < min_async_extend_tokens:
skip, reason=extend_below_min
```
注意threshold 应基于 batch aggregate work而不是单个 request。bs>1 的目的就是把多个 200-2000 token extend 合起来打满 compute如果逐 request threshold会错误关闭有收益的 batch prefetch。
### 3.3 prefix materialize
bs>1 第一版可先使用现有 SM IPC / local materialize 语义,但必须接受 spans
```text
for (start_slot, end_slot) in prefix_slot_spans:
materialize slot range into dense buffer 对应位置
```
不能把 spans 合并成一个包含 current/gap 的 bounding span除非合并后仍能证明
1. 中间 gap 都是有效 prefix
2. 不包含 current suffix
3. 不包含 empty/padded page slots
4. dense remap 对 gap 的写入不会影响 attention 可见范围。
当前 row-major bs>1 下通常不能证明这些条件,因此默认使用 exact spans。
### 3.4 consume/current fill
consume 时:
```text
wait prefix_prefetch_event(layer L)
for (start_slot, end_slot) in current_slot_spans:
materialize current pages / fill current rows
reduce current rows if仍使用 collective path
remap logical locs to dense locs
return dense_kv_cache, dense_locs
```
当前 suffix 不应再用:
```text
start_slot = prefix_pages
end_slot = total_slots
```
这会在 bs>1 下把多个 request 的 prefix/current/gap 混在一起。
### 3.5 page_inverse 要保持 per-request 语义
之前 cache-hit 掉点的强 root cause 是 batch-global logical page inverse 把不同 request 的相同 logical page value 合并到同一个 dense page。prefetch 也必须遵守同一规则:
```text
page_inverse key = (req_id, logical_page)
不能只用 logical_page
```
现有 remap helper 已经有 per-row sorted/dense metadataprefetch 不应绕开它创建 batch-global inverse。
---
## 4. 0SM/低 SM transport 设计
### 4.1 为什么现有 IPC kernel 不够
现有 `materialize_cuda_ipc_peer_pages_slot_dense` 是 GPU SM copy kernel。它适合低延迟 SM copy baseline但作为 prefetch 长 prefix 时有两个问题:
1. 会和当前 layer attention / MQA / MOE 抢 SM
2. 如果 prefetch 过长,可能拖慢当前 layer抵消 overlap 收益。
目标 0SM primitive 应把 prefix page materialize 变成 Copy Engine 或 CPU gather + CE DMA 路径。
### 4.2 0SM 第一版 API 建议
在 tai-kernel 中新增独立 API保留现有 SM IPC kernel
```python
materialize_cuda_ipc_peer_pages_slot_dense_ce(
peer_ptrs,
dst,
owner_ranks,
src_page_indices,
dst_page_indices,
page_nbytes: int,
stream,
) -> None
```
语义:
```text
for i in [0, page_count):
src = peer_ptrs[owner_ranks[i]] + src_page_indices[i] * page_nbytes
dst = dst_base + dst_page_indices[i] * page_nbytes
copy page_nbytes bytes
```
实现路线:
1. C++/CUDA extension 中构造 CPU descriptor arrays。
2. 对连续 `(src, dst)` runs 做 merge减少 CE command 数。
3. 优先使用 `cudaMemcpyBatchAsync`CUDA >= 12.8)或多次 `cudaMemcpyPeerAsync` baseline。
4. 如果运行环境不支持 batch API显式 warning/fail-fast 到 SM IPC baseline不 silent fallback 到 all-reduce。
### 4.3 descriptor 必须避免 hidden D2H
0SM CE path 不能每层把 CUDA tensor `.cpu()` 成 descriptor。descriptor 来源应是:
```text
ForwardBatch CPU metadata
-> request prefix/current slot spans
-> owner rank + physical page index descriptors
-> cached CPU pinned descriptor arrays
```
要求:
1. `owner_ranks/src_page_indices/dst_page_indices` 尽量在 batch prepare/create 阶段构造;
2. layer 间只换 source base pointer不重建 page mapping
3. descriptor cache key 应包含:
- `slot_logical_pages` identity/version
- layout cp_rank/cp_size
- page size
- prefix/current spans
- dtype/page_nbytes。
### 4.4 readiness / ordering 风险
all-reduce 路径天然有跨 rank 同步语义0SM peer-read 路径没有 collective barrier。必须明确 ordering
```text
L2->L1 load finished on owner rank
-> owner rank device page visible to peer CE read
-> non-owner rank prefetch CE copy starts
```
可选实现:
1. 第一版保守L1 prefetch 只读本 rank owner pages跨 rank prefix 仍走现有 all-reduce/SM IPC path。
2. 更完整:为 L2->L1 load 暴露 per-layer CUDA IPC event 或 ready flagprefetch stream 在读 peer page 前等待对应 peer ready。
3. 依赖 lockstep layer schedule所有 CP ranks 在同一个 layer 同步推进L2->L1 提前 2 层足够覆盖。但这需要 ETE/Nsight 验证,不能作为唯一正确性保证。
推荐实现顺序:先接入 SM IPC spans baseline 验证 bs>1 correctness再做 0SM CE path0SM 首版只在能证明 ready 的 prefix pages 上启用。
---
## 5. 实施 phases
### P0. 文档与现状锁定
**文件:**
- 新增:`docs/advanced_features/nsa_prefill_cp_shared_kv_bs_gt1_l1_prefetch_zero_sm_plan_zh.md`
**验收:**
- 文档明确 L2->L1 `+2 layer`、L1 prefetch `+1 layer`
- 文档明确当前 prefetcher 的 bs=1 假设和风险。
- 文档明确 0SM 与现有 SM IPC kernel 的区别。
### P1. 补 bs>1 prefetch plan 单测
**文件:**
- 修改:`test/registered/unit/mem_cache/test_cp_shared_kv_runtime.py`
- 修改:`test/registered/unit/mem_cache/test_cp_shared_kv_prefetch.py`(如果不存在则新增同级测试文件)
**测试用例:**
1. `batch_size=2`
```text
page_size=64
req0 prefix=128, extend=65
req1 prefix=64, extend=129
```
期望:
```text
prefix_slot_spans = [(0, 2), (pages_per_req, pages_per_req + 1)]
current_slot_spans = [(2, 4), (pages_per_req + 1, pages_per_req + 4)]
```
2. prefix 为 0 的 request 不产生 prefix span但 current span 仍存在。
3. 不允许 `prefix_pages=sum(prefix_pages)` 后直接切 `[0:prefix_pages)`
4. page inverse 按 `(req_id, logical_page)` 区分两个 request 的相同 logical page value。
**远端命令:**
```bash
cd /sgl-workspace/sglang-tai
PYTHONPATH=python python -m pytest -q \
test/registered/unit/mem_cache/test_cp_shared_kv_runtime.py \
test/registered/unit/mem_cache/test_cp_shared_kv_prefetch.py
```
### P2. 改造 MLA prefetcher 支持 bs>1 spans
**文件:**
- 修改:`python/sglang/srt/layers/attention/nsa/cp_shared_kv_prefetch.py`
- 修改:`python/sglang/srt/layers/attention/nsa/cp_shared_kv_runtime.py`(仅在缺少 helper 时修改)
**实现点:**
1. `CpSharedKVMlaPrefetcher.maybe_create()` 不再因为 `batch_size != 1` 直接 skip。
2. create 阶段构造 `prefix_slot_spans/current_slot_spans`
3. `start_next_layer_prefix()` 只 materialize prefix spans。
4. `consume_prefix_with_current()` 只 fill/reduce current spans。
5. `consume()` 如果仍存在 legacy suffix pathbs>1 下 fail-fast避免错误 bounding suffix。
**第一版允许:**
- prefix 使用现有 local materialize + all-reduce 或 SM IPC baseline。
**第一版不允许:**
- silent fallback 到 `[prefix_pages:total_slots)`
- batch-global page inverse。
### P3. 改造 index prefetcher 支持 bs>1 spans
**文件:**
- 修改:`python/sglang/srt/layers/attention/nsa/cp_shared_kv_prefetch.py`
- 修改:`python/sglang/srt/layers/attention/nsa/nsa_indexer.py`(只在 consume 接口需要更明确的 active-layer 映射时修改)
**实现点:**
1. `CpSharedKVIndexPrefetcher.maybe_create()` 使用同一套 batch prefetch plan。
2. index prefetch 只对 active index layer 启动。
3. index `dense_page_buffer` 的 remap 和 current spans 必须 request-aware。
4. index skip layer 不创建 handle不产生 warning。
**风险控制:**
- 如果 active index layer 映射缺失fail-fast
```text
[CP_SHARED_KV_FAIL_FAST][index_prefetch] reason=missing_active_state_layer
```
### P4. 接入 L2->L1 +2 layer readiness
**文件:**
- 修改HiCache load/prefetch 管理相关文件,优先从已有文档定位:
- `docs/advanced_features/nsa_prefill_cp_hicache_load_prefetch_overlap_notes.md`
- `docs/advanced_features/nsa_prefill_cp_hicache_async_state_sync.md`
- 预计涉及:
- `python/sglang/srt/mem_cache/hiradix_cache.py`
- `python/sglang/srt/mem_cache/hicache_storage.py`
- `python/sglang/srt/layers/attention/nsa_backend.py`
**实现点:**
1. 暴露 `ensure_hicache_load_started(layer_id)` 或等价机制。
2. 在处理 layer `L-2` 时启动 layer `L` 的 L2->L1 load。
3. L1 prefetch stream 在处理 layer `L-1` 时等待 layer `L` 的 L2->L1 load event。
4. 如果当前实现已经一次性提交 all-layer load应记录为 `load_window=all_layers`,并先只接 wait event不额外提交重复 load。
**不能做:**
- forward stream 上同步等待 L2->L1。
- 用新的 all-reduce 确认所有 rank load 完成。
### P5. TAI SM IPC spans baseline
**文件:**
- SGLang
- `python/sglang/srt/layers/attention/nsa/cp_shared_kv_runtime.py`
- tai-kernel
- `python/tai_kernel/nsa_prefill/ipc.py`
- `python/tai_kernel/nsa_prefill/csrc/kvcacheio_ops.cc`
- `python/tai_kernel/nsa_prefill/csrc/kvcacheio_lf_pf.cu`
- `tests/nsa_prefill/test_cuda_ipc_gather.py`
- `benchmark/nsa_prefill/benchmark_cp_shared_kv_ipc_gather.py`
**实现点:**
1. 支持 spans list而不是只支持 `start_slot == 0` 的连续 range。
2. 支持 `dst_page_indices`,使 prefix pages 可以写到 dense buffer 的正确 page 槽位。
3. 支持 bf16/fp8 同一接口,通过 `page_nbytes` 控制。
4. benchmark 覆盖:
- bs=1/2/5/10
- prefix pages 对应 4k/8k/16k/32k/64k/120k/192k tokens
- contiguous / fragmented / owner-lane / zigzag page pattern。
### P6. 0SM CE prototype
**文件:**
- tai-kernel 新增 CE/batched-copy op具体文件沿用 P5。
**实现点:**
1. C++ extension 内部构造并提交 CE copy。
2. 支持 run merge避免每 page 一个 API 调用。
3. 提供 `--mode sm_ipc|ce_batch|cuda_memcpy_batch` benchmark 对比。
4. 如果 CUDA runtime 不支持 `cudaMemcpyBatchAsync`,测试中显式标记 skip生产中 warning 后回 SM IPC不回 all-reduce。
**验收指标:**
1. Nsight 中 CE path 不出现长时间 copy kernel SM occupancy。
2. 对大 prefix当前 layer attention/MOE SM occupancy 不被 prefetch kernel 明显挤占。
3. 小 prefix 下 CE descriptor overhead 不应超过 SM IPC baseline否则按阈值选择 SM IPC。
### P7. ETE 验证与默认开关
**远端验证:**
1. GSM8K 两轮:
```bash
cd /sgl-workspace/sglang
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
```
验收:
```text
第一轮 accuracy 接近 0.955
第二轮 cache-hit accuracy 不下降到 0.68/0.69
Invalid 接近 0
```
2. replay 压测:
```bash
ssh g0033
source /mnt/beegfs/khr/bench/.venv/bin/activate
python3 /mnt/beegfs/khr/bench/replay_openai_request_log.py \
--request-log /mnt/beegfs/khr/bench/workloads/request-chain-mixed-1500-15min-2rps/requests.jsonl \
--base-url http://g0034:17100 \
--endpoint /v1/chat/completions \
--chain-prefill-scheduler \
--prefill-target-concurrency 30 \
--max-concurrency 60 \
--metric-percentiles 50,90,95,99 \
--timeout-sec 240 \
--save-result /mnt/beegfs/results/replay-request-chain-mixed-1500-k20-prefill.json
```
验收:
```text
无 detokenizer hang
无 cache-hit 后乱码/accept len 大幅掉到 1 的回归
无 CP shared-KV fallback 热路径刷屏
prefill 吞吐不低于当前 bs>1 sync baseline
```
---
## 6. 日志与 fail-fast 要求
### 6.1 必须 warning/fail-fast 的情况
1. bs>1 prefetcher 发现缺少 `prefix_slot_spans/current_slot_spans`
2. `prefix_slot_spans``real_page_table` row 数不匹配。
3. `page_inverse` 不是 request-aware。
4. index active layer 映射缺失。
5. L1 prefetch 试图读取尚未完成 L2->L1 load 的 layer。
6. 0SM CE path 不支持当前 dtype/page layout但配置要求强制 0SM。
### 6.2 不应 warning 刷屏的情况
1. prefix 为 0。
2. prefix/extend 低于阈值,按策略跳过 prefetch。
3. index skip layer 不需要 prefetch。
4. draft/EAGLE prefetch 第一版未启用。
这些只应进入 debug/timing 日志,不能在生产路径刷 warning。
---
## 7. 剩余风险
### R1. peer-read ordering 不是 collective ordering
如果 0SM/IPC prefix prefetch 直接读 peer L1 pages需要证明 owner rank 的 L2->L1 load 和当前 layer writes 已经完成。否则会读到旧 KV 或未初始化 KV。
第一版规避方式:
```text
只对历史 prefix pages 启用 prefetch
current suffix 仍由当前 layer 同步 compose/reduce
L2->L1 load 至少提前两个 layer
必要时 prefetch stream 等 local load event。
```
### R2. CE descriptor CPU overhead 可能抵消收益
如果每 layer 都构造 Python list / CPU descriptor / H2D metadata0SM copy 本身省下的 SM 可能被 CPU overhead 抵消。
要求:
```text
descriptor 在 ForwardBatch/create 阶段缓存;
layer 只更新 base pointer / stream event
benchmark 单独统计 descriptor build 与 submit latency。
```
### R3. fp8 page_nbytes 必须来自真实 tensor stride
fp8 KV cache 之前出现过 paged/ragged shape 不一致问题。prefetch/0SM 不能假设 `hidden_dim * dtype_size`,必须使用真实 page tensor stride 计算:
```text
page_nbytes = stride(page_dim) * element_size
```
### R4. bs>1 tiny extend 不能因为 prefetch skip 丢失 current/full reuse
prefetch 只是 overlap 优化。即使 prefix/extend 太短跳过 prefetch也必须保持
```text
current reuse / partial-current reuse / full current sync compose 正确可用
```
不能让 “不创建 prefetcher” 变成 “不走 reuse”。
### R5. index skip 下的 index prefetch 不应访问被压缩掉的 layer
P3-P6 已经压缩 L1/L2 index cache。prefetch 必须复用 active-layer 映射,否则会访问错误 index state layer 或越界。
---
## 8. 推荐落地顺序
最小可验证路径:
```text
P0 文档
P1 单测锁住 spans/page_inverse 语义
P2 MLA bs>1 prefetch spans baseline
P3 index bs>1 prefetch spans baseline
P4 接 L2->L1 +2 readiness
P5 TAI SM IPC spans baseline benchmark
P6 0SM CE prototype
P7 GSM8K/replay/Nsight 验证
```
不要先做 P6 再修 P2/P3。0SM kernel 只解决 transport 代价,不解决 bs>1 prefix/current slot 语义;如果语义仍是 scalar prefixkernel 越快只会越快地产生错误。