Repository navigation
feat(server): PFlash prefill compression under concurrent serving (stacked on #746) - #843
Merged
Merged
Conversation
Graffioh
marked this pull request as ready for review
October 14, 2026 11:49
Graffioh
marked this pull request as draft
October 14, 2026 11:57
Graffioh
marked this pull request as ready for review
October 14, 2026 12:50
Graffioh
force-pushed
the
codex/pflash-concurrent
branch
from
October 14, 2026 15:20
8158222 to
23b3e5b
Compare
--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>
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>
davide221
force-pushed
the
codex/pflash-concurrent
branch
from
October 14, 2026 17:11
23b3e5b to
beef5b7
Compare
Contributor
|
Rebased onto main ( One real conflict: #840 made HIP build (gfx1201 + gfx1151): |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #746. GitHub shows #746's commits too: review only this PR's two commits,
feat(server): PFlash prefill compression under concurrent servingandfix(server): make concurrent PFlash thread-safe and bound its memory(a merge commit brings in #746's latest head). Retarget ontomainonce #746 merges.What it does
With #746,
--paged-attentionrefuses PFlash, so a server with--max-concurrency > 1runs every long prompt as a full prefill. This PR lets dense Qwen3.5 serving use PFlash while it serves several requests at once:--max-ctx(kv_pool_reserve_bytes). 56 KiB rounds up the measured 53 KiB/token peak (33K–206K-token prompts, R9700).An explicit
--kv-pool-tokensmust leave that reserve free too, or the server refuses to start and says how many tokens fit.--max-ctx, the rule feat(pflash): Qwen3.5-0.8B PFlash runtime: scoring head, segment and request probes, strict budget selection, chat views, cache-aware admission, fallback #746 uses on the serial path, and the reserve covers that length. Longer prompts get a 400. Routed requests (multi-model serving) are compressed like the others. A routed prompt the drafter has already scored waits for that model instead of reporting busy, so the router never has it scored twice.usage.prompt_tokenscounts the client's prompt, streaming or not.usage.timings.pflashcarries the compression stats, as on the serial path.LUCE_KV_TQ3.setenvcould reallocate the environment while the other thread reads it.PFLASH_SCORING_HEAD_GGUF; no legacy or split scorer), the configuration the memory reserve was measured with;--draft-residency persistentis refused, because its retained scoring sessions would sit outside the reserve;qwen35with--max-concurrency > 1, so DS4 stays refused and qwen4exp concurrency keeps its previous behavior.The serial path is unchanged. Serial PFlash on qa8 measured 78.5 s on the first commit and 78.0 s on the head, run back to back (77.3 s on the #746 head in an earlier session).
Results
Setup:
--max-ctx 139264, q4_0 KV.--paged-attention --max-concurrency 4, with 4 requests in flight (8 for c8).Concurrent PFlash numbers are from the head of this branch. Serial and full-prefill numbers are from the #746 head.
† From a back-to-back run against the first commit, which measured 86.5 s and 49.1 s. One earlier single run of the head measured 93.7 s, so single runs of this arm vary by about 7%.
* In one earlier run of the same design, one cd4 reasoning reply hit its 2048-token cap and missed. Its served prompt (21,742 of 83,939 tokens) was identical to the serial run's. That is variation in batched decoding, not a compression difference.
Edge case, one 136,684-token prompt at the context limit:
usage.prompt_tokensreports 136,684.What these numbers mean:
--max-concurrency 4serves today, concurrent PFlash is 3.7–4.0× faster in wall time, with the same correctness.Scheduling the drafter differently does not change the total GPU work. I tried several variants: making long prefills or every target step wait while the drafter scores, and having the drafter and long prefills take turns in arrival order. None beat this version on mean TTFT across the workloads, so none are included. One trade-off may be worth an opt-in flag later: making long prefills wait while the drafter scores cut cd4 wall time to 260 s (against 340 s here), but raised qa8 mean TTFT to 47.1 s (against 37.8 s).
Limits
--draft-residency persistentis refused. Keeping it resident would need its retained scoring sessions in the reserve.--max-ctx 139264(557K without the reserve) and 252K at--max-ctx 204800./status.Possible follow-ups
Tests
test_feature_gatecovers paged attention with PFlash: allowed for concurrentqwen35on CUDA and HIP, refused at--max-concurrency 1and for DS4.test_admissionchecks that the reserve formula covers the five measured drafter peaks.test_server_unitadds four scheduler cases on a mock engine:usage.prompt_tokensequal to the client's prompt;test_server_unitalso checks that the last error is per thread.test_kv_quantchecks KV-type resolution without the TQ3 shorthand.test_server_unit: 944 passed, 0 failed, 4 skipped.test_feature_gate: 4 passed.--kv-pool-tokens 557056, saying that at most 456144 tokens fit. It also refuses--draft-residency persistent.🤖 Generated with Claude Code