feat(qwen35): add joint prefix cache - #836
Conversation
4dcb023 to
7d46f71
Compare
Unify Qwen3.5 KV and recurrent-state lifecycle across single-GPU and TP serving, accuracy tests, and benchmarks. Add prefix-cache coverage and record the measured performance results. Signed-off-by: wangke <364517893@qq.com>
7d46f71 to
40d56ff
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 40d56ffdff
ℹ️ 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".
| if reservation.was_replacement() { | ||
| self.stats.evictions += 1; | ||
| } | ||
| self.state.publish(reservation, kv_guards); |
There was a problem hiding this comment.
Reclaim cached KV before admission stalls
When prefix caching is enabled and a near-capacity prompt publishes a snapshot, these guards can retain most of the KV pool after the request finishes. scheduler_loop checks available_blocks() before calling begin_request, so a repeated request that could reuse those blocks is deferred as though it needed a cold allocation; because the only eviction path runs from reserve_prefix after a request has already been admitted and prefilled, an idle engine can defer that request forever. Admission must account for reusable blocks or evict unpinned prefix entries under KV pressure.
Useful? React with 👍 / 👎.
| .filter(|&v| v > 0) | ||
| .unwrap_or(pegainfer_qwen35::DEFAULT_MAX_PREFILL_TOKENS); | ||
| let handle = pegainfer_qwen35::start_engine_with_capacity_and_policy( | ||
| let handle = pegainfer_qwen35::launch_with_options_and_policy( |
There was a problem hiding this comment.
Preserve the requested benchmark seed
When a Qwen3.5 benchmark uses non-greedy sampling with --seed other than 42, this new call routes through launch_with_options_and_policy, whose EngineLoadOptions still hardcodes seed: 42; the previous path passed command_seed(&cli). Consequently the advertised seed no longer controls engine sampling for Qwen3.5, breaking reproducibility and making different seed runs use the same sampling stream. Pass the command seed through the launch options or retain the explicit EngineLoadOptions path.
Useful? React with 👍 / 👎.
Description
This PR is a follow-up of #257 and #423. It adds opt-in prefix caching for Qwen3.5 on both single-GPU and tensor-parallel serving.
Qwen3.5 is a hybrid model, so full-attention KV alone is not a valid reusable prefix. A cache hit is reported only when full-attention KV and the complete recurrent/conv state are available at the same token boundary.
Prefix caching remains disabled by default and can be enabled with:
Type of Change
What changed
KvCacheManager/RequestKv, with exact-boundary joint KV prefix lookup and attachment.KvBuffer/RecurrentStateStore, shared snapshot slot IDs, and capacity limited by the smallest rank.TokenEvent::Scheduled.cached_tokensreporting.Core components
Qwen35PrefixCachePrefixCacheStatePrefixEntryPrefixGuardPrefixReservationRequestKvRecurrentStateStoreInference flow
RequestKvand recurrent state, then runbegin_request().recurrent_slot, thenfinish_restore()validates the positions and reportscached_tokens; otherwise the request starts cold.schedule_prefill()andprefill_view(), run the remaining prompt suffix, and callapply_prefill()only after a successful forward.reserve_prefix(). If a reservation is returned, save the recurrent state and callpublish_prefix()after the copy succeeds. Abort the reservation on failure. Non-aligned tails apply without creating a snapshot.schedule_decode(), run the model throughdecode_view(), sample, and callapply_decode().PrefixEntryobjects retain their KV leases and snapshots for later requests; non-published KV returns to the pool.Tensor-parallel differences
KvCacheManager,RequestKvmap,PrefixCacheState, and page-id namespace.KvBufferandRecurrentStateStore; logical capacity is limited to the smallest rank-local KV capacity.KvViewpage IDs to every rank.Detailed
prefix_cache.rsAPI sequencebegin_request()createsRequestKv, probes the longest joint boundary, and attaches the selected KV prefix.PrefixGuard::recurrent_slot()into request-local recurrent state on each rank.finish_restore()validates KV and recurrent positions, records the hit, returnscached_tokens, and releases the restore pin.schedule_prefill()/schedule_decode()reserve pages;prefill_view()/decode_view()expose the kernel page table.apply_prefill()/apply_decode()apply a successful forward;revert_schedule()rolls back failed reservations.reserve_prefix()returns an optionalPrefixReservation; the caller saves the physical recurrent state and then callspublish_prefix()orabort_prefix().release_request()releases request-owned KV and preserves cold behavior when caching is disabled.Consistency guarantees
Validation
openinfer-kv-cache(19), Qwen3.5 release library (77; 6 TP2 cases ignored in the normal run), and Qwen3.5 server config (9).cached_tokens=0; warm requests restore the longest valid joint boundary.0.0251/0.0983(short) and0.0215/0.0684(long).-D warnings, and releaseopeninferandbench_servingbuilds passed with--features qwen35.Performance
Measured on 2026-08-06, using local Qwen3.5-4B weights and idle RTX 4090s. TP1 used GPU 1 with CUDA Graphs enabled; TP2 used GPUs 1/2 with
--cuda-graph=false. The prefix A/B matrix used a 1,024 MiB cache, 2 warmups, and 10 measured iterations.For single-token generation, the table shows TTFT p50 in milliseconds (
cache off -> cache on):For 128 generated tokens, cache reuse primarily reduces prefill/TTFT; decode remains the dominant part of E2E latency:
TP1 serving-load checks also completed:
24.67/94.41/152.25 ms, request throughput of83.11/75.87/69.04 tok/s, and a 2,048-token cache hit for every request.12.03/12.25 ms; mixed-load ITL was12.03/36.12 ms. After initial insertion, 4,096-token warm-prefix injections restored 3,840 tokens with41.42–51.00 msprefill and no warnings.31.84/48.71/93.34/146.05/282.10 msto18.51/21.42/21.34/29.16/25.51 ms.Follow-ups
Qwen35PrefixCacheexport more interfaces for DFlash feat(qwen35): add opt-in DFlash speculative decoding #626. Migrage prefix cache when DFlash is implemented.