Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions docs/models/qwen35/adaptive-scheduler-policy.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@
> **TL;DR:** Issue #727 now lands Qwen3.5 scheduler policy plumbing with
> conservative defaults: `off` remains the default, `auto` is explicit opt-in,
> `--max-prefill-tokens` remains a hard per-step cap, and TP rejects `auto`
> instead of silently downgrading to `off`.
> instead of silently downgrading to `off`. With `--decode-overlap stream`,
> `auto` keeps prefill running through the finishing window (the overlapped
> chunk no longer stalls decode), trading a redundant QPS16 TPOT win for 31%
> TTFT and 14% throughput at an unchanged tail.
>
> **Last touched:** 2026-07
> **Last touched:** 2026-09

## Preparation

Expand Down Expand Up @@ -37,6 +40,7 @@
- `Off` preserves the fixed base prefill budget.
- No active decode or no in-flight prefill keeps the fixed budget.
- Active requests with at most 4 tokens remaining get one decode-priority tick before the FIFO-front prefill continues.
- With `--decode-overlap stream` the finishing-window deferral is disabled: the overlapped chunk already runs off the decode step, so deferring only delays prefill. Measured on A100-40GB, single run (1024/128 QPS16): TTFT `1828 → 1264 ms`, output throughput `873 → 992 tok/s`, ITL p99 unchanged (`41.7 ms`), TPOT back to vLLM parity (`23.8 vs 23.6 ms`); see #727.
- `Auto` never returns more than the configured base budget; `--max-prefill-tokens` stays a hard per-step cap.
- Final chunks may shrink below the cap when fewer prompt tokens remain.

Expand Down
7 changes: 5 additions & 2 deletions docs/models/qwen35/unified-prefill-overlap.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,11 @@
> this change, vLLM 0.27.0 baseline), the combination dominates every
> single-lever config: 1024/256 c8 ITL p99 `65.5 → 34.2 ms`, c16 p99
> `81.4 → 36.5 ms` (vLLM `83.3`), QPS16 TPOT `36.7 → 20.8 ms` (vLLM `23.6`)
> and QPS16 ITL p99 `101 → 42 ms` (vLLM `93.4`); the trade is open-loop TTFT
> (QPS16 `867 → 1828 ms`, vLLM `218`) and −15% QPS16 output throughput.
> and QPS16 ITL p99 `101 → 42 ms` (vLLM `93.4`). With the combination enabled,
> the `auto` finishing-window deferral is disabled (the overlapped chunk no
> longer stalls decode), which reclaims most of its open-loop cost: QPS16 TTFT
> `1828 → 1264 ms`, output throughput `873 → 992 tok/s`, ITL p99 unchanged;
> TPOT returns to vLLM parity (`23.8 vs 23.6 ms`).

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Bind the new A/B numbers to the measured revision

The new 1828 → 1264 / 873 → 992 figures were measured from fb16fc15 plus this patch according to the commit's benchmark contract, but here they remain under the source binding on lines 12–13 that attributes the combination evidence to 70a600b7 + this change. Those revisions straddle earlier overlap-policy work, so this prevents a reproducible same-context A/B and can cause readers to attribute the result to the wrong build; record the exact source for the new run separately.

AGENTS.md reference: AGENTS.md:L121-L123

Useful? React with 👍 / 👎.

>
> **Last touched:** 2026-09

