Repository navigation
Conversation
The PagedAttention scheduler partitioned every decode batch by KV length, ran the single shortest bucket, and *preempted* the rest — which frees their KV blocks, so they must be re-prefilled. With B sequences over D distinct lengths that decodes B/D per step, and in real serving lengths always diverge. It was also unnecessary. On this path every row contributes exactly one token, so there is no padding; the mask is None; RoPE takes its per-sequence branch; and block tables, context lens and flash varlen seqlens are all per sequence already. Neither vLLM nor SGLang partitions a decode batch by KV length. Scope: paged-attention models only. DeepSeek V4 returns false from supports_paged_attention and runs on DefaultScheduler, so it is unaffected by this change. Also fixes IntervalLogger reporting a lifetime prefix-cache hit rate alongside a per-interval throughput on the same line. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
ArcGate triage. NEEDS-OWNER — half of this is superseded, half of it is unique, and that is why I am not closing it. The scheduler half is superseded by #118. Both fix the same defect —
But this PR carries something #118 does not, and it is a real correctness fix in its own right: Also unique: So: not closeable as-is. Per the standing rule, nothing gets closed whose content is not reproduced elsewhere, and the logger fix is not reproduced anywhere. Suggested disposition for the owner — pick one, this is not mine to decide:
Either way this branch needs to absorb master before it can produce a |
|
Closing as superseded, not abandoned. The functional change here — stop bucketing the decode leg by KV length, so the losers are not preempted and re-prefilled — is already on master via What is given up: (1) the Note for anyone reviving this area: PagedAttention on V4 is currently a landmine — |
The defect
PagedAttentionScheduler— the scheduler every paged-attention model serves on — partitions each decode batch by KV length viabucket_and_preempt_sequences, runs the single shortest bucket, and drops the rest.It does not merely waitlist them.
_preemptcallskv_cache_manager.free(seq_id), so every dropped sequence loses its KV blocks and must be re-prefilled. WithBsequences overDdistinct lengths,B / Ddecode per step and the remainder are re-prefilled. In real serving lengths always diverge — users arrive at different times, with different prompts, and generate different numbers of tokens.This is upstream (
8010b6a05, mistral.rs #2354), not Arc-introduced. It is distinct from #104, which fixes the same class of bug on the denseDefaultSchedulerpath. The two are disjoint files and disjoint model sets.Why the bucketing was unnecessary
Everything the decode forward consumes is already addressed per sequence. Each row contributes exactly one token, so there is nothing to pad:
make_completion_chunk[B, 1]— one token per row, no paddinglayers_masker.rsNone, taken becausetgt_len == 1layers.rslen() == 1fast path narrowsmake_completion_chunkmake_completion_chunkseq.len()per sequencemake_completion_chunkseqlens_k[i] = 1 + start_pos_i, then cumsumarc-cuda-graph/src/buffers.rscontext_lens: [padded_bs] u32;max_context_lenis a scalar arg, not a shapeThe comment the bucketing shipped with — that equal lengths are "required for correct flash attention varlen operation (avoiding soundness issues with padding)" — has it backwards. Varlen is the mechanism for unequal lengths. The constraint being defended belongs to the dense cache (
clone_in_cachetemplates one batch cache offseqs[0]), which is theDefaultScheduler's substrate, not this one.Where this leaves us against vLLM and SGLang
Neither partitions a decode batch by KV length; both were read at pinned revisions rather than from memory.
flash_attn_varlen_func(..., seqused_k=attn_metadata.seq_lens, cu_seqlens_q=query_start_loc, block_table=...)(vllm/v1/attention/backends/flash_attn.py:1041-1056). Per-row KV length;max_seqlen_*are launch hints only. The scheduler emits one flatdict[req_id -> num_tokens](vllm/v1/core/sched/output.py:203-208).2e7c85d—prepare_for_decodeis a vectorizedself.seq_lens = self.seq_lens + 1over the whole running set (schedule_batch.py:3038-3082); FlashInfer readskv_indptr[1:bs+1] = torch.cumsum(paged_kernel_lens, dim=0)(flashinfer_backend.py:2059-2095).Both preempt only under memory pressure — vLLM LIFO-recompute (
scheduler.py:577-625), SGLangretract_decodefewest-generated-first (schedule_batch.py:2816-2905). We preempted on length divergence, which neither does.Honest standing after this PR: parity on ragged decode, still behind on chunked prefill (our prompt path right-pads to
max_lenand buckets), on CPU/GPU overlap scheduling (SGLang's is default-on), and on prefix-aware queue ordering (SGLang LPM; we are FCFS by timestamp). Those are named as follow-ups, not claimed here.Structural flag-off identity
Not "a test observed the same numbers". The
Bucketedarm holds the pre-change three lines verbatim, and theRaggedarm is a no-op becauseself.runningalready holds the admitted set:ARC_SCHED_BUCKETED_DECODE=1restores it. Per D18 the refusal is a named value, not an absence:DecodeAdmission::decidereturns the reason andnewlogs it, so a run that reverted to bucketing says so rather than looking like a run that was never fixed.Speculative decode is not reachable here
Ragged decode assumes one token per row. Nothing on this path produces more:
MtpSpeculativePipelinedoes not support paged attention, andmistralrs/src/speculative.rsbuilds aSchedulerConfig::DefaultScheduler.🔴 Scope — this does NOT touch DeepSeek V4
DeepSeekV4Loader::supports_paged_attentionreturnsfalse(normal_loaders.rs:3265), so V4 serves onDefaultSchedulerand is unaffected. This PR helps every model that does use paged attention — the trait default isOk(true), so most text models. V4's equivalent fix is #104's dense path. Anyone reading this as "V4 throughput fixed" is misreading it.Measured — CPU tests, not hardware (D14)
cargo test -p mistralrs-core --lib→ 380 passed, 0 failed. Scoped clippy lane exits 0.cargo check -p mistralrs-core -p mistralrs-cli -p mistralrs-server-coregreen.Five tests, and two of them validate each other's fixture:
bucketed_decode_shatters_the_batch_to_onereturning exactly 1 is what proves the fixture really producesD = ndistinct lengths — had divergence silently failed, every row would share a bucket and that test would fail rather than the ragged one passing vacuously.ragged_decode_runs_the_whole_admitted_batchbucketed_decode_shatters_the_batch_to_onebucketed_decode_frees_the_kv_of_every_sequence_it_dropsb1_is_untouched_by_either_rulethe_refusal_names_itselfARC_SCHED_BUCKETED_DECODE=1Mutation run. Making the
Raggedarm bucket anyway — i.e. the fix as a no-op — fails withB=8 at 8 distinct KV lengths: only 1 of 8 decoded. The test is not vacuous.Also in here
IntervalLoggerprinted a lifetime prefix-cache hit rate next to a per-interval throughput on the same line, becausetokens_processedwasswapped whileprefix_cache_hits/total_new_seqswereloaded. A "1.43% prefix hit rate" was quoted from it that belonged to no single test. Now differenced against the previous reading so both cover the same window, and omitted rather than printed as0.00%when no sequence arrived — a zero there reads as "nothing hit the cache" when it means "nothing was admitted to hit it".The hardware measurement
arc-tools/arcsched_decode_admission_ab.sh. Not run — D15, I never callruncrate. Same binary, same model, same workload; the only difference between arms isARC_SCHED_BUCKETED_DECODE.The direct fingerprint is
mean_runningscraped fromN running, M waiting— that needs no throughput inference, becauseNis literally how many sequences went into the forward. Prediction stated in the script so it can be refuted: BUCKETED collapses toward 1 while RAGGED tracks B. If BUCKETED'srunningdoes not collapse, the defect did not fire for that workload and it should be recorded as a refutation, not retried. B=1 must agree across arms — one sequence is one bucket, so a disagreement there invalidates the run.