[CI/Build][MiniCPM-o] Fix the Nightly tests (#7758)
Signed-off-by: chickeyton <ngton2014@gmail.com> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
aa2fadac42
commit
6df62d5157
8 files changed
+233
-18
No files matched your search
@@ -26,8 +26,8 @@
|
||||
{
|
||||
"dataset_name": "seed-tts",
|
||||
"dataset_path": "zhaochenyang20/seed-tts-eval",
|
||||
"backend": "openai-realtime-tts",
|
||||
"endpoint": "/v1/realtime",
|
||||
"backend": "openai-chat-omni",
|
||||
"endpoint": "/v1/chat/completions",
|
||||
"num_prompts": [
|
||||
32,
|
||||
64,
|
||||
@@ -42,7 +42,73 @@
|
||||
"disable_shuffle": true,
|
||||
"trust_remote_code": true,
|
||||
"seed_tts_locale": "en",
|
||||
"percentile-metrics": "ttft,e2el,audio_ttfp,audio_rtf,audio_duration"
|
||||
"extra_body": {
|
||||
"modalities": [
|
||||
"text",
|
||||
"audio"
|
||||
],
|
||||
"chat_template_kwargs": {
|
||||
"enable_thinking": false,
|
||||
"use_tts_template": true
|
||||
}
|
||||
},
|
||||
"percentile-metrics": "ttft,e2el,audio_ttfp,audio_rtf,audio_duration",
|
||||
"baseline": {
|
||||
"H100": {
|
||||
"request_throughput": [
|
||||
0.9731,
|
||||
1.1986,
|
||||
1.686
|
||||
],
|
||||
"mean_ttft_ms": [
|
||||
152.5665,
|
||||
270.2559,
|
||||
301.455
|
||||
],
|
||||
"mean_e2el_ms": [
|
||||
1027.2953,
|
||||
3306.3735,
|
||||
4686.5366
|
||||
],
|
||||
"mean_audio_ttfp_ms": [
|
||||
458.1553,
|
||||
2030.2335,
|
||||
2068.6233
|
||||
],
|
||||
"mean_audio_rtf": [
|
||||
0.2357,
|
||||
0.791,
|
||||
1.0324
|
||||
]
|
||||
},
|
||||
"A3": {
|
||||
"request_throughput": [
|
||||
0.5383,
|
||||
0.6042,
|
||||
0.7547
|
||||
],
|
||||
"mean_ttft_ms": [
|
||||
333.2633,
|
||||
533.8656,
|
||||
547.3388
|
||||
],
|
||||
"mean_e2el_ms": [
|
||||
1857.2154,
|
||||
6600.5101,
|
||||
10450.6811
|
||||
],
|
||||
"mean_audio_ttfp_ms": [
|
||||
986.4666,
|
||||
3411.0754,
|
||||
3352.7515
|
||||
],
|
||||
"mean_audio_rtf": [
|
||||
0.4423,
|
||||
1.5734,
|
||||
2.3024
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -72,8 +138,8 @@
|
||||
{
|
||||
"dataset_name": "seed-tts",
|
||||
"dataset_path": "zhaochenyang20/seed-tts-eval",
|
||||
"backend": "openai-realtime-tts",
|
||||
"endpoint": "/v1/realtime",
|
||||
"backend": "openai-chat-omni",
|
||||
"endpoint": "/v1/chat/completions",
|
||||
"num_prompts": [
|
||||
32,
|
||||
64,
|
||||
@@ -88,6 +154,16 @@
|
||||
"disable_shuffle": true,
|
||||
"trust_remote_code": true,
|
||||
"seed_tts_locale": "en",
|
||||
"extra_body": {
|
||||
"modalities": [
|
||||
"text",
|
||||
"audio"
|
||||
],
|
||||
"chat_template_kwargs": {
|
||||
"enable_thinking": false,
|
||||
"use_tts_template": true
|
||||
}
|
||||
},
|
||||
"percentile-metrics": "ttft,e2el,audio_ttfp,audio_rtf,audio_duration"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -26,8 +26,8 @@
|
||||
{
|
||||
"dataset_name": "seed-tts",
|
||||
"dataset_path": "zhaochenyang20/seed-tts-eval",
|
||||
"backend": "openai-realtime-tts",
|
||||
"endpoint": "/v1/realtime",
|
||||
"backend": "openai-chat-omni",
|
||||
"endpoint": "/v1/chat/completions",
|
||||
"num_prompts": [
|
||||
10,
|
||||
40
|
||||
@@ -40,7 +40,41 @@
|
||||
"disable_shuffle": true,
|
||||
"trust_remote_code": true,
|
||||
"seed_tts_locale": "en",
|
||||
"percentile-metrics": "ttft,e2el,audio_ttfp,audio_rtf,audio_duration"
|
||||
"extra_body": {
|
||||
"modalities": [
|
||||
"text",
|
||||
"audio"
|
||||
],
|
||||
"chat_template_kwargs": {
|
||||
"enable_thinking": false,
|
||||
"use_tts_template": true
|
||||
}
|
||||
},
|
||||
"percentile-metrics": "ttft,e2el,audio_ttfp,audio_rtf,audio_duration",
|
||||
"baseline": {
|
||||
"H100": {
|
||||
"request_throughput": [
|
||||
0.9731,
|
||||
1.1986
|
||||
],
|
||||
"mean_ttft_ms": [
|
||||
152.5665,
|
||||
270.2559
|
||||
],
|
||||
"mean_e2el_ms": [
|
||||
1027.2953,
|
||||
3306.3735
|
||||
],
|
||||
"mean_audio_ttfp_ms": [
|
||||
458.1553,
|
||||
2030.2335
|
||||
],
|
||||
"mean_audio_rtf": [
|
||||
0.2357,
|
||||
0.791
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -29,7 +29,16 @@ _SERVER_ENV: dict[str, str] = {"VLLM_OMNI_INPUT_WAIT_TIMEOUT_S": str(SERVER_INPU
|
||||
if TOKENIZER:
|
||||
_SERVER_ENV["NEMOTRON_VOICECHAT_LLM_PATH"] = TOKENIZER
|
||||
|
||||
pytestmark = [pytest.mark.full_model, pytest.mark.omni]
|
||||
pytestmark = [
|
||||
pytest.mark.full_model,
|
||||
pytest.mark.omni,
|
||||
# Since #7413 a duplex-capable model is served by the duplex engine only when
|
||||
# its pipeline declares a duplex_plugin. The Nemotron VoiceChat pipeline does
|
||||
# not, so the server starts the turn engine and the /v1/realtime websocket
|
||||
# falls through to vllm's speech-to-text realtime handler, which rejects the
|
||||
# native duplex client's session.update ("Missing required field: model").
|
||||
pytest.mark.skip(reason="Nemotron VoiceChat is not served by the duplex engine yet (no duplex_plugin)"),
|
||||
]
|
||||
|
||||
|
||||
@hardware_test(res={"cuda": ["H100", "B200"]}, num_cards=1)
|
||||
|
||||
@@ -20,12 +20,14 @@ from vllm_omni.model_executor.models.minicpmo_4_5.minicpmo_4_5_omni_llm import (
|
||||
from vllm_omni.model_executor.models.minicpmo_4_5.minicpmo_4_5_omni_tts import (
|
||||
_CODEC_PENALTY_WINDOW,
|
||||
_DUPLEX_CODEC_TOKENS_PER_CHUNK,
|
||||
_DUPLEX_TURN_END_CODEC_TOKENS,
|
||||
_OFFLINE_CODEC_MAX_NEW_TOKENS,
|
||||
_REPETITION_PENALTY_CHUNK_SIZE,
|
||||
MiniCPMO45OmniTTSForConditionalGeneration,
|
||||
_apply_batched_repetition_penalty,
|
||||
_native_duplex_chunk_budget,
|
||||
_restore_weight_norm_weight,
|
||||
_turn_end_boundary_eos_masked,
|
||||
blank_scheduler_prompt_for_penalties,
|
||||
)
|
||||
from vllm_omni.utils.mm_outputs import to_payload_element
|
||||
@@ -453,7 +455,9 @@ def test_sample_leaves_unforced_rows_to_the_sampler(mocker) -> None:
|
||||
def test_native_duplex_chunk_budget_masks_eos_until_generate_chunk_is_full() -> None:
|
||||
assert _native_duplex_chunk_budget({}) == (_DUPLEX_CODEC_TOKENS_PER_CHUNK, _DUPLEX_CODEC_TOKENS_PER_CHUNK)
|
||||
assert _native_duplex_chunk_budget({"turn_start": True}) == (_DUPLEX_CODEC_TOKENS_PER_CHUNK, 0)
|
||||
assert _native_duplex_chunk_budget({"turn_end": True}) == (_DUPLEX_CODEC_TOKENS_PER_CHUNK, 0)
|
||||
# The turn-end chunk drains the owed text: no floor, multi-unit ceiling.
|
||||
assert _native_duplex_chunk_budget({"turn_end": True}) == (_DUPLEX_TURN_END_CODEC_TOKENS, 0)
|
||||
assert _DUPLEX_TURN_END_CODEC_TOKENS > _DUPLEX_CODEC_TOKENS_PER_CHUNK
|
||||
|
||||
talker = _make_talker()
|
||||
talker._request_audio_states["req-chunk"] = {
|
||||
@@ -1097,3 +1101,43 @@ def test_request_cleanup_evicts_decode_state() -> None:
|
||||
|
||||
assert "req-done" not in talker._request_audio_states
|
||||
assert "req-done" not in talker._request_condition_states
|
||||
|
||||
|
||||
def test_turn_end_drain_masks_only_the_cadence_eos() -> None:
|
||||
# Early EOS (text spoken before the first boundary) is allowed.
|
||||
assert not _turn_end_boundary_eos_masked(0)
|
||||
assert not _turn_end_boundary_eos_masked(18)
|
||||
assert not _turn_end_boundary_eos_masked(24)
|
||||
# The cadence EOS right after each 25-frame boundary is masked for a short window.
|
||||
assert _turn_end_boundary_eos_masked(25)
|
||||
assert _turn_end_boundary_eos_masked(29)
|
||||
assert not _turn_end_boundary_eos_masked(30)
|
||||
assert not _turn_end_boundary_eos_masked(49)
|
||||
assert _turn_end_boundary_eos_masked(50)
|
||||
assert not _turn_end_boundary_eos_masked(60)
|
||||
|
||||
talker = _make_talker()
|
||||
talker._request_audio_states["req-drain"] = {
|
||||
"finished": False,
|
||||
"step": 25,
|
||||
"max_tokens": _DUPLEX_TURN_END_CODEC_TOKENS,
|
||||
"min_tokens": 0,
|
||||
"turn_end_drain": True,
|
||||
}
|
||||
output = talker.make_omni_output(
|
||||
torch.ones(1, 2),
|
||||
model_intermediate_buffer=[{"request_id": "req-drain", "codes": {"audio": torch.empty(0)}}],
|
||||
request_token_spans=[(0, 1)],
|
||||
)
|
||||
logits = talker.compute_logits(output.text_hidden_states)
|
||||
assert logits[0, 7].item() == float("-inf")
|
||||
assert output.multimodal_outputs["meta"]["finished"][0].item() is False
|
||||
|
||||
talker._request_audio_states["req-drain"]["step"] = 30
|
||||
output = talker.make_omni_output(
|
||||
torch.ones(1, 2),
|
||||
model_intermediate_buffer=[{"request_id": "req-drain", "codes": {"audio": torch.empty(0)}}],
|
||||
request_token_spans=[(0, 1)],
|
||||
)
|
||||
logits = talker.compute_logits(output.text_hidden_states)
|
||||
assert torch.isfinite(logits[0, 7])
|
||||
@@ -10,6 +10,11 @@ from vllm_omni.metrics import OrchestratorAggregator
|
||||
|
||||
from .utils.logging import get_connector_logger
|
||||
|
||||
# Upper bound on a previous streaming chunk's generation reserve: a duplex
|
||||
# turn-end chunk may retain more codec ids than the steady-state per-unit
|
||||
# reserve (see MINICPMO45_DUPLEX_TURN_END_CODEC_TOKENS).
|
||||
_MAX_PREVIOUS_CHUNK_GENERATION_RESERVE = 26 * 4
|
||||
|
||||
logger = get_connector_logger(__name__)
|
||||
|
||||
|
||||
@@ -345,7 +350,9 @@ def construct_next_stage_streaming_input_prompt(
|
||||
f"confirmed={confirmed_num_computed_tokens}, prompt={request.num_prompt_tokens}"
|
||||
)
|
||||
previous_codec_ids = list(request._all_token_ids[request.num_prompt_tokens : confirmed_num_computed_tokens])
|
||||
max_confirmed_codec_tokens = generation_reserve - 1
|
||||
# The previous chunk may have been a turn-end unit that ran to the
|
||||
# Talker's EOS with a larger reserve than this chunk's.
|
||||
max_confirmed_codec_tokens = max(generation_reserve, _MAX_PREVIOUS_CHUNK_GENERATION_RESERVE) - 1
|
||||
if len(previous_codec_ids) > max_confirmed_codec_tokens:
|
||||
raise ValueError(
|
||||
"streaming prompt recompute retained too many codec tokens: "
|
||||
|
||||
@@ -9,3 +9,9 @@
|
||||
|
||||
# One MiniCPMTTS.generate_chunk emits 25 codec frames plus its terminator.
|
||||
MINICPMO45_DUPLEX_CODEC_TOKENS_PER_CHUNK = 26
|
||||
# Codec budget for the Talker chunk that carries the Thinker's turn end. The
|
||||
# Talker paces ~1 s of speech per unit and emits its chunk EOS at the 25-frame
|
||||
# cadence, while the text it still owes at turn end can exceed one unit. The
|
||||
# turn-end chunk may run for several units so the tail of the turn is spoken
|
||||
# (see ``_native_duplex_chunk_budget`` and the boundary EOS mask in the Talker).
|
||||
MINICPMO45_DUPLEX_TURN_END_CODEC_TOKENS = MINICPMO45_DUPLEX_CODEC_TOKENS_PER_CHUNK * 4
|
||||
@@ -28,7 +28,10 @@ from vllm.model_executor.models.utils import maybe_prefix
|
||||
from vllm.v1.sample.sampler import Sampler
|
||||
|
||||
from vllm_omni.engine.duplex.intermediate import get_tts_handoff
|
||||
from vllm_omni.model_executor.models.minicpmo_4_5 import MINICPMO45_DUPLEX_CODEC_TOKENS_PER_CHUNK
|
||||
from vllm_omni.model_executor.models.minicpmo_4_5 import (
|
||||
MINICPMO45_DUPLEX_CODEC_TOKENS_PER_CHUNK,
|
||||
MINICPMO45_DUPLEX_TURN_END_CODEC_TOKENS,
|
||||
)
|
||||
from vllm_omni.model_executor.models.output_templates import OmniOutput
|
||||
from vllm_omni.platforms import current_omni_platform
|
||||
|
||||
@@ -48,13 +51,38 @@ _OFFLINE_CODEC_MAX_NEW_TOKENS = 2048
|
||||
# Without this, the single-vocab Sampler keeps the stage-1 request alive
|
||||
# until codec EOS / 4096 and Thinker never starts the next model turn.
|
||||
_DUPLEX_CODEC_TOKENS_PER_CHUNK = MINICPMO45_DUPLEX_CODEC_TOKENS_PER_CHUNK
|
||||
_DUPLEX_TURN_END_CODEC_TOKENS = MINICPMO45_DUPLEX_TURN_END_CODEC_TOKENS
|
||||
#: Frames the Talker forwards per generate_chunk before its cadence EOS.
|
||||
_DUPLEX_CODEC_FRAMES_PER_CHUNK = MINICPMO45_DUPLEX_CODEC_TOKENS_PER_CHUNK - 1
|
||||
#: On a turn-end chunk the Talker's EOS is masked for this many steps after
|
||||
#: each 25-frame boundary: the model emits a cadence EOS there whether or not
|
||||
#: its text is spoken, while a genuine end of text shows up as an EOS anywhere
|
||||
#: else in the window.
|
||||
_DUPLEX_TURN_END_BOUNDARY_MASK_STEPS = 5
|
||||
|
||||
|
||||
def _native_duplex_chunk_budget(meta: Mapping[str, Any] | None) -> tuple[int, int]:
|
||||
"""Return ``(max_tokens, min_tokens)`` for one native-duplex Talker request."""
|
||||
boundary = isinstance(meta, Mapping) and (bool(meta.get("turn_start")) or bool(meta.get("turn_end")))
|
||||
turn_start = isinstance(meta, Mapping) and bool(meta.get("turn_start"))
|
||||
turn_end = isinstance(meta, Mapping) and bool(meta.get("turn_end"))
|
||||
if turn_end:
|
||||
# The turn-end chunk drains the text the Talker still owes: no floor
|
||||
# (an early EOS means the text is spoken) and a multi-unit ceiling.
|
||||
return _DUPLEX_TURN_END_CODEC_TOKENS, 0
|
||||
ceiling = _DUPLEX_CODEC_TOKENS_PER_CHUNK
|
||||
return ceiling, 0 if boundary else ceiling
|
||||
return ceiling, 0 if turn_start else ceiling
|
||||
|
||||
|
||||
def _turn_end_boundary_eos_masked(step: int) -> bool:
|
||||
"""Whether a turn-end chunk masks codec EOS at ``step`` forwarded frames.
|
||||
|
||||
The Talker emits a cadence EOS after every 25 frames regardless of the text
|
||||
left, so a turn-end chunk ignores EOS in a short window after each boundary
|
||||
and lets the model continue; EOS elsewhere ends the chunk as usual.
|
||||
"""
|
||||
if step < _DUPLEX_CODEC_FRAMES_PER_CHUNK:
|
||||
return False
|
||||
return step % _DUPLEX_CODEC_FRAMES_PER_CHUNK < _DUPLEX_TURN_END_BOUNDARY_MASK_STEPS
|
||||
|
||||
|
||||
def blank_scheduler_prompt_for_penalties(
|
||||
@@ -367,7 +395,7 @@ class MiniCPMO45OmniTTSForConditionalGeneration(nn.Module, SupportsPP):
|
||||
code_ids = torch.as_tensor(previous_codes, device=self.emb_code[0].weight.device, dtype=torch.long)
|
||||
else:
|
||||
raise ValueError("streaming prompt recompute is missing confirmed codec ids")
|
||||
if code_ids.numel() > _DUPLEX_CODEC_TOKENS_PER_CHUNK - 1:
|
||||
if code_ids.numel() > _DUPLEX_TURN_END_CODEC_TOKENS - 1:
|
||||
raise ValueError(f"streaming prompt recompute has too many codec ids: {code_ids.numel()}")
|
||||
if code_ids.numel() and bool(((code_ids < 0) | (code_ids >= self._codec_eos_id)).any()):
|
||||
raise ValueError("streaming prompt recompute codec ids include an invalid or terminal token")
|
||||
@@ -500,6 +528,7 @@ class MiniCPMO45OmniTTSForConditionalGeneration(nn.Module, SupportsPP):
|
||||
"step": 0,
|
||||
"max_tokens": max_tokens,
|
||||
"min_tokens": min_tokens,
|
||||
"turn_end_drain": bool(native_duplex and isinstance(meta, Mapping) and bool(meta.get("turn_end"))),
|
||||
}
|
||||
if retained_codes:
|
||||
state["recent_codes"] = retained_codes
|
||||
@@ -653,7 +682,10 @@ class MiniCPMO45OmniTTSForConditionalGeneration(nn.Module, SupportsPP):
|
||||
# plane does not wait for a follow-up empty decode.
|
||||
state["finished"] = True
|
||||
force_eos_rows[index] = chunk_done
|
||||
mask_eos_rows[index] = not force_eos_rows[index] and min_tokens is not None and step < int(min_tokens)
|
||||
mask_eos_rows[index] = not force_eos_rows[index] and (
|
||||
(min_tokens is not None and step < int(min_tokens))
|
||||
or (bool(state.get("turn_end_drain")) and _turn_end_boundary_eos_masked(step))
|
||||
)
|
||||
terminal_flags[index] = torch.tensor(chunk_done, dtype=torch.bool)
|
||||
|
||||
# Empty-speech rows, finished duplex chunks, and offline requests that
|
||||
|
||||
@@ -16,7 +16,10 @@ from vllm_omni.engine.duplex.intermediate import (
|
||||
set_tts_handoff,
|
||||
)
|
||||
from vllm_omni.inputs.data import OmniTokensPrompt
|
||||
from vllm_omni.model_executor.models.minicpmo_4_5 import MINICPMO45_DUPLEX_CODEC_TOKENS_PER_CHUNK
|
||||
from vllm_omni.model_executor.models.minicpmo_4_5 import (
|
||||
MINICPMO45_DUPLEX_CODEC_TOKENS_PER_CHUNK,
|
||||
MINICPMO45_DUPLEX_TURN_END_CODEC_TOKENS,
|
||||
)
|
||||
from vllm_omni.model_executor.models.minicpmo_4_5.pipeline import MINICPMO45_REFERENCE_AUDIO_KEY
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -1010,7 +1013,11 @@ def llm2tts(
|
||||
handoff_meta = model_intermediate_buffer.setdefault("meta", {})
|
||||
handoff_meta["next_stage_prompt_len"] = condition_length
|
||||
if is_native_duplex_handoff:
|
||||
handoff_meta["next_stage_generation_tokens"] = MINICPMO45_DUPLEX_CODEC_TOKENS_PER_CHUNK
|
||||
handoff_meta["next_stage_generation_tokens"] = (
|
||||
MINICPMO45_DUPLEX_TURN_END_CODEC_TOKENS
|
||||
if native_turn_end_handoff
|
||||
else MINICPMO45_DUPLEX_CODEC_TOKENS_PER_CHUNK
|
||||
)
|
||||
bridge_states = getattr(_streaming_context, "bridge_states", None)
|
||||
handoff_state = bridge_states.get("minicpmo45_tts_handoff") if isinstance(bridge_states, dict) else None
|
||||
if not isinstance(handoff_state, dict) or handoff_state.get("request_id") != str(llm_output.request_id):
|
||||
|
||||
Reference in new issue
Block a user