Fix CP HiCache catch_up_all_layers fallback on chunked-prefill final chunk

Under overlap scheduling a chunked request's final-chunk write-backup prepare
read a stale `is_chunked` (>0): that per-tick counter is decremented in
process_batch_result, which the overlap loop runs AFTER the run_batch prepare
hook. So prepare floored `backup_end` to a page boundary (the intermediate-chunk
rule) and dropped the now-complete final tail page, while the final non-chunked
insert builds the radix node at full unaligned length. The exact-equality attach
predicate (prepared.logical_len == len(value)) then failed by
(num_tokens-1) mod page_size, dropped the per-layer overlap backup, and forced
the serial all-layer catch-up (~89% of large chunked requests in prod).

Decouple the floor decision from the stale counter: the scheduler marks
`req.cp_backup_is_intermediate_chunk = (req is self.chunked_req)` in
`_prepare_hicache_write_backups_before_forward` (the live chunked_req identity
is the authoritative "will be chunked further" signal, set this tick before
run_batch), and the prepare candidate builder floors on that flag instead of
`is_chunked`. Intermediate chunks still floor; only the misclassified final
chunk now backs up its full tail and attaches the overlap backup.

Tests: update the chunked prepare test to the new flag; add a regression test
that a final chunk with stale is_chunked reserves the full tail; add an offline
repro that drives the real prepare/insert/probe and sweeps unaligned tails.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-18 11:54:24 +00:00
parent a4ec2236ef
commit 512fe92a83
4 changed files with 351 additions and 6 deletions

View File

@@ -2990,6 +2990,23 @@ class Scheduler(
):
return
# Tell the CP HiCache backup prepare which req (if any) is a genuine
# INTERMEDIATE chunk, i.e. will be prefilled further next round, so it
# floors off its still-incomplete tail page; every other req in this
# batch completes here and must back up its full (now-complete) tail.
#
# We mark it from the live `chunked_req` identity rather than letting
# prepare read `req.is_chunked`, because `is_chunked` is a per-tick
# counter whose decrement lags one iteration under overlap scheduling
# (it is decremented in process_batch_result, which runs AFTER this
# run_batch). On a request's final chunk it therefore reads stale (>0),
# making prepare floor off the now-complete tail page -> prepared
# logical_len != final radix node len -> the overlap backup is dropped
# and the node falls back to the serial all-layer catch-up.
carried_chunked_req = self.chunked_req
for req in batch.reqs:
req.cp_backup_is_intermediate_chunk = req is carried_chunked_req
prepare_batch_fn = getattr(
self.tree_cache, "prepare_write_backups_for_reqs", None
)

View File

@@ -2581,12 +2581,22 @@ class HiRadixCache(RadixCache):
token_ids = req.fill_ids
keys = convert_to_bigram_key(token_ids) if self.is_eagle else token_ids
backup_end = len(keys)
if getattr(req, "is_chunked", 0) > 0 and self.page_size > 1:
# Chunked prefill should still populate CP HiCache, but only for
# pages whose KV has become fully available. Leaving the current
# sub-page tail out of the prepared write avoids creating a
# scheduler-visible stale tail that the next chunk has to prune
# while the same request still owns/locks it.
if getattr(req, "cp_backup_is_intermediate_chunk", False) and self.page_size > 1:
# Only a genuine INTERMEDIATE chunk floors off its tail page: a later
# chunk of the same request will extend it, so backing up that still
# incomplete page now would create a scheduler-visible stale tail the
# next chunk has to prune while this request still owns/locks it. The
# FINAL chunk's tail page is complete and must be backed up in full,
# to match the radix node that the final (non-chunked) insertion
# builds -- otherwise prepared logical_len != len(value) and the
# exact-equality attach predicate drops the overlap backup.
#
# NOTE: we must NOT key this off `req.is_chunked`. It is a per-tick
# counter whose decrement lags one iteration under overlap scheduling
# and so reads stale (>0) on the final chunk. The scheduler sets
# `cp_backup_is_intermediate_chunk` from the live `chunked_req`
# identity in `_prepare_hicache_write_backups_before_forward`, which
# runs in the same run_batch immediately before this.
backup_end = floor_to_page_len(backup_end, self.page_size)
if backup_end <= 0:
logger.debug(