sampler.py 10.7 KB
Newer Older
1
# SPDX-License-Identifier: Apache-2.0
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4
5
6
7
"""A layer that samples the next tokens from the model's outputs."""

import torch
import torch.nn as nn

Yu Guo's avatar
Yu Guo committed
8
from vllm.utils import async_tensor_h2d, is_pin_memory_available
9
from vllm.v1.outputs import LogprobsTensors, SamplerOutput
10
from vllm.v1.sample.metadata import SamplingMetadata
11
from vllm.v1.sample.ops.bad_words import apply_bad_words
Woosuk Kwon's avatar
Woosuk Kwon committed
12
13
from vllm.v1.sample.ops.penalties import (apply_all_penalties,
                                          apply_min_token_penalties)
14
from vllm.v1.sample.ops.topk_topp_sampler import TopKTopPSampler
15
16
17
18
19
20

_SAMPLING_EPS = 1e-5


class Sampler(nn.Module):

21
22
23
    def __init__(self):
        super().__init__()
        self.topk_topp_sampler = TopKTopPSampler()
Yu Guo's avatar
Yu Guo committed
24
        self.pin_memory = is_pin_memory_available()
25

26
27
28
29
30
    def forward(
        self,
        logits: torch.Tensor,
        sampling_metadata: SamplingMetadata,
    ) -> SamplerOutput:
31
32
33
34
35
36
37
38
39
        # NOTE(woosuk): Use the original logits (before any penalties or
        # temperature scaling) for the top-k logprobs.
        # This is different from the V0 sampler, which uses the logits that
        # is used for sampling (after penalties and temperature scaling).
        # TODO(rob): provide option for logprobs post sampling.
        # See https://vllm-dev.slack.com/archives/C07UUL8E61Z/p1735907856007919 # noqa: E501
        num_logprobs = sampling_metadata.max_num_logprobs
        if num_logprobs is not None:
            raw_logprobs = self.compute_logprobs(logits)
40

41
42
        # Use float32 for the logits.
        logits = logits.to(torch.float32)
43
44
        # Apply allowed token ids.
        logits = self.apply_allowed_token_ids(logits, sampling_metadata)
45
46
        # Apply bad words exclusion.
        logits = self.apply_bad_words(logits, sampling_metadata)
47
48
        # Apply logits bias.
        logits = self.apply_logits_bias(logits, sampling_metadata)
49
50
51
52
        # Apply penalties (e.g., min_tokens, freq_penalties).
        logits = self.apply_penalties(logits, sampling_metadata)
        # Sample the next token.
        sampled = self.sample(logits, sampling_metadata)
53
54
55
56
57
        # Convert sampled token ids to int64 (long) type to ensure compatibility
        # with subsequent operations that may use these values as indices.
        # This conversion is necessary because FlashInfer sampling operations
        # return int32 (while PyTorch argmax and topk return int64).
        sampled = sampled.long()
58
59
60
61
62
63

        # Gather the logprobs of the topk and sampled token (if requested).
        # Get logprobs and rank tensors (if requested)
        logprobs_tensors = None if num_logprobs is None else \
            self.gather_logprobs(raw_logprobs, num_logprobs, token_ids=sampled)

64
65
66
        # Use int32 to reduce the tensor size.
        sampled = sampled.to(torch.int32)

67
        # These are GPU tensors.
68
        sampler_output = SamplerOutput(
69
70
71
72
            # The sampled tokens are expanded to 2D tensor with shape
            # [num_requests, 1], where each row represents one generated
            # token per request.
            sampled_token_ids=sampled.unsqueeze(-1),
73
            logprobs_tensors=logprobs_tensors,
74
75
76
77
78
79
80
81
82
        )
        return sampler_output

    def apply_temperature(
        self,
        logits: torch.Tensor,
        temp: torch.Tensor,
    ) -> torch.Tensor:
        # Use in-place division to avoid creating a new tensor.
83
        return logits.div_(temp.unsqueeze(dim=1))
84

85
86
87
88
    def greedy_sample(self, logits: torch.Tensor) -> torch.Tensor:
        return logits.argmax(dim=-1).view(-1)

    def sample(
89
90
91
92
        self,
        logits: torch.Tensor,
        sampling_metadata: SamplingMetadata,
    ) -> torch.Tensor:
93
94
95
96
97
98
        """Sample logits based on sampling metadata.

        The various logits processing functions called in this method
        may update the logits tensor in-place.
        """

99
100
        assert not (sampling_metadata.all_greedy
                    and sampling_metadata.all_random)
101
102
103
104
105
106
        if sampling_metadata.all_random:
            greedy_sampled = None
        else:
            greedy_sampled = self.greedy_sample(logits)
            if sampling_metadata.all_greedy:
                return greedy_sampled
107

108
109
        assert sampling_metadata.temperature is not None

110
111
112
113
        # Apply temperature.
        logits = self.apply_temperature(logits, sampling_metadata.temperature)

        # Apply min_p.
114
        if sampling_metadata.min_p is not None:
115
116
117
            logits = self.apply_min_p(logits, sampling_metadata.min_p)

        # Apply top_k and/or top_p.
118
        random_sampled = self.topk_topp_sampler(
119
            logits,
120
            sampling_metadata.generators,
121
122
123
            sampling_metadata.top_k,
            sampling_metadata.top_p,
        )
124

125
        if greedy_sampled is None:
126
            return random_sampled
127
128
129
130
131

        sampled = torch.where(
            sampling_metadata.temperature < _SAMPLING_EPS,
            greedy_sampled,
            random_sampled,
132
            out=greedy_sampled,  # Reuse tensor
133
134
135
        )
        return sampled

136
137
138
139
    def compute_logprobs(self, logits: torch.Tensor) -> torch.Tensor:
        return logits.log_softmax(dim=-1, dtype=torch.float32)

    def gather_logprobs(
140
        self,
141
142
143
144
145
146
147
148
        logprobs: torch.Tensor,
        num_logprobs: int,
        token_ids: torch.Tensor,
    ) -> LogprobsTensors:
        """
        Gather logprobs for topk and sampled/prompt token.

        Args:
Chen1022's avatar
Chen1022 committed
149
          logprobs: (num tokens) x (vocab) tensor
150
151
152
153
154
155
          num_logprobs: minimum number of logprobs to
                        retain per token
          token_ids: prompt tokens (if prompt logprobs)
                     or sampled tokens (if sampled
                     logprobs); 1D token ID tensor
                     with (num tokens) elements
156
                     Must be int64.
157
158
159
160
161
162

        Returns:
          Top-k int indices tensor, (num tokens) x (num_logprobs + 1)
          Top-k float logprobs tensor, (num tokens) x (num_logprobs + 1)
          Sampled token rank tensor, (num tokens)
        """
163
        assert token_ids.dtype == torch.int64
164
165
166
167
168
169
        # Find the topK values.
        topk_logprobs, topk_indices = torch.topk(logprobs,
                                                 num_logprobs,
                                                 dim=-1)

        # Get with the logprob of the prompt or sampled token.
170
        token_ids = token_ids.unsqueeze(-1)
171
172
173
174
175
176
177
178
179
        token_logprobs = logprobs.gather(-1, token_ids)

        # Compute the ranks of the actual token.
        token_ranks = (logprobs >= token_logprobs).sum(-1)

        # Concatenate together with the topk.
        indices = torch.cat((token_ids, topk_indices), dim=1)
        logprobs = torch.cat((token_logprobs, topk_logprobs), dim=1)

180
        # Use int32 to reduce the tensor size.
181
182
183
        indices = indices.to(torch.int32)

        return LogprobsTensors(indices, logprobs, token_ranks)
184

185
186
187
188
189
    def apply_penalties(
        self,
        logits: torch.Tensor,
        sampling_metadata: SamplingMetadata,
    ) -> torch.Tensor:
