Files
sglang/test/registered/unit/sampling/test_sampling_batch_info.py
leavelet 87a22b17ce Fuse SamplingBatchInfo tensor construction into one pass
from_schedule_batch built temperature/top_p/top_k/min_p (+seed) with
4-5 separate list comprehensions and one synchronous H2D copy each,
plus 4 more passes for the is_all_greedy/need_* flags. Collect
everything in a single pass over reqs and upload the float params as
one pinned non-blocking H2D copy (disjoint device views of one
buffer; filter/merge only index and cat, producing fresh tensors, so
the shared buffer is safe), int32 top_k and optional int64 seeds as
their own pinned copies.

B300 (torch 2.11 cu130), scheduler-thread blocking time per call:
  bs=8: 38.8 -> 18.2 us (2.1x); bs=32: 1.9x; bs=200: 1.2x
CPU-only construction at bs=200: 2890 -> 1387 us (2.1x).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 05:04:39 +00:00

619 lines
26 KiB
Python

"""Unit tests for srt/sampling/sampling_batch_info.py — no server, no model loading."""
from sglang.test.ci.ci_register import register_cpu_ci
register_cpu_ci(est_time=5, suite="stage-a-test-cpu")
import unittest
from unittest.mock import MagicMock, patch
import torch
from sglang.srt.sampling.sampling_batch_info import (
SamplingBatchInfo,
merge_bias_tensor,
)
from sglang.srt.sampling.sampling_params import TOP_K_ALL
from sglang.test.test_utils import CustomTestCase
VOCAB_SIZE = 32
DEVICE = "cpu"
# Helper: construct a minimal SamplingBatchInfo
def _make_info(batch_size=2, **overrides):
"""Create a SamplingBatchInfo with sane defaults for testing."""
defaults = dict(
temperatures=torch.ones(batch_size, 1),
top_ps=torch.ones(batch_size),
top_ks=torch.full((batch_size,), TOP_K_ALL, dtype=torch.int32),
min_ps=torch.zeros(batch_size),
is_all_greedy=False,
need_top_p_sampling=False,
need_top_k_sampling=False,
need_min_p_sampling=False,
vocab_size=VOCAB_SIZE,
device=DEVICE,
penalizer_orchestrator=MagicMock(is_required=False),
)
defaults.update(overrides)
return SamplingBatchInfo(**defaults)
class TestMergeBiasTensor(CustomTestCase):
def test_both_none_returns_none(self):
"""Test that merging two None tensors returns None."""
result = merge_bias_tensor(None, None, 2, 3, DEVICE, 0.0)
self.assertIsNone(result)
def test_both_present_concatenates(self):
"""Test that two present tensors are concatenated along batch dim."""
lhs = torch.ones(2, VOCAB_SIZE)
rhs = torch.zeros(3, VOCAB_SIZE)
result = merge_bias_tensor(lhs, rhs, 2, 3, DEVICE, 0.0)
self.assertEqual(result.shape, (5, VOCAB_SIZE))
self.assertEqual(result[0, 0].item(), 1.0)
self.assertEqual(result[3, 0].item(), 0.0)
def test_lhs_none_fills_default(self):
"""Test that missing lhs is filled with default value before concatenation."""
rhs = torch.ones(3, VOCAB_SIZE)
result = merge_bias_tensor(None, rhs, 2, 3, DEVICE, 0.0)
self.assertEqual(result.shape, (5, VOCAB_SIZE))
# First 2 rows filled with default (0.0)
self.assertEqual(result[0, 0].item(), 0.0)
# Last 3 rows from rhs
self.assertEqual(result[2, 0].item(), 1.0)
def test_rhs_none_fills_default(self):
"""Test that missing rhs is filled with default value before concatenation."""
lhs = torch.ones(2, VOCAB_SIZE)
result = merge_bias_tensor(lhs, None, 2, 3, DEVICE, 0.0)
self.assertEqual(result.shape, (5, VOCAB_SIZE))
self.assertEqual(result[0, 0].item(), 1.0)
# Last 3 rows filled with default (0.0)
self.assertEqual(result[3, 0].item(), 0.0)
def test_custom_default_value(self):
"""Test that a custom default (-1.0) fills the missing lhs rows."""
rhs = torch.ones(1, VOCAB_SIZE)
result = merge_bias_tensor(None, rhs, 2, 1, DEVICE, -1.0)
self.assertEqual(result[0, 0].item(), -1.0)
self.assertEqual(result[1, 0].item(), -1.0)
self.assertEqual(result[2, 0].item(), 1.0)
# SamplingBatchInfo.__len__
class TestSamplingBatchInfoLen(CustomTestCase):
def test_len_matches_batch_size(self):
"""Test that __len__ returns batch size (number of temperature rows)."""
info = _make_info(batch_size=5)
self.assertEqual(len(info), 5)
class TestMergeCustomLogitProcessor(CustomTestCase):
def test_both_none_returns_none(self):
"""Test that merging two None processor dicts returns None."""
result = SamplingBatchInfo.merge_custom_logit_processor(
None, None, 2, 3, DEVICE
)
self.assertIsNone(result)
def test_same_key_merges_masks(self):
"""Test that same processor key concatenates the boolean masks."""
proc = MagicMock()
lhs = {42: (proc, torch.tensor([True, False]))}
rhs = {42: (proc, torch.tensor([False, True, True]))}
result = SamplingBatchInfo.merge_custom_logit_processor(lhs, rhs, 2, 3, DEVICE)
self.assertIn(42, result)
self.assertEqual(result[42][1].shape[0], 5)
self.assertTrue(result[42][1][0].item()) # from lhs
self.assertFalse(result[42][1][1].item()) # from lhs
self.assertTrue(result[42][1][3].item()) # from rhs
def test_disjoint_keys(self):
"""Test that disjoint processor keys are merged with zero-filled padding."""
proc_a = MagicMock()
proc_b = MagicMock()
lhs = {1: (proc_a, torch.tensor([True, False]))}
rhs = {2: (proc_b, torch.tensor([True]))}
result = SamplingBatchInfo.merge_custom_logit_processor(lhs, rhs, 2, 1, DEVICE)
# Key 1: lhs mask [True, False] + zero-filled rhs [False]
self.assertEqual(result[1][1].shape[0], 3)
self.assertTrue(result[1][1][0].item())
self.assertFalse(result[1][1][2].item())
# Key 2: zero-filled lhs [False, False] + rhs mask [True]
self.assertEqual(result[2][1].shape[0], 3)
self.assertFalse(result[2][1][0].item())
self.assertTrue(result[2][1][2].item())
def test_lhs_none_rhs_present(self):
"""Test that None lhs is treated as empty dict and rhs mask is padded."""
proc = MagicMock()
rhs = {10: (proc, torch.tensor([True]))}
result = SamplingBatchInfo.merge_custom_logit_processor(None, rhs, 2, 1, DEVICE)
self.assertIn(10, result)
self.assertEqual(result[10][1].shape[0], 3)
# apply_logits_bias
class TestApplyLogitsBias(CustomTestCase):
def test_applies_linear_penalties(self):
"""Test that pre-accumulated linear penalties are added to logits."""
info = _make_info(batch_size=1)
info.acc_linear_penalties = torch.tensor([[-1.0] * VOCAB_SIZE])
logits = torch.zeros(1, VOCAB_SIZE)
info.apply_logits_bias(logits)
self.assertAlmostEqual(logits[0, 0].item(), -1.0, places=5)
def test_applies_logit_bias(self):
"""Test that per-token logit_bias is added to logits."""
info = _make_info(batch_size=1)
bias = torch.zeros(1, VOCAB_SIZE)
bias[0, 5] = 10.0
info.logit_bias = bias
logits = torch.zeros(1, VOCAB_SIZE)
info.apply_logits_bias(logits)
self.assertAlmostEqual(logits[0, 5].item(), 10.0, places=5)
self.assertAlmostEqual(logits[0, 0].item(), 0.0, places=5)
def test_applies_vocab_mask(self):
"""Test that vocab_mask triggers the apply_mask_func callback."""
info = _make_info(batch_size=1)
info.vocab_mask = torch.ones(1, VOCAB_SIZE)
info.apply_mask_func = MagicMock()
logits = torch.zeros(1, VOCAB_SIZE)
info.apply_logits_bias(logits)
info.apply_mask_func.assert_called_once()
def test_applies_penalizer_orchestrator(self):
"""Test that a required orchestrator's apply() is called on logits."""
orch = MagicMock(is_required=True)
info = _make_info(batch_size=1, penalizer_orchestrator=orch)
logits = torch.zeros(1, VOCAB_SIZE)
info.apply_logits_bias(logits)
orch.apply.assert_called_once_with(logits)
def test_no_bias_no_change(self):
"""Test that logits stay unchanged when no bias sources are set."""
info = _make_info(batch_size=1)
info.acc_linear_penalties = None
info.logit_bias = None
info.vocab_mask = None
logits = torch.zeros(1, VOCAB_SIZE)
original = logits.clone()
info.apply_logits_bias(logits)
self.assertTrue(torch.equal(logits, original))
# update_penalties
class TestUpdatePenalties(CustomTestCase):
def test_required_creates_penalties_tensor(self):
"""Test that update_penalties allocates a zero tensor and calls orchestrator.apply."""
orch = MagicMock(is_required=True)
info = _make_info(batch_size=2, penalizer_orchestrator=orch)
info.update_penalties()
self.assertIsNotNone(info.acc_linear_penalties)
self.assertEqual(info.acc_linear_penalties.shape, (2, VOCAB_SIZE))
orch.apply.assert_called_once()
def test_not_required_sets_none(self):
"""Test that update_penalties sets acc_linear_penalties to None when not required."""
orch = MagicMock(is_required=False)
info = _make_info(batch_size=2, penalizer_orchestrator=orch)
info.update_penalties()
self.assertIsNone(info.acc_linear_penalties)
# update_regex_vocab_mask
class TestUpdateRegexVocabMask(CustomTestCase):
def test_no_grammars_clears_mask(self):
"""Test that None grammars clears both vocab_mask and apply_mask_func."""
info = _make_info(batch_size=1)
info.grammars = None
info.update_regex_vocab_mask()
self.assertIsNone(info.vocab_mask)
self.assertIsNone(info.apply_mask_func)
def test_empty_grammars_clears_mask(self):
"""Test that empty grammars list clears vocab_mask."""
info = _make_info(batch_size=1)
info.grammars = []
info.update_regex_vocab_mask()
self.assertIsNone(info.vocab_mask)
def test_with_grammars_allocates_and_fills(self):
"""Test that an active grammar gets allocate, fill, and move called."""
grammar = MagicMock()
grammar.finished = False
grammar.is_terminated.return_value = False
grammar.allocate_vocab_mask.return_value = torch.zeros(1, VOCAB_SIZE)
grammar.move_vocab_mask.return_value = torch.zeros(1, VOCAB_SIZE)
info = _make_info(batch_size=1)
info.grammars = [grammar]
info.update_regex_vocab_mask()
grammar.allocate_vocab_mask.assert_called_once()
grammar.fill_vocab_mask.assert_called_once()
grammar.move_vocab_mask.assert_called_once()
def test_mixed_grammars_only_active_fills(self):
"""Test that finished, terminated, and None grammars are skipped."""
active = MagicMock()
active.finished = False
active.is_terminated.return_value = False
active.allocate_vocab_mask.return_value = torch.zeros(3, VOCAB_SIZE)
active.move_vocab_mask.return_value = torch.zeros(3, VOCAB_SIZE)
finished = MagicMock()
finished.finished = True
terminated = MagicMock()
terminated.finished = False
terminated.is_terminated.return_value = True
info = _make_info(batch_size=3)
info.grammars = [active, finished, terminated]
info.update_regex_vocab_mask()
active.fill_vocab_mask.assert_called_once()
finished.fill_vocab_mask.assert_not_called()
terminated.fill_vocab_mask.assert_not_called()
# filter_batch
class TestFilterBatch(CustomTestCase):
def test_filter_keeps_correct_indices(self):
"""Test that filter retains rows at indices 0 and 2, dropping index 1."""
info = _make_info(batch_size=3)
info.temperatures = torch.tensor([[1.0], [2.0], [3.0]])
info.top_ps = torch.tensor([0.9, 0.8, 0.7])
info.top_ks = torch.tensor([10, 20, 30], dtype=torch.int32)
info.min_ps = torch.tensor([0.0, 0.1, 0.2])
info.logit_bias = torch.ones(3, VOCAB_SIZE)
keep = torch.tensor([0, 2])
info.filter_batch([0, 2], keep)
self.assertEqual(len(info), 2)
self.assertAlmostEqual(info.temperatures[0, 0].item(), 1.0)
self.assertAlmostEqual(info.temperatures[1, 0].item(), 3.0)
self.assertAlmostEqual(info.top_ps[1].item(), 0.7)
# logit_bias should also be filtered
self.assertEqual(info.logit_bias.shape, (2, VOCAB_SIZE))
def test_filter_with_custom_logit_processor(self):
"""Test that filter updates both custom_params list and processor mask."""
proc = MagicMock()
info = _make_info(batch_size=3)
info.has_custom_logit_processor = True
info.custom_logit_processor = {42: (proc, torch.tensor([True, False, True]))}
info.custom_params = [{"a": 1}, {"b": 2}, {"c": 3}]
keep = torch.tensor([0, 2])
info.filter_batch([0, 2], keep)
self.assertEqual(info.custom_params, [{"a": 1}, {"c": 3}])
mask = info.custom_logit_processor[42][1]
self.assertEqual(mask.shape[0], 2)
def test_filter_removes_all_custom_processors(self):
"""Test cleanup when filter removes all requests using a processor."""
proc = MagicMock()
info = _make_info(batch_size=3)
info.has_custom_logit_processor = True
info.custom_logit_processor = {42: (proc, torch.tensor([False, True, False]))}
info.custom_params = [None, {"x": 1}, None]
# Keep only index 0 and 2 — processor 42's mask becomes [False, False]
keep = torch.tensor([0, 2])
info.filter_batch([0, 2], keep)
self.assertFalse(info.has_custom_logit_processor)
self.assertIsNone(info.custom_logit_processor)
def test_filter_with_none_sampling_seed(self):
"""Test that filter preserves None sampling_seed without error."""
info = _make_info(batch_size=3)
info.sampling_seed = None
keep = torch.tensor([1])
info.filter_batch([1], keep)
self.assertIsNone(info.sampling_seed)
# merge_batch
class TestMergeBatch(CustomTestCase):
def test_merge_concatenates_tensors(self):
"""Test that merge concatenates temperature tensors from both batches."""
info1 = _make_info(batch_size=2)
info1.temperatures = torch.tensor([[1.0], [2.0]])
info2 = _make_info(batch_size=1)
info2.temperatures = torch.tensor([[3.0]])
info1.merge_batch(info2)
self.assertEqual(len(info1), 3)
self.assertAlmostEqual(info1.temperatures[2, 0].item(), 3.0)
def test_merge_combines_flags(self):
"""Test that merge ANDs is_all_greedy and ORs need_*_sampling flags."""
info1 = _make_info(
is_all_greedy=True,
need_top_p_sampling=False,
need_top_k_sampling=False,
need_min_p_sampling=False,
)
info2 = _make_info(
is_all_greedy=False,
need_top_p_sampling=True,
need_top_k_sampling=True,
need_min_p_sampling=True,
)
info1.merge_batch(info2)
self.assertFalse(info1.is_all_greedy) # AND semantics
self.assertTrue(info1.need_top_p_sampling) # OR semantics
self.assertTrue(info1.need_top_k_sampling) # OR semantics
self.assertTrue(info1.need_min_p_sampling) # OR semantics
def test_merge_with_logit_bias(self):
"""Test that merge pads missing logit_bias with zeros before concatenation."""
info1 = _make_info(batch_size=1)
info1.logit_bias = torch.ones(1, VOCAB_SIZE)
info2 = _make_info(batch_size=1)
info2.logit_bias = None
info1.merge_batch(info2)
self.assertEqual(info1.logit_bias.shape, (2, VOCAB_SIZE))
def test_merge_with_custom_logit_processor(self):
"""Test that merge combines processors when only one side has them."""
proc = MagicMock()
info1 = _make_info(batch_size=1)
info1.has_custom_logit_processor = True
info1.custom_logit_processor = {1: (proc, torch.tensor([True]))}
info1.custom_params = [{"a": 1}]
info2 = _make_info(batch_size=1)
info2.has_custom_logit_processor = False
info2.custom_logit_processor = None
info2.custom_params = None
info1.merge_batch(info2)
self.assertTrue(info1.has_custom_logit_processor)
self.assertEqual(len(info1.custom_params), 2)
def test_merge_with_none_sampling_seed(self):
"""Test that merge preserves None when both sampling_seeds are None."""
info1 = _make_info(batch_size=1)
info1.sampling_seed = None
info2 = _make_info(batch_size=1)
info2.sampling_seed = None
info1.merge_batch(info2)
self.assertIsNone(info1.sampling_seed)
def test_merge_with_both_sampling_seeds(self):
"""Test that merge concatenates both sampling_seed tensors."""
info1 = _make_info(batch_size=2)
info1.sampling_seed = torch.tensor([10, 20], dtype=torch.int64)
info2 = _make_info(batch_size=1)
info2.sampling_seed = torch.tensor([30], dtype=torch.int64)
info1.merge_batch(info2)
self.assertEqual(info1.sampling_seed.shape[0], 3)
self.assertEqual(info1.sampling_seed[0].item(), 10)
self.assertEqual(info1.sampling_seed[1].item(), 20)
self.assertEqual(info1.sampling_seed[2].item(), 30)
# copy_for_forward
class TestCopyForForward(CustomTestCase):
def test_returns_copy_without_orchestrator(self):
"""Test that copy_for_forward returns a copy with orchestrator set to None."""
orch = MagicMock(is_required=False)
info = _make_info(batch_size=1, penalizer_orchestrator=orch)
copied = info.copy_for_forward()
self.assertIsNone(copied.penalizer_orchestrator)
# Original should still have orchestrator
self.assertIsNotNone(info.penalizer_orchestrator)
# from_schedule_batch
class TestFromScheduleBatch(CustomTestCase):
def _make_req(
self,
temp=1.0,
top_p=1.0,
top_k=-1,
min_p=0.0,
freq=0.0,
presence=0.0,
min_tokens=0,
logit_bias=None,
seed=None,
stop_ids=None,
eos_id=2,
):
req = MagicMock()
req.sampling_params.temperature = temp
req.sampling_params.top_p = top_p
req.sampling_params.top_k = top_k
req.sampling_params.min_p = min_p
req.sampling_params.frequency_penalty = freq
req.sampling_params.presence_penalty = presence
req.sampling_params.min_new_tokens = min_tokens
req.sampling_params.logit_bias = logit_bias
req.sampling_params.sampling_seed = seed
req.sampling_params.stop_token_ids = stop_ids
req.sampling_params.custom_params = None
req.custom_logit_processor = None
req.tokenizer.additional_stop_token_ids = None
req.tokenizer.eos_token_id = eos_id
return req
@patch("sglang.srt.sampling.sampling_batch_info.get_global_server_args")
def test_basic_construction(self, mock_server_args):
"""Test that from_schedule_batch correctly extracts sampling params from requests."""
mock_server_args.return_value.enable_deterministic_inference = False
mock_server_args.return_value.enable_custom_logit_processor = False
reqs = [self._make_req(temp=0.8, top_p=0.9, top_k=50, min_p=0.1)]
batch = MagicMock()
batch.reqs = reqs
batch.device = DEVICE
info = SamplingBatchInfo.from_schedule_batch(batch, VOCAB_SIZE)
self.assertEqual(len(info), 1)
self.assertAlmostEqual(info.temperatures[0, 0].item(), 0.8, places=5)
self.assertAlmostEqual(info.top_ps[0].item(), 0.9, places=5)
self.assertEqual(info.top_ks[0].item(), 50)
@patch("sglang.srt.sampling.sampling_batch_info.get_global_server_args")
def test_mixed_batch_tensor_values_dtypes_shapes(self, mock_server_args):
"""All per-request params land in the right slot with the right dtype,
and the fused float tensors are independent after filtering."""
mock_server_args.return_value.enable_deterministic_inference = False
mock_server_args.return_value.enable_custom_logit_processor = False
params = [
dict(temp=0.5, top_p=0.7, top_k=10, min_p=0.2),
dict(temp=1.0, top_p=1.0, top_k=1, min_p=0.0),
dict(temp=1.3, top_p=0.95, top_k=TOP_K_ALL, min_p=0.05),
]
batch = MagicMock()
batch.reqs = [self._make_req(**p) for p in params]
batch.device = DEVICE
info = SamplingBatchInfo.from_schedule_batch(batch, VOCAB_SIZE)
self.assertEqual(info.temperatures.shape, (3, 1))
self.assertEqual(info.temperatures.dtype, torch.float32)
self.assertEqual(info.top_ps.shape, (3,))
self.assertEqual(info.top_ps.dtype, torch.float32)
self.assertEqual(info.min_ps.shape, (3,))
self.assertEqual(info.top_ks.dtype, torch.int32)
for i, p in enumerate(params):
self.assertAlmostEqual(info.temperatures[i, 0].item(), p["temp"], places=5)
self.assertAlmostEqual(info.top_ps[i].item(), p["top_p"], places=5)
self.assertEqual(info.top_ks[i].item(), p["top_k"])
self.assertAlmostEqual(info.min_ps[i].item(), p["min_p"], places=5)
self.assertFalse(info.is_all_greedy)
self.assertTrue(info.need_top_p_sampling)
self.assertTrue(info.need_top_k_sampling)
self.assertTrue(info.need_min_p_sampling)
# Filtering must yield independent tensors with the right values even
# though construction shares one fused buffer.
keep = torch.tensor([2, 0], dtype=torch.int64)
info.filter_batch([2, 0], keep)
self.assertAlmostEqual(info.temperatures[0, 0].item(), 1.3, places=5)
self.assertAlmostEqual(info.top_ps[1].item(), 0.7, places=5)
self.assertEqual(info.top_ks[0].item(), TOP_K_ALL)
@patch("sglang.srt.sampling.sampling_batch_info.get_global_server_args")
def test_greedy_detection(self, mock_server_args):
"""Test that top_k=1 sets is_all_greedy=True."""
mock_server_args.return_value.enable_deterministic_inference = False
mock_server_args.return_value.enable_custom_logit_processor = False
reqs = [self._make_req(top_k=1)]
batch = MagicMock()
batch.reqs = reqs
batch.device = DEVICE
info = SamplingBatchInfo.from_schedule_batch(batch, VOCAB_SIZE)
self.assertTrue(info.is_all_greedy)
@patch("sglang.srt.sampling.sampling_batch_info.get_global_server_args")
def test_logit_bias_construction(self, mock_server_args):
"""Test that logit_bias dict is converted to a tensor with correct values."""
mock_server_args.return_value.enable_deterministic_inference = False
mock_server_args.return_value.enable_custom_logit_processor = False
reqs = [self._make_req(logit_bias={"5": 2.0, "10": -1.0})]
batch = MagicMock()
batch.reqs = reqs
batch.device = DEVICE
info = SamplingBatchInfo.from_schedule_batch(batch, VOCAB_SIZE)
self.assertIsNotNone(info.logit_bias)
self.assertAlmostEqual(info.logit_bias[0, 5].item(), 2.0)
self.assertAlmostEqual(info.logit_bias[0, 10].item(), -1.0)
self.assertAlmostEqual(info.logit_bias[0, 0].item(), 0.0)
@patch("sglang.srt.sampling.sampling_batch_info.get_global_server_args")
def test_deterministic_seed(self, mock_server_args):
"""Test that explicit seed=123 is kept and missing seed defaults to 42."""
mock_server_args.return_value.enable_deterministic_inference = True
mock_server_args.return_value.enable_custom_logit_processor = False
reqs = [self._make_req(seed=123), self._make_req(seed=None)]
batch = MagicMock()
batch.reqs = reqs
batch.device = DEVICE
info = SamplingBatchInfo.from_schedule_batch(batch, VOCAB_SIZE)
self.assertIsNotNone(info.sampling_seed)
self.assertEqual(info.sampling_seed[0].item(), 123)
self.assertEqual(info.sampling_seed[1].item(), 42) # default
@patch("sglang.srt.sampling.sampling_batch_info.get_global_server_args")
def test_from_schedule_batch_sampling_flags(self, mock_server_args):
"""Test that sampling flags (need_top_p/top_k/min_p) are set correctly."""
mock_server_args.return_value.enable_deterministic_inference = False
mock_server_args.return_value.enable_custom_logit_processor = False
reqs = [self._make_req(top_p=0.9, top_k=50, min_p=0.1)]
batch = MagicMock()
batch.reqs = reqs
batch.device = DEVICE
info = SamplingBatchInfo.from_schedule_batch(batch, VOCAB_SIZE)
self.assertTrue(info.need_top_p_sampling) # 0.9 != 1.0
self.assertTrue(info.need_top_k_sampling) # 50 != TOP_K_ALL
self.assertTrue(info.need_min_p_sampling) # 0.1 > 0
self.assertFalse(info.is_all_greedy) # top_k=50 > 1
@patch("sglang.srt.sampling.sampling_batch_info.get_global_server_args")
def test_no_logit_bias_when_all_none(self, mock_server_args):
"""Test that logit_bias stays None when no request has logit_bias set."""
mock_server_args.return_value.enable_deterministic_inference = False
mock_server_args.return_value.enable_custom_logit_processor = False
reqs = [self._make_req(), self._make_req()]
batch = MagicMock()
batch.reqs = reqs
batch.device = DEVICE
info = SamplingBatchInfo.from_schedule_batch(batch, VOCAB_SIZE)
self.assertIsNone(info.logit_bias)
@patch("sglang.srt.sampling.sampling_batch_info.get_global_server_args")
def test_custom_logit_processor_merging(self, mock_server_args):
"""Test deserialization and merging of custom logit processors."""
from sglang.srt.sampling.custom_logit_processor import (
DisallowedTokensLogitsProcessor,
)
mock_server_args.return_value.enable_deterministic_inference = False
mock_server_args.return_value.enable_custom_logit_processor = True
proc_str = DisallowedTokensLogitsProcessor.to_str()
req1 = self._make_req()
req1.custom_logit_processor = proc_str
req1.sampling_params.custom_params = {"token_ids": [1]}
req2 = self._make_req()
req2.custom_logit_processor = None # no processor
req2.sampling_params.custom_params = None
batch = MagicMock()
batch.reqs = [req1, req2]
batch.device = DEVICE
info = SamplingBatchInfo.from_schedule_batch(batch, VOCAB_SIZE)
self.assertTrue(info.has_custom_logit_processor)
self.assertIsNotNone(info.custom_logit_processor)
self.assertEqual(len(info.custom_logit_processor), 1)
# Check the mask: req1 has processor (True), req2 doesn't (False)
key = list(info.custom_logit_processor.keys())[0]
proc, mask = info.custom_logit_processor[key]
self.assertIsInstance(proc, DisallowedTokensLogitsProcessor)
self.assertTrue(mask[0].item())
self.assertFalse(mask[1].item())
# custom_params should be collected for all reqs
self.assertEqual(len(info.custom_params), 2)
if __name__ == "__main__":
unittest.main()