[Bugfix] Fix colocate-async sleep admission race and deliver AR abort tokens (#6367)
Signed-off-by: knlnguyen1802 <knlnguyen1802@gmail.com> Signed-off-by: Cursor <cursoragent@cursor.com> Signed-off-by: Samit <285365963@qq.com> Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Samit <285365963@qq.com>
This commit is contained in:
23 files changed
+1229
-157
No files matched your search
@@ -67,7 +67,8 @@ def test_abort_async_waits_for_ack(mocker: MockerFixture):
|
||||
assert not task.done()
|
||||
|
||||
rpc_q.put(AbortResultMessage(rpc_id="abort-rpc-1", success=True))
|
||||
await task
|
||||
result = await task
|
||||
assert result == []
|
||||
|
||||
try:
|
||||
asyncio.run(_run())
|
||||
|
||||
@@ -256,8 +256,32 @@ class FakeOutputProcessor:
|
||||
)
|
||||
|
||||
def abort_requests(self, request_ids, internal: bool = False):
|
||||
self.abort_calls.append(request_ids)
|
||||
return request_ids
|
||||
aborted_ids, _outputs = self.abort_requests_collecting_outputs(request_ids, internal=internal)
|
||||
return aborted_ids
|
||||
|
||||
def abort_requests_collecting_outputs(self, request_ids, *, internal: bool = False, commit_state: bool = True):
|
||||
"""Mirror MultimodalOutputProcessor collecting for AR abort-prefix tests."""
|
||||
del internal, commit_state
|
||||
ids = list(request_ids)
|
||||
self.abort_calls.append(ids)
|
||||
outputs: list[RequestOutput] = []
|
||||
for rid in ids:
|
||||
seeded = next(
|
||||
(ro for ro in self.request_outputs if getattr(ro, "request_id", None) == rid),
|
||||
None,
|
||||
)
|
||||
token_ids = list(seeded.outputs[0].token_ids) if seeded is not None and seeded.outputs else [1, 2]
|
||||
# Intentionally stamp an internal-looking id on the RequestOutput so
|
||||
# StagePool must re-key by the orchestrator id it passed in.
|
||||
outputs.append(
|
||||
_build_request_output(
|
||||
f"engine-internal-{rid}",
|
||||
token_ids=token_ids,
|
||||
finished=True,
|
||||
finish_reason="abort",
|
||||
)
|
||||
)
|
||||
return ids, outputs
|
||||
|
||||
def update_scheduler_stats(self, _scheduler_stats) -> None:
|
||||
return None
|
||||
@@ -320,6 +344,7 @@ def _build_request_output(
|
||||
prompt_token_ids: list[int] | None = None,
|
||||
finished: bool = True,
|
||||
text: str = "test",
|
||||
finish_reason: str | None = None,
|
||||
) -> RequestOutput:
|
||||
completion = CompletionOutput(
|
||||
index=0,
|
||||
@@ -327,7 +352,7 @@ def _build_request_output(
|
||||
token_ids=list(token_ids or [1, 2]),
|
||||
cumulative_logprob=0.0,
|
||||
logprobs=None,
|
||||
finish_reason="stop" if finished else None,
|
||||
finish_reason=(finish_reason if finish_reason is not None else ("stop" if finished else None)),
|
||||
stop_reason=None,
|
||||
)
|
||||
return RequestOutput(
|
||||
@@ -983,7 +1008,11 @@ async def test_run_abort(orchestrator_factory) -> None:
|
||||
async def test_run_abort_emits_result_when_rpc_id_set(orchestrator_factory) -> None:
|
||||
stages = [
|
||||
FakeStageClient(stage_type="llm", final_output=False),
|
||||
FakeStageClient(stage_type="llm", final_output=True),
|
||||
FakeStageClient(
|
||||
stage_type="llm",
|
||||
final_output=True,
|
||||
next_inputs=[{"prompt_token_ids": [7, 8, 9]}],
|
||||
),
|
||||
]
|
||||
processors = [
|
||||
FakeOutputProcessor(request_outputs=[_build_request_output("req-ack", token_ids=[1], finished=True)]),
|
||||
@@ -1002,6 +1031,11 @@ async def test_run_abort_emits_result_when_rpc_id_set(orchestrator_factory) -> N
|
||||
final_stage_id=1,
|
||||
)
|
||||
await _wait_for(lambda: len(stages[0].add_request_calls) == 1)
|
||||
# Abort outputs come from the final AR stage's output processor, and
|
||||
# StagePool.abort_requests only collects for requests with a live
|
||||
# replica binding. Forward off stage-0 first so stage-1 is bound.
|
||||
stages[0].push_engine_core_outputs(_engine_core_outputs("stage0-raw", 1.0))
|
||||
await _wait_for(lambda: len(stages[1].add_request_calls) == 1)
|
||||
|
||||
orchestrator_fixture.request_sync_q.put_nowait(AbortRequestMessage(request_ids=["req-ack"], rpc_id="abort-1"))
|
||||
result = await _get_rpc_message(orchestrator_fixture)
|
||||
@@ -1011,11 +1045,53 @@ async def test_run_abort_emits_result_when_rpc_id_set(orchestrator_factory) -> N
|
||||
assert result.error is None
|
||||
assert result.rpc_correlation_key == ("abort", "abort-1")
|
||||
assert stages[0].abort_calls == [["req-ack"]]
|
||||
assert stages[1].abort_calls == [["req-ack"]]
|
||||
assert "req-ack" not in orchestrator_fixture.orchestrator.request_states
|
||||
assert result.abort_outputs is not None
|
||||
assert len(result.abort_outputs) == 1
|
||||
abort_msg = result.abort_outputs[0]
|
||||
assert abort_msg.request_id == "req-ack"
|
||||
assert abort_msg.finished is True
|
||||
assert abort_msg.stage_id == 1
|
||||
assert list(abort_msg.engine_outputs.outputs[0].token_ids) == [2]
|
||||
assert abort_msg.engine_outputs.outputs[0].finish_reason == "abort"
|
||||
finally:
|
||||
await _shutdown_orchestrator(orchestrator_fixture)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_abort_marks_only_last_final_stage_output_finished() -> None:
|
||||
"""A request with two final AR stages must keep earlier abort outputs unfinished."""
|
||||
stages = [
|
||||
FakeStageClient(stage_type="llm", final_output=True),
|
||||
FakeStageClient(stage_type="llm", final_output=True),
|
||||
]
|
||||
processors = [
|
||||
FakeOutputProcessor(request_outputs=[_build_request_output("req-two", token_ids=[1], finished=True)]),
|
||||
FakeOutputProcessor(request_outputs=[_build_request_output("req-two", token_ids=[2], finished=True)]),
|
||||
]
|
||||
pools = _build_stage_pools([[stages[0]], [stages[1]]], output_processors=processors)
|
||||
pools[0]._request_bindings["req-two"] = 0
|
||||
pools[1]._request_bindings["req-two"] = 0
|
||||
|
||||
orchestrator = Orchestrator(
|
||||
request_async_queue=asyncio.Queue(),
|
||||
output_async_queue=asyncio.Queue(),
|
||||
rpc_async_queue=asyncio.Queue(),
|
||||
stage_pools=pools,
|
||||
)
|
||||
orchestrator.request_states["req-two"] = OrchestratorRequestState(
|
||||
request_id="req-two",
|
||||
final_stage_id=1,
|
||||
final_output_stage_ids={0, 1},
|
||||
)
|
||||
|
||||
outputs = await orchestrator._abort_request_ids(["req-two"])
|
||||
assert [(msg.stage_id, msg.finished) for msg in outputs] == [(0, False), (1, True)]
|
||||
assert list(outputs[0].engine_outputs.outputs[0].token_ids) == [1]
|
||||
assert list(outputs[1].engine_outputs.outputs[0].token_ids) == [2]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_handle_abort_emits_error_result_on_failure() -> None:
|
||||
rpc_queue: asyncio.Queue = asyncio.Queue()
|
||||
|
||||
@@ -700,3 +700,62 @@ def test_num_cache_creation_tokens_reaches_direct_request_output():
|
||||
|
||||
assert direct.num_cached_tokens == 3
|
||||
assert direct.num_cache_creation_tokens == 2
|
||||
|
||||
|
||||
def _abort_processor_with_parent(child_ids: list[str]):
|
||||
processor = object.__new__(MultimodalOutputProcessor)
|
||||
processor.request_states = {}
|
||||
processor.external_req_ids = {}
|
||||
processor.parent_requests = {}
|
||||
processor._native_text_metrics_by_request = {}
|
||||
processor.lora_states = SimpleNamespace(request_finished=lambda *_args, **_kwargs: None)
|
||||
parent = SimpleNamespace(request_id="parent", child_requests=set(child_ids))
|
||||
processor.parent_requests["parent"] = parent
|
||||
for child_id in child_ids:
|
||||
req_state = SimpleNamespace(
|
||||
parent_req=parent,
|
||||
lora_name=None,
|
||||
output_kind=RequestOutputKind.CUMULATIVE,
|
||||
detokenizer=SimpleNamespace(output_token_ids=[7, 8]),
|
||||
queue=None,
|
||||
external_req_id="parent",
|
||||
make_request_output=MagicMock(return_value=SimpleNamespace(request_id=child_id)),
|
||||
)
|
||||
processor.request_states[child_id] = req_state
|
||||
return processor, parent
|
||||
|
||||
|
||||
def test_abort_last_parallel_child_drops_parent_request():
|
||||
processor, parent = _abort_processor_with_parent(["0_parent", "1_parent"])
|
||||
|
||||
aborted, _outputs = processor.abort_requests_collecting_outputs(["0_parent"], internal=True)
|
||||
assert aborted == ["0_parent"]
|
||||
assert "parent" in processor.parent_requests
|
||||
assert parent.child_requests == {"1_parent"}
|
||||
|
||||
aborted, _outputs = processor.abort_requests_collecting_outputs(["1_parent"], internal=True)
|
||||
assert aborted == ["1_parent"]
|
||||
assert "parent" not in processor.parent_requests
|
||||
assert not parent.child_requests
|
||||
|
||||
|
||||
def test_abort_snapshot_leaves_state_until_commit():
|
||||
processor, parent = _abort_processor_with_parent(["0_parent"])
|
||||
processor.external_req_ids["parent"] = ["0_parent"]
|
||||
|
||||
aborted, outputs = processor.abort_requests_collecting_outputs(
|
||||
["parent"],
|
||||
internal=False,
|
||||
commit_state=False,
|
||||
)
|
||||
assert aborted == ["0_parent"]
|
||||
assert outputs
|
||||
assert "0_parent" in processor.request_states
|
||||
assert processor.external_req_ids["parent"] == ["0_parent"]
|
||||
assert "parent" in processor.parent_requests
|
||||
assert parent.child_requests == {"0_parent"}
|
||||
|
||||
processor.commit_aborted_request_state(["parent"], internal=False)
|
||||
assert "0_parent" not in processor.request_states
|
||||
assert "parent" not in processor.parent_requests
|
||||
assert "parent" not in processor.external_req_ids
|
||||
@@ -64,8 +64,61 @@ def test_collective_rpc_control_helper_honors_timeout():
|
||||
return "slept"
|
||||
|
||||
pool, _client = _make_pool(sleep_async=slow_sleep)
|
||||
result = await pool.collective_rpc(0, "sleep", timeout=0.01, args=(1,))
|
||||
assert result != "slept"
|
||||
with pytest.raises(asyncio.TimeoutError):
|
||||
await pool.collective_rpc(0, "sleep", timeout=0.01, args=(1,))
|
||||
|
||||
asyncio.run(run())
|
||||
|
||||
|
||||
@pytest.mark.cpu
|
||||
def test_collective_rpc_control_method_reraises_worker_error():
|
||||
async def run() -> None:
|
||||
pool, _client = _make_pool(wake_up_async=AsyncMock(side_effect=RuntimeError("worker died")))
|
||||
with pytest.raises(RuntimeError, match="worker died"):
|
||||
await pool.collective_rpc(0, "wake_up")
|
||||
|
||||
asyncio.run(run())
|
||||
|
||||
|
||||
@pytest.mark.cpu
|
||||
def test_collective_rpc_non_control_method_still_returns_error_dict():
|
||||
async def run() -> None:
|
||||
pool, _client = _make_pool(collective_rpc_async=AsyncMock(side_effect=RuntimeError("probe failed")))
|
||||
result = await pool.collective_rpc(0, "reset_prefix_cache")
|
||||
assert result["supported"] is False
|
||||
assert "probe failed" in result["error"]
|
||||
|
||||
asyncio.run(run())
|
||||
|
||||
|
||||
@pytest.mark.cpu
|
||||
def test_abort_requests_does_not_commit_op_state_when_engine_abort_fails():
|
||||
async def run() -> None:
|
||||
class RecordingOutputProcessor:
|
||||
def __init__(self) -> None:
|
||||
self.collected = False
|
||||
self.committed = False
|
||||
|
||||
def abort_requests_collecting_outputs(self, request_ids, *, internal=False, commit_state=True):
|
||||
del internal
|
||||
self.collected = True
|
||||
assert commit_state is False
|
||||
return list(request_ids), [SimpleNamespace(request_id=request_ids[0])]
|
||||
|
||||
def commit_aborted_request_state(self, request_ids, *, internal=False):
|
||||
del request_ids, internal
|
||||
self.committed = True
|
||||
|
||||
abort = AsyncMock(side_effect=RuntimeError("engine abort failed"))
|
||||
output_processor = RecordingOutputProcessor()
|
||||
client = SimpleNamespace(stage_type="llm", abort_requests_async=abort)
|
||||
pool = StagePool(0, [client], output_processor=output_processor) # type: ignore[arg-type]
|
||||
pool._request_bindings["req-1"] = 0
|
||||
|
||||
with pytest.raises(RuntimeError, match="engine abort failed"):
|
||||
await pool.abort_requests(["req-1"])
|
||||
assert output_processor.collected is True
|
||||
assert output_processor.committed is False
|
||||
abort.assert_awaited_once()
|
||||
|
||||
asyncio.run(run())
|
||||
Reference in new issue
Block a user