SGLang-Omni ASR 重构:机制与模型策略的边界
SGLang-Omni 的 #1267 不是一个性能优化 tracker,而是一轮架构重构。
它暴露出的核心问题可以概括为:
共享框架中的 reusable mechanics 与模型特有的 semantics / policy 混在了一起。
当系统只支持少数模型时,这种设计还能工作;随着 Whisper、Qwen3-ASR、Fun-ASR、MOSS-Transcribe 等模型进入同一个 serving framework,历史实现里的模型假设开始泄漏到共享基础设施里,最终直接造成 correctness、feature extension 和维护成本问题。
1. 第一原则:shared mechanics 与 model policy 必须分开
Section titled “1. 第一原则:shared mechanics 与 model policy 必须分开”#1267 明确给出了边界:
shared framework owns reusable mechanics
model directories own model semantics and policy并且 shared code 不应该出现基于 model name 的分支。
一个理想结构应该接近:
shared ASR serving/runtime │ ┌────────────┼────────────┐ │ │ │ Whisper Qwen3-ASR Fun/MOSS │ │ │ own prompt own capacity own batching own decode own chunking own cache/retry own context own frontend own semantics共享层只提供:
- audio decode / resample;
- queue / worker / future;
- stream bookkeeping;
- HTTP / SSE mechanics;
- common engine bootstrap。
模型层则保留:
- prompt 语义;
- context / duration limit;
- chunking policy;
- cache policy;
- retry / timeout;
- tokenizer decode 与 text post-processing。
2. 一个典型 abstraction leak:Whisper 的 30 秒窗口限制了 Qwen3-ASR
Section titled “2. 一个典型 abstraction leak:Whisper 的 30 秒窗口限制了 Qwen3-ASR”Qwen3-ASR 早期 serving context 的容量估算依赖:
feature_extractor.nb_max_frames // 2问题在于 nb_max_frames 来自 Whisper 风格的 feature extractor,30 秒是 Whisper frontend 的历史窗口,不是 Qwen3-ASR 的 native model limit。
而 Qwen3-ASR 官方 wrapper 实际支持最长约 1200 秒单次输入。
于是三个完全不同的概念被混在了一起:
1. model capacity 模型本身最多能接受多少音频
2. scheduling chunk target 为了 batching / fairness,希望单次送多少
3. HTTP upload policy 一个用户最多允许上传多长这三个值可能分别是:
native model limit = 1200 sscheduler chunk = 30 / 60 / ... smax total upload = 3600 s / ...不能再让同一个 nb_max_frames 同时代表这三层语义。
这也是为什么长音频 chunk threshold 最终应该是 model-owned policy,而不是 ASR framework 的全局默认。
3. M1:Shared Audio Preparation
Section titled “3. M1:Shared Audio Preparation”#1268 抽取的是音频准备阶段的共享机制:
load↓decode↓resample↓duration↓fingerprint但它明确要求下列内容仍留在 model-local:
- model-specific frontend;
- prompt;
- duration limit;
- source-selection policy。
这不是单纯为了减少重复代码,而是在重新定义“什么东西可以共享”。
一个好的 shared helper 应该只表达不带模型语义的事实:
这是 waveform这是 sample rate这是 duration这是 fingerprint而不是:
因为是 Qwen3-ASR,所以最多 60 秒4. M2:从 TTS-oriented builder 中抽出真正通用的 generation runtime
Section titled “4. M2:从 TTS-oriented builder 中抽出真正通用的 generation runtime”#1269 的背景很有代表性。
历史上 runtime infrastructure 很多是围绕 TTS 生长出来的,于是 ASR 后来也复用了 TtsEngineBuilder。
但以下能力本质上都不是 TTS 专属:
- server args;
- SGLang infrastructure;
- CUDA Graph;
- model runner;
- adapter;
- scheduler;
- cleanup lifecycle。
所以正确方向是:
SGLangGenerationEngineBuilder ↑ ┌─────┴─────┐ TTS ASRTtsEngineBuilder 应该退化成 compatibility subclass,而不是让所有 generation workload 都继承 TTS 语义。
这是一种非常典型的“历史命名暴露错误抽象层级”的重构。
5. M3:只抽 control skeleton,不强行统一所有模型 policy
Section titled “5. M3:只抽 control skeleton,不强行统一所有模型 policy”#1270 是这轮设计里很成熟的一部分。
Fun-ASR 与 MOSS 都有 pre-LM encoder service,因此确实存在共享骨架:
queueworker threadfuture dispatchbatch executionembedding attachment但二者策略不同:
Fun-ASR
Section titled “Fun-ASR”- bounded batching;
- batch wait;
- single-flight;
- cache validation;
- statistics;
- model-specific close behavior。
- immediate drain;
- different cache key;
- timeout;
- per-item retry;
- different lifecycle。
因此 shared service 只应该暴露 hooks:
encode_batch(items)cache behaviorbatch policyembedding splittingattachment而不是试图做一个包含所有分支的“万能 ASR base class”。
这是很典型的 mechanism / strategy 分离。
6. M4:共享 token streaming mechanics,但模型保留 decode 语义
Section titled “6. M4:共享 token streaming mechanics,但模型保留 decode 语义”#1271 抽取:
token buffer↓decode scheduling↓rate limit↓stream state↓OutgoingMessage construction但模型仍拥有:
- tokenizer decode;
- text post-processing;
- terminal flush;
- state naming;
- metadata。
重构的 acceptance criterion 也非常严格:
同一个固定 token trace 必须产生完全相同的
OutgoingMessage序列,包括 delta、metadata、ordering 和 terminal behavior。
这意味着:
Refactor PR 不应该顺手改变行为。
行为改变、bug fix、新 feature 应该单独跟踪和验证。
7. M5:Serve 层也需要 endpoint-neutral abstraction
Section titled “7. M5:Serve 层也需要 endpoint-neutral abstraction”#1272 先把 /v1/audio/transcriptions 从巨大的 openai_api.py 中抽到独立的 serve/transcriptions.py。
它包含:
- request construction;
- response formatting;
- SSE generator;
- duration probe;
- adapter resolution;
- disconnect / abort / cleanup。
但后来做 /v1/audio/translations 时,又发现第一轮 abstraction 仍然写死了 task="transcribe"。
于是架构继续演进:
openai_api.py 巨石 ↓transcriptions.py ↓endpoint-neutral ASR serving helpers ↓transcribe / translate policy这说明好的 abstraction 往往不是一次设计出来的,而是在新增相邻 feature 时被不断检验出来的。
8. 长音频 F1:真正争论的是“策略属于哪一层”
Section titled “8. 长音频 F1:真正争论的是“策略属于哪一层””围绕 Qwen3-ASR long audio,公开 PR 里出现过多种方案。
一种做法倾向于:
60s 左右切 chunk↓low-energy boundary↓并发执行多个 chunk↓提高 batching / fairness另一种做法则强调:
≤1200s → native single forward
>1200s → serving-level chunking这两种方案并不是简单在争论“60 秒还是 1200 秒哪个数字更好”。
它们对应两种不同 policy:
Native capacity policy
Section titled “Native capacity policy”回答:
模型本身能正确处理多长输入?
Scheduling policy
Section titled “Scheduling policy”回答:
即使模型能处理 1200 秒,为了 batch formation / fairness,是否值得提前切成 30/60 秒?
这两个 policy 都有合理性,但不能再隐式绑定成一个常量。
正确的配置关系应该是:
model-owned native limit ↓serving-owned scheduling strategy ↓endpoint-owned total resource guard9. Long-audio chunking 的 correctness 不只是“结果非空”
Section titled “9. Long-audio chunking 的 correctness 不只是“结果非空””长音频切分会引入新的正确性风险:
- chunk seam 丢词;
- CJK / Latin 拼接错误;
- prev-context 丢失;
- chunk 失败却返回 partial transcript;
- timestamp mapping 错;
- 长请求把短请求饿死。
因此测试至少要覆盖:
short audiolong audiomixed short + longEnglish / Chinesechunk boundaryfailure / abortstream / non-stream而且 latency 与 WER/CER 应该一起报告:
如果一个方案因为截断、漏 chunk 或少生成内容而“更快”,只有质量指标才能暴露这个伪收益。
10. Streaming ASR 的语义与 offline ASR 不完全相同
Section titled “10. Streaming ASR 的语义与 offline ASR 不完全相同”Streaming 并不是把 offline transcription 简单切小。
需要额外定义:
- partial text 的稳定性;
- prev-context;
- token text streaming;
- chunk state;
- client disconnect;
- final flush;
- timestamp / segment semantics。
Whisper 的 prompt 就是一个典型模型语义:它应该映射到 prev-context token,而不是由 shared streaming layer 猜测。
因此 streaming framework 适合共享的是:
state machinemessage routingbufferingabort / lifecycle不适合共享的是:
prompt encodingcontext token rulesdecode postprocessmodel-specific timestamp semantics11. 为什么这种重构对 Agent / KDA 很重要
Section titled “11. 为什么这种重构对 Agent / KDA 很重要”性能 Agent 只能在正确的 abstraction boundary 上工作。
如果 shared framework 把模型 policy 混在一起,Agent 很容易产生这种错误优化:
看到 30s 限制↓以为是所有 ASR 模型 capacity↓在 shared code 里加更多 chunking 分支↓问题越来越复杂而正确架构下,Agent 可以清楚判断:
capacity 问题→ model-local
batch / fairness 问题→ scheduler policy
HTTP memory guard→ serve layer所以 #1267 与 KDA/performance work 看似不同,实际上高度相关:
没有清晰的 mechanism/policy boundary,就很难让 Agent 自动化做出可信的系统优化。
12. 推荐的设计检查表
Section titled “12. 推荐的设计检查表”以后往多模型 serving framework 里接新 ASR/TTS 模型,可以先问:
- 这段逻辑是模型语义,还是框架机制?
- 如果换成另一个模型,这个常量仍然成立吗?
- shared code 是否需要知道 model name?如果需要,抽象可能有问题。
- 同一套 control skeleton 能否通过 hooks 适配不同模型?
- Refactor 是否保持行为完全一致?
- 新 feature 是否单独做 correctness / performance gate?
- capacity、scheduler policy、API resource guard 是否被明确区分?
如果这几个问题答清楚,后续的性能优化和自动化 Agent 才不容易在错误边界上反复打补丁。
References
Section titled “References”- ASR Refactor Roadmap #1267
- M1 Shared audio preparation #1268
- M2 Shared generation engine builder #1269
- M3 Shared pre-LM encoder service mechanics #1270
- M4 Shared token text streaming mechanics #1271
- M5 Transcription serve extraction #1272
- Long-audio chunking #1347
- Native 1200s alignment draft #1350
- Model-owned chunking policy draft #1352