[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:
authored and GitHub committed 2026-07-25 17:00:13 -07:00
1 parent 76e4a78afc
commit b6c4578dac
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()