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 5, 12:15 UTC
[Feat][Core] safely abort requests when FSM fails to advance (#38663)
Signed-off-by: walterbm <walter.beller.morales@gmail.com>
Walter Beller-Morales committed
Apr 6, 2026 at 11:00 UTC
e69a265135ef48312d78130f64b7bfce4cd81a37
3 files changed
+164
-13
tests/v1/core/test_async_scheduler.py
+66
-1
@@ -1,10 +1,12 @@
1
# SPDX-License-Identifier: Apache-2.0
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
from collections import deque
4
+from unittest.mock import Mock
5
6
import pytest
7
7
-from vllm.v1.core.sched.output import SchedulerOutput
8
+from vllm.v1.core.sched.async_scheduler import AsyncScheduler
9
+from vllm.v1.core.sched.output import CachedRequestData, SchedulerOutput
10
from vllm.v1.outputs import ModelRunnerOutput
11
from vllm.v1.request import RequestStatus
12
from vllm.v1.utils import ConstantList
@@ -247,3 +249,66 @@ def test_prefix_caching_for_multi_turn():
249
# requests.
250
for req in next_turn_requests:
251
assert req.num_cached_tokens == req.num_prompt_tokens // BLOCK_SIZE * BLOCK_SIZE
252
+
253
+
254
+def test_abort_request_when_structured_output_fsm_cannot_advance():
255
+ scheduler = object.__new__(AsyncScheduler)
256
+ request = create_requests(num_requests=1, num_tokens=1)[0]
257
+ request.structured_output_request = Mock()
258
+ request.structured_output_request.grammar = Mock()
259
+ request.structured_output_request.grammar.accept_tokens.return_value = False
260
+ request.status = RequestStatus.RUNNING
261
+ request.num_computed_tokens = request.num_tokens
262
+ request.num_output_placeholders = 1
263
+
264
+ scheduler.perf_metrics = None
265
+ scheduler.connector = None
266
+ scheduler.structured_output_manager = Mock()
267
+ scheduler.structured_output_manager.should_advance.return_value = True
268
+ scheduler.requests = {request.request_id: request}
269
+ scheduler.running = [request]
270
+ scheduler.waiting = Mock()
271
+ scheduler.kv_cache_manager = Mock()
272
+ scheduler.kv_cache_manager.take_events.return_value = None
273
+ scheduler.kv_event_publisher = Mock()
274
+ scheduler.finished_req_ids = set()
275
+ scheduler.finished_req_ids_dict = None
276
+ scheduler.vllm_config = Mock()
277
+ scheduler.vllm_config.model_config.enable_return_routed_experts = False
278
+ scheduler.recompute_kv_load_failures = False
279
+ scheduler.make_stats = Mock(return_value=None)
280
+ scheduler.max_model_len = 128
281
+
282
+ def free_request(req, delay_free_blocks=False):
283
+ scheduler.finished_req_ids.add(req.request_id)
284
+ scheduler.requests.pop(req.request_id, None)
285
+ return None
286
+
287
+ scheduler._free_request = Mock(side_effect=free_request)
288
+
289
+ output = SchedulerOutput(
290
+ scheduled_new_reqs=[],
291
+ scheduled_cached_reqs=CachedRequestData.make_empty(),
292
+ num_scheduled_tokens={request.request_id: 1},
293
+ total_num_scheduled_tokens=1,
294
+ scheduled_encoder_inputs={},
295
+ scheduled_spec_decode_tokens={},
296
+ num_common_prefix_blocks=[],
297
+ finished_req_ids=set(),
298
+ free_encoder_mm_hashes=[],
299
+ )
300
+ model_runner_output = ModelRunnerOutput(
301
+ req_ids=[request.request_id],
302
+ req_id_to_index={request.request_id: 0},
303
+ sampled_token_ids=[[123]],
304
+ logprobs=None,
305
+ prompt_logprobs_dict={},
306
+ pooler_output=[],
307
+ )
308
+
309
+ scheduler.update_from_output(output, model_runner_output)
310
+
311
+ assert request.resumable is False
312
+ assert request.status == RequestStatus.FINISHED_ERROR
313
+ assert request.request_id not in scheduler.requests
314
+ assert not scheduler.running
tests/v1/core/test_scheduler.py
+81
@@ -26,6 +26,7 @@ from vllm.v1.core.encoder_cache_manager import EncoderCacheManager
26
from vllm.v1.core.kv_cache_utils import get_request_block_hasher, init_none_hash
27
from vllm.v1.core.sched.output import CachedRequestData, SchedulerOutput
28
from vllm.v1.core.sched.scheduler import Scheduler
29
+from vllm.v1.engine import FinishReason
30
from vllm.v1.kv_cache_interface import (
31
FullAttentionSpec,
32
KVCacheConfig,
@@ -2463,6 +2464,86 @@ def test_schedule_skip_tokenizer_init_structured_output_request():
2464
assert len(scheduler.skipped_waiting) == 1
2465
2466
2467
+def test_abort_request_when_structured_output_fsm_cannot_advance():
2468
+ scheduler = object.__new__(Scheduler)
2469
+ sampling_params = SamplingParams(ignore_eos=True, max_tokens=4)
2470
+ sampling_params.update_from_generation_config({}, EOS_TOKEN_ID)
2471
+
2472
+ request = Request(
2473
+ request_id="0",
2474
+ prompt_token_ids=[0, 1],
2475
+ mm_features=None,
2476
+ sampling_params=sampling_params,
2477
+ pooling_params=None,
2478
+ )
2479
+ request.structured_output_request = Mock()
2480
+ request.structured_output_request.grammar = Mock()
2481
+ request.structured_output_request.grammar.accept_tokens.return_value = False
2482
+ request.status = RequestStatus.RUNNING
2483
+ request.num_computed_tokens = request.num_tokens
2484
+
2485
+ scheduler.perf_metrics = None
2486
+ scheduler.connector = None
2487
+ scheduler.structured_output_manager = Mock()
2488
+ scheduler.structured_output_manager.should_advance.return_value = True
2489
+ scheduler.requests = {request.request_id: request}
2490
+ scheduler.running = [request]
2491
+ scheduler.waiting = Mock()
2492
+ scheduler.kv_cache_manager = Mock()
2493
+ scheduler.kv_cache_manager.take_events.return_value = None
2494
+ scheduler.kv_event_publisher = Mock()
2495
+ scheduler.finished_req_ids = set()
2496
+ scheduler.finished_req_ids_dict = None
2497
+ scheduler.vllm_config = Mock()
2498
+ scheduler.vllm_config.model_config.enable_return_routed_experts = False
2499
+ scheduler.recompute_kv_load_failures = False
2500
+ scheduler.make_stats = Mock(return_value=None)
2501
+ scheduler.max_model_len = 128
2502
+
2503
+ def free_request(req: Request, delay_free_blocks: bool = False):
2504
+ scheduler.finished_req_ids.add(req.request_id)
2505
+ scheduler.requests.pop(req.request_id, None)
2506
+ return None
2507
+
2508
+ scheduler._free_request = Mock(side_effect=free_request)
2509
+
2510
+ output = SchedulerOutput(
2511
+ scheduled_new_reqs=[],
2512
+ scheduled_cached_reqs=CachedRequestData.make_empty(),
2513
+ num_scheduled_tokens={request.request_id: 1},
2514
+ total_num_scheduled_tokens=1,
2515
+ scheduled_encoder_inputs={},
2516
+ scheduled_spec_decode_tokens={},
2517
+ num_common_prefix_blocks=[],
2518
+ finished_req_ids=set(),
2519
+ free_encoder_mm_hashes=[],
2520
+ )
2521
+
2522
+ model_runner_output = ModelRunnerOutput(
2523
+ req_ids=[request.request_id],
2524
+ req_id_to_index={request.request_id: 0},
2525
+ sampled_token_ids=[[123]],
2526
+ logprobs=None,
2527
+ prompt_logprobs_dict={},
2528
+ pooler_output=[],
2529
+ )
2530
+ engine_core_outputs = scheduler.update_from_output(output, model_runner_output)
2531
+
2532
+ request.structured_output_request.grammar.accept_tokens.assert_called_once_with(
2533
+ request.request_id, [123]
2534
+ )
2535
+ assert request.resumable is False
2536
+ assert request.status == RequestStatus.FINISHED_ERROR
2537
+ assert request.request_id not in scheduler.requests
2538
+ assert not scheduler.running
2539
+ scheduler._free_request.assert_called_once_with(request)
2540
+ assert len(engine_core_outputs[0].outputs) == 1
2541
+ engine_core_output = engine_core_outputs[0].outputs[0]
2542
+ assert engine_core_output.request_id == request.request_id
2543
+ assert engine_core_output.new_token_ids == [123]
2544
+ assert engine_core_output.finish_reason == FinishReason.ERROR
2545
+
2546
+
2547
@pytest.mark.parametrize(
2548
"use_ec_connector, ec_role", [(False, None), (True, "ec_consumer")]
2549
)
vllm/v1/core/sched/scheduler.py
+17
-12
@@ -1406,6 +1406,23 @@ class Scheduler(SchedulerInterface):
1406
request.status = RequestStatus.FINISHED_STOPPED
1407
stopped = True
1408
1409
+ if new_token_ids and self.structured_output_manager.should_advance(request):
1410
+ struct_output_request = request.structured_output_request
1411
+ assert struct_output_request is not None
1412
+ assert struct_output_request.grammar is not None
1413
+ if not struct_output_request.grammar.accept_tokens( # type: ignore[union-attr]
1414
+ req_id, new_token_ids
1415
+ ):
1416
+ logger.error(
1417
+ "Unexpected: grammar rejected tokens %s for request %s. "
1418
+ "Terminating request.",
1419
+ new_token_ids,
1420
+ req_id,
1421
+ )
1422
+ request.status = RequestStatus.FINISHED_ERROR
1423
+ request.resumable = False
1424
+ stopped = True
1425
+
1426
routed_experts = None
1427
finish_reason = None
1428
if stopped:
@@ -1431,18 +1448,6 @@ class Scheduler(SchedulerInterface):
1448
):
1449
new_logprobs = logprobs.slice_request(req_index, len(new_token_ids))
1450
1434
- if new_token_ids and self.structured_output_manager.should_advance(request):
1435
- struct_output_request = request.structured_output_request
1436
- assert struct_output_request is not None
1437
- assert struct_output_request.grammar is not None
1438
- ok = struct_output_request.grammar.accept_tokens(req_id, new_token_ids)
1439
- if not ok:
1440
- logger.warning(
1441
- "Unexpected: grammar rejected tokens %s for request %s.",
1442
- new_token_ids,
1443
- req_id,
1444
- )
1445
-
1451
if num_nans_in_logits is not None and req_id in num_nans_in_logits:
1452
request.num_nans_in_logits = num_nans_in_logits[req_id]
1453