[Enhancement] Custom chunk_size for mimo-audio model (#1964)
Signed-off-by: 齐保元 <qibaoyuan@xiaomi.com>
This commit is contained in:
1 parent
f8777d85bc
commit
8619321b90
6 files changed
+747
-86
No files matched your search
@@ -0,0 +1,227 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import Mock
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from vllm_omni.model_executor.models.mimo_audio.config_mimo_audio import TALKER_CODEC_PAD_TOKEN_ID
|
||||
from vllm_omni.model_executor.models.mimo_audio.mimo_audio_code2wav import (
|
||||
AudioStreamerConfig,
|
||||
MiMoAudioToken2WavForConditionalGenerationVLLM,
|
||||
flat_codec_group_element_count,
|
||||
)
|
||||
|
||||
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
|
||||
|
||||
_GROUP = 4
|
||||
_AC = 8
|
||||
_GROUP_WIDTH = flat_codec_group_element_count(_GROUP, _AC)
|
||||
_FTP = 2 * 2 * 240 # frames_per_token from mocked tokenizer.config
|
||||
|
||||
|
||||
def _codes_ns(empty: int = 555, eostm: int = 666):
|
||||
return SimpleNamespace(empty=empty, eostm=eostm)
|
||||
|
||||
|
||||
def _make_dummy_code_tensor() -> torch.Tensor:
|
||||
"""Pad-only talker dummy; matches _check_dummy_code_tensor."""
|
||||
t = torch.zeros(flat_codec_group_element_count(_GROUP, _AC), dtype=torch.long)
|
||||
t = t.view(_GROUP, _AC + 1)
|
||||
t[:, 0] = TALKER_CODEC_PAD_TOKEN_ID
|
||||
return t.view(-1)
|
||||
|
||||
|
||||
def _make_valid_flat_codes(num_groups: int = 1, *, empty_id: int = 555) -> torch.Tensor:
|
||||
"""Flat layout that extract_audio_code_tensor accepts (text col == empty)."""
|
||||
parts = []
|
||||
for _ in range(num_groups):
|
||||
g = torch.zeros(_GROUP, _AC + 1, dtype=torch.long)
|
||||
g[0, 0] = empty_id
|
||||
g[:, 1:] = torch.randint(1, 50, (_GROUP, _AC))
|
||||
parts.append(g.reshape(-1))
|
||||
return torch.cat(parts)
|
||||
|
||||
|
||||
def _make_invalid_flat_immediate_eostm(eostm_id: int = 666) -> torch.Tensor:
|
||||
g = torch.zeros(_GROUP, _AC + 1, dtype=torch.long)
|
||||
g[0, 0] = eostm_id
|
||||
return g.reshape(-1)
|
||||
|
||||
|
||||
def _minimal_model():
|
||||
"""Avoid __init__ (HF tokenizer paths); only fields used by _batch_decode_waveforms."""
|
||||
model = object.__new__(MiMoAudioToken2WavForConditionalGenerationVLLM)
|
||||
model.device = torch.device("cpu")
|
||||
model.config = SimpleNamespace(group_size=_GROUP, audio_channels=_AC)
|
||||
model.streamer_config = AudioStreamerConfig(group_size=_GROUP, audio_channels=_AC)
|
||||
model.codes = _codes_ns()
|
||||
|
||||
decode_vq = Mock(
|
||||
side_effect=lambda audio_codes: torch.ones(
|
||||
audio_codes.shape[1],
|
||||
7,
|
||||
dtype=torch.float32,
|
||||
device=audio_codes.device,
|
||||
)
|
||||
)
|
||||
decoder = Mock()
|
||||
|
||||
audio_tok = SimpleNamespace(
|
||||
encoder=SimpleNamespace(decode_vq=decode_vq),
|
||||
decoder=decoder,
|
||||
config=SimpleNamespace(avg_pooler=2, stride_size=2, hop_length=240),
|
||||
)
|
||||
model._tokenizer_service = SimpleNamespace(audio_tokenizer=audio_tok)
|
||||
return model, audio_tok
|
||||
|
||||
|
||||
def test_batch_decode_waveforms_empty_input_list():
|
||||
"""Empty input list returns a single zero-length float32 tensor on model device."""
|
||||
model, _ = _minimal_model()
|
||||
out = MiMoAudioToken2WavForConditionalGenerationVLLM._batch_decode_waveforms(model, [])
|
||||
assert len(out) == 1
|
||||
assert out[0].dtype == torch.float32
|
||||
assert out[0].numel() == 0
|
||||
assert out[0].device == model.device
|
||||
|
||||
|
||||
def test_batch_decode_waveforms_single_vs_multiple_decoder_shapes():
|
||||
"""Single and multi-request batches produce correctly shaped packed hidden states and trimmed waveforms."""
|
||||
model, audio_tok = _minimal_model()
|
||||
decoder = audio_tok.decoder
|
||||
|
||||
# Single valid request: decoder output rank-3 for double squeeze path
|
||||
flat = _make_valid_flat_codes(1)
|
||||
decoder.return_value = torch.ones(1, 1, 4 * _FTP + 100, dtype=torch.float32)
|
||||
out1 = MiMoAudioToken2WavForConditionalGenerationVLLM._batch_decode_waveforms(model, [flat])
|
||||
decoder.assert_called_once()
|
||||
packed_hs, input_lengths = decoder.call_args[0]
|
||||
assert packed_hs.shape == (4, 7) # T from decode_vq mock
|
||||
assert input_lengths.tolist() == [4]
|
||||
assert out1[0].shape == (4 * _FTP,)
|
||||
assert out1[0].dtype == torch.float32
|
||||
|
||||
decoder.reset_mock()
|
||||
a = _make_valid_flat_codes(1)
|
||||
b = _make_valid_flat_codes(2)
|
||||
decoder.return_value = torch.ones(2, 1, 8 * _FTP + 100, dtype=torch.float32)
|
||||
out2 = MiMoAudioToken2WavForConditionalGenerationVLLM._batch_decode_waveforms(model, [a, b])
|
||||
decoder.assert_called_once()
|
||||
packed_hs2, input_lengths2 = decoder.call_args[0]
|
||||
assert packed_hs2.shape == (4 + 8, 7) # 1 group -> T=4; 2 groups -> T=8
|
||||
assert input_lengths2.tolist() == [4, 8]
|
||||
assert len(out2) == 2
|
||||
assert out2[0].shape == (4 * _FTP,)
|
||||
assert out2[1].shape == (8 * _FTP,)
|
||||
|
||||
|
||||
def test_batch_decode_waveforms_mixed_valid_invalid_requests():
|
||||
"""Mixed valid and invalid requests: invalid slots get empty tensors, valid slots get decoded waveforms."""
|
||||
model, audio_tok = _minimal_model()
|
||||
valid_a = _make_valid_flat_codes(1)
|
||||
valid_b = _make_valid_flat_codes(1)
|
||||
dummy = _make_dummy_code_tensor()
|
||||
bad_extract = _make_invalid_flat_immediate_eostm(eostm_id=model.codes.eostm)
|
||||
too_short = torch.tensor([1, 2, 3], dtype=torch.long)
|
||||
|
||||
audio_tok.decoder.return_value = torch.ones(2, 1, 5000, dtype=torch.float32)
|
||||
|
||||
inputs = [
|
||||
None,
|
||||
torch.tensor([], dtype=torch.long),
|
||||
dummy,
|
||||
bad_extract,
|
||||
too_short,
|
||||
valid_a,
|
||||
valid_b,
|
||||
]
|
||||
out = MiMoAudioToken2WavForConditionalGenerationVLLM._batch_decode_waveforms(model, inputs)
|
||||
|
||||
assert len(out) == len(inputs)
|
||||
for i in range(5):
|
||||
assert out[i].numel() == 0, f"index {i} should be empty"
|
||||
assert out[5].shape == (4 * _FTP,)
|
||||
assert out[6].shape == (4 * _FTP,)
|
||||
audio_tok.decoder.assert_called_once()
|
||||
packed_hs, input_lengths = audio_tok.decoder.call_args[0]
|
||||
assert packed_hs.shape[0] == 8
|
||||
assert input_lengths.tolist() == [4, 4]
|
||||
|
||||
|
||||
def test_batch_decode_waveforms_all_invalid_returns_per_request_empty():
|
||||
"""All-invalid batch skips decoder entirely and returns empty tensors for every slot."""
|
||||
model, audio_tok = _minimal_model()
|
||||
out = MiMoAudioToken2WavForConditionalGenerationVLLM._batch_decode_waveforms(
|
||||
model,
|
||||
[None, _make_dummy_code_tensor(), torch.tensor([], dtype=torch.long)],
|
||||
)
|
||||
assert len(out) == 3
|
||||
assert all(t.numel() == 0 for t in out)
|
||||
audio_tok.decoder.assert_not_called()
|
||||
|
||||
|
||||
def test_batch_decode_waveforms_output_shape_trim_when_decoder_returns_extra_samples():
|
||||
"""Decoder output longer than valid_len is trimmed to the exact expected waveform length."""
|
||||
model, audio_tok = _minimal_model()
|
||||
flat = _make_valid_flat_codes(1)
|
||||
# Longer than valid_len so branch wav = wav[:valid_len] runs
|
||||
audio_tok.decoder.return_value = torch.ones(1, 1, 10_000, dtype=torch.float32)
|
||||
out = MiMoAudioToken2WavForConditionalGenerationVLLM._batch_decode_waveforms(model, [flat])
|
||||
assert out[0].dim() == 1
|
||||
assert out[0].numel() == 4 * _FTP
|
||||
assert out[0].dtype == torch.float32
|
||||
|
||||
|
||||
def test_batch_decode_waveforms_multi_request_trims_each_row_when_decoder_returns_extra():
|
||||
"""Else-branch split: per-request wav[:valid_len] when decoder pads each batch row."""
|
||||
model, audio_tok = _minimal_model()
|
||||
a = _make_valid_flat_codes(1)
|
||||
b = _make_valid_flat_codes(2)
|
||||
audio_tok.decoder.return_value = torch.ones(2, 1, 10_000, dtype=torch.float32)
|
||||
out = MiMoAudioToken2WavForConditionalGenerationVLLM._batch_decode_waveforms(model, [a, b])
|
||||
assert len(out) == 2
|
||||
assert out[0].shape == (4 * _FTP,)
|
||||
assert out[1].shape == (8 * _FTP,)
|
||||
assert out[0].dtype == torch.float32
|
||||
assert out[1].dtype == torch.float32
|
||||
|
||||
|
||||
def test_batch_decode_waveforms_valid_only_at_edges_maps_to_correct_indices():
|
||||
"""Tensor packing order must match valid_indices when invalid requests are in the middle."""
|
||||
model, audio_tok = _minimal_model()
|
||||
first = _make_valid_flat_codes(1)
|
||||
last = _make_valid_flat_codes(2)
|
||||
inputs = [
|
||||
first,
|
||||
None,
|
||||
_make_dummy_code_tensor(),
|
||||
last,
|
||||
]
|
||||
audio_tok.decoder.return_value = torch.ones(2, 1, 10_000, dtype=torch.float32)
|
||||
out = MiMoAudioToken2WavForConditionalGenerationVLLM._batch_decode_waveforms(model, inputs)
|
||||
assert len(out) == 4
|
||||
assert out[0].numel() == 4 * _FTP
|
||||
assert out[1].numel() == 0
|
||||
assert out[2].numel() == 0
|
||||
assert out[3].numel() == 8 * _FTP
|
||||
packed_hs, input_lengths = audio_tok.decoder.call_args[0]
|
||||
assert packed_hs.shape[0] == 12
|
||||
assert input_lengths.tolist() == [4, 8]
|
||||
|
||||
|
||||
def test_batch_decode_waveforms_output_shapes_1d_float32_for_all_slots():
|
||||
"""Every slot is a 1-D float32 vector (empty or waveform), matching downstream expectations."""
|
||||
model, audio_tok = _minimal_model()
|
||||
inputs = [_make_valid_flat_codes(1), None, _make_valid_flat_codes(1)]
|
||||
audio_tok.decoder.return_value = torch.ones(2, 1, 5000, dtype=torch.float32)
|
||||
out = MiMoAudioToken2WavForConditionalGenerationVLLM._batch_decode_waveforms(model, inputs)
|
||||
assert len(out) == 3
|
||||
for i, t in enumerate(out):
|
||||
assert t.dim() == 1, f"slot {i}"
|
||||
assert t.dtype == torch.float32, f"slot {i}"
|
||||
assert out[0].numel() == 4 * _FTP
|
||||
assert out[1].numel() == 0
|
||||
assert out[2].numel() == 4 * _FTP
|
||||
@@ -0,0 +1,78 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from vllm_omni.model_executor.stage_input_processors.mimo_audio import _flush_remaining_codes
|
||||
|
||||
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
|
||||
|
||||
|
||||
def _sentinel():
|
||||
return {"code_predictor_codes": [], "finished": torch.tensor(True, dtype=torch.bool)}
|
||||
|
||||
|
||||
def test_flush_remaining_codes_when_no_codes_accumulated_missing_request_id():
|
||||
"""No entry for request_id: treat as empty, return finished sentinel with empty codes."""
|
||||
tm = SimpleNamespace(code_prompt_token_ids={})
|
||||
out = _flush_remaining_codes(tm, "missing", chunk_size=3, left_context_size=3)
|
||||
assert out["code_predictor_codes"] == _sentinel()["code_predictor_codes"]
|
||||
assert out["finished"].equal(_sentinel()["finished"])
|
||||
|
||||
|
||||
def test_flush_remaining_codes_when_no_codes_accumulated_empty_list():
|
||||
"""Explicit empty accumulation list returns the same sentinel."""
|
||||
tm = SimpleNamespace(code_prompt_token_ids={"r": []})
|
||||
out = _flush_remaining_codes(tm, "r", chunk_size=3, left_context_size=3)
|
||||
assert out["code_predictor_codes"] == []
|
||||
assert out["finished"].item() is True
|
||||
|
||||
|
||||
def test_flush_remaining_codes_partial_chunk_remaining():
|
||||
"""
|
||||
length=7, chunk_size=3 -> remainder 1 frame in last chunk; context window uses
|
||||
left_context + that partial (3+1), capped by length -> last 4 scalar entries.
|
||||
"""
|
||||
tm = SimpleNamespace(
|
||||
code_prompt_token_ids={"r": [[1], [2], [3], [4], [5], [6], [7]]},
|
||||
)
|
||||
out = _flush_remaining_codes(tm, "r", chunk_size=3, left_context_size=3)
|
||||
assert out["finished"].item() is True
|
||||
assert out["code_predictor_codes"] == [4, 5, 6, 7]
|
||||
|
||||
|
||||
def test_flush_remaining_codes_when_length_is_exact_multiple_of_chunk_size():
|
||||
"""length % chunk_size == 0 uses full last chunk as context_length (not remainder)."""
|
||||
tm = SimpleNamespace(
|
||||
code_prompt_token_ids={"r": [[1], [2], [3], [4], [5], [6]]},
|
||||
)
|
||||
out = _flush_remaining_codes(tm, "r", chunk_size=3, left_context_size=3)
|
||||
# context_length = chunk_size = 3, end_index = min(6, 6) -> all 6
|
||||
assert out["code_predictor_codes"] == [1, 2, 3, 4, 5, 6]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"length,chunk_size,left_context,expected_end_index",
|
||||
[
|
||||
(2, 3, 3, 2), # chunk_length=2 -> context 2, min(2,5)=2
|
||||
(5, 3, 3, 5), # chunk_length=2 -> min(5,5)=5
|
||||
(7, 3, 3, 4), # chunk_length=1 -> min(7,4)=4
|
||||
(6, 3, 3, 6), # chunk_length=0 -> context 3, min(6,6)=6
|
||||
(10, 3, 3, 4), # chunk_length=1 -> min(10,4)=4
|
||||
(1, 5, 10, 1), # chunk_length=1 -> min(1,11)=1
|
||||
],
|
||||
)
|
||||
def test_flush_remaining_codes_context_window_end_index(
|
||||
length: int, chunk_size: int, left_context: int, expected_end_index: int
|
||||
) -> None:
|
||||
"""Mirror _flush_remaining_codes context_length and end_index rules."""
|
||||
accumulated = [[i] for i in range(length)]
|
||||
tm = SimpleNamespace(code_prompt_token_ids={"r": accumulated})
|
||||
out = _flush_remaining_codes(tm, "r", chunk_size=chunk_size, left_context_size=left_context)
|
||||
expected_flat = list(range(length - expected_end_index, length))
|
||||
assert out["code_predictor_codes"] == expected_flat
|
||||
assert out["finished"].item() is True
|
||||
@@ -814,11 +814,13 @@ class MiMoAudioForConditionalGeneration(
|
||||
)
|
||||
)
|
||||
|
||||
audio_tensor = self.generate_audio(code)
|
||||
audio_result = self.token2wav(codes=code, **kwargs)
|
||||
if isinstance(audio_result, OmniOutput):
|
||||
return audio_result
|
||||
return OmniOutput(
|
||||
text_hidden_states=None,
|
||||
multimodal_outputs={
|
||||
"model_outputs": audio_tensor.reshape(1, -1) if audio_tensor is not None else audio_tensor
|
||||
"model_outputs": audio_result.reshape(1, -1) if audio_result is not None else audio_result
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import os
|
||||
import time
|
||||
from collections.abc import Iterable
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
import torch
|
||||
import torchaudio
|
||||
@@ -20,11 +21,21 @@ from vllm.v1.sample.sampler import Sampler
|
||||
|
||||
from vllm_omni.model_executor.models.mimo_audio.config_mimo_audio import TALKER_CODEC_PAD_TOKEN_ID, MiMoAudioConfig
|
||||
from vllm_omni.model_executor.models.mimo_audio.modeling_audio_tokenizer import MiMoAudioTokenizer
|
||||
from vllm_omni.model_executor.models.output_templates import OmniOutput
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
DUMMY_CODE_SHAPE = 36
|
||||
def flat_codec_group_element_count(group_size: int, audio_channels: int) -> int:
|
||||
"""Flat token count for one MiMo talker codec group on the code2wav wire.
|
||||
|
||||
The talker flattens a ``(group_size, audio_channels + 1)`` layout in column-major
|
||||
order: one leading text/special column per row plus ``audio_channels`` RVQ code
|
||||
columns. Each group therefore occupies ``group_size * (audio_channels + 1)``
|
||||
consecutive ids. This matches ``group_width`` in :func:`extract_audio_code_tensor`
|
||||
and is used to count how many such groups are present in a flat ``codes`` tensor.
|
||||
"""
|
||||
return group_size * (audio_channels + 1)
|
||||
|
||||
|
||||
class MiMoAudioTokenizerWorker:
|
||||
@@ -308,7 +319,7 @@ def extract_audio_code_tensor(
|
||||
if flat_codes.numel() == 0:
|
||||
return None
|
||||
|
||||
group_width = group_size * (audio_channels + 1)
|
||||
group_width = flat_codec_group_element_count(group_size, audio_channels)
|
||||
usable = (flat_codes.numel() // group_width) * group_width
|
||||
if usable == 0:
|
||||
return None
|
||||
@@ -377,6 +388,9 @@ def get_tokenizer_worker(
|
||||
class MiMoAudioToken2WavForConditionalGenerationVLLM(nn.Module, SupportsPP):
|
||||
"""Decode MiMo audio codes to waveform for the code2wav stage."""
|
||||
|
||||
have_multimodal_outputs = True
|
||||
enable_update_additional_information = True
|
||||
|
||||
def __init__(self, vllm_config: VllmConfig, prefix: str = ""):
|
||||
super().__init__()
|
||||
|
||||
@@ -429,6 +443,23 @@ class MiMoAudioToken2WavForConditionalGenerationVLLM(nn.Module, SupportsPP):
|
||||
* getattr(audio_tokenizer_config, "hop_length", 240)
|
||||
) * self.config.group_size
|
||||
|
||||
connector_cfg = getattr(vllm_config.model_config, "stage_connector_config", None)
|
||||
extra_cfg = (
|
||||
(
|
||||
connector_cfg.get("extra", connector_cfg)
|
||||
if isinstance(connector_cfg, dict)
|
||||
else getattr(connector_cfg, "extra", None)
|
||||
)
|
||||
if connector_cfg
|
||||
else None
|
||||
)
|
||||
self._codec_chunk_frames = int(extra_cfg.get("codec_chunk_frames", 3)) if isinstance(extra_cfg, dict) else 3
|
||||
if self._codec_chunk_frames <= 0:
|
||||
raise ValueError(f"codec_chunk_frames must be positive, got {self._codec_chunk_frames}")
|
||||
self._codec_left_context_frames = (
|
||||
int(extra_cfg.get("codec_left_context_frames", 3)) if isinstance(extra_cfg, dict) else 3
|
||||
)
|
||||
|
||||
def load_weights(
|
||||
self,
|
||||
weights: Iterable[tuple[str, torch.Tensor]],
|
||||
@@ -438,6 +469,61 @@ class MiMoAudioToken2WavForConditionalGenerationVLLM(nn.Module, SupportsPP):
|
||||
# Decoder has no trainable weights to load in this stage.
|
||||
return set()
|
||||
|
||||
@staticmethod
|
||||
def _split_flat_codes_for_requests(
|
||||
ids: torch.Tensor,
|
||||
seq_token_counts: list[int] | None,
|
||||
runtime_additional_information: list[dict[str, Any]] | None,
|
||||
) -> list[torch.Tensor]:
|
||||
"""Split flat codec token ids per request.
|
||||
|
||||
Prefer ``code_flat_numel`` from async-chunk / connector payload (same idea as
|
||||
qwen3 omni/tts passing decode metadata in additional_information). Fall back
|
||||
to runner-provided ``seq_token_counts`` (no forward_context / ubatch slicing).
|
||||
"""
|
||||
n = ids.numel()
|
||||
if n == 0:
|
||||
return [ids]
|
||||
|
||||
if runtime_additional_information and all(
|
||||
isinstance(info.get("code_flat_numel"), int) and int(info["code_flat_numel"]) > 0
|
||||
for info in runtime_additional_information
|
||||
):
|
||||
sizes = [int(info["code_flat_numel"]) for info in runtime_additional_information]
|
||||
if sum(sizes) == n:
|
||||
parts: list[torch.Tensor] = []
|
||||
offset = 0
|
||||
for sz in sizes:
|
||||
parts.append(ids[offset : offset + sz])
|
||||
offset += sz
|
||||
return parts
|
||||
|
||||
if seq_token_counts is not None and len(seq_token_counts) > 1:
|
||||
boundaries = [0]
|
||||
for count in seq_token_counts:
|
||||
boundaries.append(boundaries[-1] + count)
|
||||
return [ids[boundaries[i] : min(boundaries[i + 1], n)] for i in range(len(seq_token_counts))]
|
||||
|
||||
return [ids]
|
||||
|
||||
@staticmethod
|
||||
def _mimo_codec_runtime_lists(
|
||||
num_req: int,
|
||||
runtime_additional_information: list[dict[str, Any]] | None,
|
||||
) -> tuple[list[int | None], list[int | None]]:
|
||||
"""Per-request ``left_context_size`` / ``codec_chunk_frames`` from runtime buffer."""
|
||||
left_frames: list[int | None] = [None] * num_req
|
||||
chunk_frames: list[int | None] = [None] * num_req
|
||||
if not runtime_additional_information:
|
||||
return left_frames, chunk_frames
|
||||
for i in range(min(num_req, len(runtime_additional_information))):
|
||||
info = runtime_additional_information[i]
|
||||
if "left_context_size" in info:
|
||||
left_frames[i] = int(info["left_context_size"])
|
||||
if "codec_chunk_frames" in info:
|
||||
chunk_frames[i] = int(info["codec_chunk_frames"])
|
||||
return left_frames, chunk_frames
|
||||
|
||||
def chunked_decode_streaming(
|
||||
self,
|
||||
codes: torch.Tensor,
|
||||
@@ -453,9 +539,12 @@ class MiMoAudioToken2WavForConditionalGenerationVLLM(nn.Module, SupportsPP):
|
||||
wav_chunk = self._decode_waveform_from_codes(codes)
|
||||
if wav_chunk.numel() == 0:
|
||||
return wav_chunk
|
||||
# Infer number of codec frames: flat format has 36 elements per frame (9*4 with prepend)
|
||||
num_flat = codes.numel() if codes.ndim == 1 else codes.shape[-1]
|
||||
num_chunks = num_flat // 36
|
||||
elt_per_group = flat_codec_group_element_count(
|
||||
self.streamer_config.group_size,
|
||||
self.streamer_config.audio_channels,
|
||||
)
|
||||
num_chunks = num_flat // elt_per_group
|
||||
if num_chunks <= chunk_size:
|
||||
context_size = 0
|
||||
else:
|
||||
@@ -469,50 +558,65 @@ class MiMoAudioToken2WavForConditionalGenerationVLLM(nn.Module, SupportsPP):
|
||||
self,
|
||||
input_ids: torch.Tensor | None = None,
|
||||
codes: torch.Tensor | None = None,
|
||||
runtime_additional_information: list[dict[str, Any]] | None = None,
|
||||
**kwargs,
|
||||
) -> torch.Tensor | IntermediateTensors:
|
||||
# Support both input_ids (from vLLM) and codes (from mimo_audio.py)
|
||||
) -> OmniOutput | torch.Tensor | IntermediateTensors:
|
||||
if runtime_additional_information is None:
|
||||
runtime_additional_information = kwargs.get("model_intermediate_buffer") or kwargs.get(
|
||||
"runtime_additional_information"
|
||||
)
|
||||
code_tensor = codes if codes is not None else input_ids
|
||||
empty = torch.zeros((0,), dtype=torch.float32, device=self.device)
|
||||
|
||||
if code_tensor is None:
|
||||
raise ValueError("Either input_ids or codes must be provided")
|
||||
|
||||
if code_tensor.numel() == 0:
|
||||
raise ValueError("code_tensor is empty.")
|
||||
|
||||
if getattr(self.vllm_config.model_config, "async_chunk", False):
|
||||
waveform = self.chunked_decode_streaming(code_tensor, chunk_size=10, left_context_size=10)
|
||||
else:
|
||||
waveform = self._decode_waveform_from_codes(code_tensor)
|
||||
|
||||
# For warmup/dummy run: if waveform is empty, return dummy hidden_states
|
||||
# _dummy_run expects a tensor that can be processed by extract_multimodal_outputs
|
||||
if waveform.numel() == 0:
|
||||
# Return dummy hidden_states with shape [seq_len, hidden_size]
|
||||
# This is needed for warmup to avoid NoneType errors
|
||||
hidden_size = getattr(self.config, "hidden_size", None)
|
||||
if hidden_size is None:
|
||||
# Fallback: try to get from vllm_config.model_config
|
||||
hidden_size = getattr(
|
||||
self.vllm_config.model_config,
|
||||
"hidden_size",
|
||||
4096, # Default fallback
|
||||
)
|
||||
|
||||
# Return [seq_len=1, hidden_size] tensor for warmup
|
||||
dummy_hidden = torch.zeros(
|
||||
(1, hidden_size),
|
||||
dtype=torch.bfloat16,
|
||||
device=self.device,
|
||||
if code_tensor is None or code_tensor.numel() == 0:
|
||||
return OmniOutput(
|
||||
text_hidden_states=None,
|
||||
multimodal_outputs={"model_outputs": [empty]},
|
||||
)
|
||||
|
||||
return dummy_hidden
|
||||
is_async_chunk = getattr(self.vllm_config.model_config, "async_chunk", False)
|
||||
ids = code_tensor.reshape(-1).to(dtype=torch.long)
|
||||
request_ids_list = self._split_flat_codes_for_requests(
|
||||
ids, kwargs.get("seq_token_counts"), runtime_additional_information
|
||||
)
|
||||
per_left, per_chunk = self._mimo_codec_runtime_lists(len(request_ids_list), runtime_additional_information)
|
||||
|
||||
if waveform.numel() == 1 and waveform.item() == 0:
|
||||
# Designed for Text output only
|
||||
return None
|
||||
is_capturing = torch.cuda.is_current_stream_capturing()
|
||||
if is_capturing:
|
||||
return OmniOutput(
|
||||
text_hidden_states=None,
|
||||
multimodal_outputs={"model_outputs": [empty] * len(request_ids_list)},
|
||||
)
|
||||
|
||||
return waveform
|
||||
if is_async_chunk:
|
||||
audios = self._batch_chunked_decode_streaming(
|
||||
request_ids_list,
|
||||
default_chunk_size=self._codec_chunk_frames,
|
||||
default_left_context_size=self._codec_left_context_frames,
|
||||
per_req_left_context_frames=per_left,
|
||||
per_req_codec_chunk_frames=per_chunk,
|
||||
)
|
||||
else:
|
||||
audios = self._batch_decode_waveforms(request_ids_list)
|
||||
|
||||
return OmniOutput(
|
||||
text_hidden_states=None,
|
||||
multimodal_outputs={"model_outputs": audios},
|
||||
)
|
||||
|
||||
def make_omni_output(self, model_output: torch.Tensor | OmniOutput, **kwargs) -> OmniOutput:
|
||||
"""Convert raw model output to OmniOutput if needed."""
|
||||
if isinstance(model_output, OmniOutput):
|
||||
return model_output
|
||||
empty = torch.zeros((0,), dtype=torch.float32, device=self.device)
|
||||
if model_output is None:
|
||||
return OmniOutput(text_hidden_states=None, multimodal_outputs={"model_outputs": [empty]})
|
||||
if isinstance(model_output, torch.Tensor):
|
||||
return OmniOutput(
|
||||
text_hidden_states=None,
|
||||
multimodal_outputs={"model_outputs": [model_output.to(dtype=torch.float32).reshape(-1)]},
|
||||
)
|
||||
raise TypeError(f"Unexpected model output type: {type(model_output)}")
|
||||
|
||||
def _get_full_code_sequence(
|
||||
self,
|
||||
@@ -545,8 +649,194 @@ class MiMoAudioToken2WavForConditionalGenerationVLLM(nn.Module, SupportsPP):
|
||||
|
||||
return code_tensor
|
||||
|
||||
@torch.inference_mode()
|
||||
def _batch_decode_waveforms(
|
||||
self,
|
||||
request_codes_list: list[torch.Tensor],
|
||||
) -> list[torch.Tensor]:
|
||||
"""Batch-decode multiple requests' codes in a single decoder forward pass.
|
||||
|
||||
Instead of calling _decode_waveform_from_codes per request (which incurs
|
||||
4 GPU↔CPU round-trips each), this method:
|
||||
1. Extracts audio_codes for all valid requests on CPU (cheap).
|
||||
2. Runs quantizer.decode_vq (embedding lookup) for each on GPU.
|
||||
3. Packs all hidden-states into one tensor and calls
|
||||
decoder(packed_hs, input_lengths) once.
|
||||
4. Splits the output waveforms back to per-request tensors.
|
||||
"""
|
||||
empty = torch.zeros((0,), dtype=torch.float32, device=self.device)
|
||||
num_req = len(request_codes_list)
|
||||
if num_req == 0:
|
||||
return [empty]
|
||||
|
||||
tokenizer = self._tokenizer_service.audio_tokenizer
|
||||
group_size = self.streamer_config.group_size
|
||||
audio_channels = self.streamer_config.audio_channels
|
||||
|
||||
hidden_list: list[torch.Tensor] = []
|
||||
lengths: list[int] = []
|
||||
valid_indices: list[int] = []
|
||||
|
||||
for i, req_codes in enumerate(request_codes_list):
|
||||
if req_codes is None or req_codes.numel() == 0:
|
||||
continue
|
||||
if self._check_dummy_code_tensor(req_codes):
|
||||
continue
|
||||
|
||||
flat_codes = req_codes.detach().to(torch.long).cpu()
|
||||
audio_codes = extract_audio_code_tensor(
|
||||
flat_codes,
|
||||
group_size,
|
||||
audio_channels,
|
||||
self.codes,
|
||||
)
|
||||
if audio_codes is None or audio_codes.numel() == 0:
|
||||
continue
|
||||
|
||||
hs = tokenizer.encoder.decode_vq(audio_codes.to(self.device))
|
||||
hidden_list.append(hs)
|
||||
lengths.append(hs.size(0))
|
||||
valid_indices.append(i)
|
||||
|
||||
if not hidden_list:
|
||||
return [empty] * num_req
|
||||
|
||||
if len(hidden_list) == 1:
|
||||
packed_hs = hidden_list[0]
|
||||
else:
|
||||
packed_hs = torch.cat(hidden_list, dim=0)
|
||||
input_lengths = torch.tensor(lengths, device=self.device)
|
||||
|
||||
recon_wav = tokenizer.decoder(packed_hs, input_lengths)
|
||||
|
||||
cfg = tokenizer.config
|
||||
frames_per_token = cfg.avg_pooler * cfg.stride_size * cfg.hop_length
|
||||
|
||||
result: list[torch.Tensor] = [empty] * num_req
|
||||
if len(valid_indices) == 1:
|
||||
wav = recon_wav.squeeze(0).squeeze(0)
|
||||
valid_len = lengths[0] * frames_per_token
|
||||
if wav.numel() > valid_len:
|
||||
wav = wav[:valid_len]
|
||||
result[valid_indices[0]] = wav.to(dtype=torch.float32).reshape(-1)
|
||||
else:
|
||||
for j, idx in enumerate(valid_indices):
|
||||
wav = recon_wav[j].squeeze(0)
|
||||
valid_len = lengths[j] * frames_per_token
|
||||
if wav.numel() > valid_len:
|
||||
wav = wav[:valid_len]
|
||||
result[idx] = wav.to(dtype=torch.float32).reshape(-1)
|
||||
|
||||
return result
|
||||
|
||||
@torch.inference_mode()
|
||||
def _batch_chunked_decode_streaming(
|
||||
self,
|
||||
request_codes_list: list[torch.Tensor],
|
||||
default_chunk_size: int,
|
||||
default_left_context_size: int,
|
||||
*,
|
||||
per_req_left_context_frames: list[int | None] | None = None,
|
||||
per_req_codec_chunk_frames: list[int | None] | None = None,
|
||||
) -> list[torch.Tensor]:
|
||||
"""Batch version of chunked_decode_streaming for async_chunk mode."""
|
||||
empty = torch.zeros((0,), dtype=torch.float32, device=self.device)
|
||||
num_req = len(request_codes_list)
|
||||
if num_req == 0:
|
||||
return [empty]
|
||||
|
||||
tokenizer = self._tokenizer_service.audio_tokenizer
|
||||
group_size = self.streamer_config.group_size
|
||||
audio_channels = self.streamer_config.audio_channels
|
||||
|
||||
hidden_list: list[torch.Tensor] = []
|
||||
lengths: list[int] = []
|
||||
valid_indices: list[int] = []
|
||||
context_sizes: list[int] = []
|
||||
|
||||
for i, req_codes in enumerate(request_codes_list):
|
||||
if req_codes is None or req_codes.numel() == 0:
|
||||
continue
|
||||
if self._check_dummy_code_tensor(req_codes):
|
||||
continue
|
||||
|
||||
flat_codes = req_codes.detach().to(torch.long).cpu()
|
||||
audio_codes = extract_audio_code_tensor(
|
||||
flat_codes,
|
||||
group_size,
|
||||
audio_channels,
|
||||
self.codes,
|
||||
)
|
||||
if audio_codes is None or audio_codes.numel() == 0:
|
||||
continue
|
||||
|
||||
hs = tokenizer.encoder.decode_vq(audio_codes.to(self.device))
|
||||
hidden_list.append(hs)
|
||||
lengths.append(hs.size(0))
|
||||
valid_indices.append(i)
|
||||
|
||||
num_flat = req_codes.numel() if req_codes.ndim == 1 else req_codes.shape[-1]
|
||||
elt_per_group = flat_codec_group_element_count(group_size, audio_channels)
|
||||
num_chunks = num_flat // elt_per_group
|
||||
chunk_sz = default_chunk_size
|
||||
if per_req_codec_chunk_frames is not None and i < len(per_req_codec_chunk_frames):
|
||||
c = per_req_codec_chunk_frames[i]
|
||||
if c is not None:
|
||||
chunk_sz = c
|
||||
if (
|
||||
per_req_left_context_frames is not None
|
||||
and i < len(per_req_left_context_frames)
|
||||
and per_req_left_context_frames[i] is not None
|
||||
):
|
||||
ctx = int(per_req_left_context_frames[i])
|
||||
else:
|
||||
ctx = 0 if num_chunks <= chunk_sz else default_left_context_size
|
||||
context_sizes.append(ctx)
|
||||
|
||||
if not hidden_list:
|
||||
return [empty] * num_req
|
||||
|
||||
if len(hidden_list) == 1:
|
||||
packed_hs = hidden_list[0]
|
||||
else:
|
||||
packed_hs = torch.cat(hidden_list, dim=0)
|
||||
input_lengths = torch.tensor(lengths, device=self.device)
|
||||
|
||||
recon_wav = tokenizer.decoder(packed_hs, input_lengths)
|
||||
|
||||
cfg = tokenizer.config
|
||||
frames_per_token = cfg.avg_pooler * cfg.stride_size * cfg.hop_length
|
||||
|
||||
result: list[torch.Tensor] = [empty] * num_req
|
||||
if len(valid_indices) == 1:
|
||||
wav = recon_wav.squeeze(0).squeeze(0)
|
||||
valid_len = lengths[0] * frames_per_token
|
||||
if wav.numel() > valid_len:
|
||||
wav = wav[:valid_len]
|
||||
drop = context_sizes[0] * self.total_upsample
|
||||
if drop > 0 and drop < wav.numel():
|
||||
wav = wav[drop:]
|
||||
elif drop >= wav.numel():
|
||||
wav = wav[:0]
|
||||
result[valid_indices[0]] = wav.to(dtype=torch.float32).reshape(-1)
|
||||
else:
|
||||
for j, idx in enumerate(valid_indices):
|
||||
wav = recon_wav[j].squeeze(0)
|
||||
valid_len = lengths[j] * frames_per_token
|
||||
if wav.numel() > valid_len:
|
||||
wav = wav[:valid_len]
|
||||
drop = context_sizes[j] * self.total_upsample
|
||||
if drop > 0 and drop < wav.numel():
|
||||
wav = wav[drop:]
|
||||
elif drop >= wav.numel():
|
||||
wav = wav[:0]
|
||||
result[idx] = wav.to(dtype=torch.float32).reshape(-1)
|
||||
|
||||
return result
|
||||
|
||||
def _check_dummy_code_tensor(self, code_tensor: torch.Tensor) -> bool:
|
||||
if code_tensor is not None and code_tensor.numel() == DUMMY_CODE_SHAPE:
|
||||
expected = flat_codec_group_element_count(self.config.group_size, self.config.audio_channels)
|
||||
if code_tensor is not None and code_tensor.numel() == expected:
|
||||
code_groups = code_tensor.view(self.config.group_size, self.config.audio_channels + 1)
|
||||
return (
|
||||
(code_groups[:, 0] == TALKER_CODEC_PAD_TOKEN_ID).all() and (code_groups[:, 1:].sum() == 0).all()
|
||||
|
||||
@@ -24,6 +24,8 @@ stage_args:
|
||||
max_model_len: 8192
|
||||
max_num_batched_tokens: 8192
|
||||
custom_process_next_stage_input_func: vllm_omni.model_executor.stage_input_processors.mimo_audio.llm2code2wav_async_chunk
|
||||
output_connectors:
|
||||
to_stage_1: connector_of_shared_memory
|
||||
is_comprehension: true
|
||||
final_output: true
|
||||
final_output_type: text
|
||||
@@ -57,6 +59,8 @@ stage_args:
|
||||
max_model_len: 8192
|
||||
max_num_batched_tokens: 8192
|
||||
engine_input_source: [ 0 ]
|
||||
input_connectors:
|
||||
from_stage_0: connector_of_shared_memory
|
||||
is_comprehension: false
|
||||
final_output: true
|
||||
final_output_type: audio
|
||||
@@ -67,3 +71,26 @@ stage_args:
|
||||
max_tokens: 18192
|
||||
seed: 42
|
||||
detokenize: false
|
||||
|
||||
runtime:
|
||||
enabled: true
|
||||
defaults:
|
||||
window_size: -1
|
||||
max_inflight: 1
|
||||
|
||||
connectors:
|
||||
connector_of_shared_memory:
|
||||
name: SharedMemoryConnector
|
||||
extra:
|
||||
shm_threshold_bytes: 365536
|
||||
codec_streaming: true
|
||||
connector_get_sleep_s: 0.001
|
||||
connector_get_max_wait_first_chunk: 3000
|
||||
connector_get_max_wait: 300
|
||||
codec_chunk_frames: 3
|
||||
codec_left_context_frames: 3
|
||||
|
||||
edges:
|
||||
- from: 0
|
||||
to: 1
|
||||
window_size: -1
|
||||
@@ -50,6 +50,58 @@ def _make_finished_sentinel() -> dict[str, Any]:
|
||||
return {"code_predictor_codes": [], "finished": torch.tensor(True, dtype=torch.bool)}
|
||||
|
||||
|
||||
def _flush_remaining_codes(
|
||||
transfer_manager: Any,
|
||||
request_id: str,
|
||||
chunk_size: int,
|
||||
left_context_size: int,
|
||||
) -> dict[str, Any]:
|
||||
"""Flush any accumulated but unsent codes when the request finishes."""
|
||||
accumulated = transfer_manager.code_prompt_token_ids.get(request_id, [])
|
||||
if not accumulated:
|
||||
return _make_finished_sentinel()
|
||||
|
||||
length = len(accumulated)
|
||||
chunk_length = length % chunk_size
|
||||
context_length = chunk_length if chunk_length != 0 else chunk_size
|
||||
end_index = min(length, left_context_size + context_length)
|
||||
|
||||
# Align with qwen3_omni talker2code2wav_async_chunk: decoder strip uses explicit frame count.
|
||||
left_ctx_frames = max(0, min(length - context_length, left_context_size))
|
||||
flat_codes = torch.tensor(accumulated[-end_index:]).reshape(-1).tolist()
|
||||
|
||||
return {
|
||||
"code_predictor_codes": flat_codes,
|
||||
"left_context_size": left_ctx_frames,
|
||||
"codec_chunk_frames": chunk_size,
|
||||
"codec_left_context_frames": left_context_size,
|
||||
"code_flat_numel": len(flat_codes),
|
||||
"finished": torch.tensor(True, dtype=torch.bool),
|
||||
}
|
||||
|
||||
|
||||
def _is_codes_empty(codes: Any) -> bool:
|
||||
"""Check whether code_predictor_codes should be treated as empty / invalid."""
|
||||
if codes is None:
|
||||
return True
|
||||
if isinstance(codes, torch.Tensor):
|
||||
return codes.numel() == 0 or not codes.any()
|
||||
if hasattr(codes, "__len__") and len(codes) == 0:
|
||||
return True
|
||||
t = torch.tensor(codes, dtype=torch.long) if not isinstance(codes, torch.Tensor) else codes
|
||||
return not t.any()
|
||||
|
||||
|
||||
def _to_code_tensor(codes: Any) -> torch.Tensor | None:
|
||||
"""Convert codes to a (B, 1, 8, 4) long tensor, or return None if shape is invalid."""
|
||||
code_tensor = codes.to(torch.long) if isinstance(codes, torch.Tensor) else torch.tensor(codes, dtype=torch.long)
|
||||
if code_tensor.ndim == 3:
|
||||
code_tensor = code_tensor.unsqueeze(0)
|
||||
if code_tensor.ndim != 4 or code_tensor.shape[-2:] != (8, 4):
|
||||
return None
|
||||
return code_tensor
|
||||
|
||||
|
||||
def llm2code2wav_async_chunk(
|
||||
transfer_manager: Any,
|
||||
pooling_output: dict[str, Any],
|
||||
@@ -62,57 +114,39 @@ def llm2code2wav_async_chunk(
|
||||
Accumulates codes in connector per request_id,
|
||||
returns payload only when chunk_size is full or request is finished; returns None when waiting.
|
||||
"""
|
||||
if "code_predictor_codes" not in pooling_output:
|
||||
|
||||
connector = getattr(transfer_manager, "connector", None)
|
||||
raw_cfg = getattr(connector, "config", {}) or {}
|
||||
cfg = raw_cfg.get("extra", raw_cfg) if isinstance(raw_cfg, dict) else {}
|
||||
chunk_size = int(cfg.get("codec_chunk_frames", 3))
|
||||
left_context_size = int(cfg.get("codec_left_context_frames", 3))
|
||||
|
||||
request_id = getattr(request, "external_req_id", None)
|
||||
|
||||
codes = pooling_output.get("code_predictor_codes")
|
||||
|
||||
if _is_codes_empty(codes):
|
||||
if is_finished:
|
||||
return _make_finished_sentinel()
|
||||
return _flush_remaining_codes(transfer_manager, request_id, chunk_size, left_context_size)
|
||||
return None
|
||||
|
||||
code_predictor_codes = pooling_output["code_predictor_codes"]
|
||||
|
||||
if code_predictor_codes is None:
|
||||
code_tensor = _to_code_tensor(codes)
|
||||
if code_tensor is None:
|
||||
if is_finished:
|
||||
return _make_finished_sentinel()
|
||||
return None
|
||||
if isinstance(code_predictor_codes, torch.Tensor):
|
||||
if code_predictor_codes.numel() == 0:
|
||||
if is_finished:
|
||||
return _make_finished_sentinel()
|
||||
return None
|
||||
elif hasattr(code_predictor_codes, "__len__"):
|
||||
if len(code_predictor_codes) == 0:
|
||||
return None
|
||||
|
||||
if isinstance(code_predictor_codes, torch.Tensor):
|
||||
if not code_predictor_codes.any():
|
||||
if is_finished:
|
||||
return _make_finished_sentinel()
|
||||
return None
|
||||
code_tensor = code_predictor_codes.to(torch.long)
|
||||
else:
|
||||
code_tensor = torch.tensor(code_predictor_codes, dtype=torch.long)
|
||||
if not code_tensor.any():
|
||||
if is_finished:
|
||||
return _make_finished_sentinel()
|
||||
return None
|
||||
|
||||
if code_tensor.ndim == 3:
|
||||
code_tensor = code_tensor.unsqueeze(0)
|
||||
if code_tensor.ndim != 4 or code_tensor.shape[-2:] != (8, 4):
|
||||
return _flush_remaining_codes(transfer_manager, request_id, chunk_size, left_context_size)
|
||||
return None
|
||||
|
||||
pad_vec = torch.tensor([TALKER_CODEC_PAD_TOKEN_ID] * 4, device=code_tensor.device, dtype=code_tensor.dtype)
|
||||
code_final = prepend_and_flatten_colmajor(code_tensor, pad_vec)
|
||||
code_list = code_final.tolist()
|
||||
code_list = prepend_and_flatten_colmajor(code_tensor, pad_vec).tolist()
|
||||
|
||||
if sum(code_list) == 0:
|
||||
if is_finished:
|
||||
return _make_finished_sentinel()
|
||||
return _flush_remaining_codes(transfer_manager, request_id, chunk_size, left_context_size)
|
||||
return None
|
||||
|
||||
request_id = getattr(request, "external_req_id", None)
|
||||
if request_id is None:
|
||||
return None
|
||||
|
||||
chunk_size = left_context_size = 10
|
||||
transfer_manager.code_prompt_token_ids[request_id].append(code_list)
|
||||
length = len(transfer_manager.code_prompt_token_ids[request_id])
|
||||
chunk_length = length % chunk_size
|
||||
@@ -121,14 +155,17 @@ def llm2code2wav_async_chunk(
|
||||
|
||||
context_length = chunk_length if chunk_length != 0 else chunk_size
|
||||
end_index = min(length, left_context_size + context_length)
|
||||
left_ctx_frames = max(0, min(length - context_length, left_context_size))
|
||||
flat_codes = torch.tensor(transfer_manager.code_prompt_token_ids[request_id][-end_index:]).reshape(-1).tolist()
|
||||
|
||||
info = {
|
||||
"code_predictor_codes": (
|
||||
torch.tensor(transfer_manager.code_prompt_token_ids[request_id][-end_index:]).reshape(-1).tolist()
|
||||
),
|
||||
return {
|
||||
"code_predictor_codes": flat_codes,
|
||||
"left_context_size": left_ctx_frames,
|
||||
"codec_chunk_frames": chunk_size,
|
||||
"codec_left_context_frames": left_context_size,
|
||||
"code_flat_numel": len(flat_codes),
|
||||
"finished": torch.tensor(is_finished, dtype=torch.bool),
|
||||
}
|
||||
return info
|
||||
|
||||
|
||||
def llm2code2wav(
|
||||
|
||||
Reference in new issue
Block a user