Skip to content

feat(pflash): auto skip-park residency with VRAM estimate and OOM fallback - #747

Draft
Graffioh wants to merge 31 commits into
Luce-Org:mainfrom
Graffioh:devin/pflash-skip-park-auto
Draft

Graffioh wants to merge 31 commits into
Luce-Org:mainfrom
Graffioh:devin/pflash-skip-park-auto

Conversation

@Graffioh

@Graffioh Graffioh commented Sep 20, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Stacked on #746. The branch merges #746's current head (4d011e68f) and then main (85a25c4f0). The commits specific to this PR are dc219e6f (the feature) and 847c26e3 (review fixes). Once #746 merges, the diff shrinks to those two commits.

--prefill-skip-park becomes tri-state auto|on|off. The default is auto, and the bare flag still means on. 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_park in placement/skip_park_guard.h runs once in server_main.cpp, after the backend has loaded. inspect_drafter_footprint reads the drafter's GGUF header and follows the buffers the Qwen3.5 strict scorer allocates:

    • PFLASH_DRAFTER_SESSIONS scoring sessions (default 2). Each is sized S + S/2 + 4096 and holds the KV of blocks 0..14 plus f32 block-15 keys.
    • Activations.
    • The [S, nq, n_head] logits of the scorer query window. nq comes from PFLASH_SELECT_QUERY_TOKENS and 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 honors LUCE_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 explicit on runs no estimate, so it never changes the drafter's residency.

  • Precedence. off wins over on, and on wins over auto. Remote-IPC and upstream-proxy drafters always resolve to off.

  • VMM guard, now scoped. The <32 GiB && max_ctx > 64K → park guard (1c562eb) protects against CUDA's VMM pool: dual residency fragmented its address space and cuMemSetAccess failed on a 24 GB card at 128K. It now applies only when the GPU backend reports a VMM pool (its ggml_backend_get_features list has no NO_VMM). HIP builds default to GGML_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 explicit on.

  • Fail-safe, OOM only. The qwen35, deepseek4 and qwen3 compress paths share run_skip_park_window().

    • Only a no-park window that ran out of device memory is retried with parking. Out-of-memory covers failures to allocate the drafter's weights, cache, sessions, activations or graphs, and computes that return GGML_STATUS_ALLOC_FAILED. These now report through set_last_oom_error() and CompressResult::out_of_memory.
    • Every other failure is returned unchanged. For example, non-finite Qwen3.5 scoring-head scores does not trigger a pointless parked retry.
    • After an OOM recovers, parking is no longer latched for the server's lifetime. The next 4 windows park, doubling on each repeat up to 64, and then skip-park is tried again. A clean no-park window resets the backoff.
    • While an OOM is unresolved, the drafter is released after each window even under keep-loaded.
  • 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.pflash gains skip_park_mode, skip_park_estimate_bytes, skip_park_free_bytes and drafter_keep_loaded. The disabled-PFlash /props body now includes every field the schema requires.

Evidence so far

Frozen gate binary, --prefill-skip-park forced on, Qwen3.5-0.8B drafter, R9700 (gfx1201):

run parked skip-park notes
dev-qa-30 @32k, median TTFT 11.7 s 7.5 s (−36%) identical selections and answers (26/30 both); peak VRAM 20.1 of 34.2 GB
single 120K smoke, TTFT 59.3 s 55.2 s

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.sh and it runs:

  • dev-qa-30 @32k with auto, off and on: TTFT, answer and selection equality, the startup decision, drafter loads per request, and peak VRAM sampled every 2 s.
  • 5 niah cases @120000 and 4 code-debug cases @98304 with on forced (no guard on HIP), auto and off: whether anything OOMs, peak VRAM, TTFT.

This description will be updated with the numbers.

Remaining risks

  • The target-prefill reserve for keep-loaded is the codebase's 1.5 GiB convention, not a value measured per target. The scheduled runs record peak VRAM during target prefill with the drafter resident, to check it.
  • The fallback only catches failures the code reports. Compute-pool allocations inside ggml-cuda/hip (FA/cuBLAS scratch) abort the process instead of returning an error, so an estimate that is too low can still crash rather than fall back.
  • The guard stays active on CUDA VMM builds. Whether a CUDA build with GGML_CUDA_NO_VMM and ≥ 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:
    • the guard applies only with a VMM pool (R9700 total VRAM)
    • keep-loaded comes only from auto
    • the estimator on synthetic Qwen3.5 GGUF headers: query-window term, sessions, defaults, non-Qwen3.5 rejected
    • a non-OOM failure does not retry
    • an OOM retries once with parking
    • the park backoff re-probes, doubles, caps and resets
  • R9700 validation above (scheduled)

Generated with Devin, review fixes with Claude Code

🤖 Generated with Claude Code

Graffioh and others added 4 commits September 19, 2026 14:08
…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>
Graffioh and others added 25 commits September 20, 2026 10:43
… 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>
Graffioh and others added 2 commits September 24, 2026 12:10
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant