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 15, 22:15 UTC
[Bugfix] Do not crash V0 engine on input errors (#13101)
Signed-off-by: Joe Runde <Joseph.Runde@ibm.com>
Joe Runde committed
Feb 26, 2025 at 04:07 UTC
3f808cc0448bc60a4412575885db6ba8f5ab4671
5 files changed
+172
-6
tests/mq_llm_engine/test_error_handling.py
+78
@@ -18,6 +18,7 @@ from vllm.engine.multiprocessing.engine import MQLLMEngine
18
from vllm.entrypoints.openai.api_server import build_async_engine_client
19
from vllm.entrypoints.openai.cli_args import make_arg_parser
20
from vllm.lora.request import LoRARequest
21
+from vllm.sequence import SequenceGroupMetadata
22
from vllm.usage.usage_lib import UsageContext
23
from vllm.utils import FlexibleArgumentParser
24
@@ -292,3 +293,80 @@ async def test_engine_process_death(tmp_socket):
293
await client.check_health()
294
295
client.close()
296
+
297
+
298
+def run_with_evil_input_processing(engine_args: AsyncEngineArgs,
299
+ ipc_path: str):
300
+ """Simulate an exception while preparing inputs for the model.
301
+ In the wild, this could be something like a multimodal input processor
302
+ failing on invalid image data."""
303
+
304
+ # Make engine.
305
+ engine = MQLLMEngine.from_engine_args(
306
+ engine_args=engine_args,
307
+ usage_context=UsageContext.UNKNOWN_CONTEXT,
308
+ ipc_path=ipc_path)
309
+
310
+ runner = engine.engine.model_executor.driver_worker.worker.model_runner
311
+
312
+ # Raise error in the model runner when adding a sequence group.
313
+ # See class ModelInputForGPUBuilder
314
+ def raiser(_, seq_group_metadata: SequenceGroupMetadata):
315
+ if seq_group_metadata.request_id.startswith("evil"):
316
+ raise RAISED_ERROR(RAISED_VALUE)
317
+
318
+ runner.builder.per_seq_group_compute_fns.append(raiser)
319
+
320
+ # Run engine.
321
+ engine.start()
322
+
323
+
324
+@pytest.mark.asyncio
325
+async def test_failed_inputs(tmp_socket):
326
+ with RemoteMQLLMEngine(engine_args=ENGINE_ARGS,
327
+ ipc_path=tmp_socket,
328
+ run_fn=run_with_evil_input_processing) as engine:
329
+
330
+ client = await engine.make_client()
331
+ assert client.is_running
332
+
333
+ # Engine should be healthy
334
+ await client.check_health()
335
+
336
+ async def run_failing_request():
337
+ async for _ in client.generate(
338
+ prompt="Hello my name is",
339
+ sampling_params=SamplingParams(max_tokens=10),
340
+ request_id="evil" + str(uuid.uuid4())):
341
+ pass
342
+
343
+ async def run_passing_request():
344
+ async for _ in client.generate(
345
+ prompt="Hello my name is",
346
+ sampling_params=SamplingParams(max_tokens=10),
347
+ request_id=str(uuid.uuid4())):
348
+ pass
349
+
350
+ passing_tasks = [
351
+ asyncio.create_task(run_passing_request()) for _ in range(10)
352
+ ]
353
+ failing_tasks = [
354
+ asyncio.create_task(run_failing_request()) for _ in range(10)
355
+ ]
356
+ await asyncio.gather(*failing_tasks, return_exceptions=True)
357
+ await asyncio.gather(*passing_tasks)
358
+
359
+ # All the bad inputs should have raised
360
+ for task in failing_tasks:
361
+ with pytest.raises(RAISED_ERROR):
362
+ task.result()
363
+
364
+ # But all good inputs should have still succeeded
365
+ for task in passing_tasks:
366
+ task.result()
367
+
368
+ # And the engine should remain healthy
369
+ assert not client.errored
370
+ await client.check_health()
371
+
372
+ client.close()
vllm/engine/llm_engine.py
+59
-3
@@ -60,6 +60,7 @@ from vllm.usage.usage_lib import (UsageContext, is_usage_stats_enabled,
60
from vllm.utils import (Counter, Device, deprecate_kwargs,
61
resolve_obj_by_qualname, weak_bind)
62
from vllm.version import __version__ as VLLM_VERSION
63
+from vllm.worker.model_runner_base import InputProcessingError
64
65
logger = init_logger(__name__)
66
_LOCAL_LOGGING_INTERVAL_SEC = 5
@@ -410,6 +411,10 @@ class LLMEngine:
411
412
self.seq_id_to_seq_group: Dict[str, SequenceGroupBase] = {}
413
414
+ # Flag to set when an input fails to process and the engine should run
415
+ # the next step without re-scheduling.
416
+ self._skip_scheduling_next_step = False
417
+
418
def _initialize_kv_caches(self) -> None:
419
"""Initialize the KV cache in the worker(s).
420
@@ -1334,7 +1339,11 @@ class LLMEngine:
1339
# Skip the scheduler if there are any remaining steps in the seq groups.
1340
# This ensures that the scheduler is only called again when the current
1341
# batch has completed.
1337
- if not self._has_remaining_steps(seq_group_metadata_list):
1342
+ # The scheduler is also skipped if a single request caused the last
1343
+ # engine step to fail, and the previous schedule needs to be rerun.
1344
+ if not self._has_remaining_steps(
1345
+ seq_group_metadata_list
1346
+ ) and not self._skip_scheduling_next_step:
1347
# Schedule iteration
1348
(seq_group_metadata_list, scheduler_outputs,
1349
allow_async_output_proc
@@ -1388,8 +1397,23 @@ class LLMEngine:
1397
execute_model_req.async_callback = self.async_callbacks[
1398
virtual_engine]
1399
1391
- outputs = self.model_executor.execute_model(
1392
- execute_model_req=execute_model_req)
1400
+ try:
1401
+ outputs = self.model_executor.execute_model(
1402
+ execute_model_req=execute_model_req)
1403
+ self._skip_scheduling_next_step = False
1404
+ except InputProcessingError as e:
1405
+ # The input for this request cannot be processed, so we must
1406
+ # abort it. If there are remaining requests in the batch that
1407
+ # have been scheduled, they will be retried on the next step.
1408
+ invalid_request_id = e.request_id
1409
+ self._abort_and_cache_schedule(
1410
+ request_id=invalid_request_id,
1411
+ virtual_engine=virtual_engine,
1412
+ seq_group_metadata_list=seq_group_metadata_list,
1413
+ scheduler_outputs=scheduler_outputs,
1414
+ allow_async_output_proc=allow_async_output_proc)
1415
+ # Raise so the caller is notified that this request failed
1416
+ raise
1417
1418
# We need to do this here so that last step's sampled_token_ids can
1419
# be passed to the next iteration for PP.
@@ -1464,6 +1488,38 @@ class LLMEngine:
1488
1489
return ctx.request_outputs
1490
1491
+ def _abort_and_cache_schedule(
1492
+ self, request_id: str, virtual_engine: int,
1493
+ seq_group_metadata_list: List[SequenceGroupMetadata],
1494
+ scheduler_outputs: SchedulerOutputs,
1495
+ allow_async_output_proc: bool) -> None:
1496
+ """Aborts a single request, and caches the scheduler outputs minus that
1497
+ request. This allows the next step to continue processing the remaining
1498
+ requests without having to re-run the scheduler."""
1499
+
1500
+ # Abort the request and remove its sequence group from the current
1501
+ # schedule
1502
+ self.abort_request(request_id)
1503
+ for i, metadata in enumerate(seq_group_metadata_list):
1504
+ if metadata.request_id == request_id:
1505
+ del seq_group_metadata_list[i]
1506
+ break
1507
+ for i, group in enumerate(scheduler_outputs.scheduled_seq_groups):
1508
+ if group.seq_group.request_id == request_id:
1509
+ del scheduler_outputs.scheduled_seq_groups[i]
1510
+ break
1511
+
1512
+ # If there are still other sequence groups left in the schedule, cache
1513
+ # them and flag the engine to reuse the schedule.
1514
+ if len(seq_group_metadata_list) > 0:
1515
+ self._skip_scheduling_next_step = True
1516
+ # Reuse multi-step caching logic
1517
+ self._cache_scheduler_outputs_for_multi_step(
1518
+ virtual_engine=virtual_engine,
1519
+ scheduler_outputs=scheduler_outputs,
1520
+ seq_group_metadata_list=seq_group_metadata_list,
1521
+ allow_async_output_proc=allow_async_output_proc)
1522
+
1523
def _has_remaining_steps(
1524
self, seq_group_metadata_list: Optional[List[SequenceGroupMetadata]]
1525
) -> bool:
vllm/engine/multiprocessing/engine.py
+9
@@ -27,6 +27,7 @@ from vllm.engine.multiprocessing import (ENGINE_DEAD_ERROR, IPC_DATA_EXT,
27
from vllm.logger import init_logger
28
from vllm.outputs import RequestOutput
29
from vllm.usage.usage_lib import UsageContext
30
+from vllm.worker.model_runner_base import InputProcessingError
31
32
logger = init_logger(__name__)
33
@@ -210,6 +211,14 @@ class MQLLMEngine:
211
return self.engine.step()
212
except SystemExit:
213
raise
214
+ except InputProcessingError as e:
215
+ # Special case where we handle an error preparing the inputs for
216
+ # a single request in the batch
217
+ rpc_err = RPCError(request_id=e.request_id,
218
+ is_engine_errored=False,
219
+ exception=e.__cause__)
220
+ self._send_outputs(rpc_err)
221
+ return []
222
except BaseException as e:
223
self._set_errored(e)
224
rpc_err = RPCError(request_id=None,
vllm/worker/model_runner.py
+8
-3
@@ -53,8 +53,8 @@ from vllm.utils import (DeviceMemoryProfiler, GiB_bytes, PyObjectCache,
53
is_pin_memory_available, supports_dynamo,
54
weak_ref_tensor)
55
from vllm.worker.model_runner_base import (
56
- ModelRunnerBase, ModelRunnerInputBase, ModelRunnerInputBuilderBase,
57
- _add_attn_metadata_broadcastable_dict,
56
+ InputProcessingError, ModelRunnerBase, ModelRunnerInputBase,
57
+ ModelRunnerInputBuilderBase, _add_attn_metadata_broadcastable_dict,
58
_add_sampling_metadata_broadcastable_dict,
59
_init_attn_metadata_from_tensor_dict,
60
_init_sampling_metadata_from_tensor_dict)
@@ -1216,7 +1216,12 @@ class GPUModelRunnerBase(ModelRunnerBase[TModelInputForGPU]):
1216
"""
1217
self.builder.prepare(finished_requests_ids)
1218
for seq_group_metadata in seq_group_metadata_list:
1219
- self.builder.add_seq_group(seq_group_metadata)
1219
+ try:
1220
+ self.builder.add_seq_group(seq_group_metadata)
1221
+ except Exception as e:
1222
+ # Raise an exception that tracks the ID of the bad request
1223
+ raise InputProcessingError(seq_group_metadata.request_id,
1224
+ str(e)) from e
1225
1226
self.builder.reset_cached_inter_data()
1227
vllm/worker/model_runner_base.py
+18
@@ -261,3 +261,21 @@ class ModelRunnerWrapperBase:
261
262
def __getattr__(self, attr):
263
return getattr(self.model_runner, attr)
264
+
265
+
266
+class InputProcessingError(Exception):
267
+ """This exception is raised when an error occurs preparing the inputs for
268
+ a single sequence group.
269
+ This allows the engine to gracefully handle errors with a single sequence
270
+ group without having to fail the entire batch.
271
+ """
272
+
273
+ def __init__(self, request_id, message):
274
+ """request_id is the id of the offending sequence group"""
275
+ self.request_id = request_id
276
+ self.message = message
277
+ super().__init__(self.message)
278
+
279
+ def __str__(self):
280
+ return "Failed to prepare inputs for sequence group with request id: " \
281
+ f"{self.request_id}, Error: {self.message}"