Skip to content

Commit 9b3b193

Browse files
authored
Improve engine health monitoring (#4645)
* Improve engine health monitoring and wakeup scheduling * remove default value from health_probe() * revert to sync wakeup
1 parent f02d548 commit 9b3b193

5 files changed

Lines changed: 62 additions & 11 deletions

File tree

lmdeploy/pytorch/engine/engine_loop.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -446,7 +446,6 @@ async def __no_running_warning():
446446
running=next_running,
447447
forward_inputs=forward_inputs,
448448
)
449-
scheduler.tick()
450449
self.inputs_maker.deactivate_evict_seqs()
451450
has_runable_event.set()
452451

lmdeploy/pytorch/engine/inputs_maker.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -796,6 +796,7 @@ async def _send_next_inputs_impl(self, prefill: bool = None, enable_empty: bool
796796
session_ids = [seq.session_id for seq in next_running]
797797
logger.debug(f'Forward session_ids: {session_ids}')
798798
await self.executor.forward_async(forward_inputs)
799+
self.scheduler.tick()
799800
self.forward_inputs = forward_inputs
800801
return forward_inputs, next_running
801802

lmdeploy/pytorch/paging/scheduler.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ def __init__(
6767
self.scheduler_tick = 0
6868

6969
def tick(self):
70-
"""Mark one scheduler progress step."""
70+
"""Mark one scheduler progress step (once per forward dispatch)."""
7171
self.scheduler_tick += 1
7272

7373
@staticmethod

lmdeploy/serve/core/async_engine.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -280,7 +280,7 @@ def _validate_scheduler_progress(self, metrics, scheduler_stall_timeout: float)
280280
def _make_health_result(status: str, message: str) -> dict:
281281
return dict(status=status, message=message)
282282

283-
async def health_probe(self, timeout: float = 2.0, scheduler_stall_timeout: float = 15.0) -> dict:
283+
async def health_probe(self, timeout: float, scheduler_stall_timeout: float) -> dict:
284284
"""Probe backend health with a bounded, non-overlapping call."""
285285
if self.is_sleeping:
286286
return self._make_health_result(
@@ -291,7 +291,7 @@ async def health_probe(self, timeout: float = 2.0, scheduler_stall_timeout: floa
291291
if self._health_probe_task is not None:
292292
if not self._health_probe_task.done():
293293
return self._make_health_result(
294-
status='unhealthy',
294+
status='pending',
295295
message='Previous backend health probe is still pending.',
296296
)
297297
try:

lmdeploy/serve/core/health.py

Lines changed: 58 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,73 @@
22
from __future__ import annotations
33

44
import asyncio
5+
import os
56
import time
67
from typing import TYPE_CHECKING
78

9+
from lmdeploy.utils import get_logger
10+
811
if TYPE_CHECKING:
912
from .async_engine import AsyncEngine
1013

14+
logger = get_logger('lmdeploy')
15+
16+
HEALTH_POLL_INTERVAL = 'LMDEPLOY_HEALTH_POLL_INTERVAL'
17+
HEALTH_PROBE_TIMEOUT = 'LMDEPLOY_HEALTH_PROBE_TIMEOUT'
18+
HEALTH_UNHEALTHY_AFTER = 'LMDEPLOY_HEALTH_UNHEALTHY_AFTER'
19+
20+
DEFAULT_PROBE_TIMEOUT = 10.0
21+
DEFAULT_POLL_INTERVAL = 12.0
22+
DEFAULT_UNHEALTHY_AFTER = 90.0
23+
24+
25+
def _env_override_float(env_var: str, value: float) -> float:
26+
"""Return ``value`` unless ``env_var`` is set, then parse and return it."""
27+
env_value = os.getenv(env_var)
28+
if env_value is None:
29+
return value
30+
try:
31+
return float(env_value)
32+
except ValueError:
33+
return value
34+
1135

1236
class EngineHealthMonitor:
1337
"""Background engine health monitor."""
1438

1539
def __init__(self,
1640
async_engine: AsyncEngine | None,
17-
poll_interval: float = 5.0,
18-
probe_timeout: float = 2.0,
19-
unhealthy_after: float = 15.0):
41+
poll_interval: float = DEFAULT_POLL_INTERVAL,
42+
probe_timeout: float = DEFAULT_PROBE_TIMEOUT,
43+
unhealthy_after: float = DEFAULT_UNHEALTHY_AFTER):
44+
"""Initialize the background health monitor.
45+
46+
Args:
47+
async_engine: Engine instance to probe; ``None`` marks the service
48+
unhealthy until an engine is attached.
49+
poll_interval: Seconds between consecutive ``probe_once()`` calls in
50+
the background loop (default 12.0). Should be greater than
51+
``probe_timeout`` to reduce overlapping probes. Overridden by
52+
``LMDEPLOY_HEALTH_POLL_INTERVAL`` when that variable is set.
53+
probe_timeout: Maximum seconds to wait for a single
54+
``health_probe()`` (backend ``get_health_status()``) before
55+
reporting that probe as unhealthy (default 10.0). Overridden by
56+
``LMDEPLOY_HEALTH_PROBE_TIMEOUT`` when that variable is set.
57+
unhealthy_after: Seconds without a successful probe or scheduler
58+
progress before the service is considered unhealthy (default
59+
90.0). Overridden by ``LMDEPLOY_HEALTH_UNHEALTHY_AFTER`` when
60+
that variable is set. Passed to ``health_probe()`` as
61+
``scheduler_stall_timeout``, and also used in ``snapshot()``
62+
to expire stale healthy status or stuck ``initializing`` state.
63+
"""
2064
self.async_engine = async_engine
21-
self.poll_interval = poll_interval
22-
self.probe_timeout = probe_timeout
23-
self.unhealthy_after = unhealthy_after
65+
self.poll_interval = _env_override_float(HEALTH_POLL_INTERVAL, poll_interval)
66+
self.probe_timeout = _env_override_float(HEALTH_PROBE_TIMEOUT, probe_timeout)
67+
self.unhealthy_after = _env_override_float(HEALTH_UNHEALTHY_AFTER, unhealthy_after)
68+
if self.poll_interval <= self.probe_timeout:
69+
logger.warning('Engine health poll_interval (%.1fs) should be greater than probe_timeout (%.1fs) '
70+
'to avoid overlapping probes.',
71+
self.poll_interval, self.probe_timeout)
2472
self._task: asyncio.Task | None = None
2573
self._started_time = time.monotonic()
2674
self._last_success_time: float | None = None
@@ -61,14 +109,17 @@ async def probe_once(self):
61109
message=f'Engine health probe failed: {e}')
62110

63111
status = result['status']
112+
if status == 'pending':
113+
logger.info('Engine health probe skipped: previous backend health probe is still pending.')
114+
return
64115
if status in ('healthy', 'sleeping'):
65116
self._last_success_time = probe_time
66117
self._snapshot = dict(status=status, message=result['message'])
67118

68119
def snapshot(self) -> dict:
69120
snapshot = dict(self._snapshot)
70121
now = time.monotonic()
71-
if snapshot['status'] in ('healthy', 'sleeping') and self._last_success_time is not None:
122+
if snapshot['status'] == 'healthy' and self._last_success_time is not None:
72123
if now - self._last_success_time > self.unhealthy_after:
73124
snapshot['status'] = 'unhealthy'
74125
snapshot['message'] = f'No successful health probe for {now - self._last_success_time:.1f}s.'

0 commit comments

Comments
 (0)