SGLang-Omni · Fun-ASR 推理服务 · 2026

先编码,再准入带缓存的前置(Pre-LM)音频编码服务

Fun-ASR 在语言模型解码之前对整段音频做全上下文编码。过去这一步发生在 LM 阶段内部——逐请求串行执行,相同音频反复重算。本次改动把编码移到LM 准入之前,并为它配上批处理、single-flight 去重和有界 CPU LRU 缓存。

3.34×
热缓存吞吐 · 相对 MAIN
−49.2%
P99 延迟 · 严格唯一输入
+18.0pp
GPU SM 占用 · 25.3% → 43.3%
1.710%
语料 WER · 所有运行完全一致
01

问题:编码发生在准入的错误一侧

Fun-ASR 是全上下文模型:整段语音在 LM 生成第一个 token 之前就必须完成编码。 在当前 main 上,这一步发生在请求被 LM 阶段准入之后——在 model runner 里一次处理一个请求。

当前 MAIN

准入后编码

请求 → LM 准入 → 编码 → 解码
requestLM admissionencode 1×1decode
  • 串行编码。并发到达的短语音在准入后逐条编码——并发 32 时平均 SM 占用只有 25–28%
  • 无去重、无缓存。相同音频提交 N 次,就编码 N 次。
  • 编码器与 LM 阶段在 runner 内争抢调度。
本次改动

准入前编码

请求 → 前置编码 → LM 准入 → 解码
requestpre-LM encode · batch + cacheLM admissiondecode
  • 批量编码并发到达的请求,运行在专用 CUDA stream 上。
  • single-flight + LRU 缓存——相同音频最多编码一次。
  • 只有完整、通过校验的编码才会被准入;embedding 随请求一并携带。
GPU 并不慢——它在空转。编码器位于准入的下游,无法组批;利用率和尾延迟因此先于吞吐出问题。
02

设计:FunAsrPreLMEncoderService

一个独立服务在 LM 准入之前完成音频编码。请求到达 LM 阶段时已携带可直接使用的 embedding——LM 不再接触原始音频特征。

入口

请求

并发音频请求。请求元数据新增 audio_fingerprintnum_audio_tokens 两个字段。

新增 · PRE-LM

编码服务

批量处理并发到达的编码请求,对在途的相同音频去重,重复请求走缓存——运行在专用 CUDA stream 上。

BATCHSINGLE-FLIGHTLRU CACHECUDA STREAM
FunAsrPreLMEncoderService
闸门

LM 准入

只有完整的编码才能通过。缓存命中在使用前校验形状、token 数与 dtype;失败的编码永远不会进入 LM 阶段。

生成

LM 解码

预计算 embedding 直接挂到多模态请求上;原始 CPU 特征张量在编码完成后立即释放。

峰值显存 −1.1 GiB
BATCH批量编码,失败时逐条回退

并发到达的编码请求一起执行。若批量编码失败,服务降级为逐条编码,并把不可恢复的失败只传播给受影响的请求

DEDUPsingle-flight 执行

针对相同音频的并发请求共享一次编码执行。基准测试中有 421 / 1,088 个请求被合并——即使缓存完全为空,这部分复用也存在。

CACHE有界 CPU LRU 缓存

条目数与字节数上限均可配置。只缓存完整的编码器/适配器输出——刻意不做部分音频窗口的前缀复用。StageOutputCache 同步改为线程安全。

GUARD构造即正确的缓存

命名空间与键的设计让过期或错配的命中在结构上不可能发生;每次命中在使用前还要重新校验。

namespace = 模型 + 前端配置 + dtype + 设备类型 + 多模态注意力后端
key      = 归一化音频指纹
validate  = 复用前校验 embedding 的形状 · token 数 · dtype
SHARE两条路径,同一编码器实现

抽取出 _get_audio_feature_uncached(),常规路径与前置路径运行同一份编码器代码——行为不分叉。服务同时新增编码队列、批处理、延迟、失败与缓存统计指标。

03

基准测试:单卡 H100,并发 32

完整 Seed-TTS 英文集——1,088 条样本,666 个唯一音频文件——模型FunAudioLLM/Fun-ASR-Nano-2512-hf,单张 NVIDIA H100 80GB。 两次运行共用同一个新启动的 server:先在空缓存上冷启动,再热缓存复用。

