A high-throughput and memory-efficient inference and serving engine for LLMs
Mirrored from https://github.com/tduncan-lum/Nvidia-Optimization-Stack---VLLM.git
Synced Nov 14, 05:30 UTC
[Perf] Parallelize fill_bitmask to accelerate high-throughput guided decoding (#21862)
Signed-off-by: Benjamin Chislett <benjamin.chislett@centml.ai>
Benjamin Chislett committed
Aug 5, 2025 at 22:57 UTC
7e6544c7978364fcb8178f4ab8b1325e45880aa9
3 files changed
+105
-42
vllm/v1/structured_output/__init__.py
+92
-39
@@ -3,7 +3,7 @@
3
from __future__ import annotations
4
5
import multiprocessing
6
-from concurrent.futures import ThreadPoolExecutor
6
+from concurrent.futures import Future, ThreadPoolExecutor
7
from typing import TYPE_CHECKING, Optional
8
9
from vllm.config import VllmConfig
@@ -40,6 +40,17 @@ class StructuredOutputManager:
40
self._grammar_bitmask: Optional[torch.Tensor] = None
41
self._full_mask = torch.tensor(-1, dtype=torch.int32)
42
43
+ max_batch_size = self.vllm_config.scheduler_config.max_num_seqs
44
+ self.fill_bitmask_parallel_threshold = 128
45
+ if self.fill_bitmask_parallel_threshold < max_batch_size:
46
+ self.fill_bitmask_parallel_batch_size = 16
47
+ # Use:
48
+ # - at least 1 CPU
49
+ # - at most half the number of CPUs or 8, whichever is less
50
+ max_workers = max(1, min(multiprocessing.cpu_count() // 2, 8))
51
+ self.executor_for_fillmask = ThreadPoolExecutor(
52
+ max_workers=max_workers)
53
+
54
if not self.vllm_config.model_config.skip_tokenizer_init:
55
# The default max_workers if not specified is the number of
56
# CPUs * 5, which is way too high since these tasks are CPU-bound,
@@ -120,6 +131,26 @@ class StructuredOutputManager:
131
assert self.backend is not None
132
return self.backend.compile_grammar(request_type, grammar_spec)
133
134
+ def _fill_bitmasks(
135
+ self,
136
+ batch: list[tuple[StructuredOutputGrammar, int, bool]],
137
+ ) -> None:
138
+ assert self._grammar_bitmask is not None
139
+ for grammar, index, apply_bitmask in batch:
140
+ if apply_bitmask and not grammar.is_terminated():
141
+ grammar.fill_bitmask(self._grammar_bitmask, index)
142
+ else:
143
+ # Note that for thinking support, we will need to
144
+ # reset the relevant part of the bitmask for consequent
145
+ # requests here.
146
+ self._grammar_bitmask[index].fill_(self._full_mask)
147
+
148
+ def _async_submit_fill_bitmask(
149
+ self,
150
+ batch: list[tuple[StructuredOutputGrammar, int, bool]],
151
+ ) -> Future:
152
+ return self.executor_for_fillmask.submit(self._fill_bitmasks, batch)
153
+
154
def grammar_bitmask(
155
self,
156
requests: dict[str, Request],
@@ -146,7 +177,6 @@ class StructuredOutputManager:
177
self.backend.allocate_token_bitmask(
178
max_batch_size * (1 + max_num_spec_tokens))
179
149
- bitmask_tensor = self._grammar_bitmask
180
# Generate a batched bitmask for all structured output requests.
181
# When speculative decoding is enabled, we need to include multiple
182
# masks for each request, one for each possible bonus token position.
@@ -155,47 +185,61 @@ class StructuredOutputManager:
185
ordered_seq = sorted(structured_output_request_ids.items(),
186
key=lambda x: x[1])
187
158
- # Note that for thinking support, we will need to
159
- # reset the relevant part of the bitmask for consequent
160
- # request here.
161
- bitmask_tensor[:(len(ordered_seq) * (1 + max_num_spec_tokens))].fill_(
162
- self._full_mask)
163
-
164
- # NOTE: This outer loop can likely be parallelized to improve
165
- # performance of bitmask generation for large batches.
166
- for req_id, _ in ordered_seq:
167
- request = requests[req_id]
168
- structured_output_request = request.structured_output_request
169
-
170
- if TYPE_CHECKING:
171
- assert structured_output_request is not None
172
- assert structured_output_request.grammar is not None
173
- apply_bitmask: bool = True
174
- if self.reasoner is not None:
175
- if structured_output_request.reasoning_ended is None:
176
- structured_output_request.reasoning_ended = \
177
- self.reasoner.is_reasoning_end(request.prompt_token_ids)
178
- apply_bitmask = structured_output_request.reasoning_ended
179
-
180
- state_advancements = 0
181
- req_tokens = scheduled_spec_decode_tokens.get(req_id, []) + [None]
182
- for i, token in enumerate(req_tokens):
183
- if apply_bitmask and not \
184
- structured_output_request.grammar.is_terminated():
185
- structured_output_request.grammar.fill_bitmask(
186
- bitmask_tensor, cumulative_index)
187
- if token is not None:
188
- # In order to generate the correct bitmask for each
189
- # position in the speculative sequence, we advance
190
- # the FSM state for each speculative token and rollback
191
- # to restore the previous state when we are finished.
188
+ # Optimized parallel filling of bitmasks for
189
+ # non-spec, large-batch-size cases
190
+ if len(ordered_seq) > self.fill_bitmask_parallel_threshold and \
191
+ max_num_spec_tokens == 0:
192
+ promises = []
193
+ batch = []
194
+ for req_id, _ in ordered_seq:
195
+ request = requests[req_id]
196
+ structured_output_request = request.structured_output_request
197
+ if TYPE_CHECKING:
198
+ assert structured_output_request is not None
199
+ assert structured_output_request.grammar is not None
200
+
201
+ apply_bitmask = self.should_fill_bitmask(request)
202
+ batch.append((structured_output_request.grammar,
203
+ cumulative_index, apply_bitmask))
204
+ if len(batch) == self.fill_bitmask_parallel_batch_size:
205
+ promises.append(self._async_submit_fill_bitmask(batch))
206
+ batch = []
207
+
208
+ cumulative_index += 1
209
+ if batch:
210
+ promises.append(self._async_submit_fill_bitmask(batch))
211
+
212
+ # Wait for all bitmask filling tasks to complete.
213
+ for promise in promises:
214
+ promise.result()
215
+ else:
216
+ # Fallback to serial filling of bitmasks for small-batch-size cases
217
+ for req_id, _ in ordered_seq:
218
+ request = requests[req_id]
219
+ structured_output_request = request.structured_output_request
220
+
221
+ if TYPE_CHECKING:
222
+ assert structured_output_request is not None
223
+ assert structured_output_request.grammar is not None
224
+ apply_bitmask = self.should_fill_bitmask(request)
225
+
226
+ state_advancements = 0
227
+ req_tokens = scheduled_spec_decode_tokens.get(req_id, [])
228
+ for i, token in enumerate(req_tokens + [None]):
229
+ self._fill_bitmasks([(structured_output_request.grammar,
230
+ cumulative_index, apply_bitmask)])
231
+
232
+ if apply_bitmask and token is not None and \
233
+ not structured_output_request.grammar.is_terminated():
234
assert structured_output_request.grammar.accept_tokens(
235
req_id, [token])
236
state_advancements += 1
195
- cumulative_index += 1
196
- if state_advancements > 0:
197
- structured_output_request.grammar.rollback(state_advancements)
237
+ cumulative_index += 1
238
+ if state_advancements > 0:
239
+ structured_output_request.grammar.rollback(
240
+ state_advancements)
241
242
+ bitmask_tensor = self._grammar_bitmask
243
if cumulative_index < bitmask_tensor.shape[0]:
244
bitmask_tensor = bitmask_tensor[:cumulative_index]
245
@@ -204,6 +248,15 @@ class StructuredOutputManager:
248
# and deserialization when sending this to the GPU workers.
249
return bitmask_tensor.numpy()
250
251
+ def should_fill_bitmask(self, request: Request) -> bool:
252
+ if self.reasoner is not None:
253
+ assert request.structured_output_request is not None
254
+ if request.structured_output_request.reasoning_ended is None:
255
+ request.structured_output_request.reasoning_ended = \
256
+ self.reasoner.is_reasoning_end(request.prompt_token_ids)
257
+ return request.structured_output_request.reasoning_ended
258
+ return True
259
+
260
def should_advance(self, request: Request) -> bool:
261
if not request.use_structured_output:
262
return False
vllm/v1/structured_output/backend_xgrammar.py
+6
-1
@@ -148,6 +148,7 @@ class XgrammarGrammar(StructuredOutputGrammar):
148
repr=False,
149
hash=False,
150
init=False)
151
+ _is_terminated: bool = field(default=False, repr=False, hash=False)
152
153
def accept_tokens(self, request_id: str, tokens: list[int]) -> bool:
154
"""Accepts a list of tokens and advances the FSM.
@@ -155,6 +156,8 @@ class XgrammarGrammar(StructuredOutputGrammar):
156
Returns True if the FSM was advanced successfully.
157
Returns False if the FSM failed to advance.
158
"""
159
+ if self._is_terminated:
160
+ return False
161
for token in tokens:
162
if not self.matcher.accept_token(token):
163
logger.error(
@@ -162,6 +165,7 @@ class XgrammarGrammar(StructuredOutputGrammar):
165
"for tokens %s. Please file an issue.", request_id, token)
166
return False
167
self.num_processed_tokens += 1
168
+ self._is_terminated = self.matcher.is_terminated()
169
return True
170
171
def validate_tokens(self, tokens: list[int]) -> list[int]:
@@ -184,12 +188,13 @@ class XgrammarGrammar(StructuredOutputGrammar):
188
def rollback(self, num_tokens: int) -> None:
189
self.matcher.rollback(num_tokens)
190
self.num_processed_tokens -= num_tokens
191
+ self._is_terminated = self.matcher.is_terminated()
192
193
def fill_bitmask(self, bitmask: torch.Tensor, idx: int) -> None:
194
self.matcher.fill_next_token_bitmask(bitmask, idx)
195
196
def is_terminated(self) -> bool:
192
- return self.matcher.is_terminated()
197
+ return self._is_terminated
198
199
def reset(self):
200
self.num_processed_tokens = 0
vllm/v1/worker/gpu_model_runner.py
+7
-2
@@ -1324,9 +1324,14 @@ class GPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
1324
cumulative_index += 1 + num_spec_tokens
1325
grammar_bitmask = sorted_bitmask
1326
1327
+ # If the grammar bitmask and the logits have the same shape
1328
+ # we don't need to pass indices to the kernel,
1329
+ # since the bitmask is already aligned with the logits.
1330
+ skip_out_indices = grammar_bitmask.shape[0] == logits.shape[0]
1331
+
1332
# Serialization of np.ndarray is much more efficient than a tensor,
1333
# so we receive it in that format.
1329
- grammar_bitmask = torch.from_numpy(grammar_bitmask)
1334
+ grammar_bitmask = torch.from_numpy(grammar_bitmask).contiguous()
1335
1336
# Force use of the torch.compile implementation from xgrammar to work
1337
# around issues with the Triton kernel in concurrent structured output
@@ -1334,7 +1339,7 @@ class GPUModelRunner(LoRAModelRunnerMixin, KVConnectorModelRunnerMixin):
1339
xgr_torch_compile.apply_token_bitmask_inplace_torch_compile(
1340
logits,
1341
grammar_bitmask.to(self.device, non_blocking=True),
1337
- indices=out_indices,
1342
+ indices=out_indices if not skip_out_indices else None,
1343
)
1344
1345
def sync_and_slice_intermediate_tensors(