06 - 接一个新模型
前面四篇都是在讲框架自己怎么运转。这一篇换个视角:你要接一个新 TTS 模型进来,具体要写什么。
假设昨天开源了一个新模型,结构是标准的三段 —— 文本进来先做预处理,中间一个自回归骨干吐码本,最后一个声码器还原波形。你想让它能通过 /v1/audio/speech 提供服务。
好消息是:你只需要写自己那个模型目录下的文件,框架层一行不用改,只剩两处必须手工连线。
坏消息是「接得进去」和「接得对」是两回事。真正让人翻车的三类问题都不在主流程上:
| 坑 | 长什么样 | 后果 |
|---|---|---|
| 请求边界 | HTTP 层替你把没填的采样参数填上默认值,而那套默认值是照着另一个模型调的 | 你分不清这个值是用户要的还是 HTTP 层随手填的 |
| 张量在哪 | 预处理图省事写个 .cpu(),自回归那边又得原样搬回显存 | 每个请求白白多一次同步和两次拷贝 |
| 中止清理 | 请求被取消有三条不同的时机,其中一条是「预处理算完了才发现请求早没了」 | 少堵一条就是缓慢的内存泄漏 |
下面按实际动手的顺序走一遍。
一、要写的文件
模型代码全部关在 sglang_omni/models/<name>/ 里,框架层一行都不用碰:
models/<name>/
├── config.py # PipelineConfig 子类 + StageConfig 列表 + EntryClass
├── stages.py # 每个阶段的工厂函数
├── request_builders.py # 阶段间的载荷变换
├── payload_types.py # 该模型专有的流水线状态(用 dataclass,不用裸 dict)
├── sglang_model.py # 注册到 HF 架构串上的 SGLang 模型类
├── callbacks.py # 反 馈式 AR 的三个回调,按需
├── routing.py # 数据驱动的动态路由,按需
└── components/ # 模型模块、预处理器、声码器、适配器
注册是自动发现的:registry.py 遍历 models/ 下每个子包,收集 EntryClass,用它的 architecture 属性去匹配模型的 HF 配置。没有任何需要手工维护的清单。
真正需要手工连线的只剩两处:
- 把 SGLang 模型类插进
model_runner/sglang_model_runner.py::_register_omni_model。HF 架构串和类名对不上时,用model_arch_override传同一个 key。 - 上游权重加载不干净时写一个
weight_loader.py。多数模型不需要。
二、流水线的最小形状
文件放哪定了,接下来定形状:这个模型要拆成几个阶段。
绝大多数 TTS 模型是三段 — — 准备、生成码本、还原波形。这也是框架推荐的最小形状,Qwen3-TTS、Voxtral-TTS、Fish S2-Pro 都是它。两种常见的变体是往里插一个阶段或者加一条流式边:
build_sglang_server_args 和 create_sglang_infrastructure 两个共享助手,拿到 SGLang 的模型 worker、树缓存、请求池、KV 分配器、模型配置五件套,包一层 OmniScheduler 就跑起来了 —— KV cache、批选择、抢占、请求上限全部是上游原件。三、请求边界上的两个陷阱
陷阱一:端点默认值会静默盖掉模型默认值。 HTTP 层会填一套采样默认值(目前用的是 S2-Pro 那套)。对任何别的模型来说,这些值看起来跟用户显式指定的一模一样。解法是让请求带一个 explicit_generation_params 之类的列表,请求构建器据此区分「用户设的」和「端点填的」。凡是端点有主观意见的字段,都要这一套。
陷阱二:输入是异质的。 TTS 客户端会用好几个名字传文本(input、text