Skip to content

refactor(qwen3): migrate prefix/offload path to openinfer-kv-store - #829

Open
xiaguan wants to merge 2 commits into
mainfrom
feat/qwen3-kv-store-migration
Open

refactor(qwen3): migrate prefix/offload path to openinfer-kv-store#829
xiaguan wants to merge 2 commits into
mainfrom
feat/qwen3-kv-store-migration

Conversation

@xiaguan

@xiaguan xiaguan commented Aug 2, 2026

Copy link
Copy Markdown
Collaborator

概要

qwen3 的前缀恢复 / KV offload 全路径迁移到 openinfer-kv-store,crate 不再依赖 openinfer-kv-cache / openinfer-kv-offload;vLLM P/D 兼容层并入本 PR 删除(issue #828 延期决策表中的切割项全部落地)。+1163 / −1794,42 文件。

「single source of orchestration」落地:resolve 编排从 qwen3 手写的 executor 三相状态机(prefetch RemoteFetch/Loading/Committed)收敛为 kv-store 的 resolve_prefix,scheduler 侧一个 ResolveHub 统一 plain 与 LoRA-control 两条 submit 通道。

关键改动

  • executor:Option<OffloadEngine>Option<Arc<KvStore>>;prefetch 状态机(begin/drain/wait_kv_prefetch、remote_fetch.rs、scheduler loading 队列)整体删除,无开关硬切;seal/retire/flush 直调 store(flush-on-finish 屏障 = flush_saves_then)。executor 构建 PegaflowHost + 逐层 ArenaSpec(几何与 kv-offload from_buffer 逐字段对齐)。
  • scheduler:intake 时 store.resolve_prefix 上 store runtime(CacheScope{lora},TokenSink 作 CancelProbe,echo 跳过);KvPrefix RAII hold 护队列期,首个 PrefillRequestResult 释放;admission 一本账——resolved_prefix_blocks(hit_tokens/block_size)抵扣 need、pinned_blocks(0) 扣 available,双重计数堵死。
  • kv-store:吸收物理 KV 层(KvBuffer/KvLayout/KvView/KvCacheManager/BlockPool-with-events),自包含;新增嵌入 API(runtime_handle、set_resolve_enabled / set_l1_retention_disabled、flush_saves_then/blocking)。
  • vLLM P/D 切割:vllm_compat 状态机、VllmBlockHasher(xxh3_128 CBOR 链)、miss_wait 窗口、page_first 注册、Qwen3VllmCompatOptions、server --kv-pd-* flags 全删。kernels 的 PagedKvPageFirst 是无关 LayoutTag,保留。
  • 测试:kv_offload_cpu_hit 重写为新两幕(execute_prefill+retire→seal → flush_offload_saves → evict_cached_blocks → resolve_prefix 取回 → prefill 自动 match,REGRET/MEAN 容差不变);scheduler 单测 Fake → 真 tier-less KvStore。
  • 文档:design.md 迁移计划第 2 步 ✅、未决三项收口;pegaflow-offload-integration qwen3 接线改写;pd-disaggregation-m2 加 supersede 注记。

验证

本地(5070 Ti):

  • cargo check --release -p openinfer-qwen3 --all-targets
  • cargo test --release -p openinfer-qwen3 --lib 61/61(基线 69 − 随状态机删除的 remote_fetch 8 个)
  • cargo test --release -p openinfer-kv-store 全绿;cargo tree -p openinfer-qwen3 无 kv-cache/kv-offload(dev 侧也零)
  • rg vllm_compat|VllmBlockHasher|miss_wait|page_first 源码零命中

GPU gate(5090,Qwen3-4B)在本分支跟进:build + hf_golden_gate + prefix_cache + kv_offload_cpu_hit + glm52 编译回归。

Ref #828(映射表/延期决策)、#824

qwen3's prefix-restore and KV offload now ride openinfer-kv-store
end to end; the crate drops its openinfer-kv-cache / openinfer-kv-offload
deps, and the vLLM P/D compat layer is deleted in the same cut.

- executor: Option<OffloadEngine> -> Option<Arc<KvStore>>; the prefetch
  state machine (PrefetchState/PrefetchPhase, begin/drain/wait_kv_prefetch,
  remote_fetch.rs, scheduler loading queue) is deleted outright — no
  compat switch. seal/retire/flush ride KvStore; the flush-on-finish
  Finished barrier becomes store.flush_saves_then.
- scheduler: ResolveHub resolves prefixes on the store runtime at intake
  (CacheScope{lora}, default policy, TokenSink as CancelProbe; echo
  requests skip resolve). KvPrefix RAII hold covers queue time and drops
  at the first PrefillRequestResult; admission credits
  resolved_prefix_blocks (hit_tokens/block_size) and excludes
  store.pinned_blocks(0) from the budget. Both submit variants wired.
- kv-store absorbs the physical KV layer (KvBuffer/KvLayout/KvView/
  KvCacheManager/BlockPool-with-events) and gains the embedding API
  (runtime_handle, set_resolve_enabled / set_l1_retention_disabled for
  the runtime prefix-cache toggles, flush_saves_then/blocking).
- vLLM P/D compat deleted: vllm_compat state machine, VllmBlockHasher
  (xxh3_128 CBOR chain), miss_wait window, page_first registration,
  Qwen3VllmCompatOptions, --kv-pd-* server flags. PagedKvPageFirst in
  kernels is an unrelated LayoutTag and stays.
- tests: kv_offload_cpu_hit rewritten to the new two-act flow
  (seal -> flush -> evict -> resolve_prefix -> prefill match; logits
  tolerances unchanged). docs synced: design.md migration status,
  pegaflow-offload-integration rewired, pd-disaggregation-m2 superseded.

GPU gates (5090, Qwen3-4B) follow on this branch.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: xiaguan <751080330@qq.com>
@xiaguan

xiaguan commented Aug 2, 2026

Copy link
Copy Markdown
Collaborator Author

5090 GPU gate 结果(Qwen3-4B,/data/Qwen3-4B,GPU4,sm_120,2b55e031)

Gate 结果 关键数字
build --release ✅ 2m01s
hf_golden_gate ✅ 47.69s 7 surfaces(sequential/batched eager、cached replay ×2、cuda-graph ×2 + cached replay),mean Δ≈0.030 nats(bf16 floor);TP=2 pass 按设计跳过(需 2 可见卡,与本 PR 无关)
prefix_cache(第二轮 prefill 命中不重算) ✅ 2.70s exact cached-token 断言 + warm/cold logits:max Δ ≤ 0.121,全部在容差内
kv_offload_cpu_hit(offload roundtrip:seal→flush→evict→resolve_prefix 取回→prefill 自动 match) ✅ 4.29s 纯 host 恢复 + host+GPU 组合两幕,mean Δ 0.039/0.033,REGRET=0.20/MEAN=0.06 容差内
scheduler_robustness(真实 EngineHandle,客户挂断 retire 链路) ✅ 3.09s 压住新 resolve 取消路径(TokenSink 即 CancelProbe)

其余验收项:

  • cargo tree -p openinfer-qwen3:无 openinfer-kv-cache/openinfer-kv-offload(dev 侧也零);rg vllm_compat|VllmBlockHasher|miss_wait|page_first 源码零命中(仅 kernels 的无关 PagedKvPageFirst LayoutTag)。
  • prefetched_blocks 抵扣语义:由 admission 一本账继承——resolved_prefix_blocks = kv_prefix.hit_tokens()/block_size 抵扣 need、available 扣 pinned_blocks(0),scheduler 单测覆盖(61/61 绿)。
  • qwen35 e2e_scheduler:不适用——cargo tree -p openinfer-qwen35 与本 PR 改动的 crate(qwen3/kv-store/kv-cache/kv-offload)零交集,其构建不含任何本次变更代码。
  • glm52 编译回归(kv-offload 签名顺修一行):本地 NCCL 2.30.4 下 cargo check -p openinfer-glm52 --all-targets 进行中,结果随后补充。

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2b55e03186

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +1356 to 1357
let fresh_needed = footprint.saturating_sub(resolved_prefix_blocks(&req, block_size));
if fresh_needed <= budget && decode_slots > 0 {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Prevent resolved prefixes from deadlocking admission

When several CPU-tier hits resolve before admission, every KvPrefix hold removes its pages from available_blocks(), but this loop credits only the request currently being considered while leaving all other deferred holds pinned. For example, in a 100-page pool, two requests with 80-page lifetime footprints and 40-page resolved prefixes leave 20 pages free; each still needs 40 pages, so neither is admitted and neither hold is ever released. Bound concurrent reservations or release/defer later holds so at least the head request can make progress.

Useful? React with 👍 / 👎.

Comment on lines +402 to +403
self.runtime.spawn(async move {
if let Err(err) = tier.flush().await {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Chain finish barriers across drained save batches

With chunked prefill and concurrent finishes, an earlier tier.flush() can drain one request's early-chunk save handle from PegaflowTier::pending_saves; when that request later finishes, this independently spawned flush sees only its remaining handles and can invoke then before the earlier barrier completes. Under --kv-p2p-flush-on-finish, the HTTP response can therefore advertise KV readiness while part of the prefix is not yet registered, producing a short peer hit. Serialize or chain barriers so each callback transitively covers handles drained by predecessors.

Useful? React with 👍 / 👎.

Comment on lines +403 to +406
if let Err(err) = tier.flush().await {
log::warn!("kv-store flush before callback failed: {err:#}");
}
then();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Bound the P/D finish barrier

When P2P registration or a save task stalls, tier.flush().await has no timeout, so the callback documented as always running is never reached. Qwen3's P/D prefill role with --kv-p2p-flush-on-finish withholds the request's Finished event behind this callback, meaning a MetaServer or transfer stall can leave the client request hanging indefinitely instead of degrading to a peer recompute. Wrap the barrier in a bounded timeout and run then on timeout as well.

Useful? React with 👍 / 👎.

Comment on lines 954 to 957
// 2. Once idle, apply pending control commands before admitting newer
// generation requests that arrived behind them.
if active.is_empty() && deferred.is_empty() && prefilling.is_empty() {
drain_idle_control(&mut executor, &mut pending_control);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep control commands behind in-flight resolves

With LoRA plus KV offload enabled, a Generate using adapter A is moved out of all three checked queues while its prefix resolves. If an Unload(A) command arrives immediately afterward, this condition treats the scheduler as idle and unloads the adapter before the earlier generation request returns, after which that request is rejected as using an unknown adapter. Include resolve_hub.in_flight() == 0 in the idle condition so control commands cannot overtake previously submitted generation work.

Useful? React with 👍 / 👎.

Comment on lines +771 to +773
executor
.available_blocks()
.saturating_sub(resolve_hub.pinned_blocks()),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Do not subtract save pins twice

During KV offload saves, each KvBlockGuard keeps its slot active, and BlockManager::available_blocks() already excludes active slots; subtracting pinned_blocks() again double-charges every page being copied. Under sustained offload or near-capacity workloads this can defer otherwise feasible requests until D2H completes and materially reduce throughput. Pass the pool's available count directly, or change the pin metric to include only capacity not already reflected there.

Useful? React with 👍 / 👎.

CI clippy (sm_80, -D warnings) flags or_insert_with(SaveCursor::new);
the local pre-commit clippy only lints default-members, where qwen3 is
built as a capped-lint dependency, so this only surfaces in CI.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: xiaguan <751080330@qq.com>
@xiaguan

xiaguan commented Aug 2, 2026

Copy link
Copy Markdown
Collaborator Author

收尾:glm52 编译回归通过(本地,NCCL 2.30.4,cargo check --release -p openinfer-glm52 --all-targets,1m06s,仅剩既有 dead-code 警告);CI 在 4b17f112(clippy or_default() fixup)上 14/14 全绿,MERGEABLE。验收五项至此全部闭环。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant