[Bugfix][Frontend] Validate stop_token_ids against vocab size (#54196)

Signed-off-by: QwertyJack <7554089+QwertyJack@users.noreply.github.com> Co-authored-by: QwertyJack <7554089+QwertyJack@users.noreply.github.com>

jack committed Aug 30, 2026 at 00:25 UTC 5b0e5b69ac1a3884a6479c9537789c95263cc804
1 file changed +46 -15
vllm/sampling_params.py
+46 -15
@@ -789,8 +789,9 @@ class SamplingParams(
789 self._validate_logprobs(model_config)
790 self._validate_logit_bias(model_config)
791 self._validate_trace_replay(model_config, speculative_config)
792 + self._validate_stop_token_ids(model_config)
793 self._validate_logits_processors(model_config)
793 - self._validate_allowed_token_ids(tokenizer)
794 + self._validate_allowed_token_ids(model_config)
795 self._validate_spec_decode(speculative_config)
796 self._validate_diffusion(model_config)
797 self._validate_structured_outputs(
@@ -859,6 +860,31 @@ class SamplingParams(
860 value=num_prompt_logprobs,
861 )
862
863 + def _validate_stop_token_ids(self, model_config: ModelConfig) -> None:
864 + """Validate stop_token_ids are within vocabulary range."""
865 + if not self.stop_token_ids:
866 + return
867 +
868 + # stop_token_ids are used as column indices into the logits tensor,
869 + # whose width is the model's vocab size (LogitsProcessor is built from
870 + # config.vocab_size, InputBatch.vocab_size comes from
871 + # model_config.get_vocab_size()), so use the same bound here — like
872 + # _validate_logit_bias, which indexes the same tensor.
873 + vocab_size = model_config.get_vocab_size()
874 + invalid_token_ids = [
875 + token_id
876 + for token_id in self.stop_token_ids
877 + if token_id < 0 or token_id >= vocab_size
878 + ]
879 +
880 + if invalid_token_ids:
881 + raise VLLMValidationError(
882 + f"token_id(s) {invalid_token_ids} in stop_token_ids contain "
883 + f"out-of-vocab token ids. Vocabulary size: {vocab_size}",
884 + parameter="stop_token_ids",
885 + value=invalid_token_ids,
886 + )
887 +
888 def _validate_logit_bias(self, model_config: ModelConfig) -> None:
889 """Validate logit_bias token IDs are within vocabulary range."""
890 if not self.logit_bias:
@@ -941,7 +967,7 @@ class SamplingParams(
967
968 validate_logits_processors_parameters(model_config.logits_processors, self)
969
944 - def _validate_allowed_token_ids(self, tokenizer: TokenizerLike | None) -> None:
970 + def _validate_allowed_token_ids(self, model_config: ModelConfig) -> None:
971 allowed_token_ids = self.allowed_token_ids
972 if allowed_token_ids is None:
973 return
@@ -953,19 +979,24 @@ class SamplingParams(
979 value=allowed_token_ids,
980 )
981
956 - if tokenizer is not None:
957 - vocab_size = len(tokenizer)
958 - invalid_token_ids = [
959 - token_id
960 - for token_id in allowed_token_ids
961 - if token_id < 0 or token_id >= vocab_size
962 - ]
963 - if invalid_token_ids:
964 - raise VLLMValidationError(
965 - "allowed_token_ids contains out-of-vocab token id!",
966 - parameter="allowed_token_ids",
967 - value=invalid_token_ids,
968 - )
982 + # allowed_token_ids are client-supplied ids used as column indices
983 + # into the logits tensor (the mask in InputBatch is sized by
984 + # model_config.get_vocab_size(), and the ids are written as
985 + # mask[req_index][allowed_token_ids]), so use the same bound here —
986 + # like _validate_stop_token_ids and _validate_logit_bias, which
987 + # index the same tensor.
988 + vocab_size = model_config.get_vocab_size()
989 + invalid_token_ids = [
990 + token_id
991 + for token_id in allowed_token_ids
992 + if token_id < 0 or token_id >= vocab_size
993 + ]
994 + if invalid_token_ids:
995 + raise VLLMValidationError(
996 + "allowed_token_ids contains out-of-vocab token id!",
997 + parameter="allowed_token_ids",
998 + value=invalid_token_ids,
999 + )
1000
1001 def _validate_spec_decode(
1002 self,