Fix TokenizerManager crash on top_logprobs with tensor values (#26825)

This commit is contained in:
Kevin Flansburg
2026-06-03 13:55:02 -07:00
committed by GitHub
parent d1bc06b63b
commit 7716fa00e0
2 changed files with 83 additions and 1 deletions
@@ -2188,7 +2188,7 @@ class TokenizerManager(TokenizerControlMixin, TokenizerManagerScoreMixin):
# We should batch all top-k tokens in all positions.
ret = []
for i in range(len(token_logprobs_val)):
if token_logprobs_val[i]:
if token_logprobs_val[i] is not None:
ret.append(
self.detokenize_logprob_tokens(
token_logprobs_val[i], token_logprobs_idx[i], decode_to_text
@@ -0,0 +1,82 @@
"""
Unit tests for TokenizerManager.detokenize_top_logprobs_tokens.
Regression coverage for a crash where a per-position logprob value is a
multi-element torch.Tensor instead of the annotated List[float]. The old
truthiness check ``if token_logprobs_val[i]:`` raises::
RuntimeError: Boolean value of Tensor with more than one value is ambiguous.
which propagates out of the detokenization handler, gets caught by
print_exception_wrapper, and SIGKILLs the whole (prefill) process. The fix
uses an explicit ``is not None`` test that matches the actual sentinel for
skipped positions and works for both lists and tensors.
"""
import unittest
import torch
from sglang.test.ci.ci_register import register_cpu_ci
from sglang.test.test_utils import CustomTestCase, maybe_stub_sgl_kernel
maybe_stub_sgl_kernel()
from sglang.srt.managers.tokenizer_manager import TokenizerManager
register_cpu_ci(est_time=5, suite="base-a-test-cpu")
def _make_tokenizer_manager() -> TokenizerManager:
"""Create a bare TokenizerManager, bypassing __init__.
detokenize_top_logprobs_tokens / detokenize_logprob_tokens only touch
self.tokenizer when decode_to_text=True, so an uninitialised instance is
sufficient for the decode_to_text=False paths exercised here.
"""
return TokenizerManager.__new__(TokenizerManager)
class TestDetokenizeTopLogprobsTensor(CustomTestCase):
def test_multi_element_tensor_value_does_not_crash(self):
"""A multi-element tensor position must be detokenized, not raise."""
tm = _make_tokenizer_manager()
val = [torch.tensor([-0.1, -0.2, -0.3])]
idx = [[10, 20, 30]]
ret = tm.detokenize_top_logprobs_tokens(val, idx, decode_to_text=False)
self.assertEqual(len(ret), 1)
self.assertEqual(
ret[0],
[(-0.1, 10, None), (-0.2, 20, None), (-0.3, 30, None)],
)
def test_none_position_yields_none(self):
"""None is the sentinel for skipped positions and must stay None."""
tm = _make_tokenizer_manager()
val = [None, torch.tensor([-0.5, -0.6])]
idx = [None, [1, 2]]
ret = tm.detokenize_top_logprobs_tokens(val, idx, decode_to_text=False)
self.assertEqual(len(ret), 2)
self.assertIsNone(ret[0])
self.assertEqual(ret[1], [(-0.5, 1, None), (-0.6, 2, None)])
def test_plain_list_values_still_work(self):
"""The ordinary List[float] path is unaffected by the fix."""
tm = _make_tokenizer_manager()
val = [[-0.1, -0.2]]
idx = [[7, 8]]
ret = tm.detokenize_top_logprobs_tokens(val, idx, decode_to_text=False)
self.assertEqual(ret, [[(-0.1, 7, None), (-0.2, 8, None)]])
if __name__ == "__main__":
unittest.main(verbosity=2)