190
191
192
193
        if sampling_metadata.min_tokens:
            apply_min_token_penalties(logits,
                                      sampling_metadata.output_token_ids,
                                      sampling_metadata.min_tokens)
194
195
        if not sampling_metadata.no_penalties:
            assert sampling_metadata.prompt_token_ids is not None
Woosuk Kwon's avatar
Woosuk Kwon committed
196
            logits = apply_all_penalties(
197
198
                logits,
                sampling_metadata.prompt_token_ids,
Woosuk Kwon's avatar
Woosuk Kwon committed
199
200
201
                sampling_metadata.presence_penalties,
                sampling_metadata.frequency_penalties,
                sampling_metadata.repetition_penalties,
202
203
                sampling_metadata.output_token_ids,
            )
204
        return logits
205

206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
    def apply_min_p(
        self,
        logits: torch.Tensor,
        min_p: torch.Tensor,
    ) -> torch.Tensor:
        """
        Filters logits using adaptive probability thresholding.
        """
        # Convert logits to probability distribution
        probability_values = torch.nn.functional.softmax(logits, dim=-1)
        # Calculate maximum probabilities per sequence
        max_probabilities = torch.amax(probability_values,
                                       dim=-1,
                                       keepdim=True)
        # Reshape min_p for broadcasting
        adjusted_min_p = min_p.unsqueeze(1) * max_probabilities
        # Identify valid tokens using threshold comparison
        valid_token_mask = probability_values >= adjusted_min_p
        # Apply mask using boolean indexing
        logits[~valid_token_mask] = -float('inf')
        return logits

228
229
230
231
232
233
234
235
    def apply_logits_bias(
        self,
        logits: torch.Tensor,
        sampling_metadata: SamplingMetadata,
    ) -> torch.Tensor:
        # TODO(houseroad): this implementation is extremely inefficient.
        # One idea is implement this as a PyTorch C++ op, and we may
        # even optimize the logit_bias layout.
236

Yu Guo's avatar
Yu Guo committed
237
238
239
240
        rows: list[int] = []
        cols: list[int] = []
        vals: list[float] = []

241
242
243
        # Get vocabulary size from logits
        vocab_size = logits.shape[-1]

244
245
246
        for i, logit_bias in enumerate(sampling_metadata.logit_bias):
            if logit_bias:
                for token_id, bias in logit_bias.items():
247
248
249
250
251
252
                    # Check token_id bounds to ensure within vocabulary
                    if token_id < 0 or token_id >= vocab_size:
                        raise ValueError(
                            f"token_id {token_id} in logit_bias contains "
                            f"out-of-vocab token id. Vocabulary size: "
                            f"{vocab_size}")
Yu Guo's avatar
Yu Guo committed
253
254
255
256
257
258
259
260
261
262
                    rows.append(i)
                    cols.append(token_id)
                    vals.append(bias)

        if rows:
            indices = async_tensor_h2d([rows, cols], torch.int64,
                                       logits.device, self.pin_memory)
            values = async_tensor_h2d(vals, torch.float, logits.device,
                                      self.pin_memory)
            logits.index_put_(tuple(indices), values=values, accumulate=True)
263
        return logits
264
265
266
267
268
269
270
271
272
273

    def apply_allowed_token_ids(
        self,
        logits: torch.Tensor,
        sampling_metadata: SamplingMetadata,
    ) -> torch.Tensor:
        if sampling_metadata.allowed_token_ids_mask is not None:
            logits.masked_fill_(sampling_metadata.allowed_token_ids_mask,
                                float("-inf"))
        return logits
274
275
276
277
278
279
280
281
282
283
284
285
286

    def apply_bad_words(
        self,
        logits: torch.Tensor,
        sampling_metadata: SamplingMetadata,
    ) -> torch.Tensor:
        if sampling_metadata.bad_words_token_ids:
            apply_bad_words(
                logits,
                sampling_metadata.bad_words_token_ids,
                sampling_metadata.output_token_ids,
            )
        return logits