Keep current reuse independent of async CP prefetch
Async MLA/index prefetch is a scheduling optimization, not the correctness contract for target current reuse. Tiny cache-hit suffixes can skip async prefetcher creation while target partial-current reuse still composes page-slot prefix materialization with current KV rows synchronously. CP HiCache radix/device accounting now treats retained valid-tail pages as physical page spans so allocator state stays consistent when logical cache keys are shorter than the retained page. Constraint: CP shared KV ownership and HiCache residency are page-granular while request-visible cache lengths remain valid-token lengths. Constraint: Async prefetch can hang or regress on large-prefix tiny-extend traffic and must not be required for current reuse. Rejected: Treat missing prefetcher as fail-fast for target partial-current reuse | disabled useful current reuse and broke tiny-prefix/tiny-suffix traffic. Rejected: Keep async prefetcher object with synchronous consume mode | conflates prefetch object existence with current-layer correctness and hides fallback semantics. Confidence: medium Scope-risk: moderate Directive: Do not make current-only or target partial-current reuse depend on MLA/index prefetcher creation; prefetcher objects mean async next-layer work exists. Tested: Remote g0034 container py_compile for touched modules. Tested: Remote g0034 PYTHONPATH=python python -m pytest -q test/registered/unit/mem_cache/test_cp_shared_kv_runtime.py -> 73 passed. Tested: Remote g0034 PYTHONPATH=python python -m pytest -q test/registered/unit/mem_cache/test_cp_hicache_metadata.py -> 90 passed. Not-tested: Latest full ETE traffic run with GLM-5.1 CP HiCache after this commit. Not-tested: CUDA kernel-level performance impact of synchronous no-prefetch partial-current compose.
This commit is contained in:
@@ -1033,6 +1033,281 @@ Completed C14:
|
||||
123 passed, 5 warnings
|
||||
```
|
||||
|
||||
## C15: CP valid-tail radix nodes still need page-physical device accounting
|
||||
|
||||
Finding:
|
||||
|
||||
- Startup can fail immediately after disaggregated prefill warmup with:
|
||||
|
||||
```text
|
||||
token_to_kv_pool_allocator memory leak detected!
|
||||
max_total_num_tokens=833024, available_size=833024,
|
||||
evictable_size=3, protected_size=0, session_held=0
|
||||
```
|
||||
|
||||
- The observed `evictable_size=3` is consistent with EAGLE/bigram warmup:
|
||||
a 4-token request is cached as a 3-token bigram radix key. Under CP HiCache
|
||||
the radix key is valid-tail based, while the CP device allocator is
|
||||
page-granular.
|
||||
- `RadixCache.cache_finished_req()` still freed `kv_indices[len(keys):]`.
|
||||
For CP + page allocator, freeing the 4th token of a 64-token page releases the
|
||||
whole physical page even though the radix node still owns the first 3 valid
|
||||
tokens. The allocator then reports the full pool available while radix still
|
||||
reports an evictable 3-token node.
|
||||
- HiRadix device accounting also still used `len(node.key)` / `len(node.value)`
|
||||
in several places. With valid-tail nodes this is the scheduler-visible valid
|
||||
length, not the allocator-visible physical page span.
|
||||
|
||||
Correction:
|
||||
|
||||
- CP HiCache device residency must account physical page spans while radix
|
||||
matching and scheduler-visible prefix lengths remain valid-token based.
|
||||
- Tail free for CP valid-tail keys must start at the next page boundary, not at
|
||||
`len(keys)`, so partial retained pages are never released out from under a
|
||||
radix node.
|
||||
- Splits inside a CP physical page should be floored to the previous page
|
||||
boundary (or rejected/deferred at zero) even when the node is not yet backed
|
||||
by host metadata. Page is the minimum ownership unit for both GPU and host
|
||||
HiCache.
|
||||
|
||||
Tests:
|
||||
|
||||
- Add a regression for EAGLE/bigram CP finished-cache tail free: retained
|
||||
partial pages must not call allocator free on the in-page tail.
|
||||
- Add CP page-physical accounting regressions for insert/lock/evict paths.
|
||||
- Add a split-floor regression for an unbacked CP valid-tail node.
|
||||
|
||||
Implemented C15:
|
||||
|
||||
- Added `ceil_to_page_len()` and made CP `cache_finished_req()` free suffix
|
||||
pages only from the next page boundary after the valid radix key. This keeps
|
||||
a retained valid-tail node from releasing the physical page it still owns.
|
||||
- Added `HiRadixCache._node_device_resident_len()` and routed CP insert,
|
||||
single-node write locks, path locks, load-back locks, regular eviction, and
|
||||
backed demotion through allocator-visible physical page lengths.
|
||||
- Changed the CP split-floor policy to apply to unbacked valid-tail device
|
||||
nodes too. Splits inside a physical page now floor to the previous page
|
||||
boundary independent of host-backup state.
|
||||
|
||||
Completed C15 tests:
|
||||
|
||||
```text
|
||||
remote g0034 container:
|
||||
test_cp_hicache_metadata.py
|
||||
90 passed, 5 warnings
|
||||
test_cp_hicache_load_back_owner_lanes.py
|
||||
test_cp_shared_kv_layout.py
|
||||
test_cp_shared_kv_runtime.py
|
||||
103 passed, 3 warnings
|
||||
```
|
||||
|
||||
Not-tested C15:
|
||||
|
||||
- `test_radix_cache_unit.py` collection currently fails in the remote container
|
||||
before running tests because the `sgl_kernel::sgl_per_token_group_quant_8bit`
|
||||
operator is not registered in that test import path. This is an environment /
|
||||
test bootstrap issue, not a C15 assertion failure.
|
||||
|
||||
## C16: Exact valid-tail hits can make the next backup start mid-page
|
||||
|
||||
Finding:
|
||||
|
||||
- Current C7/C15 code keeps exact CP valid-tail cache hits token-precise. That
|
||||
means a request can hit, for example, 100 valid tokens on 64-token pages and
|
||||
then start its next suffix at token 100.
|
||||
- `prepare_write_backup_for_req()` derives the backup start as
|
||||
`max(req.cache_protected_len, existing_prefix_len)`. If the protected prefix
|
||||
is a valid tail, `start` can be inside a physical page.
|
||||
- `_cp_required_host_tokens_by_rank()` / `reserve_write_cp()` then call
|
||||
`pad_token_locs_to_page_boundary()` on `device_indices[start:]`. That helper
|
||||
is only correct for spans that start at a page boundary; CPU tensors validate
|
||||
this, but CUDA tensors intentionally skip the sync-heavy offset check.
|
||||
- Therefore the production CUDA path can silently build a padded physical span
|
||||
from a mid-page start, producing incorrect page-owner accounting and host
|
||||
reservation metadata.
|
||||
|
||||
Risk:
|
||||
|
||||
- This is a correctness risk for repeated prompts where the previous request
|
||||
leaves a non-page-aligned valid-tail node and the next request extends beyond
|
||||
that exact tail.
|
||||
- It also conflicts with the current design principle that CP HiCache owns cache
|
||||
at page granularity and may sacrifice sub-page cache to keep management simple.
|
||||
|
||||
Correction options:
|
||||
|
||||
- Preferred under the current page-as-minimum-unit contract: expose only the
|
||||
previous page boundary as `cache_protected_len` / `device_indices` for any hit
|
||||
that will be extended, so new backup starts at a page boundary.
|
||||
- More complex alternative: implement explicit partial-page sharing/refcounting
|
||||
between a cached prefix tail and the new current suffix. This is intentionally
|
||||
not the current direction.
|
||||
|
||||
Tests needed:
|
||||
|
||||
- Cache a 100-token valid-tail node, then issue a request that hits those 100
|
||||
tokens and extends by another suffix. Assert write reservation starts at 64,
|
||||
not 100, or otherwise fails fast before mid-page padding.
|
||||
- Add a CUDA/remote regression or source-level guard proving CUDA hot path cannot
|
||||
call `pad_token_locs_to_page_boundary()` with a non-page-start span.
|
||||
|
||||
## C17: Duplicate/no-insert frees are still token-granular under CP pages
|
||||
|
||||
Finding:
|
||||
|
||||
- C15 fixed the final tail free in `RadixCache.cache_finished_req()` by starting
|
||||
CP tail free at the next page boundary.
|
||||
- The duplicate/no-insert frees remain token-sliced:
|
||||
|
||||
```text
|
||||
cache_finished_req:
|
||||
kv_indices[req.cache_protected_len : new_prefix_len]
|
||||
kv_indices[req.cache_protected_len : len(keys)]
|
||||
|
||||
cache_unfinished_req:
|
||||
kv_indices[req.cache_protected_len : new_prefix_len]
|
||||
```
|
||||
|
||||
- With CP page allocators, freeing any loc inside a page can release the whole
|
||||
page. If the free range begins or ends inside a page, it can release a page
|
||||
that still contains retained current/request KV or radix-owned valid-tail KV.
|
||||
|
||||
Risk:
|
||||
|
||||
- Remaining memory-accounting or allocator/radix divergence after the C15 startup
|
||||
fix, especially on chunked/unfinished requests or insert-prefix duplicate
|
||||
paths.
|
||||
|
||||
Correction:
|
||||
|
||||
- Audit every CP call to `token_to_kv_pool_allocator.free(kv_indices[a:b])`.
|
||||
Under CP page mode, free ranges should either be page-aligned or deliberately
|
||||
converted to whole pages that are no longer referenced by radix/request state.
|
||||
- If a range is not page-aligned and cannot be proven safe, fail fast in tests
|
||||
first, then floor/ceil according to ownership semantics.
|
||||
|
||||
Tests needed:
|
||||
|
||||
- Finished insert where `req.cache_protected_len` and `new_prefix_len` differ
|
||||
inside the same page.
|
||||
- `is_insert=False` finished path with a valid-tail key.
|
||||
- `cache_unfinished_req()` duplicate-free path with a non-page-aligned protected
|
||||
prefix.
|
||||
|
||||
## C18: `cache_protected_len` now mixes valid-token and physical-page semantics
|
||||
|
||||
Finding:
|
||||
|
||||
- CP valid-tail matching can expose `req.cache_protected_len` as a valid-token
|
||||
length.
|
||||
- Some scheduler/runtime checker paths still assert or compute as if
|
||||
`cache_protected_len` is page-aligned physical protection:
|
||||
|
||||
```text
|
||||
scheduler_runtime_checker_mixin.py::_get_batch_uncached_size
|
||||
schedule_batch.py::_evict_swa
|
||||
```
|
||||
|
||||
Risk:
|
||||
|
||||
- Exact valid-tail cache hits may fail assertions or under/over-count uncached
|
||||
tokens in memory self-check paths.
|
||||
- Even when asserts do not fire in the current CP/NSA configuration, this is an
|
||||
API ambiguity that can reappear when batch/chunked/SWA paths are combined.
|
||||
|
||||
Correction:
|
||||
|
||||
- Split request-visible fields conceptually:
|
||||
- valid protected length for scheduler/logical prompt progress;
|
||||
- physical protected length for allocator/page accounting.
|
||||
- Until that refactor is done, CP HiCache should floor exposed protected length
|
||||
to the nearest page boundary wherever allocator/page accounting consumes it.
|
||||
|
||||
Tests needed:
|
||||
|
||||
- Valid-tail CP hit through `Req.init_next_round_input()` and scheduler runtime
|
||||
memory checker.
|
||||
- Guard that SWA/page-aligned paths either remain unreachable for CP NSA HiCache
|
||||
or receive a page-aligned protected length.
|
||||
|
||||
## C19: Prefetcher creation must not gate target partial-current reuse
|
||||
|
||||
Finding:
|
||||
|
||||
- Remote ETE run `/mnt/beegfs/cjy/sglang_cp_hicache_20260529_001129.log`
|
||||
died at `2026-05-29 00:17:24` with:
|
||||
|
||||
```text
|
||||
[CP_SHARED_KV_FAIL_FAST][mla_partial_current_prefetch]
|
||||
reason=missing_prefetcher prefix_lens=[320] extend_lens=[10808]
|
||||
```
|
||||
|
||||
- The same log shows the async MLA prefetcher was intentionally not created:
|
||||
|
||||
```text
|
||||
create_skip reason=prefix_below_min prefix_pages=5 min_prefix_pages=8
|
||||
```
|
||||
|
||||
- A later run showed the inverse shape: `prefix_lens=[16320]` and
|
||||
`extend_lens=[16]` passed the prefix gate and created async prefetchers even
|
||||
though the current suffix was sub-page. So "short extend" did not request
|
||||
prefetch directly; a large cache-hit prefix made the async prefetch gate pass.
|
||||
|
||||
Corrected contract:
|
||||
|
||||
- `CpSharedKVMlaPrefetcher` / `CpSharedKVIndexPrefetcher` mean async
|
||||
next-layer prefetch only. If async prefetch is disabled, below threshold, or
|
||||
otherwise not worthwhile, do not create a prefetcher object.
|
||||
- Target MLA partial-current reuse must not depend on prefetcher existence.
|
||||
When there is no prefetched prefix handle, `nsa_backend.py` synchronously
|
||||
materializes the page-aligned prefix into the slot-dense buffer, all-reduces
|
||||
that prefix range, then splices the current KV rows into their suffix page
|
||||
slots with tail slack mapped to `-1`.
|
||||
- Draft/EAGLE cache-hit partial-current reuse remains disabled until the draft
|
||||
KV lifetime has its own same-layer contract. This does not affect target
|
||||
partial-current reuse or cache-miss current-only reuse.
|
||||
- The disabled path is only the old compact materialize/current merge fallback;
|
||||
it can expose padded tail slack. The synchronous page-slot compose path is
|
||||
the fallback-free current-layer contract when no async prefetcher exists.
|
||||
|
||||
Tests:
|
||||
|
||||
- Target cache-hit partial-current with `cp_shared_kv_mla_prefetcher=None` must
|
||||
still return true from `should_reuse_current_extend_kv()`.
|
||||
- `materialize_prefix_and_reuse_current_kv_page_slots()` covers the no-prefetcher
|
||||
synchronous compose path and verifies tail slack maps to `-1`.
|
||||
- Tiny-extend batches with large prefixes should skip async MLA/index prefetcher
|
||||
creation before touching pool getters/remap builders.
|
||||
|
||||
## C20: Prefetch disabled must not disable full current reuse
|
||||
|
||||
Finding:
|
||||
|
||||
- Cache-miss/current-only requests (`extend_prefix_lens_cpu=[0]` and
|
||||
`seq_lens_cpu == extend_seq_lens_cpu`) do not need prefix materialization or a
|
||||
prefetcher: they can reuse the full current forward KV directly.
|
||||
- Conflating "no prefetcher" with "no current reuse" regresses warmup,
|
||||
cache-miss, and tiny-prefix traffic by forcing unnecessary full materialize or
|
||||
rebuild work.
|
||||
|
||||
Risk:
|
||||
|
||||
- Performance regression on no-cache or tiny-prefix traffic. This is especially
|
||||
visible before HiCache warms up or when a prompt falls below the MLA prefetch
|
||||
threshold (`prefix_pages < min_prefix_pages`, currently 8 pages by default for
|
||||
`cp_size=8,page_size=64`).
|
||||
|
||||
Correction:
|
||||
|
||||
- Keep `should_reuse_current_extend_kv()` structured as:
|
||||
- `current_only` is sufficient for full current reuse;
|
||||
- target `partial_current and not current_only` is also sufficient, regardless
|
||||
of prefetcher existence, because the backend has synchronous page-slot
|
||||
compose;
|
||||
- draft/EAGLE `partial_current and not current_only` remains rejected.
|
||||
- Tests include both target partial-current/no-prefetcher and target
|
||||
current-only/no-prefetcher coverage.
|
||||
|
||||
## Testing requirements / status
|
||||
|
||||
Targeted coverage needed for this contract:
|
||||
@@ -1082,3 +1357,87 @@ Targeted coverage needed for this contract:
|
||||
- Do not add a hot-path CP collective to reconcile capacity.
|
||||
- Do not make draft KV manage an independent padded/valid contract.
|
||||
- Do not silently fall back from the page-aligned contract.
|
||||
|
||||
## C21: Large prefix with tiny extend must not enter MLA/index prefetch
|
||||
|
||||
Finding:
|
||||
|
||||
- The current prefetch creation gate is prefix-sized: it skips MLA/index
|
||||
prefetch only when `prefix_pages < min_prefix_pages`.
|
||||
- The 2026-05-29 remote hang did not violate that gate. The last batch before
|
||||
detokenizer health failure had:
|
||||
- `prefix_lens=[16320]` (`255` pages at `page_size=64`), so the prefix gate
|
||||
allowed prefetch;
|
||||
- `extend_lens=[16]`, less than one page;
|
||||
- `has_mla=True has_index=True`, followed by no further forward-layer progress
|
||||
and detokenizer timeout.
|
||||
- Earlier batches with `prefix_lens=[320]` (`5` pages) correctly logged
|
||||
`create_skip reason=prefix_below_min ... min_prefix_pages=8` and produced
|
||||
`has_mla=False has_index=False`; those are not the observed hang pattern.
|
||||
- In other words, short extend did not directly request prefetch. Cache hit
|
||||
converted most of the request into a large cached prefix, and the large prefix
|
||||
satisfied the prefetch gate while leaving only a sub-page current suffix.
|
||||
|
||||
Root-cause analysis:
|
||||
|
||||
- MLA/index prefetch is currently a next-layer optimization. It starts prefix
|
||||
materialization/reduce on a separate stream from layer prepare hooks, then the
|
||||
consumer waits on the event later.
|
||||
- Partial-current reuse uses the MLA prefetcher as a page-slot compose contract:
|
||||
prefetched prefix rows are reused and the fresh current suffix KV is inserted
|
||||
into the padded tail page.
|
||||
- With `extend_len < page_size`, the suffix page has exactly one CP owner lane
|
||||
and many rank-local zero-owned cases. The current layer is also short enough
|
||||
that async prefetch collective ordering and event waits are no longer hidden by
|
||||
compute. A mismatch in next-layer async collective order or event completion
|
||||
can present as a CUDA/NCCL wait rather than a Python exception.
|
||||
- The observed symptom matches this: after `has_mla=True has_index=True` for
|
||||
`prefix_lens=[16320], extend_lens=[16]`, no later forward probe appears and
|
||||
the server only reports detokenizer heartbeat timeout.
|
||||
|
||||
Risk:
|
||||
|
||||
- Tiny cache-hit suffixes can pay prefetch launch/materialize/collective overhead
|
||||
that cannot be amortized by the short extend.
|
||||
- More importantly, sub-page suffixes exercise the still-sensitive padded-tail
|
||||
partial-current path. Until that path is proven by ETE, allowing MLA/index
|
||||
prefetch on `extend_len < page_size` is a correctness/hang risk.
|
||||
|
||||
Correction:
|
||||
|
||||
- Do not disable partial-current reuse just because `extend_len` is short.
|
||||
Short cache-hit suffixes are exactly where avoiding suffix materialization is
|
||||
valuable.
|
||||
- Add a separate minimum-extend gate only for the async next-layer prefetch
|
||||
strategy. When `extend_len` is below the async threshold, do **not** create an
|
||||
MLA/index prefetcher object; a prefetcher object now means async next-layer
|
||||
prefetch machinery exists.
|
||||
- Target partial-current reuse is independent of prefetcher existence. Without
|
||||
a prefetched prefix handle, the backend takes the synchronous page-slot compose
|
||||
path: materialize/reduce the page-aligned prefix in the current layer and then
|
||||
splice current KV rows into their padded suffix page slots.
|
||||
- Full current reuse is also independent of prefetcher existence. Cache-miss
|
||||
current-only batches keep using the forward KV directly even when async
|
||||
prefetch is disabled, skipped by threshold, or not created.
|
||||
- If partial-current reuse is intentionally rejected, for example the current
|
||||
conservative draft/EAGLE cache-hit guard, that rejection must not disable
|
||||
current-only/full-current reuse for other request shapes.
|
||||
- Default the async gate to one page when `page_size` is known; allow an env
|
||||
override to lower or raise the threshold for experiments.
|
||||
- This must not force tiny cache-hit suffixes back to compact full
|
||||
materialization.
|
||||
|
||||
Tests:
|
||||
|
||||
- Add runtime helper coverage for the min-extend threshold default and override.
|
||||
- Add MLA/index prefetch creation tests where `prefix_pages >= 8` but
|
||||
`extend_len < page_size`; both prefetchers should return `None` before
|
||||
touching pool getters/remap builders, while current reuse remains available.
|
||||
- Add a synchronous no-prefetch compose test proving sub-page current suffix rows
|
||||
are filled while tail slack in the current page is masked.
|
||||
|
||||
Verified:
|
||||
|
||||
- Remote container `/sgl-workspace/sglang-tai` on `g0034`:
|
||||
- `test/registered/unit/mem_cache/test_cp_shared_kv_runtime.py`: `73 passed`
|
||||
- `test/registered/unit/mem_cache/test_cp_hicache_metadata.py`: `90 passed`
|
||||
|
||||
Reference in New Issue
Block a user