背景痛点:PC端语音合成服务的三座大山
把 ChatTTS 搬到 Windows 工作站后,最先撞上的不是算法精度,而是“PC 级”部署独有的三件套:
- 线程阻塞:默认的
torch.nn.Module.forward()会霸占 Python GIL,10 路并发就能把延迟拉到 2 s 以上。 - GPU 内存泄漏:PyTorch 默认缓存 CUDA Context,连续跑 1 k 段文本后显存只增不减,最终触发
cudaMalloc retry failed。 - 音频流卡顿:Wave 采样率 24 kHz,单段 10 s 音频约 480 kB,如果一次性
send()到前端,浏览器端播放缓冲会频繁 underrun。
解决思路一句话:把“同步”拆成“异步”,把“大段”切成“流式”,把“泄漏”换成“池化”。
技术选型:为什么放弃 gRPC,拥抱 FastAPI+WebSocket
| 维度 | gRPC | WebSocket |
|---|---|---|
| 双工延迟 | 需要 HTTP/2 + 流式 gRPC,Windows 下 ALPN 支持不完整 | 单 TCP 连接全双工,握手一次即可 |
| 前端友好 | 需要 envoy/grpc-web 桥接,多一层代理 | 浏览器原生 API,直接new WebSocket() |
| 二进制音频 | 需 protobuf 封包,客户端要编解码 | 直接Blob或ArrayBuffer,零拷贝 |
| 中间件生态 | 负载均衡器对 HTTP/2 流感知弱 | Nginx 原生支持proxy_read_timeout细粒度控制 |
结论:PC 端既要给 Python 服务端,又要给 Electron/Web 前端,WebSocket 的“一次握手、持续帧流”最贴合“边合成边播放”场景。
核心实现:三段式流水线
1. asyncio 任务调度
采用asyncio.Queue做“请求等待区”,主进程启动WORKER个asyncio.create_task()消费者,每个消费者绑定一张 CUDA Stream,实现“请求级”并行而非“线程级”并行。
# worker.py async def tts_worker(q: asyncio.Queue, gpu_id: int): torch.cuda.set_device(gpu_id) stream = torch.cuda.Stream(device=gpu_id) while True: item = await q.get() async with semaphore: # 限制同卡并发 with torch.cuda.stream(stream): wav = await run_tensorrt(item.text) await item.websocket.send_bytes(wav.tobytes())2. 动态批处理(Dynamic Batching)
目标:在 50 ms 滑动窗口内自动拼 batch,提升 GPU 利用率。
- 入口:WebSocket
on_receive()把{"text": "xxx"}塞进batch_queue。 - 调度:独立
asyncio.create_task(batch_scheduler())每 50 ms 捞一次队列,长度不足用空串补齐,保证静态 shape 与 TensorRT engine 一致。 - 出口:合成后按
request_id切分,通过asyncio.Event通知对应协程回包。
实测 8 张 A10 卡,QPS 从 120 → 340,提升 2.8 倍。
3. TensorRT 模型优化关键参数
ChatTTS 基于 Transformer 解码器,主要耗时在MultiHeadAttention。优化记录:
- FP16 + 显式量化:权重预量化,峰值显存下降 42%。
- BuilderFlag::kENABLE_REFIT:热更新 vocab 嵌入,无需重编 engine。
- CUDA Graph:捕获
context.enqueue_v3()调用,CPU 调度开销 < 0.2 ms。 - 最大序列长度 512,batch=8,engine 文件 1.9 GB,加载时间 3.4 s。
代码示例:Dockerfile & 核心路由
Dockerfile(多阶段,含 TensorRT 插件)
# 阶段1:编译 TensorRT 插件 FROM nvcr.io/nvidia/tensorrt:23.04-py3 as trt-builder WORKDIR /build COPY chatts_onnx/ /build RUN trtexec --onnx=model.onnx --saveEngine=model.plan \ --fp16 --workspace-schedule=policy=prefer_speed # 阶段2:运行镜像 FROM python:3.10-slim COPY --from=trt-builder /build/model.plan /app/ RUN pip install fastapi uvicorn torch tensorrt asyncio-mqtt COPY app/ /app CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--loop", "uvloop"]WebSocket 路由:分块传输 & 健康检查
# main.py @app.websocket("/ws/tts") async def websocket_tts(websocket: WebSocket): await websocket.accept() try: while True: data = await websocket.receive_json() req_id = data["req_id"] text = data["text"] q_item = QueueItem(req_id, text, websocket) await batch_queue.put(q_item) # 等待调度完成 await q_item.event.wait() except WebSocketDisconnect: pass @app.get("/health") async def health(): return {"gpu_free_memory": torch.cuda.mem_get_info()[0], "queue_length": batch_queue.qsize()}音频流分块:合成完整体 wav 后,按 20 ms 粒度切片,通过websocket.send_bytes()连续发送,前端AudioContext.decodeAudioData顺序入队播放,实现“零缓冲”播放。
性能考量:压测与内存泄漏
| 方案 | 平均延迟 | 95th 延迟 | QPS | GPU 显存峰值 |
|---|---|---|---|---|
| PyTorch+Flask(同步) | 1.8 s | 3.2 s | 120 | 10.7 GB |
| TensorRT+FastAPI+WebSocket | 0.35 s | 0.52 s | 340 | 6.2 GB |
内存泄漏检测:使用torch.cuda.memory_stats()每 30 s 采样,监控allocated_bytes.all.current指标,若连续 5 周期增长 > 3% 则触发torch.cuda.empty_cache()并记录日志。上线两周未出现 OOM。
避坑指南:Windows 特供版
- 音频驱动兼容
Windows 下 WASAPI 独占模式会拖慢 CUDA Kernel Launch,Docker 需加--isolation=process并关闭宿主机音效增强。 - 中文标点导致合成中断
ChatTTS 分词器对全角符号敏感,需在text_normalize()阶段把。!?映射到半角,再送入 tokenizer,否则遇到——长横线会触发<UNK>强制截断。 - 端口冲突
Windows 默认 Hyper-V 会占 8000-9000 随机口,建议服务固定到 5000 以外并写进.env。
结语:下一步往哪走?
把 ChatTTS 电脑版做成高并发服务后,新的瓶颈从“算不过来”变成“GPU 不够”。如何设计降级方案应对 GPU 资源耗尽场景?欢迎分享你的思路——是回退到 CPU 合成、排队熔断,还是直接拒绝新连接?