Fun-ASR 在语言模型解码之前对整段音频做全上下文编码。过去这一步发生在 LM 阶段内部——逐请求串行执行,相同音频反复重算。本次改动把编码移到LM 准入之前,并为它配上批处理、single-flight 去重和有界 CPU LRU 缓存。
Fun-ASR 是全上下文模型:整段语音在 LM 生成第一个 token 之前就必须完成编码。 在当前 main 上,这一步发生在请求被 LM 阶段准入之后——在 model runner 里一次处理一个请求。
一个独立服务在 LM 准入之前完成音频编码。请求到达 LM 阶段时已携带可直接使用的 embedding——LM 不再接触原始音频特征。
并发音频请求。请求元数据新增 audio_fingerprint 与 num_audio_tokens 两个字段。
批量处理并发到达的编码请求,对在途的相同音频去重,重复请求走缓存——运行在专用 CUDA stream 上。
只有完整的编码才能通过。缓存命中在使用前校验形状、token 数与 dtype;失败的编码永远不会进入 LM 阶段。
预计算 embedding 直接挂到多模态请求上;原始 CPU 特征张量在编码完成后立即释放。
峰值显存 −1.1 GiB并发到达的编码请求一起执行。若批量编码失败,服务降级为逐条编码,并把不可恢复的失败只传播给受影响的请求。
针对相同音频的并发请求共享一次编码执行。基准测试中有 421 / 1,088 个请求被合并——即使缓存完全为空,这部分复用也存在。
条目数与字节数上限均可配置。只缓存完整的编码器/适配器输出——刻意不做部分音频窗口的前缀复用。StageOutputCache 同步改为线程安全。
命名空间与键的设计让过期或错配的命中在结构上不可能发生;每次命中在使用前还要重新校验。
抽取出 _get_audio_feature_uncached(),常规路径与前置路径运行同一份编码器代码——行为不分叉。服务同时新增编码队列、批处理、延迟、失败与缓存统计指标。
完整 Seed-TTS 英文集——1,088 条样本,666 个唯一音频文件——模型FunAudioLLM/Fun-ASR-Nano-2512-hf,单张 NVIDIA H100 80GB。 两次运行共用同一个新启动的 server:先在空缓存上冷启动,再热缓存复用。
| 指标 | Main | 改动 · 冷 | 改动 · 热 |
|---|---|---|---|
| 吞吐 (samples/s) | 25.962 | 34.738 | 86.789 |
| 墙钟时间 (s) | 41.907 | 31.320 | 12.536 |
| 平均延迟 (s) | 1.229 | 0.914 | 0.362 |
| p95 延迟 (s) | 1.802 | 1.114 | 0.504 |
| p99 延迟 (s) | 3.015 | 1.896 | 0.800 |
| 平均 RTF | 0.26585 | 0.19965 | 0.07828 |
| 语料 WER | 0.01710 | 0.01710 | 0.01710 |
| 跳过请求数 | 0 | 0 | 0 |
// 冷启动:吞吐 +33.8%,平均延迟 −25.6%——此时缓存里还什么都没有。
// 热缓存:+234.3%(3.343×),墙钟 41.9s → 12.5s。WER 完全一致。
后续测试将 main(f916b86c)与本次改动(e6f49902)固定对比,并加入严格无命中对照:666 个经 SHA-256 校验的唯一文件,遥测确认hits=0, merged=0。这把缓存的作用与其他因素彻底分开。
// 仅批处理一项就把有效利用率翻倍——完全不需要缓存命中。
// 完全空闲时间 ~0.70s → ~0.41s;峰值显存 −1,099 MiB;平均功耗 +17.6W(GPU 终于在工作了)。
| 无命中对照 | Main | 改动 | Δ |
|---|---|---|---|
| 吞吐 (samples/s) | 32.178 | 32.231 | +0.17% |
| 平均延迟 (s) | 0.991 | 0.974 | −1.7% |
| p95 延迟 (s) | 1.543 | 1.062 | −31.2% |
| p99 延迟 (s) | 2.177 | 1.105 | −49.2% |
| p95 RTF | 0.3326 | 0.2998 | −9.9% |
| 峰值显存 (MiB) | 73,062.9 | 71,963.5 | −1,099.4 |
| 语料 WER | 0.017715 | 0.017715 | 一致 |
// 666/666 全部完成评估,0 跳过,逐样本 WER 分布一致。
冷启动 pass,1,088 个请求,t=0 时缓存为空。666 次 miss 与 666 个唯一文件精确对应—— 在此之上,single-flight 合并了 421 个并发重复请求(它们在对应音频的首次编码完成之前到达)。
// 缓存填满后,每个请求都复用完整的缓存编码输出。
// 注意:服务内部的窄口径 hits/(hits+misses) 指标在冷启动 pass 只有 0.15%,
// 因为它不含 single-flight 合并——38.79% 的有效复用才是有意义的数字。
main 与本次改动——无论冷热缓存——在全量数据集上产出了完全一致的质量; 不完整或失败的编码在结构上被挡在 LM 阶段之外。
"GPU 并不慢——它只是在排队。
在上游编码一次,只准入完整的结果。"