feat(qwen3): make KV page size configurable#554
Conversation
Replace the hardcoded page_size = 16 in kv_budget_geometry() with a configurable knob threaded through Qwen3MemoryOptions, exposed via the --kv-page-size CLI flag (default 16). FlashInfer's paged attention only accepts 16 and 64, so validate() rejects anything else. Closes openinfer-project#545
5ad3fc3 to
c41c229
Compare
|
@FeathBow could u help review this? thks |
|
Thanks for picking up #545 — the direction is sound. What needs work is the state of the branch. Blocking — the pushed tree doesn't build (it's already on post-#495 main, so the hunks need fixing, not a rebase):
The feature itself works: I applied just the On the OOM specifically: the memory profile gives every profiled decode row a full block, so both the profiling transient and the 64-block minimum floor scale ~4× with page size — those block-count constants were tuned for 16-token pages and are worth revisiting while you're here. Minor:
|
…+ fmt clean - rename openinfer_qwen3_4b:: -> openinfer_qwen3:: (engine.rs, both dflash tests, config.rs) - gate kv_page_size on qwen3 (not qwen3-4b) so it exists under the default build - fix malformed import blocks (dangling };) + cargo fmt --all - pass page_size as 3rd arg to Qwen3MemoryOptions::new at tp_concurrent_decode.rs - restore flashinfer submodule pointer to origin/main (57ba7eeb) - print page size in the 'KV cache (profiled)' load line
|
LGTM. It seems I'm unable to trigger CI due to a settings issue, so this hasn't been validated by CI. the formal grant is yours @xiaguan :) Also, I reviewed on an sm89 GPU (CUDA 12.9). Environment: sm89 · x86_64 · CUDA 12.9 · Qwen3-4B Startup log (blocks scale with page size, capacity unchanged):
The Performance — decode (
|
| Metric | page 16 | page 64 | Δ |
|---|---|---|---|
| single-stream TPOT | 9.84 ms | 9.81 ms | -0.3% |
| conc1 throughput | 100.8 tok/s | 101.0 tok/s | +0.2% |
| conc16 throughput | 1378.6 tok/s | 1360.4 tok/s | -1.3% |
| conc16 latency p50 | 1.490 s | 1.494 s | +0.3% |
| conc16 latency p99 | 1.506 s | 1.509 s | +0.2% |
| conc32 throughput | 2378.3 tok/s | 2384.4 tok/s | +0.3% |
| conc32 latency p99 | 1.710 s | 1.705 s | -0.3% |
All within noise — page size does not move decode.
Performance — prefix cache (cache on)
Re-send same prompt (warm hit):
| prompt_tokens | cached@16 | cached@64 | warm TTFT@16 | warm TTFT@64 |
|---|---|---|---|---|
| 69 | 64 | 64 | 13.1 ms | 12.9 ms |
| 137 | 128 | 128 | 13.0 ms | 12.7 ms |
| 273 | 272 | 256 | 12.6 ms | 12.5 ms |
Two-turn (A -> R, then A+R+suffix, turn2 = 253 tokens):
| Page size | cached | warm TTFT |
|---|---|---|
| 16 | 240 | 13.6 ms |
| 64 | 192 | 13.6 ms |
The granularity cost is real (up to 48 tokens, the "up to page-1 wasted" effect), but the extra prefill is sub-millisecond, so warm TTFT is unchanged.
Performance — fragmentation (KV util=0.30 -> 5.3 GB pool, short requests, high concurrency)
| Scenario | page 16 | page 64 |
|---|---|---|
| KV pool | 2364 blocks | 591 blocks (both 37824 token slots) |
| 64 concurrent / 128 reqs | 0 rejections · 142.4 req/s | 0 rejections · 142.3 req/s |
| 128 concurrent / 256 reqs | 0 rejections · 207.4 req/s | 0 rejections · 201.5 req/s (-3%) |
Same total token capacity; normal concurrency never reaches KV pressure, no rejections.
Verdict
64 is correct, accuracy-lossless, and carries no measurable serving cost on any of the three axes. Keeping 16 as the default with 64 opt-in is the right call.
One line worth adding to the flag help text (not a blocker): the memory profile gives each profiled decode row a full block, so the startup profiling transient scales ~4x with page size — that is why 64 ran out of memory on the smaller card. --kv-page-size 64 wants a >=16 GB card to start; worth documenting so it does not read as a bug.
What
Replace the hardcoded
page_size = 16in Qwen3'skv_budget_geometry()with a configurable knob threaded throughQwen3MemoryOptions. The new--kv-page-sizeCLI flag (default 16, only 16/64 valid per FlashInfer constraint) flows end-to-end: CLI → config →Qwen3MemoryOptions→kv_budget_geometry()→KvBudget.block_size→KvCacheManager→PagedKvLayout→ FlashInfer kernel.Closes #545.
Why
In vLLM-compat P/D mode (#540) the block hash granularity, seal granularity, and the 1:1 remote-block-to-GPU-page load mapping all derive from
budget.block_size. Lifting the hardcoded 16 into a config knob allows the vLLM prefill peer to align to either granularity — and GLM natively uses 64-token pages.Changes
weights.rs:DEFAULT_KV_PAGE_SIZE=16+VALID_KV_PAGE_SIZES=[16,64]constants;page_sizefield onQwen3MemoryOptionswithvalidate()enforcing the 16/64 constraint;kv_budget_geometry(&self, page_size)now parametric; 3 unit tests (valid/invalid/default).config.rs+main.rs:--kv-page-sizeCLI flag (default 16), threaded intoQwen3MemoryOptions.dynamo-backend: mirrored--kv-page-sizearg +DEFAULT_KV_PAGE_SIZEimport.Qwen3MemoryOptions::newcall sites for the new 3rd param.Verification
cargo build --release— clean (0 warnings)page_sizesuite) — all pass" Paris. The capital of Germany is Berlin. The"Trade-offs (from #545)
page_size - 1wasted tokens per boundary).