fix(server): prefix-cache deepening (stop pinning the restore point + stop truncating at stray end-markers) - #665
Conversation
There was a problem hiding this comment.
All reported issues were addressed across 4 files
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
deb0631 to
cabce2f
Compare
There was a problem hiding this comment.
1 issue found across 4 files
You’re at about 90% of the monthly reviewed-line limit. You may want to disable incremental reviews to conserve quota. Reviews will continue until that limit is exceeded. If you need help avoiding interruptions, please contact contact@cubic.dev.
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="server/src/server/prefix_cache.h">
<violation number="1" location="server/src/server/prefix_cache.h:53">
P3: When `skip_index` is set and every leaf is protected, the implementation returns `-1` rather than the oldest protected leaf; qualify this fallback as applying only without a restore source.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
Independent validation on AMD / ROCm (gfx1201)We hit this same livelock in a long ds-harness coding session and worked it One useful additional data point is that your verification is on RTX 3090s, Setup
Results
The protected pin behaves as intended: the log shows For what it's worth, we independently wrote a fix with one difference: our One reproducer caveat that cost us a cycleOur first end-to-end reproducer passed against the buggy binary. Ten turns We made both explicit parameters for that reason. Happy to contribute the Thanks for the fix — it takes a real coding-agent session from unusable back to |
|
@michele1967lux thanks for testing and reporting it. Good to know that I am not the only who suffered from this issue. I guess I really push the agents and encounter these issues. |
e3befe7 to
49f851b
Compare
There was a problem hiding this comment.
All reported issues were addressed across 4 files (changes from recent commits).
You’re at about 94% of the monthly reviewed-line limit. You may want to disable incremental reviews to conserve quota. Reviews will continue until that limit is exceeded. If you need help avoiding interruptions, please contact contact@cubic.dev.
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
This fork exists only to host two branches from an independent AMD/ROCm evaluation. It is not a maintained alternative to upstream. States up front that the linear prefix-cache livelock was independently reproduced and diagnosed here, and that upstream PR Luce-Org#665 by @jkyamog, opened two days earlier, had independently identified essentially the same root cause and fix. Our branch is retained for reproducibility, not as a claim of priority. Also records a retraction: the justification we gave for our fallback differing from Luce-Org#665 -- that it avoided orphaning a live side branch -- is wrong, since every cache entry is a self-contained snapshot. Only addition to main; no upstream file is modified.
With --prefix-cache-slots 4 and a long linearly-growing tool-heavy conversation, all slots form one chain and the only leaf is the deepest slot = the current restore source. select_inline_evict_victim() evicted that leaf and prepare_inline_snap() assigned the new snapshot to the same slot; the prepare_generation_cache guard (snap_slot == cache_slot) then cancelled it, pinning the restore point at the deepest slot while per-turn prefill grew unbounded (observed 1s -> 60s on the 3090s). Thread the restore-source slot into victim selection: - select_inline_evict_victim(..., int skip_index): the in-flight restore source is never a victim. - prepare_inline_snap(..., int restore_source_slot): http_server passes cache.cache_slot when using_restore, else -1. - Fallback: if skipping the restore source leaves no unprotected leaf, evict the shallowest non-protected ancestor instead (its KV is subsumed by every deeper entry); never the protected tools pin, never the restore source. If nothing safe remains, skip the snapshot, and clear pending_protect_ on the refusal path so it cannot leak into a later confirm_inline_snap. - The free-slot path (below capacity) skips the restore source too: round-robin could otherwise hand out the restore slot itself, and the guards would cancel the reservation, leaving the restore point pinned despite a vacancy. The new snapshot now lands in a different slot, so the restore point slides forward each turn. Adds five unit tests: linear-chain eviction picks an ancestor and commits to a different slot; restore source never evicted; protected pin never evicted; branching victim selection unchanged; free-slot allocation skips the restore source. The select_inline_evict_victim header also qualifies the protected-leaf last-resort with the skip_index < 0 precondition (the implementation makes it conditional; the previous wording read as a global rule).
e9c4b0e to
ee398df
Compare
There was a problem hiding this comment.
All reported issues were addressed across 2 files (changes from recent commits).
You’re at about 98% of the monthly reviewed-line limit. You may want to disable incremental reviews to conserve quota. Reviews will continue until that limit is exceeded. If you need help avoiding interruptions, please contact contact@cubic.dev.
Tip: Review your code locally with the cubic CLI to iterate faster.
Re-trigger cubic
8cf4d1d to
94a1340
Compare
…n content find_all_boundaries() walks the prompt looking for end-of-message markers followed by a role start within 5 tokens. When an end marker had no following role start — stray chatml tokens embedded in message content (file dumps, terminal output, model-echoed markers) — the walk broke out of the loop, truncating the boundary list at the first stray. Every real boundary after it stayed hidden, which pinned the inline-snapshot deepen target (the second-to-last boundary) at the already-restored prefix length: no snapshot was ever deepened and every turn re-prefilled the entire conversation tail. Skip the stray marker and keep scanning instead of truncating. Boundaries inside echoed content are still suboptimal cut points, never incorrect ones — any prefix cut is a valid KV restore point, and the stable system-head protection keys off candidates.front(), which precedes any content. A consecutive-stray cap (8192, reset on each real boundary) bounds pathological input and marker-family mismatches. Also log from prepare_inline_snap when the deepen target does not pass the restored prefix (target_cut <= 0), so a truncated boundary list is visible in the logs rather than a silent full re-prefill. That path is an expected no-op for single-turn or cache-primed prompts and can be reached twice per request, so the message is logged once per process instead of per call. Adds two unit tests: a stray end marker mid-content no longer hides later boundaries, and a clean prompt produces the identical boundary list as before the change. (Also corrects an off-by-one token count in the first test's layout comment.)
94a1340 to
4e53532
Compare
ee398df7snap_slot == cache_slotguard, so per-turn prefill grew unbounded (observed 1s -> 60s on the 3090s)4e53532find_all_boundaries()truncates the boundary walk at a stray end-of-message marker embedded in content, so no boundary is recorded past it and the inline snap has nothing to deepen toThey fail for different reasons: one cancels the snapshot (eviction), the other
leaves no candidate boundary to snapshot (truncation). Both must be fixed for
the restore point to keep sliding forward turn after turn.
4e53532— don't truncate boundaries on stray end-markers in contentfind_all_boundaries()walks the prompt for end-of-message markers. On anend-marker not followed by a role-start within the 5-token probe window it
breaked, cutting the list off at the first stray. Content that legitimatelycontains a stray end-marker (file/log dumps, chatml-in-content, tool output)
therefore lost every real message boundary after it. Fix: skip the stray marker
and keep scanning (capped at 8192 consecutive strays to bound pathological
input); clean prompts are byte-for-byte unchanged.
prefix_cache.cpp+28 -2;2 regression tests —
test_find_boundaries_stray_end_msg_does_not_truncate(
{5,15,19}) andtest_find_boundaries_clean_prompt_unchanged({6,10,14});plus a one-line diagnostic on the
prepare_inline_snapblocked path.ee398df7— slide the restore point past the deepest slotWith
--prefix-cache-slots 4and a long linearly-growing tool-heavyconversation, all slots form one chain and the only leaf is the deepest slot =
the current restore source.
select_inline_evict_victim()evicted that leaf andprepare_inline_snap()re-assigned the snapshot to the same slot; theprepare_generation_cacheguard (snap_slot == cache_slot) then cancelled it,pinning the restore point. Fix: thread the restore-source slot into victim
selection so it is never evicted (the snapshot lands in a different slot, so the
restore point slides forward each turn), with a shallowest-ancestor fallback
that never touches the protected tools pin or the restore source. 5 unit tests.
Verification
test_server_unit: 441/441 pass (newlucebox-hub:latest).find_all_boundaries(): stray ->[5,15,19],clean ->
[6,10,14], deepseek ->[8].deepens the inline snap to
prefix_len=684(past the marker) and the nextturn HITs it —
lookup hit slot=3 prefix_len=684,cache_hit=true,