阿里DashScope SDK的真的能用么?简直就是代码里下毒,跑起来CPU占满。

597 阅读3分钟

事情的开始

最近,我在完成一项与同声传译相关的工作,其中需要用到语音识别(ASR, Automatic Speech Recognition)功能。研究了一圈市面上的ASR服务,包括字节、百度、阿里和讯飞,最后发现阿里的价格最便宜,于是选择了他们的服务。

然而,在查看阿里ASR的使用文档时,我发现只有SDK接口,没有提供OpenAPI。我想,既然没有OpenAPI,那就用SDK吧。于是我参考文档,尝试了一下他们的SDK,代码如下:

class Callback(RecognitionCallback):
    def on_complete(self) -> None:
        # 识别完成

    def on_error(self, result: RecognitionResult) -> None:
        # 错误处理
    
    def on_event(self, result: RecognitionResult) -> None:
        # 处理识别结果
 
callback = Callback()
recognition = Recognition(model='paraformer-realtime-v2',
                          format='pcm',
                          sample_rate=16000,
                          callback=callback)
recognition.start()

一看,这SDK竟然不支持异步!而且写法非常的古怪,明显是根据java翻译而来的。我当前使用的是FastAPI框架,需要处理大量I/O操作,所以异步能力对我来说非常重要。进一步研究后发现,这个流式ASR通过WebSocket实现通信,底层启动了一个线程来处理逻辑。虽然可以用asyncio.to_thread将同步代码包装成异步,但这显然并不理想。

性能问题的暴露

当我开始开发同声传译的核心逻辑时,需要用到声音可用性检测(VAD, Voice Activity Detection),以判断声音是否中断。我使用的是基于机器学习的库 Silero VAD,这个操作相当耗费CPU资源。为了评估其性能,我用py-spy生成了火焰图,结果大吃一惊: image.png

火焰图显示,ASR中的几行代码消耗了极高的CPU资源。仔细检查调用栈后,发现以下代码是罪魁祸首: image.png 这段代码实现了一个生成器,通过单独线程监听WebSocket的返回结果。问题在于,当输入数据为空时,代码会空转,占用大量CPU资源。显然,这里缺少一个time.sleep()来释放CPU。

视频里的原始代码,由于没有开源,找了个存档,大家可以在这里品鉴。 github.com/ringsaturn/…

官方文档 help.aliyun.com/zh/dashscop…

优化实现

虽然加一个sleep能解决问题,但实在不够优雅。我改用asyncio.to_thread实现同步与异步之间的平滑转换,最终优化代码如下:

    async def a_asr(
        self, input_q: queue.Queue[bytes]
    ) -> AsyncGenerator[RecognitionResponse, None]:
        out_q = asyncio.Queue()
        asr_task = asyncio.create_task(asyncio.to_thread(self.asr, input_q, out_q))

        while data := await out_q.get():
            yield data

        await asr_task

    def asr(
        self,
        input_q: queue.Queue[bytes],
        out_q: asyncio.Queue,
    ):
        def input_generator() -> Generator[bytes, None, None]:
            while data := input_q.get():
                yield data

        response_stream = self.call(
            model=self.model,
            task_group="audio",
            task="asr",
            function="recognition",
            input=input_generator(),
            api_protocol=ApiProtocol.WEBSOCKET,
            ws_stream_mode=WebsocketStreamingMode.DUPLEX,
            is_binary_input=True,
            sample_rate=16000,
            format="wav",
            stream=True,
        )

        for part in response_stream:
            if part.status_code != HTTPStatus.OK:
                raise Exception(part.message)
            out_q.put_nowait(RecognitionResponse.from_api_response(part))

        out_q.put_nowait(None)

优化后的代码运行效果明显改善,火焰图显示CPU时间主要用于VAD处理,线程切换的开销显著减少: image.png

最后

不知道阿里的DashScope水平为什么是这样。2024年了,依然没有提供OpenAPI,Python SDK缺乏异步支持(实际上内部是将异步功能转成了同步调用 😷)。更令人费解的是,同步代码实现水平令人堪忧,显然是并非python开发者实现的,且并有进行过详细的代码评审和单元测试,这种问题在现代开发中堪称“下毒”。使用这样的SDK,不禁让人怀疑,这真的是面向开发者的吗?