Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

延迟分析相关问题 #845

Open
ZhouFangru opened this issue Feb 19, 2025 · 21 comments
Open

延迟分析相关问题 #845

ZhouFangru opened this issue Feb 19, 2025 · 21 comments
Assignees

Comments

@ZhouFangru
Copy link

Image
您好,图片显示tts延时0.1s,想问一下minicpm是因为什么可以使得延时这么短呢?
因为对于短文本(3~5个字),我自己测试chattts(https://github.com/2noise/ChatTTS)的延时在4090大概是0.5s左右。
期待您的解答~

@ZhouFangru
Copy link
Author

并且根据这个延时来看,端到端的延时好像比两阶段还慢?在4090上,短问题(今天天气怎么样)qwen2延时0.1+chattts延时0.5=0.6s,而相同的问题在4090上测试minicpmo(声音克隆)延时0.7s左右。
是否是我测试的有什么问题呢,minicpm-o在延时上相对于两阶段的语音克隆有优势吗?

@bokesyo
Copy link
Collaborator

bokesyo commented Feb 20, 2025

您好,我负责MiniCPM-o语音模态。感谢您的反馈!

表格里写的是首响,也就是最开始的一段音频输出。

o2.6可以流式输出音频,也可以非流式输出音频。ChatTTS是一个非流式模型,我们在训练o2.6时把ChatTTS改成了流式版本,可以在只生成非常少文本的情况下就立刻产生第一段语音输出。

  • 在流式状态下,首响其实很低,所以可以比Qwen2+ChatTTS快很多。
  • 但如果是非流式模式,速度和Qwen2+ChatTTS应该是一样的。您测试的应该是在命令行下跑的非流式模式。

不知道是否解答了您的疑问?

@bokesyo bokesyo self-assigned this Feb 20, 2025
@ZhouFangru
Copy link
Author

ZhouFangru commented Feb 20, 2025

您好,我负责MiniCPM-o语音模态。感谢您的反馈!

表格里写的是首响,也就是最开始的一段音频输出。

o2.6可以流式输出音频,也可以非流式输出音频。ChatTTS是一个非流式模型,我们在训练o2.6时把ChatTTS改成了流式版本,可以在只生成非常少文本的情况下就立刻产生第一段语音输出。

  • 在流式状态下,首响其实很低,所以可以比Qwen2+ChatTTS快很多。
  • 但如果是非流式模式,速度和Qwen2+ChatTTS应该是一样的。您测试的应该是在命令行下跑的非流式模式。

不知道是否解答了您的疑问?

您好,不是的,我测试的是流式的首token延时。不知道您说的快很多是多少,我这边测试延时如上述所示,的确不算太快。
仔细看了代码,发现有一部分应该是可以优化,在“_generate_mel_spec_audio_streaming”函数中,因为音频融合,在第二次生成音频时才会返回首token。

Image
如上修改以后,首token延时会比两阶段快。(第一次生成音频就返回,保留音频后半部分和之后的音频做融合)
不知道我理解的是否正确,目前输出的效果是正常的,修改以后的延时也可以达到我的要求了。
感谢~

@ZhouFangru
Copy link
Author

ZhouFangru commented Feb 20, 2025

  • 首响其实很低,所以可以比Qwen2+ChatTTS快很多。

您好,您这边是否可以提供一个minicpm语音克隆首token延时指标呢,我想判断一下我的操作是否有问题。如果是我这边操作有误,该如何修改?

@ZhouFangru
Copy link
Author

可以在只生成非常少文本的情况下就立刻产生第一段语音输出。

我看默认是收集10个字会生成第一段语音,您说的非常少文本是指多少文本呢?如果将参数streaming_text_chunk_size改小,好像会影响效果?(改成5以后,句子连贯性好像有点问题,而且生成的语音会在最后循环)这个参数一般设置多少比较合适?

@bokesyo
Copy link
Collaborator

bokesyo commented Feb 20, 2025

很少token其实就是streaming_text_chunk_sizestreaming_text_chunk_size 是训练时随机数,从7到10都训练过,为了效果最好,非常建议用10。

可以在只生成非常少文本的情况下就立刻产生第一段语音输出。

我看默认是收集10个字会生成第一段语音,您说的非常少文本是指多少文本呢?如果将参数streaming_text_chunk_size改小,好像会影响效果?(改成5以后,句子连贯性好像有点问题,而且生成的语音会在最后循环)这个参数一般设置多少比较合适?

@bokesyo
Copy link
Collaborator

bokesyo commented Feb 20, 2025

您好,我负责MiniCPM-o语音模态。感谢您的反馈!
表格里写的是首响,也就是最开始的一段音频输出。
o2.6可以流式输出音频,也可以非流式输出音频。ChatTTS是一个非流式模型,我们在训练o2.6时把ChatTTS改成了流式版本,可以在只生成非常少文本的情况下就立刻产生第一段语音输出。

  • 在流式状态下,首响其实很低,所以可以比Qwen2+ChatTTS快很多。
  • 但如果是非流式模式,速度和Qwen2+ChatTTS应该是一样的。您测试的应该是在命令行下跑的非流式模式。

不知道是否解答了您的疑问?

您好,不是的,我测试的是流式的首token延时。不知道您说的快很多是多少,我这边测试延时如上述所示,的确不算太快。 仔细看了代码,发现有一部分应该是可以优化,在“_generate_mel_spec_audio_streaming”函数中,因为音频融合,在第二次生成音频时才会返回首token。

Image 如上修改以后,首token延时会比两阶段快。(第一次生成音频就返回,保留音频后半部分和之后的音频做融合) 不知道我理解的是否正确,目前输出的效果是正常的,修改以后的延时也可以达到我的要求了。 感谢~

我看了一下您的意思,您的思路理论上没有任何问题,但具体代码我还需要再确认一下~

@bokesyo
Copy link
Collaborator

bokesyo commented Feb 20, 2025

  • 首响其实很低,所以可以比Qwen2+ChatTTS快很多。

您好,您这边是否可以提供一个minicpm语音克隆首token延时指标呢,我想判断一下我的操作是否有问题。如果是我这边操作有误,该如何修改?

好的,理论的延时是 llm 部分 prefill 再 decode 出5-6个文本llm 文本token(约对应10个TTS 文本token),然后TTS部分再prefill(10个TTS 文本token),然后decode出大约25个音频token,这些加起来就是首响中最耗时的部分,然后prefill其实很快,decode的话不同gpu有差距,可以尝试在llm prefill, llm decode, tts prefill, tts decode处加上print看下:

begin: https://huggingface.co/openbmb/MiniCPM-o-2_6/blob/f3436794e9416fd369750dca25473191d0197090/modeling_minicpmo.py#L1509

llm decode done: https://huggingface.co/openbmb/MiniCPM-o-2_6/blob/f3436794e9416fd369750dca25473191d0197090/modeling_minicpmo.py#L1628

tts prefill done: https://huggingface.co/openbmb/MiniCPM-o-2_6/blob/f3436794e9416fd369750dca25473191d0197090/modeling_minicpmo.py#L1652

tts decode done: https://huggingface.co/openbmb/MiniCPM-o-2_6/blob/f3436794e9416fd369750dca25473191d0197090/modeling_minicpmo.py#L1668

在4090上,具体 @tc-mb 可以给一个具体的数字嘛?

@ZhouFangru
Copy link
Author

感谢您的解答,方便问一下tts的训练损失设计嘛?
“在训练过程中,来自语音解码器的梯度会反向传播到包含大语言模型主干和音频编码器的整个模型参数。模型通过端到端方式训练,没有使用任何中间损失和监督。“
技术文档中,看起来llm和tts是联合训练的,但是又说没有用中间损失。所以想问一下llm和tts是同时训练嘛,是分别有一个损失嘛?

@ZhouFangru
Copy link
Author

ZhouFangru commented Feb 21, 2025

抱歉,还想问一下,在生成语音的时候,for 循环文本,当个数达到streaming_text_chunk_size以后,就去生成,for循环结束的时候,为什么会存在语音生成没有结束的情况,即outputs.finished=Flase,并且进入1740行的if语句中继续生成。并且继续生成的时间挺长的,导致生成音频的总时间很长。
是因为tts是流式,所以每次返回的不是整个文本的语音嘛?但是为什么总时间这么长呢?生成40个字的音频,需要差不多20秒。我看chattts官方有参数compile的设置,是否是因为这个导致总的生成时间很长呢?
期待您的解答。感谢~

@bokesyo
Copy link
Collaborator

bokesyo commented Feb 27, 2025

抱歉,还想问一下,在生成语音的时候,for 循环文本,当个数达到streaming_text_chunk_size以后,就去生成,for循环结束的时候,为什么会存在语音生成没有结束的情况,即outputs.finished=Flase,并且进入1740行的if语句中继续生成。并且继续生成的时间挺长的,导致生成音频的总时间很长。 是因为tts是流式,所以每次返回的不是整个文本的语音嘛?但是为什么总时间这么长呢?生成40个字的音频,需要差不多20秒。我看chattts官方有参数compile的设置,是否是因为这个导致总的生成时间很长呢? 期待您的解答。感谢~

是这样的:是因为tts是流式,所以每次返回的不是整个文本的语音。语音和文本不是对齐的,文本生成会有冗余性,于是当文本生成结束,语音生成并没有结束,还需要再继续生成。不是因为没有开启compile。

如果开启compile的话会让语音解码速度快2倍,但有一个初始化过程比较缓慢,我们目前也没有找到好的方法初始化一次之后后面都不重新初始化,所以就没有写到readme里。

@bokesyo
Copy link
Collaborator

bokesyo commented Feb 27, 2025

感谢您的解答,方便问一下tts的训练损失设计嘛? “在训练过程中,来自语音解码器的梯度会反向传播到包含大语言模型主干和音频编码器的整个模型参数。模型通过端到端方式训练,没有使用任何中间损失和监督。“ 技术文档中,看起来llm和tts是联合训练的,但是又说没有用中间损失。所以想问一下llm和tts是同时训练嘛,是分别有一个损失嘛?

是联合训练的,有两个loss,一个是LLM部分对文本的CE loss,且TTS部分是用了CE loss,与TTS训练时的loss一样的,TTS的loss会通过Speech embedding传给LLM。没有中间损失和辅助损失。

@ZhouFangru
Copy link
Author

抱歉,还想问一下,在生成语音的时候,for 循环文本,当个数达到streaming_text_chunk_size以后,就去生成,for循环结束的时候,为什么会存在语音生成没有结束的情况,即outputs.finished=Flase,并且进入1740行的if语句中继续生成。并且继续生成的时间挺长的,导致生成音频的总时间很长。 是因为tts是流式,所以每次返回的不是整个文本的语音嘛?但是为什么总时间这么长呢?生成40个字的音频,需要差不多20秒。我看chattts官方有参数compile的设置,是否是因为这个导致总的生成时间很长呢? 期待您的解答。感谢~

是这样的:是因为tts是流式,所以每次返回的不是整个文本的语音。语音和文本不是对齐的,文本生成会有冗余性,于是当文本生成结束,语音生成并没有结束,还需要再继续生成。不是因为没有开启compile。

如果开启compile的话会让语音解码速度快2倍,但有一个初始化过程比较缓慢,我们目前也没有找到好的方法初始化一次之后后面都不重新初始化,所以就没有写到readme里。

所以按照目前代码的逻辑,生成40个字的音频需要20s是正常的嘛~?

@tc-mb
Copy link
Collaborator

tc-mb commented Feb 28, 2025

抱歉,还想问一下,在生成语音的时候,for 循环文本,当个数达到streaming_text_chunk_size以后,就去生成,for循环结束的时候,为什么会存在语音生成没有结束的情况,即outputs.finished=Flase,并且进入1740行的if语句中继续生成。并且继续生成的时间挺长的,导致生成音频的总时间很长。 是因为tts是流式,所以每次返回的不是整个文本的语音嘛?但是为什么总时间这么长呢?生成40个字的音频,需要差不多20秒。我看chattts官方有参数compile的设置,是否是因为这个导致总的生成时间很长呢? 期待您的解答。感谢~

是这样的:是因为tts是流式,所以每次返回的不是整个文本的语音。语音和文本不是对齐的,文本生成会有冗余性,于是当文本生成结束,语音生成并没有结束,还需要再继续生成。不是因为没有开启compile。
如果开启compile的话会让语音解码速度快2倍,但有一个初始化过程比较缓慢,我们目前也没有找到好的方法初始化一次之后后面都不重新初始化,所以就没有写到readme里。

所以按照目前代码的逻辑,生成40个字的音频需要20s是正常的嘛~?

40个字大概对应26.6个tokens,一秒7个token,所以大概需要4秒。
而4090应该能充分的满足流式能力,如果你这里的时间是20秒,可能是有什么问题。
建议你先别像上面从原理上找,问题不出在那里,先看下速度耗时多出去的在哪里比较好。

@ZhouFangru
Copy link
Author

好的,感谢您的解答~

@bokesyo
Copy link
Collaborator

bokesyo commented Feb 28, 2025 via email

@ZhouFangru
Copy link
Author

ZhouFangru commented Feb 28, 2025

似乎不太正常,您用的是什么设备呢?如果是4090,40字相当于10s,需要6s生成。

获取 Outlook for iOShttps://aka.ms/o0ukef

抱歉,是我的问题,之前测试时gpu有其他进程占用,40字,6s,没问题的。

@Lastshadow999
Copy link

@ZhouFangru @bokesyo 感谢你们的讨论对我非常有帮助。@ZhouFangru 我对您关于延迟的改进很感兴趣,可以开源修改部分的代码吗?十分感谢!

@ZhouFangru
Copy link
Author

ZhouFangru commented Mar 4, 2025

@ZhouFangru @bokesyo 感谢你们的讨论对我非常有帮助。@ZhouFangru 我对您关于延迟的改进很感兴趣,可以开源修改部分的代码吗?十分感谢!

您好,在返回音频时做了小的改动。以下为相关代码:

                    fast_zfr = True
                    overlap = 512*4
                    if prev_wav is not None:
                        if fast_zfr:
                            wav_np, prev_wav = self._linear_overlap_add2_wav2(
                                [prev_wav, wav_np], overlap=overlap
                            )  # tts_hop256*2
                        else:
                            wav_np, prev_wav = self._linear_overlap_add2_wav(
                                [prev_wav, wav_np], overlap=overlap
                            )  # tts_hop256*2
                        
                        cur_text = gen_text_raw[prev_text_len:]
                        prev_text_len = len(gen_text_raw)
                        yield OmniOutput(text=cur_text, audio_wav=wav_np, sampling_rate=sr)

                    else:
                        if fast_zfr:
                            prev_wav = wav_np[-overlap:]
                            wav_np = wav_np[:-overlap]
                            cur_text = gen_text_raw[prev_text_len:]
                            prev_text_len = len(gen_text_raw)
                            yield OmniOutput(text=cur_text, audio_wav=wav_np, sampling_rate=sr)
                        else:
                            prev_wav = wav_np
def _linear_overlap_add2_wav2(self, frames: List[torch.Tensor], overlap: int):
       """
       Merge two audio waveforms with smooth in streaming audio generation.
       Borrowed some codes from `https://github.com/huggingface/transformers/blob/main/src/transformers/models/encodec/modeling_encodec.py`
       """
       assert len(frames) == 2
       device = frames[0].device
       dtype = frames[0].dtype
       # shape = frames[0].shape[:-1]

       frame0_length = frames[0].shape[-1] #上个音频最后overlap大小
       frame1_length = frames[1].shape[-1]
       total_size = frame0_length + frame1_length - overlap
       weight_len = max(frame0_length, frame1_length) + overlap
       t = torch.linspace(0, 1, weight_len + 2, device=device, dtype=dtype)[1:-1]
       weight = 0.5 - (t - 0.5).abs()

       sum_weight = torch.zeros(total_size, device=device, dtype=dtype)
       out = torch.zeros(total_size, device=device, dtype=dtype)
       offset: int = 0

       out[offset : offset + frame0_length] += weight[-frame0_length:] * frames[0]
       sum_weight[offset : offset + frame0_length] += weight[-frame0_length:]
       offset += frame0_length - overlap
       out[offset : offset + frame1_length] += weight[:frame1_length] * frames[1]
       sum_weight[offset : offset + frame1_length] += weight[:frame1_length]

       assert sum_weight.min() > 0
       out = out / sum_weight
       return out[:frame1_length- overlap], out[frame1_length- overlap:]

@Lastshadow999
Copy link

@ZhouFangru @bokesyo 感谢你们的讨论对我非常有帮助。@ZhouFangru 我对您关于延迟的改进很感兴趣,可以开源修改部分的代码吗?十分感谢!

您好,在返回音频时做了小的改动。以下为相关代码:

                    fast_zfr = True
                    overlap = 512*4
                    if prev_wav is not None:
                        if fast_zfr:
                            wav_np, prev_wav = self._linear_overlap_add2_wav2(
                                [prev_wav, wav_np], overlap=overlap
                            )  # tts_hop256*2
                        else:
                            wav_np, prev_wav = self._linear_overlap_add2_wav(
                                [prev_wav, wav_np], overlap=overlap
                            )  # tts_hop256*2
                        
                        cur_text = gen_text_raw[prev_text_len:]
                        prev_text_len = len(gen_text_raw)
                        yield OmniOutput(text=cur_text, audio_wav=wav_np, sampling_rate=sr)

                    else:
                        if fast_zfr:
                            prev_wav = wav_np[-overlap:]
                            wav_np = wav_np[:-overlap]
                            cur_text = gen_text_raw[prev_text_len:]
                            prev_text_len = len(gen_text_raw)
                            yield OmniOutput(text=cur_text, audio_wav=wav_np, sampling_rate=sr)
                        else:
                            prev_wav = wav_np
def _linear_overlap_add2_wav2(self, frames: List[torch.Tensor], overlap: int):
       """
       Merge two audio waveforms with smooth in streaming audio generation.
       Borrowed some codes from `https://github.com/huggingface/transformers/blob/main/src/transformers/models/encodec/modeling_encodec.py`
       """
       assert len(frames) == 2
       device = frames[0].device
       dtype = frames[0].dtype
       # shape = frames[0].shape[:-1]

       frame0_length = frames[0].shape[-1] #上个音频最后overlap大小
       frame1_length = frames[1].shape[-1]
       total_size = frame0_length + frame1_length - overlap
       weight_len = max(frame0_length, frame1_length) + overlap
       t = torch.linspace(0, 1, weight_len + 2, device=device, dtype=dtype)[1:-1]
       weight = 0.5 - (t - 0.5).abs()

       sum_weight = torch.zeros(total_size, device=device, dtype=dtype)
       out = torch.zeros(total_size, device=device, dtype=dtype)
       offset: int = 0

       out[offset : offset + frame0_length] += weight[-frame0_length:] * frames[0]
       sum_weight[offset : offset + frame0_length] += weight[-frame0_length:]
       offset += frame0_length - overlap
       out[offset : offset + frame1_length] += weight[:frame1_length] * frames[1]
       sum_weight[offset : offset + frame1_length] += weight[:frame1_length]

       assert sum_weight.min() > 0
       out = out / sum_weight
       return out[:frame1_length- overlap], out[frame1_length- overlap:]

感谢大佬!不知道您在播放音频时卡顿吗,这个缓冲策略大概是怎么设计的?现在我在前端收集两段再播放,播放时再异步缓冲,但仍有地方明显卡顿。不知道如何平衡卡顿和延迟

@ZhouFangru
Copy link
Author

ZhouFangru commented Mar 4, 2025

感谢您的解答,方便问一下tts的训练损失设计嘛? “在训练过程中,来自语音解码器的梯度会反向传播到包含大语言模型主干和音频编码器的整个模型参数。模型通过端到端方式训练,没有使用任何中间损失和监督。“ 技术文档中,看起来llm和tts是联合训练的,但是又说没有用中间损失。所以想问一下llm和tts是同时训练嘛,是分别有一个损失嘛?

是联合训练的,有两个loss,一个是LLM部分对文本的CE loss,且TTS部分是用了CE loss,与TTS训练时的loss一样的,TTS的loss会通过Speech embedding传给LLM。没有中间损失和辅助损失。

@bokesyo 请问训练时Speech embedding如何提取呢,在推理过程,模型输入会给一个生成提示并得到spk_bounds,然后将spk_bounds位置的特征输入tts。但是在训练过程,输入并没有生成spk_bounds,这时如何提取Speech embedding呢?
期待您的解答,感谢~

@ZhouFangru ZhouFangru reopened this Mar 4, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants