server: extend live-prefix rewind to Flash on Metal, bounded by raw-cache budget - #710
Open
Flor1an-B wants to merge 1 commit into
Open
server: extend live-prefix rewind to Flash on Metal, bounded by raw-cache budget#710Flor1an-B wants to merge 1 commit into
Flor1an-B wants to merge 1 commit into
Conversation
…ache budget antirez#668 added live-KV rewind for repeated exact-prefix prompts, but gated it to GLM only (ds4_engine_is_glm_dsa()) -- DeepSeek V4 Flash on Metal still does a full cold prefill on a retry/regenerate, even when the incoming prompt is an exact token-prefix of the live session, per antirez#698. Root cause of the GLM-only gate (confirmed by reading the encode path, not guessed): Metal's raw SWA cache is a ring buffer, `raw_row = pos % raw_cap` (metal_graph_encode_token_raw_swa()). ds4_session_rewind() only truncates checkpoint.len; it does not shift or revalidate that ring. GLM's dense KV cache has no such ring -- ds4_session_glm_cap_dense_cache() keeps it consistent unconditionally -- but naively enabling rewind for Flash risks reusing a raw row that the discarded continuation has since overwritten, silently conditioning the resumed generation on the wrong tokens. Fix: new ds4_session_raw_rewind_budget() returns 0 for GLM (unaffected, gated separately as before) and, for Flash, (raw_cap - raw_window) -- the margin the graph already reserves so chunked prefill doesn't evict window-relevant rows mid-chunk. The rewind is only allowed when the discarded tail (old_pos - prompt_len) stays under that budget, which guarantees no raw row the rewound tail depends on has wrapped. ## Empirical validation (not just the theoretical argument above) M5 Max, Metal, production Flash q2-q4 quant: 1. Positive control: real ctx=8192 session, ~1000-token prompt, resend the exact same prompt after a short generation -> log shows "rewound Flash live prefix from N to M", continuation is byte-identical to the first generation. 2. Negative control: forced raw_cap=300 via DS4_METAL_GRAPH_RAW_CAP (so the budget is a easy-to-exceed 44 tokens), *with the budget check removed* to confirm the failure this guards against is real, not hypothetical -- generated 400 tokens (>> budget), resent the same prompt: continuation corrupted into a degenerate "140,140,140,..." repetition loop instead of the correct count, while the unmodified first generation was correct. 3. Same forced raw_cap=300, *with* the guard: the identical over-budget scenario correctly falls through to a full re-prefill (`reason=token-mismatch`) instead of rewinding, and the output stays correct -- confirming the guard degrades to today's existing (slower but correct) behavior exactly when it should, and a short within-budget retry under the same forced raw_cap still rewinds and matches. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #698.
Background
#668 added live-KV rewind for repeated exact-prefix prompts (retry/regenerate workflows), but gated it to GLM only via
ds4_engine_is_glm_dsa(). #698 reports that DeepSeek V4 Flash on Metal still does a full cold prefill in the same scenario, even when the incoming prompt is an exact token-prefix of the live session (e.g. a 175K-token prompt re-prefilled from scratch after a client-side stream abort).Why the gate exists (this isn't a small oversight)
Read
metal_graph_encode_token_raw_swa()rather than guessing: Metal's raw SWA cache is a genuine ring buffer,raw_row = pos % raw_cap.ds4_session_rewind()only truncatescheckpoint.len— it does not shift or revalidate that ring. GLM's dense KV cache has no such ring (ds4_session_glm_cap_dense_cache()keeps it consistent unconditionally on rewind), but naively enabling rewind for Flash risks reusing a raw row that the discarded continuation has already overwritten — silently conditioning the resumed generation on stale/wrong tokens. That's a correctness bug, not just a missed cache hit, so I didn't want to just flip the gate.Fix
New
ds4_session_raw_rewind_budget():raw_cap - raw_window— the margin the graph already reserves specifically so chunked prefill doesn't evict window-relevant rows mid-chunk. That existing margin turns out to be exactly the safe reuse budget for this too.The rewind is only allowed when the discarded tail (
old_pos - prompt_len) stays under that budget, which guarantees no raw row the rewound tail depends on has been overwritten.Empirical validation, not just the theoretical argument
M5 Max, Metal, production Flash q2-q4 quant:
ctx=8192session, ~1000-token prompt, resend the exact same prompt after a short generation → log showsrewound Flash live prefix from N to M, continuation is byte-identical to the first generation.raw_cap=300viaDS4_METAL_GRAPH_RAW_CAP(budget becomes an easy-to-exceed 44 tokens), with the budget check temporarily removed to confirm the failure mode is real and not just theoretical — generated 400 tokens (well past budget), resent the same prompt: continuation corrupted into a degenerate140,140,140,...repetition loop instead of the correct count, while the unmodified first generation was correct. Same forcedraw_cap, same over-budget scenario, with the guard in place: correctly falls through to a full re-prefill (reason=token-mismatchin the log) instead of rewinding, and stays correct.raw_cap=300still rewinds and matches, confirming the guard isn't just conservative to the point of never firing.Happy to re-run any of this against a different quant/ctx if useful — the scratch negative-control build was throwaway and not included here, only the guarded version in this diff.
🤖 Generated with Claude Code