Skip to content

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 s
scheduler chunk = 30 / 60 / ... s
max total upload = 3600 s / ...

不能再让同一个 nb_max_frames 同时代表这三层语义。

这也是为什么长音频 chunk threshold 最终应该是 model-owned policy,而不是 ASR framework 的全局默认。


#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 ASR

TtsEngineBuilder 应该退化成 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,因此确实存在共享骨架:

queue
worker thread
future dispatch
batch execution
embedding attachment

但二者策略不同:

  • 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 behavior
batch policy
embedding splitting
attachment

而不是试图做一个包含所有分支的“万能 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:

回答:

模型本身能正确处理多长输入?

回答:

即使模型能处理 1200 秒,为了 batch formation / fairness,是否值得提前切成 30/60 秒?

这两个 policy 都有合理性,但不能再隐式绑定成一个常量。

正确的配置关系应该是:

model-owned native limit
↓
serving-owned scheduling strategy
↓
endpoint-owned total resource guard

9. Long-audio chunking 的 correctness 不只是“结果非空”

Section titled “9. Long-audio chunking 的 correctness 不只是“结果非空””

长音频切分会引入新的正确性风险:

  • chunk seam 丢词;
  • CJK / Latin 拼接错误;
  • prev-context 丢失;
  • chunk 失败却返回 partial transcript;
  • timestamp mapping 错;
  • 长请求把短请求饿死。

因此测试至少要覆盖:

short audio
long audio
mixed short + long
English / Chinese
chunk boundary
failure / abort
stream / 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 machine
message routing
buffering
abort / lifecycle

不适合共享的是:

prompt encoding
context token rules
decode postprocess
model-specific timestamp semantics

11. 为什么这种重构对 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 自动化做出可信的系统优化。


以后往多模型 serving framework 里接新 ASR/TTS 模型,可以先问:

  1. 这段逻辑是模型语义,还是框架机制?
  2. 如果换成另一个模型,这个常量仍然成立吗?
  3. shared code 是否需要知道 model name?如果需要,抽象可能有问题。
  4. 同一套 control skeleton 能否通过 hooks 适配不同模型?
  5. Refactor 是否保持行为完全一致?
  6. 新 feature 是否单独做 correctness / performance gate?
  7. capacity、scheduler policy、API resource guard 是否被明确区分?

如果这几个问题答清楚,后续的性能优化和自动化 Agent 才不容易在错误边界上反复打补丁。