Files
vllm-frontdoor/router/tests/test_idle.py
bing 80eef4ce6a Initial commit: router front-door vLLM stack
Three always-running vLLM services (text TP=2 GPU0+1, ocr + embed on GPU2,
sleep mode) behind a FastAPI router that auto-wakes models on request.
Tiered idle (sleep 15 min / offload 3 h), depth-aware 503s with
Retry-After, persisted wake-intent recovery, admin API on 127.0.0.1:8010.
Routine control via vllmctl is pure HTTP — no docker on the request path.

Verified: 91 router unit tests + 15-test E2E on real hardware
(measurements in CALIBRATION.md; design record in
.claude/memory/router-front-door-plan.md).

Old nginx stack files removed before git init; design survives in
.claude/memory/sleep-mode-implementation-plan.md.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 10:17:42 +00:00

196 lines
7.2 KiB
Python

"""Tiered idle management: thresholds, lock races, active-request guard."""
from __future__ import annotations
import asyncio
import time
from config import DEPTH_AWAKE, DEPTH_OFFLOADED, DEPTH_SLEEPING
def _age(svc, seconds: float) -> None:
svc.last_activity = time.monotonic() - seconds
async def test_idle_level1_after_threshold(stack, backend):
stack.cfg.idle_sleep_min = 15 / 60.0 # 15 s in "minutes"
stack.cfg.idle_offload_min = 180 / 60.0
text = stack.manager.services["text"]
text.depth = DEPTH_AWAKE
_age(text, 20)
await stack.manager.idle_tick()
assert backend.count("POST vllm-text/sleep") == 1
assert "level=1" in backend.calls[backend.last("POST vllm-text/sleep")]
assert text.depth == DEPTH_SLEEPING
async def test_idle_escalates_to_level2(stack, backend):
stack.cfg.idle_sleep_min = 15 / 60.0
stack.cfg.idle_offload_min = 180 / 60.0
text = stack.manager.services["text"]
text.depth = DEPTH_AWAKE
_age(text, 400)
await stack.manager.idle_tick()
assert "level=2" in backend.calls[backend.last("POST vllm-text/sleep")]
assert text.depth == DEPTH_OFFLOADED
async def test_level1_service_escalates_to_level2(stack, backend):
"""Already napping (level 1) and still idle -> escalate to offload.
A direct POST /sleep?level=2 on a level-1-sleeping backend is a
well-behaved NO-OP that retains the host-RAM copy (calibration
2026-08-17), so the escalation must wake into RAM first and then offload.
"""
stack.cfg.idle_sleep_min = 1 / 60.0
stack.cfg.idle_offload_min = 5 / 60.0
text = stack.manager.services["text"]
assert (await stack.manager.sleep_service("text", 1))["ok"] is True
backend.calls.clear()
svc = backend.services["vllm-text"]
svc["wake_up_calls"] = svc["sleep_calls"] = 0
_age(text, 400)
await stack.manager.idle_tick()
assert text.depth == DEPTH_OFFLOADED
assert svc["wake_up_calls"] == 1 # wake into RAM ...
assert svc["sleep_calls"] == 1 # ... then offload
assert backend.last("POST vllm-text/sleep?level=2") > backend.last("POST vllm-text/wake_up")
async def test_offload_from_awake_is_direct(stack, backend):
"""Only from depth 'awake' can level 2 be entered directly."""
backend.services["vllm-embed"]["wake_up_calls"] = 0
result = await stack.manager.sleep_service("embed", 2)
assert result["ok"] is True
assert backend.services["vllm-embed"]["wake_up_calls"] == 0
assert backend.count("POST vllm-embed/sleep?level=2") == 1
assert stack.manager.services["embed"].depth == DEPTH_OFFLOADED
async def test_escalation_failure_is_reported(stack, backend):
assert (await stack.manager.sleep_service("ocr", 1))["ok"] is True
backend.services["vllm-ocr"]["wake_fails"] = 1
backend.calls.clear()
result = await stack.manager.sleep_service("ocr", 2)
assert result["ok"] is False
assert result["reason"] == "escalation_failed"
assert backend.count("POST vllm-ocr/sleep") == 0
assert stack.manager.services["ocr"].depth == DEPTH_SLEEPING
async def test_offloaded_service_is_left_alone(stack, backend):
stack.cfg.idle_offload_min = 1 / 60.0
text = stack.manager.services["text"]
text.depth = DEPTH_OFFLOADED
_age(text, 10_000)
await stack.manager.idle_tick()
assert backend.calls == []
async def test_active_requests_block_idle_sleep(stack, backend):
stack.cfg.idle_sleep_min = 1 / 60.0
text = stack.manager.services["text"]
text.depth = DEPTH_AWAKE
_age(text, 600)
manager = stack.manager
manager.begin_request(text) # long generation in flight
try:
await manager.idle_tick()
assert backend.calls == []
finally:
manager.end_request(text)
async def test_stream_completes_then_idle_can_sleep(stack, backend, pub):
"""end_request (background task of the streamed response) re-opens sleep."""
stack.cfg.idle_sleep_min = 1 / 60.0
text = stack.manager.services["text"]
r = await pub.post("/v1/chat/completions", json={"model": "text"})
assert r.status_code == 200
assert text.active_requests == 0 # released after the body drained
_age(text, 600)
await stack.manager.idle_tick()
assert backend.count("POST vllm-text/sleep") == 1
async def test_race_last_activity_refreshed_under_lock(stack, backend):
"""A request landed between the threshold check and the locked re-check."""
stack.cfg.idle_sleep_min = 1 / 60.0 # threshold = 60 s
text = stack.manager.services["text"]
text.depth = DEPTH_AWAKE
_age(text, 600) # what the idle scan saw
# ... and then a request arrived, refreshing last_activity *now*
text.last_activity = time.monotonic()
result = await stack.manager.sleep_service("text", 1, reason="idle", min_idle_s=60.0)
assert result["ok"] is False
assert result["reason"] == "activity_resumed"
assert backend.count("POST vllm-text/sleep") == 0
async def test_race_active_request_seen_under_lock(stack, backend):
text = stack.manager.services["text"]
text.depth = DEPTH_AWAKE
_age(text, 600)
text.active_requests = 2 # arrived while we were scanning
result = await stack.manager.sleep_service("text", 1, reason="idle", min_idle_s=60.0)
assert result["ok"] is False
assert result["reason"] == "active_requests"
assert backend.calls == []
text.active_requests = 0
async def test_request_at_the_moment_the_timer_expires(pub, backend, stack):
"""E2E case 11 in miniature: the request wins, no sleep mid-flight."""
stack.cfg.idle_sleep_min = 1 / 60.0
stack.cfg.idle_offload_min = 5 / 60.0
text = stack.manager.services["text"]
text.depth = DEPTH_AWAKE
_age(text, 600)
async def tick_soon():
await asyncio.sleep(0) # run after the request started
return await stack.manager.idle_tick()
tick, response = await asyncio.gather(tick_soon(), pub.post(
"/v1/chat/completions", json={"model": "text"}))
assert response.status_code == 200
assert backend.count("POST vllm-text/sleep") == 0
async def test_admin_sleep_refuses_when_active(adm, stack, backend):
text = stack.manager.services["text"]
text.depth = DEPTH_AWAKE
stack.manager.begin_request(text)
r = await adm.post("/admin/sleep/text?level=1")
assert r.status_code == 409
assert r.json()["reason"] == "active_requests"
assert backend.count("/sleep") == 0
stack.manager.end_request(text)
async def test_idle_loop_runs_in_background_when_started(stack, backend):
stack.cfg.idle_enabled = True
stack.cfg.idle_poll_s = 0.01
stack.cfg.idle_sleep_min = 1 / 60.0 # 60 s
stack.cfg.idle_offload_min = 1000.0 # far away: expect the level-1 tier
text = stack.manager.services["text"]
text.depth = DEPTH_AWAKE
_age(text, 600)
stack.manager.start()
try:
for _ in range(100):
if backend.count("POST vllm-text/sleep"):
break
await asyncio.sleep(0.01)
finally:
await stack.manager.stop()
assert backend.count("POST vllm-text/sleep") == 1
assert "level=1" in backend.calls[backend.last("POST vllm-text/sleep")]