[Logs] DP Supervisor Log Improvement (#48278)
Signed-off-by: Robert Shaw <robertgshaw2-redhat@ip-172-31-18-125.us-east-2.compute.internal> Co-authored-by: Robert Shaw <robertgshaw2-redhat@ip-172-31-18-125.us-east-2.compute.internal>
Robert Shaw committed
Jul 11, 2026 at 00:07 UTC
3d99b0499aff7ce3b91942b0385558dab4eecf76
2 files changed
+96
-42
tests/entrypoints/openai/test_dp_supervisor.py
+50
-30
@@ -301,9 +301,20 @@ async def test_shutdown_if_supervisor_server_error_on_startup(
301
async def fake_shutdown_children(self):
302
return None
303
304
+ def fake_start_children(self):
305
+ return None
306
+
307
+ async def fake_monitor_children(self):
308
+ # Mark ready so the supervisor server is started, then block until
309
+ # shutdown (triggered when the failing server task exits).
310
+ self._is_ready = True
311
+ await self._shutdown_event.wait()
312
+
313
monkeypatch.setattr(dp_sup.asyncio, "get_running_loop", lambda: FakeLoop())
314
monkeypatch.setattr(dp_sup.uvicorn, "Server", FakeServer)
315
monkeypatch.setattr(DPSupervisor, "_shutdown_children", fake_shutdown_children)
316
+ monkeypatch.setattr(DPSupervisor, "_start_children", fake_start_children)
317
+ monkeypatch.setattr(DPSupervisor, "_monitor_children", fake_monitor_children)
318
319
supervisor = DPSupervisor(_make_unit_args())
320
@@ -437,8 +448,11 @@ def launch_mock_vllm_with_drain(
448
449
async def _poll_supervisor_health(expected_status: int, use_ssl: bool = False) -> bool:
450
"""
440
- Poll GET /health on the supervisor until expected_status is seen.
441
- A connection error is treated as 503-equivalent when expected_status != 200.
451
+ GET /health on the supervisor once and check for expected_status.
452
+
453
+ Pass expected_status=-1 to assert the supervisor is not listening yet
454
+ (a connection error is expected). The supervisor only starts its HTTP
455
+ server once every child is ready, so /health is refused until then.
456
"""
457
scheme = "https" if use_ssl else "http"
458
url = f"{scheme}://127.0.0.1:{_SUPERVISOR_PORT}/health"
@@ -456,6 +470,17 @@ async def _poll_supervisor_health(expected_status: int, use_ssl: bool = False) -
470
return True
471
472
473
+async def _await_supervisor_health(
474
+ expected_status: int, use_ssl: bool = False, retries: int = 20
475
+) -> bool:
476
+ """Retry _poll_supervisor_health, tolerating supervisor server startup."""
477
+ for _ in range(retries):
478
+ if await _poll_supervisor_health(expected_status, use_ssl=use_ssl):
479
+ return True
480
+ await asyncio.sleep(0.5)
481
+ return False
482
+
483
+
484
async def _poll_until_api_server_running(
485
port: int, retries: int = 10, use_ssl: bool = False
486
) -> None:
@@ -534,7 +559,7 @@ async def _run_supervisor(
559
@pytest.mark.asyncio
560
async def test_basic_lifecycle(monkeypatch):
561
"""
537
- A) Supervisor /health returns 503 while children are unhealthy.
562
+ A) Supervisor is not listening while children are unhealthy.
563
B) /health returns 200 once every child reports healthy.
564
C) SIGTERM and shutdown
565
"""
@@ -543,24 +568,23 @@ async def test_basic_lifecycle(monkeypatch):
568
vllm_server_ports = [_CHILD_PORT_BASE + i for i in range(_N_CHILDREN)]
569
570
async with _run_supervisor(args, monkeypatch) as (supervisor, _task):
546
- assert await _poll_supervisor_health(503)
571
+ assert await _poll_supervisor_health(-1)
572
assert not supervisor.is_ready
573
574
for port in vllm_server_ports:
550
- assert await _poll_supervisor_health(503)
575
+ assert await _poll_supervisor_health(-1)
576
assert not supervisor.is_ready
577
await _poll_until_api_server_running(port)
578
579
await _set_healthy(vllm_server_ports[0])
580
await asyncio.sleep(1.0)
556
- assert await _poll_supervisor_health(503)
581
+ assert await _poll_supervisor_health(-1)
582
assert not supervisor.is_ready
558
- print("/health is 503 --- expected!")
583
+ print("supervisor not listening --- expected!")
584
585
for port in vllm_server_ports:
586
await _set_healthy(port)
562
- await asyncio.sleep(1.0)
563
- assert await _poll_supervisor_health(200)
587
+ assert await _await_supervisor_health(200)
588
assert supervisor.is_ready
589
print("/health is 200 --- expected!")
590
@@ -589,19 +613,18 @@ async def test_basic_lifecycle_with_ssl(monkeypatch):
613
vllm_server_ports = [_CHILD_PORT_BASE + i for i in range(_N_CHILDREN)]
614
615
async with _run_supervisor(args, monkeypatch) as (supervisor, _task):
592
- assert await _poll_supervisor_health(503, use_ssl=True)
616
+ assert await _poll_supervisor_health(-1, use_ssl=True)
617
assert not supervisor.is_ready
618
619
for port in vllm_server_ports:
596
- assert await _poll_supervisor_health(503, use_ssl=True)
620
+ assert await _poll_supervisor_health(-1, use_ssl=True)
621
assert not supervisor.is_ready
622
await _poll_until_api_server_running(port, use_ssl=True)
623
624
for port in vllm_server_ports:
625
await _set_healthy(port, use_ssl=True)
602
- await asyncio.sleep(1.0)
626
604
- assert await _poll_supervisor_health(200, use_ssl=True)
627
+ assert await _await_supervisor_health(200, use_ssl=True)
628
assert supervisor.is_ready
629
630
@@ -616,7 +639,7 @@ async def test_failed_startup(monkeypatch):
639
vllm_server_ports = [_CHILD_PORT_BASE + i for i in range(_N_CHILDREN)]
640
641
async with _run_supervisor(args, monkeypatch) as (supervisor, _task):
619
- assert await _poll_supervisor_health(503)
642
+ assert await _poll_supervisor_health(-1)
643
assert not supervisor.is_ready
644
645
for port in vllm_server_ports:
@@ -632,7 +655,7 @@ async def test_failed_startup(monkeypatch):
655
@pytest.mark.asyncio
656
async def test_becomes_unhealthy(monkeypatch):
657
"""
635
- A) Supervisor /health returns 503 while children are unhealthy.
658
+ A) Supervisor is not listening while children are unhealthy.
659
B) /health returns 200 once every child reports healthy.
660
C) Child process becomes unhealthy.
661
D) Detected and shutdown.
@@ -642,24 +665,23 @@ async def test_becomes_unhealthy(monkeypatch):
665
vllm_server_ports = [_CHILD_PORT_BASE + i for i in range(_N_CHILDREN)]
666
667
async with _run_supervisor(args, monkeypatch) as (supervisor, _task):
645
- assert await _poll_supervisor_health(503)
668
+ assert await _poll_supervisor_health(-1)
669
assert not supervisor.is_ready
670
671
for port in vllm_server_ports:
649
- assert await _poll_supervisor_health(503)
672
+ assert await _poll_supervisor_health(-1)
673
assert not supervisor.is_ready
674
await _poll_until_api_server_running(port)
675
676
await _set_healthy(vllm_server_ports[0])
677
await asyncio.sleep(1.0)
655
- assert await _poll_supervisor_health(503)
678
+ assert await _poll_supervisor_health(-1)
679
assert not supervisor.is_ready
657
- print("/health is 503 --- expected!")
680
+ print("supervisor not listening --- expected!")
681
682
for port in vllm_server_ports:
683
await _set_healthy(port)
661
- await asyncio.sleep(1.0)
662
- assert await _poll_supervisor_health(200)
684
+ assert await _await_supervisor_health(200)
685
assert supervisor.is_ready
686
print("/health is 200 --- expected!")
687
@@ -674,7 +696,7 @@ async def test_becomes_unhealthy(monkeypatch):
696
@pytest.mark.asyncio
697
async def test_dp_server_fails(monkeypatch):
698
"""
677
- A) Supervisor /health returns 503 while children are unhealthy.
699
+ A) Supervisor is not listening while children are unhealthy.
700
B) /health returns 200 once every child reports healthy.
701
C) Child process fails.
702
D) Detected and shutdown.
@@ -684,24 +706,23 @@ async def test_dp_server_fails(monkeypatch):
706
vllm_server_ports = [_CHILD_PORT_BASE + i for i in range(_N_CHILDREN)]
707
708
async with _run_supervisor(args, monkeypatch) as (supervisor, _task):
687
- assert await _poll_supervisor_health(503)
709
+ assert await _poll_supervisor_health(-1)
710
assert not supervisor.is_ready
711
712
for port in vllm_server_ports:
691
- assert await _poll_supervisor_health(503)
713
+ assert await _poll_supervisor_health(-1)
714
assert not supervisor.is_ready
715
await _poll_until_api_server_running(port)
716
717
await _set_healthy(vllm_server_ports[0])
718
await asyncio.sleep(1.0)
697
- assert await _poll_supervisor_health(503)
719
+ assert await _poll_supervisor_health(-1)
720
assert not supervisor.is_ready
699
- print("/health is 503 --- expected!")
721
+ print("supervisor not listening --- expected!")
722
723
for port in vllm_server_ports:
724
await _set_healthy(port)
703
- await asyncio.sleep(1.0)
704
- assert await _poll_supervisor_health(200)
725
+ assert await _await_supervisor_health(200)
726
assert supervisor.is_ready
727
print("/health is 200 --- expected!")
728
@@ -739,8 +760,7 @@ async def test_shutdown_timeout(monkeypatch: pytest.MonkeyPatch):
760
761
for port in vllm_server_ports:
762
await _set_healthy(port)
742
- await asyncio.sleep(1.0)
743
- assert await _poll_supervisor_health(200)
763
+ assert await _await_supervisor_health(200)
764
assert supervisor.is_ready
765
766
start_t = time.perf_counter()
vllm/entrypoints/openai/dp_supervisor.py
+46
-12
@@ -283,14 +283,46 @@ class DPSupervisor:
283
284
async def run(self) -> None:
285
loop = asyncio.get_running_loop()
286
+ decorate_logs("DPSupervisor")
287
288
# K8s sends SIGTERM for shutdown - begin graceful termination.
289
for sig in (signal.SIGTERM, signal.SIGINT):
290
loop.add_signal_handler(sig, partial(self._handle_signal, sig))
291
291
- # Launch DPSupervisor Server.
292
+ supervisor_server: uvicorn.Server | None = None
293
+ supervisor_server_task: asyncio.Task[None] | None = None
294
+ try:
295
+ # Launch vLLM DP Servers and begin monitoring them.
296
+ self._start_children()
297
+ monitor_task = asyncio.create_task(
298
+ self._monitor_children(), name="dp-monitor"
299
+ )
300
+
301
+ # Only start the DPSupervisor server once the vLLM DP Servers are
302
+ # ready. This avoids the supervisor /health endpoint returning 503
303
+ # to external load balancer probes while the engines initialize.
304
+ await self._wait_until_ready(monitor_task)
305
+ if self.is_ready and not monitor_task.done():
306
+ supervisor_server, supervisor_server_task = await self._start_server()
307
+
308
+ await monitor_task
309
+ finally:
310
+ self._is_ready = False
311
+ await self._shutdown_children()
312
+
313
+ # Shutdown the DP Supervisor server.
314
+ if supervisor_server is not None and supervisor_server_task is not None:
315
+ supervisor_server.should_exit = True
316
+ await supervisor_server_task
317
+
318
+ async def _start_server(self) -> tuple[uvicorn.Server, asyncio.Task[None]]:
319
+ """
320
+ Launch the DPSupervisor HTTP server.
321
+
322
+ Called only after the vLLM DP Servers are ready so that /health does
323
+ not return 503 to external probes while the engines are initializing.
324
+ """
325
app = _build_dp_supervisor_app(self)
293
- decorate_logs("DPSupervisor")
326
host = self.args.host or "0.0.0.0"
327
config = uvicorn.Config(
328
app,
@@ -319,18 +351,20 @@ class DPSupervisor:
351
raise RuntimeError("DPSupervisor exited before startup.")
352
await asyncio.sleep(0.05)
353
logger.info("Started DPSupervisor on %s:%d", host, self.supervisor_port)
354
+ return supervisor_server, supervisor_server_task
355
323
- # Launch and Monitor vLLM Server Processes.
324
- try:
325
- self._start_children()
326
- await self._monitor_children()
327
- finally:
328
- self._is_ready = False
329
- await self._shutdown_children()
356
+ async def _wait_until_ready(self, monitor_task: asyncio.Task[None]) -> None:
357
+ """
358
+ Block until the vLLM DP Servers are ready or shutdown is triggered.
359
331
- # Shutdown the DP Supervisor server.
332
- supervisor_server.should_exit = True
333
- await supervisor_server_task
360
+ Returns early if monitoring stops (e.g. a DP Server dies during
361
+ startup), in which case the supervisor server is never started.
362
+ """
363
+ logger.info("Waiting for vLLM DP Servers to become ready.")
364
+ while not self._is_ready and not self._shutdown_event.is_set():
365
+ if monitor_task.done():
366
+ return
367
+ await asyncio.sleep(0.05)
368
369
def _handle_signal(self, signum: int) -> None:
370
"""