Conversation
…ing head
Teach the PFlash drafter to score with a Qwen3.5-0.8B hybrid prefix instead
of only the Qwen3-0.6B tail-attention scorer. The new drafter lives in
qwen35_drafter.cpp behind qwen35_drafter_score_and_compress, its GGUF and
optional companion files load through qwen35_loader.cpp, and the pieces both
architectures share moved into qwen3_drafter_common.cpp. An optional scoring
head replaces the block-15 Q/K projections and an optional segment probe
proposes variable-length candidates instead of fixed chunks; both are GGUF
files validated against an explicit contract and fail closed when it is not
met. pflash_selection.{cpp,h} turns the PFLASH_SELECT_* environment into a
strict budget selector that ranks candidates, honours structurally required
spans and stops at the token budget.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ression trace Wire the request path to the strict selector. A request may now carry an explicit `pflash_query`, mapped to drafter tokens by the parser rules, and `required_text` spans that must survive compression; tool definitions and the instruction structure of the chat template are mapped the same way and passed to the drafter as required spans. Compression failures fail closed instead of silently falling back, and a trace records the resolved config, the mapped spans and the retained budget. The adaptive keep-ratio controller now seeds a new session from the configured curve for its prompt length rather than a fixed default. The drafter IPC gains compress2/compress3 so a remote drafter receives the unquantized keep ratio, the query window and the required spans, with one formatter and one parser shared by both ends. The server unit tests move with the query-mapping API they exercise, since the older helpers this replaces had no other callers. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Cover the strict selector's budget, ordering and required-span rules in test_pflash_selection.cpp, and the compress2/compress3 wire format — its round trips, the legacy quantized parser and its malformed inputs — in test_pflash_drafter_ipc.cpp, including a case that shows a remote drafter selects exactly what the local one would. Add parser coverage for the explicit `pflash_query` request field at both the top level and inside extra_body. Document the Qwen3.5-0.8B drafter, the two optional GGUF files and the PFLASH_SELECT_* contract in the server README. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…lback --prefill-skip-park is now tri-state (auto|on|off, default auto; bare flag keeps the historical boolean meaning = on). Once the backend has fully loaded, the server reads the pflash drafter's GGUF header, estimates its worst-case resident footprint (weights + per-token KV/activation/score buffers over min(max_ctx, drafter ctx) + fixed state) and enables skip-park when it fits the free VRAM measured on the drafter device with a 25% margin. Explicit on/off override the estimate; the <32GiB/ctx>64K crash guard still applies to every mode. Remote-IPC and upstream-proxy drafters resolve off unconditionally. Under auto+resolved-on, draft-residency=auto now also keeps the pflash drafter loaded between requests instead of releasing it — the same probe that proved co-residency makes the per-request reload pure overhead. Each typed compress path (qwen35, deepseek4, qwen3) retries a failed skip-park window once with target+draft parked and latches parking for later windows, so an underestimate fails safe instead of OOMing the request. /props.pflash gains skip_park_mode, skip_park_estimate_bytes and skip_park_free_bytes for forensics. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Closed
3 tasks
… scorer
src/qwen3/ had become a catch-all mixing three things: the Qwen3-0.6B
standalone inference backend, the legacy Qwen3-0.6B PFlash drafter, and
the Qwen3.5-0.8B PFlash scorer that replaced it. Split along the real
boundary:
- src/pflash/ now owns the whole PFlash pipeline: pflash_drafter (was
qwen3_drafter), pflash_compress (was qwen3_drafter_common),
pflash_selection, qwen35_drafter/qwen35_loader, kvflash_drafter_scorer
(was qwen3_kvflash_scorer), anchor_scan/anchor_params.
- src/qwen3/ keeps only standalone Qwen3-0.6B inference, with the
drafter terminology removed: qwen3_model.h (Qwen3Weights,
load/free_qwen3_model).
- The Qwen3-0.6B PFlash path is deleted outright: qwen3_graph.cpp,
qwen3_buffer_plan.h, score_range.h, DrafterArch dispatch, the 0.6B
scoring-head loader, and the tests that covered them. Qwen3.5-0.8B is
now the only scorer; the optional drafter_arch command arg is still
accepted but ignored.
- PFlash selection symbols move from dflash::qwen3 to dflash::pflash.
- score_query_end < 0 ("legacy tail window") is still on the
CompressRequest/IPC wire formats; backends and the IPC daemon now
translate it to input_ids.size() at the boundary since the qwen35
scorer requires an explicit end.
- Default/located drafter paths, help text, docs and scripts point at
Qwen3.5-0.8B-BF16.gguf.
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
PFlash is the whole compression concept; pflash_drafter is the scorer model behind it — Qwen3.5-0.8B for now, not permanently. Make the seam explicit in the file headers instead of implying the coupling is fixed. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
The budget selector fills a fixed fraction of the prompt by descending segment score, but on compact-evidence traffic the evidence sits in the first few ranks, so a rank rule reaches it for far fewer tokens. TopK keeps the mandatory candidates and then the K highest-scoring optional ones in score order, with the token budget still a hard ceiling and oversized candidates skipped exactly as budget_only does, and stops with top_k_reached when K binds before the budget. PFLASH_SELECT_TOPK is required and validated in that mode, the config line and the compression trace both carry the K that applied, and split selection rejects the mode rather than quietly keeping up to 2K segments. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Per-segment density ranking throws away what the head already knows at document level: on BRIGHT the gold document ranks first by aggregated mass in 16 of 28 prompts while its median gold segment ranks 68 of 215. PFLASH_SELECT_DOC_PRIOR scales an optional candidate's ranking score by its document's share of the prompt's total mass raised to the exponent, so a document the head likes as a whole lifts its own segments. Zero, the default, leaves the ranking untouched, and the prior applies in every mode, so it composes with top_k. Documents come from a new pflash_documents request field or, absent it, from the served prompt's own "Document <n>:" and [DOC-<n>] markers; fewer than three documents makes the prior a no-op, which the trace records with the exponent and the document count. The drafter IPC compress protocol carries no document spans, so the prior is inert on that path until the wire format gains them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Keeping a document's body while dropping the header that names it leaves the model unable to cite what it is quoting, and the score ranking drops headers first because they are short and low density. PFLASH_SELECT_FORCE_DOC_HEADS keeps the first segment of each of the D highest-mass documents, charged after the structurally required spans and before the fill, so a header can never starve a mandatory span and a header that no longer fits is dropped rather than fatal. It reuses the document detection the prior added and composes with the prior and with top_k. The trace and the selector log record the configured D and the headers actually forced. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Production PFlash is now chat-only: the scorer query is the last N
tokens of the final message's content, located by the rendered
prompt's own control markers (ChatMarkers) instead of benchmark
message bookkeeping. Template machinery ("<|im_start|>", role names,
"<|im_end|>", generation prompts) is never scored, and the last
turn's role header is pinned mandatory so compression keeps the role
envelope. Marker-less prompts and the latest_user parser keep the
sentinel-render path; pflash_query stays a benchmark-only override,
mapped against decoded text and pinned in full while the scorer
consumes its bounded tail.
Strict selection now owns multi-turn chit-chat: continuations run
whole-prompt PFlash on the full history plus current turn instead of
failing closed or routing to FlowKV. Request-scoped FlowKV disk
compression remains the only rejected combination; unconfigured
requests keep the legacy FlowKV continuation path.
Generated with [Devin](https://devin.ai)
Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
PFlash scores the input prompt against the query and nothing else: no document detection, no client pflash_documents ranges, no mass prior and no forced document headers. Reverts 2455efb and 739a8c3; the decoded-offset helpers they introduced stay because the chat-tail query mapping uses them. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Every chat template appends a think or channel prefix after the
assistant generation marker ("<|im_start|>assistant\n<think>\n",
"<|Assistant|></think>", "<|turn>model\n<|channel>thought..."). The
marker-based tail extractor required only whitespace there, so on real
prompts it took the generation prompt as the last message: the scorer
query became "<think>", the assistant header was pinned instead of the
user's, and pflash_required failed to map. The legacy unconfigured
path used the same span and regressed with it.
The extractor now scans the rendered prompt's turns: an assistant turn
left open at the end is the generation prompt whatever follows its
marker, tool output wrapped in a user turn does not count, and the
query comes from the latest user turn (else the latest turn with
content). The derived query then takes the explicit pflash_query path:
its span is the scorer window, pinned under strict selection together
with the turn's role header; an explicit pflash_query still replaces
it. Tests render prompts through render_chat_template for Qwen, Gemma,
DeepSeek and Laguna with thinking on and off, instead of hand-written
strings that omitted the think prefix. The stale default-parser
assertion is updated, and the BPE tokenizer fixture now writes
per-process paths so parallel ctest runs stop overwriting each other.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
In an agent loop the latest user turn is followed by assistant and tool turns. Selection treated every token after the query window as a kept suffix and the head scorer masked those keys, so the tool output was pinned whole: a 2K-token tool result against a 0.3 keep ratio failed with mandatory_query_exceeds_budget. The prompt minus the query is now the candidate pool whatever side of the query it sits on. When turns follow the query's turn, the server pins only the rest of that turn through its closing marker and the generation prompt, and sets CompressRequest::query_suffix_candidates. The block-15 head then scores keys after the query window too (NoPE: no position term) and the selector stops treating that suffix as structural. A query in the final user turn keeps the old contract, so single-turn and plain multi-turn requests select exactly as before. The running-max scorer, alone or in the split, and the remote drafter IPC keep the kept-suffix contract. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Strict selection keeps system and developer messages, tool definitions, the query with its turn's envelope and the generation prompt, and it charged them against keep_ratio x the whole prompt. A 15K system prompt under the default 5% ratio exhausted the budget of anything shorter than 300K tokens and the request failed with mandatory_query_exceeds_budget. The server now counts the kept tokens with the selector's own mandatory-chunk rule and passes an effective ratio, (kept + keep_ratio x (input - kept) + 1) / input, so the selector budget and the target-token ceiling both add the kept part on top of the ratio's share of the rest. Auto mode compares the threshold with the droppable tokens, so a large system prompt plus a short chat is served as is. Instructions that alone would not fit the context (a document pasted into the system prompt) lose their pin and compete for the budget against the query. Live on the R9700 (27B target, 0.8B drafter, keep 0.3, 16K context): a 2K system-prompt document answers instead of failing; a 19K one compresses to 5.8K and answers correctly; the four chat shapes still answer, keeping their kept tokens on top (2237 -> 701 instead of 637). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A system prompt that alone does not fit the context now fails the request instead of losing its pin: PFlash does not compress system prompts. Developer messages and tool definitions that would not fit still lose their pin and are scored like any other context. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Strict selection re-selected the whole history on every turn, so each turn's compressed prompt diverged from the last early on and the target prefilled it from scratch; evidence the model had just used could also vanish between turns. The server now remembers the prompt it served for each conversation, matched by raw prompt prefix up to the old generation prompt (most clients send no session id). The next turn serves that view plus the new turns. The fresh compression still runs: whatever it keeps for the new question that the view lacks is recalled as excerpts at the start of the new user turn, after everything the target cached, so the view always holds the fresh selection. The view is rebuilt from the fresh prompt when it outgrows twice the fresh prompt or the context. The selector reports its kept spans (CompressResult::kept_spans) for this, and the view asks for its prefix-cache snapshot at the start of its generation prompt, where the next turn branches off; the default second-to-last boundary lands on the system prompt of a first turn. PFLASH_CHAT_VIEW=0 turns views off. Live on the R9700 (27B target, 0.8B drafter, keep 0.1, 20.6K-token conversation): three turns answer correctly, turn 2 recalls the segment turn 1 dropped, and target prefill drops from 7.7 s to 2.8 s (turn 2, 2048 tokens restored) and from 2.7 s to 0.9 s (turn 3, 3584 restored). The drafter still reads the whole history each turn. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The Qwen3.5 drafter re-read the whole prompt on every call, so every turn of a conversation paid a forward over its entire history (about 4 s at 20K tokens on the R9700) even when only the last few hundred tokens were new. Blocks 0..14 read left to right, so the cache state and the block-15 keys of a shared prefix never change, and NoPE keys carry no position. The strict scorer now keeps up to PFLASH_DRAFTER_SESSIONS (default 2) scoring sessions: a blocks-0..14 cache with headroom, the block-15 keys, the probe's raw logits, the last query window's block-14 rows, and a recurrent-state checkpoint 64 tokens before the prompt's end. A prompt that shares a session's prefix resumes from the session's end or, when it diverged before it (the previous turn's generation prompt), from the checkpoint, runs only its new tokens, and scores the query against every stored key. The query rows are reused while the query stays put (an agent step appends tool output after the same user turn). Anything else starts the least recently used session over; any failure forgets the session's prompt. Sessions live with the loaded drafter. Live on the R9700 (27B target, 0.8B drafter resident, keep 0.1, 20.6K tokens, three turns): drafter forward 3.9 s -> 0.04 s on turn 2 (resumed at 20571, 90 new) and 3.3 s -> 0.13 s on turn 3, with the same selections, served prompts and answers as scoring from scratch; turns take 3.9 s and 3.3 s instead of 8.8 s and 6.2 s. An agent step resumes at 10993 with the stored query rows and runs only the 8435-token tool output (1.9 s). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A rebuild (and every turn's fresh selection) scored the conversation against the latest question alone and could drop the dialogue itself: earlier questions, the model's own short answers, role headers. The chat-turn scan now reports every turn. Under strict selection every other turn keeps its role header, and user turns and assistant answers up to PFLASH_CHAT_SKELETON_TOKENS (default 256; 0 keeps headers) stay whole; like developer text and tool definitions, the skeleton is scored as context when it alone would not fit. The tails of the last PFLASH_CHAT_HISTORY_QUERIES (default 3) earlier user turns score the context alongside the current query (CompressRequest::history_query_spans): the head scores each window against the query's key set and mixes the masses at weights 1, 1/2, 1/4, 1/8. The drafter session keeps the block-14 rows of up to 8 recent windows, so a new turn's history windows come from the turns that computed them. Live on the R9700 (20.6K-token conversation, sessions on): turn 2 recalls 64 tokens instead of 1472 and completes in 1.0 s instead of 3.9 s; turn 3 in 3.0 s; all three answers correct. Mixing in old questions trades the new question's share of the budget, so both knobs are ablations for the multi-turn evaluation. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Benchmarks could only see PFlash's multi-turn behaviour by scraping server logs. usage.timings now carries a pflash object on compressed requests: compress time, drafter input, kept and compressed tokens, the effective keep ratio, the query rule, the history-query count, the drafter session's resume point, new tokens and forward time, and the view outcome (fresh, continue, rebuild or repeat, with served, reused, delta, recalled and fresh token counts). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…race A continuing turn appended all its new material verbatim, whatever its size, and a large paste or tool output only got compressed indirectly, by outgrowing the view and forcing a rebuild of everything. What a turn adds is now appended verbatim below PFLASH_CHAT_COMPRESS_NEW_TOKENS (default 16384), the way full prefill appends a follow-up; from there on only the parts the fresh selection keeps of the new material are appended, and the view before it stays cached (view mode "continue-compressed"). PFLASH_CHAT_RECALL=0 turns recall off, and then a small follow-up (like a repeated prompt) is served before any compression runs: no drafter, no scoring, exactly full prefill's cost on top of the cached view. The view logic is one function the server calls before compressing (for the turns that need no scoring) and after. PFLASH_VIEW_TRACE_PATH appends every compressed request's served prompt text with its PFlash details as JSONL, so an evaluation can tell evidence the selection dropped from answers the model got wrong. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
On the multi-turn development chats every PFlash failure still had its
gold evidence in the served prompt; the losses came from what surrounded
it. For a content-free follow-up ("which documents support that
answer?") recall filled its budget with 20-odd low-relevance documents
right beside the question. Segment lifts from the head (mass per token
relative to uniform attention) separate evidence (typically 20-160x)
from background (90th percentile about 1x).
The selector now reports every candidate's lift
(CompressResult::candidate_lifts), and recall takes only segments the
view lacks with a lift of at least PFLASH_CHAT_RECALL_MIN_LIFT (default
8), strongest first, up to PFLASH_CHAT_RECALL_TOKENS (default 2048
drafter tokens); without lifts it keeps the previous rule.
PFLASH_SELECT_PARAGRAPH_JOIN=1 (off by default, under test) rebuilds
the compressed text from the kept spans with a paragraph break between
pieces that were not adjacent ("...other bands.Document 1:" otherwise),
for fresh selections and compressed follow-ups; the breaks do not count
against the target-token ceiling.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…t-probe-experiment # Conflicts: # server/src/deepseek4/deepseek4_backend.h # server/src/qwen35/qwen35_backend.h
On the multi-turn development chats (7 non-held-out 32K sessions, 42 turns) the paragraph join recovered every citation turn (6/6, like full prefill, against 5/6 without it) and was never worse elsewhere; PFLASH_SELECT_PARAGRAPH_JOIN=0 turns it off. Single-turn compressed prompts change accordingly: the article's single-turn numbers were measured without it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The chat scorer query was the tail of the latest user turn, so a question before or inside a pasted document was missed. The query is now the prompt's last token -- where the model starts answering, having read the whole request -- and nothing is parsed out of the user's text: the latest turn follows the skeleton rule like the others, the generation prompt is pinned, and history queries are the last token of the header of the reply that followed each earlier user turn. Agent tool turns after the user's become ordinary context. Marker-less prompts keep the content tail; pflash_query keeps its explicit span. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The 2048-token recall cap never bound once a lift gate was on (at most ~960 tokens on the dev chats). On the development chats (Jev-judged), a gate of 2 against the previous 8: 212 vs 197 of 252 turns on the query-layout chats (question first, in the middle, among the user's sentences, two questions, emails; full prefill 231), 37 vs 37 of 42 on the original chats, at ~190 recalled tokens per turn and 1.01 s vs 0.81 s median later-turn TTFT (full prefill 1.42 s). Recall now takes every out-of-view segment at lift >= 2 (PFLASH_CHAT_RECALL_MIN_LIFT); PFLASH_CHAT_RECALL_TOKENS is gone. A question that needs more than a third of a fresh selection rebuilds the view from it instead: past that, the fresh prompt costs about the same and keeps order. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
One request in ~500 of the recall-gate development run failed with "non-finite Qwen3.5 scoring-head scores" on a fresh chat whose prompt the other arm scored cleanly; it did not reproduce. The failure already forgets the scoring session, so score the prompt once more from scratch (one drafter forward) before failing the request with a 500. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
On SCBench's multi-turn chats the prompt-end query answered 112 of 171 turns, the benchmark's explicit question 127: the gap is literal lookups (an identifier, a described function) the last token does not carry. Replaying those turns through the drafter, adding the latest user turn's tail (PFLASH_SELECT_QUERY_TOKENS) as a second query window at full weight serves the gold passage as often as the explicit query (kv follow-ups 31/32 recallable vs 5/32 from the last token alone; repoqa 40/48 vs 41/48 explicit, 28/48 last token). The last token still reads questions placed anywhere in the message. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…devin/pflash-skip-park-auto Luce-Org#747 is stacked on Luce-Org#746, which has since moved on 24 commits and already merged main through the dflash->luce rename. Merging its head first keeps the stack consistent and reuses Luce-Org#746's resolution of the rename. Conflicts: the Qwen3 compress retry lambda now calls the Qwen3.5 scorer with Luce-Org#746's query arguments; the skip-park estimator, guard tests and server_main strings take the luce:: namespace and LUCE_* env names; the README and usage keep Luce-Org#746's Qwen3.5-0.8B drafter line next to the tri-state --prefill-skip-park row. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Brings the branch up to main 85a25c4 (vision batching, omp harness test fix, gfx1151 sparse MIX tiling). No conflicts on top of the Luce-Org#746 merge. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…auto estimate, VMM-scoped guard Review fixes for the auto skip-park residency: Keep-loaded (draft-residency auto -> KeepLoaded) now comes only from the auto estimate, never from an explicit `on`, which runs no estimate. The estimate that decides it counts the drafter as resident beside the target's compute reserve (1.5 GiB, the reserve the KV pool budgets use for runtime graph buffers): the window footprint already includes everything the drafter keeps between requests -- its weights, the scoring sessions it keeps for prefix reuse, its compute pool. The estimator follows the Qwen3.5 strict scorer instead of the removed dense Qwen3 drafter: PFLASH_DRAFTER_SESSIONS sessions sized S + S/2 + 4096 (KV of blocks 0..14 plus f32 block-15 keys) and the [S, nq, n_head] logits of the scorer query window (nq up to 512, from PFLASH_SELECT_QUERY_TOKENS), where the old estimate assumed an 8-token tail. /props.pflash reports drafter_keep_loaded; the disabled-PFlash /props body now carries every skip-park field the schema requires. The fail-safe retries a no-park window parked only when it ran out of device memory. Drafter load, cache, session, activation and graph allocation failures (and GGML_STATUS_ALLOC_FAILED computes) report set_last_oom_error(), CompressResult carries out_of_memory, and any other failure -- e.g. non-finite scoring-head scores -- is returned without a useless parked retry. The permanent park latch becomes a backoff: after a recovered OOM the next 4 windows park (doubling to 64 on repeats), then skip-park is probed again; a clean no-park window resets it, and while an OOM is unresolved the drafter is released after each window even under KeepLoaded. The three backends share run_skip_park_window(). The <32GiB/ctx>64K guard (1c562eb) exists for the CUDA VMM pool's cuMemSetAccess failure under fragmentation. It now applies only when the GPU backend reports a VMM pool (ggml_backend_get_features without NO_VMM). HIP builds default to GGML_HIP_NO_VMM, so on the R9700 (34.2 GB = 31.86 GiB) the estimate decides instead of the guard; CUDA builds with the VMM pool keep it. Co-Authored-By: Claude Opus 5.5 (1M context) <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.
Summary
--prefill-skip-parkbecomes tri-stateauto|on|off. The default isauto, and the bare flag still meanson. Parking the target and decode draft around every compression window adds a flat ~4 s to TTFT on each long prompt. When VRAM is ample, that round-trip does no useful work.What it does
Startup estimate.
resolve_skip_parkinplacement/skip_park_guard.hruns once inserver_main.cpp, after the backend has loaded.inspect_drafter_footprintreads the drafter's GGUF header and follows the buffers the Qwen3.5 strict scorer allocates:PFLASH_DRAFTER_SESSIONSscoring sessions (default 2). Each is sized S + S/2 + 4096 and holds the KV of blocks 0..14 plus f32 block-15 keys.[S, nq, n_head]logits of the scorer query window.nqcomes fromPFLASH_SELECT_QUERY_TOKENSand can be up to 512.The estimate is taken over
min(max_ctx, drafter ctx). Skip-park turns on when the footprint plus a 25% margin fits the free VRAM measured on the drafter device. The estimate honorsLUCE_KV_*. TQ3 is suppressed, as it is in the drafter's own cache.Keep-loaded (auto only). With
--draft-residency auto, the drafter stays loaded between requests only when the auto estimate also fits the drafter resident next to the target's compute reserve. That reserve is 1.5 GiB, the same one the KV-pool budgets use for runtime graph buffers. The check is(window footprint + 1.5 GiB) × 1.25 ≤ free. The window footprint counts everything the drafter keeps between requests: its weights, its kept sessions and its compute pool. An explicitonruns no estimate, so it never changes the drafter's residency.Precedence.
offwins overon, andonwins overauto. Remote-IPC and upstream-proxy drafters always resolve tooff.VMM guard, now scoped. The
<32 GiB && max_ctx > 64K → parkguard (1c562eb) protects against CUDA's VMM pool: dual residency fragmented its address space andcuMemSetAccessfailed on a 24 GB card at 128K. It now applies only when the GPU backend reports a VMM pool (itsggml_backend_get_featureslist has noNO_VMM). HIP builds default toGGML_HIP_NO_VMM=ON, so on the R9700 (34.2 GB = 31.86 GiB, just under the line) the estimate decides instead of the guard. CUDA builds with the VMM pool keep the guard, including for expliciton.Fail-safe, OOM only. The qwen35, deepseek4 and qwen3 compress paths share
run_skip_park_window().GGML_STATUS_ALLOC_FAILED. These now report throughset_last_oom_error()andCompressResult::out_of_memory.non-finite Qwen3.5 scoring-head scoresdoes not trigger a pointless parked retry.Observability. A startup log line records the mode, the decision and its reason, the window and keep-loaded GiB, free GiB, the window and query tokens, and
vmm_pool./props.pflashgainsskip_park_mode,skip_park_estimate_bytes,skip_park_free_bytesanddrafter_keep_loaded. The disabled-PFlash/propsbody now includes every field the schema requires.Evidence so far
Frozen gate binary,
--prefill-skip-parkforced on, Qwen3.5-0.8B drafter, R9700 (gfx1201):The saving is a flat ~4 s per request. It does not include the keep-loaded gain: in those runs the drafter still reloaded on every request (31 loads for 31 requests).
GPU validation (scheduled)
The PR build is queued on the R9700 behind the multi-turn runs, which have GPU-0 priority today. The script is
.benchmark-work/pr747/gated-validation.shand it runs:auto,offandon: TTFT, answer and selection equality, the startup decision, drafter loads per request, and peak VRAM sampled every 2 s.onforced (no guard on HIP),autoandoff: whether anything OOMs, peak VRAM, TTFT.This description will be updated with the numbers.
Remaining risks
GGML_CUDA_NO_VMMand ≥ 24 GB is safe at >64K without the guard has not been tested.Test plan
test_server_unit(HIP gfx1201 build): 707 passed, 0 failed. New cases cover:autoGenerated with Devin, review fixes with Claude Code
🤖 Generated with Claude Code