Reuse prepared HiCache load descriptors across CP prefill layers
CP shared-KV bs>1 cache-hit loads already merge request load ops, but the host pool still rebuilt layer-invariant mapping work from the same host/device indices. Introduce a PreparedLoadDescriptor lifecycle around begin/end load, wire MLA KV and NSA index H2D loads through tai-kernel prepared submit when available, and add timing hooks plus regression coverage for descriptor reuse and explicit fallback logging. Record the P4/P6b design and benchmark results in the advanced feature notes. Constraint: Radix residency and allocator decisions remain synchronous; only the data-transfer descriptor is prepared for per-layer async submit. Constraint: Production fast path must not silently fall back when tai prepared H2D support is missing. Rejected: Cross-batch descriptor reuse | descriptor lifetime and tensor ownership are only safe within one load operation. Rejected: Change L2->L1 scheduling to layer-ahead prefetch in this commit | that is a separate lifecycle change after descriptor reuse is stable. Confidence: medium Scope-risk: moderate Directive: Keep LayerDoneCounter per-layer readiness semantics; do not replace with all-layer waits. Tested: python -m py_compile python/sglang/srt/mem_cache/memory_pool_host.py python/sglang/srt/managers/cache_controller.py Tested: Remote g0034:cjy-glm5-new PYTHONPATH=python python -m pytest -q test/registered/unit/managers/test_hicache_controller_cp.py (88 passed) Tested: Remote tai-kernel prepared descriptor CUDA test (6 passed) and P4 benchmark full matrix (90 rows) Not-tested: ETE replay/GSM8K cache-hit correctness after this commit Not-tested: Layer-ahead L2->L1 prefetch scheduling Co-authored-by: OmX <omx@oh-my-codex.dev>
This commit is contained in:
@@ -0,0 +1,773 @@
|
||||
# NSA Prefill CP HiCache bs>1 L2->L1 Transfer Prefetch Descriptor 计划
|
||||
|
||||
> 日期:2026-06-10
|
||||
> 分支:`cjy-cp-refactor`
|
||||
> 范围:CP shared-KV + HiCache + bs>1 cache-hit 场景下,L2/host -> L1/device 数据搬运的 batch-aware per-layer prefetch 与 descriptor 复用。
|
||||
> 相关文档:
|
||||
> - `docs/advanced_features/nsa_prefill_cp_hicache_load_prefetch_overlap_notes.md`
|
||||
> - `docs/advanced_features/nsa_prefill_cp_shared_kv_bs_gt1_l1_prefetch_zero_sm_plan_zh.md`
|
||||
> - `docs/advanced_features/nsa_prefill_cp_hicache_layer_page_first_direct_plan.md`
|
||||
|
||||
## 0. 目标和非目标
|
||||
|
||||
### 0.1 目标
|
||||
|
||||
1. 在 `--enable-cp-shared-kv-prefill-bs-gt1` 下,让多个 cache-hit request 的 L2->L1 transfer 以 batch 级 descriptor 组织。
|
||||
2. 降低每层重复构造 descriptor / Python wrapper / extension submit 造成的 CPU overhead。
|
||||
3. 保留现有正确性合同:
|
||||
- radix / residency / allocator reservation 同步完成;
|
||||
- data transfer 异步执行;
|
||||
- target / draft KV 跟随同一个 logical cache node;
|
||||
- 每层消费前精确等待本层 transfer event。
|
||||
4. 保持当前 per-layer H2D overlap 能力,并为后续“提前两层 L2->L1 transfer prefetch”打基础。
|
||||
5. 支持当前生产参数:
|
||||
- `--hicache-io-backend direct`
|
||||
- `--hicache-mem-layout page_first_direct`
|
||||
- `--kv-cache-dtype fp8_e4m3`
|
||||
- CP owner-lane / zigzag / bs>1 batch plan。
|
||||
|
||||
### 0.2 非目标
|
||||
|
||||
1. 第一阶段不改 radix tree 语义,不让异步 transfer 决定 cache node 是否可见。
|
||||
2. 第一阶段不做跨 batch descriptor 复用。descriptor 只在当前 load op / 当前 batch 生命周期内复用。
|
||||
3. 第一阶段不改 decode KV transfer / Mooncake transfer。
|
||||
4. 第一阶段不强制切换到 `layer_page_first`。先让 `page_first_direct` 路径有 prepared descriptor,再评估 LPF 切换收益。
|
||||
5. 第一阶段不新增 collective。descriptor 一致性必须来自本地 deterministic metadata,不依赖 all-reduce/all-gather。
|
||||
|
||||
---
|
||||
|
||||
## 1. 当前代码事实
|
||||
|
||||
### C1. 当前已有 batch 内 op 粗粒度合并
|
||||
|
||||
`python/sglang/srt/managers/cache_controller.py`
|
||||
|
||||
```text
|
||||
CacheOperation.merge_ops(load_queue)
|
||||
host_indices = torch.cat([op.host_indices for op in ops])
|
||||
device_indices = torch.cat([op.device_indices for op in ops])
|
||||
```
|
||||
|
||||
结论:当前 bs>1 下多个 request 的 load-back op 已经会合并成一个 `CacheOperation`。
|
||||
但这个合并只是 tensor concat,不是 transfer descriptor compact。
|
||||
|
||||
### C2. 当前 per-layer transfer 每层都会重新进入 host pool API
|
||||
|
||||
`HiCacheController.start_loading()`:
|
||||
|
||||
```text
|
||||
begin_load_to_device_op(host_indices, device_indices, io_backend)
|
||||
for layer_id in range(layer_num):
|
||||
mem_pool_host.load_to_device_per_layer(
|
||||
mem_pool_device,
|
||||
host_indices,
|
||||
device_indices,
|
||||
layer_id,
|
||||
io_backend,
|
||||
)
|
||||
end_load_to_device_op()
|
||||
```
|
||||
|
||||
结论:indices 是同一份,但每层仍会重新调用 Python method / extension wrapper。
|
||||
direct backend 下,底层 TAI op 仍可能每层重新构造 H2D copy descriptor。
|
||||
|
||||
### C3. `begin_load_to_device_op()` 已经是 descriptor 预处理入口
|
||||
|
||||
`python/sglang/srt/mem_cache/memory_pool_host.py`
|
||||
|
||||
base host pool:
|
||||
|
||||
```python
|
||||
def begin_load_to_device_op(self, host_indices, device_indices, io_backend):
|
||||
"""Prepare layer-invariant metadata for one host->device load op."""
|
||||
```
|
||||
|
||||
NSA host pool 已经使用该入口预计算 indexer page indices:
|
||||
|
||||
```text
|
||||
NSATokenToKVPoolHost.begin_load_to_device_op()
|
||||
_active_load_indexer_page_indices = _get_indexer_page_indices(...)
|
||||
```
|
||||
|
||||
结论:代码结构已经允许“per load op prepare once, per layer reuse”。
|
||||
当前 MLA KV 主体还没有充分利用这个入口。
|
||||
|
||||
### C4. 当前 H2D readiness 是 per-layer event
|
||||
|
||||
`LayerDoneCounter` 为每个 producer 维护每层 event:
|
||||
|
||||
```text
|
||||
LayerLoadingEvent.complete(layer_id)
|
||||
LayerDoneCounter.wait_until_on_stream(layer_id, stream)
|
||||
```
|
||||
|
||||
KV pool 访问路径会等待:
|
||||
|
||||
```text
|
||||
get_key_buffer_for_prefetch(layer_id, stream)
|
||||
-> wait_layer_transfer_on_stream(layer_id, stream)
|
||||
```
|
||||
|
||||
结论:即使 descriptor 合并,仍必须保留 per-layer complete/wait。不能退回 all-layer wait。
|
||||
|
||||
### C5. 当前 L2->L1 load 启动点仍偏晚
|
||||
|
||||
当前时序:
|
||||
|
||||
```text
|
||||
PrefillAdder.add_one_req()
|
||||
-> init_load_back()
|
||||
-> load_cp() queue load op
|
||||
|
||||
ScheduleBatch 创建后
|
||||
-> ready_to_load_host_cache()
|
||||
-> start_loading()
|
||||
-> load_stream per-layer enqueue
|
||||
|
||||
forward consume
|
||||
-> wait layer event
|
||||
```
|
||||
|
||||
这已经是 async per-layer load,但不是完整意义上的“提前两层 prefetch”。
|
||||
第一层和早期层仍可能等待 H2D。
|
||||
|
||||
---
|
||||
|
||||
## 2. 问题定义
|
||||
|
||||
当前瓶颈不是简单的“load op 没合并”,而是合并层级不够:
|
||||
|
||||
```text
|
||||
已合并:
|
||||
多 req host_indices/device_indices concat 成一个 CacheOperation
|
||||
|
||||
未合并:
|
||||
page 连续区间未 compact
|
||||
每层 transfer descriptor 未复用
|
||||
每层 Python/extension submit 仍线性增长
|
||||
L2->L1 transfer 未提前到 layer-k+2 的 prefetch window
|
||||
```
|
||||
|
||||
在 cache hit + bs>1 场景下,请求通常形态是:
|
||||
|
||||
```text
|
||||
prefix 很大:L2/host hit
|
||||
extend 很短:200~2000 tokens
|
||||
batch size:2~10
|
||||
layer 数:约 78
|
||||
```
|
||||
|
||||
如果每层都重新构造 descriptor,那么 CPU overhead 约随:
|
||||
|
||||
```text
|
||||
O(layer_num * descriptor_build_cost)
|
||||
```
|
||||
|
||||
增长。这个 overhead 很难被 GPU compute overlap,因为它发生在 transfer submit/control path。
|
||||
|
||||
目标是把可复用部分改成:
|
||||
|
||||
```text
|
||||
O(descriptor_build_cost + layer_num * cheap_submit_cost)
|
||||
```
|
||||
|
||||
并为后续:
|
||||
|
||||
```text
|
||||
layer L-2 启动 L2->L1(layer L)
|
||||
layer L-1 启动 L1 shared-KV prefix prefetch(layer L)
|
||||
layer L consume 只 wait event
|
||||
```
|
||||
|
||||
提供基础。
|
||||
|
||||
---
|
||||
|
||||
## 3. 目标架构
|
||||
|
||||
### 3.1 同步控制面
|
||||
|
||||
保持现有同步控制面:
|
||||
|
||||
```text
|
||||
scheduler admission
|
||||
match_prefix()
|
||||
init_load_back()
|
||||
alloc_pages_with_owners(page_owners)
|
||||
update prefix_indices / extend_input_len
|
||||
enqueue CacheOperation
|
||||
```
|
||||
|
||||
这些操作仍在 scheduler 线程完成,因为它们决定:
|
||||
|
||||
- radix node 是否 device-resident;
|
||||
- fresh device page owner pattern;
|
||||
- request prefix/extend metadata;
|
||||
- batch plan 输入。
|
||||
|
||||
### 3.2 异步数据面
|
||||
|
||||
把当前 data plane 从:
|
||||
|
||||
```text
|
||||
for each layer:
|
||||
build/submit transfer using host_indices/device_indices
|
||||
```
|
||||
|
||||
演进为:
|
||||
|
||||
```text
|
||||
begin_load_to_device_op:
|
||||
build PreparedLoadDescriptor once
|
||||
|
||||
for each layer:
|
||||
submit layer transfer using PreparedLoadDescriptor + layer_id
|
||||
|
||||
end_load_to_device_op:
|
||||
release descriptor lifetime refs
|
||||
```
|
||||
|
||||
### 3.3 descriptor 生命周期
|
||||
|
||||
descriptor 生命周期与 `start_loading()` 的 producer id 绑定:
|
||||
|
||||
```text
|
||||
start_loading()
|
||||
producer_id = update_producer()
|
||||
descriptor = prepare(...)
|
||||
enqueue layer transfers
|
||||
record_stream / hold tensors
|
||||
ack_load_queue.append(HiCacheAck(...))
|
||||
```
|
||||
|
||||
descriptor 不跨 batch 复用。
|
||||
descriptor 必须持有所有异步 transfer 需要的 tensor / pinned metadata 引用,直到 load stream 使用完。
|
||||
|
||||
---
|
||||
|
||||
## 4. Prepared descriptor 设计
|
||||
|
||||
### 4.1 第一版 Python-side descriptor
|
||||
|
||||
先增加轻量 Python-side descriptor,降低重复 page index 准备和 wrapper 参数构造。
|
||||
|
||||
建议 dataclass:
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class PreparedLoadDescriptor:
|
||||
host_indices: torch.Tensor
|
||||
device_indices: torch.Tensor
|
||||
host_page_indices: torch.Tensor | None
|
||||
device_page_indices: torch.Tensor | None
|
||||
page_size: int
|
||||
io_backend: str
|
||||
layout: str
|
||||
num_tokens: int
|
||||
num_pages: int
|
||||
```
|
||||
|
||||
对 MLA KV:
|
||||
|
||||
- `host_indices/device_indices` 是 token/page slot 级映射;
|
||||
- `page_first_direct` / `layer_page_first` 传给 TAI direct op;
|
||||
- 第一版不改变底层 TAI API,只把 layer-invariant tensor 准备和参数绑定集中到 `begin_load_to_device_op()`。
|
||||
|
||||
对 NSA index:
|
||||
|
||||
- 复用现有 `_active_load_indexer_page_indices`;
|
||||
- 把它纳入同一个 descriptor 生命周期,避免 target KV 和 index KV 分别散落状态。
|
||||
|
||||
### 4.2 第二版 compact segment descriptor
|
||||
|
||||
在 Python-side descriptor 稳定后,增加 segment compact:
|
||||
|
||||
```python
|
||||
@dataclass
|
||||
class TransferSegment:
|
||||
host_start: int
|
||||
device_start: int
|
||||
length: int
|
||||
```
|
||||
|
||||
compact 规则:
|
||||
|
||||
```text
|
||||
如果 host_indices 和 device_indices 同时连续:
|
||||
合并为一个 segment
|
||||
否则:
|
||||
保持 page/token 级 entries
|
||||
```
|
||||
|
||||
收益:
|
||||
|
||||
- 减少 cudaMemcpyBatch / TAI descriptor entries;
|
||||
- 与 L1/L2 allocator 连续分配优化协同;
|
||||
- 对 `layer_page_first` 更有价值。
|
||||
|
||||
### 4.3 第三版 TAI prepared descriptor API
|
||||
|
||||
最终把 descriptor 下沉到 tai-kernel:
|
||||
|
||||
```python
|
||||
desc = tai_kernel.nsa_prefill.prepare_h2d_page_descriptor(
|
||||
host_indices,
|
||||
device_indices,
|
||||
page_size,
|
||||
layout,
|
||||
)
|
||||
|
||||
tai_kernel.nsa_prefill.submit_h2d_layer(
|
||||
desc,
|
||||
src_ptr,
|
||||
dst_ptr,
|
||||
layer_id,
|
||||
)
|
||||
```
|
||||
|
||||
TAI descriptor 内部可以选择:
|
||||
|
||||
- cudaMemcpyBatchAsync entries;
|
||||
- compacted ranges;
|
||||
- future 0SM / copy-engine queue;
|
||||
- future CUDA driver batch copy API fallback。
|
||||
|
||||
SGLang 侧只负责 descriptor 生命周期和 per-layer event。
|
||||
|
||||
---
|
||||
|
||||
## 5. Phase 计划
|
||||
|
||||
### P0. 记录现状和保护合同
|
||||
|
||||
**目标:** 明确当前行为,避免后续优化误改语义。
|
||||
|
||||
**工作:**
|
||||
|
||||
1. 在本文件记录:
|
||||
- current batch `CacheOperation.merge_ops` 已经做 concat;
|
||||
- `start_loading()` 每层调用 `load_to_device_per_layer()`;
|
||||
- `LayerDoneCounter` 是 per-layer wait 合同;
|
||||
- descriptor 只能跨 layer,不能跨 batch。
|
||||
2. 不改代码。
|
||||
|
||||
**验证:**
|
||||
|
||||
- 文档自检无 “TBD/TODO/后续补充” 占位。
|
||||
|
||||
### P1. 为 MLA KV 主体增加 Python-side prepared descriptor
|
||||
|
||||
**目标:** 使用已有 `begin_load_to_device_op()` / `end_load_to_device_op()` 入口,为 MLA KV 主体缓存 layer-invariant 状态。
|
||||
|
||||
**涉及文件:**
|
||||
|
||||
- `python/sglang/srt/mem_cache/memory_pool_host.py`
|
||||
|
||||
**设计:**
|
||||
|
||||
1. 在 host pool base / MLA host pool 上添加 `_active_load_descriptor` 字段。
|
||||
2. `begin_load_to_device_op()` 创建 descriptor。
|
||||
3. `load_to_device_per_layer()` 优先使用 descriptor。
|
||||
4. `end_load_to_device_op()` 清理 descriptor。
|
||||
5. 若 descriptor 缺失,保留当前路径作为 fail-fast 或 warning fallback;生产 fast path 不应静默 fallback。
|
||||
|
||||
**测试:**
|
||||
|
||||
- unit test 覆盖:
|
||||
- begin 后 descriptor 存在;
|
||||
- per-layer 调用复用 descriptor;
|
||||
- end 后 descriptor 清空;
|
||||
- missing descriptor 的 fallback/warning 行为明确。
|
||||
|
||||
### P2. 把 NSA index load descriptor 生命周期合并到统一结构
|
||||
|
||||
**目标:** 当前 index 已经有 `_active_load_indexer_page_indices`,但状态分散。把 index descriptor 纳入统一 prepared descriptor。
|
||||
|
||||
**涉及文件:**
|
||||
|
||||
- `python/sglang/srt/mem_cache/memory_pool_host.py`
|
||||
|
||||
**设计:**
|
||||
|
||||
1. descriptor 内包含:
|
||||
- MLA KV token/page mapping;
|
||||
- NSA index page mapping;
|
||||
- active index layer ids。
|
||||
2. `NSATokenToKVPoolHost.begin_load_to_device_op()` 调 base prepare 后补充 index fields。
|
||||
3. `_load_indexer_to_device_per_layer()` 从 descriptor 读取 prepared index page indices。
|
||||
|
||||
**测试:**
|
||||
|
||||
- active index layer 被正确 load;
|
||||
- inactive index layer 不访问 compact index cache;
|
||||
- `index_topk_freq > 1` 时 descriptor 不请求 inactive layers。
|
||||
|
||||
### P3. 增加 descriptor build/submit timing
|
||||
|
||||
**目标:** 用现有 timing env 验证 CPU overhead 是否下降,不新增长期 noisy log。
|
||||
|
||||
**涉及文件:**
|
||||
|
||||
- `python/sglang/srt/managers/cache_controller.py`
|
||||
- `python/sglang/srt/mem_cache/memory_pool_host.py`
|
||||
|
||||
**设计:**
|
||||
|
||||
复用:
|
||||
|
||||
```text
|
||||
SGLANG_CP_SHARED_KV_BS_GT1_TIMING
|
||||
SGLANG_CP_SHARED_KV_BS_GT1_TIMING_LIMIT
|
||||
SGLANG_CP_SHARED_KV_BS_GT1_TIMING_SLOW_MS
|
||||
```
|
||||
|
||||
记录阶段:
|
||||
|
||||
```text
|
||||
prepare_load_descriptor
|
||||
submit_h2d_layer_loop
|
||||
submit_h2d_layer_per_call_slow
|
||||
end_load_descriptor
|
||||
```
|
||||
|
||||
**测试:**
|
||||
|
||||
- env disabled 时不输出;
|
||||
- env enabled 且 slow threshold 命中时输出;
|
||||
- 不新增独立 debug env。
|
||||
|
||||
### P4. TAI microbenchmark:descriptor reuse baseline
|
||||
|
||||
**目标:** 独立验证 descriptor reuse 是否降低 CPU submit overhead。
|
||||
|
||||
**涉及仓库:**
|
||||
|
||||
- `tai-kernel`
|
||||
|
||||
**benchmark 范围:**
|
||||
|
||||
1. tokens/pages:
|
||||
- 4k
|
||||
- 16k
|
||||
- 64k
|
||||
- 120k
|
||||
- 192k
|
||||
2. batch shape:
|
||||
- bs=1 large prefix
|
||||
- bs=5 mixed prefix
|
||||
- bs=10 small extend / large prefix
|
||||
3. layout:
|
||||
- `page_first_direct`
|
||||
- `layer_page_first`
|
||||
4. page 分布:
|
||||
- contiguous
|
||||
- allocator-like owner-lane
|
||||
- random fragmented
|
||||
|
||||
**指标:**
|
||||
|
||||
```text
|
||||
descriptor build ms
|
||||
per-layer submit total ms
|
||||
effective H2D GB/s
|
||||
entries/pages/segments
|
||||
CPU time per layer
|
||||
```
|
||||
|
||||
### P5. Segment compact
|
||||
|
||||
**目标:** 在 descriptor 内合并连续 host/device ranges,减少 bottom-level transfer entries。
|
||||
|
||||
**涉及文件:**
|
||||
|
||||
- SGLang descriptor builder;
|
||||
- tai-kernel benchmark;
|
||||
- 后续 tai-kernel runtime API。
|
||||
|
||||
**设计:**
|
||||
|
||||
先在 CPU 上做线性 scan:
|
||||
|
||||
```text
|
||||
prev_host + 1 == cur_host
|
||||
prev_device + 1 == cur_device
|
||||
```
|
||||
|
||||
连续则合并。
|
||||
不做复杂 merge/sort;不改变 order;不引入额外 O(n log n)。
|
||||
|
||||
**测试:**
|
||||
|
||||
- contiguous pages 合并成少量 segments;
|
||||
- fragmented pages 不错误合并;
|
||||
- device/host 只有一侧连续时不合并;
|
||||
- segment 展开后与原 indices 完全一致。
|
||||
|
||||
### P6. TAI prepared descriptor API
|
||||
|
||||
**目标:** 将 descriptor 进一步下沉,减少 Python/C++ wrapper 每层重复工作。
|
||||
|
||||
**涉及仓库:**
|
||||
|
||||
- `tai-kernel`
|
||||
- `sglang-dev`
|
||||
|
||||
**设计:**
|
||||
|
||||
1. tai-kernel 提供 prepare / submit / destroy 三段式 API。
|
||||
2. SGLang `begin_load_to_device_op()` 调 prepare。
|
||||
3. `load_to_device_per_layer()` 调 submit。
|
||||
4. `end_load_to_device_op()` 调 destroy。
|
||||
5. 如果 tai-kernel 不支持当前 layout/dtype,warning + 明确 fallback 到旧 direct path。
|
||||
|
||||
**测试:**
|
||||
|
||||
- bf16 / fp8_e4m3;
|
||||
- `page_first_direct`;
|
||||
- `layer_page_first`;
|
||||
- target-only;
|
||||
- target+draft;
|
||||
- index active-layer skip。
|
||||
|
||||
### P7. 提前两层 L2->L1 transfer prefetch
|
||||
|
||||
**目标:** 在 descriptor reuse 稳定后,把 transfer 启动点从 “batch 创建后立刻提交所有层” 改成可控的 layer-ahead prefetch。
|
||||
|
||||
**设计方向:**
|
||||
|
||||
第一版不改变 scheduler admission,只改变 submit scheduling:
|
||||
|
||||
```text
|
||||
start_loading()
|
||||
prepare descriptor once
|
||||
enqueue/submit first N warmup layers
|
||||
|
||||
layer end hook 或 forward progress hook:
|
||||
submit layer_id + 2 transfer
|
||||
```
|
||||
|
||||
约束:
|
||||
|
||||
1. 不阻塞 forward stream 发起 transfer。
|
||||
2. 每层仍通过 `LayerDoneCounter.complete(layer)` 通知 readiness。
|
||||
3. 如果 layer-ahead submit 来不及,consume wait 仍保证 correctness。
|
||||
4. draft target 的 layer submit 顺序一致。
|
||||
|
||||
**风险:**
|
||||
|
||||
- 当前 `start_loading()` 一次性提交所有层,简单且正确;
|
||||
- 改成逐层提交需要一个可靠 hook,不应复用占 SM 的 shared-KV materialize hook;
|
||||
- 第一版可以先保留一次性提交,只做 descriptor reuse;提前两层作为独立阶段。
|
||||
|
||||
### P8. ETE 验证
|
||||
|
||||
**目标:** 证明性能和正确性没有回退。
|
||||
|
||||
**验证项:**
|
||||
|
||||
1. 单测:
|
||||
- `test_prefill_adder.py`
|
||||
- CP shared KV runtime/layout tests
|
||||
- 新增 descriptor tests
|
||||
2. 远端 py_compile。
|
||||
3. TAI benchmark。
|
||||
4. GSM8K:
|
||||
- 第一轮 cold/cache-miss;
|
||||
- 第二轮 cache-hit;
|
||||
- 精度不掉点。
|
||||
5. replay workload:
|
||||
- cache hit 下吞吐;
|
||||
- accept len;
|
||||
- prefill failed count;
|
||||
- timing log 中 descriptor build/submit 是否下降。
|
||||
|
||||
---
|
||||
|
||||
## 6. 关键不变量
|
||||
|
||||
1. `prefix_indices` 更新必须发生在 scheduler admission 阶段,不能等 transfer 完成后异步更新。
|
||||
2. `device_indices` 分配后,即使 layer 数据尚未 load 完,也可以作为 future-ready L1 slot 暴露给 batch metadata;消费前必须 wait。
|
||||
3. `LayerDoneCounter` 的 per-layer event 不能被 all-layer event 替代。
|
||||
4. descriptor 可跨 layer 复用,不可跨 batch 复用。
|
||||
5. descriptor 不能持有会在 load stream 完成前释放的临时 tensor。
|
||||
6. zero-owned rank 仍需要 logical no-op ack,保持 CP ranks 的 load ack 顺序一致。
|
||||
7. target/draft KV 如果共享 HiCache logical node,则 descriptor 生命周期必须覆盖两者;不能 target 成功而 draft 失败后继续可见。
|
||||
|
||||
---
|
||||
|
||||
## 7. 建议实施顺序
|
||||
|
||||
推荐先做:
|
||||
|
||||
```text
|
||||
P1 -> P2 -> P3 -> P4
|
||||
```
|
||||
|
||||
原因:
|
||||
|
||||
- 低风险,不改变 transfer 启动时机;
|
||||
- 能直接验证 CPU overhead 是否来自 descriptor 重建;
|
||||
- 如果收益明显,再进入 P5/P6;
|
||||
- 如果收益不明显,说明主要瓶颈可能在 H2D bandwidth、early layer wait、shared-KV L1 prefetch/reduce 或 decode transfer。
|
||||
|
||||
不建议一开始就做 P7。
|
||||
P7 会改 forward/layer hook 生命周期,风险高于 descriptor reuse,应在 P1-P4 有数据后推进。
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 8. P4-P6 实施记录(2026-06-11)
|
||||
|
||||
### R1. P4 benchmark 已补齐并完成一轮全矩阵验证
|
||||
|
||||
新增 `tai-kernel/benchmark/nsa_prefill/benchmark_hicache_h2d_prepared_descriptor.py`。
|
||||
|
||||
覆盖维度:
|
||||
|
||||
- token 默认:`4096, 16384, 65536, 120000, 192000`;
|
||||
- batch shape 默认:`1, 5, 10`;
|
||||
- layout:`page_first_direct`, `layer_page_first`;
|
||||
- page pattern:`contiguous`, `owner_lane`, `random`,并支持 `fragmented/strided/reverse`;
|
||||
- 输出:descriptor build ms、per-layer submit ms、submit per layer us、total ms、effective GB/s、segments。
|
||||
|
||||
远端 quick smoke:
|
||||
|
||||
```text
|
||||
page_first_direct,contiguous,bs=1,tokens=4096,layers=2,segments=64,total=0.373190ms
|
||||
layer_page_first,contiguous,bs=1,tokens=4096,layers=2,segments=1,total=0.081027ms
|
||||
```
|
||||
|
||||
P4 全矩阵远端验证:
|
||||
|
||||
```text
|
||||
容器:g0034:cjy-glm5-new
|
||||
命令:benchmark_hicache_h2d_prepared_descriptor.py
|
||||
参数:tokens=4096,16384,65536,120000,192000
|
||||
参数:bs=1,5,10
|
||||
参数:patterns=contiguous,owner_lane,random
|
||||
参数:layouts=page_first_direct,layer_page_first
|
||||
参数:warmup=1 repeat=2
|
||||
CSV:/mnt/beegfs/cjy/log/hicache_h2d_prepared_descriptor_p4_20260610_210053.csv
|
||||
行数:90 = 2 layouts * 3 patterns * 3 bs * 5 token sizes
|
||||
```
|
||||
|
||||
代表性结果:
|
||||
|
||||
```text
|
||||
page_first_direct contiguous bs=1 tokens=65536:
|
||||
segments=1024 build=1.224ms submit=74.595ms total=107.546ms gbps=54.756
|
||||
|
||||
layer_page_first contiguous bs=1 tokens=65536:
|
||||
segments=1 build=0.882ms submit=0.918ms total=106.841ms gbps=55.117
|
||||
|
||||
page_first_direct owner_lane bs=10 tokens=192000:
|
||||
segments=3000 build=4.158ms submit=283.340ms total=315.447ms gbps=54.692
|
||||
|
||||
layer_page_first owner_lane bs=10 tokens=192000:
|
||||
segments=3000 build=3.636ms submit=283.182ms total=315.782ms gbps=54.634
|
||||
|
||||
page_first_direct random bs=10 tokens=192000:
|
||||
segments=3000 build=4.129ms submit=283.274ms total=315.489ms gbps=54.685
|
||||
|
||||
layer_page_first random bs=10 tokens=192000:
|
||||
segments=2998 build=3.665ms submit=282.752ms total=315.268ms gbps=54.723
|
||||
```
|
||||
|
||||
结论:
|
||||
|
||||
1. benchmark 已能覆盖 owner-lane 大样本,不再因为 `pool_tokens` 不足失败。实现上按 pattern 动态估算 host page 池,并按 layout lazy 分配 host pinned tensor,避免同时持有 PFD/LPF 两份大 buffer。
|
||||
2. `layer_page_first` 对“跨 request/跨 page 真实连续”的 contiguous 场景能显著降低 submit CPU:64k 时 `74.6ms -> 0.9ms`,segments `1024 -> 1`。
|
||||
3. `owner_lane/random` 下 LPF 基本 neutral,因为物理 page 不连续,segments 仍约等于 pages;这说明 LPF 的收益强依赖 host/L2 allocator 能提供按 owner-lane/request 连续的物理 page。
|
||||
4. 大 transfer 的有效带宽稳定约 `54-55 GB/s`;当前 P4 主要暴露的是 submit/descriptor 开销,不是 H2D 带宽不足。
|
||||
|
||||
### R2. P5 segment compact 已实现为保守线性扫描
|
||||
|
||||
新增 tai-kernel API:
|
||||
|
||||
```python
|
||||
prepare_h2d_page_descriptor(src_indices, dst_indices, page_size=..., layout=...)
|
||||
```
|
||||
|
||||
合同:
|
||||
|
||||
- indices 必须是 CPU int64 contiguous;CUDA indices fail-fast;
|
||||
- token indices 必须 page aligned;
|
||||
- `page_first_direct` 固定 layer H2D 不合并相邻 page,因为 host 物理布局是 `[page, layer, page_size, ...]`,相邻 page 的同一 layer 中间隔着其他 layer;
|
||||
- `layer_page_first` 只在 host 和 device token start 都连续时合并 segment;不 sort、不 reorder、不做 O(n log n)。
|
||||
|
||||
新增单测:
|
||||
|
||||
```text
|
||||
tests/nsa_prefill/test_kvcacheio_prepared_descriptor.py
|
||||
```
|
||||
|
||||
验证:
|
||||
|
||||
- `page_first_direct` contiguous pages 仍保持每页一个 segment;
|
||||
- `layer_page_first` contiguous run 合并;
|
||||
- 只有一侧连续不合并;
|
||||
- 非 page aligned / CUDA indices fail-fast;
|
||||
- destroy 后 submit fail-fast。
|
||||
|
||||
### R3. P6 tai-kernel prepared API + SGLang 接入完成
|
||||
|
||||
新增 tai-kernel public API:
|
||||
|
||||
```python
|
||||
H2DPageDescriptor
|
||||
H2DPageSegment
|
||||
prepare_h2d_page_descriptor(...)
|
||||
submit_h2d_layer(desc, src_ptrs, dst_ptrs, layer_id=...)
|
||||
destroy_h2d_page_descriptor(desc)
|
||||
```
|
||||
|
||||
SGLang 接入:
|
||||
|
||||
- `HostKVCache.begin_load_to_device_op()` 会为 direct + `page_first_direct/layer_page_first` 预构建 `tai_h2d_descriptor`;
|
||||
- `NSATokenToKVPoolHost.begin_load_to_device_op()` 会额外预构建 index `tai_index_h2d_descriptor`(`page_size=1`);
|
||||
- `MLATokenToKVPoolHost.load_to_device_per_layer()` 优先走 `submit_h2d_layer()`;
|
||||
- NSA index load 优先走 index prepared descriptor;
|
||||
- `end_load_to_device_op()` 统一 destroy;
|
||||
- tai API 缺失时 warning fallback 到旧 direct path,不 silent fallback;prepare API 存在但合同不满足时 fail-fast。
|
||||
|
||||
当前 P6 已完成到 P6b:
|
||||
|
||||
1. Python API 层保留 `H2DPageDescriptor` 合同;
|
||||
2. tai-kernel C++ 新增:
|
||||
- `prepare_h2d_page_descriptor_data(...)`
|
||||
- `submit_h2d_layer_prepared(...)`
|
||||
3. prepare 阶段在 C++ 中完成 page-aligned 校验、page starts 和 segment starts/pages 构建;
|
||||
4. submit 阶段直接使用 C++ prepared tensors,不再每层从 Python wrapper 重新做 validation/segment 构造;
|
||||
5. SGLang 在 direct + `page_first_direct/layer_page_first` 下会检查 `cpp_prepared`,如果 tai Python API 存在但 C++ prepared submit 数据不可用,会输出明确 fallback warning,不 silent fallback。
|
||||
|
||||
仍未做的是“真正 opaque C++ custom descriptor / 预生成每层 pointer vector”。当前 C++ prepared submit 仍会在每层构造 `cudaMemcpyBatchAsync` 的 src/dst/size vector;但已把重复 page contract 校验和 segment build 从每层热路径移出。若 ETE timing 仍显示 submit CPU overhead 高,下一步才需要继续做 custom descriptor 或 copy-engine queue。
|
||||
|
||||
### R4. 验证记录
|
||||
|
||||
本地:
|
||||
|
||||
```text
|
||||
python -m py_compile sglang memory_pool_host.py / tai kvcacheio.py / benchmark script: OK
|
||||
PYTHONPATH=python python -m pytest -q tests/nsa_prefill/test_kvcacheio_prepared_descriptor.py: 4 passed
|
||||
```
|
||||
|
||||
远端 `g0034:cjy-glm5-new`:
|
||||
|
||||
```text
|
||||
/mnt/beegfs/cjy/tai-kernel: test_kvcacheio_prepared_descriptor.py -> 4 passed
|
||||
/mnt/beegfs/cjy/sglang-dev: test_hicache_controller_cp.py -> 88 passed
|
||||
benchmark_hicache_h2d_prepared_descriptor.py quick smoke -> OK
|
||||
```
|
||||
|
||||
补充验证(P6b 完成后):
|
||||
|
||||
```text
|
||||
/mnt/beegfs/cjy/tai-kernel: test_kvcacheio_prepared_descriptor.py -> 6 passed
|
||||
/mnt/beegfs/cjy/sglang-dev: test_hicache_controller_cp.py -> 88 passed
|
||||
P4 full matrix benchmark -> 90 rows completed
|
||||
```
|
||||
|
||||
未验证:
|
||||
|
||||
- ETE replay / GSM8K cache-hit 正确性;
|
||||
- 底层 C++ opaque descriptor / copy-engine queue 的进一步 CPU submit 降幅(尚未实现)。
|
||||
Reference in New Issue
Block a user