[Bugfix][MiniCPM-o] Bound full-duplex Talker context with sliding recompute (#6626)
Signed-off-by: natureofnature <wzliu@connect.hku.hk>
This commit is contained in:
1 parent
3adee400a0
commit
87736a3cc5
14 files changed
+1389
-72
No files matched your search
@@ -57,6 +57,19 @@ def _req(req_id: str, status: RequestStatus, external_req_id: str | None = None)
|
||||
return request
|
||||
|
||||
|
||||
def _streaming_request(mocker: MockerFixture, num_computed_tokens: int) -> SimpleNamespace:
|
||||
prompt_token_ids = [0] * num_computed_tokens
|
||||
return SimpleNamespace(
|
||||
_all_token_ids=prompt_token_ids.copy(),
|
||||
_output_token_ids=[],
|
||||
prompt_token_ids=prompt_token_ids,
|
||||
num_computed_tokens=num_computed_tokens,
|
||||
num_prompt_tokens=num_computed_tokens,
|
||||
num_output_placeholders=0,
|
||||
update_block_hashes=mocker.Mock(),
|
||||
)
|
||||
|
||||
|
||||
def test_streaming_payload_can_replace_placeholder_prompt(mocker: MockerFixture) -> None:
|
||||
request = SimpleNamespace(
|
||||
_all_token_ids=[0, 0, 7, 8],
|
||||
@@ -84,30 +97,219 @@ def test_streaming_payload_can_replace_placeholder_prompt(mocker: MockerFixture)
|
||||
request.update_block_hashes.assert_called_once_with()
|
||||
|
||||
|
||||
def test_streaming_payload_can_append_exact_prompt_length(mocker: MockerFixture) -> None:
|
||||
def test_turn_start_replacement_ignores_accumulated_prompt_capacity(mocker: MockerFixture) -> None:
|
||||
request = _streaming_request(mocker, num_computed_tokens=4064)
|
||||
payload = {
|
||||
"ids": {"prompt": [1]},
|
||||
"meta": {
|
||||
"replace_streaming_prompt": True,
|
||||
"next_stage_prompt_len": 10,
|
||||
"next_stage_generation_tokens": 26,
|
||||
},
|
||||
}
|
||||
|
||||
replaced = construct_next_stage_streaming_input_prompt(
|
||||
payload,
|
||||
request,
|
||||
max_model_len=4096,
|
||||
previous_condition_len=10,
|
||||
previous_condition_seq=7,
|
||||
condition_seq=8,
|
||||
recompute_previous_chunks=1,
|
||||
)
|
||||
|
||||
assert replaced is True
|
||||
assert request.prompt_token_ids == [0] * 10
|
||||
assert payload["meta"]["streaming_prompt_recompute"] is False
|
||||
assert "streaming_prompt_previous_codes" not in payload["ids"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("reserve", [True, 0, -1, 1.5, "26", None])
|
||||
def test_streaming_prompt_rejects_invalid_generation_reserve(
|
||||
mocker: MockerFixture,
|
||||
reserve: object,
|
||||
) -> None:
|
||||
request = _streaming_request(mocker, num_computed_tokens=10)
|
||||
payload = {
|
||||
"ids": {"prompt": [1]},
|
||||
"meta": {
|
||||
"next_stage_prompt_len": 10,
|
||||
"next_stage_generation_tokens": reserve,
|
||||
},
|
||||
}
|
||||
|
||||
with pytest.raises(ValueError, match="next_stage_generation_tokens must be a positive integer"):
|
||||
construct_next_stage_streaming_input_prompt(payload, request, max_model_len=4096)
|
||||
|
||||
|
||||
def test_capacity_managed_streaming_prompt_at_limit_still_appends(mocker: MockerFixture) -> None:
|
||||
request = _streaming_request(mocker, num_computed_tokens=4060)
|
||||
payload = {
|
||||
"ids": {"prompt": [1]},
|
||||
"meta": {
|
||||
"next_stage_prompt_len": 10,
|
||||
"next_stage_generation_tokens": 26,
|
||||
},
|
||||
}
|
||||
|
||||
replaced = construct_next_stage_streaming_input_prompt(payload, request, max_model_len=4096)
|
||||
|
||||
assert replaced is False
|
||||
assert request.num_computed_tokens == 4060
|
||||
assert request.num_prompt_tokens == 4070
|
||||
|
||||
|
||||
def test_streaming_window_builds_one_chunk_recompute_recipe(mocker: MockerFixture) -> None:
|
||||
previous_condition_len = 10
|
||||
previous_codes = list(range(25))
|
||||
request = SimpleNamespace(
|
||||
_all_token_ids=[0, 0, 7, 8],
|
||||
_output_token_ids=[7, 8],
|
||||
prompt_token_ids=[0, 0],
|
||||
num_computed_tokens=4,
|
||||
num_prompt_tokens=2,
|
||||
_all_token_ids=[0] * 4039 + previous_codes + [999],
|
||||
_output_token_ids=previous_codes + [999],
|
||||
prompt_token_ids=[0] * 4039,
|
||||
num_computed_tokens=4065,
|
||||
num_prompt_tokens=4039,
|
||||
num_output_placeholders=1,
|
||||
update_block_hashes=mocker.Mock(),
|
||||
)
|
||||
payload = {
|
||||
"ids": {"prompt": [1, 2, 3]},
|
||||
"meta": {"next_stage_prompt_len": 3},
|
||||
"ids": {"prompt": [1]},
|
||||
"meta": {
|
||||
"next_stage_prompt_len": 10,
|
||||
"next_stage_generation_tokens": 26,
|
||||
},
|
||||
}
|
||||
|
||||
construct_next_stage_streaming_input_prompt(payload, request)
|
||||
replaced = construct_next_stage_streaming_input_prompt(
|
||||
payload,
|
||||
request,
|
||||
max_model_len=4096,
|
||||
previous_condition_len=previous_condition_len,
|
||||
previous_condition_seq=7,
|
||||
condition_seq=8,
|
||||
recompute_previous_chunks=1,
|
||||
)
|
||||
|
||||
assert request.prompt_token_ids == [0, 0, 7, 8, 0, 0, 0]
|
||||
assert request._all_token_ids == [0, 0, 7, 8, 0, 0, 0]
|
||||
assert request._output_token_ids == []
|
||||
assert request.num_computed_tokens == 4
|
||||
assert request.num_prompt_tokens == 7
|
||||
assert replaced is True
|
||||
assert request.prompt_token_ids == [0] * 45
|
||||
assert request.num_prompt_tokens == 45
|
||||
assert request.num_computed_tokens == 0
|
||||
assert payload["ids"]["streaming_prompt_previous_codes"] == previous_codes
|
||||
assert payload["meta"] == {
|
||||
"next_stage_prompt_len": 10,
|
||||
"next_stage_generation_tokens": 26,
|
||||
"streaming_prompt_recompute": True,
|
||||
"streaming_condition_seq": 8,
|
||||
}
|
||||
request.update_block_hashes.assert_called_once_with()
|
||||
|
||||
|
||||
def test_streaming_window_recomputes_each_condition_without_accumulating(mocker: MockerFixture) -> None:
|
||||
request = SimpleNamespace(
|
||||
_all_token_ids=[0] * 10 + [101, 102],
|
||||
_output_token_ids=[101, 102],
|
||||
prompt_token_ids=[0] * 10,
|
||||
num_computed_tokens=12,
|
||||
num_prompt_tokens=10,
|
||||
num_output_placeholders=0,
|
||||
update_block_hashes=mocker.Mock(),
|
||||
)
|
||||
second = {
|
||||
"ids": {"prompt": [1]},
|
||||
"meta": {
|
||||
"next_stage_prompt_len": 12,
|
||||
"next_stage_generation_tokens": 26,
|
||||
},
|
||||
}
|
||||
|
||||
assert construct_next_stage_streaming_input_prompt(
|
||||
second,
|
||||
request,
|
||||
max_model_len=4096,
|
||||
previous_condition_len=10,
|
||||
previous_condition_seq=0,
|
||||
condition_seq=1,
|
||||
recompute_previous_chunks=1,
|
||||
)
|
||||
assert request.num_prompt_tokens == 24
|
||||
assert second["ids"]["streaming_prompt_previous_codes"] == [101, 102]
|
||||
|
||||
request._all_token_ids.extend([201, 202, 203])
|
||||
request._output_token_ids.extend([201, 202, 203])
|
||||
request.num_computed_tokens = 27
|
||||
third = {
|
||||
"ids": {"prompt": [2]},
|
||||
"meta": {
|
||||
"next_stage_prompt_len": 8,
|
||||
"next_stage_generation_tokens": 26,
|
||||
},
|
||||
}
|
||||
|
||||
assert construct_next_stage_streaming_input_prompt(
|
||||
third,
|
||||
request,
|
||||
max_model_len=4096,
|
||||
previous_condition_len=12,
|
||||
previous_condition_seq=1,
|
||||
condition_seq=2,
|
||||
recompute_previous_chunks=1,
|
||||
)
|
||||
assert request.num_prompt_tokens == 23
|
||||
assert third["ids"]["streaming_prompt_previous_codes"] == [201, 202, 203]
|
||||
assert third["meta"]["streaming_condition_seq"] == 2
|
||||
|
||||
|
||||
def test_capacity_rollover_requires_explicit_window_contract(mocker: MockerFixture) -> None:
|
||||
request = _streaming_request(mocker, num_computed_tokens=4064)
|
||||
payload = {
|
||||
"ids": {"prompt": [1]},
|
||||
"meta": {
|
||||
"next_stage_prompt_len": 10,
|
||||
"next_stage_generation_tokens": 26,
|
||||
},
|
||||
}
|
||||
|
||||
with pytest.raises(ValueError, match="requires window_size=1"):
|
||||
construct_next_stage_streaming_input_prompt(
|
||||
payload,
|
||||
request,
|
||||
max_model_len=4096,
|
||||
previous_condition_len=10,
|
||||
previous_condition_seq=0,
|
||||
condition_seq=1,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("next_stage_prompt_len", [0, -1])
|
||||
def test_capacity_managed_streaming_prompt_rejects_nonpositive_length(
|
||||
mocker: MockerFixture, next_stage_prompt_len: int
|
||||
) -> None:
|
||||
request = _streaming_request(mocker, num_computed_tokens=0)
|
||||
payload = {
|
||||
"ids": {"prompt": [1]},
|
||||
"meta": {
|
||||
"next_stage_prompt_len": next_stage_prompt_len,
|
||||
"next_stage_generation_tokens": 26,
|
||||
},
|
||||
}
|
||||
|
||||
with pytest.raises(ValueError, match="positive next_stage_prompt_len"):
|
||||
construct_next_stage_streaming_input_prompt(payload, request, max_model_len=4096)
|
||||
|
||||
|
||||
def test_capacity_managed_streaming_prompt_rejects_fresh_overflow(mocker: MockerFixture) -> None:
|
||||
request = _streaming_request(mocker, num_computed_tokens=0)
|
||||
payload = {
|
||||
"ids": {"prompt": [1]},
|
||||
"meta": {
|
||||
"next_stage_prompt_len": 4071,
|
||||
"next_stage_generation_tokens": 26,
|
||||
},
|
||||
}
|
||||
|
||||
with pytest.raises(ValueError, match="exceeds max_model_len"):
|
||||
construct_next_stage_streaming_input_prompt(payload, request, max_model_len=4096)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def build_adapter(monkeypatch, mocker: MockerFixture):
|
||||
def _build(
|
||||
@@ -115,6 +317,10 @@ def build_adapter(monkeypatch, mocker: MockerFixture):
|
||||
stage_id: int = 1,
|
||||
model_mode: str = "ar",
|
||||
max_num_seqs: int = 2,
|
||||
max_model_len: int = 0,
|
||||
tts_max_model_len: int = 0,
|
||||
tts_attention_type: str = "full_attention",
|
||||
flat_tts_config: bool = False,
|
||||
active_stream_window: int = 0,
|
||||
connector_extra: dict | None = None,
|
||||
):
|
||||
@@ -131,6 +337,10 @@ def build_adapter(monkeypatch, mocker: MockerFixture):
|
||||
self._cancelled_load_reqs = set()
|
||||
self._pending_save_reqs = deque()
|
||||
self._finished_save_reqs = set()
|
||||
self._send_failures = {}
|
||||
self._send_failure_lock = threading.Lock()
|
||||
self._receive_failures = {}
|
||||
self._receive_failure_lock = threading.Lock()
|
||||
self.stop_event = threading.Event()
|
||||
self._recv_cond = threading.Condition()
|
||||
self._save_cond = threading.Condition()
|
||||
@@ -142,9 +352,23 @@ def build_adapter(monkeypatch, mocker: MockerFixture):
|
||||
classmethod(lambda cls, _model_config: connector),
|
||||
)
|
||||
|
||||
hf_config = SimpleNamespace(
|
||||
model_type="minicpmtts",
|
||||
max_position_embeddings=tts_max_model_len,
|
||||
attention_type=tts_attention_type,
|
||||
)
|
||||
if not flat_tts_config:
|
||||
hf_config = SimpleNamespace(
|
||||
tts_config=SimpleNamespace(
|
||||
max_position_embeddings=tts_max_model_len,
|
||||
attention_type=tts_attention_type,
|
||||
)
|
||||
)
|
||||
model_config = SimpleNamespace(
|
||||
worker_type=model_mode,
|
||||
max_num_seqs=max_num_seqs,
|
||||
max_model_len=max_model_len,
|
||||
hf_config=hf_config,
|
||||
active_stream_window=active_stream_window,
|
||||
stage_connector_config={
|
||||
"name": "SharedMemoryConnector",
|
||||
@@ -160,6 +384,27 @@ def build_adapter(monkeypatch, mocker: MockerFixture):
|
||||
return _build
|
||||
|
||||
|
||||
def _dequeue_load_entry(adapter, request):
|
||||
"""Register and take the entry as the background recv loop would."""
|
||||
adapter.load_async(request)
|
||||
entry = adapter._registered_load_entries[request.request_id]
|
||||
try:
|
||||
adapter._pending_load_reqs.remove(entry)
|
||||
except ValueError:
|
||||
pass
|
||||
return entry
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("attention_type", "expected_previous_chunks"),
|
||||
[("full_attention", 0), ("sliding_recompute", 1)],
|
||||
)
|
||||
def test_talker_attention_policy_controls_streaming_recompute(build_adapter, attention_type, expected_previous_chunks):
|
||||
adapter, _ = build_adapter(tts_attention_type=attention_type)
|
||||
|
||||
assert adapter._streaming_prompt_previous_chunks == expected_previous_chunks
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("raw_cfg", "expected_name", "expected_extra"),
|
||||
[
|
||||
@@ -200,15 +445,78 @@ def test_load_poll(build_adapter):
|
||||
"meta": {"finished": torch.tensor(True, dtype=torch.bool)},
|
||||
}
|
||||
connector.get.return_value = (payload, 16)
|
||||
adapter._poll_single_request(request)
|
||||
adapter._poll_single_request(_dequeue_load_entry(adapter, request))
|
||||
|
||||
assert request.additional_information is None
|
||||
assert "req-1" in adapter._finished_load_reqs
|
||||
request.status = RequestStatus.WAITING_FOR_CHUNK
|
||||
adapter.process_pending_chunks(
|
||||
DummyWaitingQueue(),
|
||||
[request],
|
||||
scheduler_requests={request.request_id: request},
|
||||
)
|
||||
assert request.additional_information == payload
|
||||
assert adapter.get_req_chunk["req-1"] == 1
|
||||
assert "req-1" in adapter._finished_load_reqs
|
||||
assert "req-1" not in adapter._finished_load_reqs
|
||||
assert "req-1" in adapter.upstream_exhausted_requests
|
||||
assert "req-1" not in adapter._pending_load_reqs
|
||||
|
||||
|
||||
def test_load_async_does_not_requeue_registered_inflight_request(build_adapter):
|
||||
adapter, connector = build_adapter(stage_id=1, model_mode="ar")
|
||||
request = _req("req-inflight", RequestStatus.RUNNING, external_req_id="ext-inflight")
|
||||
|
||||
entry = _dequeue_load_entry(adapter, request)
|
||||
|
||||
# The recv loop owns the popped request until polling completes. A
|
||||
# scheduler-side duplicate registration must not append a second copy.
|
||||
adapter.load_async(request)
|
||||
assert adapter._pending_load_reqs == deque()
|
||||
|
||||
connector.get.return_value = (
|
||||
{
|
||||
"ids": {"prompt": [1]},
|
||||
"meta": {"finished": True},
|
||||
},
|
||||
1,
|
||||
)
|
||||
assert adapter._poll_single_request(entry) is True
|
||||
|
||||
# Successful polling releases the registration for the next chunk.
|
||||
adapter.load_async(request)
|
||||
assert [queued.request for queued in adapter._pending_load_reqs] == [request]
|
||||
|
||||
|
||||
def test_cleanup_then_reregister_drops_stale_queued_entry(build_adapter):
|
||||
adapter, connector = build_adapter(stage_id=1, model_mode="ar")
|
||||
request = _req("req-resume", RequestStatus.WAITING, external_req_id="ext-resume")
|
||||
|
||||
adapter.load_async(request)
|
||||
stale_entry = adapter._pending_load_reqs[0]
|
||||
adapter.cleanup_receiver(request.request_id)
|
||||
adapter.load_async(request)
|
||||
fresh_entry = adapter._pending_load_reqs[-1]
|
||||
assert len(adapter._pending_load_reqs) == 2
|
||||
assert adapter._pending_load_reqs.popleft() is stale_entry
|
||||
assert adapter._pending_load_reqs.popleft() is fresh_entry
|
||||
|
||||
connector.get.return_value = (
|
||||
{
|
||||
"ids": {"prompt": [1]},
|
||||
"meta": {"finished": True},
|
||||
},
|
||||
1,
|
||||
)
|
||||
assert adapter._poll_single_request(stale_entry) is True
|
||||
connector.get.assert_not_called()
|
||||
assert adapter._registered_load_entries[request.request_id] is fresh_entry
|
||||
|
||||
assert adapter._poll_single_request(fresh_entry) is True
|
||||
connector.get.assert_called_once()
|
||||
assert request.request_id not in adapter._registered_load_entries
|
||||
assert adapter.get_req_chunk[request.request_id] == 1
|
||||
|
||||
|
||||
def test_load_poll_ar_requeues_explicitly_replaced_running_prompt(build_adapter):
|
||||
adapter, connector = build_adapter(stage_id=1, model_mode="ar")
|
||||
request = _req("req-replace", RequestStatus.RUNNING, external_req_id="external-replace")
|
||||
@@ -234,15 +542,18 @@ def test_load_poll_ar_requeues_explicitly_replaced_running_prompt(build_adapter)
|
||||
1,
|
||||
)
|
||||
|
||||
assert adapter._poll_single_request(request) is True
|
||||
assert request.num_computed_tokens == 0
|
||||
assert request.prompt_token_ids == [0] * 10
|
||||
assert request.request_id in adapter.replaced_streaming_prompt_ids
|
||||
|
||||
assert adapter._poll_single_request(_dequeue_load_entry(adapter, request)) is True
|
||||
request.status = RequestStatus.WAITING_FOR_CHUNK
|
||||
running_queue = [request]
|
||||
waiting_queue = DummyWaitingQueue()
|
||||
adapter.process_pending_chunks(waiting_queue, running_queue)
|
||||
adapter.process_pending_chunks(
|
||||
waiting_queue,
|
||||
running_queue,
|
||||
scheduler_requests={request.request_id: request},
|
||||
)
|
||||
assert request.num_computed_tokens == 0
|
||||
assert request.prompt_token_ids == [0] * 10
|
||||
assert request.request_id in adapter.replaced_streaming_prompt_ids
|
||||
assert running_queue == []
|
||||
assert waiting_queue == [request]
|
||||
assert request.status == RequestStatus.WAITING
|
||||
@@ -258,6 +569,134 @@ def test_load_poll_ar_requeues_explicitly_replaced_running_prompt(build_adapter)
|
||||
assert request.external_req_id not in adapter.requests_num_chunks_sent
|
||||
|
||||
|
||||
@pytest.mark.parametrize("flat_tts_config", [False, True])
|
||||
def test_load_poll_ar_recomputes_native_duplex_prompt_each_condition(build_adapter, flat_tts_config):
|
||||
adapter, connector = build_adapter(
|
||||
stage_id=1,
|
||||
model_mode="ar",
|
||||
max_model_len=8192,
|
||||
tts_max_model_len=4096,
|
||||
tts_attention_type="sliding_recompute",
|
||||
flat_tts_config=flat_tts_config,
|
||||
)
|
||||
request = _req("req-rollover", RequestStatus.RUNNING, external_req_id="external-rollover")
|
||||
request.resumable = True
|
||||
previous_codes = list(range(25))
|
||||
request.prompt_token_ids = [0] * 10
|
||||
request._all_token_ids = [0] * 10 + previous_codes
|
||||
request._output_token_ids = previous_codes.copy()
|
||||
request.num_prompt_tokens = 10
|
||||
request.num_computed_tokens = 35
|
||||
request.num_output_placeholders = 0
|
||||
request.update_block_hashes = Mock()
|
||||
adapter.get_req_chunk[request.request_id] = 1
|
||||
adapter.request_ids_mapping[request.request_id] = request.external_req_id
|
||||
adapter._streaming_condition_lengths[request.request_id] = 10
|
||||
adapter._streaming_condition_seqs[request.request_id] = 0
|
||||
connector.get.return_value = (
|
||||
{
|
||||
"ids": {"prompt": [1]},
|
||||
"meta": {
|
||||
"next_stage_prompt_len": 10,
|
||||
"next_stage_generation_tokens": 26,
|
||||
"finished": True,
|
||||
},
|
||||
},
|
||||
1,
|
||||
)
|
||||
|
||||
assert adapter._poll_single_request(_dequeue_load_entry(adapter, request)) is True
|
||||
request.status = RequestStatus.WAITING_FOR_CHUNK
|
||||
running_queue = [request]
|
||||
waiting_queue = DummyWaitingQueue()
|
||||
adapter.process_pending_chunks(
|
||||
waiting_queue,
|
||||
running_queue,
|
||||
scheduler_requests={request.request_id: request},
|
||||
)
|
||||
|
||||
assert request.num_computed_tokens == 0
|
||||
assert request.prompt_token_ids == [0] * 45
|
||||
assert request.additional_information["ids"]["streaming_prompt_previous_codes"] == previous_codes
|
||||
assert request.additional_information["meta"]["streaming_prompt_recompute"] is True
|
||||
assert request.resumable is False
|
||||
assert request.request_id in adapter.replaced_streaming_prompt_ids
|
||||
assert adapter._max_model_len == 4096
|
||||
assert running_queue == []
|
||||
assert waiting_queue == [request]
|
||||
|
||||
|
||||
def test_window_condition_sequence_ignores_control_only_chunks(build_adapter) -> None:
|
||||
adapter, connector = build_adapter(
|
||||
stage_id=1,
|
||||
model_mode="ar",
|
||||
tts_attention_type="sliding_recompute",
|
||||
)
|
||||
request = _req("req-condition-seq", RequestStatus.RUNNING)
|
||||
|
||||
def receive(payload):
|
||||
connector.get.return_value = (payload, 1)
|
||||
assert adapter._poll_single_request(_dequeue_load_entry(adapter, request)) is True
|
||||
adapter.requests_with_ready_chunks.add(request.request_id)
|
||||
adapter._apply_pending_ar_prompt_updates({request.request_id: request})
|
||||
adapter.requests_with_ready_chunks.discard(request.request_id)
|
||||
return request.additional_information
|
||||
|
||||
first = receive(
|
||||
{
|
||||
"ids": {"prompt": [1]},
|
||||
"meta": {"next_stage_prompt_len": 10},
|
||||
}
|
||||
)
|
||||
assert first["meta"]["streaming_condition_seq"] == 0
|
||||
assert adapter._streaming_condition_seqs[request.request_id] == 0
|
||||
|
||||
control = receive({"meta": {"is_segment_finished": True}})
|
||||
assert "streaming_condition_seq" not in control["meta"]
|
||||
assert adapter._streaming_condition_seqs[request.request_id] == 0
|
||||
|
||||
second = receive(
|
||||
{
|
||||
"ids": {"prompt": [2]},
|
||||
"meta": {"next_stage_prompt_len": 12},
|
||||
}
|
||||
)
|
||||
assert second["meta"]["streaming_condition_seq"] == 1
|
||||
assert adapter._streaming_condition_lengths[request.request_id] == 12
|
||||
|
||||
|
||||
def test_load_poll_ar_reports_invalid_capacity_metadata(build_adapter):
|
||||
adapter, connector = build_adapter(stage_id=1, model_mode="ar", max_model_len=4096)
|
||||
request = _req("req-invalid-capacity", RequestStatus.RUNNING)
|
||||
request.resumable = True
|
||||
adapter.get_req_chunk[request.request_id] = 1
|
||||
connector.get.return_value = (
|
||||
{
|
||||
"ids": {"prompt": [1]},
|
||||
"meta": {
|
||||
"next_stage_prompt_len": 4096,
|
||||
"next_stage_generation_tokens": 26,
|
||||
"finished": False,
|
||||
},
|
||||
},
|
||||
1,
|
||||
)
|
||||
|
||||
assert adapter._poll_single_request(_dequeue_load_entry(adapter, request)) is True
|
||||
request.status = RequestStatus.WAITING_FOR_CHUNK
|
||||
running_queue = [request]
|
||||
adapter.process_pending_chunks(
|
||||
DummyWaitingQueue(),
|
||||
running_queue,
|
||||
scheduler_requests={request.request_id: request},
|
||||
)
|
||||
assert adapter.collect_failed_receive_request_ids() == {
|
||||
request.request_id: "fresh streaming prompt plus generation reserve exceeds max_model_len: "
|
||||
"prompt=4096, reserve=26, limit=4096"
|
||||
}
|
||||
assert request.request_id not in adapter._finished_load_reqs
|
||||
|
||||
|
||||
def test_load_poll_generation_tensor_codes_use_placeholder_prompt(build_adapter):
|
||||
adapter, connector = build_adapter(stage_id=1, model_mode="generation")
|
||||
request = _req("req-tensor", RequestStatus.WAITING, external_req_id="external-tensor")
|
||||
@@ -272,7 +711,7 @@ def test_load_poll_generation_tensor_codes_use_placeholder_prompt(build_adapter)
|
||||
}
|
||||
connector.get.return_value = (payload, 16)
|
||||
|
||||
adapter._poll_single_request(request)
|
||||
adapter._poll_single_request(_dequeue_load_entry(adapter, request))
|
||||
|
||||
assert request.prompt_token_ids == [0]
|
||||
assert request.num_computed_tokens == 0
|
||||
@@ -302,12 +741,13 @@ def test_load_poll_generation_empty_nonterminal_chunk_keeps_polling(build_adapte
|
||||
}
|
||||
connector.get.side_effect = [(empty_payload, 16), (ready_payload, 16)]
|
||||
|
||||
assert adapter._poll_single_request(request) is False
|
||||
entry = _dequeue_load_entry(adapter, request)
|
||||
assert adapter._poll_single_request(entry) is False
|
||||
assert request.request_id not in adapter._finished_load_reqs
|
||||
assert request.request_id not in adapter.requests_with_ready_chunks
|
||||
assert adapter.get_req_chunk[request.request_id] == 1
|
||||
|
||||
assert adapter._poll_single_request(request) is True
|
||||
assert adapter._poll_single_request(entry) is True
|
||||
assert request.request_id in adapter._finished_load_reqs
|
||||
assert torch.equal(request.additional_information["codes"]["audio"], ready_payload["codes"]["audio"])
|
||||
assert adapter.get_req_chunk[request.request_id] == 2
|
||||
@@ -701,6 +1141,77 @@ def test_send_single_request_cleans_up_after_finished_payload(build_adapter, mon
|
||||
assert args[1] == "ext-finished"
|
||||
|
||||
|
||||
def test_old_turn_terminal_send_preserves_new_turn_state_and_sender(build_adapter):
|
||||
adapter, connector = build_adapter(
|
||||
stage_id=1,
|
||||
model_mode="ar",
|
||||
max_model_len=4096,
|
||||
tts_attention_type="sliding_recompute",
|
||||
)
|
||||
request = _req("req-live", RequestStatus.RUNNING, external_req_id="ext-live")
|
||||
request.resumable = True
|
||||
request._all_token_ids = []
|
||||
request._output_token_ids = []
|
||||
request.update_block_hashes = Mock()
|
||||
adapter.custom_process_next_stage_input_func = lambda **kwargs: OmniPayloadStruct(
|
||||
meta=MetaStruct(
|
||||
finished=torch.tensor(True, dtype=torch.bool),
|
||||
codec_streaming=True,
|
||||
)
|
||||
)
|
||||
|
||||
put_started = threading.Event()
|
||||
release_put = threading.Event()
|
||||
put_keys = []
|
||||
|
||||
def blocking_first_put(*_args, **kwargs):
|
||||
put_keys.append(kwargs["put_key"])
|
||||
if len(put_keys) == 1:
|
||||
put_started.set()
|
||||
assert release_put.wait(timeout=5)
|
||||
return True, 1, {}
|
||||
|
||||
connector.put.side_effect = blocking_first_put
|
||||
adapter.save_async(multimodal_output=None, request=request)
|
||||
first_task = adapter._pending_save_reqs.popleft()
|
||||
first_sender_token = first_task["sender_token"]
|
||||
send_thread = threading.Thread(target=lambda: adapter._send_single_request(first_task))
|
||||
send_thread.start()
|
||||
assert put_started.wait(timeout=5)
|
||||
|
||||
entry = _dequeue_load_entry(adapter, request)
|
||||
connector.get.return_value = (
|
||||
{
|
||||
"ids": {"prompt": [1]},
|
||||
"meta": {
|
||||
"replace_streaming_prompt": True,
|
||||
"next_stage_prompt_len": 10,
|
||||
"next_stage_generation_tokens": 26,
|
||||
},
|
||||
},
|
||||
1,
|
||||
)
|
||||
assert adapter._poll_single_request(entry) is True
|
||||
adapter.requests_with_ready_chunks.add(request.request_id)
|
||||
adapter._apply_pending_ar_prompt_updates({request.request_id: request})
|
||||
assert adapter._streaming_condition_seqs[request.request_id] == 0
|
||||
|
||||
request.num_computed_tokens = 1
|
||||
adapter.save_async(multimodal_output=None, request=request)
|
||||
second_task = adapter._pending_save_reqs.popleft()
|
||||
assert second_task["sender_token"] is first_sender_token
|
||||
|
||||
release_put.set()
|
||||
send_thread.join(timeout=5)
|
||||
assert not send_thread.is_alive()
|
||||
assert adapter._streaming_condition_seqs[request.request_id] == 0
|
||||
assert adapter._sender_tokens[request.external_req_id] is first_sender_token
|
||||
assert first_sender_token.cancelled is False
|
||||
|
||||
adapter._send_single_request(second_task)
|
||||
assert put_keys == ["ext-live_1_0", "ext-live_1_1"]
|
||||
|
||||
|
||||
def test_load_poll_non_ar_merges_into_existing_additional_information(build_adapter):
|
||||
adapter, connector = build_adapter(stage_id=2, model_mode="diffusion")
|
||||
request = _req("req-non-ar", RequestStatus.WAITING, external_req_id="ext-non-ar")
|
||||
@@ -720,7 +1231,7 @@ def test_load_poll_non_ar_merges_into_existing_additional_information(build_adap
|
||||
}
|
||||
connector.get.return_value = (payload, 8)
|
||||
|
||||
assert adapter._poll_single_request(request) is True
|
||||
assert adapter._poll_single_request(_dequeue_load_entry(adapter, request)) is True
|
||||
|
||||
assert request.prompt_token_ids == [7, 8]
|
||||
assert request.num_computed_tokens == 0
|
||||
@@ -761,7 +1272,7 @@ def test_load_poll_generation_segment_marker_replaces_previous_chunk(build_adapt
|
||||
1,
|
||||
)
|
||||
|
||||
assert adapter._poll_single_request(request) is True
|
||||
assert adapter._poll_single_request(_dequeue_load_entry(adapter, request)) is True
|
||||
|
||||
assert request.prompt_token_ids == [7, 8]
|
||||
assert "audio" not in request.additional_information["codes"]
|
||||
@@ -790,7 +1301,7 @@ def test_load_poll_generation_empty_replacement_snapshot_is_ready(build_adapter)
|
||||
1,
|
||||
)
|
||||
|
||||
assert adapter._poll_single_request(request) is True
|
||||
assert adapter._poll_single_request(_dequeue_load_entry(adapter, request)) is True
|
||||
|
||||
assert request.prompt_token_ids == [0]
|
||||
assert "codes" not in request.additional_information
|
||||
@@ -816,7 +1327,7 @@ def test_load_poll_generation_without_snapshot_marker_keeps_incremental_state(bu
|
||||
1,
|
||||
)
|
||||
|
||||
assert adapter._poll_single_request(request) is False
|
||||
assert adapter._poll_single_request(_dequeue_load_entry(adapter, request)) is False
|
||||
|
||||
assert torch.equal(request.additional_information["codes"]["audio"], torch.tensor([1, 2]))
|
||||
assert request.additional_information["meta"]["cache_epoch"] == 3
|
||||
@@ -840,9 +1351,17 @@ def test_load_poll_ar_request_additional_information_concats_tensors(build_adapt
|
||||
}
|
||||
connector.get.return_value = (payload, 8)
|
||||
|
||||
adapter._poll_single_request(request)
|
||||
adapter._poll_single_request(_dequeue_load_entry(adapter, request))
|
||||
|
||||
# AR mode now forwards the latest payload directly.
|
||||
# The recv thread records the latest payload; scheduler-side queue
|
||||
# restoration publishes it onto the Request.
|
||||
assert torch.equal(request.additional_information["hidden_states"]["output"], torch.tensor([[1.0]]))
|
||||
request.status = RequestStatus.WAITING_FOR_CHUNK
|
||||
adapter.process_pending_chunks(
|
||||
DummyWaitingQueue(),
|
||||
[request],
|
||||
scheduler_requests={request.request_id: request},
|
||||
)
|
||||
assert request.additional_information == payload
|
||||
assert request.additional_information["meta"]["finished"].item() is True
|
||||
|
||||
@@ -860,7 +1379,7 @@ def test_non_ar_poll_reinitializes_prefill_stats_for_later_chunks(build_adapter)
|
||||
},
|
||||
8,
|
||||
)
|
||||
assert adapter._poll_single_request(request) is True
|
||||
assert adapter._poll_single_request(_dequeue_load_entry(adapter, request)) is True
|
||||
OmniGenerationScheduler._record_prefill_stats(request)
|
||||
first_chunk_stats = request.prefill_stats
|
||||
request.prefill_stats = None
|
||||
@@ -872,7 +1391,7 @@ def test_non_ar_poll_reinitializes_prefill_stats_for_later_chunks(build_adapter)
|
||||
},
|
||||
8,
|
||||
)
|
||||
assert adapter._poll_single_request(request) is True
|
||||
assert adapter._poll_single_request(_dequeue_load_entry(adapter, request)) is True
|
||||
assert isinstance(request.prefill_stats, PrefillStats)
|
||||
OmniGenerationScheduler._record_prefill_stats(request)
|
||||
second_chunk_stats = request.prefill_stats
|
||||
@@ -971,7 +1490,7 @@ def test_non_active_waiting_request_is_held_off_scheduler(build_adapter):
|
||||
assert waiting_queue == []
|
||||
assert active.status == RequestStatus.WAITING_FOR_CHUNK
|
||||
assert non_active.status == RequestStatus.WAITING
|
||||
assert list(adapter._pending_load_reqs) == [active]
|
||||
assert [entry.request for entry in adapter._pending_load_reqs] == [active]
|
||||
assert list(adapter.waiting_for_chunk_waiting_requests) == [active, non_active]
|
||||
|
||||
|
||||
@@ -1122,8 +1641,19 @@ def _populate_adapter_state(adapter, req_id="req-1", ext_id="ext-1"):
|
||||
adapter.get_req_chunk[req_id] = 3
|
||||
adapter.requests_with_ready_chunks.add(req_id)
|
||||
adapter.request_ids_mapping[req_id] = ext_id
|
||||
adapter._pending_load_reqs.append(SimpleNamespace(request_id=req_id))
|
||||
entry = SimpleNamespace(request_id=req_id)
|
||||
adapter._pending_load_reqs.append(entry)
|
||||
adapter._registered_load_entries[req_id] = entry
|
||||
adapter._finished_load_reqs.add(req_id)
|
||||
adapter._pending_ar_prompt_updates[req_id] = (
|
||||
SimpleNamespace(request_id=req_id),
|
||||
{},
|
||||
False,
|
||||
False,
|
||||
False,
|
||||
)
|
||||
adapter._streaming_condition_lengths[req_id] = 10
|
||||
adapter._streaming_condition_seqs[req_id] = 2
|
||||
|
||||
adapter.put_req_chunk[ext_id] = 5
|
||||
adapter.request_payload[ext_id] = {"hidden": [1, 2]}
|
||||
@@ -1145,12 +1675,71 @@ def test_cleanup_clears_all_state(build_adapter):
|
||||
assert req_id not in adapter.request_ids_mapping
|
||||
assert req_id in adapter._cancelled_load_reqs
|
||||
assert req_id not in adapter._finished_load_reqs
|
||||
assert req_id not in adapter._pending_ar_prompt_updates
|
||||
assert req_id not in adapter._streaming_condition_lengths
|
||||
assert req_id not in adapter._streaming_condition_seqs
|
||||
assert req_id not in adapter._registered_load_entries
|
||||
|
||||
assert ext_id not in adapter.put_req_chunk
|
||||
assert ext_id not in adapter.request_payload
|
||||
assert ext_id not in adapter.code_prompt_token_ids
|
||||
|
||||
|
||||
def test_cleanup_and_reregister_discards_inflight_old_segment_chunk(build_adapter):
|
||||
"""A late old-segment chunk cannot overwrite a resumed registration."""
|
||||
adapter, connector = build_adapter(stage_id=1, model_mode="ar")
|
||||
request = _req("req-late", RequestStatus.RUNNING, external_req_id="ext-late")
|
||||
request.resumable = True
|
||||
entry = _dequeue_load_entry(adapter, request)
|
||||
|
||||
get_started = threading.Event()
|
||||
release_get = threading.Event()
|
||||
|
||||
def blocking_get(*_args):
|
||||
get_started.set()
|
||||
assert release_get.wait(timeout=5)
|
||||
return (
|
||||
{
|
||||
"ids": {"prompt": [1]},
|
||||
"meta": {
|
||||
"next_stage_prompt_len": 10,
|
||||
},
|
||||
},
|
||||
1,
|
||||
)
|
||||
|
||||
connector.get.side_effect = blocking_get
|
||||
poll_results = []
|
||||
poll_thread = threading.Thread(target=lambda: poll_results.append(adapter._poll_single_request(entry)))
|
||||
poll_thread.start()
|
||||
assert get_started.wait(timeout=5)
|
||||
|
||||
adapter.cleanup_receiver(request.request_id)
|
||||
fresh_entry = _dequeue_load_entry(adapter, request)
|
||||
release_get.set()
|
||||
poll_thread.join(timeout=5)
|
||||
|
||||
assert not poll_thread.is_alive()
|
||||
assert poll_results == [True]
|
||||
assert request.additional_information is None
|
||||
assert request.request_id not in adapter._pending_ar_prompt_updates
|
||||
assert request.request_id not in adapter._streaming_condition_seqs
|
||||
assert adapter.get_req_chunk[request.request_id] == 0
|
||||
assert adapter._registered_load_entries[request.request_id] is fresh_entry
|
||||
|
||||
connector.get.side_effect = None
|
||||
connector.get.return_value = (
|
||||
{
|
||||
"ids": {"prompt": [2]},
|
||||
"meta": {"finished": True},
|
||||
},
|
||||
1,
|
||||
)
|
||||
assert adapter._poll_single_request(fresh_entry) is True
|
||||
assert adapter.get_req_chunk[request.request_id] == 1
|
||||
assert request.request_id not in adapter._registered_load_entries
|
||||
|
||||
|
||||
def test_cleanup_infers_external_id(build_adapter):
|
||||
"""When external_req_id is None, cleanup should look it up from the mapping."""
|
||||
adapter, _ = build_adapter(stage_id=1)
|
||||
@@ -1299,7 +1888,7 @@ def test_cleanup_after_poll_flow(build_adapter):
|
||||
"meta": {"finished": torch.tensor(True, dtype=torch.bool)},
|
||||
}
|
||||
connector.get.return_value = (payload, 8)
|
||||
adapter._poll_single_request(request)
|
||||
adapter._poll_single_request(_dequeue_load_entry(adapter, request))
|
||||
|
||||
assert "req-flow" in adapter.upstream_exhausted_requests
|
||||
assert adapter.get_req_chunk["req-flow"] == 1
|
||||
|
||||
Reference in new issue
Block a user