Skip to content

feat(pflash): Qwen3.5-0.8B PFlash runtime: scoring head, segment and request probes, strict budget selection, chat views, cache-aware admission, fallback - #746

Merged
davide221 merged 6 commits into
Luce-Org:mainfrom
Graffioh:codex/pflash-segment-probe-experiment
Oct 14, 2026
Merged

davide221 merged 6 commits into
Luce-Org:mainfrom
Graffioh:codex/pflash-segment-probe-experiment

Conversation

@Graffioh

@Graffioh Graffioh commented Sep 19, 2026 •

Copy link
Copy Markdown
Collaborator

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

  1. Admission. With --prefill-compression auto, a prompt is compressed once it brings at least --prefill-threshold tokens that no cache can restore (PFLASH_ADMISSION=cache), so a warm prefix cache is never thrown away. --max-ctx is 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.
  2. Scoring. The drafter runs blocks 0–14. A trained scoring head ranks the prompt against the request, a segment probe cuts it into whole units (paragraphs, functions, records), and a request probe finds the request in the human messages (one relative query candidate plus a 512-token verbatim pin). Each part loads from GGUF and fails closed.
  3. Selection. The question, instructions, system prompt, tool definitions and role headers are always kept. Units fill the rest of the budget by relevance per token, in document order. The budget follows a length curve.
  4. Chats. The server keeps the view it served for each conversation. A small new turn is appended verbatim and reuses the cache; a large one is compressed on its own (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.
  5. Tool sessions (a request with tools, tool calls or tool results) are served raw: the agent's history and tool outputs are never compressed. PFLASH_AGENT_PASTE=1 opts into compressing a big newest user paste; it is off because follow-ups after it are slower than full prefill.
  6. Fallbacks. If scoring fails, the raw prompt is served with full prefill when it fits (PFLASH_FAIL_CLOSED=1 returns 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 in usage.timings.pflash and /props.

Code lives in src/pflash/; the old Qwen3-0.6B drafter scorer is removed. The drafter IPC gains compress2/compress3 for 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:

setting release profile without a head
--prefill-threshold 16384 32000
keep ratio curve 16384:0.315 32768:0.270 65536:0.270 120000:0.225 flat 0.05
PFLASH_SELECT_MODE / _SEGMENTS / _SCORE budget_only / probe / density legacy selection
PFLASH_SELECT_CHUNK_SIZE / _QUERY_TOKENS / _QUERY_HEAD 256 / 128 / 1 by input size / 8 / 0
PFLASH_SELECT_QUERY_PROBE, PFLASH_QUERY_CANDIDATES, PFLASH_QUERY_PIN 1, 1, 512 off
PFLASH_SELECT_PARAGRAPH_JOIN 0 1
PFLASH_ADMISSION, PFLASH_ARRIVE, PFLASH_CHAT_REBUILD_MIN_TOKENS cache, 1, 65536 length, 0, 0
PFLASH_FALLBACK_SPREAD 0.11 off
  • The values are in server/launch_profiles.h and install like a launch profile: a variable that is already set keeps its value, and an explicit --prefill-threshold, --prefill-keep-ratio or --prefill-curve wins. The startup log says how many were applied.
  • The probe parts need their GGUFs. The query defaults step aside for experiment arms they cannot combine with (another scorer, the identifier query, request stage 2, PFLASH_SELECT_QUERY_PROBE=0).
  • --prefill-skip-park stays opt-in (the target and drafter must fit together; the results used a 32 GB card), and so does PFLASH_AGENT_PASTE.
  • The Docker entrypoint forwards LUCE_PREFILL_KEEP / LUCE_PREFILL_THRESHOLD only 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_HISTORY and LUCE_PC_SINGLE_TURN_DEEP are removed (#835 snapshots every turn).

How to run

PFLASH_SCORING_HEAD_GGUF=/path/seg-scoring-head.gguf \
PFLASH_SEGMENT_PROBE_GGUF=/path/segment-probe.gguf \
PFLASH_REQUEST_PROBE_GGUF=/path/request-probe.gguf \
luce_server Qwen3.8-27B-IQ4_XS-pure.gguf --cache-type-k q4_0 --cache-type-v q4_0 --max-ctx 204800 \
  --prefill-compression auto --prefill-drafter Qwen3.5-0.8B-BF16.gguf --prefill-skip-park

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.

set full prefill PFlash fell back (spread)
RULER (15) 13/15 13/15 2
NIAH (100, 16K–120K) 100/100 100/100 9
NoLiMa (24, 16K/32K) 14/24 14/24 11
LongBench v2 (12) 6/12 7/12 3
BRIGHT (28, 32K) 23/28 23/28 2
InfiniteBench CodeDebug (32, ~98K) 26/32 25/32 0
LongCodeQA (114, 32K/64K/128K) 57/114 62/114 0

Median time to first token, seconds:

workload full prefill PFlash speed-up
NIAH 32K / 64K / 120K 15.2 / 38.4 / 106.7 6.3 / 11.9 / 20.0 2.4x / 3.2x / 5.3x
NoLiMa 32K 15.1 6.6 2.3x
LongBench v2 12.1 5.2 2.3x
BRIGHT 32K 15.5 6.1 2.5x
CodeDebug ~98K 60.3 15.0 4.0x
LongCodeQA 32K / 64K / 128K 10.6 / 27.5 / 68.0 5.3 / 9.8 / 16.6 2.0x / 2.8x / 4.1x

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 204800 a 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.
  • Release defaults on an R9700: one binary, the explicit release env and flags against only the three GGUFs and --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_dst passes on the GPU. pflash_fold_replay and pflash_daemon replay selections offline; the replay matches all 80 request-probe parity cases.

Commits

  1. 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.
  2. feat(pflash): the drafter scorer, strict selection, chat views, request probe and resumable scoring sessions.
  3. feat(server): tool sessions raw, fallbacks and the context cap.
  4. feat(pflash): the release profile as the default with a trained head.
  5. fix(pflash): --max-ctx bounds the raw prompt PFlash takes, instead of a fixed cap.

Limitations

  • PFlash runs on the single-request engine; the batched launcher does not use it yet.
  • The drafter scores the whole prompt, history included, so --max-ctx must 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.
  • The request-probe message mapping fails on some raw agent traces (trimmed contents, tool-call-only turns); those pastes fall back to raw.

Supersedes #773 (the in-graph drafter mask is in commit 2).

🤖 Generated with Claude Code

@Graffioh
Graffioh force-pushed the codex/pflash-segment-probe-experiment branch from 07267f9 to 3814457 Compare September 19, 2026 14:16
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>
@Graffioh

Copy link
Copy Markdown
Collaborator Author

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 Graffioh/lucebox-hub, based on this PR's head 555ee2a97.

Query-head fix: PFLASH_SELECT_QUERY_HEAD=1 (d0c750100, on archive/pflash-fold-select-20260930)

  • The adopted scoring head's served configuration uses it (luce_box feat(dflash): daemon scripts improvements (GPU split, Windows, defaults) #139, recipe revision 7), so this switch is part of the configuration behind the current numbers.
  • The problem: with latest_user and a fixed 128-token tail, a user who asks first and pastes afterwards gets a query window of pure document text. On the pflash-bench pilot, question-first items scored 0/5 against 15/19 for question-after.
  • The fix: the server splits the latest user message into blocks (pflash_query_blocks).
    • The head is the leading prose block before the first paste-like line.
    • The tail is the last paragraph after the last paste-like line, and it replaces the fixed tail window.
    • Each block is capped at 256 tokens (PFLASH_SELECT_QUERY_HEAD_TOKENS / _TAIL_TOKENS) and is sent to the drafter as extra_query_spans at the query's own weight. It is pinned like an instruction span.
    • A message without paste structure keeps today's window. The switch is off by default and needs strict selection with latest_user.
  • It sits on top of the fold commits below and uses their block parser, so porting it here means taking pflash_fold.* as well, or splitting the parser out.

Fold selection: PFLASH_SELECT_STRATEGY=fold (14a4aec42, with the tests 599217b39 and e7624d638 and the pflash_fold_replay tool; also on archive/pflash-fold-select-20260930)

Adaptive top-k selection: PFLASH_SELECT_STRATEGY=adaptive (6468f55ba, on archive/pflash-adaptive-select-20260930, which contains the fold branch)

  • Not adopted. Recipe revision 7 records the result: 85 / 102 against 94 at the fixed budget on the private suite. At 64K–128K it kept a quarter to two thirds fewer tokens.

Other variants, now pushed instead of kept as local backups

  • archive/pflash-container-headers-20260930: container headers and cut markers, request-structure headers (PFLASH_SELECT_STRUCT_HEADERS), and the early recall variants. It also has the first copy of PFLASH_DRAFTER_PROFILE=1.
  • The "Tried and not adopted" rows of the description:
    • archive/pr746-recall-missing-topic-switch-20260930 (9c52b0599)
    • archive/pr746-rebuild-on-topic-switch-20260930 (5063f3c46)
    • archive/pr746-restate-strongest-segments-20260930 (d10462a77)
    • archive/pr746-recall-any-size-20260930 (db4ce1817)
  • archive/pr746-pre-fold-20260919: the branch before the 19 Sep fold (two-scorer split budget, sub-unit probe head, oversize split guard).
  • archive/pflash-trackA-head-weights-20260930: optional per-head weights for the scoring head (Track A, not adopted).

The local worktrees (pflash-qwen35-hybrid, pflash-qwen35-drafter-fixes) were removed. This PR's branch is unchanged.

@Graffioh
Graffioh force-pushed the codex/pflash-segment-probe-experiment branch from 555ee2a to 3123404 Compare October 11, 2026 09:43
@Graffioh Graffioh changed the title feat(pflash): Qwen3.5-0.8B drafter scorer, segment probe and strict budget selection feat(pflash): Qwen3.5-0.8B PFlash runtime: scoring head, segment and request probes, strict budget selection, chat views, cache-aware admission, fallback Oct 11, 2026
@Graffioh
Graffioh force-pushed the codex/pflash-segment-probe-experiment branch from de826e3 to c2de8a5 Compare October 12, 2026 12:32
@davide221

Copy link
Copy Markdown
Contributor

CI failed on c2de8a5e

Windows build fails: new PFlash file uses a Linux-only stat field (st_mtim)

The Windows (MSVC) build of the pull request fails to compile one of the new PFlash source files. The code reads the nanosecond modification time through stat::st_mtim, which does not exist in the Windows C runtime's stat struct. This is a portability bug in the change under test; Linux and HIP builds are unaffected.

How to fix: Use st_mtime on Windows (keep st_mtim on POSIX) in pflash_file_identity

  1. Edit server/src/pflash/pflash_session_offload.cpp in pflash_file_identity.
  2. Compute mtime_ns via #ifdef _WIN32 from s.st_mtime (seconds), else from s.st_mtim as before.
  3. Print mtime_ns in the identity string instead of the inline st_mtim expression.
  4. Push and re-run the Windows CI job.

windows compile error: non-portable stat member · high confidence · run

Details and evidence

Failed: Build Windows (MSVC + CUDA, library + server targets) › Build Windows targets

Job Build Windows (MSVC + CUDA, library + server targets), step Build Windows targets, fails compiling src/pflash/pflash_session_offload.cpp:

pflash_session_offload.cpp(294): error C2039: 'st_mtim': is not a member of 'stat'
C:\...\ucrt\sys/stat.h(87): note: see declaration of 'stat'

pflash_file_identity() (server/src/pflash/pflash_session_offload.cpp:288-296) builds a file identity string from stat and reads s.st_mtim.tv_sec / s.st_mtim.tv_nsec at line 294. st_mtim is a POSIX extension; the Windows UCRT struct stat only has st_mtime (whole seconds), so MSVC errors out. The function was added by this PR (new file, used from server/src/pflash/qwen35_drafter.cpp:877,884 to key stored drafter sessions to the drafter file). The unit test for it (server/test/unit/test_pflash_session_offload.cpp:397-408) only requires the identity to change when the file's size/content changes, so second resolution on Windows is acceptable.

Other things to try

  • Reproduce locally on Windows or with MinGW: compile server/src/pflash/pflash_session_offload.cpp and confirm st_mtim is the only non-portable member.
  • Alternative: use std::filesystem::last_write_time (C++17, portable) instead of stat, but that is a larger change than the #ifdef fix.
Patch (1 file)

Not applied anywhere. To try it, save the diff as fix.patch and run git apply fix.patch.

diff --git a/server/src/pflash/pflash_session_offload.cpp b/server/src/pflash/pflash_session_offload.cpp
index 5d15b523..79d762c5 100644
--- a/server/src/pflash/pflash_session_offload.cpp
+++ b/server/src/pflash/pflash_session_offload.cpp
@@ -290,8 +290,14 @@ std::string pflash_file_identity(const std::string & path) {
     struct stat s {};
     if (::stat(path.c_str(), &s) != 0) return path + "|missing";
     std::ostringstream out;
+#ifdef _WIN32
+    // The Windows CRT struct stat has no st_mtim; st_mtime is whole seconds.
+    const long long mtime_ns = (long long) s.st_mtime * 1000000000LL;
+#else
+    const long long mtime_ns = (long long) s.st_mtim.tv_sec * 1000000000LL + (long long) s.st_mtim.tv_nsec;
+#endif
     out << path << '|' << (unsigned long long) s.st_dev << ':' << (unsigned long long) s.st_ino << '|'
-        << (long long) s.st_size << '|' << (long long) s.st_mtim.tv_sec * 1000000000LL + (long long) s.st_mtim.tv_nsec;
+        << (long long) s.st_size << '|' << mtime_ns;
     return out.str();
 }
Prompt for a coding agent
A GitHub Actions run failed in Luce-Org/lucebox. Please fix it.

- Pull request #746 (branch `codex/pflash-segment-probe-experiment`), commit c2de8a5e7c206a8bce9992d0f00d1454409d845f
- Workflow: CI (https://github.com/Luce-Org/lucebox/actions/runs/38249665121)
- Failed: Build Windows (MSVC + CUDA, library + server targets) › Build Windows targets

Diagnosis:
Job `Build Windows (MSVC + CUDA, library + server targets)`, step `Build Windows targets`, fails compiling `src/pflash/pflash_session_offload.cpp`:

```
pflash_session_offload.cpp(294): error C2039: 'st_mtim': is not a member of 'stat'
C:\...\ucrt\sys/stat.h(87): note: see declaration of 'stat'
```

`pflash_file_identity()` (server/src/pflash/pflash_session_offload.cpp:288-296) builds a file identity string from `stat` and reads `s.st_mtim.tv_sec` / `s.st_mtim.tv_nsec` at line 294. `st_mtim` is a POSIX extension; the Windows UCRT `struct stat` only has `st_mtime` (whole seconds), so MSVC errors out. The function was added by this PR (new file, used from server/src/pflash/qwen35_drafter.cpp:877,884 to key stored drafter sessions to the drafter file). The unit test for it (server/test/unit/test_pflash_session_offload.cpp:397-408) only requires the identity to change when the file's size/content changes, so second resolution on Windows is acceptable.

Proposed fix: Use st_mtime on Windows (keep st_mtim on POSIX) in pflash_file_identity
1. Edit `server/src/pflash/pflash_session_offload.cpp` in `pflash_file_identity`.
2. Compute `mtime_ns` via `#ifdef _WIN32` from `s.st_mtime` (seconds), else from `s.st_mtim` as before.
3. Print `mtime_ns` in the identity string instead of the inline `st_mtim` expression.
4. Push and re-run the Windows CI job.

Suggested patch (a starting point):
```diff
diff --git a/server/src/pflash/pflash_session_offload.cpp b/server/src/pflash/pflash_session_offload.cpp
index 5d15b523..79d762c5 100644
--- a/server/src/pflash/pflash_session_offload.cpp
+++ b/server/src/pflash/pflash_session_offload.cpp
@@ -290,8 +290,14 @@ std::string pflash_file_identity(const std::string & path) {
     struct stat s {};
     if (::stat(path.c_str(), &s) != 0) return path + "|missing";
     std::ostringstream out;
+#ifdef _WIN32
+    // The Windows CRT struct stat has no st_mtim; st_mtime is whole seconds.
+    const long long mtime_ns = (long long) s.st_mtime * 1000000000LL;
+#else
+    const long long mtime_ns = (long long) s.st_mtim.tv_sec * 1000000000LL + (long long) s.st_mtim.tv_nsec;
+#endif
     out << path << '|' << (unsigned long long) s.st_dev << ':' << (unsigned long long) s.st_ino << '|'
-        << (long long) s.st_size << '|' << (long long) s.st_mtim.tv_sec * 1000000000LL + (long long) s.st_mtim.tv_nsec;
+        << (long long) s.st_size << '|' << mtime_ns;
     return out.str();
 }
```

Check out that commit, confirm the diagnosis, make the fix (adjust it if the diagnosis is wrong), and re-run the failing step locally before pushing.

Reply with /ci-fix and what to change to get a revised diagnosis.

Graffioh and others added 2 commits October 13, 2026 08:41
…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>
@Graffioh
Graffioh force-pushed the codex/pflash-segment-probe-experiment branch from 0cae773 to d32ee7f Compare October 13, 2026 08:43
@Graffioh
Graffioh marked this pull request as ready for review October 13, 2026 08:44
@Graffioh
Graffioh force-pushed the codex/pflash-segment-probe-experiment branch from d32ee7f to ddb8554 Compare October 13, 2026 09:39
…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>
@Graffioh
Graffioh force-pushed the codex/pflash-segment-probe-experiment branch from ddb8554 to 05e9b7f Compare October 13, 2026 10:01
Graffioh and others added 2 commits October 13, 2026 12:51
…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>
@davide221

Copy link
Copy Markdown
Contributor

Merged main into the branch (eca7265) to clear the conflict with #842. validate_request_context keeps main's routed flag and its reply trim, and still sends your PFlash maximum-context error when compression would run. test_admission keeps both sets of tests. No other changes.

The merged tree (main with #839, plus this PR) builds on HIP gfx1201, and test_server_unit gives 939 passed, 0 failed, 4 skipped.

One small follow-up for later: the raw fallback in pflash_fallback_decision still refuses when raw prompt + max_tokens is over max_ctx, while a direct request now gets its reply trimmed (fit_reply_to_context). It only matters with PFlash on.

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>
Graffioh added a commit to Graffioh/lucebox-hub that referenced this pull request Oct 14, 2026
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>
@davide221
davide221 merged commit c2569a0 into Luce-Org:main Oct 14, 2026
7 of 8 checks passed
dusterbloom added a commit to dusterbloom/lucebox-hub that referenced this pull request Oct 14, 2026
… 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.
davide221 pushed a commit that referenced this pull request Oct 14, 2026
… (#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>
davide221 pushed a commit that referenced this pull request Oct 14, 2026
…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>
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.

2 participants