[Perf][NemotronVoiceChat] Fast defaults for all three deploy profiles: 2.8x offline, realtime full-duplex audio (#6354)

Signed-off-by: Yuekai Zhang <zhangyuekai@foxmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Yuekai ZhangandClaude Fable 5 authored and GitHub committed 2026-09-03 18:49:19 -07:00
1 parent e43c787544
commit 4179b0cae1
22 files changed
+2606 -195

No files matched your search

@@ -0,0 +1,135 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""Rolling-window mel slicing must match full-history slicing bit-for-bit.
The duplex thinker keeps a bounded trailing audio window instead of the full
session history (the mel featurizer is local: hop-aligned STFT columns, no
cross-stream normalization). These tests replay a stream both ways through
the vendored preprocessor and require identical chunk tensors.
"""
from types import SimpleNamespace
import pytest
import torch
from vllm_omni.model_executor.models.nemotron_voicechat.nemotron_voicechat_thinker import (
_DUPLEX_MEL_MARGIN_COLS,
perception_chunk_geometry,
slice_perception_streaming_mel,
)
FRAME_SAMPLES = 1280 # 80 ms @ 16 kHz
HOP = 160 # 10 ms window stride
pytestmark = pytest.mark.core_model
def _make_preprocessor():
from vllm_omni.model_executor.models.nemotron_voicechat.nemo_vendored.asr.audio_preprocessing import (
AudioToMelSpectrogramPreprocessor,
)
# Mirrors model.stt.model.perception.preprocessor in the checkpoint with
# the thinker's streaming overrides (dither=0, pad_to=0).
return (
AudioToMelSpectrogramPreprocessor(
sample_rate=16000,
window_size=0.025,
window_stride=0.01,
n_fft=512,
features=128,
dither=0.0,
pad_to=0,
normalize="NA",
log=True,
frame_splicing=1,
)
.to(torch.float32)
.eval()
)
def _mel(preprocessor, audio: torch.Tensor) -> torch.Tensor:
with torch.inference_mode():
processed, _ = preprocessor(
input_signal=audio.unsqueeze(0),
length=torch.tensor([audio.numel()], dtype=torch.long),
)
return processed
@pytest.mark.parametrize(
"streaming_cfg",
[
SimpleNamespace(
chunk_size=[9, 8],
shift_size=[9, 8],
pre_encode_cache_size=[0, 9],
drop_extra_pre_encoded=3,
),
SimpleNamespace(
chunk_size=[17, 16],
shift_size=[9, 8],
pre_encode_cache_size=[0, 17],
drop_extra_pre_encoded=5,
),
],
)
def test_windowed_mel_chunks_match_full_history(streaming_cfg):
torch.manual_seed(0)
preprocessor = _make_preprocessor()
num_frames = 80 # 6.4 s: far past the point where trimming kicks in
stream = torch.randn(num_frames * FRAME_SAMPLES, dtype=torch.float32) * 0.1
window = None
window_col0 = 0
trimmed = False
for frame_idx in range(num_frames):
frame = stream[frame_idx * FRAME_SAMPLES : (frame_idx + 1) * FRAME_SAMPLES]
full_audio = stream[: (frame_idx + 1) * FRAME_SAMPLES]
window = frame if window is None else torch.cat([window, frame])
if frame_idx > 0:
total_samples = window_col0 * HOP + int(window.numel())
stream_total_cols = total_samples // HOP + 1
assert stream_total_cols == full_audio.numel() // HOP + 1
cache_start, _, _ = perception_chunk_geometry(frame_idx, stream_total_cols, streaming_cfg)
desired_col0 = max(0, cache_start - _DUPLEX_MEL_MARGIN_COLS)
if desired_col0 > window_col0:
window = window[(desired_col0 - window_col0) * HOP :]
window_col0 = desired_col0
trimmed = True
expected, expected_drop = slice_perception_streaming_mel(
_mel(preprocessor, full_audio), frame_idx, streaming_cfg
)
actual, actual_drop = slice_perception_streaming_mel(
_mel(preprocessor, window), frame_idx, streaming_cfg, window_col0=window_col0
)
assert actual_drop == expected_drop
assert actual.shape == expected.shape, f"frame {frame_idx}"
assert torch.equal(actual, expected), f"frame {frame_idx} mel chunk diverged (window_col0={window_col0})"
assert trimmed, "test never exercised the trimmed-window path"
# The rolling window must stay bounded instead of growing with the stream.
assert window.numel() < 4 * 16000, f"window grew to {window.numel()} samples"
def test_stream_start_requires_untrimmed_window():
streaming_cfg = SimpleNamespace(
chunk_size=[9, 8], shift_size=[9, 8], pre_encode_cache_size=[0, 9], drop_extra_pre_encoded=3
)
mel = torch.zeros(1, 128, 9)
with pytest.raises(ValueError, match="untrimmed"):
slice_perception_streaming_mel(mel, 0, streaming_cfg, window_col0=4)
def test_window_missing_needed_columns_rejected():
streaming_cfg = SimpleNamespace(
chunk_size=[9, 8], shift_size=[9, 8], pre_encode_cache_size=[0, 9], drop_extra_pre_encoded=3
)
mel = torch.zeros(1, 128, 24)
with pytest.raises(ValueError, match="needs column"):
slice_perception_streaming_mel(mel, 40, streaming_cfg, window_col0=340)
@@ -249,7 +249,7 @@ def test_code2wav_reuses_and_clears_per_request_streaming_cache() -> None:
model = NemotronVoiceChatCode2Wav.__new__(NemotronVoiceChatCode2Wav)
nn.Module.__init__(model)
model._sample_rate, model._num_quantizers, model._codebook_size, model._wav_per_frame = 22050, 31, 1024, 1764
model._duplex_codec_caches = {}
model._stream_caches = {}
model.audio_codec = Codec()
def info(codes):
@@ -258,6 +258,6 @@ def test_code2wav_reuses_and_clears_per_request_streaming_cache() -> None:
model.forward(runtime_additional_information=info(torch.zeros((1, 31), dtype=torch.long)))
model.forward(runtime_additional_information=info(torch.zeros((2, 31), dtype=torch.long)))
assert model.audio_codec.caches[0] is model.audio_codec.caches[1]
assert "req" in model._duplex_codec_caches
assert "req" in model._stream_caches
model.on_requests_finished({"req"})
assert "req" not in model._duplex_codec_caches
assert "req" not in model._stream_caches
@@ -0,0 +1,134 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""Pin the native talker's KV-recompute replay arithmetic.
Replay turns what used to be a loud desync failure into a silent
reconstruction, so these tests pin the exact index math: ``codes_rows[i]``
holds the codes sampled at step ``i + 1``, replaying step ``t`` feeds the
codes from step ``t - 1`` (``initial_code`` for ``t == 1``), the live step
(if any) is the last row of the span, and a prefill-boundary-crossing span
prepends the prefill embeds. ``build_decode_embeds`` is faked to encode
``(timeline step, prev-code sum, uncond flag)`` into each returned row so the
composition is directly assertable.
"""
from types import SimpleNamespace
import pytest
import torch
from vllm_omni.model_executor.models.nemotron_voicechat import talker_native
from vllm_omni.model_executor.models.nemotron_voicechat.nemotron_voicechat_talker import (
NemotronVoiceChatTalkerForConditionalGeneration,
)
pytestmark = pytest.mark.core_model
INIT_LEN = 4
HID = 4
Q = 2
def _fake_build_decode_embeds(model, *, prev_codes, subword_embed, uncond):
t = float(subword_embed.reshape(-1)[0])
row = torch.tensor([[[t, float(prev_codes.float().sum()), 1.0 if uncond else 0.0, 0.0]]])
return row
def _codes_for_step(step: int) -> torch.Tensor:
"""codes_rows entry for ``step`` (appended after the step ran)."""
return torch.full((1, Q), 100.0 + step)
def _make_talker(step: int, rows_through_step: int):
talker = object.__new__(NemotronVoiceChatTalkerForConditionalGeneration)
talker._dtype = torch.float32
talker._use_native_backbone = True
talker.tts = SimpleNamespace(tts_model=object())
session = {
"use_native": True,
"step": step,
"init_len": INIT_LEN,
"prefill_embeds": torch.arange(INIT_LEN * HID, dtype=torch.float32).reshape(INIT_LEN, HID),
# cas_table[t] == [t] so the fake embed builder can recover the step.
"cas_table": torch.arange(64, dtype=torch.float32).reshape(-1, 1),
"timeline": torch.zeros(64, dtype=torch.long),
"codes_rows": [_codes_for_step(s) for s in range(1, rows_through_step + 1)],
"code": torch.full((1, 1, Q), 999.0),
"initial_code": torch.full((1, 1, Q), 7.0),
"silence_codes": None,
"text_eos_id": 1,
"uncond_stream": None,
"uncond_hidden": None,
"codec_streaming": True,
"upstream_finished": False,
}
talker._sessions = {"req": session}
return talker, session
def _preprocess(talker, *, offset: int, span: int):
info = {"_omni_num_computed_tokens": offset, "request_id": "req"}
input_ids = torch.zeros(span, dtype=torch.long)
_, embeds, _ = talker._native_preprocess(input_ids, info, torch.device("cpu"), span, "req")
return embeds
@pytest.fixture(autouse=True)
def fake_embed_builder(monkeypatch):
monkeypatch.setattr(talker_native, "build_decode_embeds", _fake_build_decode_embeds)
def test_pure_replay_reads_previous_steps_codes():
talker, session = _make_talker(step=5, rows_through_step=4)
# Engine rewound to timeline steps 1..3 (offset init_len => t_first = 1).
embeds = _preprocess(talker, offset=INIT_LEN, span=3)
assert embeds.shape == (3, HID)
assert embeds[:, 0].tolist() == [1.0, 2.0, 3.0] # timeline steps
# step 1 feeds initial_code; steps 2..3 feed codes sampled at steps 1..2.
assert embeds[:, 1].tolist() == [7.0 * Q, (100.0 + 1) * Q, (100.0 + 2) * Q]
assert embeds[:, 2].tolist() == [0.0, 0.0, 0.0] # uncond never used
assert session.get("pending_step") is None # nothing to resample
assert session["replaying"] is True
def test_replay_plus_live_step_appends_live_row_last():
talker, session = _make_talker(step=5, rows_through_step=4)
# Steps 3..4 replayed, step 5 is live (t_last == step).
embeds = _preprocess(talker, offset=INIT_LEN + 2, span=3)
assert embeds.shape == (3, HID)
assert embeds[:, 0].tolist() == [3.0, 4.0, 5.0]
# Replays feed stored history; the live step feeds the latest code.
assert embeds[:, 1].tolist() == [(100.0 + 2) * Q, (100.0 + 3) * Q, 999.0 * Q]
assert session["pending_step"] == 5
assert "replaying" not in session
def test_replay_span_crossing_prefill_boundary_prepends_prefill():
talker, session = _make_talker(step=4, rows_through_step=3)
# Positions 2..3 are speaker-prompt prefill, then timeline steps 1..2.
embeds = _preprocess(talker, offset=2, span=4)
assert embeds.shape == (4, HID)
assert torch.equal(embeds[:2], session["prefill_embeds"][2:4])
assert embeds[2:, 0].tolist() == [1.0, 2.0]
assert embeds[2:, 1].tolist() == [7.0 * Q, (100.0 + 1) * Q]
assert session.get("pending_step") is None
def test_live_first_step_crossing_prefill_boundary_still_rejected():
talker, _ = _make_talker(step=1, rows_through_step=0)
with pytest.raises(ValueError, match="crossing"):
_preprocess(talker, offset=2, span=3)
def test_short_code_history_is_rejected():
talker, _ = _make_talker(step=5, rows_through_step=1)
with pytest.raises(RuntimeError, match="cannot replay"):
_preprocess(talker, offset=INIT_LEN, span=3)
def test_position_past_session_step_is_rejected():
talker, _ = _make_talker(step=3, rows_through_step=2)
with pytest.raises(RuntimeError, match="outpaced"):
_preprocess(talker, offset=INIT_LEN + 3, span=1)