[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:
David Cheung
2026-09-19 17:46:49 +00:00
committed by GitHub
co-authored by Claude Nick Hill
parent 751f6807d9
commit 5302d1fe40
32 changed files with 1215 additions and 510 deletions
+77 -276
View File
@@ -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
+9
View File
@@ -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."""
+8
View File
@@ -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
+49 -6
View File
@@ -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."""
+6
View File
@@ -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 (
+29 -6
View File
@@ -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)
+38 -7
View File
@@ -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)
+17 -7
View File
@@ -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)
-12
View File
@@ -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 "
-8
View File
@@ -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:
+28 -1
View File
@@ -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(
+2 -4
View File
@@ -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(
+8 -4
View File
@@ -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
+27 -28
View File
@@ -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
+4
View File
@@ -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,
+34 -37
View File
@@ -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
+26 -28
View File
@@ -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
+59 -64
View File
@@ -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(
+15 -4
View File
@@ -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()
+12 -16
View File
@@ -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,