File size: 10,602 Bytes
5e79b62
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
# coding=utf-8
# Copyright (c) 2026, NVIDIA CORPORATION.  All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Runtime monkey-patches making vLLM v1 scheduler CFG-pair-aware.

Call ``apply_cfg_patches()`` **before** creating the ``LLM`` instance.
Post-sampling token synchronization is handled separately by
``CFGLogitsProcessor._ensure_sample_patched()`` (runs in each worker).
"""

from __future__ import annotations

import logging
from typing import Optional

logger = logging.getLogger(__name__)

_applied = False



def _get_cfg_pair_id(request) -> Optional[str]:
    sp = request.sampling_params
    if sp and sp.extra_args:
        return sp.extra_args.get("cfg_pair_id")
    return None


def _get_cfg_role(request) -> Optional[str]:
    sp = request.sampling_params
    if sp and sp.extra_args:
        return sp.extra_args.get("cfg_role")
    return None



def _hold_incomplete_pairs(scheduler) -> list:
    """Remove from the waiting queue any CFG request whose pair partner has not
    yet been admitted to the engine.

    vLLM v1's EngineCore runs a schedule step as soon as the first request of a
    pair arrives over IPC; for small/fast models the partner often arrives only
    after that step, so the lone member prefills one step early and the pair is
    permanently offset by one token (breaking CFG).  Holding the lone member
    until both are present guarantees they prefill in the same step.
    """
    held: list = []
    keep: list = []
    for req in list(scheduler.waiting):
        pair_id = scheduler._cfg_req_to_pair.get(req.request_id)
        complete = True
        if pair_id is not None:
            roles = scheduler._cfg_pairs.get(pair_id, {})
            complete = len(roles) == 2 and all(
                rid in scheduler.requests for rid in roles.values()
            )
        (keep if complete else held).append(req)
    if held:
        scheduler.waiting.clear()
        scheduler.waiting.extend(keep)
    return held


def _reorder_waiting_for_cfg(scheduler) -> None:
    """Move CFG pair partners adjacent in the waiting queue."""
    waiting = scheduler.waiting
    if len(waiting) < 2:
        return

    requests = list(waiting)
    waiting.clear()

    seen: set[str] = set()
    result: list = []
    for req in requests:
        rid = req.request_id
        if rid in seen:
            continue
        seen.add(rid)
        result.append(req)

        pair_id = scheduler._cfg_req_to_pair.get(rid)
        if pair_id is None:
            continue
        roles = scheduler._cfg_pairs.get(pair_id, {})
        for _role, partner_id in roles.items():
            if partner_id != rid and partner_id not in seen:
                for r in requests:
                    if r.request_id == partner_id:
                        seen.add(partner_id)
                        result.append(r)
                        break

    waiting.extend(result)


def _equalize_cfg_pair_progress(scheduler, scheduler_output) -> None:
    """Ensure both members of every CFG pair reach the same num_computed_tokens.

    After ``_orig_schedule`` (which calls ``_update_after_schedule``),
    ``num_computed_tokens`` is already advanced.  Prefix-cache hits or unequal
    chunked-prefill budget can leave one member ahead of the other, causing it
    to finish prefill a step earlier and emit a decode token the partner misses.

    Fix: reduce the faster member's allocation so both land on the same
    ``num_computed_tokens``.  The over-allocated KV-cache blocks remain and are
    consumed in the next step — no blocks are leaked.
    """
    for _pair_id, roles in scheduler._cfg_pairs.items():
        cond_id = roles.get("cond")
        uncond_id = roles.get("uncond")
        if not cond_id or not uncond_id:
            continue

        cond_sched = scheduler_output.num_scheduled_tokens.get(cond_id, 0)
        uncond_sched = scheduler_output.num_scheduled_tokens.get(uncond_id, 0)
        if cond_sched == 0 or uncond_sched == 0:
            continue

        cond_req = scheduler.requests.get(cond_id)
        uncond_req = scheduler.requests.get(uncond_id)
        if not cond_req or not uncond_req:
            continue

        if cond_req.num_computed_tokens == uncond_req.num_computed_tokens:
            continue

        target = min(cond_req.num_computed_tokens,
                     uncond_req.num_computed_tokens)

        feasible = True
        for req, sched in [(cond_req, cond_sched), (uncond_req, uncond_sched)]:
            if req.num_computed_tokens - target >= sched:
                feasible = False
                break
        if not feasible:
            continue

        for req_id, req, orig_sched in [
            (cond_id, cond_req, cond_sched),
            (uncond_id, uncond_req, uncond_sched),
        ]:
            diff = req.num_computed_tokens - target
            if diff > 0:
                req.num_computed_tokens = target
                scheduler_output.num_scheduled_tokens[req_id] = orig_sched - diff
                scheduler_output.total_num_scheduled_tokens -= diff
                logger.debug(
                    "CFG equalize %s: %d -> %d scheduled, computed -> %d",
                    req_id, orig_sched, orig_sched - diff, target,
                )


def _co_preempt_split_pairs(scheduler, scheduler_output) -> None:
    """Detect a split CFG pair (one running, one not / desynced).

    On vLLM 0.20 the original manual re-preemption surgery is unsafe, so this is
    a detection-and-log guard; ``_hold_incomplete_pairs`` prevents the split.
    """
    if not scheduler._cfg_pairs:
        return

    running_ids = {r.request_id for r in scheduler.running}

    to_preempt: list[str] = []
    for _pair_id, roles in scheduler._cfg_pairs.items():
        cond_id = roles.get("cond")
        uncond_id = roles.get("uncond")
        if cond_id is None or uncond_id is None:
            continue
        if cond_id not in scheduler.requests or uncond_id not in scheduler.requests:
            continue

        cond_running = cond_id in running_ids
        uncond_running = uncond_id in running_ids
        if cond_running != uncond_running:
            to_preempt.append(cond_id if cond_running else uncond_id)
            continue

        if cond_running and uncond_running:
            cond_req = scheduler.requests.get(cond_id)
            uncond_req = scheduler.requests.get(uncond_id)
            if (cond_req and uncond_req
                    and cond_req.num_computed_tokens != uncond_req.num_computed_tokens):
                faster = cond_id if cond_req.num_computed_tokens > uncond_req.num_computed_tokens else uncond_id
                to_preempt.append(faster)

    if to_preempt:
        logger.warning(
            "CFG pair split detected (to_preempt=%s) but co-preempt surgery is "
            "disabled on vLLM 0.20; relying on pair-hold + equalize.",
            to_preempt,
        )
    return


def _patch_scheduler() -> None:
    from vllm.v1.core.sched.scheduler import Scheduler

    _orig_init = Scheduler.__init__
    _orig_add = Scheduler.add_request
    _orig_finish = Scheduler.finish_requests
    _orig_schedule = Scheduler.schedule

    def _init(self, *args, **kwargs):
        _orig_init(self, *args, **kwargs)
        self._cfg_pairs: dict[str, dict[str, str]] = {}
        self._cfg_req_to_pair: dict[str, str] = {}

    def _add(self, request):
        _orig_add(self, request)
        pair_id = _get_cfg_pair_id(request)
        role = _get_cfg_role(request)
        if pair_id and role:
            self._cfg_pairs.setdefault(pair_id, {})[role] = request.request_id
            self._cfg_req_to_pair[request.request_id] = pair_id

    def _finish(self, request_ids, finished_status):
        if request_ids is None:
            result = _orig_finish(self, request_ids, finished_status)
            self._cfg_pairs.clear()
            self._cfg_req_to_pair.clear()
            return result

        if isinstance(request_ids, str):
            request_ids = {request_ids}
        else:
            request_ids = set(request_ids)

        partner_ids: set[str] = set()
        for req_id in request_ids:
            pair_id = self._cfg_req_to_pair.get(req_id)
            if pair_id is None:
                continue
            for _role, rid in self._cfg_pairs.get(pair_id, {}).items():
                if rid != req_id and rid in self.requests:
                    partner_ids.add(rid)

        all_ids = request_ids | partner_ids
        result = _orig_finish(self, all_ids, finished_status)

        for req_id in all_ids:
            pair_id = self._cfg_req_to_pair.pop(req_id, None)
            if pair_id:
                self._cfg_pairs.pop(pair_id, None)
        return result

    def _schedule(self):
        if not self._cfg_pairs:
            return _orig_schedule(self)

        _reorder_waiting_for_cfg(self)

        held = _hold_incomplete_pairs(self)

        orig_threshold = self.scheduler_config.long_prefill_token_threshold
        self.scheduler_config.long_prefill_token_threshold = (
            self.max_num_scheduled_tokens // 2
        )

        scheduler_output = _orig_schedule(self)

        self.scheduler_config.long_prefill_token_threshold = orig_threshold

        for req in reversed(held):
            self.waiting.prepend_request(req)

        _equalize_cfg_pair_progress(self, scheduler_output)
        _co_preempt_split_pairs(self, scheduler_output)
        return scheduler_output

    Scheduler.__init__ = _init
    Scheduler.add_request = _add
    Scheduler.finish_requests = _finish
    Scheduler.schedule = _schedule



def apply_cfg_patches() -> None:
    """Apply CFG scheduler monkey-patches to vLLM v1.  Call before ``LLM()``.

    Token synchronization (post-sampling copy of cond token to uncond slot)
    is handled by ``CFGLogitsProcessor._ensure_sample_patched()`` which runs
    inside each worker process automatically.
    """
    global _applied
    if _applied:
        return
    _applied = True
    logger.info("Applying CFG scheduler patches to vLLM v1")
    _patch_scheduler()