diff --git a/python/sglang/srt/managers/tokenizer_manager.py b/python/sglang/srt/managers/tokenizer_manager.py index 6d066b198e64..6f9dac22877a 100644 --- a/python/sglang/srt/managers/tokenizer_manager.py +++ b/python/sglang/srt/managers/tokenizer_manager.py @@ -1033,6 +1033,12 @@ def _validate_one_request( # Validate generation-specific fields if isinstance(obj, GenerateReqInput): + # Only user-supplied token ids can be out of range; ids produced by + # the tokenizer are always valid, so skip the scan for text requests. + if obj.input_ids is not None: + self._validate_input_ids_in_vocab( + input_ids, self.model_config.vocab_size + ) self._validate_token_ids_logprob(obj) if ( obj.return_hidden_states @@ -1117,20 +1123,20 @@ def _validate_token_ids_logprob(self, obj: GenerateReqInput) -> None: def _validate_input_ids_in_vocab( self, input_ids: Union[List[int], List[List[int]]], vocab_size: int ) -> None: - # Handle both single sequence and batch of sequences - if isinstance(input_ids[0], list): - # Batch of sequences - for seq in input_ids: - if any(id >= vocab_size for id in seq): + # Out-of-range ids index the model's embedding out of bounds (on CUDA a + # device-side assert that crashes the whole server); an id < 0 wraps to + # the end of the embedding table (silently wrong). Both are checked. + if not input_ids: + return + # Handle both a single sequence and a batch of sequences. + seqs = input_ids if isinstance(input_ids[0], list) else [input_ids] + for seq in seqs: + for token_id in seq: + if not 0 <= token_id < vocab_size: raise ValueError( - f"The input_ids {seq} contains values greater than the vocab size ({vocab_size})." + f"input_ids contains an out-of-vocabulary token id " + f"{token_id}; valid range is [0, {vocab_size})." ) - else: - # Single sequence - if any(id >= vocab_size for id in input_ids): - raise ValueError( - f"The input_ids {input_ids} contains values greater than the vocab size ({vocab_size})." - ) def _create_tokenized_object( self, diff --git a/test/registered/unit/managers/test_tokenizer_manager_input_ids_validation.py b/test/registered/unit/managers/test_tokenizer_manager_input_ids_validation.py new file mode 100644 index 000000000000..649ff9441b05 --- /dev/null +++ b/test/registered/unit/managers/test_tokenizer_manager_input_ids_validation.py @@ -0,0 +1,56 @@ +"""Unit test for TokenizerManager._validate_input_ids_in_vocab. + +Bug: user-supplied input_ids (native /generate and OpenAI token-id prompts) were +never validated -- _validate_input_ids_in_vocab existed but had zero callers, and +even it only checked id >= vocab_size (missing id < 0). An out-of-range id indexes +the model's embedding out of bounds (on CUDA a device-side assert that crashes the +whole server); a negative id silently wraps to the end of the table. +""" + +import unittest +from types import SimpleNamespace + +from sglang.srt.managers.tokenizer_manager import TokenizerManager +from sglang.test.ci.ci_register import register_cpu_ci +from sglang.test.test_utils import CustomTestCase + +register_cpu_ci(est_time=1, suite="base-a-test-cpu") + +VOCAB_SIZE = 32000 + + +class TestValidateInputIds(CustomTestCase): + def setUp(self): + self.tm = TokenizerManager.__new__(TokenizerManager) + self.tm.model_config = SimpleNamespace(vocab_size=VOCAB_SIZE) + + def _validate(self, ids): + self.tm._validate_input_ids_in_vocab(ids, VOCAB_SIZE) + + def test_above_vocab_raises(self): + with self.assertRaises(ValueError): + self._validate([1, 2, VOCAB_SIZE]) + with self.assertRaises(ValueError): + self._validate([1, 2, 2_000_000_000]) + + def test_negative_raises(self): + # The previous check only guarded id >= vocab_size, so this used to pass. + with self.assertRaises(ValueError): + self._validate([1, -1, 2]) + + def test_batch_out_of_range_raises(self): + with self.assertRaises(ValueError): + self._validate([[1, 2], [3, VOCAB_SIZE + 5]]) + with self.assertRaises(ValueError): + self._validate([[1, 2], [-4, 5]]) + + def test_in_range_passes(self): + self._validate([0, 1, VOCAB_SIZE - 1]) + self._validate([[0, 1], [2, VOCAB_SIZE - 1]]) + + def test_empty_passes(self): + self._validate([]) + + +if __name__ == "__main__": + unittest.main()