呜… 大过年的终于是把整个假期消磨在晚六朝三(六点睡十五点起;12 小时制)的离谱作息里了,过年前说好的写博客记录配置的豪言壮语也是不知道抛到哪里去了;趁着假期还没完全结束(我还没忘干净),就把我的一点心得记录一下吧。
背景
事情的起因是这样的:音色克隆是个危险的事情,把私有的数据交给云端,哪怕只有一句话,都有可能被日渐先进的技术去噪,扩增到几个小时,训练变声模型,巴拉巴拉拿来干什么就不知道了(没错我就是这么干的)。正好手上有 gpu 运算资源,VoxCPM 这种 Zero-Shot 的 TTS 效果也已经不错了,于是便打算本地部署一个推理服务。
推理
推理好啊,写成 OpenAI 兼容的 API 端点就可以无缝兼容各种成熟的应用了。正好,VoxCPM 的 README 里就有社区项目:voxcpm-fastapi,将原生调用包装成了 OpenAI 兼容的端点。
一顿操作猛如虎,一看推理速度5。在我的 RTX 4060 上,它的推理速度大概只有 5~6 it/s。尽管 RTF 勉强小于 1 吧,但现在的应用很少有实现流式 TTS 的 —— 其实技术复杂度挺高的,你需要类似 WebRTC 的实现,得到的就只是不需要像视频那样鼠标跟手的音频(又不是变声器)。而非流式就意味着热启动在几秒内不能生成一段不短的音频的话,直观感受就是挺慢的。
我也换到 RTX 4070 Ti Super 上试了下(是的,我的家里云有两张不同型号的N卡),上下文短的时候有 30 it/s 左右(其实这两张卡算力差距大概只是翻倍,这个推理速度差距大概率意味着 CUDA OOM 被 Windows 交换到内存了,这是推理服务这种计算密集型任务要极力避免的),但整个 pipeline 走完还是挺慢的。
关键是,原生 Pytorch 推理,不支持并发!FastAPI 的并发请求,就只能积压等待串行处理。作为一个用户会靠切分长请求解决高延迟和长上下文问题的服务,这显然是不可接受的。
咋办?回去翻 VoxCPM 的 README。还有一个社区项目,nanovllm-voxcpm,将 VoxCPM 里的 LLM 部分用 nanovLLM(一个使用 vLLM 优化思路达到相近的性能但轻量得多的 LLM 推理框架)实现了。
众所周知,vLLM 作为生产级的推理框架,显存优化和并发推理不是手拿把掐——于是让 LLM 帮我用它替换原生 Pytorch 推理,达到了 4060 上 ~30 step/s,4070 Ti Super 上 ~60 step/s,RTF 逼近不足 0.1 的性能。这对于我的应用场景已经绰绰有余(其实应该已经远超 VoxCPM 团队的预期了,但我没做详细测试,团队是在 4090 上测试的,也没啥比较的意义)。接下来,就是部署了。
部署
家里云嘛,能装 Windows 还装 Linux 多浪费;wsl 嘛,交互式用用还行,拿来跑服务就不太优雅了;至于 Docker,这么重的环境,也不够优雅;何况虚拟化必然有性能开销,尤其是对于计算密集型任务。然而众所周知,机器学习的推理对 Windows 平台并不友好。
首先,在原生 Pytorch 推理的时候torch.compile 需要的 triton 库就没有。这倒是好办,装对应版本(务必看清楚版本对应矩阵)的windows-triton就好了。(附:不 compile 的话大概是 4060 ~3 it/s, 4070 Ti Super ~17 it/s;compile的话加载模型会慢不少,是正常的)
然而,当换到 nanovLLM,依赖地狱才刚刚开始。vLLM 类框架需要的不只是 triton,还有 Flash Attention (flash-attn,一个更快的,显存更高效的注意力实现)。而这个东西,在 Windows 平台,没!有!预!构!建!编译这玩意儿大概在十几核的现代小型工作站(或者说,GitHub 的 CI Runner)上都要至少3个小时。幸好,GitHub 上有人 fork 了上传了预构建。只要下载安装对应版本(务必看清楚torch(包括cuda版本)和cpython的版本对应关系,以及预装相应版本的 CUDA Toolkit 并确保CUDA_PATH环境变量配置正确)即可正常运行。。。吗?
并不能。你大概率会收到报错:nccl 在 Windows 上不被支持。这是什么?这是分布式计算的后端。是,我知道我们没有用张量并行,理论上用不着分布式,但生产级的推理大概率不会是单卡的,为单卡额外维护一套逻辑没必要,典型的处理方法是起一个只有一张卡的“伪”分布式环境。然而,nanovLLM的分布式计算后端是硬编码的nccl 。那么,只要改成 Windows 可用的gloo ,就可以了。。。吗?
还是不能。因为:Pytorch 2.8~2.9 的内置gloo,是坏的!理论上这个问题在 2.10 应该已经修复(没试过,但 nightly 肯定是好的),但预构建的flash-attn 未必跟得上。所以,我直接回退到了 2.7 版本,重装了相关依赖(顺便提一句,如果是choco装的ffmpeg ,torchcodec会找不到它,但我已经提PR了),然后——
终于能正常运行了!看着龟速爬行的 tqdm 进度条被不断闪过的 200 响应日志取代,我只想说一句话:
我作业忘写了啊啊啊!!!
(PS:如果评论区有人感兴趣的话,我就抽空把我的方案精简一下以更“可部署”的方式分享出来)