L0SG's picture
Upload Nemotron-Labs-Audex-2B
5e79b62 verified
Raw
History Blame Contribute Delete
9.67 kB
# 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.
"""
Classifier-Free Guidance (CFG) logits processor for vLLM v1.
Implements CFG by pairing conditional and unconditional requests in the same
batch. The processor blends their logits before sampling so both requests
produce identical tokens.
Usage:
Submit prompts in alternating pairs:
[cond_0, uncond_0, cond_1, uncond_1, ...]
Each request carries SamplingParams.extra_args:
cond: {"cfg_scale": 3.0, "cfg_role": "cond", "cfg_pair_id": "pair_0"}
uncond: {"cfg_scale": 3.0, "cfg_role": "uncond", "cfg_pair_id": "pair_0"}
Pass this processor to the vLLM engine:
LLM(..., logits_processors=[CFGLogitsProcessor])
"""
from __future__ import annotations
import logging
from typing import Optional
import torch
from vllm.config import VllmConfig
from vllm.sampling_params import SamplingParams
from vllm.v1.sample.logits_processor import BatchUpdate, LogitsProcessor, MoveDirectionality
logger = logging.getLogger(__name__)
class CFGLogitsProcessor(LogitsProcessor):
"""Blend conditional + unconditional logits for classifier-free guidance.
Pairs are matched by explicit ``cfg_pair_id`` in extra_args. For each pair
the processor computes:
blended = uncond_logits + cfg_scale * (cond_logits - uncond_logits)
and writes the result to *both* rows so the sampler picks the same token.
On first instantiation in each worker process, this class also patches
``GPUModelRunner._sample`` to copy the conditional sampled token to the
unconditional slot, guaranteeing identical token sequences. This must
live here (not in a separate monkey-patch module) because vLLM may
``spawn`` workers as fresh processes where main-process patches are lost.
"""
_sample_patched = False
@classmethod
def validate_params(cls, params: SamplingParams) -> None:
ea = params.extra_args
if not ea:
return
role = ea.get("cfg_role")
if role is not None and role not in ("cond", "uncond"):
raise ValueError(f"cfg_role must be 'cond' or 'uncond', got '{role}'")
scale = ea.get("cfg_scale")
if scale is not None and (not isinstance(scale, (int, float)) or scale < 1.0):
raise ValueError(f"cfg_scale must be >= 1.0, got {scale}")
def __init__(
self, vllm_config: VllmConfig, device: torch.device, is_pin_memory: bool
) -> None:
self._info: dict[int, dict] = {}
self._output_tokens: dict[int, list[int]] = {}
self._pairs: list[tuple[int, int, float]] = []
self._dirty = True
self._ensure_sample_patched()
@classmethod
def _ensure_sample_patched(cls) -> None:
"""Patch ``GPUModelRunner._sample`` to sync tokens after sampling.
Runs once per process (guarded by ``_sample_patched`` flag).
Because vLLM may spawn workers via ``multiprocessing.Process``,
main-process monkey-patches are invisible here -- so we patch
from within ``__init__`` which vLLM calls in every worker.
"""
if cls._sample_patched:
return
cls._sample_patched = True
from vllm.v1.worker.gpu_model_runner import GPUModelRunner
_orig_sample = GPUModelRunner._sample
def _sample_with_cfg_sync(self, logits, spec_decode_metadata):
sampler_output = _orig_sample(self, logits, spec_decode_metadata)
for proc in self.input_batch.logitsprocs.all:
if hasattr(proc, "_pairs") and proc._pairs:
for cond_idx, uncond_idx, _ in proc._pairs:
sampler_output.sampled_token_ids[uncond_idx] = (
sampler_output.sampled_token_ids[cond_idx]
)
break
return sampler_output
GPUModelRunner._sample = _sample_with_cfg_sync
logger.info(
"CFGLogitsProcessor: patched GPUModelRunner._sample "
"for post-sampling token sync (pid=%d)", __import__("os").getpid()
)
def is_argmax_invariant(self) -> bool:
return False
def _reset(self) -> None:
self._info.clear()
self._output_tokens.clear()
self._pairs.clear()
self._dirty = True
def update_state(self, batch_update: Optional[BatchUpdate]) -> None:
if batch_update is None:
return
for idx in batch_update.removed:
logger.debug("Removing idx=%d", idx)
self._info.pop(idx, None)
self._output_tokens.pop(idx, None)
if not self._info and batch_update.added:
self._reset()
for idx, params, _, output_token_ids in batch_update.added:
ea = params.extra_args if params else None
logger.debug(
"Adding idx=%d role=%s pair_id=%s output_len=%d",
idx,
ea.get("cfg_role") if ea else None,
ea.get("cfg_pair_id") if ea else None,
len(output_token_ids),
)
if ea and ea.get("cfg_role") in ("cond", "uncond"):
self._info[idx] = {
"role": ea["cfg_role"],
"cfg_scale": float(ea.get("cfg_scale", 1.0)),
"pair_id": ea.get("cfg_pair_id"),
}
self._output_tokens[idx] = output_token_ids
else:
self._info.pop(idx, None)
self._output_tokens.pop(idx, None)
self._dirty = True
if self._info:
for adx, bdx, direction in batch_update.moved:
logger.debug("Moving %d -> %d direction=%s", adx, bdx, direction)
a_val = self._info.pop(adx, None)
b_val = self._info.pop(bdx, None)
a_tok = self._output_tokens.pop(adx, None)
b_tok = self._output_tokens.pop(bdx, None)
if a_val is not None:
self._info[bdx] = a_val
if a_tok is not None:
self._output_tokens[bdx] = a_tok
if direction == MoveDirectionality.SWAP:
if b_val is not None:
self._info[adx] = b_val
if b_tok is not None:
self._output_tokens[adx] = b_tok
self._dirty = True
def _rebuild_pairs(self) -> None:
"""Match cond/uncond by ``cfg_pair_id``."""
by_pair: dict[str, dict[str, tuple[int, float]]] = {}
for idx, info in self._info.items():
pair_id = info.get("pair_id")
if pair_id is None:
continue
by_pair.setdefault(pair_id, {})[info["role"]] = (
idx,
info["cfg_scale"],
)
self._pairs = [
(roles["cond"][0], roles["uncond"][0], roles["cond"][1])
for roles in by_pair.values()
if "cond" in roles and "uncond" in roles
]
self._dirty = False
_apply_step = 0
_LOG_EVERY = 200
def apply(self, logits: torch.Tensor) -> torch.Tensor:
if not self._info:
return logits
if self._dirty:
self._rebuild_pairs()
do_log = (CFGLogitsProcessor._apply_step % self._LOG_EVERY == 0
and self._pairs)
CFGLogitsProcessor._apply_step += 1
for i, (cond_idx, uncond_idx, cfg_scale) in enumerate(self._pairs):
cond_toks = self._output_tokens.get(cond_idx)
uncond_toks = self._output_tokens.get(uncond_idx)
if cond_toks and uncond_toks and len(cond_toks) != len(uncond_toks):
logger.debug(
"CFG pair (%d, %d) output length mismatch: %d vs %d",
cond_idx, uncond_idx, len(cond_toks), len(uncond_toks),
)
if do_log and i == 0:
k = 5
cond_top = torch.topk(logits[cond_idx], k)
uncond_top = torch.topk(logits[uncond_idx], k)
blended = logits[uncond_idx] + cfg_scale * (
logits[cond_idx] - logits[uncond_idx]
)
logits[cond_idx] = blended
logits[uncond_idx] = blended
if do_log and i == 0:
blended_top = torch.topk(blended, k)
logger.warning(
"CFG probe step=%d scale=%.1f | "
"cond top%d: ids=%s vals=%s | "
"uncond top%d: ids=%s vals=%s | "
"blended top%d: ids=%s vals=%s",
CFGLogitsProcessor._apply_step - 1, cfg_scale,
k, cond_top.indices.tolist(),
[f"{v:.2f}" for v in cond_top.values.tolist()],
k, uncond_top.indices.tolist(),
[f"{v:.2f}" for v in uncond_top.values.tolist()],
k, blended_top.indices.tolist(),
[f"{v:.2f}" for v in blended_top.values.tolist()],
)
return logits