main
py 202 lines 7.12 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 from __future__ import annotations
4
5 from collections.abc import Sequence
6 from dataclasses import dataclass
7 from typing import NamedTuple
8
9 import numpy as np
10 import torch
11
12 from vllm.triton_utils import tl, triton
13 from vllm.v1.outputs import LogprobsTensors, SamplingMaskLists
14
15
16 @dataclass
17 class SamplerOutput:
18 sampled_token_ids: torch.Tensor
19 logprobs_tensors: LogprobsTensors | None
20 num_nans: torch.Tensor | None
21 num_sampled: torch.Tensor
22 num_rejected: torch.Tensor
23 sampling_mask_tensors: SamplingMaskTensors | None = None
24
25
26 @triton.jit
27 def _compact_sampling_mask_kernel(
28 logits_ptr,
29 logits_row_stride,
30 logits_col_stride,
31 cu_num_logits_ptr,
32 num_sampled_tokens_ptr,
33 token_ids_ptr,
34 token_ids_row_stride,
35 packed_mask_ptr,
36 packed_mask_row_stride,
37 counts_ptr,
38 vocab_size,
39 max_num_kept,
40 ROWS_PER_REQUEST: tl.constexpr,
41 BLOCK_SIZE: tl.constexpr,
42 ):
43 """Per row: first ``max_num_kept`` finite-logit ids, the full count, the bitmask."""
44 output_row = tl.program_id(0)
45 req_idx = output_row // ROWS_PER_REQUEST
46 slot_idx = output_row % ROWS_PER_REQUEST
47 if slot_idx >= tl.load(num_sampled_tokens_ptr + req_idx):
48 tl.store(counts_ptr + output_row, 0)
49 return
50 source_row = tl.load(cu_num_logits_ptr + req_idx) + slot_idx
51
52 count = tl.zeros((), dtype=tl.int32)
53
54 for start_idx in range(0, vocab_size, BLOCK_SIZE):
55 offsets = start_idx + tl.arange(0, BLOCK_SIZE)
56 logits = tl.load(
57 logits_ptr + source_row * logits_row_stride + offsets * logits_col_stride,
58 mask=offsets < vocab_size,
59 other=-float("inf"),
60 )
61 keep = (logits > -float("inf")) & (logits < float("inf"))
62 keep_i32 = keep.to(tl.int32)
63 pos = count + tl.cumsum(keep_i32, axis=0) - keep_i32
64 tl.store(
65 token_ids_ptr + output_row * token_ids_row_stride + pos,
66 offsets.to(tl.int32),
67 mask=keep & (pos < max_num_kept),
68 )
69 count += tl.sum(keep_i32)
70
71 bits = tl.reshape(keep_i32, (BLOCK_SIZE // 8, 8)) << tl.arange(0, 8)[None, :]
72 byte_offsets = start_idx // 8 + tl.arange(0, BLOCK_SIZE // 8)
73 tl.store(
74 packed_mask_ptr + output_row * packed_mask_row_stride + byte_offsets,
75 tl.sum(bits, axis=1).to(tl.uint8),
76 mask=byte_offsets < tl.cdiv(vocab_size, 8),
77 )
78
79 tl.store(counts_ptr + output_row, count)
80
81
82 # Bounds the [num_reqs, width] int32 buffer; wider rows use the bitmask instead.
83 MAX_COMPACT_SUPPORT = 2048
84
85
86 class SamplingMaskTensors(NamedTuple):
87 """Device-side masks pending async D2H: compact ids, plus the bitmask as
88 the exact fallback for rows wider than ``max_num_kept``."""
89
90 # [num_requests * rows_per_request, max_num_kept]
91 token_ids: torch.Tensor
92 # [num_requests * rows_per_request, ceil(vocab_size / 8)]
93 packed_mask: torch.Tensor
94 # [num_requests * rows_per_request]
95 counts: torch.Tensor
96 vocab_size: int
97 # Fixed request-major output slots. Ordinary sampling has one slot.
98 rows_per_request: int = 1
99
100 @classmethod
101 def from_logits(
102 cls,
103 logits: torch.Tensor,
104 cu_num_logits: torch.Tensor,
105 num_sampled_tokens: torch.Tensor,
106 max_num_kept: int,
107 rows_per_request: int = 1,
108 ) -> SamplingMaskTensors:
109 """Capture committed target supports in fixed request-major slots."""
110 num_reqs = num_sampled_tokens.shape[0]
111 vocab_size = logits.shape[1]
112 max_num_kept = min(max_num_kept, vocab_size, MAX_COMPACT_SUPPORT)
113 num_output_rows = num_reqs * rows_per_request
114 device = logits.device
115
116 token_ids = torch.empty(
117 (num_output_rows, max_num_kept), dtype=torch.int32, device=device
118 )
119 packed_mask = torch.empty(
120 (num_output_rows, (vocab_size + 7) // 8),
121 dtype=torch.uint8,
122 device=device,
123 )
124 counts = torch.empty(num_output_rows, dtype=torch.int32, device=device)
125 _compact_sampling_mask_kernel[(num_output_rows,)](
126 logits,
127 logits.stride(0),
128 logits.stride(1),
129 cu_num_logits,
130 num_sampled_tokens,
131 token_ids,
132 token_ids.stride(0),
133 packed_mask,
134 packed_mask.stride(0),
135 counts,
136 vocab_size,
137 max_num_kept,
138 ROWS_PER_REQUEST=rows_per_request,
139 BLOCK_SIZE=8192,
140 )
141 return cls(token_ids, packed_mask, counts, vocab_size, rows_per_request)
142
143 @classmethod
144 def cat(cls, chunks: Sequence[SamplingMaskTensors]) -> SamplingMaskTensors:
145 """Join request-contiguous chunks that share one output layout."""
146 first = chunks[0]
147 assert len({(c.vocab_size, c.rows_per_request) for c in chunks}) == 1
148 return cls(
149 torch.cat([chunk.token_ids for chunk in chunks]),
150 torch.cat([chunk.packed_mask for chunk in chunks]),
151 torch.cat([chunk.counts for chunk in chunks]),
152 first.vocab_size,
153 first.rows_per_request,
154 )
155
156 def to_cpu_nonblocking(self) -> SamplingMaskTensors:
157 if self.token_ids.device.type == "cpu":
158 return self
159 return SamplingMaskTensors(
160 self.token_ids.to("cpu", non_blocking=True),
161 self.packed_mask.to("cpu", non_blocking=True),
162 self.counts.to("cpu", non_blocking=True),
163 self.vocab_size,
164 self.rows_per_request,
165 )
166
167 def tolists(
168 self, num_sampled_tokens: np.ndarray | None = None
169 ) -> SamplingMaskLists:
170 """Convert fixed output rows to the scheduler's CSR representation."""
171 counts = self.counts.cpu().numpy()
172 token_ids = self.token_ids.cpu().numpy()
173 packed_mask = self.packed_mask.cpu().numpy()
174 width = token_ids.shape[1]
175
176 cu_num_generated_tokens = None
177 if self.rows_per_request == 1:
178 sampled_rows = np.arange(len(counts))
179 else:
180 assert num_sampled_tokens is not None
181 active_slots = (
182 np.arange(self.rows_per_request)[None, :] < num_sampled_tokens[:, None]
183 )
184 sampled_rows = np.flatnonzero(active_slots)
185 cu_num_generated_tokens = np.cumsum(
186 np.concatenate(([0], num_sampled_tokens))
187 ).tolist()
188
189 def support(row: int) -> np.ndarray:
190 if counts[row] <= width:
191 return token_ids[row, : counts[row]]
192 # Wider than the compact row (ties or a huge top_k): use the bitmask.
193 bits = np.unpackbits(
194 packed_mask[row], count=self.vocab_size, bitorder="little"
195 )
196 return np.flatnonzero(bits).astype(np.int32, copy=False)
197
198 supports = [support(row) for row in sampled_rows]
199 offsets = np.zeros(len(supports) + 1, dtype=np.int64)
200 np.cumsum([len(s) for s in supports], out=offsets[1:])
201 flat = np.concatenate([np.empty(0, dtype=np.int32), *supports])
202 return SamplingMaskLists(flat, offsets, cu_num_generated_tokens)