main
py 446 lines 18.1 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 import torch
7
8 from tests.v1.sample.utils import create_allowed_token_ids
9 from vllm.platforms import current_platform
10 from vllm.utils.platform_utils import is_pin_memory_available
11 from vllm.utils.torch_utils import make_tensor_with_pad
12 from vllm.v1.sample.logits_processor import LogitsProcessors
13 from vllm.v1.sample.metadata import SamplingMetadata
14 from vllm.v1.sample.sampler import Sampler
15
16 PIN_MEMORY_AVAILABLE = is_pin_memory_available()
17 MAX_NUM_REQS = 256
18 VOCAB_SIZE = 1024
19 NUM_OUTPUT_TOKENS = 20
20 DEVICE_TYPE = current_platform.device_type
21 DEVICES = [
22 f"{DEVICE_TYPE}:{i}"
23 for i in range(1 if current_platform.device_count() == 1 else 2)
24 ]
25 MAX_NUM_PROMPT_TOKENS = 64
26
27
28 def _create_fake_logits(batch_size: int, vocab_size: int) -> torch.Tensor:
29 fake_logits = torch.full((batch_size, vocab_size), 1e-2, dtype=torch.float)
30 return fake_logits
31
32
33 def _create_penalty_tensor(
34 batch_size: int, penalty_value: float, device: torch.device
35 ) -> torch.Tensor:
36 return torch.full(
37 (batch_size,), fill_value=penalty_value, dtype=torch.float, device=device
38 )
39
40
41 def _create_prompt_tokens_tensor(
42 prompt_token_ids: list[list[int]],
43 vocab_size: int,
44 device: torch.device,
45 ) -> torch.Tensor:
46 return make_tensor_with_pad(
47 prompt_token_ids,
48 pad=vocab_size,
49 device=device,
50 dtype=torch.int64,
51 pin_memory=False,
52 )
53
54
55 def _create_bad_words_token_ids(
56 batch_size: int,
57 vocab_size: int,
58 bad_words_lengths: tuple[int, ...],
59 ) -> dict[int, list[list[int]]]:
60 bad_words_token_ids = {}
61 for batch_idx in range(batch_size):
62 token_ids_single_batch = []
63 for bad_words_length in bad_words_lengths:
64 token_ids = np.random.choice(
65 vocab_size, size=bad_words_length, replace=True
66 ).tolist()
67 token_ids_single_batch.append(token_ids)
68 bad_words_token_ids[batch_idx] = token_ids_single_batch
69 if batch_size >= 2:
70 # Test no bad_words for some batch
71 no_bad_words_batch_idx = np.random.choice(batch_size)
72 bad_words_token_ids.pop(no_bad_words_batch_idx, None)
73 return bad_words_token_ids
74
75
76 # Returns all last tokens of bad word sequences that share the same prefix
77 # as `given_prefix` (excluding the last token).
78 def _collect_suffixes_with_same_prefix(
79 given_prefix: list[int], bad_words_token_ids: list[list[int]]
80 ) -> list[int]:
81 return [bwt[-1] for bwt in bad_words_token_ids if bwt[:-1] == given_prefix]
82
83
84 # generate a valid token id that is not in bad_words_token_ids
85 def _generate_valid_token_id(
86 bad_words_token_ids: list[list[int]], vocab_size: int
87 ) -> int:
88 forbidden_start_tokens = set()
89 for bad_word in bad_words_token_ids:
90 forbidden_start_tokens.add(bad_word[0])
91 # Get a safe token that's not in forbidden starts
92 safe_token_candidates = list(set(range(vocab_size)) - forbidden_start_tokens)
93 # Pick a random safe token
94 return np.random.choice(safe_token_candidates)
95
96
97 def _update_output_token_ids_for_bad_words(
98 metadata: SamplingMetadata, vocab_size: int
99 ) -> dict[int, list[int]]:
100 bad_words_last_tokens = {}
101 for batch_idx, bad_words_token_ids in metadata.bad_words_token_ids.items():
102 output_token_ids = metadata.output_token_ids[batch_idx]
103 bad_words_last_token: list[int] = []
104 for i, bad_word_token_ids in enumerate(bad_words_token_ids):
105 if len(bad_word_token_ids) == 1:
106 # Single token id always affects logits
107 bad_words_last_token.append(bad_word_token_ids[0])
108 else:
109 prefix_length = len(bad_word_token_ids) - 1
110 has_bad_words = np.random.choice([True, False])
111 if has_bad_words:
112 prefix = bad_word_token_ids[:-1]
113 output_token_ids[-prefix_length:] = prefix
114 # Collect all last tokens from other bad words
115 # that share this prefix
116 bad_words_last_token.extend(
117 _collect_suffixes_with_same_prefix(prefix, bad_words_token_ids)
118 )
119 break # Maximum one update to output_token_ids
120 else: # Make sure no accidental match to bad words
121 output_token_ids[-1] = _generate_valid_token_id(
122 bad_words_token_ids, vocab_size
123 )
124 bad_words_last_tokens[batch_idx] = bad_words_last_token
125 return bad_words_last_tokens
126
127
128 def _create_default_sampling_metadata(
129 num_output_tokens: int,
130 batch_size: int,
131 vocab_size: int,
132 device: torch.device,
133 ) -> SamplingMetadata:
134 output_token_ids: list[list[int]] = []
135 prompt_token_ids: list[list[int]] = []
136 for _ in range(batch_size):
137 output_token_ids.append(
138 np.random.randint(0, vocab_size, size=num_output_tokens).tolist()
139 )
140 prompt_token_ids.append(
141 np.random.randint(
142 0, vocab_size, size=np.random.randint(1, MAX_NUM_PROMPT_TOKENS)
143 ).tolist()
144 )
145 fake_sampling_metadata = SamplingMetadata(
146 temperature=torch.full((batch_size,), 0.0),
147 all_greedy=True,
148 all_random=False,
149 top_p=None,
150 top_k=None,
151 generators={},
152 max_num_logprobs=0,
153 prompt_token_ids=_create_prompt_tokens_tensor(
154 prompt_token_ids, vocab_size, device
155 ),
156 output_token_ids=output_token_ids,
157 spec_token_ids=[[] for _ in range(batch_size)],
158 frequency_penalties=_create_penalty_tensor(batch_size, 0.0, device),
159 presence_penalties=_create_penalty_tensor(batch_size, 0.0, device),
160 repetition_penalties=_create_penalty_tensor(batch_size, 1.0, device),
161 no_penalties=True,
162 allowed_token_ids_mask=None,
163 bad_words_token_ids={},
164 logitsprocs=LogitsProcessors(),
165 )
166 return fake_sampling_metadata
167
168
169 def _create_weighted_output_token_list(
170 batch_size: int, vocab_size: int
171 ) -> tuple[list[list[int]], list[list[int]]]:
172 """Creates an output token list where each token occurs a distinct
173 number of times.
174
175 For each batch, a random subset of token IDs is selected from the
176 vocabulary. The selected tokens are then added to the output token
177 list, each with a different frequency.
178
179 Returns:
180 tuple[list[list[int]], list[list[int]]]:
181 - The first element is the output token list, where each sublist
182 corresponds to a batch and contains tokens with weighted
183 frequencies.
184 - The second element is a list of distinct token IDs for each
185 batch, ordered by their frequency in the corresponding output
186 list.
187
188 """
189 output_token_ids: list[list[int]] = []
190 sorted_token_ids_in_output: list[list[int]] = []
191 for _ in range(batch_size):
192 distinct_token_ids = np.random.choice(
193 vocab_size, size=np.random.randint(1, 10), replace=False
194 ).tolist()
195 sorted_token_ids_in_output.append(distinct_token_ids)
196 output_token_ids_for_batch = []
197 for index, token_id in enumerate(distinct_token_ids):
198 output_token_ids_for_batch.extend([token_id for _ in range(index + 1)])
199 output_token_ids.append(output_token_ids_for_batch)
200 return output_token_ids, sorted_token_ids_in_output
201
202
203 @pytest.mark.parametrize("device", DEVICES)
204 @pytest.mark.parametrize("batch_size", [1, 2, 32])
205 @pytest.mark.parametrize("presence_penalty", [-2.0, 2.0])
206 def test_sampler_presence_penalty(
207 device: str, batch_size: int, presence_penalty: float
208 ):
209 """Test to verify that if presence penalty is enabled then tokens
210 are penalized as per their presence in the existing output.
211 """
212 torch.set_default_device(device)
213 # Create fake logits where each token is assigned the same
214 # logit value.
215 fake_logits = _create_fake_logits(batch_size, VOCAB_SIZE)
216 sampling_metadata = _create_default_sampling_metadata(
217 NUM_OUTPUT_TOKENS, batch_size, VOCAB_SIZE, torch.device(device)
218 )
219 output_token_ids = sampling_metadata.output_token_ids
220 sampling_metadata.presence_penalties = _create_penalty_tensor(
221 batch_size, presence_penalty, torch.device(device)
222 )
223 sampling_metadata.no_penalties = False
224 sampler = Sampler()
225 logits = sampler.apply_penalties(
226 fake_logits, sampling_metadata, sampling_metadata.output_token_ids
227 )
228 logits = logits.cpu()
229 for batch_idx in range(batch_size):
230 # Since all tokens initially have the same logits, the non-penalized
231 # token ID will be the one with the highest logit value, while the
232 # penalized token ID will be the one with the lowest logit value.
233 non_penalized_token_id = logits[batch_idx].argmax().item()
234 penalized_token_id = logits[batch_idx].argmin().item()
235 if presence_penalty > 0:
236 # If `presence_penalty` is set to a value greater than 0, it
237 # indicates a preference for new tokens over those already
238 # present in the output.
239 # Verify that the penalized token ID exists in the output, while the
240 # non-penalized token ID does not.
241 assert penalized_token_id in output_token_ids[batch_idx]
242 assert non_penalized_token_id not in output_token_ids[batch_idx]
243 elif presence_penalty < 0:
244 # If `presence_penalty` is set to a value less than 0, it indicates
245 # a preference for existing tokens over new ones. Verify that the
246 # non-penalized token ID exists in the output, while the penalized
247 # token ID does not.
248 assert non_penalized_token_id in output_token_ids[batch_idx]
249 assert penalized_token_id not in output_token_ids[batch_idx]
250
251
252 @pytest.mark.parametrize("device", DEVICES)
253 @pytest.mark.parametrize("batch_size", [1, 2, 32])
254 @pytest.mark.parametrize("frequency_penalty", [-2.0, 2.0])
255 def test_sampler_frequency_penalty(
256 device: str, batch_size: int, frequency_penalty: float
257 ):
258 """Test to verify that if frequency penalty is enabled then tokens are
259 penalized as per their frequency of occurrence.
260 """
261 torch.set_default_device(device)
262 # Create fake logits where each token is assigned the same
263 # logit value.
264 fake_logits = _create_fake_logits(batch_size, VOCAB_SIZE)
265 sampling_metadata = _create_default_sampling_metadata(
266 NUM_OUTPUT_TOKENS, batch_size, VOCAB_SIZE, torch.device(device)
267 )
268 sampling_metadata.frequency_penalties = _create_penalty_tensor(
269 batch_size, frequency_penalty, torch.device(device)
270 )
271 output_token_ids, sorted_token_ids_in_output = _create_weighted_output_token_list(
272 batch_size,
273 VOCAB_SIZE,
274 )
275 sampling_metadata.output_token_ids = output_token_ids
276 sampling_metadata.no_penalties = False
277 sampler = Sampler()
278 logits = sampler.apply_penalties(
279 fake_logits, sampling_metadata, sampling_metadata.output_token_ids
280 )
281 logits = logits.cpu()
282 for batch_idx in range(batch_size):
283 non_penalized_token_id = logits[batch_idx].argmax().item()
284 penalized_token_id = logits[batch_idx].argmin().item()
285 distinct_sorted_token_ids_in_output = sorted_token_ids_in_output[batch_idx]
286 most_frequent_token_id = distinct_sorted_token_ids_in_output[
287 len(distinct_sorted_token_ids_in_output) - 1
288 ]
289 if frequency_penalty > 0:
290 # If `frequency_penalty` is set to > 0, it indicates
291 # a preference for new tokens over existing ones. Verify that the
292 # non-penalized token ID is not present in the output, while the
293 # most penalized token is the one that occurs most frequently in
294 # the output.
295 assert non_penalized_token_id not in distinct_sorted_token_ids_in_output
296 assert penalized_token_id == most_frequent_token_id
297 elif frequency_penalty < 0:
298 # If `frequency_penalty` is set to < 0, it indicates
299 # a preference for existing tokens over new ones. Verify that the
300 # non-penalized token ID is the one that occurs most frequently
301 # in the output, while the penalized token ID is one that has not
302 # yet appeared.
303 assert non_penalized_token_id == most_frequent_token_id
304 assert penalized_token_id not in distinct_sorted_token_ids_in_output
305
306
307 @pytest.mark.parametrize("device", DEVICES)
308 @pytest.mark.parametrize("batch_size", [1, 2, 32])
309 @pytest.mark.parametrize("repetition_penalty", [0.1, 1.9])
310 def test_sampler_repetition_penalty(
311 device: str, batch_size: int, repetition_penalty: float
312 ):
313 """Test to verify that when the repetition penalty is enabled, tokens
314 are penalized based on their presence in the prompt or the existing
315 output.
316 """
317 torch.set_default_device(device)
318 # Create fake logits where each token is assigned the same
319 # logit value.
320 fake_logits = _create_fake_logits(batch_size, VOCAB_SIZE)
321 sampling_metadata = _create_default_sampling_metadata(
322 NUM_OUTPUT_TOKENS, batch_size, VOCAB_SIZE, torch.device(device)
323 )
324 sampling_metadata.repetition_penalties = _create_penalty_tensor(
325 batch_size, repetition_penalty, torch.device(device)
326 )
327 sampling_metadata.no_penalties = False
328 sampler = Sampler()
329 logits = sampler.apply_penalties(
330 fake_logits, sampling_metadata, sampling_metadata.output_token_ids
331 )
332 logits = logits.cpu()
333 for batch_idx in range(batch_size):
334 non_penalized_token_id = logits[batch_idx].argmax().item()
335 penalized_token_id = logits[batch_idx].argmin().item()
336 assert sampling_metadata.prompt_token_ids is not None
337 prompt_tokens = sampling_metadata.prompt_token_ids[batch_idx][:].tolist()
338 output_tokens = sampling_metadata.output_token_ids[batch_idx]
339 if repetition_penalty > 1.0:
340 # If `repetition_penalty` > 1.0, verify that the non-penalized
341 # token ID has not been seen before, while the penalized token ID
342 # exists either in the prompt or the output.
343 assert (
344 non_penalized_token_id not in prompt_tokens
345 and non_penalized_token_id not in output_tokens
346 )
347 assert (
348 penalized_token_id in prompt_tokens
349 or penalized_token_id in output_tokens
350 )
351 elif repetition_penalty < 1.0:
352 # If `repetition_penalty` < 1.0, verify that the penalized
353 # token ID has not been seen before, while the non-penalized
354 # token ID exists either in the prompt or the output.
355 assert (
356 penalized_token_id not in prompt_tokens
357 and penalized_token_id not in output_tokens
358 )
359 assert (
360 non_penalized_token_id in prompt_tokens
361 or non_penalized_token_id in output_tokens
362 )
363
364
365 @pytest.mark.parametrize("device", DEVICES)
366 @pytest.mark.parametrize("batch_size", [1, 2, 32])
367 @pytest.mark.parametrize("num_allowed_token_ids", [0, 1, 2])
368 def test_sampler_allowed_token_ids(
369 device: str, batch_size: int, num_allowed_token_ids: int
370 ):
371 """Test to verify that when the repetition penalty is enabled, tokens
372 are penalized based on their presence in the prompt or the existing
373 output.
374 """
375 torch.set_default_device(device)
376 # Create fake logits where each token is assigned the same
377 # logit value.
378 fake_logits = _create_fake_logits(batch_size, VOCAB_SIZE)
379 sampling_metadata = _create_default_sampling_metadata(
380 NUM_OUTPUT_TOKENS, batch_size, VOCAB_SIZE, torch.device(device)
381 )
382 mask = create_allowed_token_ids(
383 batch_size=batch_size,
384 vocab_size=VOCAB_SIZE,
385 num_allowed_token_ids=num_allowed_token_ids,
386 device=device,
387 )
388 sampling_metadata.allowed_token_ids_mask = mask
389 sampler = Sampler()
390 logits = sampler.apply_logits_processors(
391 fake_logits, sampling_metadata, predict_bonus_token=False
392 )
393 logits = logits.cpu()
394 for batch_idx in range(batch_size):
395 logits_for_req = logits[batch_idx]
396 if batch_idx % 2 == 1:
397 assert torch.all(logits_for_req != -float("inf"))
398 continue
399 for token_id in range(VOCAB_SIZE):
400 start = min(batch_idx, VOCAB_SIZE - 1)
401 end = min(batch_idx + num_allowed_token_ids, VOCAB_SIZE - 1)
402 if token_id >= start and token_id < end:
403 assert logits_for_req[token_id] == -float("inf"), (
404 f"{batch_idx}, {token_id}"
405 )
406 else:
407 assert logits_for_req[token_id] != -float("inf")
408
409
410 @pytest.mark.parametrize("device", DEVICES)
411 @pytest.mark.parametrize("batch_size", [1, 2, 32])
412 @pytest.mark.parametrize("bad_words_lengths", [(1,), (1, 3), (2, 2)])
413 def test_sampler_bad_words(
414 device: str, batch_size: int, bad_words_lengths: tuple[int, ...]
415 ):
416 """Test to verify that when the bad words restriction is present, tokens
417 are penalized based on their match with the bad words.
418 """
419 torch.set_default_device(device)
420 # Create fake logits where each token is assigned the same
421 # logit value.
422 fake_logits = _create_fake_logits(batch_size, VOCAB_SIZE)
423 sampling_metadata = _create_default_sampling_metadata(
424 NUM_OUTPUT_TOKENS, batch_size, VOCAB_SIZE, torch.device(device)
425 )
426 sampling_metadata.bad_words_token_ids = _create_bad_words_token_ids(
427 batch_size, VOCAB_SIZE, bad_words_lengths
428 )
429 bad_words_last_tokens = _update_output_token_ids_for_bad_words(
430 sampling_metadata, VOCAB_SIZE
431 )
432 sampler = Sampler()
433 logits = sampler.apply_logits_processors(
434 fake_logits, sampling_metadata, predict_bonus_token=False
435 )
436 logits = logits.cpu()
437 for batch_idx in range(batch_size):
438 logits_for_req = logits[batch_idx]
439 for token_id in range(VOCAB_SIZE):
440 if (
441 batch_idx in bad_words_last_tokens
442 and token_id in bad_words_last_tokens[batch_idx]
443 ):
444 assert logits_for_req[token_id] == -float("inf")
445 else:
446 assert logits_for_req[token_id] != -float("inf")