[Model Runner V2] Support custom logits processors (#56497)
Signed-off-by: David Cheung <d7cheung@gmail.com> Signed-off-by: Nick Hill <nickhill123@gmail.com> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Nick Hill <nickhill123@gmail.com>
This commit is contained in:
co-authored by
Claude
Nick Hill
parent
751f6807d9
commit
5302d1fe40
@@ -8,84 +8,40 @@ A "custom" logits processor is written by a user of vLLM and is loaded into vLLM
|
||||
|
||||
This document shows how to write, load and use a custom logits processor.
|
||||
|
||||
!!! note
|
||||
This document covers the Model Runner V2 (MRV2) interface. The legacy V1
|
||||
model runner (`VLLM_USE_V2_MODEL_RUNNER=0`) uses a different interface,
|
||||
`vllm.v1.sample.logits_processor.LogitsProcessor`, which is not covered
|
||||
here.
|
||||
|
||||
## Logits Processors Background
|
||||
|
||||
A logits processor adjusts the next-token probability distribution, usually with the intention of steering the model towards a desired type of behavior.
|
||||
|
||||
In vLLM, logits processors operate at batch granularity. During a given engine step, the logits processor consumes a `(num_requests) x (vocab_size)` tensor of raw logits output by the model. For all requests which enable the logits processor, the logits processor applies a transformation to the corresponding row of the logits tensor, while leaving other rows unmodified. The transformed logits tensor is then passed to softmax.
|
||||
In vLLM, logits processors operate at batch granularity. During a given engine step, the logits processor consumes a `(num_logits_rows) x (vocab_size)` tensor of raw logits output by the model. Note that a logits row is not a request: rows are reordered every step, and under speculative decoding a request owns one row per draft token. The logits processor applies a transformation to the rows of the logits tensor, while leaving other rows unmodified. The transformed logits tensor is then used for sampling.
|
||||
|
||||
## Creating a Custom Logits Processor
|
||||
|
||||
Custom logits processors must subclass `vllm.v1.sample.logits_processor.LogitsProcessor` and define (at minimum) the following methods:
|
||||
Custom logits processors must subclass `vllm.v1.worker.gpu.sample.logits_processor.LogitsProcessor` and define (at minimum) the following methods:
|
||||
|
||||
* `validate_params(cls, sampling_params: SamplingParams)`:
|
||||
* Raise `ValueError` if `SamplingParams` has invalid arguments (especially custom arguments) used by logits processor.
|
||||
* When request is sent to entrypoint, `validate_params()` will validate `SamplingParams` and refuse request with invalid arguments.
|
||||
* **Note:** it's important to implement `validate_params()` to prevent invalid parameters for custom logits processor. Otherwise requests with invalid parameters can cause unexpected behaviour in custom logits processor.
|
||||
|
||||
* `__init__(self, vllm_config: VllmConfig, device: torch.device, is_pin_memory: bool)`
|
||||
* `__init__(self, vllm_config: VllmConfig, req_states: LogitsProcRequestState)`:
|
||||
* `vllm_config`: engine configuration data structure
|
||||
* `device`: hardware accelerator device info
|
||||
* `is_pin_memory`: flag indicating whether pin memory is available to support logits processor implementation
|
||||
* `req_states`: a narrow, read-only view of the persistent batch, exposing the on-device token history (`all_token_ids`, `prompt_len`, `prefill_len`, `total_len`) plus `device`, `max_num_reqs` and `vocab_size`
|
||||
|
||||
* `apply(self, logits: torch.Tensor) -> torch.Tensor`:
|
||||
* Consume a `(num_requests) x (vocab_size)` logits tensor (`logits`)
|
||||
* `add_request(self, req_idx, sampling_params) -> bool`:
|
||||
* Initialize per-slot state for a request entering the batch. Slots are recycled through a free list, so per-slot state must be fully (re)initialized here; there is no removal hook, since freed slots are never read
|
||||
* Return whether this processor modifies logits for the request. The sampler skips the logits-processing pipeline for batches in which no request needs it
|
||||
|
||||
* `apply_staged_writes(self) -> None` (optional):
|
||||
* Flush host-side writes staged by `add_request()` to the device; called once per step before the forward pass
|
||||
|
||||
* `validate_params(cls, sampling_params) -> None` (optional classmethod):
|
||||
* Raise `ValueError` for invalid per-request arguments (especially custom arguments); runs at request admission, so invalid arguments fail the request instead of reaching the sampler
|
||||
|
||||
* `apply(self, logits: torch.Tensor, ctx: LogitsContext) -> torch.Tensor`:
|
||||
* Consume a `(num_logits_rows) x (vocab_size)` logits tensor and the step's batch layout (`ctx`: row-to-slot mappings, `input_ids`, positions)
|
||||
* Apply logits processor transformation at batch granularity
|
||||
* Return a transformed `(num_requests) x (vocab_size)` logits tensor
|
||||
* You can modify the input logits processors in-place or out-of-place; in-place is more memory-efficient
|
||||
|
||||
* `is_argmax_invariant(self) -> bool`:
|
||||
* Return `True` if the logits processor is argmax invariant (never changes what is the highest-logit-value token ID for a given request), `False` if the logits processor may modify argmax
|
||||
* `is_argmax_invariant()` is evaluated once at startup; if `True`, vLLM will skip applying this logits processor in a given step when all requests use greedy sampling
|
||||
|
||||
* `update_state(self, batch_update: Optional["BatchUpdate"]) -> None`:
|
||||
* Consume a `BatchUpdate` data structure representing persistent batch state changes at the beginning of the current engine step
|
||||
* Use the `BatchUpdate` members to update logits processor internal state
|
||||
* **Note:** batch update data structure may be `None`, signaling no change to the batch constituents. In this case, the LogitsProcessor might still want to update its state based on the updated `output_token_ids` lists that it could have retained when they were added.
|
||||
|
||||
### How the vLLM engine builds the `BatchUpdate` data structure
|
||||
|
||||
!!! important
|
||||
Some logits processors design changes are still in progress. We expect
|
||||
that in the future you will not need to account for batch state changes
|
||||
when implementing a logits processor, and the information in this section
|
||||
will become irrelevant.
|
||||
|
||||
Logits processor `update_state()` implementations should assume the following model for how the model runner updates persistent batch state (expressed here in terms of the `BatchUpdate` abstraction):
|
||||
|
||||
1. Identify indices of requests which finished in the current engine step
|
||||
|
||||
2. Identify new requests introduced in the current step
|
||||
|
||||
3. Use Add operations to replace as many finished requests with new requests, in order of increasing index of the replaced request starting with the lowest index
|
||||
|
||||
4. Based on the relative number of new and finished requests:
|
||||
|
||||
1. If the numbers of new and finished requests are the same, proceed to next step
|
||||
|
||||
2. *If there are more new requests than finished requests:* apply Add operations to extend the batch with the remaining new requests which did not replace finished requests. Assign consecutive indices to these new requests, starting with `current_max_batch_index + 1`
|
||||
|
||||
3. *If there are fewer new requests than finished requests:*
|
||||
|
||||
* Apply Remove operations to finished requests which were not replaced with new requests. These removed request indices will necessarily be greater than the greatest index of the finished requests which were replaced in the previous step. The Removes may leave the batch in a non-contiguous state
|
||||
|
||||
* **"Condense" the batch to be contiguous:** starting with the lowest-index empty slot (which was caused by a Remove), apply a Unidirectional Move from the current highest non-empty slot in the batch to fill the empty slot. Proceed with additional Unidirectional Move operations in order of increasing empty slot destination index and decreasing non-empty slot source index until the batch is contiguous
|
||||
|
||||
* **Shrink the batch:** a side effect of condensing the batch is that empty slots resulting from Remove operations are grouped in a contiguous block at the end of the batch array. Thus, after condensing, update `BatchUpdate.batch_size` to reflect the number of non-empty slots
|
||||
|
||||
5. Reorder the batch for improved efficiency. Depending on the attention backend implementation and the current characteristics of the batch, zero or more Swap Move operations may be applied to reorder the batch
|
||||
|
||||
Notes:
|
||||
|
||||
* A logits processor `update_state()` method must process batch update operations in the following order: removes, adds, moves
|
||||
|
||||
* The index argument for Add operations refers to the index *at the time the Add occurred*, i.e. before any Move operations
|
||||
* Example: if a request is Added at index 5 and then swapped with index 3, the Add operation in `BatchUpdate.added` will be associated with index 5 not 3
|
||||
* In other words Move operations can be assumed to be applied after Adds and Removes
|
||||
|
||||
* Move operations can be assumed to be applied in the order in which they appear in `BatchUpdate.moved`
|
||||
|
||||
* If there are no new/finished requests and there is no batch reordering, then the batch update for the logits processors will be `None`
|
||||
* Return a transformed logits tensor. You can modify the input logits in-place or out-of-place; in-place is more memory-efficient
|
||||
|
||||
### Passing Custom Argument to a Custom Logits Processor
|
||||
|
||||
@@ -93,7 +49,7 @@ Unlike built-in logits processors, custom logits processors may require configur
|
||||
|
||||
### Example Custom Logits Processor Implementation
|
||||
|
||||
The contrived example below implements a custom logits processor which consumes a `(num\_requests) \times (vocab\_size)` logits tensor and masks out all tokens except for one (`target_token`) with `float(-inf)`. The logits processor is disabled for any request that does not specify `target_token`. To determine whether the logits processor is enabled and which token to leave unmasked, the logits processor checks `SamplingParams.extra_args` for a `target_token` custom argument associated with each request:
|
||||
The contrived example below implements a custom logits processor which masks out all tokens except for one (`target_token`) for the requests that enable it. The processor is disabled for any request that does not specify `target_token`; `add_request()` reports this via its return value, so the sampler can skip the logits-processing pipeline entirely when no request enables the processor. To determine whether the logits processor is enabled and which token to leave unmasked, the processor checks `SamplingParams.extra_args` for a `target_token` custom argument associated with each request:
|
||||
|
||||
??? code "Example custom logits processor definition"
|
||||
|
||||
@@ -101,193 +57,55 @@ The contrived example below implements a custom logits processor which consumes
|
||||
import torch
|
||||
from vllm.config import VllmConfig
|
||||
from vllm.sampling_params import SamplingParams
|
||||
from vllm.v1.sample.logits_processor import (BatchUpdate,
|
||||
LogitsProcessor,
|
||||
MoveDirectionality)
|
||||
|
||||
class DummyLogitsProcessor(LogitsProcessor):
|
||||
"""Fake logit processor to support unit testing and examples"""
|
||||
|
||||
@classmethod
|
||||
def validate_params(cls, params: SamplingParams):
|
||||
target_token: int | None = params.extra_args and params.extra_args.get(
|
||||
"target_token"
|
||||
)
|
||||
if target_token is not None and not isinstance(target_token, int):
|
||||
raise ValueError(f"target_token value {target_token} is not int")
|
||||
|
||||
def __init__(self, vllm_config: "VllmConfig", device: torch.device,
|
||||
is_pin_memory: bool):
|
||||
self.req_info: dict[int, int] = {}
|
||||
|
||||
def is_argmax_invariant(self) -> bool:
|
||||
"""Never impacts greedy sampling"""
|
||||
return False
|
||||
|
||||
def update_state(self, batch_update: BatchUpdate | None):
|
||||
if not batch_update:
|
||||
return
|
||||
|
||||
# Process added requests.
|
||||
for index, params, _, _ in batch_update.added:
|
||||
assert params is not None
|
||||
self.validate_params(params)
|
||||
if params.extra_args and (target_token :=
|
||||
params.extra_args.get("target_token")):
|
||||
self.req_info[index] = target_token
|
||||
else:
|
||||
self.req_info.pop(index, None)
|
||||
|
||||
if self.req_info:
|
||||
# Process removed requests.
|
||||
for index in batch_update.removed:
|
||||
self.req_info.pop(index, None)
|
||||
|
||||
# Process moved requests, unidirectional move (a->b) and swap
|
||||
# (a<->b)
|
||||
for adx, bdx, direct in batch_update.moved:
|
||||
a_val = self.req_info.pop(adx, None)
|
||||
b_val = self.req_info.pop(bdx, None)
|
||||
if a_val is not None:
|
||||
self.req_info[bdx] = a_val
|
||||
if direct == MoveDirectionality.SWAP and b_val is not None:
|
||||
self.req_info[adx] = b_val
|
||||
|
||||
def apply(self, logits: torch.Tensor) -> torch.Tensor:
|
||||
if not self.req_info:
|
||||
return logits
|
||||
|
||||
# Save target values before modification
|
||||
cols = torch.tensor(
|
||||
list(self.req_info.values()), dtype=torch.long, device=logits.device
|
||||
)
|
||||
rows = torch.tensor(
|
||||
list(self.req_info.keys()), dtype=torch.long, device=logits.device
|
||||
)
|
||||
values_to_keep = logits[rows, cols].clone()
|
||||
|
||||
# Mask all but target tokens
|
||||
logits[rows] = float('-inf')
|
||||
logits[rows, cols] = values_to_keep
|
||||
|
||||
return logits
|
||||
|
||||
```
|
||||
|
||||
In the rest of this document, we will use `DummyLogitsProcessor` as an example of a custom logits processor.
|
||||
|
||||
The `DummyLogitsProcessor.update_state()` implementation maintains a "sparse" representation of the batched requests in the `self.req_info` dictionary: only those requests which specify a `target_token` value have a key in the dictionary. `update_state()` adjusts the stored request indices and `target_token` values (keys and values respectively in `self.req_info`) in response to Add, Remove and Move operations against the persistent batch.
|
||||
|
||||
### Wrapping an Existing Request-Level Logits Processor
|
||||
|
||||
Although the vLLM engine applies logits processors at batch granularity, some users may want to use vLLM with a "request-level" logits processor implementation - an implementation which operates on individual requests. This will be especially true if your logits processor was developed for vLLM version 0, which required it to be a `Callable` (as described [here][vllm.logits_process]) conforming to the following type annotation:
|
||||
|
||||
``` python
|
||||
RequestLogitsProcessor = Union[
|
||||
|
||||
# (output token ids, logits tensor) -> logits tensor
|
||||
Callable[[list[int], Tensor], Tensor],
|
||||
|
||||
# (prompt token ids, output token ids, logits tensor) -> logits tensor
|
||||
Callable[[list[int], list[int], Tensor], Tensor],
|
||||
]
|
||||
```
|
||||
|
||||
While request-level logits processors are explicitly *not* supported in the vLLM engine, vLLM *does* provide a convenient process to wrap an existing `Callable` request-level logits processor and create a batch-level logits processor that is compatible with vLLM. The `Callable` must conform to the type annotation above; if your request-level logits processor has a different interface, then in order to wrap it, you may need to modify it or implement an additional wrapper layer to comply with the interface specification above.
|
||||
|
||||
You can wrap the request-level logits processor by subclassing `AdapterLogitsProcessor` as shown in the example below (in this example, `DummyPerReqLogitsProcessor` is a stand-in for your request-level logits processor which needs to be wrapped.):
|
||||
|
||||
* Override `AdapterLogitsProcessor.validate_params(cls,params)` to validate request's sampling parameters.
|
||||
|
||||
* Override `AdapterLogitsProcessor.is_argmax_invariant(self)` to accurately reflect whether your request-level logits processor may impact which token has the highest-value logit.
|
||||
|
||||
* Override `AdapterLogitsProcessor.new_req_logits_processor(self,params)` to create a new request-level logits processor instance from a `SamplingParams` instance:
|
||||
|
||||
??? code "Example of Wrapping a Request-Level Logits Processor"
|
||||
|
||||
``` python
|
||||
...
|
||||
|
||||
from vllm.v1.sample.logits_processor import (
|
||||
AdapterLogitsProcessor, # Wrapper base-class
|
||||
RequestLogitsProcessor, # Request-level logitsproc type annotation
|
||||
from vllm.v1.worker.gpu.sample.logits_processor import (
|
||||
LogitsProcRequestState,
|
||||
LogitsContext,
|
||||
LogitsProcessor,
|
||||
)
|
||||
|
||||
...
|
||||
|
||||
# Stand-in for your request-level logits processor:
|
||||
class DummyPerReqLogitsProcessor:
|
||||
"""The request-level logits processor masks out all logits except the
|
||||
token id identified by `target_token`"""
|
||||
class TargetTokenLogitsProcessor(LogitsProcessor):
|
||||
"""Masks out all tokens except `target_token` (a per-request custom
|
||||
argument); requests without it are left alone."""
|
||||
|
||||
def __init__(self, target_token: int) -> None:
|
||||
"""Specify `target_token`"""
|
||||
self.target_token = target_token
|
||||
def __init__(self, vllm_config: "VllmConfig", req_states: LogitsProcRequestState):
|
||||
# Per-slot target; -1 means disabled. Staged on the host and
|
||||
# flushed to the device once per step in apply_staged_writes().
|
||||
self.target_token = torch.full(
|
||||
(req_states.max_num_reqs,), -1, dtype=torch.int64
|
||||
)
|
||||
self.target_token_dev = torch.full(
|
||||
(req_states.max_num_reqs,), -1, dtype=torch.int64, device=req_states.device
|
||||
)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
output_ids: list[int],
|
||||
logits: torch.Tensor,
|
||||
) -> torch.Tensor:
|
||||
val_to_keep = logits[self.target_token].item()
|
||||
logits[:] = float("-inf")
|
||||
logits[self.target_token] = val_to_keep
|
||||
def add_request(self, req_idx: int, sampling_params: SamplingParams) -> bool:
|
||||
target = (sampling_params.extra_args or {}).get("target_token")
|
||||
self.target_token[req_idx] = target if target is not None else -1
|
||||
return target is not None
|
||||
|
||||
def apply_staged_writes(self) -> None:
|
||||
self.target_token_dev.copy_(self.target_token, non_blocking=True)
|
||||
|
||||
def apply(self, logits: torch.Tensor, ctx: LogitsContext) -> torch.Tensor:
|
||||
# ctx.expanded_idx_mapping maps each logits row to its request slot.
|
||||
cols = self.target_token_dev[ctx.expanded_idx_mapping.long()]
|
||||
rows = torch.nonzero(cols >= 0).squeeze(1)
|
||||
if rows.numel() == 0:
|
||||
return logits
|
||||
kept = logits[rows, cols[rows]].clone()
|
||||
logits[rows] = float("-inf")
|
||||
logits[rows, cols[rows]] = kept
|
||||
return logits
|
||||
|
||||
...
|
||||
|
||||
# Example of wrapping the request-level logits processor:
|
||||
class WrappedPerReqLogitsProcessor(AdapterLogitsProcessor):
|
||||
"""Example of wrapping a fake request-level logit processor to create a
|
||||
batch-level logits processor"""
|
||||
|
||||
@classmethod
|
||||
def validate_params(cls, params: SamplingParams):
|
||||
target_token: Any | None = params.extra_args and params.extra_args.get(
|
||||
"target_token"
|
||||
)
|
||||
if target_token is not None and not isinstance(target_token, int):
|
||||
raise ValueError(
|
||||
f"target_token value {target_token} is not int"
|
||||
)
|
||||
|
||||
def is_argmax_invariant(self) -> bool:
|
||||
return False
|
||||
|
||||
def new_req_logits_processor(
|
||||
self,
|
||||
params: SamplingParams,
|
||||
) -> Optional[RequestLogitsProcessor]:
|
||||
"""This method returns a new request-level logits processor, customized
|
||||
to the `target_token` value associated with a particular request.
|
||||
|
||||
Returns None if the logits processor should not be applied to the
|
||||
particular request. To use the logits processor the request must have
|
||||
a "target_token" custom argument with an integer value.
|
||||
|
||||
Args:
|
||||
params: per-request sampling params
|
||||
|
||||
Returns:
|
||||
`Callable` request logits processor, or None
|
||||
"""
|
||||
target_token: Any | None = params.extra_args and params.extra_args.get(
|
||||
"target_token"
|
||||
)
|
||||
if target_token is None:
|
||||
return None
|
||||
return DummyPerReqLogitsProcessor(target_token)
|
||||
```
|
||||
|
||||
!!! note
|
||||
Your `new_req_logits_processor()` override can return `None` to signal that the wrapped logits processor should not be applied to the request in question.
|
||||
|
||||
Once you have created a custom subclass (like `WrappedPerReqLogitsProcessor`) which wraps your request level logits processor, you can pass the custom subclass to vLLM via any of the methods described in the following section.
|
||||
Per-request state is keyed by the request slot index, and slots are recycled through a free list. The example keeps a per-slot state where `-1` means disabled; note that `add_request()` overwrites the slot's entry unconditionally, which is what keeps recycled slots from leaking state between requests.
|
||||
|
||||
## Ways to Load Your Custom Logits Processor in vLLM
|
||||
|
||||
Logits processors are loaded at initialization. Critically, the set of loaded logits processors cannot be modified after the vLLM engine finishes loading, and new logits processors cannot be loaded on-demand for individual requests.
|
||||
|
||||
Loaded classes are validated against the Model Runner V2 interface; a class that does not subclass `vllm.v1.worker.gpu.sample.logits_processor.LogitsProcessor` is rejected with a clear error.
|
||||
|
||||
This section details different ways of making your logits processor visible to vLLM and triggering vLLM to load your logits processor.
|
||||
|
||||
### Method 1: Pass the Custom Logits Processor Fully-Qualified Class Name (FQCN) to vLLM at Initialization Time
|
||||
@@ -314,7 +132,7 @@ See examples below:
|
||||
# Pass in FQCN
|
||||
llm = LLM(
|
||||
model="facebook/opt-125m",
|
||||
logits_processors=["your.module.path:DummyLogitsProcessor"],
|
||||
logits_processors=["your.module.path:TargetTokenLogitsProcessor"],
|
||||
)
|
||||
```
|
||||
|
||||
@@ -323,14 +141,14 @@ See examples below:
|
||||
``` python
|
||||
# Pass in FQCN
|
||||
engine_args = AsyncEngineArgs(model="facebook/opt-125m",
|
||||
logits_processors=["your.module.path:DummyLogitsProcessor"])
|
||||
logits_processors=["your.module.path:TargetTokenLogitsProcessor"])
|
||||
async_llm = AsyncLLM.from_engine_args(engine_args)
|
||||
```
|
||||
|
||||
??? code "Passing custom logits processor FQCN to vLLM server via CLI"
|
||||
|
||||
```bash
|
||||
vllm serve facebook/opt-125m --logits_processors your.module.path:DummyLogitsProcessor
|
||||
vllm serve facebook/opt-125m --logits_processors your.module.path:TargetTokenLogitsProcessor
|
||||
```
|
||||
|
||||
### Method 2: Automatically Detect Custom Logits Processors Installed in Your Python Environment As Entry Points
|
||||
@@ -345,7 +163,7 @@ Suppose that you have developed a Python package that holds your custom logits p
|
||||
|
||||
``` toml
|
||||
[project.entry-points."vllm.logits_processors"]
|
||||
dummy_logits_processor = "your.module.path:DummyLogitsProcessor"
|
||||
target_token_logits_processor = "your.module.path:TargetTokenLogitsProcessor"
|
||||
```
|
||||
|
||||
Once your package is installed, your custom logits processor will be loaded automatically whenever vLLM is initialized. You do *not* need to pass the custom logits processor to the `LLM` or `AsyncLLM` constructors or to the vLLM server explicitly at initialization time if your logits processor is exposed as an entry point.
|
||||
@@ -361,26 +179,26 @@ You can pass one or more custom logits processor class objects to the `LLM` and
|
||||
|
||||
``` python
|
||||
# Import custom logits processor
|
||||
from some.module import DummyLogitsProcessor
|
||||
from some.module import TargetTokenLogitsProcessor
|
||||
|
||||
# ...or...
|
||||
|
||||
# Define custom logits processor locally
|
||||
from vllm.v1.sample.logits_processor import LogitsProcessor
|
||||
from vllm.v1.worker.gpu.sample.logits_processor import LogitsProcessor
|
||||
|
||||
class DummyLogitsProcessor(LogitsProcessor):
|
||||
# See DummyLogitsProcessor implementation above
|
||||
class TargetTokenLogitsProcessor(LogitsProcessor):
|
||||
# See TargetTokenLogitsProcessor implementation above
|
||||
...
|
||||
|
||||
# Pass class object to LLM constructor
|
||||
llm = LLM(
|
||||
model="facebook/opt-125m",
|
||||
logits_processors=[DummyLogitsProcessor],
|
||||
logits_processors=[TargetTokenLogitsProcessor],
|
||||
)
|
||||
|
||||
# Pass class object to AsyncLLM constructor
|
||||
engine_args = AsyncEngineArgs(model="facebook/opt-125m",
|
||||
logits_processors=[DummyLogitsProcessor])
|
||||
logits_processors=[TargetTokenLogitsProcessor])
|
||||
async_llm = AsyncLLM.from_engine_args(engine_args)
|
||||
```
|
||||
|
||||
@@ -388,7 +206,7 @@ You can pass one or more custom logits processor class objects to the `LLM` and
|
||||
|
||||
The design of the custom logits processor determines whether the logits processor must be enabled/disabled for a given request, and what arguments must be provided to configure the logits processor.
|
||||
|
||||
The examples below show how a user would pass a custom argument (`target_token`) to `DummyLogitsProcessor` in order to (1) enable the logits processor for that particular request and (2) control the logits processor's behavior.
|
||||
The examples below show how a user would pass a custom argument (`target_token`) to `TargetTokenLogitsProcessor` in order to (1) enable the logits processor for that particular request and (2) control the logits processor's behavior.
|
||||
|
||||
??? code "vLLM REST API: configure custom logits processor for a request"
|
||||
|
||||
@@ -419,7 +237,7 @@ The examples below show how a user would pass a custom argument (`target_token`)
|
||||
??? code "Offline: configure custom logits processor for an `LLM` request"
|
||||
|
||||
``` python
|
||||
outputs_logitproc = llm.generate("your prompt",
|
||||
outputs_logitproc = llm.generate("your prompt",
|
||||
SamplingParams(...,
|
||||
extra_args={"target_token": 67}))
|
||||
```
|
||||
@@ -438,31 +256,14 @@ The examples below show how a user would pass a custom argument (`target_token`)
|
||||
|
||||
## Best Practices for Writing Custom Logits Processors
|
||||
|
||||
Once vLLM loads a logits processor during initialization, then vLLM will invoke `update_state()` and `apply()` against that logits processor in every engine step. Both methods operate on all requests which currently reside in the vLLM persistent batch. Thus, it is important to implement these methods efficiently.
|
||||
|
||||
* Write efficient `apply()` and `update_state()` implementations in light of the fact that logits processors operate at batch granularity
|
||||
* For example, you may be able to use efficient vectorized operations to implement `apply()` or update internal state vectors in `update_state()`
|
||||
* However, if you think that a logits processor may be used infrequently, it may be appropriate to use a "sparse" representation of request state i.e. the class can represent request configuration using a dictionary which only stores metadata about requests that enable the logits processor
|
||||
* **Note:** wrapped request-level logits processors do not need to implement `apply()` and `update_state()`; the default `AdapterLogitsProcessor.update_state()` implementation maintains a sparse representation of request state, wherein requests for which `new_req_logits_processor()` returns `None` are not represented in the base-class state dictionary. The default implementation of `AdapterLogitsProcessor.apply()` applies the request-level logits processor to each row of input logits sequentially and assembles the output logits tensor. If the performance of this `AdapterLogitsProcessor` default implementation is insufficient, then avoid wrapping your request-level logits processor and instead re-implement it as a `LogitsProcessor` subclass with optimized `apply()` and `update_state()` implementations that operate at batch granularity
|
||||
* Write an efficient `apply()` implementation in light of the fact that logits processors operate at batch granularity. For example, you may be able to use efficient vectorized operations to implement `apply()`, and to stage per-request state into contiguous tensors in `add_request()` instead of per-row Python loops
|
||||
|
||||
* It is up to the logits processor author to determine:
|
||||
|
||||
1. **The per-request attributes which configure the logits processor's behavior against that request.** Your custom logits processor's `update_state()` override determines how `SamplingParams` fields are mapped into logits processor state
|
||||
1. **The per-request attributes which configure the logits processor's behavior against that request.** Your custom logits processor's `add_request()` override determines how `SamplingParams` fields are mapped into logits processor state
|
||||
|
||||
* **Note:** for wrapped request-level logits processors, `new_req_logits_processor()` determines how `SamplingParams` fields are used to initialize a request-level logits processor instance.
|
||||
2. **The conditions under which the logits processor is or is not enabled on a per-request basis.** Unless your intention is for the custom logits processor to act on all requests all the time, you should write your logits processor in such a way that it is possible to disable the logits processor for a given request, i.e. by defaulting an argument to `None` or by passing in a specific do-nothing argument value i.e. `0.0`. Return `False` from `add_request()` for such requests so that the sampler can skip the logits-processing pipeline for batches in which no request needs it
|
||||
|
||||
2. **The conditions under which the logits processor is or is not enabled on a per-request basis.** Unless your intention is for the custom logits processor to act on all requests all the time, you should write your logits processor in such a way that it is possible to disable the logits processor for a given request, i.e. by defaulting an argument to `None` or by passing in a specific do-nothing argument value i.e. `0.0`. Try to save compute and memory for requests which disable the logits processor
|
||||
* Stage host-side writes in `add_request()` and flush them once per step in `apply_staged_writes()`, instead of writing device tensors directly; this is the discipline every built-in sampler state follows
|
||||
|
||||
* **Note:** for wrapped per-request logits processors, the default `AdapterLogitsProcessor.update_state()` implementation ensures that the request-level logits processor is disabled when `new_req_logits_processor()` returns `None` for that request
|
||||
|
||||
3. **The conditions under which the logits processor is short-circuited at the batch level.** Even if you have defined a way to disable the custom logits processor at the request level, it may be difficult to translate this into compute savings i.e. if your `update_state()` and `apply()` implementations use efficient vectorized implementations that operate on the whole persistent batch in a single command. For example, you cannot skip an entire vectorized operation in `apply()` just because one request disabled the logits processor. To save compute in the edge-case where no running requests utilize the custom logits processor, we recommend designing `apply()` to return the unmodified input tensor if all requests have the logits processor disabled. Similarly, consider whether steps can be skipped in `update_state()` if no requests enable the logits processor
|
||||
|
||||
* Additionally, an easy way to save compute in `update_state()` is to exit early when the `batch_update` is `None`
|
||||
|
||||
* **Note:** for wrapped per-request logits processors, the `AdapterLogitsProcessor` base-class implements the above optimizations by default
|
||||
|
||||
* Ensure that the logits processor `update_state` method discards information about finished requests (i.e. requests which are replaced by an Add or which are subject to a Remove)
|
||||
|
||||
* **Note:** for wrapped per-request logits processors, the `AdapterLogitsProcessor` base-class handles this by default
|
||||
|
||||
* `is_argmax_invariant()` can be hard-coded to `True` or `False` if the logits processor has consistent behavior. However, the argmax invariance may also be determined programmatically (i.e. if your logits processor is user-customizable in some way that impacts whether the logits processor is argmax invariant). For this reason, `is_argmax_invariant()` is not a class method
|
||||
* Since there is no removal hook, `add_request()` must fully (re)initialize the slot's state; do not rely on state left behind by the slot's previous occupant
|
||||
|
||||
@@ -652,6 +652,15 @@ def test_v2_model_runner_supports_extract_hidden_states():
|
||||
assert config._get_v2_model_runner_unsupported_features() == []
|
||||
|
||||
|
||||
def test_v2_model_runner_supports_custom_logits_processors():
|
||||
config = VllmConfig()
|
||||
config.model_config = cast(
|
||||
ModelConfig, SimpleNamespace(logits_processors=["a.b:C"])
|
||||
)
|
||||
|
||||
assert config._get_v2_model_runner_unsupported_features() == []
|
||||
|
||||
|
||||
def test_dflash2_draft_forces_v2_model_runner():
|
||||
"""A DFlash2 draft must reach the V2 speculator, the only one that runs its
|
||||
candidate selector; on V1 it would draft as DFlash1 without raising."""
|
||||
|
||||
@@ -51,6 +51,14 @@ def test_non_diffusion_models_unaffected():
|
||||
params.verify(MockModelConfig(), None, None, None)
|
||||
|
||||
|
||||
def test_verify_leaves_logits_processors_to_admission():
|
||||
"""verify() is runner-agnostic; LP validation lives in the admission
|
||||
layer, so an unimportable FQCN must not fail verify()."""
|
||||
SamplingParams().verify(
|
||||
MockModelConfig(logits_processors=["no.such:Cls"]), None, None, None
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("value", [-(2**63) - 1, 2**64])
|
||||
def test_extra_args_rejects_nested_integer_overflow(value):
|
||||
"""Reject extension values before they reach the engine transport."""
|
||||
|
||||
@@ -85,6 +85,7 @@ def _validate(enable_trace_replay: bool) -> None:
|
||||
speculative_config=None,
|
||||
structured_outputs_config=None,
|
||||
tokenizer=None,
|
||||
validate_logits_processors_params=lambda params: None,
|
||||
)
|
||||
params = SamplingParams(trace_decode_token_ids=[1, 2, 3])
|
||||
with patch.object(SamplingParams, "verify"):
|
||||
|
||||
@@ -162,6 +162,9 @@ def test_custom_logitsprocs(monkeypatch, logitproc_source: CustomLogitprocSource
|
||||
"""
|
||||
# Test that logitproc info is passed to workers
|
||||
monkeypatch.setenv("VLLM_ENABLE_V1_MULTIPROCESSING", "1")
|
||||
# These tests exercise the V1-interface logits processor; Model Runner V2
|
||||
# rejects V1-interface processors at load time by design.
|
||||
monkeypatch.setenv("VLLM_USE_V2_MODEL_RUNNER", "0")
|
||||
set_random_seed(40)
|
||||
|
||||
# Choose LLM args based on logitproc source
|
||||
@@ -217,6 +220,9 @@ def test_custom_logitsprocs_req(monkeypatch):
|
||||
"""
|
||||
# Test that logitproc info is passed to workers
|
||||
monkeypatch.setenv("VLLM_ENABLE_V1_MULTIPROCESSING", "1")
|
||||
# These tests exercise the V1-interface logits processor; Model Runner V2
|
||||
# rejects V1-interface processors at load time by design.
|
||||
monkeypatch.setenv("VLLM_USE_V2_MODEL_RUNNER", "0")
|
||||
set_random_seed(40)
|
||||
_run_test(
|
||||
{"logits_processors": [WrappedPerReqLogitsProcessor]}, logitproc_loaded=True
|
||||
@@ -261,6 +267,9 @@ def test_rejects_custom_logitsprocs(
|
||||
|
||||
"""
|
||||
monkeypatch.setenv("VLLM_ENABLE_V1_MULTIPROCESSING", "0")
|
||||
# These tests exercise V1-interface rejection and V1 runner internals;
|
||||
# Model Runner V2 rejects V1-interface processors at load time by design.
|
||||
monkeypatch.setenv("VLLM_USE_V2_MODEL_RUNNER", "0")
|
||||
set_random_seed(40)
|
||||
|
||||
test_params: dict[str, dict[str, Any]] = {
|
||||
|
||||
@@ -79,6 +79,9 @@ def server(default_server_args, request, monkeypatch):
|
||||
"""
|
||||
# Test that logitproc info is passed to workers
|
||||
monkeypatch.setenv("VLLM_ENABLE_V1_MULTIPROCESSING", "1")
|
||||
# These tests exercise the V1-interface logits processor; Model Runner V2
|
||||
# rejects V1-interface processors at load time by design.
|
||||
monkeypatch.setenv("VLLM_USE_V2_MODEL_RUNNER", "0")
|
||||
|
||||
if request.param:
|
||||
# Launch server, append FQCN argument, inject dummy logitproc module
|
||||
|
||||
@@ -14,6 +14,10 @@ if not torch.cuda.is_available():
|
||||
|
||||
from vllm.sampling_params import SamplingParams
|
||||
from vllm.v1.worker.gpu.sample.bad_words import BadWordsState
|
||||
from vllm.v1.worker.gpu.sample.logits_processor import (
|
||||
LogitsContext,
|
||||
LogitsProcRequestState,
|
||||
)
|
||||
from vllm.v1.worker.gpu.states import RequestState
|
||||
|
||||
DEVICE = torch.device("cuda")
|
||||
@@ -47,7 +51,7 @@ def _make_state(bad_words_token_ids: list[list[int]]) -> tuple[BadWordsState, in
|
||||
req_states.apply_staged_writes()
|
||||
|
||||
req_idx = req_states.req_id_to_index["req"]
|
||||
state = BadWordsState(req_states)
|
||||
state = BadWordsState(None, LogitsProcRequestState.from_request_state(req_states))
|
||||
state.add_request(req_idx, SamplingParams(_bad_words_token_ids=bad_words_token_ids))
|
||||
state.apply_staged_writes()
|
||||
return state, req_idx
|
||||
@@ -61,16 +65,55 @@ def _apply(bad_words_token_ids: list[list[int]]) -> torch.Tensor:
|
||||
expanded_idx_mapping = torch.tensor(
|
||||
[req_idx] * num_logits, dtype=torch.int32, device=DEVICE
|
||||
)
|
||||
state.apply_bad_words(
|
||||
state.apply(
|
||||
logits,
|
||||
expanded_idx_mapping,
|
||||
idx_mapping_np,
|
||||
torch.tensor(INPUT_IDS, dtype=torch.int32, device=DEVICE),
|
||||
torch.tensor(LOCAL_POS, dtype=torch.int32, device=DEVICE),
|
||||
LogitsContext(
|
||||
expanded_idx_mapping=expanded_idx_mapping,
|
||||
idx_mapping=torch.tensor([req_idx], dtype=torch.int32, device=DEVICE),
|
||||
idx_mapping_np=idx_mapping_np,
|
||||
expanded_local_pos=torch.tensor(
|
||||
LOCAL_POS, dtype=torch.int32, device=DEVICE
|
||||
),
|
||||
input_ids=torch.tensor(INPUT_IDS, dtype=torch.int32, device=DEVICE),
|
||||
pos=torch.zeros(num_logits, dtype=torch.int32, device=DEVICE),
|
||||
seq_lens_upper_bound_np=np.full(1, num_logits, dtype=np.int64),
|
||||
),
|
||||
)
|
||||
return logits.cpu()
|
||||
|
||||
|
||||
def test_v2_bad_words_slot_reuse_clears_state():
|
||||
"""A slot recycled from a bad-words request must not keep masking.
|
||||
|
||||
There is no removal hook by design; safety comes from add_request()
|
||||
overwriting the slot's state unconditionally.
|
||||
"""
|
||||
state, req_idx = _make_state([[10, 11, 60]])
|
||||
state.add_request(req_idx, SamplingParams())
|
||||
state.apply_staged_writes()
|
||||
assert int(state.num_bad_words.np[req_idx]) == 0
|
||||
|
||||
num_logits = len(INPUT_IDS)
|
||||
idx_mapping = torch.tensor([req_idx], dtype=torch.int32, device=DEVICE)
|
||||
out = state.apply(
|
||||
torch.zeros((num_logits, VOCAB_SIZE), device=DEVICE),
|
||||
LogitsContext(
|
||||
expanded_idx_mapping=torch.tensor(
|
||||
[req_idx] * num_logits, dtype=torch.int32, device=DEVICE
|
||||
),
|
||||
idx_mapping=idx_mapping,
|
||||
idx_mapping_np=np.array([req_idx], dtype=np.intp),
|
||||
expanded_local_pos=torch.tensor(
|
||||
LOCAL_POS, dtype=torch.int32, device=DEVICE
|
||||
),
|
||||
input_ids=torch.tensor(INPUT_IDS, dtype=torch.int32, device=DEVICE),
|
||||
pos=torch.zeros(num_logits, dtype=torch.int32, device=DEVICE),
|
||||
seq_lens_upper_bound_np=np.full(1, num_logits, dtype=np.int64),
|
||||
),
|
||||
)
|
||||
assert not torch.isinf(out).any()
|
||||
|
||||
|
||||
def test_v2_bad_words_prefix_inside_draft_tokens():
|
||||
"""A prefix matching entirely within the draft tokens must mask the bad
|
||||
word's last token at the draft position that completes the prefix."""
|
||||
|
||||
@@ -183,6 +183,12 @@ def test_local_batch_partition(tp_size: int, seed: int):
|
||||
).all()
|
||||
assert (_np(local.logits_indices) == _np(batch.logits_indices)[rows]).all()
|
||||
assert (_np(local.seq_lens) == _np(batch.seq_lens)[owned]).all()
|
||||
# Indexed by batch position, like seq_lens: a consumer holding local row
|
||||
# indices reads another request's length if this is not localised too.
|
||||
assert (
|
||||
_np(local.seq_lens_cpu_upper_bound)
|
||||
== _np(batch.seq_lens_cpu_upper_bound)[owned]
|
||||
).all()
|
||||
assert batch.num_draft_tokens_per_req is not None
|
||||
assert local.num_draft_tokens_per_req is not None
|
||||
assert (
|
||||
|
||||
@@ -14,6 +14,11 @@ if not torch.cuda.is_available():
|
||||
|
||||
from vllm.sampling_params import SamplingParams, StructuredOutputsParams
|
||||
from vllm.v1.worker.gpu.sample.logit_bias import LogitBiasState
|
||||
from vllm.v1.worker.gpu.sample.logits_processor import (
|
||||
LogitsContext,
|
||||
LogitsProcRequestState,
|
||||
)
|
||||
from vllm.v1.worker.gpu.states import RequestState
|
||||
|
||||
DEVICE = torch.device("cuda")
|
||||
VOCAB_SIZE = 128
|
||||
@@ -45,17 +50,35 @@ def _only_stop_token_left(num_rows: int) -> torch.Tensor:
|
||||
|
||||
def _apply(logits: torch.Tensor, structured: list[bool]) -> torch.Tensor:
|
||||
"""Run the fused bias kernel with one logits row per request."""
|
||||
state = LogitBiasState(max_num_reqs=4, device=DEVICE)
|
||||
req_states = RequestState(
|
||||
max_num_reqs=4,
|
||||
max_model_len=64,
|
||||
max_num_batched_tokens=16,
|
||||
num_speculative_steps=1,
|
||||
vocab_size=VOCAB_SIZE,
|
||||
device=DEVICE,
|
||||
)
|
||||
# add_request() reads prompt_len per slot; every slot here uses the same one.
|
||||
req_states.prompt_len.np[:] = PROMPT_LEN
|
||||
|
||||
state = LogitBiasState(None, LogitsProcRequestState.from_request_state(req_states))
|
||||
for req_idx, is_structured in enumerate(structured):
|
||||
state.add_request(req_idx, PROMPT_LEN, _params(is_structured))
|
||||
state.add_request(req_idx, _params(is_structured))
|
||||
state.apply_staged_writes()
|
||||
|
||||
num_reqs = len(structured)
|
||||
state.apply_logit_bias(
|
||||
idx_mapping = torch.arange(num_reqs, dtype=torch.int32, device=DEVICE)
|
||||
state.apply(
|
||||
logits,
|
||||
torch.arange(num_reqs, dtype=torch.int32, device=DEVICE),
|
||||
np.arange(num_reqs, dtype=np.intp),
|
||||
torch.full((num_reqs,), POS, dtype=torch.int32, device=DEVICE),
|
||||
LogitsContext(
|
||||
expanded_idx_mapping=idx_mapping,
|
||||
idx_mapping=idx_mapping,
|
||||
idx_mapping_np=np.arange(num_reqs, dtype=np.intp),
|
||||
expanded_local_pos=torch.zeros(num_reqs, dtype=torch.int32, device=DEVICE),
|
||||
input_ids=torch.zeros(num_reqs, dtype=torch.int32, device=DEVICE),
|
||||
pos=torch.full((num_reqs,), POS, dtype=torch.int32, device=DEVICE),
|
||||
seq_lens_upper_bound_np=np.full(num_reqs, POS + 1, dtype=np.int64),
|
||||
),
|
||||
)
|
||||
return logits.cpu()
|
||||
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
from types import SimpleNamespace
|
||||
from typing import Any
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
pytest.importorskip("triton")
|
||||
if not torch.cuda.is_available():
|
||||
pytest.skip(
|
||||
"CUDA required for Model Runner V2 logits processor tests",
|
||||
allow_module_level=True,
|
||||
)
|
||||
|
||||
from vllm.sampling_params import SamplingParams
|
||||
from vllm.v1.worker.gpu.sample.logits_processor import (
|
||||
LogitsContext,
|
||||
LogitsProcessor,
|
||||
)
|
||||
from vllm.v1.worker.gpu.sample.sampler import Sampler
|
||||
from vllm.v1.worker.gpu.states import RequestState
|
||||
|
||||
DEVICE = torch.device("cuda")
|
||||
VOCAB_SIZE = 128
|
||||
TARGET_TOKEN = 7
|
||||
BIAS = 5.0
|
||||
BOOST = 100.0
|
||||
TEMPERATURE = 2.0
|
||||
|
||||
|
||||
class MockReasoningConfig:
|
||||
reasoning_start_token_ids = [90]
|
||||
reasoning_end_token_ids = [91]
|
||||
natural_reasoning_end_token_ids = [91]
|
||||
|
||||
|
||||
class RecordingProcessor(LogitsProcessor):
|
||||
"""Adds BOOST to TARGET_TOKEN and records every lifecycle callback."""
|
||||
|
||||
def __init__(self, vllm_config: Any, req_state: Any):
|
||||
self.events: list[str] = []
|
||||
self.seen_logits: torch.Tensor | None = None
|
||||
|
||||
def add_request(self, req_idx: int, sampling_params: SamplingParams) -> bool:
|
||||
self.events.append(f"add_request:{req_idx}")
|
||||
return True
|
||||
|
||||
def apply_staged_writes(self) -> None:
|
||||
self.events.append("apply_staged_writes")
|
||||
|
||||
def apply(self, logits: torch.Tensor, ctx: LogitsContext) -> torch.Tensor:
|
||||
self.events.append("apply")
|
||||
self.seen_logits = logits.clone()
|
||||
logits[:, TARGET_TOKEN] += BOOST
|
||||
return logits
|
||||
|
||||
|
||||
def _make_sampler(processor: RecordingProcessor) -> Sampler:
|
||||
req_states = RequestState(
|
||||
max_num_reqs=4,
|
||||
max_model_len=64,
|
||||
max_num_batched_tokens=16,
|
||||
num_speculative_steps=1,
|
||||
vocab_size=VOCAB_SIZE,
|
||||
device=DEVICE,
|
||||
)
|
||||
return Sampler(
|
||||
vllm_config=SimpleNamespace(reasoning_config=MockReasoningConfig()),
|
||||
max_num_reqs=4,
|
||||
vocab_size=VOCAB_SIZE,
|
||||
device=DEVICE,
|
||||
req_states=req_states,
|
||||
custom_logits_processors=[processor],
|
||||
)
|
||||
|
||||
|
||||
def test_custom_processor_lifecycle_and_pipeline_position():
|
||||
"""A custom processor's hooks fire in order, it runs after the built-in
|
||||
bias stage, and before temperature scaling."""
|
||||
proc = RecordingProcessor(None, None)
|
||||
sampler = _make_sampler(proc)
|
||||
sampler.add_request(
|
||||
0,
|
||||
SamplingParams(logit_bias={TARGET_TOKEN: BIAS}, temperature=TEMPERATURE),
|
||||
)
|
||||
sampler.apply_staged_writes()
|
||||
|
||||
num_reqs = 1
|
||||
idx_mapping = torch.arange(num_reqs, dtype=torch.int32, device=DEVICE)
|
||||
raw = torch.zeros(num_reqs, VOCAB_SIZE, device=DEVICE)
|
||||
out = sampler.apply_sampling_params(
|
||||
raw,
|
||||
expanded_idx_mapping=idx_mapping,
|
||||
idx_mapping=idx_mapping,
|
||||
idx_mapping_np=np.arange(num_reqs, dtype=np.intp),
|
||||
pos=torch.zeros(num_reqs, dtype=torch.int32, device=DEVICE),
|
||||
input_ids=torch.zeros(num_reqs, dtype=torch.int32, device=DEVICE),
|
||||
expanded_local_pos=torch.zeros(num_reqs, dtype=torch.int32, device=DEVICE),
|
||||
seq_lens_upper_bound_np=np.ones(num_reqs, dtype=np.int64),
|
||||
)
|
||||
|
||||
assert proc.events == [
|
||||
"add_request:0",
|
||||
"apply_staged_writes",
|
||||
"apply",
|
||||
]
|
||||
# The processor sees the built-in bias applied but no temperature scaling.
|
||||
assert proc.seen_logits is not None
|
||||
assert proc.seen_logits[0, TARGET_TOKEN].item() == BIAS
|
||||
# Its own modification is then temperature-scaled by the sampler.
|
||||
assert out[0, TARGET_TOKEN].item() == (BIAS + BOOST) / TEMPERATURE
|
||||
@@ -56,7 +56,16 @@ class FakeEplbState:
|
||||
def _make_runner(**overrides: Any) -> Any:
|
||||
runner: Any = mrv2.GPUModelRunner.__new__(mrv2.GPUModelRunner)
|
||||
runner.device = torch.device("cpu")
|
||||
runner.model_config = SimpleNamespace(model="test-model")
|
||||
runner.model_config = SimpleNamespace(model="test-model", logits_processors=None)
|
||||
runner.req_states = SimpleNamespace(
|
||||
device=runner.device,
|
||||
max_num_reqs=8,
|
||||
vocab_size=0,
|
||||
all_token_ids=None,
|
||||
prompt_len=None,
|
||||
prefill_len=None,
|
||||
total_len=None,
|
||||
)
|
||||
runner.load_config = SimpleNamespace(load_format="hf")
|
||||
runner.parallel_config = SimpleNamespace(
|
||||
enable_eplb=True,
|
||||
|
||||
@@ -47,6 +47,9 @@ def test_chunked_scores_match_full_batch(logprobs_mode: str):
|
||||
[np.arange(count, dtype=np.int32) for count in num_logits_per_req]
|
||||
)
|
||||
).to(device),
|
||||
seq_lens_cpu_upper_bound=torch.from_numpy(
|
||||
np.array([10, 20, 30, 40], dtype=np.int32)
|
||||
),
|
||||
)
|
||||
rejection_sampler = object.__new__(RejectionSampler)
|
||||
rejection_sampler.sampler = SimpleNamespace(logprobs_mode=logprobs_mode)
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
from collections.abc import Sequence
|
||||
from types import SimpleNamespace
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
import torch
|
||||
@@ -10,6 +13,10 @@ if not torch.cuda.is_available():
|
||||
pytest.skip("CUDA required for sampler flag tests", allow_module_level=True)
|
||||
|
||||
from vllm.sampling_params import SamplingParams
|
||||
from vllm.v1.worker.gpu.sample.logits_processor import (
|
||||
LogitsContext,
|
||||
LogitsProcessor,
|
||||
)
|
||||
from vllm.v1.worker.gpu.sample.sampler import Sampler
|
||||
from vllm.v1.worker.gpu.states import RequestState
|
||||
|
||||
@@ -23,7 +30,7 @@ class MockReasoningConfig:
|
||||
natural_reasoning_end_token_ids = [91]
|
||||
|
||||
|
||||
def _make_sampler() -> Sampler:
|
||||
def _make_sampler(custom_logits_processors: Sequence[LogitsProcessor] = ()) -> Sampler:
|
||||
req_states = RequestState(
|
||||
max_num_reqs=4,
|
||||
max_model_len=64,
|
||||
@@ -33,11 +40,12 @@ def _make_sampler() -> Sampler:
|
||||
device=DEVICE,
|
||||
)
|
||||
return Sampler(
|
||||
vllm_config=SimpleNamespace(reasoning_config=MockReasoningConfig()),
|
||||
max_num_reqs=4,
|
||||
vocab_size=VOCAB_SIZE,
|
||||
device=DEVICE,
|
||||
req_states=req_states,
|
||||
reasoning_config=MockReasoningConfig(),
|
||||
custom_logits_processors=custom_logits_processors,
|
||||
)
|
||||
|
||||
|
||||
@@ -65,23 +73,46 @@ def test_logits_processing_cache_matches_request_features(
|
||||
sampling_params: SamplingParams, expected: bool
|
||||
):
|
||||
sampler = _make_sampler()
|
||||
sampler.add_request(3, prompt_len=1, sampling_params=sampling_params)
|
||||
sampler.add_request(3, sampling_params=sampling_params)
|
||||
|
||||
assert sampler.needs_logits_processing[3] == expected
|
||||
|
||||
|
||||
def test_logits_processing_cache_is_overwritten_when_slot_is_reused():
|
||||
sampler = _make_sampler()
|
||||
sampler.add_request(3, 1, SamplingParams.for_sampler_warmup())
|
||||
sampler.add_request(3, 1, SamplingParams())
|
||||
sampler.add_request(3, SamplingParams.for_sampler_warmup())
|
||||
sampler.add_request(3, SamplingParams())
|
||||
|
||||
assert not sampler.needs_logits_processing[3]
|
||||
|
||||
|
||||
class _GateProcessor(LogitsProcessor):
|
||||
"""Reports a fixed admission decision from add_request()."""
|
||||
|
||||
def __init__(self, admitted: bool):
|
||||
self.admitted = admitted
|
||||
|
||||
def add_request(self, req_idx: int, sampling_params: SamplingParams) -> bool:
|
||||
return self.admitted
|
||||
|
||||
def apply(self, logits: torch.Tensor, ctx: LogitsContext) -> torch.Tensor:
|
||||
raise AssertionError("the pipeline never runs in this test")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("admitted", [True, False])
|
||||
def test_custom_processor_add_request_gates_pipeline_flag(admitted: bool):
|
||||
"""A custom processor's add_request() return value is OR-ed into the
|
||||
per-request needs_logits_processing flag."""
|
||||
sampler = _make_sampler(custom_logits_processors=[_GateProcessor(admitted)])
|
||||
sampler.add_request(3, SamplingParams())
|
||||
|
||||
assert sampler.needs_logits_processing[3] == admitted
|
||||
|
||||
|
||||
def test_logits_processing_cache_only_checks_active_requests():
|
||||
sampler = _make_sampler()
|
||||
sampler.add_request(0, 1, SamplingParams(temperature=0.0))
|
||||
sampler.add_request(2, 1, SamplingParams.for_sampler_warmup())
|
||||
sampler.add_request(0, SamplingParams(temperature=0.0))
|
||||
sampler.add_request(2, SamplingParams.for_sampler_warmup())
|
||||
|
||||
sampling_only = np.array([0], dtype=np.int32)
|
||||
with_processing = np.array([0, 2], dtype=np.int32)
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
from types import SimpleNamespace
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
@@ -12,6 +15,7 @@ if not torch.cuda.is_available():
|
||||
)
|
||||
|
||||
from vllm.sampling_params import SamplingParams
|
||||
from vllm.v1.worker.gpu.sample.logits_processor import LogitsContext
|
||||
from vllm.v1.worker.gpu.sample.sampler import Sampler
|
||||
from vllm.v1.worker.gpu.sample.thinking_budget import ThinkingBudgetState
|
||||
from vllm.v1.worker.gpu.states import RequestState
|
||||
@@ -75,11 +79,17 @@ def _apply(
|
||||
idx_mapping_np = idx_mapping.cpu().numpy()
|
||||
state.apply(
|
||||
logits,
|
||||
expanded_idx_mapping,
|
||||
idx_mapping,
|
||||
idx_mapping_np,
|
||||
torch.tensor(input_ids, dtype=torch.int32, device=DEVICE),
|
||||
torch.tensor(local_pos, dtype=torch.int32, device=DEVICE),
|
||||
LogitsContext(
|
||||
expanded_idx_mapping=expanded_idx_mapping,
|
||||
idx_mapping=idx_mapping,
|
||||
idx_mapping_np=idx_mapping_np,
|
||||
expanded_local_pos=torch.tensor(
|
||||
local_pos, dtype=torch.int32, device=DEVICE
|
||||
),
|
||||
input_ids=torch.tensor(input_ids, dtype=torch.int32, device=DEVICE),
|
||||
pos=torch.zeros(len(input_ids), dtype=torch.int32, device=DEVICE),
|
||||
seq_lens_upper_bound_np=np.full(1, len(input_ids), dtype=np.int64),
|
||||
),
|
||||
)
|
||||
return logits.cpu()
|
||||
|
||||
@@ -190,15 +200,14 @@ def test_v2_greedy_sampling_applies_thinking_budget():
|
||||
"""Greedy-only requests must not bypass thinking-budget processing."""
|
||||
req_states = _make_req_states([1, START, 10, 11, 12], prompt_len=1)
|
||||
sampler = Sampler(
|
||||
vllm_config=SimpleNamespace(reasoning_config=MockReasoningConfig()),
|
||||
max_num_reqs=4,
|
||||
vocab_size=VOCAB_SIZE,
|
||||
device=DEVICE,
|
||||
req_states=req_states,
|
||||
reasoning_config=MockReasoningConfig(),
|
||||
)
|
||||
sampler.add_request(
|
||||
req_idx=3,
|
||||
prompt_len=1,
|
||||
sampling_params=SamplingParams(
|
||||
temperature=0.0,
|
||||
thinking_token_budget=3,
|
||||
@@ -219,6 +228,7 @@ def test_v2_greedy_sampling_applies_thinking_budget():
|
||||
torch.tensor([4], dtype=torch.int32, device=DEVICE),
|
||||
input_ids,
|
||||
torch.tensor([0], dtype=torch.int32, device=DEVICE),
|
||||
np.full(1, 4, dtype=np.int64),
|
||||
)
|
||||
|
||||
assert out[0, END].item() == pytest.approx(1.0e9)
|
||||
|
||||
@@ -0,0 +1,228 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""CPU-only tests for the V2 custom logits processor loader.
|
||||
|
||||
Load-time validation is pure Python (importlib + issubclass), so it runs
|
||||
without CUDA. Pipeline behavior is covered by test_gpu_logits_processors.py.
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
from enum import Enum, auto
|
||||
from types import SimpleNamespace
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
import vllm.v1.worker.gpu.sample.logits_processor.loader as loader
|
||||
from vllm.exceptions import VLLMValidationError
|
||||
from vllm.sampling_params import SamplingParams
|
||||
from vllm.v1.sample.logits_processor import (
|
||||
LogitsProcessor as V1LogitsProcessor,
|
||||
)
|
||||
from vllm.v1.worker.gpu.sample.logits_processor import (
|
||||
LogitsContext,
|
||||
LogitsProcessor,
|
||||
)
|
||||
|
||||
|
||||
class DummyV2Processor(LogitsProcessor):
|
||||
"""Records its constructor args so tests can assert how it was built."""
|
||||
|
||||
def __init__(self, vllm_config: Any, req_state: Any):
|
||||
self.ctor_args = (vllm_config, req_state)
|
||||
|
||||
def apply(self, logits: torch.Tensor, ctx: LogitsContext) -> torch.Tensor:
|
||||
return logits
|
||||
|
||||
|
||||
class AltV2Processor(DummyV2Processor):
|
||||
"""Second V2 processor, used to assert plugin/FQCN load ordering."""
|
||||
|
||||
|
||||
class DummyV1Processor(V1LogitsProcessor):
|
||||
"""A V1-interface processor. Never instantiated: the loader must reject
|
||||
it at the issubclass check, before any constructor runs."""
|
||||
|
||||
|
||||
def not_a_processor() -> None:
|
||||
"""FQCN target that resolves but is not a class."""
|
||||
|
||||
|
||||
def _fake_entrypoint(cls: type) -> SimpleNamespace:
|
||||
return SimpleNamespace(
|
||||
name=cls.__name__.lower(),
|
||||
value=f"fake:{cls.__name__}",
|
||||
load=lambda: cls,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def no_installed_plugins(monkeypatch: pytest.MonkeyPatch):
|
||||
"""Isolate tests from entry points registered in the environment."""
|
||||
monkeypatch.setattr(loader, "entry_points", lambda group: [])
|
||||
|
||||
|
||||
class ProcSource(Enum):
|
||||
CLASS = auto()
|
||||
FQCN = auto()
|
||||
ENTRYPOINT = auto()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("source", list(ProcSource))
|
||||
def test_loads_v2_processors(monkeypatch: pytest.MonkeyPatch, source: ProcSource):
|
||||
"""A V2 subclass loads via class object, FQCN string, or entrypoint."""
|
||||
fake_req_states = SimpleNamespace(
|
||||
device="cpu",
|
||||
max_num_reqs=4,
|
||||
vocab_size=128,
|
||||
all_token_ids=None,
|
||||
prompt_len=None,
|
||||
prefill_len=None,
|
||||
total_len=None,
|
||||
)
|
||||
custom: list[Any]
|
||||
if source is ProcSource.CLASS:
|
||||
custom = [DummyV2Processor]
|
||||
elif source is ProcSource.FQCN:
|
||||
custom = [f"{__name__}:DummyV2Processor"]
|
||||
else:
|
||||
monkeypatch.setattr(
|
||||
loader, "entry_points", lambda group: [_fake_entrypoint(DummyV2Processor)]
|
||||
)
|
||||
# Plugins load before user-specified processors.
|
||||
custom = [f"{__name__}:AltV2Processor"]
|
||||
|
||||
procs = loader.build_custom_logits_processors(None, fake_req_states, False, custom)
|
||||
|
||||
if source is ProcSource.ENTRYPOINT:
|
||||
assert [type(p) for p in procs] == [DummyV2Processor, AltV2Processor]
|
||||
else:
|
||||
assert [type(p) for p in procs] == [DummyV2Processor]
|
||||
vllm_config, req_state = procs[0].ctor_args
|
||||
assert vllm_config is None
|
||||
assert isinstance(req_state, loader.LogitsProcRequestState)
|
||||
assert req_state.max_num_reqs == 4
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("custom", "plugin_cls", "exc_type", "msg_fragment"),
|
||||
[
|
||||
pytest.param(
|
||||
[DummyV1Processor],
|
||||
None,
|
||||
ValueError,
|
||||
"not a subclass",
|
||||
id="v1-class-object",
|
||||
),
|
||||
pytest.param(
|
||||
[f"{__name__}:DummyV1Processor"],
|
||||
None,
|
||||
ValueError,
|
||||
"not a subclass",
|
||||
id="v1-fqcn",
|
||||
),
|
||||
pytest.param(
|
||||
[],
|
||||
DummyV1Processor,
|
||||
ValueError,
|
||||
"V1-interface plugins are not supported",
|
||||
id="v1-entrypoint",
|
||||
),
|
||||
pytest.param(
|
||||
[f"{__name__}:not_a_processor"],
|
||||
None,
|
||||
ValueError,
|
||||
"must be a type",
|
||||
id="non-type-fqcn",
|
||||
),
|
||||
pytest.param(
|
||||
["no.such.module:Nope"],
|
||||
None,
|
||||
RuntimeError,
|
||||
"no.such.module:Nope",
|
||||
id="missing-module",
|
||||
),
|
||||
pytest.param(
|
||||
["no_colon_here"],
|
||||
None,
|
||||
ValueError,
|
||||
"Expected format",
|
||||
id="fqcn-missing-colon",
|
||||
),
|
||||
pytest.param(
|
||||
["too:many:colons"],
|
||||
None,
|
||||
ValueError,
|
||||
"Expected format",
|
||||
id="fqcn-extra-colons",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_rejects_invalid(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
custom: list,
|
||||
plugin_cls: type | None,
|
||||
exc_type: type[Exception],
|
||||
msg_fragment: str,
|
||||
):
|
||||
"""V1 processors and malformed references fail at load time, not at
|
||||
sampling time, with a message naming the offending reference."""
|
||||
if plugin_cls is not None:
|
||||
monkeypatch.setattr(
|
||||
loader, "entry_points", lambda group: [_fake_entrypoint(plugin_cls)]
|
||||
)
|
||||
|
||||
with pytest.raises(exc_type, match=msg_fragment):
|
||||
loader.build_custom_logits_processors(None, None, False, custom)
|
||||
|
||||
|
||||
def test_pooling_model_rejects_custom_logitsprocs():
|
||||
"""Pooling models reject custom processors instead of ignoring them."""
|
||||
with pytest.raises(ValueError, match="Pooling models do not support"):
|
||||
loader.build_custom_logits_processors(None, None, True, [DummyV2Processor])
|
||||
assert loader.build_custom_logits_processors(None, None, True, []) == []
|
||||
|
||||
|
||||
class ValidatingProcessor(DummyV2Processor):
|
||||
"""Rejects target_token=-1 via the optional validate_params hook."""
|
||||
|
||||
@classmethod
|
||||
def validate_params(cls, sampling_params: SamplingParams) -> None:
|
||||
if (sampling_params.extra_args or {}).get("target_token") == -1:
|
||||
raise ValueError("target_token must not be -1")
|
||||
|
||||
|
||||
def test_validate_params_runs_at_admission():
|
||||
"""The factory loads classes up front; the returned validator runs
|
||||
validate_params per request and wraps rejections as VLLMValidationError."""
|
||||
loader._cached_load_v2_logitsprocs.cache_clear()
|
||||
validate = loader.build_custom_logits_processors_params_validator(
|
||||
[ValidatingProcessor]
|
||||
)
|
||||
validate(SamplingParams(extra_args={"target_token": 1}))
|
||||
with pytest.raises(VLLMValidationError, match="target_token"):
|
||||
validate(SamplingParams(extra_args={"target_token": -1}))
|
||||
|
||||
|
||||
def test_validator_factory_loads_classes_eagerly():
|
||||
"""A bad reference fails when the validator is built (at startup), not on
|
||||
the first request."""
|
||||
loader._cached_load_v2_logitsprocs.cache_clear()
|
||||
with pytest.raises(RuntimeError, match="no.such.module:Nope"):
|
||||
loader.build_custom_logits_processors_params_validator(["no.such.module:Nope"])
|
||||
|
||||
|
||||
def test_loader_import_stays_frontend_safe():
|
||||
"""The frontend process imports the loader to validate params; importing
|
||||
it must not pull in model-runner side modules."""
|
||||
code = (
|
||||
"import sys\n"
|
||||
"import vllm.v1.worker.gpu.sample.logits_processor.loader\n"
|
||||
"assert 'vllm.sampling_params' not in sys.modules\n"
|
||||
"assert 'vllm.v1.sample.logits_processor' not in sys.modules\n"
|
||||
"assert 'vllm.v1.worker.gpu.states' not in sys.modules\n"
|
||||
"assert 'vllm.v1.worker.gpu.buffer_utils' not in sys.modules\n"
|
||||
)
|
||||
subprocess.run([sys.executable, "-c", code], check=True)
|
||||
@@ -2880,7 +2880,6 @@ class VllmConfig:
|
||||
def _get_v2_model_runner_unsupported_features(self) -> list[str]:
|
||||
"""Collect features not yet supported by the V2 model runner."""
|
||||
unsupported: list[str] = []
|
||||
model_config = self.model_config
|
||||
speculative_config = self.speculative_config
|
||||
|
||||
if self.compilation_config.mode == CompilationMode.STOCK_TORCH_COMPILE:
|
||||
@@ -2929,17 +2928,6 @@ class VllmConfig:
|
||||
if self.parallel_config.enable_elastic_ep:
|
||||
unsupported.append("elastic expert parallelism")
|
||||
|
||||
has_logitsproc_plugins = False
|
||||
if model_config is not None:
|
||||
from importlib.metadata import entry_points
|
||||
|
||||
has_logitsproc_plugins = bool(entry_points(group="vllm.logits_processors"))
|
||||
|
||||
if model_config is not None and (
|
||||
model_config.logits_processors or has_logitsproc_plugins
|
||||
):
|
||||
unsupported.append("custom logits processors")
|
||||
|
||||
if self.cache_config.mamba_cache_mode == "all":
|
||||
unsupported.append("mamba cache mode 'all'")
|
||||
|
||||
|
||||
@@ -1014,7 +1014,7 @@ class DiffusionSampler:
|
||||
# on the subsequent commit step when num_sampled=CANVAS_LEN.
|
||||
self._pending_logprobs: dict[int, LogprobsTensors] = {}
|
||||
|
||||
def add_request(self, req_idx: int, prompt_len: int, sampling_params: Any) -> None:
|
||||
def add_request(self, req_idx: int, sampling_params: Any) -> None:
|
||||
if use_penalty(sampling_params):
|
||||
logger.warning_once(
|
||||
"DiffusionGemma does not support repetition/frequency/presence "
|
||||
|
||||
@@ -823,7 +823,6 @@ class SamplingParams(
|
||||
self._validate_logit_bias(model_config)
|
||||
self._validate_trace_replay(model_config, speculative_config)
|
||||
self._validate_stop_token_ids(model_config)
|
||||
self._validate_logits_processors(model_config)
|
||||
self._validate_allowed_token_ids(model_config)
|
||||
self._validate_spec_decode(speculative_config)
|
||||
self._validate_diffusion(model_config)
|
||||
@@ -993,13 +992,6 @@ class SamplingParams(
|
||||
value=invalid_token_ids,
|
||||
)
|
||||
|
||||
def _validate_logits_processors(self, model_config: ModelConfig) -> None:
|
||||
from vllm.v1.sample.logits_processor import (
|
||||
validate_logits_processors_parameters,
|
||||
)
|
||||
|
||||
validate_logits_processors_parameters(model_config.logits_processors, self)
|
||||
|
||||
def _validate_allowed_token_ids(self, model_config: ModelConfig) -> None:
|
||||
allowed_token_ids = self.allowed_token_ids
|
||||
if allowed_token_ids is None:
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
import time
|
||||
from collections.abc import Mapping
|
||||
from collections.abc import Callable, Mapping
|
||||
from functools import partial
|
||||
from typing import Any, Literal
|
||||
|
||||
import vllm.envs as envs
|
||||
@@ -51,6 +52,11 @@ class InputProcessor:
|
||||
self.speculative_config = vllm_config.speculative_config
|
||||
self.structured_outputs_config = vllm_config.structured_outputs_config
|
||||
self.observability_config = vllm_config.observability_config
|
||||
# Load the custom logits processor classes once; the returned callable
|
||||
# runs their validate_params hooks per request at admission.
|
||||
self.validate_logits_processors_params = (
|
||||
self._build_logits_processors_params_validator()
|
||||
)
|
||||
|
||||
self.generation_config_fields = model_config.try_get_generation_config()
|
||||
|
||||
@@ -81,6 +87,25 @@ class InputProcessor:
|
||||
def get_tokenizer(self) -> TokenizerLike:
|
||||
return self.renderer.get_tokenizer()
|
||||
|
||||
def _build_logits_processors_params_validator(
|
||||
self,
|
||||
) -> Callable[[SamplingParams], None]:
|
||||
"""Load the custom logits processor classes and return the per-request
|
||||
params validator for the active model runner."""
|
||||
custom_logitsprocs = self.model_config.logits_processors
|
||||
if self.vllm_config.use_v2_model_runner:
|
||||
from vllm.v1.worker.gpu.sample.logits_processor import (
|
||||
build_custom_logits_processors_params_validator,
|
||||
)
|
||||
|
||||
return build_custom_logits_processors_params_validator(custom_logitsprocs)
|
||||
|
||||
from vllm.v1.sample.logits_processor import (
|
||||
validate_logits_processors_parameters,
|
||||
)
|
||||
|
||||
return partial(validate_logits_processors_parameters, custom_logitsprocs)
|
||||
|
||||
def _validate_params(
|
||||
self,
|
||||
params: SamplingParams | PoolingParams,
|
||||
@@ -101,6 +126,8 @@ class InputProcessor:
|
||||
self.tokenizer,
|
||||
)
|
||||
|
||||
self.validate_logits_processors_params(params)
|
||||
|
||||
if self.model_config.return_sampling_mask:
|
||||
if params.temperature <= 0:
|
||||
raise ValueError(
|
||||
|
||||
@@ -36,10 +36,8 @@ class GPUWatermarkSampler(Sampler):
|
||||
self.watermarking.np.fill(True)
|
||||
self.watermarking.copy_to_uva()
|
||||
|
||||
def add_request(
|
||||
self, req_idx: int, prompt_len: int, sampling_params: SamplingParams
|
||||
) -> None:
|
||||
super().add_request(req_idx, prompt_len, sampling_params)
|
||||
def add_request(self, req_idx: int, sampling_params: SamplingParams) -> None:
|
||||
super().add_request(req_idx, sampling_params)
|
||||
self.watermarking.np[req_idx] = sampling_params.watermarking
|
||||
if sampling_params.watermarking and sampling_params.temperature == 0:
|
||||
logger.warning_once(
|
||||
|
||||
@@ -143,6 +143,7 @@ from vllm.v1.worker.gpu.sample.batch_shard import (
|
||||
all_to_all_logits,
|
||||
gather_sampler_output,
|
||||
)
|
||||
from vllm.v1.worker.gpu.sample.logits_processor import build_custom_logits_processors
|
||||
from vllm.v1.worker.gpu.sample.output import SamplerOutput
|
||||
from vllm.v1.worker.gpu.sample.prompt_logprob import PromptLogprobsWorker
|
||||
from vllm.v1.worker.gpu.sample.sampler import Sampler
|
||||
@@ -447,8 +448,13 @@ class GPUModelRunner(LoRAModelRunnerMixin):
|
||||
)
|
||||
|
||||
# Initialize samplers. Model states may override via custom_sampler().
|
||||
config_processors = self.model_config.logits_processors or ()
|
||||
custom_logits_processors = build_custom_logits_processors(
|
||||
self.vllm_config, self.req_states, self.is_pooling_model, config_processors
|
||||
)
|
||||
if self.is_last_pp_rank and not self.is_pooling_model:
|
||||
sampler_kwargs: dict[str, Any] = {
|
||||
"vllm_config": self.vllm_config,
|
||||
"max_num_reqs": self.max_num_reqs,
|
||||
"vocab_size": self.vocab_size,
|
||||
"device": self.device,
|
||||
@@ -457,8 +463,8 @@ class GPUModelRunner(LoRAModelRunnerMixin):
|
||||
"num_speculative_tokens": self.decode_query_len,
|
||||
"use_fp64_gumbel": self.model_config.use_fp64_gumbel,
|
||||
"enable_trace_replay": self.model_config.enable_trace_replay,
|
||||
"reasoning_config": self.vllm_config.reasoning_config,
|
||||
"return_sampling_mask": self.model_config.return_sampling_mask,
|
||||
"custom_logits_processors": custom_logits_processors,
|
||||
}
|
||||
if self.vllm_config.watermark_config is None:
|
||||
self.sampler = Sampler(**sampler_kwargs)
|
||||
@@ -1147,9 +1153,7 @@ class GPUModelRunner(LoRAModelRunnerMixin):
|
||||
|
||||
if self.is_last_pp_rank and new_req_data.sampling_params is not None:
|
||||
assert self.sampler is not None
|
||||
self.sampler.add_request(
|
||||
req_index, prompt_len, new_req_data.sampling_params
|
||||
)
|
||||
self.sampler.add_request(req_index, new_req_data.sampling_params)
|
||||
assert self.prompt_logprobs_worker is not None
|
||||
self.prompt_logprobs_worker.add_request(
|
||||
req_id, req_index, new_req_data.sampling_params
|
||||
|
||||
@@ -1,43 +1,47 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
import numpy as np
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import torch
|
||||
|
||||
import vllm.envs as envs
|
||||
from vllm.sampling_params import SamplingParams
|
||||
from vllm.triton_utils import tl, triton
|
||||
from vllm.v1.worker.gpu.buffer_utils import StagedWriteTensor, UvaBackedTensor
|
||||
from vllm.v1.worker.gpu.states import RequestState
|
||||
from vllm.v1.worker.gpu.sample.logits_processor.interface import (
|
||||
LogitsContext,
|
||||
LogitsProcessor,
|
||||
LogitsProcRequestState,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from vllm.config import VllmConfig
|
||||
|
||||
|
||||
class BadWordsState:
|
||||
def __init__(self, req_states: RequestState):
|
||||
class BadWordsState(LogitsProcessor):
|
||||
def __init__(self, vllm_config: "VllmConfig", req_states: LogitsProcRequestState):
|
||||
self.req_states = req_states
|
||||
self.max_num_reqs = req_states.max_num_reqs
|
||||
self.device = req_states.device
|
||||
max_num_reqs = req_states.max_num_reqs
|
||||
device = req_states.device
|
||||
|
||||
max_total_tokens = envs.VLLM_MAX_BAD_WORDS_TOTAL_TOKENS
|
||||
max_num_bad_words = envs.VLLM_MAX_NUM_BAD_WORDS
|
||||
# flattened bad word tokens: [max_num_reqs, VLLM_MAX_BAD_WORDS_TOTAL_TOKENS]
|
||||
self.bad_word_token_ids = StagedWriteTensor(
|
||||
(self.max_num_reqs, max_total_tokens),
|
||||
dtype=torch.int32,
|
||||
device=self.device,
|
||||
(max_num_reqs, max_total_tokens), dtype=torch.int32, device=device
|
||||
)
|
||||
# cumulative offsets of bad words: [max_num_reqs, VLLM_MAX_NUM_BAD_WORDS + 1]
|
||||
self.bad_word_offsets = StagedWriteTensor(
|
||||
(self.max_num_reqs, max_num_bad_words + 1),
|
||||
dtype=torch.int32,
|
||||
device=self.device,
|
||||
(max_num_reqs, max_num_bad_words + 1), dtype=torch.int32, device=device
|
||||
)
|
||||
# number of bad words per request
|
||||
self.num_bad_words = UvaBackedTensor(self.max_num_reqs, dtype=torch.int32)
|
||||
self.num_bad_words = UvaBackedTensor(max_num_reqs, dtype=torch.int32)
|
||||
|
||||
def add_request(self, req_idx: int, sampling_params: SamplingParams) -> None:
|
||||
def add_request(self, req_idx: int, sampling_params: SamplingParams) -> bool:
|
||||
bad_words_token_ids = sampling_params.bad_words_token_ids
|
||||
if not bad_words_token_ids:
|
||||
self.num_bad_words.np[req_idx] = 0
|
||||
return
|
||||
return False
|
||||
|
||||
num_bad_words = len(bad_words_token_ids)
|
||||
max_num_bad_words = envs.VLLM_MAX_NUM_BAD_WORDS
|
||||
@@ -65,38 +69,33 @@ class BadWordsState:
|
||||
self.bad_word_token_ids.stage_write(req_idx, 0, flattened_tokens)
|
||||
self.bad_word_offsets.stage_write(req_idx, 0, offsets)
|
||||
self.num_bad_words.np[req_idx] = num_bad_words
|
||||
return True
|
||||
|
||||
def apply_staged_writes(self) -> None:
|
||||
self.num_bad_words.copy_to_uva()
|
||||
self.bad_word_token_ids.apply_write()
|
||||
self.bad_word_offsets.apply_write()
|
||||
|
||||
def apply_bad_words(
|
||||
self,
|
||||
logits: torch.Tensor,
|
||||
expanded_idx_mapping: torch.Tensor,
|
||||
idx_mapping_np: np.ndarray,
|
||||
input_ids: torch.Tensor,
|
||||
expanded_local_pos: torch.Tensor,
|
||||
) -> None:
|
||||
max_num_bad_words = int(self.num_bad_words.np[idx_mapping_np].max())
|
||||
def apply(self, logits: torch.Tensor, ctx: LogitsContext) -> torch.Tensor:
|
||||
max_num_bad_words = int(self.num_bad_words.np[ctx.idx_mapping_np].max())
|
||||
if max_num_bad_words == 0:
|
||||
# No request uses bad words. Skip the kernel launch.
|
||||
return
|
||||
return logits
|
||||
|
||||
apply_bad_words(
|
||||
logits,
|
||||
expanded_idx_mapping,
|
||||
ctx.expanded_idx_mapping,
|
||||
self.bad_word_token_ids.gpu,
|
||||
self.bad_word_offsets.gpu,
|
||||
self.num_bad_words.gpu,
|
||||
self.req_states.all_token_ids.gpu,
|
||||
self.req_states.prompt_len.gpu,
|
||||
self.req_states.total_len.gpu,
|
||||
input_ids,
|
||||
expanded_local_pos,
|
||||
ctx.input_ids,
|
||||
ctx.expanded_local_pos,
|
||||
max_num_bad_words,
|
||||
)
|
||||
return logits
|
||||
|
||||
|
||||
@triton.jit
|
||||
|
||||
@@ -200,6 +200,9 @@ class BatchSharder:
|
||||
num_logits_per_req_np[local_req_indices_np], out=local_cu_num_logits_np[1:]
|
||||
)
|
||||
max_num_logits_per_req = int(num_logits_per_req_np.max()) if num_reqs else 1
|
||||
local_seq_lens_cpu_upper_bound = input_batch.seq_lens_cpu_upper_bound[
|
||||
torch.from_numpy(local_req_indices_np)
|
||||
]
|
||||
|
||||
# Shard the input batch GPU tensors.
|
||||
sorted_logits_indices = torch.empty(
|
||||
@@ -269,6 +272,7 @@ class BatchSharder:
|
||||
expanded_idx_mapping=local_expanded_idx_mapping,
|
||||
expanded_local_pos=local_expanded_local_pos,
|
||||
seq_lens=local_seq_lens,
|
||||
seq_lens_cpu_upper_bound=local_seq_lens_cpu_upper_bound,
|
||||
logits_indices=local_logits_indices,
|
||||
cu_num_logits=local_cu_num_logits,
|
||||
cu_num_logits_np=local_cu_num_logits_np,
|
||||
|
||||
@@ -1,60 +1,61 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
|
||||
from vllm.sampling_params import SamplingParams
|
||||
from vllm.triton_utils import tl, triton
|
||||
from vllm.v1.worker.gpu.buffer_utils import StagedWriteTensor, UvaBackedTensor
|
||||
from vllm.v1.worker.gpu.sample.logits_processor.interface import (
|
||||
LogitsContext,
|
||||
LogitsProcessor,
|
||||
LogitsProcRequestState,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from vllm.config import VllmConfig
|
||||
|
||||
MAX_NUM_ALLOWED_TOKEN_IDS = 1024
|
||||
MAX_NUM_LOGIT_BIAS_TOKENS = 1024
|
||||
MAX_NUM_STOP_TOKEN_IDS = 128
|
||||
|
||||
|
||||
class LogitBiasState:
|
||||
def __init__(self, max_num_reqs: int, device: torch.device):
|
||||
self.max_num_reqs = max_num_reqs
|
||||
class LogitBiasState(LogitsProcessor):
|
||||
def __init__(self, vllm_config: "VllmConfig", req_states: LogitsProcRequestState):
|
||||
self.prompt_len_np = req_states.prompt_len.np
|
||||
max_num_reqs = req_states.max_num_reqs
|
||||
device = req_states.device
|
||||
|
||||
# Allowed token IDs.
|
||||
self.num_allowed_token_ids = UvaBackedTensor(
|
||||
self.max_num_reqs, dtype=torch.int32
|
||||
)
|
||||
self.num_allowed_token_ids = UvaBackedTensor(max_num_reqs, dtype=torch.int32)
|
||||
self.allowed_token_ids = StagedWriteTensor(
|
||||
(self.max_num_reqs, MAX_NUM_ALLOWED_TOKEN_IDS),
|
||||
dtype=torch.int32,
|
||||
device=device,
|
||||
(max_num_reqs, MAX_NUM_ALLOWED_TOKEN_IDS), dtype=torch.int32, device=device
|
||||
)
|
||||
# Logit bias.
|
||||
self.num_logit_bias = UvaBackedTensor(self.max_num_reqs, dtype=torch.int32)
|
||||
self.num_logit_bias = UvaBackedTensor(max_num_reqs, dtype=torch.int32)
|
||||
self.logit_bias_token_ids = StagedWriteTensor(
|
||||
(self.max_num_reqs, MAX_NUM_LOGIT_BIAS_TOKENS),
|
||||
dtype=torch.int32,
|
||||
device=device,
|
||||
(max_num_reqs, MAX_NUM_LOGIT_BIAS_TOKENS), dtype=torch.int32, device=device
|
||||
)
|
||||
self.logit_bias = StagedWriteTensor(
|
||||
(self.max_num_reqs, MAX_NUM_LOGIT_BIAS_TOKENS),
|
||||
(max_num_reqs, MAX_NUM_LOGIT_BIAS_TOKENS),
|
||||
dtype=torch.float32,
|
||||
device=device,
|
||||
)
|
||||
# Min tokens.
|
||||
self.min_lens = UvaBackedTensor(self.max_num_reqs, dtype=torch.int32)
|
||||
self.num_stop_token_ids = UvaBackedTensor(self.max_num_reqs, dtype=torch.int32)
|
||||
self.restore_when_all_masked = UvaBackedTensor(
|
||||
self.max_num_reqs, dtype=torch.int32
|
||||
)
|
||||
self.min_lens = UvaBackedTensor(max_num_reqs, dtype=torch.int32)
|
||||
self.num_stop_token_ids = UvaBackedTensor(max_num_reqs, dtype=torch.int32)
|
||||
self.restore_when_all_masked = UvaBackedTensor(max_num_reqs, dtype=torch.int32)
|
||||
self.stop_token_ids = StagedWriteTensor(
|
||||
(self.max_num_reqs, MAX_NUM_STOP_TOKEN_IDS),
|
||||
dtype=torch.int32,
|
||||
device=device,
|
||||
(max_num_reqs, MAX_NUM_STOP_TOKEN_IDS), dtype=torch.int32, device=device
|
||||
)
|
||||
|
||||
# Using any of the above.
|
||||
self.use_logit_bias = np.zeros(max_num_reqs, dtype=bool)
|
||||
|
||||
def add_request(
|
||||
self, req_idx: int, prompt_len: int, sampling_params: SamplingParams
|
||||
) -> None:
|
||||
def add_request(self, req_idx: int, sampling_params: SamplingParams) -> bool:
|
||||
prompt_len = int(self.prompt_len_np[req_idx])
|
||||
# Using any logit bias.
|
||||
use_logit_bias = False
|
||||
|
||||
@@ -112,6 +113,7 @@ class LogitBiasState:
|
||||
self.restore_when_all_masked.np[req_idx] = 0
|
||||
|
||||
self.use_logit_bias[req_idx] = use_logit_bias
|
||||
return use_logit_bias
|
||||
|
||||
def apply_staged_writes(self) -> None:
|
||||
self.num_allowed_token_ids.copy_to_uva()
|
||||
@@ -126,25 +128,19 @@ class LogitBiasState:
|
||||
self.restore_when_all_masked.copy_to_uva()
|
||||
self.stop_token_ids.apply_write()
|
||||
|
||||
def apply_logit_bias(
|
||||
self,
|
||||
logits: torch.Tensor,
|
||||
expanded_idx_mapping: torch.Tensor,
|
||||
idx_mapping_np: np.ndarray,
|
||||
pos: torch.Tensor,
|
||||
) -> None:
|
||||
if not np.any(self.use_logit_bias[idx_mapping_np]):
|
||||
def apply(self, logits: torch.Tensor, ctx: LogitsContext) -> torch.Tensor:
|
||||
if not np.any(self.use_logit_bias[ctx.idx_mapping_np]):
|
||||
# No request uses logit bias. Skip the kernel launch.
|
||||
return
|
||||
return logits
|
||||
|
||||
enable_stop_token_restore = bool(
|
||||
np.any(self.restore_when_all_masked.np[idx_mapping_np])
|
||||
np.any(self.restore_when_all_masked.np[ctx.idx_mapping_np])
|
||||
)
|
||||
|
||||
apply_logit_bias(
|
||||
logits,
|
||||
expanded_idx_mapping,
|
||||
pos,
|
||||
ctx.expanded_idx_mapping,
|
||||
ctx.pos,
|
||||
self.num_allowed_token_ids.gpu,
|
||||
self.allowed_token_ids.gpu,
|
||||
self.num_logit_bias.gpu,
|
||||
@@ -156,6 +152,7 @@ class LogitBiasState:
|
||||
self.stop_token_ids.gpu,
|
||||
enable_stop_token_restore,
|
||||
)
|
||||
return logits
|
||||
|
||||
|
||||
@triton.jit
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""Custom logits processors for the V2 model runner.
|
||||
|
||||
Kept import-light: the frontend process imports this package to validate
|
||||
per-request params, so nothing here may pull in model-runner side modules
|
||||
(torch, triton, worker state) at import time.
|
||||
"""
|
||||
|
||||
from vllm.v1.worker.gpu.sample.logits_processor.interface import (
|
||||
LogitsContext,
|
||||
LogitsProcessor,
|
||||
LogitsProcRequestState,
|
||||
)
|
||||
from vllm.v1.worker.gpu.sample.logits_processor.loader import (
|
||||
build_custom_logits_processors,
|
||||
build_custom_logits_processors_params_validator,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"LogitsProcRequestState",
|
||||
"LogitsContext",
|
||||
"LogitsProcessor",
|
||||
"build_custom_logits_processors",
|
||||
"build_custom_logits_processors_params_validator",
|
||||
]
|
||||
@@ -0,0 +1,158 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""V2 logits processor interface.
|
||||
|
||||
Import-light by design: custom processor classes are also loaded in the
|
||||
frontend process (to validate per-request params), so this module must not
|
||||
pull in model-runner side modules (torch, triton, worker state) at import
|
||||
time. Heavy imports live under TYPE_CHECKING; annotations are deferred.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import numpy as np
|
||||
import torch
|
||||
|
||||
from vllm.config import VllmConfig
|
||||
from vllm.sampling_params import SamplingParams
|
||||
from vllm.v1.worker.gpu.buffer_utils import StagedWriteTensor, UvaBackedTensor
|
||||
from vllm.v1.worker.gpu.states import RequestState
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class LogitsProcRequestState:
|
||||
"""State associated with active requests, shared with logits processors.
|
||||
|
||||
Wraps the model runner's per-slot buffers, which are mutated in place,
|
||||
so reads always see current values. Processors must treat every field
|
||||
as read-only.
|
||||
"""
|
||||
|
||||
device: torch.device
|
||||
max_num_reqs: int
|
||||
vocab_size: int
|
||||
|
||||
# [max_num_reqs, max_model_len] committed token ids per request slot.
|
||||
all_token_ids: StagedWriteTensor
|
||||
# [max_num_reqs] tokens in the user-provided prompt.
|
||||
prompt_len: UvaBackedTensor
|
||||
# [max_num_reqs] tokens fed at the latest (re)fill: the prompt plus any
|
||||
# partial output on resumption after preemption.
|
||||
prefill_len: UvaBackedTensor
|
||||
# [max_num_reqs] prompt_len + output_len; grows as the request progresses.
|
||||
total_len: StagedWriteTensor
|
||||
|
||||
@classmethod
|
||||
def from_request_state(cls, req_states: RequestState) -> LogitsProcRequestState:
|
||||
return cls(
|
||||
device=req_states.device,
|
||||
max_num_reqs=req_states.max_num_reqs,
|
||||
vocab_size=req_states.vocab_size,
|
||||
all_token_ids=req_states.all_token_ids,
|
||||
prompt_len=req_states.prompt_len,
|
||||
prefill_len=req_states.prefill_len,
|
||||
total_len=req_states.total_len,
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class LogitsContext:
|
||||
"""The current step's batch layout, passed to every ``apply()`` call.
|
||||
|
||||
A row is a logits row, not a request: rows are reordered every step, and
|
||||
under speculative decoding a request owns one row per draft token.
|
||||
Committed tokens live in ``req_states.all_token_ids`` (valid up to
|
||||
``total_len``); this step's draft tokens are only in ``input_ids``.
|
||||
"""
|
||||
|
||||
# [num_logits_rows] row -> persistent request slot.
|
||||
expanded_idx_mapping: torch.Tensor
|
||||
# [num_reqs] batch position -> persistent request slot.
|
||||
idx_mapping: torch.Tensor
|
||||
# [num_reqs] batch position -> persistent request slot, on the host, for
|
||||
# skipping work without a device sync.
|
||||
idx_mapping_np: np.ndarray
|
||||
# [num_logits_rows] row -> its offset among the rows of its own request.
|
||||
expanded_local_pos: torch.Tensor
|
||||
# [num_logits_rows] token fed to the model at each row's input position.
|
||||
input_ids: torch.Tensor
|
||||
# [num_logits_rows] position of each row within its sequence.
|
||||
pos: torch.Tensor
|
||||
# [num_reqs] batch position -> upper bound of tokens visible to the token
|
||||
# being sampled this step, on the host. Exact when spec decoding isn't in use.
|
||||
# Exact per-row lengths on device are `pos + 1`.
|
||||
seq_lens_upper_bound_np: np.ndarray
|
||||
|
||||
|
||||
class LogitsProcessor(ABC):
|
||||
"""Custom logits processor for Model Runner V2.
|
||||
|
||||
Per-request state is keyed by the request slot index; slots are recycled
|
||||
through a free list, so per-slot state must be fully (re)initialized in
|
||||
``add_request()``.
|
||||
|
||||
``apply()`` runs after the built-in bias, penalty, bad-words and grammar
|
||||
stages and before temperature, min_p and top-k/top-p, so it sees unscaled
|
||||
logits and must not re-inflate grammar-masked tokens. Thinking-budget
|
||||
forcing runs after ``apply()`` and wins over its edits for requests whose
|
||||
budget is exhausted.
|
||||
|
||||
State that is constant for a request belongs in ``__init__()`` or
|
||||
``add_request()``; ``apply()`` receives only what changes per step.
|
||||
"""
|
||||
|
||||
def __init__( # noqa: B027
|
||||
self, vllm_config: VllmConfig, req_states: LogitsProcRequestState
|
||||
):
|
||||
"""Capture what stays constant for the processor's lifetime.
|
||||
|
||||
``req_states`` exposes the on-device token history and batch constants a
|
||||
processor may read. Treat it as read-only.
|
||||
"""
|
||||
|
||||
@classmethod # noqa: B027
|
||||
def validate_params(cls, sampling_params: SamplingParams) -> None:
|
||||
"""Raise ``ValueError`` for invalid per-request arguments.
|
||||
|
||||
Runs at request admission, so invalid arguments fail the request
|
||||
with an error instead of reaching the sampler.
|
||||
"""
|
||||
|
||||
def add_request(self, req_idx: int, sampling_params: SamplingParams) -> bool:
|
||||
"""Initialize per-slot state for a request entering the batch.
|
||||
|
||||
The slot may hold a previous occupant's state; overwrite or
|
||||
neutralize all of it here.
|
||||
|
||||
Returns whether this processor modifies logits for the request.
|
||||
"""
|
||||
return True
|
||||
|
||||
def apply_staged_writes(self) -> None: # noqa: B027
|
||||
"""Flush any host-side writes staged by ``add_request()`` to the device.
|
||||
|
||||
Called once per step before the forward pass, after the model runner
|
||||
has flushed ``req_states``, so a processor that stages writes here can
|
||||
read the request's tokens back on device.
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def apply(self, logits: torch.Tensor, ctx: LogitsContext) -> torch.Tensor:
|
||||
"""Modify logits in place or return a new tensor. In-place modification
|
||||
is preferred for efficiency.
|
||||
|
||||
``apply()`` is called once for the whole batch, including rows of
|
||||
requests this processor declined in ``add_request()``, so filter rows
|
||||
via ``ctx.expanded_idx_mapping``.
|
||||
|
||||
Args:
|
||||
logits: [num_logits_rows, vocab_size] float32 tensor.
|
||||
ctx: this step's batch layout.
|
||||
|
||||
"""
|
||||
raise NotImplementedError
|
||||
@@ -0,0 +1,211 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""Loading of custom logits processor classes for the V2 model runner.
|
||||
|
||||
This module is deliberately import-light: the frontend process uses it to
|
||||
validate per-request params, so importing it must not pull in model-runner
|
||||
side modules (torch, triton, worker state). Imports needed to instantiate
|
||||
processors are deferred to ``build_custom_logits_processors``.
|
||||
"""
|
||||
|
||||
import importlib
|
||||
from collections.abc import Callable, Sequence
|
||||
from functools import lru_cache
|
||||
from importlib.metadata import entry_points
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from vllm.exceptions import VLLMValidationError
|
||||
from vllm.logger import init_logger
|
||||
from vllm.v1.worker.gpu.sample.logits_processor.interface import (
|
||||
LogitsProcessor,
|
||||
LogitsProcRequestState,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from vllm.config import VllmConfig
|
||||
from vllm.sampling_params import SamplingParams
|
||||
from vllm.v1.worker.gpu.states import RequestState
|
||||
|
||||
logger = init_logger(__name__)
|
||||
|
||||
# Same entry-points group as
|
||||
# vllm.v1.sample.logits_processor.LOGITSPROCS_GROUP; duplicated here so this
|
||||
# module stays importable without the V1 sampler machinery.
|
||||
LOGITSPROCS_GROUP = "vllm.logits_processors"
|
||||
|
||||
|
||||
def _load_v2_logitsprocs_plugins() -> list[type[LogitsProcessor]]:
|
||||
"""Load installed logits processor plugins.
|
||||
|
||||
Plugins registered under ``LOGITSPROCS_GROUP`` ship V1-interface
|
||||
processors today; those crash under the V2 sampler, so reject them at
|
||||
load time.
|
||||
"""
|
||||
from vllm.utils.torch_utils import guard_cuda_initialization
|
||||
|
||||
installed_logitsprocs_plugins = entry_points(group=LOGITSPROCS_GROUP)
|
||||
if len(installed_logitsprocs_plugins) == 0:
|
||||
logger.debug("No logitsprocs plugins installed (group %s).", LOGITSPROCS_GROUP)
|
||||
return []
|
||||
|
||||
logger.debug("Loading installed logitsprocs plugins (group %s):", LOGITSPROCS_GROUP)
|
||||
classes: list[type[LogitsProcessor]] = []
|
||||
for entrypoint in installed_logitsprocs_plugins:
|
||||
try:
|
||||
logger.debug(
|
||||
"- Loading logitproc plugin entrypoint=%s target=%s",
|
||||
entrypoint.name,
|
||||
entrypoint.value,
|
||||
)
|
||||
with guard_cuda_initialization():
|
||||
cls = entrypoint.load()
|
||||
except Exception as e:
|
||||
logger.error("Failed to load LogitsProcessor plugin %s: %s", entrypoint, e)
|
||||
raise RuntimeError(
|
||||
f"Failed to load LogitsProcessor plugin {entrypoint}"
|
||||
) from e
|
||||
if not (isinstance(cls, type) and issubclass(cls, LogitsProcessor)):
|
||||
raise ValueError(
|
||||
f"LogitsProcessor plugin {entrypoint.name} is not a subclass of "
|
||||
"vllm.v1.worker.gpu.sample.logits_processor.LogitsProcessor; "
|
||||
"V1-interface plugins are not supported by the V2 model runner."
|
||||
)
|
||||
classes.append(cls)
|
||||
return classes
|
||||
|
||||
|
||||
def _load_v2_logitsprocs_by_fqcns(
|
||||
logits_processors: Sequence[str | type],
|
||||
) -> list[type[LogitsProcessor]]:
|
||||
"""Resolve a mixed list of processor types and FQCN strings into types.
|
||||
|
||||
FQCN syntax is <module>:<type> i.e. x.y.z:CustomLogitProc.
|
||||
"""
|
||||
from vllm.utils.torch_utils import guard_cuda_initialization
|
||||
|
||||
classes: list[type[LogitsProcessor]] = []
|
||||
for ldx, logitproc in enumerate(logits_processors):
|
||||
if isinstance(logitproc, type):
|
||||
logger.debug(" - Already-loaded logit processor: %s", logitproc.__name__)
|
||||
if not issubclass(logitproc, LogitsProcessor):
|
||||
raise ValueError(
|
||||
f"{logitproc.__name__} is not a subclass of "
|
||||
"vllm.v1.worker.gpu.sample.logits_processor.LogitsProcessor"
|
||||
)
|
||||
classes.append(logitproc)
|
||||
continue
|
||||
|
||||
logger.debug("- Loading logits processor %s", logitproc)
|
||||
parts = logitproc.split(":")
|
||||
if len(parts) != 2:
|
||||
raise ValueError(
|
||||
f"Invalid logits processor FQCN {logitproc!r}. "
|
||||
"Expected format: '<module>:<type>'"
|
||||
)
|
||||
module_path, qualname = parts
|
||||
|
||||
try:
|
||||
with guard_cuda_initialization():
|
||||
module = importlib.import_module(module_path)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
"Failed to load %sth LogitsProcessor plugin %s: %s",
|
||||
ldx,
|
||||
logitproc,
|
||||
e,
|
||||
)
|
||||
raise RuntimeError(
|
||||
f"Failed to load {ldx}th LogitsProcessor plugin {logitproc}"
|
||||
) from e
|
||||
|
||||
obj = module
|
||||
for attr in qualname.split("."):
|
||||
obj = getattr(obj, attr)
|
||||
if not isinstance(obj, type):
|
||||
raise ValueError("Loaded logit processor must be a type.")
|
||||
if not issubclass(obj, LogitsProcessor):
|
||||
raise ValueError(
|
||||
f"{obj.__name__} is not a subclass of "
|
||||
"vllm.v1.worker.gpu.sample.logits_processor.LogitsProcessor"
|
||||
)
|
||||
classes.append(obj)
|
||||
|
||||
return classes
|
||||
|
||||
|
||||
def _load_v2_logitsprocs(
|
||||
logits_processors: Sequence[str | type] | None,
|
||||
) -> list[type[LogitsProcessor]]:
|
||||
"""Load all custom logits processors for the V2 model runner.
|
||||
|
||||
Combines installed plugins (validated as V2 processors) with the
|
||||
user-specified list of processor types and FQCN strings.
|
||||
"""
|
||||
return _load_v2_logitsprocs_plugins() + _load_v2_logitsprocs_by_fqcns(
|
||||
logits_processors or ()
|
||||
)
|
||||
|
||||
|
||||
@lru_cache
|
||||
def _cached_load_v2_logitsprocs(
|
||||
custom_logitsprocs: tuple[str | type, ...],
|
||||
) -> list[type[LogitsProcessor]]:
|
||||
return _load_v2_logitsprocs(list(custom_logitsprocs))
|
||||
|
||||
|
||||
def build_custom_logits_processors(
|
||||
vllm_config: "VllmConfig",
|
||||
req_states: "RequestState",
|
||||
is_pooling_model: bool,
|
||||
custom_logitsprocs: Sequence[str | type] = (),
|
||||
) -> list[LogitsProcessor]:
|
||||
"""Load and instantiate custom logits processors, entrypoint plugins first.
|
||||
|
||||
Raises:
|
||||
ValueError: if a pooling model specifies custom processors, or a
|
||||
loaded class does not implement the V2 interface.
|
||||
RuntimeError: if an FQCN fails to import.
|
||||
|
||||
"""
|
||||
from vllm.v1.sample.logits_processor import STR_POOLING_REJECTS_LOGITSPROCS
|
||||
|
||||
if is_pooling_model:
|
||||
if custom_logitsprocs:
|
||||
raise ValueError(STR_POOLING_REJECTS_LOGITSPROCS)
|
||||
logger.debug(
|
||||
"Skipping logits processor loading because pooling models"
|
||||
" do not support logits processors."
|
||||
)
|
||||
return []
|
||||
|
||||
custom_logitsprocs_classes = _load_v2_logitsprocs(custom_logitsprocs)
|
||||
lp_req_state = LogitsProcRequestState.from_request_state(req_states)
|
||||
return [ctor(vllm_config, lp_req_state) for ctor in custom_logitsprocs_classes]
|
||||
|
||||
|
||||
def build_custom_logits_processors_params_validator(
|
||||
custom_logitsprocs: Sequence[str | type] | None,
|
||||
) -> "Callable[[SamplingParams], None]":
|
||||
"""Load custom processor classes once and return a params validator.
|
||||
|
||||
Called from the frontend at startup. The returned callable runs each
|
||||
processor's ``validate_params`` at request admission.
|
||||
|
||||
Raises:
|
||||
ValueError: if a loaded class does not implement the V2 interface.
|
||||
RuntimeError: if an FQCN fails to import.
|
||||
|
||||
"""
|
||||
if not custom_logitsprocs:
|
||||
return lambda _: None
|
||||
|
||||
classes = _cached_load_v2_logitsprocs(tuple(custom_logitsprocs))
|
||||
|
||||
def validate_params(sampling_params: "SamplingParams") -> None:
|
||||
for cls in classes:
|
||||
try:
|
||||
cls.validate_params(sampling_params)
|
||||
except ValueError as e:
|
||||
raise VLLMValidationError(str(e)) from e
|
||||
|
||||
return validate_params
|
||||
@@ -1,5 +1,7 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
|
||||
@@ -8,16 +10,22 @@ from vllm.triton_utils import tl, triton
|
||||
from vllm.utils.math_utils import cdiv
|
||||
from vllm.utils.torch_utils import async_tensor_h2d
|
||||
from vllm.v1.worker.gpu.buffer_utils import UvaBackedTensor
|
||||
from vllm.v1.worker.gpu.states import RequestState
|
||||
from vllm.v1.worker.gpu.sample.logits_processor.interface import (
|
||||
LogitsContext,
|
||||
LogitsProcessor,
|
||||
LogitsProcRequestState,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from vllm.config import VllmConfig
|
||||
|
||||
|
||||
class PenaltiesState:
|
||||
def __init__(self, req_states: RequestState):
|
||||
class PenaltiesState(LogitsProcessor):
|
||||
def __init__(self, vllm_config: "VllmConfig", req_states: LogitsProcRequestState):
|
||||
self.req_states = req_states
|
||||
|
||||
max_num_reqs = req_states.max_num_reqs
|
||||
self.vocab_size = req_states.vocab_size
|
||||
self.device = req_states.device
|
||||
max_num_reqs = req_states.max_num_reqs
|
||||
vocab_size = req_states.vocab_size
|
||||
|
||||
self.repetition_penalty = UvaBackedTensor(max_num_reqs, dtype=torch.float32)
|
||||
self.frequency_penalty = UvaBackedTensor(max_num_reqs, dtype=torch.float32)
|
||||
@@ -30,20 +38,17 @@ class PenaltiesState:
|
||||
|
||||
# Statistics for penalties.
|
||||
self.prompt_bin_mask = torch.zeros(
|
||||
max_num_reqs,
|
||||
cdiv(self.vocab_size, 32),
|
||||
dtype=torch.int32,
|
||||
device=self.device,
|
||||
max_num_reqs, cdiv(vocab_size, 32), dtype=torch.int32, device=self.device
|
||||
)
|
||||
# TODO(woosuk): This tensor is rarely used but can be very large, taking up
|
||||
# GBs of GPU memory. Optimize the memory usage.
|
||||
self.output_bin_counts = torch.zeros(
|
||||
max_num_reqs, self.vocab_size, dtype=torch.int32, device=self.device
|
||||
max_num_reqs, vocab_size, dtype=torch.int32, device=self.device
|
||||
)
|
||||
|
||||
self._new_penalties_reqs: list[int] = []
|
||||
|
||||
def add_request(self, req_idx: int, sampling_params: SamplingParams) -> None:
|
||||
def add_request(self, req_idx: int, sampling_params: SamplingParams) -> bool:
|
||||
self.repetition_penalty.np[req_idx] = sampling_params.repetition_penalty
|
||||
self.frequency_penalty.np[req_idx] = sampling_params.frequency_penalty
|
||||
self.presence_penalty.np[req_idx] = sampling_params.presence_penalty
|
||||
@@ -52,13 +57,12 @@ class PenaltiesState:
|
||||
self.use_penalty[req_idx] = do_penalty
|
||||
if do_penalty:
|
||||
self._new_penalties_reqs.append(req_idx)
|
||||
return do_penalty
|
||||
|
||||
def apply_staged_writes(self) -> None:
|
||||
if self._new_penalties_reqs:
|
||||
idx_mapping = async_tensor_h2d(
|
||||
self._new_penalties_reqs,
|
||||
dtype=torch.int32,
|
||||
device=self.device,
|
||||
self._new_penalties_reqs, dtype=torch.int32, device=self.device
|
||||
)
|
||||
|
||||
prefill_lens = self.req_states.prefill_len.np[self._new_penalties_reqs]
|
||||
@@ -78,29 +82,23 @@ class PenaltiesState:
|
||||
self.frequency_penalty.copy_to_uva()
|
||||
self.presence_penalty.copy_to_uva()
|
||||
|
||||
def apply_penalties(
|
||||
self,
|
||||
logits: torch.Tensor,
|
||||
expanded_idx_mapping: torch.Tensor,
|
||||
idx_mapping_np: np.ndarray,
|
||||
input_ids: torch.Tensor,
|
||||
expanded_local_pos: torch.Tensor,
|
||||
) -> None:
|
||||
if not np.any(self.use_penalty[idx_mapping_np]):
|
||||
def apply(self, logits: torch.Tensor, ctx: LogitsContext) -> torch.Tensor:
|
||||
if not np.any(self.use_penalty[ctx.idx_mapping_np]):
|
||||
# No request uses penalties. Skip the kernel launch.
|
||||
return
|
||||
return logits
|
||||
|
||||
apply_penalties(
|
||||
logits,
|
||||
expanded_idx_mapping,
|
||||
input_ids,
|
||||
expanded_local_pos,
|
||||
ctx.expanded_idx_mapping,
|
||||
ctx.input_ids,
|
||||
ctx.expanded_local_pos,
|
||||
self.repetition_penalty.gpu,
|
||||
self.frequency_penalty.gpu,
|
||||
self.presence_penalty.gpu,
|
||||
self.prompt_bin_mask,
|
||||
self.output_bin_counts,
|
||||
)
|
||||
return logits
|
||||
|
||||
|
||||
@triton.jit
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
from collections.abc import Sequence
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
|
||||
import vllm.envs as envs
|
||||
from vllm.config import VllmConfig
|
||||
from vllm.config.model import PROCESSED_LOGPROBS_MODES, LogprobsMode
|
||||
from vllm.config.reasoning import ReasoningConfig
|
||||
from vllm.sampling_params import SamplingParams
|
||||
from vllm.v1.sample.ops.topk_topp_sampler import (
|
||||
apply_top_k_top_p,
|
||||
@@ -18,6 +20,11 @@ from vllm.v1.worker.gpu.metrics.logits import get_num_nans
|
||||
from vllm.v1.worker.gpu.sample.bad_words import BadWordsState
|
||||
from vllm.v1.worker.gpu.sample.gumbel import gumbel_sample
|
||||
from vllm.v1.worker.gpu.sample.logit_bias import LogitBiasState
|
||||
from vllm.v1.worker.gpu.sample.logits_processor.interface import (
|
||||
LogitsContext,
|
||||
LogitsProcessor,
|
||||
LogitsProcRequestState,
|
||||
)
|
||||
from vllm.v1.worker.gpu.sample.logprob import (
|
||||
LogprobTokenIdsState,
|
||||
compute_topk_scores,
|
||||
@@ -33,6 +40,7 @@ from vllm.v1.worker.gpu.states import RequestState
|
||||
class Sampler:
|
||||
def __init__(
|
||||
self,
|
||||
vllm_config: VllmConfig,
|
||||
max_num_reqs: int,
|
||||
vocab_size: int,
|
||||
device: torch.device,
|
||||
@@ -41,8 +49,8 @@ class Sampler:
|
||||
num_speculative_tokens: int = 1,
|
||||
use_fp64_gumbel: bool = False,
|
||||
enable_trace_replay: bool = False,
|
||||
reasoning_config: ReasoningConfig | None = None,
|
||||
return_sampling_mask: bool = False,
|
||||
custom_logits_processors: Sequence[LogitsProcessor] = (),
|
||||
):
|
||||
self.logprobs_mode = logprobs_mode
|
||||
self.compute_nans = envs.VLLM_COMPUTE_NANS_IN_LOGITS # False by default.
|
||||
@@ -50,11 +58,25 @@ class Sampler:
|
||||
|
||||
self.req_states = req_states
|
||||
self.sampling_states = SamplingStates(max_num_reqs, vocab_size)
|
||||
self.penalties_state = PenaltiesState(req_states)
|
||||
self.logit_bias_state = LogitBiasState(max_num_reqs, device)
|
||||
self.bad_words_state = BadWordsState(req_states)
|
||||
|
||||
lp_req_state = LogitsProcRequestState.from_request_state(req_states)
|
||||
self.penalties_state = PenaltiesState(vllm_config, lp_req_state)
|
||||
logit_bias_state = LogitBiasState(vllm_config, lp_req_state)
|
||||
bad_words_state = BadWordsState(vllm_config, lp_req_state)
|
||||
|
||||
# List order is pipeline order: bias adds, penalties scale, so the
|
||||
# two do not commute.
|
||||
self.logits_processors: list[LogitsProcessor] = [
|
||||
logit_bias_state,
|
||||
self.penalties_state,
|
||||
bad_words_state,
|
||||
*custom_logits_processors,
|
||||
]
|
||||
|
||||
self.logprob_token_ids_state = LogprobTokenIdsState(max_num_reqs, device)
|
||||
self.thinking_budget_state = ThinkingBudgetState(req_states, reasoning_config)
|
||||
self.thinking_budget_state = ThinkingBudgetState(
|
||||
req_states, vllm_config.reasoning_config
|
||||
)
|
||||
self.trace_replay_state = (
|
||||
TraceReplayState(req_states) if enable_trace_replay else None
|
||||
)
|
||||
@@ -65,41 +87,25 @@ class Sampler:
|
||||
not return_sampling_mask and flashinfer_sampler_supported()
|
||||
)
|
||||
|
||||
def add_request(
|
||||
self, req_idx: int, prompt_len: int, sampling_params: SamplingParams
|
||||
) -> None:
|
||||
self.sampling_states.add_request(req_idx, sampling_params)
|
||||
self.penalties_state.add_request(req_idx, sampling_params)
|
||||
self.logit_bias_state.add_request(req_idx, prompt_len, sampling_params)
|
||||
self.bad_words_state.add_request(req_idx, sampling_params)
|
||||
def add_request(self, req_idx: int, sampling_params: SamplingParams) -> None:
|
||||
needs_processing = self.sampling_states.add_request(req_idx, sampling_params)
|
||||
needs_processing |= self.thinking_budget_state.add_request(
|
||||
req_idx, sampling_params
|
||||
)
|
||||
for processor in self.logits_processors:
|
||||
needs_processing |= processor.add_request(req_idx, sampling_params)
|
||||
self.needs_logits_processing[req_idx] = needs_processing
|
||||
|
||||
self.logprob_token_ids_state.add_request(req_idx, sampling_params)
|
||||
self.thinking_budget_state.add_request(req_idx, sampling_params)
|
||||
if self.trace_replay_state is not None:
|
||||
self.trace_replay_state.add_request(req_idx, sampling_params)
|
||||
|
||||
states = self.sampling_states
|
||||
temperature = states.temperature.np[req_idx]
|
||||
self.needs_logits_processing[req_idx] = (
|
||||
self.logit_bias_state.use_logit_bias[req_idx]
|
||||
or self.penalties_state.use_penalty[req_idx]
|
||||
or self.bad_words_state.num_bad_words.np[req_idx] > 0
|
||||
or (
|
||||
self.thinking_budget_state.enabled
|
||||
and self.thinking_budget_state.use_thinking_budget[req_idx]
|
||||
)
|
||||
or (temperature != 0.0 and temperature != 1.0)
|
||||
or states.min_p.np[req_idx] != 0.0
|
||||
or states.top_k.np[req_idx] != states.vocab_size
|
||||
or states.top_p.np[req_idx] != 1.0
|
||||
)
|
||||
|
||||
def apply_staged_writes(self) -> None:
|
||||
self.sampling_states.apply_staged_writes()
|
||||
self.penalties_state.apply_staged_writes()
|
||||
self.logit_bias_state.apply_staged_writes()
|
||||
self.bad_words_state.apply_staged_writes()
|
||||
self.logprob_token_ids_state.apply_staged_writes()
|
||||
for processor in self.logits_processors:
|
||||
processor.apply_staged_writes()
|
||||
self.thinking_budget_state.apply_staged_writes()
|
||||
self.logprob_token_ids_state.apply_staged_writes()
|
||||
if self.trace_replay_state is not None:
|
||||
self.trace_replay_state.apply_staged_writes()
|
||||
|
||||
@@ -131,6 +137,7 @@ class Sampler:
|
||||
expanded_local_pos = input_batch.expanded_local_pos
|
||||
pos = input_batch.positions[input_batch.logits_indices]
|
||||
input_ids = input_batch.input_ids[input_batch.logits_indices]
|
||||
seq_lens_upper_bound_np = input_batch.seq_lens_cpu_upper_bound.numpy()
|
||||
|
||||
# NOTE(woosuk): We intentionally compute num_nans before sampling to make clear
|
||||
# that num_nans is computed before applying penalties and temperature.
|
||||
@@ -146,6 +153,7 @@ class Sampler:
|
||||
pos,
|
||||
input_ids,
|
||||
expanded_local_pos,
|
||||
seq_lens_upper_bound_np,
|
||||
return_logprobs=logprobs_dims is not None,
|
||||
)
|
||||
|
||||
@@ -215,6 +223,7 @@ class Sampler:
|
||||
pos: torch.Tensor,
|
||||
input_ids: torch.Tensor,
|
||||
expanded_local_pos: torch.Tensor,
|
||||
seq_lens_upper_bound_np: np.ndarray,
|
||||
skip_top_k_top_p: bool = False,
|
||||
) -> torch.Tensor:
|
||||
if not np.any(self.needs_logits_processing[idx_mapping_np]):
|
||||
@@ -223,39 +232,23 @@ class Sampler:
|
||||
# Copy logits to a new FP32 tensor.
|
||||
logits = torch.empty_like(logits, dtype=torch.float32).copy_(logits)
|
||||
|
||||
# Apply logit bias (e.g., allowed_token_ids, min_tokens) in place.
|
||||
self.logit_bias_state.apply_logit_bias(
|
||||
logits, expanded_idx_mapping, idx_mapping_np, pos
|
||||
ctx = LogitsContext(
|
||||
expanded_idx_mapping=expanded_idx_mapping,
|
||||
idx_mapping=idx_mapping,
|
||||
idx_mapping_np=idx_mapping_np,
|
||||
expanded_local_pos=expanded_local_pos,
|
||||
input_ids=input_ids,
|
||||
pos=pos,
|
||||
seq_lens_upper_bound_np=seq_lens_upper_bound_np,
|
||||
)
|
||||
|
||||
# Apply penalties in place.
|
||||
self.penalties_state.apply_penalties(
|
||||
logits,
|
||||
expanded_idx_mapping,
|
||||
idx_mapping_np,
|
||||
input_ids,
|
||||
expanded_local_pos,
|
||||
)
|
||||
# Apply logits processors (native + any custom).
|
||||
for processor in self.logits_processors:
|
||||
logits = processor.apply(logits, ctx)
|
||||
|
||||
# Apply bad words masking in place.
|
||||
self.bad_words_state.apply_bad_words(
|
||||
logits,
|
||||
expanded_idx_mapping,
|
||||
idx_mapping_np,
|
||||
input_ids,
|
||||
expanded_local_pos,
|
||||
)
|
||||
|
||||
# Force the reasoning end marker once a request's thinking budget is
|
||||
# reached; applied before temperature so the forced token is always kept.
|
||||
self.thinking_budget_state.apply(
|
||||
logits,
|
||||
expanded_idx_mapping,
|
||||
idx_mapping,
|
||||
idx_mapping_np,
|
||||
input_ids,
|
||||
expanded_local_pos,
|
||||
)
|
||||
# Forcing runs last so no stage can overwrite the forced end marker
|
||||
# or weaken it by scaling.
|
||||
self.thinking_budget_state.apply(logits, ctx)
|
||||
|
||||
# Apply temperature in place.
|
||||
self.sampling_states.apply_temperature(
|
||||
@@ -282,6 +275,7 @@ class Sampler:
|
||||
pos: torch.Tensor,
|
||||
input_ids: torch.Tensor,
|
||||
expanded_local_pos: torch.Tensor,
|
||||
seq_lens_upper_bound_np: np.ndarray,
|
||||
return_logprobs: bool = False,
|
||||
) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
processed_logits = self.apply_sampling_params(
|
||||
@@ -292,6 +286,7 @@ class Sampler:
|
||||
pos,
|
||||
input_ids,
|
||||
expanded_local_pos,
|
||||
seq_lens_upper_bound_np,
|
||||
skip_top_k_top_p=True,
|
||||
)
|
||||
top_k, top_p = self.sampling_states.get_top_k_top_p(
|
||||
|
||||
@@ -38,14 +38,18 @@ class SamplingStates:
|
||||
# -1 means no logprobs are requested.
|
||||
self.num_logprobs.fill(NO_LOGPROBS)
|
||||
|
||||
def add_request(self, req_idx: int, sampling_params: SamplingParams) -> None:
|
||||
self.temperature.np[req_idx] = sampling_params.temperature
|
||||
self.top_p.np[req_idx] = sampling_params.top_p
|
||||
def add_request(self, req_idx: int, sampling_params: SamplingParams) -> bool:
|
||||
temperature = sampling_params.temperature
|
||||
top_k = sampling_params.top_k
|
||||
top_p = sampling_params.top_p
|
||||
min_p = sampling_params.min_p
|
||||
|
||||
self.temperature.np[req_idx] = temperature
|
||||
self.top_p.np[req_idx] = top_p
|
||||
if top_k <= 0 or top_k > self.vocab_size:
|
||||
top_k = self.vocab_size
|
||||
self.top_k.np[req_idx] = top_k
|
||||
self.min_p.np[req_idx] = sampling_params.min_p
|
||||
self.min_p.np[req_idx] = min_p
|
||||
|
||||
seed = sampling_params.seed
|
||||
self.seeds_set[req_idx] = seed is not None
|
||||
@@ -60,6 +64,13 @@ class SamplingStates:
|
||||
num_logprobs = self.vocab_size
|
||||
self.num_logprobs[req_idx] = num_logprobs
|
||||
|
||||
return temperature != 0.0 and (
|
||||
temperature != 1.0
|
||||
or min_p != 0.0
|
||||
or top_k != self.vocab_size
|
||||
or top_p != 1.0
|
||||
)
|
||||
|
||||
def apply_staged_writes(self) -> None:
|
||||
self.temperature.copy_to_uva()
|
||||
self.top_p.copy_to_uva()
|
||||
|
||||
@@ -9,6 +9,7 @@ from vllm.sampling_params import SamplingParams
|
||||
from vllm.triton_utils import tl, triton
|
||||
from vllm.utils.torch_utils import async_tensor_h2d
|
||||
from vllm.v1.worker.gpu.buffer_utils import UvaBackedTensor
|
||||
from vllm.v1.worker.gpu.sample.logits_processor import LogitsContext
|
||||
from vllm.v1.worker.gpu.states import RequestState
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -78,11 +79,12 @@ class ThinkingBudgetState:
|
||||
end_ids, dtype=torch.int32, device=self.device
|
||||
)
|
||||
|
||||
def add_request(self, req_idx: int, sampling_params: SamplingParams) -> None:
|
||||
def add_request(self, req_idx: int, sampling_params: SamplingParams) -> bool:
|
||||
if not self.enabled:
|
||||
return
|
||||
return False
|
||||
budget = sampling_params.thinking_token_budget
|
||||
self.use_thinking_budget[req_idx] = budget is not None
|
||||
use_thinking_budget = budget is not None
|
||||
self.use_thinking_budget[req_idx] = use_thinking_budget
|
||||
if budget is None:
|
||||
budget = -1
|
||||
else:
|
||||
@@ -91,6 +93,7 @@ class ThinkingBudgetState:
|
||||
if self.thinking_token_budget.np[req_idx] != budget:
|
||||
self.thinking_token_budget.np[req_idx] = budget
|
||||
self._budget_dirty = True
|
||||
return use_thinking_budget
|
||||
|
||||
def apply_staged_writes(self) -> None:
|
||||
if not self.enabled:
|
||||
@@ -107,27 +110,20 @@ class ThinkingBudgetState:
|
||||
self.thinking_token_budget.copy_to_uva()
|
||||
self._budget_dirty = False
|
||||
|
||||
def apply(
|
||||
self,
|
||||
logits: torch.Tensor,
|
||||
expanded_idx_mapping: torch.Tensor,
|
||||
idx_mapping: torch.Tensor,
|
||||
idx_mapping_np: np.ndarray,
|
||||
input_ids: torch.Tensor,
|
||||
expanded_local_pos: torch.Tensor,
|
||||
) -> None:
|
||||
def apply(self, logits: torch.Tensor, ctx: LogitsContext) -> None:
|
||||
idx_mapping_np = ctx.idx_mapping_np
|
||||
if not self.enabled or not np.any(self.use_thinking_budget[idx_mapping_np]):
|
||||
return
|
||||
|
||||
apply_thinking_budget(
|
||||
logits,
|
||||
idx_mapping,
|
||||
expanded_idx_mapping,
|
||||
ctx.idx_mapping,
|
||||
ctx.expanded_idx_mapping,
|
||||
self.thinking_token_budget.gpu,
|
||||
self.req_states.all_token_ids.gpu,
|
||||
self.req_states.total_len.gpu,
|
||||
input_ids,
|
||||
expanded_local_pos,
|
||||
ctx.input_ids,
|
||||
ctx.expanded_local_pos,
|
||||
self.cached_last_start,
|
||||
self.cached_last_end,
|
||||
self.cached_scan_pos,
|
||||
|
||||
@@ -176,6 +176,7 @@ class RejectionSampler:
|
||||
idx_mapping_np: np.ndarray,
|
||||
expanded_idx_mapping: torch.Tensor,
|
||||
expanded_local_pos: torch.Tensor,
|
||||
seq_lens_upper_bound_np: np.ndarray,
|
||||
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
||||
processed_logits = self.sampler.apply_sampling_params(
|
||||
logits,
|
||||
@@ -185,6 +186,7 @@ class RejectionSampler:
|
||||
pos,
|
||||
draft_sampled,
|
||||
expanded_local_pos,
|
||||
seq_lens_upper_bound_np,
|
||||
)
|
||||
sampled, num_sampled = rejection_sample(
|
||||
processed_logits,
|
||||
@@ -251,6 +253,7 @@ class RejectionSampler:
|
||||
input_batch.idx_mapping_np[start:end],
|
||||
input_batch.expanded_idx_mapping[lo:hi],
|
||||
input_batch.expanded_local_pos[lo:hi],
|
||||
input_batch.seq_lens_cpu_upper_bound.numpy()[start:end],
|
||||
)
|
||||
chunk_logprobs = self._get_logprobs_tensors(
|
||||
sampled,
|
||||
|
||||
Reference in New Issue
Block a user