main
py 258 lines 8.5 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 import numpy as np
5 import pytest
6
7 from vllm.benchmarks.datasets.utils import get_sampling_params
8 from vllm.tokenizers import TokenizerLike
9
10
11 class _FakeTokenizer(TokenizerLike):
12 """Minimal tokenizer implementing the TokenizerLike protocol
13 for testing get_sampling_params."""
14
15 def __init__(self, vocab_size: int = 1000, num_special_tokens: int = 0) -> None:
16 self._vocab_size = vocab_size
17 self._num_special_tokens = num_special_tokens
18
19 # -- Properties required by TokenizerLike --
20
21 @classmethod
22 def from_pretrained(cls, path_or_repo_id, *a, **kw):
23 return cls()
24
25 @property
26 def vocab_size(self) -> int:
27 return self._vocab_size
28
29 @property
30 def all_special_tokens(self) -> list[str]:
31 return []
32
33 @property
34 def all_special_ids(self) -> list[int]:
35 return []
36
37 @property
38 def bos_token_id(self) -> int:
39 return 0
40
41 @property
42 def eos_token_id(self) -> int:
43 return 1
44
45 @property
46 def pad_token_id(self) -> int:
47 return 2
48
49 @property
50 def is_fast(self) -> bool:
51 return False
52
53 @property
54 def max_token_id(self) -> int:
55 return self._vocab_size - 1
56
57 @property
58 def max_chars_per_token(self) -> int:
59 return 4
60
61 @property
62 def truncation_side(self) -> str:
63 return "right"
64
65 def num_special_tokens_to_add(self) -> int:
66 return self._num_special_tokens
67
68 def __call__(self, text, text_pair=None, **kw): # type: ignore[override]
69 raise NotImplementedError
70
71 def get_vocab(self) -> dict[str, int]:
72 return {}
73
74 def get_added_vocab(self) -> dict[str, int]:
75 return {}
76
77 def encode(self, text, **kw) -> list[int]: # type: ignore[override]
78 raise NotImplementedError
79
80 def apply_chat_template(self, messages, **kw): # type: ignore[override]
81 raise NotImplementedError
82
83 def convert_tokens_to_ids(self, tokens):
84 raise NotImplementedError
85
86 def convert_tokens_to_string(self, tokens: list[str]) -> str:
87 raise NotImplementedError
88
89 def decode(self, ids, skip_special_tokens: bool = False) -> str:
90 raise NotImplementedError
91
92 def convert_ids_to_tokens(
93 self, ids, skip_special_tokens: bool = False
94 ) -> list[str]:
95 raise NotImplementedError
96
97
98 class TestGetSamplingParams:
99 """Tests for ``get_sampling_params`` in ``vllm.benchmarks.datasets.shared``."""
100
101 # -- helpers --
102
103 @staticmethod
104 def _tok(vocab_size: int = 1000, num_special: int = 0) -> _FakeTokenizer:
105 return _FakeTokenizer(vocab_size=vocab_size, num_special_tokens=num_special)
106
107 # -- return shape / dtype --
108
109 def test_returns_three_arrays(self):
110 rng = np.random.default_rng(0)
111 result = get_sampling_params(rng, 5, 0.0, 100, 50, self._tok())
112 assert len(result) == 3
113 for arr in result:
114 assert isinstance(arr, np.ndarray)
115
116 @pytest.mark.parametrize("n", [1, 10, 100])
117 def test_output_length_matches_num_requests(self, n: int):
118 rng = np.random.default_rng(42)
119 input_lens, output_lens, offsets = get_sampling_params(
120 rng, n, 0.0, 64, 32, self._tok()
121 )
122 assert input_lens.shape == (n,)
123 assert output_lens.shape == (n,)
124 assert offsets.shape == (n,)
125
126 # -- fixed lengths (range_ratio = 0) --
127
128 def test_zero_range_ratio_gives_constant_lengths(self):
129 rng = np.random.default_rng(7)
130 input_lens, output_lens, _ = get_sampling_params(
131 rng, 20, 0.0, 128, 64, self._tok()
132 )
133 assert np.all(input_lens == 128)
134 assert np.all(output_lens == 64)
135
136 def test_special_tokens_subtracted_from_input_only(self):
137 rng = np.random.default_rng(7)
138 input_lens, output_lens, _ = get_sampling_params(
139 rng, 10, 0.0, 100, 50, self._tok(num_special=4)
140 )
141 # real_input_len = 100 - 4 = 96, range_ratio 0 → all 96
142 assert np.all(input_lens == 96)
143 # special tokens are not subtracted from output length
144 assert np.all(output_lens == 50)
145
146 # -- range ratios --
147
148 def test_input_range_bounds(self):
149 rng = np.random.default_rng(0)
150 ratio = 0.5
151 base = 200
152 input_lens, _, _ = get_sampling_params(
153 rng, 500, {"input": ratio, "output": 0.0}, base, 50, self._tok()
154 )
155 lo = int(np.floor(base * (1 - ratio)))
156 hi = int(np.ceil(base * (1 + ratio)))
157 assert np.all(input_lens >= lo)
158 assert np.all(input_lens <= hi)
159
160 def test_output_range_bounds(self):
161 rng = np.random.default_rng(0)
162 ratio = 0.3
163 base = 100
164 _, output_lens, _ = get_sampling_params(
165 rng, 500, {"input": 0.0, "output": ratio}, 50, base, self._tok()
166 )
167 lo = max(1, int(np.floor(base * (1 - ratio))))
168 hi = int(np.ceil(base * (1 + ratio)))
169 assert np.all(output_lens >= lo)
170 assert np.all(output_lens <= hi)
171
172 def test_output_low_clamped_to_one(self):
173 """Even with a high ratio that would push output_low to 0,
174 the function clamps it to 1."""
175 rng = np.random.default_rng(0)
176 # output_len=1, ratio=0.99 → floor(1*0.01)=0, should clamp to 1
177 _, output_lens, _ = get_sampling_params(
178 rng, 50, {"input": 0.0, "output": 0.99}, 100, 1, self._tok()
179 )
180 assert np.all(output_lens >= 1)
181
182 # -- offsets bounded by vocab_size --
183
184 @pytest.mark.parametrize("vocab", [100, 32000, 128256])
185 def test_offsets_within_vocab(self, vocab: int):
186 rng = np.random.default_rng(0)
187 _, _, offsets = get_sampling_params(
188 rng, 200, 0.0, 64, 32, self._tok(vocab_size=vocab)
189 )
190 assert np.all(offsets >= 0)
191 assert np.all(offsets < vocab)
192
193 # -- reproducibility --
194
195 def test_same_seed_same_results(self):
196 tok = self._tok()
197 rr = {"input": 0.3, "output": 0.2}
198 a = get_sampling_params(np.random.default_rng(42), 50, rr, 256, 64, tok)
199 b = get_sampling_params(np.random.default_rng(42), 50, rr, 256, 64, tok)
200 for arr_a, arr_b in zip(a, b):
201 np.testing.assert_array_equal(arr_a, arr_b)
202
203 def test_different_seed_different_results(self):
204 tok = self._tok()
205 rr = {"input": 0.3, "output": 0.2}
206 a = get_sampling_params(np.random.default_rng(0), 50, rr, 256, 64, tok)
207 b = get_sampling_params(np.random.default_rng(1), 50, rr, 256, 64, tok)
208 # Extremely unlikely all three arrays match with different seeds
209 assert not all(np.array_equal(arr_a, arr_b) for arr_a, arr_b in zip(a, b))
210
211 # -- validation / error paths --
212
213 @pytest.mark.parametrize("bad_ratio", [-0.1, 1.0, 1.5])
214 def test_invalid_input_range_ratio(self, bad_ratio: float):
215 rng = np.random.default_rng(0)
216 with pytest.raises(ValueError, match="input_range_ratio"):
217 get_sampling_params(
218 rng, 10, {"input": bad_ratio, "output": 0.0}, 100, 50, self._tok()
219 )
220
221 @pytest.mark.parametrize("bad_ratio", [-0.1, 1.0, 1.5])
222 def test_invalid_output_range_ratio(self, bad_ratio: float):
223 rng = np.random.default_rng(0)
224 with pytest.raises(ValueError, match="output_range_ratio"):
225 get_sampling_params(
226 rng, 10, {"input": 0.0, "output": bad_ratio}, 100, 50, self._tok()
227 )
228
229 def test_invalid_dict_missing_keys(self):
230 rng = np.random.default_rng(0)
231 with pytest.raises(ValueError, match="input.*output"):
232 get_sampling_params(rng, 10, {"input": 0.1}, 100, 50, self._tok())
233
234 def test_input_len_zero_with_special_tokens(self):
235 """input_len < num_special_tokens → real_input_len = 0, which is fine
236 (range [0, 0])."""
237 rng = np.random.default_rng(0)
238 input_lens, _, _ = get_sampling_params(
239 rng, 5, 0.0, 5, 50, self._tok(num_special=10)
240 )
241 # real_input_len = max(0, 5 - 10) = 0
242 assert np.all(input_lens == 0)
243
244 # -- edge cases --
245
246 def test_single_request(self):
247 rng = np.random.default_rng(0)
248 i, o, off = get_sampling_params(rng, 1, 0.0, 100, 50, self._tok())
249 assert i.shape == (1,)
250 assert o.shape == (1,)
251 assert off.shape == (1,)
252
253 def test_large_num_requests(self):
254 rng = np.random.default_rng(0)
255 i, o, off = get_sampling_params(rng, 10_000, 0.5, 512, 128, self._tok())
256 assert i.shape == (10_000,)
257 assert o.shape == (10_000,)
258 assert off.shape == (10_000,)