[Bugfix][Frontend] Stop beam search on generation config EOS tokens (#62072)
pre-commit / pre-run-check (push) Canceled after 0s
pre-commit / pre-commit (push) Canceled after 0s

Signed-off-by: mottopanikeiku <176798723+mottopanikeiku@users.noreply.github.com>
Co-authored-by: mottopanikeiku <176798723+mottopanikeiku@users.noreply.github.com>
This commit is contained in:
alp
2026-10-18 16:49:42 +00:00
committed by GitHub
co-authored by mottopanikeiku
parent fe8f2140ee
commit ff01a6b14f
5 changed files with 110 additions and 16 deletions
+1
View File
@@ -99,6 +99,7 @@ def test_beam_search_abort_returns_partial_outputs_and_continues_other_prompts(
llm = LLM.__new__(LLM) llm = LLM.__new__(LLM)
llm.llm_engine = Mock() llm.llm_engine = Mock()
llm.llm_engine.input_processor.generation_config_fields = {}
tokenizer = SimpleNamespace( tokenizer = SimpleNamespace(
eos_token_id=0, eos_token_id=0,
decode=lambda tokens, skip_special_tokens=False: " ".join( decode=lambda tokens, skip_special_tokens=False: " ".join(
+70 -1
View File
@@ -67,6 +67,7 @@ class _VllmConfig:
class _InputProcessor: class _InputProcessor:
vllm_config = _VllmConfig() vllm_config = _VllmConfig()
generation_config_fields: dict[str, Any] = {}
def resolve_watermarking(self, params): def resolve_watermarking(self, params):
return self.vllm_config._check_supports_watermarking(params) return self.vllm_config._check_supports_watermarking(params)
@@ -128,7 +129,7 @@ class _OfflineServing(BeamSearchOfflineMixin):
self, self,
instances_batch: list[BeamSearchInstance], instances_batch: list[BeamSearchInstance],
base_sampling_params: SamplingParams, base_sampling_params: SamplingParams,
eos_token_id: int | None, eos_token_ids: set[int],
ignore_eos: bool, ignore_eos: bool,
beam_width: int, beam_width: int,
sort_beams_key: Callable[..., Any], sort_beams_key: Callable[..., Any],
@@ -140,6 +141,74 @@ class _OfflineServing(BeamSearchOfflineMixin):
return True return True
@pytest.mark.asyncio
@pytest.mark.parametrize("offline", [False, True])
@pytest.mark.parametrize("config_eos", [None, 11, [0, 11]])
@pytest.mark.parametrize("ignore_eos", [False, True])
async def test_beam_search_respects_generation_config_eos(
monkeypatch, offline: bool, config_eos: int | list[int] | None, ignore_eos: bool
) -> None:
"""Model-specific end-of-turn tokens must finish beams like tokenizer EOS."""
monkeypatch.setattr(
_InputProcessor, "generation_config_fields", {"eos_token_id": config_eos}
)
prompt: TokensInput = {
"type": "token",
"prompt_token_ids": [1],
}
params = BeamSearchParams(
beam_width=1,
max_tokens=3,
ignore_eos=ignore_eos,
include_stop_str_in_output=True,
watermarking=False,
)
result = await anext(
_EngineClient().generate(prompt, SamplingParams(watermarking=False))
)
result.outputs[0].logprobs = [{11: Logprob(-0.1)}]
should_stop = config_eos is not None and not ignore_eos
expected_tokens = [11] if should_stop else [11, 11, 11]
engine_params: list[SamplingParams] = []
if offline:
offline_serving = _OfflineServing()
monkeypatch.setattr(
_OfflineServing,
"_beam_search_step",
BeamSearchOfflineMixin._beam_search_step,
)
def render_and_run_requests(*, params, **kwargs):
engine_params.extend(params)
return [result]
monkeypatch.setattr(
offline_serving, "_render_and_run_requests", render_and_run_requests
)
beam = offline_serving.beam_search(cast(list, [prompt]), params)[0].sequences[0]
assert beam.tokens == [1, *expected_tokens]
else:
async def generate(engine_prompt, sampling_params, *args, **kwargs):
engine_params.append(sampling_params)
yield result
online_serving = _AsyncServing()
monkeypatch.setattr(online_serving.engine_client, "generate", generate)
output = (
await anext(online_serving.beam_search(prompt, "request", params))
).outputs[0]
assert output.token_ids == expected_tokens
assert output.finish_reason == ("stop" if should_stop else "length")
assert output.stop_reason == (11 if should_stop else None)
# The engine validates stop_token_ids against the vocab before it applies
# the generation config, so the config EOS ids must not be copied into them.
assert engine_params
assert all(not p.stop_token_ids for p in engine_params)
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_beam_search_handles_extra_logprob_candidates() -> None: async def test_beam_search_handles_extra_logprob_candidates() -> None:
prompt: TokensInput = { prompt: TokensInput = {
@@ -27,6 +27,7 @@ from .utils import (
BeamSearchOutput, BeamSearchOutput,
BeamSearchSequence, BeamSearchSequence,
create_sort_beams_key_function, create_sort_beams_key_function,
get_beam_search_eos_token_ids,
) )
logger = init_logger(__name__) logger = init_logger(__name__)
@@ -86,8 +87,6 @@ class BeamSearchOfflineMixin(OfflineInferenceMixin):
self.llm_engine.input_processor.resolve_watermarking(params) self.llm_engine.input_processor.resolve_watermarking(params)
tokenizer = self.renderer.get_tokenizer() tokenizer = self.renderer.get_tokenizer()
eos_token_id = tokenizer.eos_token_id
sort_beams_key = create_sort_beams_key_function(eos_token_id, length_penalty)
engine_inputs = self._preprocess_cmpl(prompts) engine_inputs = self._preprocess_cmpl(prompts)
lora_requests = self._lora_request_to_seq(lora_request, len(engine_inputs)) lora_requests = self._lora_request_to_seq(lora_request, len(engine_inputs))
@@ -129,6 +128,11 @@ class BeamSearchOfflineMixin(OfflineInferenceMixin):
params.structured_outputs, tokenizer params.structured_outputs, tokenizer
) )
eos_token_ids = get_beam_search_eos_token_ids(
self.llm_engine.input_processor.generation_config_fields,
tokenizer.eos_token_id,
)
sort_beams_key = create_sort_beams_key_function(eos_token_ids, length_penalty)
instances: list[BeamSearchInstance] = [] instances: list[BeamSearchInstance] = []
for lora_req, prompt in zip(lora_requests, engine_inputs): for lora_req, prompt in zip(lora_requests, engine_inputs):
@@ -169,7 +173,7 @@ class BeamSearchOfflineMixin(OfflineInferenceMixin):
should_stop = self._beam_search_step( should_stop = self._beam_search_step(
instances_batch=instances_batch, instances_batch=instances_batch,
base_sampling_params=base_sampling_params, base_sampling_params=base_sampling_params,
eos_token_id=eos_token_id, eos_token_ids=eos_token_ids,
ignore_eos=ignore_eos, ignore_eos=ignore_eos,
beam_width=beam_width, beam_width=beam_width,
sort_beams_key=sort_beams_key, sort_beams_key=sort_beams_key,
@@ -204,7 +208,7 @@ class BeamSearchOfflineMixin(OfflineInferenceMixin):
self, self,
instances_batch: list[BeamSearchInstance], instances_batch: list[BeamSearchInstance],
base_sampling_params: SamplingParams, base_sampling_params: SamplingParams,
eos_token_id: int | None, eos_token_ids: set[int],
ignore_eos: bool, ignore_eos: bool,
beam_width: int, beam_width: int,
sort_beams_key: Callable, sort_beams_key: Callable,
@@ -350,7 +354,7 @@ class BeamSearchOfflineMixin(OfflineInferenceMixin):
cum_logprob=current_beam.cum_logprob + logprob_obj.logprob, cum_logprob=current_beam.cum_logprob + logprob_obj.logprob,
) )
if token_id == eos_token_id and not ignore_eos: if token_id in eos_token_ids and not ignore_eos:
instance.completed.append(new_beam) instance.completed.append(new_beam)
else: else:
instance_new_beams.append(new_beam) instance_new_beams.append(new_beam)
@@ -20,6 +20,7 @@ from vllm.utils.async_utils import collect_from_async_generator
from .utils import ( from .utils import (
BeamSearchSequence, BeamSearchSequence,
create_sort_beams_key_function, create_sort_beams_key_function,
get_beam_search_eos_token_ids,
) )
@@ -47,8 +48,6 @@ class BeamSearchOnlineMixin(ABC):
self.engine_client.input_processor.resolve_watermarking(params) self.engine_client.input_processor.resolve_watermarking(params)
tokenizer = self.renderer.get_tokenizer() tokenizer = self.renderer.get_tokenizer()
eos_token_id = tokenizer.eos_token_id
sort_beams_key = create_sort_beams_key_function(eos_token_id, length_penalty)
if prompt["type"] == "embeds": if prompt["type"] == "embeds":
raise NotImplementedError("Embedding prompt not supported for beam search") raise NotImplementedError("Embedding prompt not supported for beam search")
@@ -74,6 +73,11 @@ class BeamSearchOnlineMixin(ABC):
sampling_params, self.engine_client.input_processor.vllm_config sampling_params, self.engine_client.input_processor.vllm_config
) )
allowed = set(params.allowed_token_ids) if params.allowed_token_ids else None allowed = set(params.allowed_token_ids) if params.allowed_token_ids else None
eos_token_ids = get_beam_search_eos_token_ids(
self.engine_client.input_processor.generation_config_fields,
tokenizer.eos_token_id,
)
sort_beams_key = create_sort_beams_key_function(eos_token_ids, length_penalty)
all_beams = [ all_beams = [
BeamSearchSequence( BeamSearchSequence(
orig_prompt=prompt, orig_prompt=prompt,
@@ -159,17 +163,17 @@ class BeamSearchOnlineMixin(ABC):
candidate_logprob = ( candidate_logprob = (
current_beam.cum_logprob + logprob_obj.logprob current_beam.cum_logprob + logprob_obj.logprob
) )
if token_id == eos_token_id and not ignore_eos: if token_id in eos_token_ids and not ignore_eos:
completed.append( completed.append(
BeamSearchSequence( BeamSearchSequence(
orig_prompt=prompt, orig_prompt=prompt,
tokens=current_beam.tokens + [eos_token_id] tokens=current_beam.tokens + [token_id]
if include_stop_str_in_output if include_stop_str_in_output
else current_beam.tokens, else current_beam.tokens,
logprobs=current_beam.logprobs + [logprobs], logprobs=current_beam.logprobs + [logprobs],
cum_logprob=candidate_logprob, cum_logprob=candidate_logprob,
finish_reason="stop", finish_reason="stop",
stop_reason=eos_token_id, stop_reason=token_id,
) )
) )
else: else:
@@ -219,7 +223,7 @@ class BeamSearchOnlineMixin(ABC):
best_beams = sorted_completed[:beam_width] best_beams = sorted_completed[:beam_width]
for beam in best_beams: for beam in best_beams:
if beam.tokens[-1] == eos_token_id and not ignore_eos: if beam.tokens[-1] in eos_token_ids and not ignore_eos:
# Skip the eos token in the text. # Skip the eos token in the text.
tokens = beam.tokens[tokenized_length:-1] tokens = beam.tokens[tokenized_length:-1]
else: else:
+20 -4
View File
@@ -2,6 +2,7 @@
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
from dataclasses import dataclass from dataclasses import dataclass
from typing import Any
from vllm.inputs import ( from vllm.inputs import (
DecoderOnlyEngineInput, DecoderOnlyEngineInput,
@@ -13,6 +14,7 @@ from vllm.inputs import (
) )
from vllm.logprobs import Logprob from vllm.logprobs import Logprob
from vllm.lora.request import LoRARequest from vllm.lora.request import LoRARequest
from vllm.sampling_params import SamplingParams
@dataclass @dataclass
@@ -137,7 +139,7 @@ class BeamSearchInstance:
def get_beam_search_score( def get_beam_search_score(
tokens: list[int], tokens: list[int],
cumulative_logprob: float, cumulative_logprob: float,
eos_token_id: int, eos_token_ids: set[int],
length_penalty: float = 1.0, length_penalty: float = 1.0,
) -> float: ) -> float:
"""Calculate the beam search score with length penalty. """Calculate the beam search score with length penalty.
@@ -147,17 +149,31 @@ def get_beam_search_score(
https://github.com/huggingface/transformers/blob/ccb92be23def445f2afdea94c31286f84b89eb5b/src/transformers/generation/beam_search.py#L938 https://github.com/huggingface/transformers/blob/ccb92be23def445f2afdea94c31286f84b89eb5b/src/transformers/generation/beam_search.py#L938
""" """
seq_len = len(tokens) seq_len = len(tokens)
if tokens[-1] == eos_token_id: if tokens[-1] in eos_token_ids:
seq_len -= 1 seq_len -= 1
# An aborted beam may contain only an EOS prompt token. # An aborted beam may contain only an EOS prompt token.
return cumulative_logprob / (max(seq_len, 1) ** length_penalty) return cumulative_logprob / (max(seq_len, 1) ** length_penalty)
def create_sort_beams_key_function(eos_token_id: int, length_penalty: float): def create_sort_beams_key_function(eos_token_ids: set[int], length_penalty: float):
def sort_beams_key(x: BeamSearchSequence) -> float: def sort_beams_key(x: BeamSearchSequence) -> float:
return get_beam_search_score( return get_beam_search_score(
x.tokens, x.cum_logprob, eos_token_id, length_penalty x.tokens, x.cum_logprob, eos_token_ids, length_penalty
) )
return sort_beams_key return sort_beams_key
def get_beam_search_eos_token_ids(
generation_config: dict[str, Any], eos_token_id: int | None
) -> set[int]:
"""Return the token ids that end generation: the tokenizer EOS plus the
generation config's ``eos_token_id``.
This uses a separate SamplingParams so the params sent to the engine keep
their own ``stop_token_ids`` and are validated as before.
"""
params = SamplingParams()
params.update_from_generation_config(generation_config, eos_token_id)
return params.all_stop_token_ids