Repository navigation
feat(pflash): Qwen3.5-0.8B PFlash runtime: scoring head, segment and request probes, strict budget selection, chat views, cache-aware admission, fallback - #746
Conversation
07267f9 to
3814457
Compare
…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>
|
Record of the unpushed selection work, 30 Sep 2026 (worktree cleanup). None of this is in this PR. Each item is now on a branch of the fork Query-head fix:
Fold selection: Adaptive top-k selection:
Other variants, now pushed instead of kept as local backups
The local worktrees ( |
555ee2a to
3123404
Compare
de826e3 to
c2de8a5
Compare
CI failed on
|
c2de8a5 to
235dbd4
Compare
…ation ggml_cuda_cpy chose the tiled transpose kernel from the source layout alone. That kernel writes the destination densely and ignores its strides, so a contiguous [1, n, m] source (ne00 == 1 also gives nb01 == element size) copied into a strided view landed in the wrong elements and left the right ones unwritten. The PFlash Qwen3.5 scorer hits exactly this when its chunked logits copy ends on a one-key-wide chunk (S % key_chunk == 1, e.g. a 65,537-token prompt): the unwritten memory held NaN and the request failed with non-finite scores. Require a contiguous src1 for the transpose path, as upstream llama.cpp does (134b2bb756, ggml-org/llama.cpp#27663). test_cpy_strided_dst runs the scorer's chunked-copy pattern and the bare [1, n, m] copy on the GPU backend against a host reference and the CPU backend, for key counts with remainders 0, 1, 2 and chunk-1, and keeps the legitimate transpose fast path covered. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… views, request probe and resumable scoring sessions PFlash compresses a long prompt by scoring it with a small drafter and serving the target only the spans that matter. This commit holds the scorer and everything the served view is built from. Drafter and selection: - The drafter is Qwen3.5-0.8B: blocks 0..14 run as a prefix and block 15's NoPE Q/K attention mass from the query window scores every key. Optional PFLASH_SCORING_HEAD_GGUF and PFLASH_SEGMENT_PROBE_GGUF companions are validated against an explicit contract and fail closed. The Qwen3-0.6B scorer is removed; src/pflash/ owns the pipeline and the IPC gains compress2/compress3. - Strict selection (PFLASH_SELECT_MODE, PFLASH_SELECT_SCORE) keeps the structurally required spans and stops at the token budget. The scorer query comes from the latest user turn or the pflash_query field; pflash_required pins client spans; PFLASH_TRACE_PATH writes a record per compression. - The causal mask is built in the graph, head+tail query windows (PFLASH_SELECT_QUERY_HEAD) and line-aligned splits are added, and a fold selection (PFLASH_SELECT_STRATEGY=fold) stays an off-by-default experiment with pflash_fold_replay. Query: - A request probe (PFLASH_REQUEST_PROBE_GGUF, PFLASH_SELECT_QUERY_PROBE=1) picks the scorer query from the human user messages, also on the chat parser. An optional stage 2 look-back (PFLASH_REQUEST_STAGE2_GGUF) and relative query candidates with a verbatim pin (PFLASH_QUERY_CANDIDATES, PFLASH_QUERY_PIN) refine it. - An identifier query stays as an evaluation reference arm, the server validates the selection config at startup, and PFLASH_VIEW_TRACE_PATH records the served view with its provenance. Parity and capture scripts for the probes are included. Chat views and admission: - Per conversation the server keeps the prompt it served and, on a follow-up, appends a small new turn verbatim, recalls dropped passages the new turn needs and rebuilds a drifted view (PFLASH_CHAT_VIEW, PFLASH_CHAT_RECALL, PFLASH_CHAT_COMPRESS_NEW_TOKENS, PFLASH_CHAT_REBUILD_*). - Cache-aware admission (PFLASH_ADMISSION=cache) compresses only when the target would otherwise prefill a threshold's worth of uncached droppable tokens, and PFLASH_ARRIVE=1 compresses just a large uncached chunk. Scoring sessions: - The drafter keeps per-conversation scoring sessions (PFLASH_DRAFTER_SESSIONS), so a shared prefix is scored once. Resume is exact by construction (the fresh call's ubatch and key-chunk partition, also for the query step), sessions are copied to host RAM across a drafter release (plain host memory by default, PFLASH_SESSION_OFFLOAD_PINNED=1 for pinned) and their capacity is clamped to the engine and drafter context. - pflash_file_identity reads st_mtime on Windows, where MSVC's struct stat has no st_mtim (the identity is only compared on the machine that wrote it). Unit tests cover selection, fold, request probe, stage 2, query candidates, identifier query, scoring resume, session offload, the causal mask and the drafter IPC; test_server_unit covers the server side. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
0cae773 to
d32ee7f
Compare
d32ee7f to
ddb8554
Compare
…ontext cap Serve tool sessions raw under cache-aware auto admission. The opt-in PFLASH_AGENT_PASTE switch compresses only a large newest user paste and continues the resulting view while preserving history and tool outputs. Fall back to the raw prompt on compression failure when it fits, with telemetry and an optional fail-closed mode. Drop matching compressed chat views after successful raw fallback so later requests cannot reuse stale selections; preserve views when the request is refused. Includes a multi-turn regression for fallback, retries and fail-closed preservation. Apply relevance-spread fallback only to whole-prompt plans and cap PFlash context at 225280 tokens. Include policy, admission and context tests and README documentation for coding-agent sessions. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ddb8554 to
05e9b7f
Compare
…ained head With a trained scoring head (PFLASH_SCORING_HEAD_GGUF) and compression on, every PFlash setting left unset now takes the release profile the PFlash v2 results were measured with: the 16384 threshold, the curve 16384:0.315 32768:0.270 65536:0.270 120000:0.225, budget selection over probe segments with density scores, the request-probe query with one candidate and a 512-token pin, cache admission, arrival scoring and the 0.11 spread fallback. A run needs only the model files. The profile lives next to the cluster defaults in launch_profiles.h and installs the same way: a variable that is already set keeps its value, and an explicit --prefill-threshold, --prefill-keep-ratio or --prefill-curve wins. The query defaults step aside for experiment arms they cannot combine with. Without a head the built-in defaults stay; before, a head without PFLASH_SELECT_MODE failed every request. --prefill-skip-park stays opt-in. The Docker entrypoint forwarded the old 0.05 ratio and 32000 threshold on every PFlash run; it now passes them only when LUCE_PREFILL_KEEP or LUCE_PREFILL_THRESHOLD is set. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Resolve the conflict with Luce-Org#842. validate_request_context keeps main's routed flag and its reply trim, and sends this branch's PFlash maximum-context error when compression would run. test_admission keeps both sets of tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Merged main into the branch (eca7265) to clear the conflict with #842. The merged tree (main with #839, plus this PR) builds on HIP gfx1201, and One small follow-up for later: the raw fallback in |
How long a prompt the drafter can score depends on the card and on the target resident beside it, so the 225,280-token constant measured on one R9700 setup did not fit other setups. With compression on, --max-ctx is now the longest raw prompt PFlash takes. A longer prompt gets a 400 in OpenAI's prompt-overflow wording, which clients use to compact. A prompt that fits but leaves no room for its reply is still compressed. If the drafter runs out of memory, the request falls back to full prefill as before. The README gives the measured R9700 setting: with the 27B IQ4_XS target resident, --max-ctx 204800 holds a 204K-token prompt at a 30.6 GiB peak of 31.9 GiB. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Luce-Org#746 now bounds PFlash prompts by --max-ctx on every path, which is the limit concurrent serving already used. should_reject_oversized loses the concurrent-only limit parameter and its test; server_main keeps the admission.h include for the drafter reserve. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… gap on R9700, arms A/B/C 5-turn no-tools chat, pool 16384, max-ctx 65536, Qwen3.8-27B Q4_K_M. Arm A (merge) compresses turn 5 (39456 -> 6691 tokens) because restorable_prefix_tokens() counts only the turn-2 RAM snapshot; arm B (merge + restorable_prefix_tokens defers to the backend) serves it raw with a live restore (L=31063 replay=8393) and answers byte-identical to the compression-off reference C.
… (#840) * feat(pflash): PFLASH_UNIFIED serves one view/recall mechanism for chat and agent turns Behind PFLASH_UNIFIED=1 (read at startup; unset runs the release code unchanged). After the shared learned query and pins, the newest turn's large uncached content -- the user messages since the last reply, or tool outputs of tools in PFLASH_REGION_TOOLS (names or *) -- is paged: whole lines between cuts where both tokenisations start a token, ranked by the drafter's candidate scores under the release keep-ratio curve, served as raw target token slices with a preamble and line-count markers. Later turns continue the stored view token for token; a new user question recalls unheld units of earlier regions as labelled excerpts before the question (PFLASH_RECALL=0 turns it off). Re-reads are found by content and served verbatim; history is never paged. Counters under /props pflash.unified, trace schema 3. The region module (server/src/server/pflash_unified.*) is format-agnostic: line structure is plain lines or the escaped lines of a JSON string, whichever has more; markers, preamble and labels are format-neutral; no harness-specific formats. - The unified view owns continuations: without a strict-selection environment they are served raw (raw.no_strict_selection), never by the FlowKV path. - Re-read coverage runs after paging, so a newest message served verbatim (cap, layout, unscored) still covers the lines it repeats; a unit is held when its informative lines are covered. - A turn served raw instead of continuing a stored view leaves a raw view when the raw prompt fits, so later turns do not resume the replaced view. - Regions keep their unwidened cap; a stage-2 widened pass widens it once, at paging and at recall alike. Tests: region helpers, paging/recall/re-read through the server, distinct target and drafter tokenizers (separator-only and unavailable line cuts, escaped JSON lines, continued-prefix equality). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(pflash): need-based recall gate for the unified view Without a gate, recall ranks a region's unheld units and fills the region's paging cap at every user question, so a pasted document comes back in full after a few questions whatever is asked. Two opt-in arms: - PFLASH_RECALL_GATE=<theta>: the head's mass on unheld units over its mass on every non-pinned candidate (D/S). Below theta nothing is appended. Above it, unheld units are taken best first across regions, each under its region's cap, until D/S < theta. Pinned candidates and what this request's new regions dropped are outside S; a non-finite lift fails closed (recall.unscorable); deficit_after is computed from the units that survive the cap trim. - PFLASH_RECALL_SOURCE=fresh: recall only unheld units the drafter's own selection for the question keeps (any token in a kept span). Either arm skips the drafter pass when every unit of every region is held. Bad values, both arms at once, or an arm with PFLASH_RECALL=0 fail startup. The view record carries recall_gate {decision, theta, context mass, deficit before/after, new_dropped_mass, trimmed_units}. PFLASH_RECALL_TRACE_UNITS records each region's units for an offline recall replay. Tests: gate and source arms, NaN lift fails closed, a paste arriving with the question (S excludes new drops), served == planned deficit, a document and its question sent as two user messages page like one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(server): unified view release defaults, hand-back to the chat view PFLASH_UNIFIED=1 joins the release configuration, so under the flag it now runs with the measured settings and no other unified knob: every tool's output may be paged (PFLASH_REGION_TOOLS unset = "*", empty = none) and recall is gated at theta 0.075 (PFLASH_RECALL_GATE; "off" = ungated). The fresh recall source is removed (PFLASH_RECALL_SOURCE now fails startup). Requests the unified view cannot serve well go to the chat view and are served exactly as without the flag, instead of a raw prefill: the not-ready cases (no messages array, unreadable turns, remote drafter), a generation prompt or text it cannot match, and a conversation it would start whose uncached, unpinned history before the newest turn -- with newest content it cannot cut into lines -- is a prefill threshold's worth ("uncached", or "reset_uncached" when a stored view could not continue). A cold history the view would prefill raw is compressed by the chat view's cache admission; a tool session there keeps its release behaviour. The reason is counted (handback.<reason>), kept through the raw fallback and traced as pflash.unified = {decision: handback}. A reset hand-back drops the replaced view's lineage, so no later turn continues it or an ancestor of it. Without a strict-selection environment the view serves nothing; startup now says so. Tests: hand-back requests are byte-identical to a flag-off run (cold chat history, unsplittable paste, no selection environment, a reset that no longer fits, whose next turn no longer continues the replaced view); defaults give tools "*" and gate 0.075. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(pflash): unified view on the SSD session cache, in the release profile Admission counts the disk session cache. The restorable prefix is the deeper of the RAM snapshot and the deepest disk entry for the prompt (DiskPrefixCache::longest_prefix, an index lookup that reads no file; ties go to RAM), in the chat view's cache admission and in the unified view's admission and hand-back check, so after a restart or an eviction a conversation the disk holds is not treated as cold. The disk counts only with --kv-cache-dir set and the request's disk scope Full. With the disk cache on, admission records and log lines carry the cached source; with it off they are unchanged. Shortened conversations survive a restart and eviction. With the disk session cache on, the unified views are kept in <kv-cache-dir>/.pflash-views (skipped by the KV cache's scan): written when the RAM view store evicts them and at shutdown before the KV flush, read when a request finds no view in RAM (a longer one on disk wins). One file per view, a one-line header that startup indexes, then the view as JSON; views written under another identity (model, template, context) are never read; at most 256 views, least recently written dropped first, with the cache's TTL checked at every lookup. A view read from disk is checked in full (lines, cuts, per-line and per-unit vectors, token ranges, each region's digest against its bytes, each unit cut against both tokenisations) before use; one that fails is deleted and the request proceeds without it. A prompt served from a view gets a disk KV key for the tokens it served. A restarted server numbers new conversations after the largest lineage on disk. The drafter's own scoring state stays in RAM. The release profile serves the unified view: with a trained scoring head, PFLASH_UNIFIED=1 joins its environment defaults; PFLASH_UNIFIED=0 keeps the chat view. - The request probe's own pins (the question it finds and its recency pin) stay whole when the unified view pages a message. - A selection without per-candidate scores (legacy or split scorer, fold strategy) is handed back to the chat view ("unscored_selection"); a failed drafter pass is refused under PFLASH_FAIL_CLOSED=1. - A raw fallback after a PFlash failure, and a hand-back to the chat view once the request is accepted, drop the unified views the request would continue, in RAM and on disk; a refused request keeps them. - A repeated request is answered with the stored view only when the view holds every span the request pins. Tests: restart with the history on disk (unified and chat view), disk scope and missing entries, view JSON round-trip and fail-closed fields, the disk view store across a restart (index, prefix and identity checks, forks, cap, lineage drop, broken body, TTL, partial write, digest mismatch), a restart through the server keeping a shortened chat's served prefix, probe pins, unscored selections, drafter failure with and without fail-closed, refused reset then retry, a repeat with a new pin. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…acked on #746) (#843) * feat(server): PFlash prefill compression under concurrent serving --paged-attention with --max-concurrency > 1 now runs PFlash on dense Qwen3.5 targets. A prompt over the compression threshold goes to a compression thread: the drafter scores it while the live slots keep decoding, and it returns to the head of the queue with the compressed prompt. The scheduler keeps the prefix cache and measures the prompt's restorable prefix before the hand-off. The drafter scores beside the resident target (skip-park is forced). The paged pool's automatic budget leaves free the drafter's weights plus 56 KiB per prompt token up to --max-ctx (kv_pool_reserve_bytes; measured peak 53 KiB/token from 33K to 206K tokens), and concurrent serving refuses raw prompts longer than --max-ctx. It requires the strict selection profile and an in-process drafter. usage.prompt_tokens counts the client's prompt on streaming and non-streaming replies; usage.timings.pflash carries the compression stats. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(server): make concurrent PFlash thread-safe and bound its memory The compression thread scores beside the scheduler thread, so state both reach is no longer shared: - The drafter resolves its cache's KV types without the LUCE_KV_TQ3 shorthand instead of setting and unsetting it around cache creation; setenv could reallocate the environment while another thread read it. - The last-error string is per thread: a caller reads the error its own call set, and another thread can no longer free or replace it. - On HIP, graphs computed without the qwen4exp profile leave the MMB and HC bookkeeping alone when it is empty, instead of clearing those process-wide containers under another thread's graph. Concurrent serving also covers the paths it skipped or under-budgeted: - Routed requests are compressed too; a routed prompt the drafter already scored waits for its model instead of reporting busy. - An explicit --kv-pool-tokens must leave the drafter's reserve free. - Startup requires the trained head's scorer in strict selection mode and refuses --draft-residency persistent, whose retained scoring sessions are outside the reserve. - The stage and its startup checks apply only to dense Qwen3.5; qwen4exp concurrency keeps its previous behaviour. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
PFlash makes long-prompt prefill cheaper. A small drafter (Qwen3.5-0.8B) reads the whole prompt, keeps the parts that matter for the request, and the target model prefills only those. This PR is the complete C++ runtime, in 5 commits on
main(5f402c61f).How it works
--prefill-compression auto, a prompt is compressed once it brings at least--prefill-thresholdtokens that no cache can restore (PFLASH_ADMISSION=cache), so a warm prefix cache is never thrown away.--max-ctxis the longest raw prompt PFlash takes; a longer one gets HTTP 400 in OpenAI's prompt-overflow wording. A prompt that fits but leaves no room for its reply is compressed.PFLASH_ARRIVE); recall brings back passages a new question needs; a drifted view is rebuilt. Drafter scoring sessions resume exactly, and survive a drafter release in host RAM, so a follow-up scores only its new tokens.PFLASH_AGENT_PASTE=1opts into compressing a big newest user paste; it is off because follow-ups after it are slower than full prefill.PFLASH_FAIL_CLOSED=1returns the error instead). When the relevance over a whole prompt is too flat for the budget to choose (PFLASH_FALLBACK_SPREAD, e.g. "list every X"), the request also gets full prefill. Every fallback is recorded inusage.timings.pflashand/props.Code lives in
src/pflash/; the old Qwen3-0.6B drafter scorer is removed. The drafter IPC gainscompress2/compress3for a remote drafter.Defaults
With a trained head (
PFLASH_SCORING_HEAD_GGUF) and compression on, every setting left unset takes the release profile, the configuration the results below were measured with:--prefill-threshold16384:0.315 32768:0.270 65536:0.270 120000:0.225PFLASH_SELECT_MODE/_SEGMENTS/_SCOREbudget_only/probe/densityPFLASH_SELECT_CHUNK_SIZE/_QUERY_TOKENS/_QUERY_HEADPFLASH_SELECT_QUERY_PROBE,PFLASH_QUERY_CANDIDATES,PFLASH_QUERY_PINPFLASH_SELECT_PARAGRAPH_JOINPFLASH_ADMISSION,PFLASH_ARRIVE,PFLASH_CHAT_REBUILD_MIN_TOKENScache, 1, 65536length, 0, 0PFLASH_FALLBACK_SPREADserver/launch_profiles.hand install like a launch profile: a variable that is already set keeps its value, and an explicit--prefill-threshold,--prefill-keep-ratioor--prefill-curvewins. The startup log says how many were applied.PFLASH_SELECT_QUERY_PROBE=0).--prefill-skip-parkstays opt-in (the target and drafter must fit together; the results used a 32 GB card), and so doesPFLASH_AGENT_PASTE.LUCE_PREFILL_KEEP/LUCE_PREFILL_THRESHOLDonly when set, so the profile applies there too.Every flag, its range and the experiments that remain (fold, request stage 2, query grid and oracle replay, identifier query) are in PFLASH_CONFIGURATION.md. Settings are validated at startup; an invalid one stops the server and names the variable.
PFLASH_COLD_HISTORY,PFLASH_AGENT_HISTORYandLUCE_PC_SINGLE_TURN_DEEPare removed (#835 snapshots every turn).How to run
Results
Modal L40S, Qwen3.8-27B IQ4_XS with Q4_0 KV, release profile against full prefill (
--prefill-compression off). About 1 answer in 10 flips between near-identical runs, so ±3 on a small set is noise.Median time to first token, seconds:
Prompts under 16K tokens are served raw. Answers that fell back are byte-identical to full prefill.
On an R9700 (same configuration): tool sessions are served raw with answers byte-identical to full prefill and time to first token within 0.6 s; at
--max-ctx 204800a 204,650-token prompt compresses to 46,327 tokens with a 30.6 GiB peak of 31.9 GiB and a 206K one gets the 400, and a resumed drafter session cuts a follow-up's scoring from ~30 s to ~2 s.Tests
test_server_unit(HIP gfx1201): 938 passed, 0 failed, 4 skipped. It covers selection, fold, request probe and stage 2, query candidates, identifier query, session resume and host-RAM offload, admission against--max-ctx, tool sessions with the paste switch on and off, the raw-fallback view cleanup, the fix(server): each turn reuses the previous one, long prompts share time, late system messages work #835 snapshot of a tool-session view, and the release profile (installed only with a head, explicit settings win, experiment arms still start).test_entrypoint.sh: PASS, including the PFlash ratio and threshold forwarding.--prefill-compression auto, on five public requests (LongBench v2, Document QA, CodeDebug, BRIGHT, a two-turn chat). Served prompts, cache reuse, answers and selection records are identical on all 6 requests (timings aside), including the spread fallback on the LongBench item and the cached follow-up; the server log shows the 14 defaults applied, threshold 16384 and the curve.test_cpy_strided_dstpasses on the GPU.pflash_fold_replayandpflash_daemonreplay selections offline; the replay matches all 80 request-probe parity cases.Commits
fix(ggml-cuda): take the transpose copy only into a contiguous destination (backport of ggml-cuda : fix cpy transposed path corrupting non-contiguous dst ggml-org/llama.cpp#27663). Without it, a prompt of 8192k+1 tokens gave NaN scorer logits on HIP.feat(pflash): the drafter scorer, strict selection, chat views, request probe and resumable scoring sessions.feat(server): tool sessions raw, fallbacks and the context cap.feat(pflash): the release profile as the default with a trained head.fix(pflash):--max-ctxbounds the raw prompt PFlash takes, instead of a fixed cap.Limitations
--max-ctxmust leave room for its scoring memory beside the target: 204800 on a 32 GB R9700 with the 27B IQ4_XS target. If the drafter runs out of memory, the request falls back to full prefill.Supersedes #773 (the in-graph drafter mask is in commit 2).
🤖 Generated with Claude Code