Expand Down
12 changes: 7 additions & 5 deletions pegainfer-qwen35/src/scheduler/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -671,6 +671,10 @@ fn scheduler_loop(
let mut prefilling: Vec<PrefillingRequest35> = Vec::new();
let mut inflight_prefill: Option<InflightPrefill> = None;
let max_batch = backend.max_batch();
let decode_overlap = matches!(
&backend,
SchedulerBackend::Single(single) if single.overlap_enabled()
);

info!("scheduler ready (max_batch={})", max_batch);

Expand Down Expand Up @@ -946,6 +950,7 @@ fn scheduler_loop(
prefill_budget,
&active_decode,
&prefill_queue,
decode_overlap,
);
let scheduled = take_prefill_chunks(&mut prefilling, step_prefill_budget);
// ITL diagnostics (#470): capture the *actual* prefill-chunk token count
Expand All @@ -958,18 +963,15 @@ fn scheduler_loop(
let plan = plan::build_next_plan(!active.is_empty(), scheduled);
if let Some(plan) = plan {
let itl_plan_kind = match &plan {
ExecutionPlan::Unified { .. } if matches!(&backend, SchedulerBackend::Single(single) if single.overlap_enabled()) => {
"overlap_launch"
}
ExecutionPlan::Unified { .. } if decode_overlap => "overlap_launch",
ExecutionPlan::Unified { .. } => "unified",
ExecutionPlan::Prefill { .. } => "prefill",
ExecutionPlan::Decode => "decode",
};
let itl_step_start = itl_debug.then(Instant::now);
let step_result = match plan {
ExecutionPlan::Unified { pending } => {
if matches!(&backend, SchedulerBackend::Single(single) if single.overlap_enabled())
{
if decode_overlap {
launch_overlap_step(
&mut backend,
&mut active,
Expand Down
88 changes: 79 additions & 9 deletions pegainfer-qwen35/src/scheduler/plan.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ pub(super) fn choose_prefill_budget(
base_budget: usize,
active: &[ActiveDecodeState],
prefilling: &[PrefillQueueState],
decode_overlap: bool,
) -> usize {
assert!(
base_budget > 0,
Expand All @@ -76,9 +77,13 @@ pub(super) fn choose_prefill_budget(
return base_budget;
}

if active
.iter()
.any(|req| req.remaining_tokens() <= DECODE_FINISH_WINDOW_TOKENS)
// With stream overlap the finishing request's last tokens keep ticking on
// the decode stream while the chunk runs aside, so the full prefill
// deferral would only delay prefill without buying decode latency.
if !decode_overlap
&& active
.iter()
.any(|req| req.remaining_tokens() <= DECODE_FINISH_WINDOW_TOKENS)
{
return 0;
}
Expand Down Expand Up @@ -312,7 +317,13 @@ mod tests {
}];

assert_eq!(
choose_prefill_budget(Qwen35SchedulerPolicy::Off, 1024, &active, &prefilling),
choose_prefill_budget(
Qwen35SchedulerPolicy::Off,
1024,
&active,
&prefilling,
false
),
1024,
"off keeps the fixed chunk budget"
);
Expand All @@ -329,7 +340,13 @@ mod tests {
}];

assert_eq!(
choose_prefill_budget(Qwen35SchedulerPolicy::Auto, 1024, &active, &prefilling),
choose_prefill_budget(
Qwen35SchedulerPolicy::Auto,
1024,
&active,
&prefilling,
false
),
1024,
"auto preserves --max-prefill-tokens as a hard per-step cap"
);
Expand All @@ -346,7 +363,13 @@ mod tests {
}];

assert_eq!(
choose_prefill_budget(Qwen35SchedulerPolicy::Auto, 1024, &active, &prefilling),
choose_prefill_budget(
Qwen35SchedulerPolicy::Auto,
1024,
&active,
&prefilling,
false
),
1024,
"standard serving cells with long outputs keep the fixed chunk path"
);
Expand All @@ -363,7 +386,13 @@ mod tests {
}];

assert_eq!(
choose_prefill_budget(Qwen35SchedulerPolicy::Auto, 1024, &active, &prefilling),
choose_prefill_budget(
Qwen35SchedulerPolicy::Auto,
1024,
&active,
&prefilling,
false
),
512,
"auto may shrink the final chunk but never expands beyond the configured cap"
);
Expand All @@ -386,10 +415,45 @@ mod tests {
}];

assert_eq!(
choose_prefill_budget(Qwen35SchedulerPolicy::Auto, 1024, &active, &prefilling),
choose_prefill_budget(
Qwen35SchedulerPolicy::Auto,
1024,
&active,
&prefilling,
false
),
0,
"a near-finished active request gets a decode-priority tick before a long prefill"
);
}

#[test]
fn adaptive_prefill_budget_keeps_prefill_running_under_stream_overlap() {
let active = [
ActiveDecodeState {
generated_count: 252,
max_tokens: 256,
},
ActiveDecodeState {
generated_count: 16,
max_tokens: 4096,
},
];
let prefilling = [PrefillQueueState {
remaining_tokens: 4096,
}];

assert_eq!(
choose_prefill_budget(
Qwen35SchedulerPolicy::Auto,
1024,
&active,
&prefilling,
true
),
1024,
"with stream overlap the finishing window keeps prefill on the prefill stream instead of deferring it"
);
assert!(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no blocking but the first assertion correctly exercises the changed branch: a decoder has four tokens left, overlap is enabled, and the budget remains 1024 instead of returning zero. The final assertion supplies a separate empty queue to build_next_plan(), so it does not use that budget or establish the overlap behaviour, its “zero prefill budget” explanation contradicts this case's expected result.

matches!(
build_next_plan::<Pending>(true, vec![]),
Expand All @@ -410,7 +474,13 @@ mod tests {
}];

assert_eq!(
choose_prefill_budget(Qwen35SchedulerPolicy::Auto, 1024, &active, &prefilling),
choose_prefill_budget(
Qwen35SchedulerPolicy::Auto,
1024,
&active,
&prefilling,
false
),
0,
"decode-priority applies before even a final prefill chunk when an active request is finishing"
);
Expand Down
Loading