main
py 424 lines 14.7 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 from typing import TYPE_CHECKING
4
5 import numpy as np
6 import torch
7
8 from vllm.sampling_params import SamplingParams
9 from vllm.triton_utils import tl, triton
10 from vllm.utils.torch_utils import async_tensor_h2d
11 from vllm.v1.worker.gpu.buffer_utils import UvaBackedTensor
12 from vllm.v1.worker.gpu.sample.logits_processor import LogitsContext
13 from vllm.v1.worker.gpu.states import RequestState
14
15 if TYPE_CHECKING:
16 from vllm.config.reasoning import ReasoningConfig
17
18 _INT32_MAX = np.iinfo(np.int32).max
19 _COLD_SCAN_BLOCK = 1024
20
21
22 class ThinkingBudgetState:
23 """Model Runner V2 state for per-request thinking token budgets."""
24
25 def __init__(
26 self,
27 req_states: RequestState,
28 reasoning_config: "ReasoningConfig | None",
29 ):
30 self.req_states = req_states
31 self.max_num_reqs = req_states.max_num_reqs
32 self.device = req_states.device
33
34 start_ids = (
35 []
36 if reasoning_config is None
37 else reasoning_config.reasoning_start_token_ids or []
38 )
39 end_ids = (
40 []
41 if reasoning_config is None
42 else reasoning_config.reasoning_end_token_ids or []
43 )
44 natural_end_ids = (
45 []
46 if reasoning_config is None
47 else reasoning_config.natural_reasoning_end_token_ids or []
48 )
49 self.enabled = bool(start_ids and end_ids and natural_end_ids)
50 if not self.enabled:
51 return
52
53 self.thinking_token_budget = UvaBackedTensor(
54 self.max_num_reqs, dtype=torch.int32
55 )
56 self.thinking_token_budget.np.fill(-1)
57 self.thinking_token_budget.copy_to_uva()
58 self.use_thinking_budget = np.zeros(self.max_num_reqs, dtype=bool)
59
60 self.cached_last_start = torch.full(
61 (self.max_num_reqs,), -1, dtype=torch.int32, device=self.device
62 )
63 self.cached_last_end = torch.full(
64 (self.max_num_reqs,), -1, dtype=torch.int32, device=self.device
65 )
66 self.cached_scan_pos = torch.zeros(
67 self.max_num_reqs, dtype=torch.int32, device=self.device
68 )
69 self._reset_reqs: list[int] = []
70 self._budget_dirty = False
71
72 self.reasoning_start_token_ids = torch.tensor(
73 start_ids, dtype=torch.int32, device=self.device
74 )
75 self.natural_reasoning_end_token_ids = torch.tensor(
76 natural_end_ids, dtype=torch.int32, device=self.device
77 )
78 self.reasoning_end_token_ids = torch.tensor(
79 end_ids, dtype=torch.int32, device=self.device
80 )
81
82 def add_request(self, req_idx: int, sampling_params: SamplingParams) -> bool:
83 if not self.enabled:
84 return False
85 budget = sampling_params.thinking_token_budget
86 use_thinking_budget = budget is not None
87 self.use_thinking_budget[req_idx] = use_thinking_budget
88 if budget is None:
89 budget = -1
90 else:
91 budget = min(budget, _INT32_MAX)
92 self._reset_reqs.append(req_idx)
93 if self.thinking_token_budget.np[req_idx] != budget:
94 self.thinking_token_budget.np[req_idx] = budget
95 self._budget_dirty = True
96 return use_thinking_budget
97
98 def apply_staged_writes(self) -> None:
99 if not self.enabled:
100 return
101 if self._reset_reqs:
102 idx = async_tensor_h2d(
103 self._reset_reqs, dtype=torch.int64, device=self.device
104 )
105 self.cached_last_start.index_fill_(0, idx, -1)
106 self.cached_last_end.index_fill_(0, idx, -1)
107 self.cached_scan_pos.index_fill_(0, idx, 0)
108 self._reset_reqs.clear()
109 if self._budget_dirty:
110 self.thinking_token_budget.copy_to_uva()
111 self._budget_dirty = False
112
113 def apply(self, logits: torch.Tensor, ctx: LogitsContext) -> None:
114 idx_mapping_np = ctx.idx_mapping_np
115 if not self.enabled or not np.any(self.use_thinking_budget[idx_mapping_np]):
116 return
117
118 apply_thinking_budget(
119 logits,
120 ctx.idx_mapping,
121 ctx.expanded_idx_mapping,
122 self.thinking_token_budget.gpu,
123 self.req_states.all_token_ids.gpu,
124 self.req_states.total_len.gpu,
125 ctx.input_ids,
126 ctx.expanded_local_pos,
127 self.cached_last_start,
128 self.cached_last_end,
129 self.cached_scan_pos,
130 self.reasoning_start_token_ids,
131 self.natural_reasoning_end_token_ids,
132 self.reasoning_end_token_ids,
133 )
134
135
136 @triton.jit
137 def _load_effective_token(
138 all_token_ids_ptr,
139 all_token_ids_stride,
140 input_ids_ptr,
141 cur_req_first_pos,
142 req_state_idx,
143 total_len,
144 pos,
145 ):
146 if pos < total_len:
147 return tl.load(all_token_ids_ptr + req_state_idx * all_token_ids_stride + pos)
148 # In decode/spec-decode, input_ids at local position 0 is the already
149 # committed last sampled token. Effective draft-prefix positions start at
150 # local position 1.
151 input_pos = cur_req_first_pos + pos - total_len + 1
152 return tl.load(input_ids_ptr + input_pos)
153
154
155 @triton.jit
156 def _update_committed_marker_cache_kernel(
157 req_ids_ptr,
158 thinking_token_budget_ptr,
159 all_token_ids_ptr,
160 all_token_ids_stride,
161 total_len_ptr,
162 cached_last_start_ptr,
163 cached_last_end_ptr,
164 cached_scan_pos_ptr,
165 reasoning_start_token_ids_ptr,
166 natural_reasoning_end_token_ids_ptr,
167 START_LEN: tl.constexpr,
168 NATURAL_END_LEN: tl.constexpr,
169 MAX_LEN: tl.constexpr,
170 BLOCK: tl.constexpr,
171 ):
172 req_state_idx = tl.load(req_ids_ptr + tl.program_id(0))
173 budget = tl.load(thinking_token_budget_ptr + req_state_idx)
174 if budget < 0:
175 return
176
177 total_len = tl.load(total_len_ptr + req_state_idx)
178 scan_pos = tl.load(cached_scan_pos_ptr + req_state_idx)
179 last_start = tl.load(cached_last_start_ptr + req_state_idx)
180 last_end = tl.load(cached_last_end_ptr + req_state_idx)
181
182 if scan_pos > total_len:
183 scan_pos = 0
184 last_start = -1
185 last_end = -1
186
187 if scan_pos == 0 and last_start < 0 and last_end < 0:
188 # Cold scan: walk backward in vectorized blocks, stopping at the first
189 # block with a marker; only the relative order of the two positions
190 # found matters below.
191 block_hi = total_len
192 while block_hi > 0 and last_start < 0 and last_end < 0:
193 block_lo = block_hi - BLOCK
194 if block_lo < 0:
195 block_lo = 0
196 offs = block_lo + tl.arange(0, BLOCK)
197
198 start_match = (offs < block_hi) & (offs + START_LEN <= total_len)
199 for j in tl.static_range(0, START_LEN):
200 expected = tl.load(reasoning_start_token_ids_ptr + j)
201 actual = tl.load(
202 all_token_ids_ptr + req_state_idx * all_token_ids_stride + offs + j,
203 mask=offs + j < total_len,
204 other=-1,
205 )
206 start_match = start_match & (actual == expected)
207
208 end_match = (offs < block_hi) & (offs + NATURAL_END_LEN <= total_len)
209 for j in tl.static_range(0, NATURAL_END_LEN):
210 expected = tl.load(natural_reasoning_end_token_ids_ptr + j)
211 actual = tl.load(
212 all_token_ids_ptr + req_state_idx * all_token_ids_stride + offs + j,
213 mask=offs + j < total_len,
214 other=-1,
215 )
216 end_match = end_match & (actual == expected)
217
218 last_start = tl.max(tl.where(start_match, offs, -1), axis=0)
219 last_end = tl.max(tl.where(end_match, offs, -1), axis=0)
220 block_hi = block_lo
221 else:
222 for i in tl.range(scan_pos, total_len):
223 if i + START_LEN <= total_len:
224 start_match = True
225 for j in tl.static_range(0, START_LEN):
226 expected = tl.load(reasoning_start_token_ids_ptr + j)
227 actual = tl.load(
228 all_token_ids_ptr + req_state_idx * all_token_ids_stride + i + j
229 )
230 start_match = start_match & (actual == expected)
231 if start_match:
232 last_start = i
233
234 if i + NATURAL_END_LEN <= total_len:
235 end_match = True
236 for j in tl.static_range(0, NATURAL_END_LEN):
237 expected = tl.load(natural_reasoning_end_token_ids_ptr + j)
238 actual = tl.load(
239 all_token_ids_ptr + req_state_idx * all_token_ids_stride + i + j
240 )
241 end_match = end_match & (actual == expected)
242 if end_match:
243 last_end = i
244
245 tl.store(cached_last_start_ptr + req_state_idx, last_start)
246 tl.store(cached_last_end_ptr + req_state_idx, last_end)
247 new_scan_pos = total_len - (MAX_LEN - 1)
248 if new_scan_pos < 0:
249 new_scan_pos = 0
250 tl.store(cached_scan_pos_ptr + req_state_idx, new_scan_pos)
251
252
253 @triton.jit
254 def _thinking_budget_kernel(
255 logits_ptr,
256 logits_stride,
257 expanded_idx_mapping_ptr,
258 thinking_token_budget_ptr,
259 all_token_ids_ptr,
260 all_token_ids_stride,
261 total_len_ptr,
262 input_ids_ptr,
263 expanded_local_pos_ptr,
264 cached_last_start_ptr,
265 cached_last_end_ptr,
266 reasoning_start_token_ids_ptr,
267 natural_reasoning_end_token_ids_ptr,
268 reasoning_end_token_ids_ptr,
269 START_LEN: tl.constexpr,
270 NATURAL_END_LEN: tl.constexpr,
271 END_LEN: tl.constexpr,
272 ):
273 token_idx = tl.program_id(0).to(tl.int64)
274 req_state_idx = tl.load(expanded_idx_mapping_ptr + token_idx)
275 budget = tl.load(thinking_token_budget_ptr + req_state_idx)
276 if budget < 0:
277 return
278
279 local_pos = tl.load(expanded_local_pos_ptr + token_idx)
280 cur_req_first_pos = token_idx - local_pos
281 total_len = tl.load(total_len_ptr + req_state_idx)
282 effective_len = total_len + local_pos
283
284 last_start = tl.load(cached_last_start_ptr + req_state_idx)
285 last_end = tl.load(cached_last_end_ptr + req_state_idx)
286
287 start_lo = total_len - START_LEN + 1
288 if start_lo < 0:
289 start_lo = 0
290 for i in tl.range(start_lo, effective_len - START_LEN + 1):
291 start_match = True
292 for j in tl.static_range(0, START_LEN):
293 expected = tl.load(reasoning_start_token_ids_ptr + j)
294 actual = _load_effective_token(
295 all_token_ids_ptr,
296 all_token_ids_stride,
297 input_ids_ptr,
298 cur_req_first_pos,
299 req_state_idx,
300 total_len,
301 i + j,
302 )
303 start_match = start_match & (actual == expected)
304 if start_match:
305 last_start = i
306
307 end_lo = total_len - NATURAL_END_LEN + 1
308 if end_lo < 0:
309 end_lo = 0
310 for i in tl.range(end_lo, effective_len - NATURAL_END_LEN + 1):
311 end_match = True
312 for j in tl.static_range(0, NATURAL_END_LEN):
313 expected = tl.load(natural_reasoning_end_token_ids_ptr + j)
314 actual = _load_effective_token(
315 all_token_ids_ptr,
316 all_token_ids_stride,
317 input_ids_ptr,
318 cur_req_first_pos,
319 req_state_idx,
320 total_len,
321 i + j,
322 )
323 end_match = end_match & (actual == expected)
324 if end_match:
325 last_end = i
326
327 if last_start < 0 or last_start <= last_end:
328 return
329
330 reasoning_start = last_start + START_LEN
331 # If the request resumes from a prompt that already contains generated
332 # reasoning content, count it against the remaining budget.
333 num_reasoning_tokens = effective_len - reasoning_start
334 if num_reasoning_tokens < budget:
335 return
336
337 # If the tail already ends with a prefix of the forced end sequence
338 # (even from a resumed prompt), continue from the next marker token.
339 end_prefix_len = 0
340 max_prefix_len = END_LEN - 1
341 if effective_len < max_prefix_len:
342 max_prefix_len = effective_len
343
344 for prefix_len in tl.static_range(1, END_LEN):
345 if prefix_len <= max_prefix_len:
346 prefix_match = True
347 suffix_start = effective_len - prefix_len
348 for j in tl.static_range(0, END_LEN):
349 if j < prefix_len:
350 expected = tl.load(reasoning_end_token_ids_ptr + j)
351 actual = _load_effective_token(
352 all_token_ids_ptr,
353 all_token_ids_stride,
354 input_ids_ptr,
355 cur_req_first_pos,
356 req_state_idx,
357 total_len,
358 suffix_start + j,
359 )
360 prefix_match = prefix_match & (actual == expected)
361 if prefix_match:
362 end_prefix_len = prefix_len
363
364 force_token_id = tl.load(reasoning_end_token_ids_ptr + end_prefix_len)
365 tl.store(logits_ptr + token_idx * logits_stride + force_token_id, 1.0e9)
366
367
368 def apply_thinking_budget(
369 logits: torch.Tensor,
370 req_ids: torch.Tensor,
371 expanded_idx_mapping: torch.Tensor,
372 thinking_token_budget: torch.Tensor,
373 all_token_ids: torch.Tensor,
374 total_len: torch.Tensor,
375 input_ids: torch.Tensor,
376 expanded_local_pos: torch.Tensor,
377 cached_last_start: torch.Tensor,
378 cached_last_end: torch.Tensor,
379 cached_scan_pos: torch.Tensor,
380 reasoning_start_token_ids: torch.Tensor,
381 natural_reasoning_end_token_ids: torch.Tensor,
382 reasoning_end_token_ids: torch.Tensor,
383 ) -> None:
384 num_tokens = logits.shape[0]
385 start_len = reasoning_start_token_ids.shape[0]
386 natural_end_len = natural_reasoning_end_token_ids.shape[0]
387 end_len = reasoning_end_token_ids.shape[0]
388
389 _update_committed_marker_cache_kernel[(req_ids.shape[0],)](
390 req_ids,
391 thinking_token_budget,
392 all_token_ids,
393 all_token_ids.stride(0),
394 total_len,
395 cached_last_start,
396 cached_last_end,
397 cached_scan_pos,
398 reasoning_start_token_ids,
399 natural_reasoning_end_token_ids,
400 START_LEN=start_len,
401 NATURAL_END_LEN=natural_end_len,
402 MAX_LEN=max(start_len, natural_end_len),
403 BLOCK=_COLD_SCAN_BLOCK,
404 )
405
406 _thinking_budget_kernel[(num_tokens,)](
407 logits,
408 logits.stride(0),
409 expanded_idx_mapping,
410 thinking_token_budget,
411 all_token_ids,
412 all_token_ids.stride(0),
413 total_len,
414 input_ids,
415 expanded_local_pos,
416 cached_last_start,
417 cached_last_end,
418 reasoning_start_token_ids,
419 natural_reasoning_end_token_ids,
420 reasoning_end_token_ids,
421 START_LEN=start_len,
422 NATURAL_END_LEN=natural_end_len,
423 END_LEN=end_len,
424 )