指标Main改动 · 冷改动 · 热
吞吐 (samples/s)25.96234.73886.789
墙钟时间 (s)41.90731.32012.536
平均延迟 (s)1.2290.9140.362
p95 延迟 (s)1.8021.1140.504
p99 延迟 (s)3.0151.8960.800
平均 RTF0.265850.199650.07828
语料 WER0.017100.017100.01710
跳过请求数000

// 冷启动:吞吐 +33.8%,平均延迟 −25.6%——此时缓存里还什么都没有。
// 热缓存:+234.3%(3.343×),墙钟 41.9s → 12.5s。WER 完全一致。

吞吐 —— 每秒样本数
当前 main25.96
改动 · 冷缓存34.74 · 1.34×
改动 · 热缓存86.79 · 3.34×
延迟 —— 秒,越低越好
main · 均值 / p95 / p991.23 / 1.80 / 3.02
改动·冷 · 均值 / p95 / p990.91 / 1.11 / 1.90
改动·热 · 均值 / p95 / p990.36 / 0.50 / 0.80
RTF 0.266 → 0.078热缓存 · 同模型、同一张 H100
04

归因:加速到底来自哪里

后续测试将 main(f916b86c)与本次改动(e6f49902)固定对比,并加入严格无命中对照:666 个经 SHA-256 校验的唯一文件,遥测确认hits=0, merged=0。这把缓存的作用与其他因素彻底分开。

GPU SM 占用 —— 均值 %,100MS NVML 采样
冷 · main25.3%
冷 · 改动43.3% · +18.0 pp
无命中 · main28.1%
无命中 · 改动43.5% · +15.4 pp

// 仅批处理一项就把有效利用率翻倍——完全不需要缓存命中。
// 完全空闲时间 ~0.70s → ~0.41s;峰值显存 −1,099 MiB;平均功耗 +17.6W(GPU 终于在工作了)。

无命中对照Main改动Δ
吞吐 (samples/s)32.17832.231+0.17%
平均延迟 (s)0.9910.974−1.7%
p95 延迟 (s)1.5431.062−31.2%
p99 延迟 (s)2.1771.105−49.2%
p95 RTF0.33260.2998−9.9%
峰值显存 (MiB)73,062.971,963.5−1,099.4
语料 WER0.0177150.017715一致

// 666/666 全部完成评估,0 跳过,逐样本 WER 分布一致。

诚实的适用范围。当每个请求严格唯一时,总吞吐基本持平(+0.17%)。无条件的收益是GPU 利用率(+15.4 pp SM 占用)和尾延迟(p99 −49.2%);大幅吞吐提升还需要 重复或相同的音频——而真实负载通常恰好如此。
05

缓存遥测:从空缓存开始的复用

冷启动 pass,1,088 个请求,t=0 时缓存为空。666 次 miss 与 666 个唯一文件精确对应—— 在此之上,single-flight 合并了 421 个并发重复请求(它们在对应音频的首次编码完成之前到达)。

冷启动 PASS —— 1,088 个请求
缓存 miss(= 唯一文件数)666
single-flight 合并421
普通缓存命中1
38.79%的请求免去了一次编码执行——单 pass、从空缓存开始
热缓存 PASS —— 同一 SERVER,第二次运行
缓存命中1,088 / 1,088
miss · 合并 · 编码执行0 · 0 · 0

// 缓存填满后,每个请求都复用完整的缓存编码输出。
// 注意:服务内部的窄口径 hits/(hits+misses) 指标在冷启动 pass 只有 0.15%,
// 因为它不含 single-flight 合并——38.79% 的有效复用才是有意义的数字。

06

正确性:除了速度,什么都没变

main 与本次改动——无论冷热缓存——在全量数据集上产出了完全一致的质量; 不完整或失败的编码在结构上被挡在 LM 阶段之外。

1.710%
语料 WER——main、冷、热三种运行完全一致,逐样本 WER 分布亦一致
0
跳过请求——1,088 条样本在每种配置下全部完成
2,125/2,125
单元测试在 GPU CI 上全部通过——覆盖批处理、缓存、去重与失败处理
仅完整输出
缓存策略——永远不做部分音频窗口的前缀复用
失败隔离:批量编码失败会回退为逐条重试;不可恢复的失败只传播给对应请求。 缓存只存完整输出,命名空间防止跨配置复用,每次命中在准入前都经过形状、token 数与 dtype 校验。
"GPU 并不慢——它只是在排队
在上游编码一次,只准入完整的结果。"