fix: cancel abandoned generation tasks

- Propagate stream closure and stop-sequence termination into scheduler cancellation
- Defer active KV release to the scheduler owner and close metrics safely
- Expose lifecycle counters and cover waiting, active, and allocation-race cleanup
This commit is contained in:
0z5a
2026-09-02 12:55:05 +08:00
parent 90de5bc1bd
commit 9c3ef0c2a1
10 changed files with 410 additions and 130 deletions
+99
View File
@@ -1,6 +1,7 @@
"""Tests for scheduler concurrency."""
import threading
import time
from types import SimpleNamespace
from unittest.mock import MagicMock, patch
@@ -259,6 +260,104 @@ def _make_real_scheduler(device):
return scheduler, tokenizer, model
def test_cancel_waiting_task_storm_returns_to_baseline(device):
scheduler, _tok, _model = _make_real_scheduler(device)
try:
task_ids = [
scheduler.add_task(f"waiting-{index}", max_tokens=32) for index in range(32)
]
assert all(scheduler.cancel_task(task_id) for task_id in task_ids)
stats = scheduler.get_stats()
assert stats["active_tasks"] == 0
assert stats["waiting_tasks"] == 0
assert stats["in_flight_tasks"] == 0
assert stats["kv_cache_tasks"] == 0
assert stats["cancelled_total"] == len(task_ids)
finally:
scheduler.stop()
def test_cancel_active_task_releases_metrics_and_kv(device):
scheduler, _tok, _model = _make_real_scheduler(device)
try:
task_id = scheduler.add_task("active", max_tokens=32)
task = scheduler._task_mgr.pull_candidates(1)[0]
assert scheduler._task_cache.task_alloc(task.task_id, task.prompt_ids)
assert scheduler._task_mgr.activate(task)
before = scheduler.get_stats()
assert before["active_tasks"] == 1
assert before["in_flight_tasks"] == 1
assert before["kv_cache_tasks"] == 1
assert scheduler.cancel_task(task_id)
scheduler.start()
deadline = time.monotonic() + 5
while time.monotonic() < deadline:
after = scheduler.get_stats()
if (
after["active_tasks"] == 0
and after["in_flight_tasks"] == 0
and after["kv_cache_tasks"] == 0
):
break
time.sleep(0.01)
assert after["active_tasks"] == 0
assert after["waiting_tasks"] == 0
assert after["in_flight_tasks"] == 0
assert after["kv_cache_tasks"] == 0
assert after["cancelled_total"] == 1
finally:
scheduler.stop()
def test_cancel_during_kv_allocation_releases_metrics_and_kv(device):
scheduler, _tok, _model = _make_real_scheduler(device)
allocation_started = threading.Event()
continue_allocation = threading.Event()
original_alloc = scheduler._task_cache.task_alloc
def blocking_alloc(*args, **kwargs):
allocation_started.set()
assert continue_allocation.wait(timeout=5)
return original_alloc(*args, **kwargs)
try:
with patch.object(
scheduler._task_cache,
"task_alloc",
side_effect=blocking_alloc,
):
scheduler.start()
task_id = scheduler.add_task("allocation-race", max_tokens=32)
assert allocation_started.wait(timeout=5)
assert scheduler.cancel_task(task_id)
continue_allocation.set()
deadline = time.monotonic() + 5
while time.monotonic() < deadline:
stats = scheduler.get_stats()
if (
stats["active_tasks"] == 0
and stats["waiting_tasks"] == 0
and stats["in_flight_tasks"] == 0
and stats["kv_cache_tasks"] == 0
):
break
time.sleep(0.01)
assert stats["active_tasks"] == 0
assert stats["waiting_tasks"] == 0
assert stats["in_flight_tasks"] == 0
assert stats["kv_cache_tasks"] == 0
assert stats["cancelled_total"] == 1
finally:
continue_allocation.set()
scheduler.stop()
def test_run_batch_returns_token_sequences(device):
scheduler, _tok, _model = _make_real_scheduler(device)
try: