解决 ZeroGPU Space 中的 NaN 张量和 Pickling 错误

社区文章 发布于 2024 年 11 月 13 日

大家好,在这篇文章中,我将讨论最近我在处理 XTTS Space 和 Hugging Face 的 ZeroGPU 时遇到的一个难题。

这个问题让我对 Python、Hugging Face 和 ZeroGPU 有了很多新的认识!我希望它能帮助到正在经历类似问题的人!

葡萄牙语版本

Space

涉及的 Space 是这个:https://huggingface.co/spaces/rrg92/xtts
这个 Space 包含一个 XTTS 版本,它是一个文本转语音 (TTS) 模型和语音克隆模型!
有用户尝试使用这个 Space 时评论说语音克隆不起作用。我去测试时也发现它不起作用,但其他功能都正常。
为了让大家理解这个问题涉及的主要组件,我将总结一下其结构

  • 这是一个 Gradio Space,使用 Gradio 5.5.0 版本。

  • 有两个主要文件(模块):xttsapp

    • xtts 模块是我放置所有用于调用 XTTS 模型及其直接交互函数的地方。
    • app 模块是 Gradio 应用程序所在的位置,以及其各自的 event_listeners。因此,它们会调用 xtts 模块中的函数。
    • 这个结构是对 xtts-streaming-server 项目的微小调整。我将 API 和模型放在同一个应用程序中,这样我就可以在 Hugging Face 上使用 Gradio,主要的好处是:使用 ZeroGPU!
  • 在所有函数中,与问题相关的函数是这些

    • xtts.predict_speaker
      这是调用模型推理以克隆语音的函数。
      基本上,它接收参考音频文件的二进制数据,并通过调用模型计算语音嵌入。
      它使用 model.get_conditioning_latents 调用模型,并传入文件二进制数据。它返回这些嵌入,这些嵌入稍后可以作为说话人语音发送到 xtts.predict_speech

    • xtts.predict_speech
      这是将文本转换为语音的函数。
      它接受的参数中,对我们最重要的是:要转换的文本和 speaker
      这个 speaker 是代表语音的嵌入。
      XTTS 带有各种标准、高质量的录音棚语音,我们也可以使用 xtts.predict_speaker 生成新的嵌入。
      无论如何,这些都是主要参数。该函数返回生成音频的二进制数据。

    • app.clone_voice
      这是当有人点击克隆语音按钮时触发的函数。
      它接收用户在 Gradio 界面中提供的参考音频作为其第一个参数。它是一个包含文件路径的字符串。
      然后,我们使用 Python 的 open 函数(rb 模式)打开文件,并调用 xtts.predict_speaker 函数,传入 open 返回的二进制数据。

    • app.tts
      这是当用户在 Gradio 界面上点击 TTS 按钮时调用的函数。
      该函数执行一系列操作,但归结为:确定文本、界面中选择的 speaker 的嵌入,并调用 xtts.predict_speech

最后,由于我希望使用 ZeroGPU 运行 TTS,我用 @spaces.GPU 装饰器装饰了 xtts.predict_speech 函数。这是 Hugging Face 官方文档中说明的在使用 GPU 时的过程

现在,您已经了解了 Space 的结构。让我们深入探讨我发现的两个问题!

问题 1:概率张量包含 inf、nan 或元素 < 0

我在克隆过程中注意到的第一个问题是尝试使用克隆语音生成文本时返回的错误

Traceback (most recent call last):
  File "/usr/local/lib/python3.10/site-packages/spaces/zero/wrappers.py", line 256, in thread_wrapper
    res = future.result()
  File "/usr/local/lib/python3.10/concurrent/futures/_base.py", line 451, in result
    return self.__get_result()
  File "/usr/local/lib/python3.10/concurrent/futures/_base.py", line 403, in __get_result
    raise self._exception
  File "/usr/local/lib/python3.10/concurrent/futures/thread.py", line 58, in run
    result = self.fn(*self.args, **self.kwargs)
  File "/home/user/app/xtts.py", line 185, in predict_speech
    out = model.inference(
  File "/usr/local/lib/python3.10/site-packages/torch/utils/_contextlib.py", line 116, in decorate_context
    return func(*args, **kwargs)
  File "/usr/local/lib/python3.10/site-packages/TTS/tts/models/xtts.py", line 548, in inference
    gpt_codes = self.gpt.generate(
  File "/usr/local/lib/python3.10/site-packages/TTS/tts/layers/xtts/gpt.py", line 592, in generate
    gen = self.gpt_inference.generate(
  File "/usr/local/lib/python3.10/site-packages/torch/utils/_contextlib.py", line 116, in decorate_context
    return func(*args, **kwargs)
  File "/usr/local/lib/python3.10/site-packages/transformers/generation/utils.py", line 2215, in generate
    result = self._sample(
  File "/usr/local/lib/python3.10/site-packages/transformers/generation/utils.py", line 3249, in _sample
    next_tokens = torch.multinomial(probs, num_samples=1).squeeze(1)
RuntimeError: probability tensor contains either `inf`, `nan` or element < 0
Traceback (most recent call last):
  File "/usr/local/lib/python3.10/site-packages/gradio/queueing.py", line 624, in process_events
    response = await route_utils.call_process_api(
  File "/usr/local/lib/python3.10/site-packages/gradio/route_utils.py", line 323, in call_process_api
    output = await app.get_blocks().process_api(
  File "/usr/local/lib/python3.10/site-packages/gradio/blocks.py", line 2015, in process_api
    result = await self.call_function(
  File "/usr/local/lib/python3.10/site-packages/gradio/blocks.py", line 1562, in call_function
    prediction = await anyio.to_thread.run_sync(  # type: ignore
  File "/usr/local/lib/python3.10/site-packages/anyio/to_thread.py", line 56, in run_sync
    return await get_async_backend().run_sync_in_worker_thread(
  File "/usr/local/lib/python3.10/site-packages/anyio/_backends/_asyncio.py", line 2441, in run_sync_in_worker_thread
    return await future
  File "/usr/local/lib/python3.10/site-packages/anyio/_backends/_asyncio.py", line 943, in run
    result = context.run(func, *args)
  File "/usr/local/lib/python3.10/site-packages/gradio/utils.py", line 865, in wrapper
    response = f(*args, **kwargs)
  File "/home/user/app/app.py", line 218, in tts
    generated_audio = xtts.predict_speech(ipts)
  File "/usr/local/lib/python3.10/site-packages/spaces/zero/wrappers.py", line 214, in gradio_handler
    raise res.value
RuntimeError: probability tensor contains either `inf`, `nan` or element < 0

image/png

此错误仅在使用克隆语音时发生,而使用录音棚语音时则不会。
而且,它发生在 TTS 时,而不是在克隆语音时。换句话说,它发生在 xtts.predict_speech 函数中。

此外,在我的本地测试中,我没有遇到任何问题。

如果您查看空间文件,您会看到有一个 Dockerfile 已创建。
这个 Docker 是为了我在本地测试时使用的。
如果你想在本地尝试这个空间,只需运行 git clone,然后运行 docker compose up

最重要的是,堆栈的最后一条消息引用了 spaces 库中的一个文件。
所有这些让我相信差异在于与 ZeroGPU 相关的东西,因为它是本地测试和 ZeroGPU 的主要区别之一。

由于消息提到了张量,并且在堆栈中提到了 predict_speech 函数,我决定做的第一件事是包含语音嵌入的 print。具体来说,我在这个函数的两个点添加了 print

@spaces.GPU
def predict_speech(parsed_input: TTSInputs):
    print("device", model.device)
    speaker_embedding = torch.tensor(parsed_input.speaker_embedding).unsqueeze(0).unsqueeze(-1)
    gpt_cond_latent = torch.tensor(parsed_input.gpt_cond_latent).reshape((-1, 1024)).unsqueeze(0)
    
    print(speaker_embedding)
    
    print("latent:")
    print(gpt_cond_latent)

image/png

我希望能够确认至少有一些张量值为 NaN……然后成功了。

image/png

image/png

不仅其中一个张量的值是 NaN,而且所有张量的值都是 NaN。
如果你看一下函数,它返回两个代表说话者的值。两者都是张量,而且它们都是 NaN。请记住,在克隆语音的情况下,这些张量是由 xtts.predict_speaker 函数生成的。

因此,我决定深入研究源代码,并将打印语句直接添加到此函数的输出中

def predict_speaker(wav_file):
    """Compute conditioning inputs from reference audio file."""
    temp_audio_name = next(tempfile._get_candidate_names())
    with open(temp_audio_name, "wb") as temp, torch.inference_mode():
        print("device", model.device)
        temp.write(io.BytesIO(wav_file.read()).getbuffer())
        gpt_cond_latent, speaker_embedding = model.get_conditioning_latents(
            temp_audio_name
        )
        
        print(gpt_cond_latent);
        print(speaker_embedding);
        
    result = {
        "gpt_cond_latent": gpt_cond_latent.cpu().squeeze().half().tolist(),
        "speaker_embedding": speaker_embedding.cpu().squeeze().half().tolist(),
    }
    
    print(result);
    
    return result;

image/png

我又一次看到,在 model.get_conditioning_latents 的输出中,张量已经以 NaN 的形式出现。

我深入研究了 XTTS 源代码,以了解这是如何完成的。

image/png

这是 forked coqui-tts 代码的一部分

由于两个计算出的嵌入都是 NaN,我查看了首先计算的 speaker_embedding。这个函数基本上是转换音频的采样率并调用 hifigan_decoder 对象的一个方法。
我之前不知道这个,但我看到有一篇关于名为 HiFi-GAN 的神经网络的论文:https://arxiv.org/abs/2010.05646 但从快速阅读来看,我看到它是一个用于合成语音的网络……这显然对语音克隆来说非常有意义!

尽管我对这个层面的知识有限,但我注意到此时 to 方法被大量调用,将张量放置到另一个设备。这让我思考这段代码如何能够工作,考虑到这里没有 GPU 参与,只有 CPU。然后,我记得一个简单的细节:predict_speaker 函数在 CPU 上运行,而 predict_speech 函数在 GPU 上……我猜想这可能存在一些不兼容问题……

当我添加日志以查看 XTTS 模型加载到哪个设备时,这变得更加奇怪。这是代码片段

image/png

以下是生成的日志

image/png

让我感到惊讶的是以下几点

  • device 变量以“cuda”值开头,到目前为止一切顺利,因为这是其意图。
  • 接下来,就在下面,有一个检查:如果 torch 中 cuda 不可用,则生成错误……
    但没有生成错误……
    这意味着,即使代码在带有 ZeroGPU 且没有装饰器的 Space 中运行,它也会检测到 CUDA 确实可用。
  • 然后,模型被加载,并且如预期,加载到 CPU 上。“before”消息显示的值为“cpu”。
  • 然而,模型被移动到 CUDA,并且奇怪的是,它成功完成了……即使是无需装饰器即可运行的代码……

也就是说,我之前没有注意到这一点,但在没有装饰器的 ZeroGPU Space 中,模型很容易加载到 GPU 上……

这让我相信,当一个没有装饰器的函数运行时,这些对设备的移动可能会以某种方式在张量中生成 NaN。我仍然没有弄清楚确切的原因,我正在这个 Space 中进行测试:https://huggingface.co/spaces/rrg92/zero-test 尝试模拟这种情况。当我有了更新,我会发布。

为了解决这个问题,我只是将 @spaces.GPU 装饰器添加到 predict_speaker 函数中。

image/png

这正确地生成了张量……然而,当尝试克隆时,又出现了一个新错误……

问题 2:无法序列化 '_io.BufferedReader' 对象

xtts.predict_speaker 函数中添加装饰器后,尝试克隆时生成了一个错误。

Traceback (most recent call last):
  File "/usr/local/lib/python3.10/site-packages/spaces/utils.py", line 43, in put
    super().put(obj)
  File "/usr/local/lib/python3.10/multiprocessing/queues.py", line 371, in put
    obj = _ForkingPickler.dumps(obj)
  File "/usr/local/lib/python3.10/multiprocessing/reduction.py", line 51, in dumps
    cls(buf, protocol).dump(obj)
TypeError: cannot pickle '_io.BufferedReader' object


During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/usr/local/lib/python3.10/site-packages/gradio/queueing.py", line 624, in process_events
    response = await route_utils.call_process_api(
  File "/usr/local/lib/python3.10/site-packages/gradio/route_utils.py", line 323, in call_process_api
    output = await app.get_blocks().process_api(
  File "/usr/local/lib/python3.10/site-packages/gradio/blocks.py", line 2015, in process_api
    result = await self.call_function(
  File "/usr/local/lib/python3.10/site-packages/gradio/blocks.py", line 1562, in call_function
    prediction = await anyio.to_thread.run_sync(  # type: ignore
  File "/usr/local/lib/python3.10/site-packages/anyio/to_thread.py", line 56, in run_sync
    return await get_async_backend().run_sync_in_worker_thread(
  File "/usr/local/lib/python3.10/site-packages/anyio/_backends/_asyncio.py", line 2441, in run_sync_in_worker_thread
    return await future
  File "/usr/local/lib/python3.10/site-packages/anyio/_backends/_asyncio.py", line 943, in run
    result = context.run(func, *args)
  File "/usr/local/lib/python3.10/site-packages/gradio/utils.py", line 865, in wrapper
    response = f(*args, **kwargs)
  File "/home/user/app/app.py", line 127, in clone_speaker
    embeddings =  xtts.predict_speaker(open(upload_file,"rb"))
  File "/usr/local/lib/python3.10/site-packages/spaces/zero/wrappers.py", line 202, in gradio_handler
    worker.arg_queue.put(((args, kwargs), GradioPartialContext.get()))
  File "/usr/local/lib/python3.10/site-packages/spaces/utils.py", line 51, in put
    raise PicklingError(message)
_pickle.PicklingError: cannot pickle '_io.BufferedReader' object

image/png

这是当我尝试使用克隆语音生成音频时产生的错误。

现在是一个 pickle 错误……我不知道那是什么,经过一些研究,我了解到它与对象序列化有关,这是我从其他语言中了解到的一个过程。

基本上,我的函数调用中有些东西无法被序列化。
而且,由于唯一的区别是装饰器,我再次查看了装饰器代码,在问题发生的部分

image/png

我看到有问题的那部分是用来把东西放到队列里的……看了一下这个队列的代码,它不是很复杂,我注意到它基本上需要序列化这些对象。

image/png

由于错误消息提到了 _io.BufferedReader,并且我看到参数被序列化了,那么我立刻转向了传递给这个函数的参数:wav_file。这个参数是用户在界面中提供的文件。具体来说,是文件的二进制数据。它是通过 app.clone_speaker 以这种方式传递的

image/png

也就是说,我们以二进制模式打开文件并将其传递给函数……这样,xtts.predict_speaker 接收到一个二进制数据。我想象着,与其传递二进制数据,不如尝试传递路径,那将是一个字符串。所以我重写了它,以保持兼容性

image/png

image/png

瞧!克隆开始工作了!总而言之,有两个问题

  • xtts.predict_speaker 函数没有用 Space 装饰器装饰,由于某种我仍然不知道的原因,模型没有产生错误,也没有转移到 CPU,而是生成了 NaN 的张量。
    解决方案:将 @spaces.GPU 装饰器添加到 xtts.predict_speaker 函数中。

  • 将该函数包含在 ZeroGPU 中,由于参数类型的原因导致了错误;因为 ZeroGPU 会序列化参数。
    解决方案:传入包含文件路径的字符串,并在 xtts.predict_speaker 函数内部打开它。

最终思考

有趣的是,这引发了我一个新的问题:Hugging Face 是如何实现 ZeroGPU 的?我一直想知道,它是动态添加显卡,还是移动机器,或者是一个自定义驱动程序拦截调用并设法只将请求发送到带有 ZeroGPU 的机器……等等。总之,许多问题……

我创建了这个空间:https://huggingface.co/spaces/rrg92/zero-test

我正在其中进行测试,以帮助我回答所有仍未解决的问题。
总之,完成整个过程帮助我学到了很多关于 Python、PyTorch、Hugging Face、ZeroGPU 和 XTTS 的知识。这已经很值得了!

当我有更多答案时,我会更新这篇文章和/或发布新文章!

此外,我意识到我的调试方法——添加打印语句并从本地仓库推送更改——并不是最佳实践。我尝试使用开发空间模式,这是一个很棒的功能,但我遇到了一些困难,最终还是选择了这种老式方法。不过,我确实从经验中学到了一些东西,并希望下次能更有效地使用它。

特别感谢 @p3nGu1nZz 审阅本文并提供了宝贵的指导和许多提示,加深了我对人工智能的理解。他们目前正在开发一个名为 Tau 的激动人心项目。请务必 在 GitHub 上关注他们,以获取他们的最新动态!

在 Github 上关注我

感谢您的阅读!

社区

注册登录 发表评论