[BugFix]Add MiniCPM-o 4.5 end-to-end test suite (offline, online serving) (#5237)
Signed-off-by: y_null <y_null@qq.com> Signed-off-by: amy-why-3459 <wuhaiyan17@huawei.com> Co-authored-by: amy-why-3459 <wuhaiyan17@huawei.com> Co-authored-by: Hongsheng Liu <liuhongsheng4@huawei.com>
This commit is contained in:
27 files changed
+1338
-46
No files matched your search
@@ -315,6 +315,86 @@ def test_empty_final_ignores_generation_scheduler_placeholder_token():
|
||||
assert model._states == {}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"info",
|
||||
[
|
||||
# The runner injects the engine request id on every step (GPU
|
||||
# _preprocess, NPU _gather_runtime_additional_information)...
|
||||
{"request_id": "a", "meta": {"request_id": "a"}},
|
||||
# ...but a pre-warm step can also reach the model with nothing at all.
|
||||
{},
|
||||
],
|
||||
)
|
||||
def test_prewarm_placeholder_step_emits_silence_without_touching_state(info):
|
||||
# async-chunk pre-warm submits Stage 2 with a reserved placeholder prompt.
|
||||
# If it gets scheduled before the first codec window lands, those reserved
|
||||
# tokens must neither be vocoded nor held to the codec payload contract.
|
||||
model, token2wav = _model()
|
||||
|
||||
output = _forward(model, [info], request_ids=["a"])
|
||||
|
||||
assert output.multimodal_outputs["model_outputs"][0].numel() == 0
|
||||
assert model._states == {}
|
||||
assert token2wav.hift.calls == []
|
||||
|
||||
|
||||
def test_metadata_only_payload_still_decodes_codec_from_prompt_tokens():
|
||||
# The connector strips 1-D codec tensors out of additional_information and
|
||||
# leaves them in the prompt tokens, so a real chunk reaches the model as
|
||||
# producer metadata plus input ids. It must still be vocoded.
|
||||
model, _ = _model()
|
||||
info = {
|
||||
"request_id": "a",
|
||||
"meta": {
|
||||
"request_id": "a",
|
||||
"chunk_seq": 0,
|
||||
"code_flat_numel": 2,
|
||||
"prompt_cache_id": "shared",
|
||||
},
|
||||
}
|
||||
|
||||
output = _forward(model, [info], placeholder_counts=[2])
|
||||
|
||||
assert output.multimodal_outputs["model_outputs"][0].numel() > 0
|
||||
assert set(model._states) == {"a"}
|
||||
|
||||
|
||||
def test_non_final_chunk_shorter_than_lookahead_window_is_rejected():
|
||||
token2wav = _FakeToken2Wav()
|
||||
token2wav.flow.encoder.pre_lookahead_layer = SimpleNamespace(pre_lookahead_len=3)
|
||||
adapter = BatchedToken2Wav(token2wav)
|
||||
prompt = adapter.prepare_prompt("shared", "/fake/prompt.wav")
|
||||
states = adapter.setup_batch(prompt, 1)
|
||||
|
||||
with pytest.raises(RuntimeError, match="chunk_below_lookahead_window"):
|
||||
adapter.decode_batch(torch.tensor([[10]]), prompt, states, last_chunk=False)
|
||||
|
||||
# The final chunk is zero-padded by the encoder, so it stays decodable.
|
||||
audios, _ = adapter.decode_batch(torch.tensor([[10]]), prompt, states, last_chunk=True)
|
||||
assert len(audios) == 1
|
||||
|
||||
|
||||
def test_forward_builds_backend_when_weight_loading_was_skipped(monkeypatch):
|
||||
# load_format=dummy never calls load_weights(), so Stage 2 would otherwise
|
||||
# reach its first request with no Token2wav assets at all.
|
||||
model = MiniCPMO45Code2Wav(vllm_config=_config())
|
||||
token2wav = _FakeToken2Wav()
|
||||
builds = 0
|
||||
|
||||
def build_backend():
|
||||
nonlocal builds
|
||||
builds += 1
|
||||
model.backend = BatchedToken2Wav(token2wav)
|
||||
|
||||
monkeypatch.setattr(model, "_build_backend", build_backend)
|
||||
|
||||
output = _forward(model, [_info("a", 0, [10, 11])])
|
||||
_forward(model, [_info("a", 1, [12, 13])])
|
||||
|
||||
assert builds == 1
|
||||
assert output.multimodal_outputs["model_outputs"][0].numel() > 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("info", "reason"),
|
||||
[
|
||||
|
||||
@@ -236,6 +236,38 @@ def test_missing_conditioning_fails_clearly() -> None:
|
||||
)
|
||||
|
||||
|
||||
def test_empty_speech_segment_finishes_without_sampling_codes() -> None:
|
||||
talker = _make_talker()
|
||||
talker.emb_text = nn.Embedding(8, 4)
|
||||
talker.emb_code = nn.ModuleList([nn.Embedding(8, 4)])
|
||||
talker._text_eos_id = 5
|
||||
talker._tts_bos_id = 6
|
||||
|
||||
_, embeds, updates = talker.preprocess(
|
||||
torch.zeros(2, dtype=torch.long),
|
||||
None,
|
||||
_omni_is_prefill=True,
|
||||
request_id="req-empty",
|
||||
tts_token_ids=torch.empty(0, dtype=torch.long),
|
||||
tts_hidden_states=torch.empty(0, 4),
|
||||
)
|
||||
|
||||
assert torch.equal(embeds, talker.emb_text(torch.tensor([5, 6])))
|
||||
assert updates["audio_state"]["finished"] is True
|
||||
|
||||
# Stage 1's sampling min_tokens keeps scheduling decode steps until the stop
|
||||
# token becomes eligible, and those steps have no previous code to embed.
|
||||
_, decode_embeds, _ = talker.preprocess(
|
||||
torch.zeros(1, dtype=torch.long),
|
||||
None,
|
||||
request_id="req-empty",
|
||||
audio_state=updates["audio_state"],
|
||||
audio_codes=updates["audio_codes"],
|
||||
)
|
||||
|
||||
assert decode_embeds.shape == (1, 4)
|
||||
|
||||
|
||||
def test_request_cleanup_evicts_ar_rng_and_decode_state() -> None:
|
||||
talker = _make_talker()
|
||||
talker._request_generators["req-done"] = torch.Generator()
|
||||
|
||||
Reference in new issue
Block a user