
模态至关重要
一个100万token的文本提示可以容纳整个《哈利·波特》系列,且仅占用约5 MB。这个规模听起来巨大,但输入本身是紧凑的。文本也几乎可以直接用于推理:分词、批处理,然后通过模型即可。
音频改变了问题的形态。同样的《哈利·波特》语料库作为有声读物大小为5到10 GB,大约比文本大三个数量级。在数据到达GPU之前,服务器必须解码容器、重采样、过滤噪声、运行语音活动检测(VAD)、分割语音并计算音频特征。
模型端也发生了变化。如今的LLM拥有数千亿或数万亿参数,因此服务工作自然集中在GPU内部:量化、KV缓存、注意力内核、批处理和并行化。语音转文本模型则小得多,通常参数在数亿到数十亿之间,因此周围的数据路径更为重要。
这使得ASR服务成为一个涵盖GPU执行、CPU预处理、内存移动、传输、连接调度和运行时行为的全路径系统问题。同一套栈还必须服务于两种不同的模式:离线转录(吞吐量最重要)和流式转录(延迟和抖动占主导)。
Together的ASR栈服务于Artificial Analysis排名中延迟最低的两个语音转文本模型:NVIDIA的Parakeet-TDT 0.6B v3和OpenAI的Whisper Large v3。其中较快的NVIDIA Parakeet-TDT 0.6B v3可以在10秒内转录大约20小时的语音,相当于《哈利·波特》电影系列的时长。
本文的其余部分将分解实现这一结果的生成环境变化:针对真实音频形状的TensorRT配置文件、GPU端解码器控制流、低拷贝CPU路径、事件驱动的流式I/O以及运行时GC控制。
为真实音频形状编译编码器
Parakeet采用编码器-解码器架构,其约95%的权重位于编码器中。编码器接收可变长度的语音片段并为解码器生成声学帧,这使其成为优化的首要目标。
音频输入的长度范围很广,从200毫秒的流式数据包到30秒的不间断语音。针对一种输入形状调整的内核计划在另一种形状下可能明显变慢,因此引擎需要在编译时了解它将看到的形状分布。
在使用TensorRT之前,我们已经使用了优化的PyTorch路径,包括torch.compile和CUDA图,并在相同的形状配置文件中进行了调优。这为我们提供了一个强大的基线:在不离开PyTorch栈的情况下实现配置文件感知的执行。
TensorRT为生产环境提供了更快的编码器路径。它提前构建优化的执行计划,在可能的情况下融合内核,调整内存布局,并针对我们预期服务的形状范围对内核变体进行基准测试。
关键细节是配置文件调优。仅针对最大输入形状调优的单个引擎会迫使较短的音频段进入填充路径,这对于流式块和短话语尤其昂贵。多配置文件的TensorRT引擎允许我们在内存中保留一份编码器权重副本,同时为每个请求选择正确的优化配置文件。
内存节省幅度不大,大约从 6 GB 降至 5 GB。更大的收益在于避免了错误的形状匹配,并从优化后的 PyTorch 迁移到 TensorRT 以使用调优后的配置文件。在小输入场景下,感知配置文件的 TensorRT 比通过大型填充配置文件发送请求快数倍。
编码器优化后,解码器循环成为下一个瓶颈。
将 CPU 从解码器循环中移除
Parakeet 的解码器遍历编码器的声学帧,并为每个不推进转录的帧输出一个 token 或 BLANK。代码本质上是:
state = init()
for frame in encoder_output:
token = predict(frame, state)
if token != BLANK:
emit(token)
state = update(state, token)
在性能分析时,我们发现 predict 和 update 都很快。每次迭代的 GPU 工作以微秒计。
耗时的行是分支:
if token != BLANK:
该分支需要 CPU 从 GPU 内存读取 token 以决定走哪条路径。这种主机同步阻止了解码循环被捕获为单个 CUDA 图,并迫使每次迭代通过 Python 往返。GPU 执行几微秒的工作,等待 CPU,启动下一个内核,然后每个请求重复数千次。
条件 CUDA 图节点将该分支移至 GPU。一个小的设备端内核评估条件,并告诉 CUDA 运行时是否进入 token 发射和状态更新子图。分支在 GPU 内解析,无需离开 GPU,因此整个解码器循环(计数器、条件、发射和状态更新)可以被捕获并作为一个 CUDA 图启动。
CPU 离开解码器内部循环,结果是解码器速度提升 2 到 3 倍。

停止复制音频字节
当编码器和解码器运行良好后,剩余的延迟来自模型周围的 CPU 路径。这是我们审计过的大多数 ASR 代码花费延迟预算的地方:冗余复制、热路径上不必要的进程跳转,以及可以从更高并行度中受益的单线程函数。
第一个杠杆是消除不必要的进程边界。
音频预处理,无论是文件解码、重采样、语音活动检测(VAD)、特征提取还是块处理,主要是 I/O 或原生 C/C++ 工作,会释放 Python 全局解释器锁(GIL)。典型的微服务架构将预处理拆分为三到四个独立进程,为工作负载不需要的隔离付出代价。将大部分工作合并到更少的进程中,可以消除内核复制和序列化/反序列化传递,这些在大型文件上可能耗费数百毫秒。
当确实需要进程间通信时,常见的选项如 ZeroMQ 也会带来显著开销。在我们的工作负载中,一个简单的自定义协议(基于持久 Unix 域套接字传输原始音频字节)在高并发下表现最佳,因为它保持帧结构最小化并避免重复的连接建立。
对于大文件,套接字仍然会带来两次拷贝:发送方用户空间到内核缓冲区,然后内核缓冲区到接收方用户空间。为了避免这种路径,我们使用共享内存。通过共享内存,两个进程映射相同的物理区域,因此生产者写入的数据对消费者可见,无需经过内核往返。这为我们提供了零拷贝数据路径。
复杂性的代价是真实的,因此只有当数据量证明其合理性时,才值得使用共享内存。
使用事件驱动 I/O 进行流式处理
流式 ASR 还带来了另一个问题:连接生命周期。
我们最初的流式实现为每个连接使用一个线程。当数百个流同时发送数据块时,数百个线程同时唤醒,GIL 争用激增,尾部延迟飙升。
我们改为使用一个线程阻塞在 epoll 上。
epoll 允许一个线程注册数千个连接,并通过单个系统调用询问内核:“当这些连接中的任何一个有数据时唤醒我。”当消息到达时,内核返回完整的就绪集合,该线程处理活动套接字,然后继续休眠。
相同的工作负载,调度压力大大降低。对于流式 ASR,这种可预测性很重要,因为延迟的部分转录可能会使语音系统感觉缓慢,即使平均延迟看起来不错。

冻结启动状态以消除 GC 尾部延迟
我们差点错过了这一点。
在流式工作负载下,p50 和 p90 延迟看起来正常,但 p95 会定期飙升约 200 毫秒。日志显示队列深度小,GPU 时间正常,但通常运行时间低于 5 毫秒的 CPU 函数突然花费超过 100 毫秒。
后台有东西在窃取请求循环的时间。
性能分析指向了 Python 的垃圾回收器(GC)。Python 使用引用计数进行大多数内存管理,并使用循环检测回收器来捕获引用循环。该回收器按代运行。最老的一代包含长期存在的对象,完整回收可能会遍历大型对象图。
我们在启动时预分配了大量缓冲区、模型状态和查找表,专门为了避免稳态下的分配延迟。这些长期存在的对象位于最老的一代,因此完整的 GC 遍历会遍历数十万个引用。这就是 200 毫秒的停顿。
修复方法是在启动预分配后添加一行代码:
gc.freeze()
gc.freeze() 告诉 Python 将预分配的状态排除在未来的 GC 扫描之外,因此正常的请求范围对象仍然会被回收,而巨大的初始状态则保持不变。
p95 峰值消失了,p50 也有所改善,因为系统可以维持更平滑的流量模式。
教训是:性能分析要超越模型本身。GPU 时间、队列深度和模型执行看起来都正常;延迟峰值存在于 Python 运行时中。
语音延迟是一个端到端的系统问题
语音代理通常以级联方式运行:ASR 生成转录文本,LLM 生成响应,TTS 生成音频。ASR 是该路径的第一阶段,因此其延迟和抖动设定了用户可见响应时间的最早界限。
上述优化针对该路径的不同部分。TensorRT 多配置文件引擎针对真实音频形状调整编码器执行。条件 CUDA 图从解码器循环中移除 CPU 往返。持久化 Unix 域套接字、共享内存和 epoll
减少 CPU 路径开销。gc.freeze()
消除了运行时级别的 p95 故障模式。
同样的约束适用于堆栈的其余部分:每个阶段都必须控制模型执行、预处理、传输、调度和运行时行为的中位延迟和尾延迟。
NVIDIA Parakeet-TDT 0.6B v3 和 OpenAI Whisper Large v3 可在 Together 上使用。如果您在生产环境中扩展语音 AI,请联系我们。
Parakeet v3 是 v2 的后续版本,v2 是一个仅支持英语的模型,在 Hugging Face 开放 ASR 排行榜上以单语言吞吐量领先。v3 显著扩展了该基础,将语言支持从英语扩展到 25 种欧洲语言,增加了自动语言检测功能,无需语言提示,并在 170 万小时的音频数据上进行了训练——包括 NVIDIA 的 Granary 多语言语料库。
