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>
Graffioh
force-pushed
the
codex/pflash-segment-probe-experiment
branch
from
September 19, 2026 14:16
07267f9 to
3814457
Compare
This was referenced Sep 20, 2026
Closed
… 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>
Graffioh
added a commit
to Graffioh/lucebox-hub
that referenced
this pull request
Sep 24, 2026
…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>
Graffioh
added a commit
to Graffioh/lucebox-hub
that referenced
this pull request
Sep 24, 2026
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>
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.
Draft. Runtime changes for PFlash on the Qwen3.5-0.8B drafter: a strict token-budget selector over variable-length segments, a segment probe and an optional scoring head loaded from GGUF, and a chat-first scorer query derived from the rendered prompt. The previous Qwen3-0.6B path is untouched and remains the default.
What changes
Qwen3.5-0.8B drafter (
qwen35_drafter.cpp/h,qwen35_loader.cpp, new;qwen3_drafter.cppkeeps the Qwen3-0.6B path and dispatches onDrafterArch; shared helpers inqwen3_drafter_common.cpp/h)--prefill-drafteraccepts a Qwen3.5-0.8B GGUF (selected byqwen3.5/qwen35in the file name). The drafter runs only its first fifteen blocks and scores the context with block 15's NoPE Q/K projections — the same attention-mass scorer the Qwen3-0.6B block-13 head uses — on the model's native 262K context, so inputs beyond the 0.6B drafter's 32K window can be scored.PFLASH_SCORING_HEAD_GGUFloads a replacement block-15 Q/K head (GGUF architecturepflash_scoring_head, keysscoringhead.*, schemaqwen3_5_0_8b_nope_qk_mass_v1); the file is bound to the drafter by sha256 and refused otherwise.PFLASH_QWEN35_LEGACY_SCORER=1keeps the all-layer running-max scorer available on this drafter.Strict budget selection (
pflash_selection.cpp/h, new)PFLASH_SELECT_MODE=budget_only|top_pfills an exact token budget from the keep ratio /--prefill-curve(or a cumulative-mass cutoff), over fixed chunks (PFLASH_SELECT_CHUNK_SIZE, scheduled by input length when unset) or probe segments.PFLASH_SELECT_SCORE=auto|sum|densityranks candidates by total mass or mass density;autopicks density with probe segments, sum with fixed chunks.stop=budget_reached|mandatory_query_exceeds_budget|…and the re-encode is retried when the rendered prompt overshoots the ceiling.PFLASH_SELECT_SCORER=head|legacy|split,PFLASH_SELECT_SPLIT): the head ranks first, the all-layer scorer fills the remainder.Segment probe (
PFLASH_SEGMENT_PROBE_GGUF)qwen3_5_0_8b_segment_probe_v1; threshold, min/max segment length and conv taps come from the GGUF metadata, sub-unit head for the oversize split rule). The context is cut at boundaries above threshold plus the query start and instruction-span edges, and the selector ranks whole functions, classes, files or paragraphs instead of fixed windows, skipping segments that do not fit the remaining budget. Falls back to fixed chunks when fewer than four boundaries are found.PFLASH_SELECT_SEGMENTS=auto|fixed|probeoverrides.Chat-first scorer query (
http_server.cpp/h,prefix_cache.cpp/hmarkers)<|im_start|>…<|im_end|>, DeepSeek<|User|>/<|Assistant|>, Gemma<|turn>…<turn|>, Laguna<user>…</user>) rather than by message bookkeeping. Marker strings are resolved on the target tokenizer and searched in the decoded prompt, so a drafter whose vocabulary lacks the control tokens still maps correctly.<|im_start|>assistant\n<think>\n,<|Assistant|></think>,<|turn>model\n<|channel>thought…). It is never the query. Tool output wrapped in a user turn (<tool_response>,<tool_result>) doesn't count as the user's turn.PFLASH_SELECT_QUERY_TOKENS) of the latest user turn scores as a second query window at the same weight: the last token reads the whole request, the user's own tokens match literal strings (an identifier, a described function) the last token does not carry. On SCBench's multi-turn chats (36 chats, 171 turns, Jev/SCBench metrics) this answers 135 turns against 112 for the last token alone, 127 with SCBench's question sent aspflash_query, and 140 for full prefill.<|im_start|>user\n) are kept; the latest user turn follows the same skeleton rule as every other turn (whole when short, otherwise scored). A prompt without chat markers scores the tail (PFLASH_SELECT_QUERY_TOKENS, default 8) of its content.PFLASH_SELECT_QUERY_PARSER=arbitrary_tailis the default;latest_userremains selectable for benchmark comparisons. Marker-less prompts andlatest_userstill find the boundary through sentinel re-renders.pflash_querystays a benchmark-only override. It replaces the derived query, is mapped against the decoded prompt text (BPE merges such asWhatstay inside the span), and is kept whole, with the scorer consuming only its bounded tail.pflash_requiredspans are kept verbatim under compression.render_chat_templatefor all four families, with thinking on and off, instead of hand-written strings.PFLASH_TRACE_PATH, JSONL): input ids, segments, per-candidate scores, selected chunks, forced chunks, budget, mass, parser record (input kind, selection rule, parser name, fingerprints, content/query bounds).Budget over droppable tokens
kept + keep_ratio × (input − kept). Kept means the system and developer messages, tool definitions, the query with its turn's envelope, and the generation prompt, counted with the selector's own mandatory-chunk rule. Both the selector budget and the target-token ceiling follow it. Before, a system prompt larger thankeep_ratio × promptfailed the request.--prefill-thresholdwith the droppable tokens.Multi-turn ownership and views
disk_cache_policy.compress) is still rejected. Without a selection environment the legacy path is unchanged: continuations go to FlowKV and full prefix-cache restores stay enabled.PreparedPrompt::snapshot_cut); the default second-to-last boundary lands on the system prompt of a first turn.CompressResult::kept_spans) are recalled as excerpts at the start of the new user turn, after everything cached, so the view always contains the fresh selection. Agent steps (assistant call plus tool output) append without recalling.PFLASH_CHAT_RECALL_MIN_LIFT(default 2; lift = mass per token relative to uniform attention); there is no token cap. A question that needs more than a third of a fresh selection rebuilds the view from it instead. On the development chats (Jev-judged): gate 2 212/252 against 200 at 4 and 197 at 8 on the query-layout chats (full prefill 231), 37 against 37 of 42 on the original chats, ~190 recalled tokens per turn, later-turn TTFT 1.01 s (full prefill 1.42 s). Non-finite head scores (seen once, not reproduced) rescore the prompt from scratch before failing.PFLASH_CHAT_VIEW=0serves the fresh compression every turn.Dialogue skeleton and history queries
PFLASH_CHAT_SKELETON_TOKENS(default 256) stay whole. Like developer text, the skeleton is scored as context when it alone would not fit.PFLASH_CHAT_HISTORY_QUERIES(default 3) earlier user turns score the context alongside the query (CompressRequest::history_query_spans), their masses mixed in at weights 1/2, 1/4, 1/8. Drafter sessions keep the rows of up to 8 recent query windows.usage.timings.pflashDrafter scoring sessions
PFLASH_DRAFTER_SESSIONS(default 2) per-conversation sessions: a blocks-0..14 cache, the block-15 NoPE keys, the probe's raw logits, the last query window's rows, and a recurrent-state checkpoint 64 tokens before the prompt end.--draft-residency persistent(plus--prefill-skip-parkwhere both models fit).Adaptive keep ratio (
adaptive_keep_ratio.h): a new session starts from the configured ratio for its prompt length (--prefill-keep-ratio/--prefill-curve) instead of a fixed 10 %; the DFlash-acceptance controller then moves it within 2.5–20 %.IPC daemon (
pflash_drafter_ipc*.cpp/h,pflash_drafter_ipc_daemon.cpp,pflash_types.h,model_backend.h): drafter architecture, probe and head paths and the selection config travel over the drafter IPC;pflash_daemonruns the drafter standalone.Tests:
test_pflash_selection.cpp(selection modes, mandatory spans, density vs sum, probe segments, fallback, parser defaults),test_server_unit.cpp(chat-tail span extraction for Qwen/DeepSeek/open-ended/marker-less prompts, query mapping, required text, strict continuation ownership, FlowKV fallback, trace),test_pflash_drafter_ipc.cpp(config transport),test_bandit_integration.cpp.Not in this PR
ggml_flash_attn_ext; the block-sparse FlashPrefill kernels still dispatch head dimension 128 only.Known limitations
pflash_queryfar from the end of its own user turn makes the kept suffix exceed the budget, and the request fails the same way.🤖 Generated with Claude Code