main
py 148 lines 5.28 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 pytest.importorskip("triton")
9 if not torch.cuda.is_available():
10 pytest.skip(
11 "CUDA required for Model Runner V2 bad words tests",
12 allow_module_level=True,
13 )
14
15 from vllm.sampling_params import SamplingParams
16 from vllm.v1.worker.gpu.sample.bad_words import BadWordsState
17 from vllm.v1.worker.gpu.sample.logits_processor import (
18 LogitsContext,
19 LogitsProcRequestState,
20 )
21 from vllm.v1.worker.gpu.states import RequestState
22
23 DEVICE = torch.device("cuda")
24 VOCAB_SIZE = 128
25
26 # Committed tokens: prompt [5], output [10, 11]. Draft tokens: [12, 13].
27 # The sampler passes input_ids gathered at logits_indices, so local position 0
28 # holds the last committed token (11) and draft tokens start at position 1.
29 PROMPT_LEN = 1
30 COMMITTED = [5, 10, 11]
31 INPUT_IDS = [11, 12, 13]
32 LOCAL_POS = [0, 1, 2]
33
34
35 def _make_state(bad_words_token_ids: list[list[int]]) -> tuple[BadWordsState, int]:
36 req_states = RequestState(
37 max_num_reqs=4,
38 max_model_len=64,
39 max_num_batched_tokens=16,
40 num_speculative_steps=4,
41 vocab_size=VOCAB_SIZE,
42 device=DEVICE,
43 )
44 req_states.add_request(
45 req_id="req",
46 prompt_len=PROMPT_LEN,
47 all_token_ids=COMMITTED,
48 num_computed_tokens=len(COMMITTED),
49 max_tokens=32,
50 )
51 req_states.apply_staged_writes()
52
53 req_idx = req_states.req_id_to_index["req"]
54 state = BadWordsState(None, LogitsProcRequestState.from_request_state(req_states))
55 state.add_request(req_idx, SamplingParams(_bad_words_token_ids=bad_words_token_ids))
56 state.apply_staged_writes()
57 return state, req_idx
58
59
60 def _apply(bad_words_token_ids: list[list[int]]) -> torch.Tensor:
61 state, req_idx = _make_state(bad_words_token_ids)
62 num_logits = len(INPUT_IDS)
63 logits = torch.zeros((num_logits, VOCAB_SIZE), device=DEVICE)
64 idx_mapping_np = np.array([req_idx], dtype=np.intp)
65 expanded_idx_mapping = torch.tensor(
66 [req_idx] * num_logits, dtype=torch.int32, device=DEVICE
67 )
68 state.apply(
69 logits,
70 LogitsContext(
71 expanded_idx_mapping=expanded_idx_mapping,
72 idx_mapping=torch.tensor([req_idx], dtype=torch.int32, device=DEVICE),
73 idx_mapping_np=idx_mapping_np,
74 expanded_local_pos=torch.tensor(
75 LOCAL_POS, dtype=torch.int32, device=DEVICE
76 ),
77 input_ids=torch.tensor(INPUT_IDS, dtype=torch.int32, device=DEVICE),
78 pos=torch.zeros(num_logits, dtype=torch.int32, device=DEVICE),
79 seq_lens_upper_bound_np=np.full(1, num_logits, dtype=np.int64),
80 ),
81 )
82 return logits.cpu()
83
84
85 def test_v2_bad_words_slot_reuse_clears_state():
86 """A slot recycled from a bad-words request must not keep masking.
87
88 There is no removal hook by design; safety comes from add_request()
89 overwriting the slot's state unconditionally.
90 """
91 state, req_idx = _make_state([[10, 11, 60]])
92 state.add_request(req_idx, SamplingParams())
93 state.apply_staged_writes()
94 assert int(state.num_bad_words.np[req_idx]) == 0
95
96 num_logits = len(INPUT_IDS)
97 idx_mapping = torch.tensor([req_idx], dtype=torch.int32, device=DEVICE)
98 out = state.apply(
99 torch.zeros((num_logits, VOCAB_SIZE), device=DEVICE),
100 LogitsContext(
101 expanded_idx_mapping=torch.tensor(
102 [req_idx] * num_logits, dtype=torch.int32, device=DEVICE
103 ),
104 idx_mapping=idx_mapping,
105 idx_mapping_np=np.array([req_idx], dtype=np.intp),
106 expanded_local_pos=torch.tensor(
107 LOCAL_POS, dtype=torch.int32, device=DEVICE
108 ),
109 input_ids=torch.tensor(INPUT_IDS, dtype=torch.int32, device=DEVICE),
110 pos=torch.zeros(num_logits, dtype=torch.int32, device=DEVICE),
111 seq_lens_upper_bound_np=np.full(1, num_logits, dtype=np.int64),
112 ),
113 )
114 assert not torch.isinf(out).any()
115
116
117 def test_v2_bad_words_prefix_inside_draft_tokens():
118 """A prefix matching entirely within the draft tokens must mask the bad
119 word's last token at the draft position that completes the prefix."""
120 out = _apply([[12, 13, 40]])
121 expected = torch.zeros_like(out)
122 expected[2, 40] = -float("inf")
123 torch.testing.assert_close(out, expected)
124
125
126 def test_v2_bad_words_prefix_spanning_committed_and_draft_tokens():
127 """A prefix spanning the committed/draft boundary must mask at the row
128 where the prefix completes, not one draft position later."""
129 out = _apply([[11, 12, 30]])
130 expected = torch.zeros_like(out)
131 expected[1, 30] = -float("inf")
132 torch.testing.assert_close(out, expected)
133
134
135 def test_v2_bad_words_no_spurious_match_from_last_committed_token():
136 """The last committed token must not be double-counted as the first draft
137 token; [11, 11] never occurs in output [10, 11] + drafts [12, 13]."""
138 out = _apply([[11, 11, 50]])
139 expected = torch.zeros_like(out)
140 torch.testing.assert_close(out, expected)
141
142
143 def test_v2_bad_words_committed_prefix():
144 """Baseline: a fully committed prefix masks at the first row."""
145 out = _apply([[10, 11, 60]])
146 expected = torch.zeros_like(out)
147 expected[0, 60] = -float("inf")
148 torch.testing.assert_close(out, expected)