mudler / mudler/vllm.cpp

fix(SCHED-PRIORITY-PREEMPT): a preempted victim below the cursor silently drops the next request

Open
#2,527 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
C++
Stars
423
Forks
53
Avg merge
20h 26m
Merged PRs (30d)
310

Description

Row: -

Owed by .agents/specs/upstream-sync-pinport.md
## Owed. Found while re-deriving the PORT-NOW queue for the cdefd9d499 sync
cycle (#2524).

The bug

Under the priority scheduling policy, a preemption can make the scheduler
silently never schedule a running request for that step.

Scheduler::schedule() walks running with a cursor req_index. When the KV
allocator cannot fit the current request, the priority branch picks the
lowest-priority victim anywhere in running and erases it:

src/vllm/v1/core/sched/scheduler.cpp:739 (at 63889449c)

preempted_req = *victim;
running.erase(victim);

The cursor is then stepped back only inside the branch that handles a victim
which was already scheduled this step:

src/vllm/v1/core/sched/scheduler.cpp:762

if (sit != scheduled_running_reqs.end()) {
  ...
  req_index -= 1;
}

The loop has three paths that advance req_index without scheduling the
request: the async max-tokens guard (:663), the PP decode-cadence guard
(:670), and num_new_tokens == 0 (:711, reached when a request hits its cap
or its encoder budget is exhausted). A victim taken from one of those positions
sits before the cursor but is absent from scheduled_running_reqs, so
running shrinks in front of the cursor with no decrement. The request that
shifts into the victim's slot is stepped over and never scheduled.

The loop's own comment at :649-651 says the signed req_index exists so the
"req_index -= 1 fix-up" is well-defined, so the tree mirrors the pre-fix
upstream shape deliberately rather than by accident.

Upstream

Fixed by vllm#49206 at
4d341ca829 fix: resolve silent request skipping in PRIORITY scheduling,
which records the victim's index before erasing and decrements on
victim_index < req_index regardless of whether the victim was scheduled.
4d341ca829 is an ancestor of the sync target cdefd9d499. Upstream also added
tests/v1/core/test_priority_preemption_bug.py, which drives the skip through
throttle_prefills plus long_prefill_token_threshold.

Old behaviour is a strict subset of new: a victim that WAS scheduled is
necessarily at an index below the cursor, so the added case is exactly the
skipped-victim one.

Reproduction shape for our tree

We have no throttle_prefills, so the skip must come from another path. The
num_new_tokens == 0 path is reachable through the max_model_len clamp at
:695-697: CreateSchedulerWithPriority already takes max_model_len, so a
request whose num_computed_tokens reaches max_model_len - 1 yields zero new
tokens and is skipped. Give that request the worst priority so it is the victim,
put two better-priority requests after it, and exhaust KV on the second: the
third is silently dropped from the step.

Fix

Verified against the upstream diff and our tree, but NOT COMPILED — the dev
box was at load ~50 with two other worktrees compiling and ~1 GB free, and a
third build is the recorded OOM failure mode, so this was deliberately not
landed unverified. The patch below is the reconciled change; it still needs the
red test, a build, and a focused green before it lands.

diff --git a/src/vllm/v1/core/sched/scheduler.cpp b/src/vllm/v1/core/sched/scheduler.cpp
index 36f8953db..585912624 100644
--- a/src/vllm/v1/core/sched/scheduler.cpp
+++ b/src/vllm/v1/core/sched/scheduler.cpp
@@ -12,6 +12,7 @@
 #include <cstdint>
 #include <cstdlib>
 #include <iostream>
+#include <iterator>
 #include <map>
 #include <memory>
 #include <mutex>
@@ -647,8 +648,8 @@ SchedulerOutput Scheduler::schedule() {
       scheduler_config_.policy == SchedulerPolicy::kPriority;
 
   // First, schedule the RUNNING requests. req_index is a signed int so the
-  // priority-preemption `req_index -= 1` fix-up (upstream scheduler.py:570) is
-  // well-defined.
+  // priority-preemption `req_index -= 1` fix-up (scheduler.py:596-603 @
+  // cdefd9d499, vllm#49206) is well-defined.
   int req_index = 0;
   while (req_index < static_cast<int>(running.size()) && token_budget > 0) {
     Request* request = running[req_index];
@@ -735,11 +736,22 @@ SchedulerOutput Scheduler::schedule() {
               return std::make_pair(a->priority, a->arrival_time) <
                      std::make_pair(b->priority, b->arrival_time);
             });
+        // Record the victim's index BEFORE erasing it, so the loop cursor can
+        // be corrected below (scheduler.py:596-603, vllm#49206).
+        const int victim_index =
+            static_cast<int>(std::distance(running.begin(), victim));
         preempted_req = *victim;
         running.erase(victim);
+        // The victim sat before the cursor, so `running` shrank in front of it.
+        // Step the cursor back one, or the request that shifts into the
+        // victim's slot is silently never scheduled. This must NOT be
+        // conditional on the victim having been scheduled this step: a victim
+        // the loop SKIPPED (it reached its cap, or its encoder budget was
+        // exhausted) also sits before the cursor, and that is the case
+        // vllm#49206 fixes.
+        if (victim_index < req_index) req_index -= 1;
         // If the victim was already scheduled earlier this step, undo it:
-        // restore its token budget, drop its block reservation, and step
-        // req_index back one (running shrank in front of the cursor).
+        // restore its token budget and drop its block reservation.
         auto sit = std::find(scheduled_running_reqs.begin(),
                              scheduled_running_reqs.end(), preempted_req);
         if (sit != scheduled_running_reqs.end()) {
@@ -759,7 +771,6 @@ SchedulerOutput Scheduler::schedule() {
             }
             scheduled_encoder_inputs.erase(enc_it);
           }
-          req_index -= 1;
         }
       } else {
         // FCFS: preempt the tail (lowest scheduling priority = last arrival).

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in src/vllm/v1/core/sched/scheduler.cpp around Scheduler::schedule(), especially the priority preemption and req_index handling near lines 649-771. Use CreateSchedulerWithPriority with max_model_len to reproduce the skipped request, then add a focused regression test under tests/v1/core and build the project. Done means the regression fails before the fix and passes afterward with a successful build.

Written by the indexing model from the issue text.

Assessment

Tech stack
cpp
Domain
backend
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
72/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.