流水线、窗口解码与流式合成

s2_pipeline.cpp 是把 Tokenizer、SlowARModel、AudioCodec 组装起来的”总装车间”,同时实现引擎最有工程难度的部分:流式合成——边生成语义帧、边解码出可播放的音频块。本章先看初始化编排,再深入窗口解码的 stride/holdback/context 三参数。

1. Pipeline 的初始化编排

Pipeline::init() 严格按顺序执行,任一步失败即清理返回:

1. 加载 tokenizer(tokenizer.json)
2. 以 {true, nullptr} 打开共享 GGUF —— 只要元数据,不建张量
3. model.load_shared(shared_gguf, ...)  形状绑定 + 后端/调度器
4. codec 后端决策(见第 3 节)→ codec.load_shared(GPU 失败回退 CPU)
5. read_all_tensor_data:单遍磁盘读取,分发两类张量
6. sync_tokenizer_config_from_model:以模型实际配置反哺分词器

一个值得注意的顺序细节:第 2 步打开的共享 GGUF 上下文 {true, nullptr} 的第二个参数为 nullptr,张量元信息只存在 gguf 这一侧;模型在 load_shared 内部会再开一次 {true, &ctx_w} 建立自己的张量视图。最终数据读取完成后共享 gguf 立即释放(:512)。初始化输出完整指标:分词/模型/codec/总耗时 + max RSS(:520-530)。

init_from_components() 则支持外部注入已构造的三个组件(供导出库/测试复用)。

2. 单遍数据读取

read_all_tensor_data() 取代了”模型读一遍、codec 再读一遍”的重复路径(README 中提到的 duplicate-load 修复即此):

  • 遍历 GGUF 全部张量一次,按名字在模型 ctx_w 或 codec ctx_w 中查找对应的目标张量;
  • 找到谁就把字节 tensor_set 给谁;两边都不认识的张量跳过;
  • codec 张量读完后触发 refresh_host_caches 重建 VQ 主机缓存;
  • Linux 专有优化:读取结束后对 GGUF 文件做 posix_fadvise(DONTNEED)(:377-385),提示内核丢弃页缓存——张量已进后端 buffer,留着文件缓存纯属重复占用内存。

3. Codec 后端的自动选型

GPU 并不总是 codec 的最优选择:codec 卷积在小批量、CPU 核多时未必比 GPU 慢,而 GPU 还有初始化和数据搬运开销。s2.cpp 给出三档策略(CLI 参数 main.cpp:195-197):

策略 触发条件 行为
--codec-auto(默认) auto && follow && 有 GPU 实测后选快的
--codec-follow-backend follow,不 auto codec 跟随主后端
--codec-cpu 两者皆否 codec 固定 CPU

自动模式的判定 should_auto_select_codec_backend() 排除了纯 CPU 场景。基准过程 benchmark_codec_backend() 构造一个临时 codec,用 8 帧零码做一次解码并计时(:169-184)。关键选型条件(:450-452):

choose_gpu = gpu.ok && (!cpu.ok || gpu_ms < cpu_ms * 0.90)

GPU 必须快出 10% 以上才被选用——这个迟滞阈值避免在两者接近时为微小收益承担 GPU 开销。任一 GPU 加载/分配失败,最终都落回 CPU 路径(:494-504),保证引擎总能启动。

4. 非流式合成

synthesize_prompt_codes_locked() 是一次合成的核心:

CodecDecodeCacheScope(RAII 管理解码缓存生命周期)
clear_kv_cache → build_prompt → init_kv_cache(prompt.cols + max_new_tokens)
generate(...)                                     双 AR 主循环
decode_codes_windowed(stride=16 默认)            码 → 完整音频
指标:含 RTF(音频时长 / 合成耗时)

对外的 synthesize() / synthesize_to_memory() / synthesize_raw() 只是不同的结果封装(写文件、返回 WAV 字节、返回 float)。参考音频解析统一经 resolve_reference_prompt_locked()(现场编码或载入 .s2voice)。

5. 窗口解码:为什么不能”等全部生成完再解”

流式场景要求音频尽早可播放。但 codec 不是状态化/增量式的——解码第 f 帧需要它之前的上下文(卷积感受野 + RVQ 滑窗),无法只解一帧。s2.cpp 的折中是窗口重解码:

每次新码到达,从"已提交帧 − context"处开始,重解一个窗口,
但只把"稳定区间"对应的样本作为新音频吐出,尾部 holdback 帧暂不提交
(因为它们的解码结果会随后续新码变化)。

decode_codes_windowed()(非流式结果整理用)与流式路径的 decode_window_and_emit(:1035)共用这套思想。三个参数是关键:

5.1 stride(步长)

两次窗口解码之间至少新增多少帧(:209、:272-278)。stride 越大,重解码批次越少、CPU 越省,但首包和块间隔越慢。默认值因场景而异:

  • 服务端流式:4(:1009-1010)——优先响应速度;
  • 文件/离线:16(:209、synthesize_streaming_file 强制 16,:1182)——优先吞吐。

5.2 context(上下文窗口)

窗口起点向前回溯多少帧(:236、:1047)。默认取 codec 的 streaming_history_frames(典型 160,由 RVQ window 推导,见第 08 章),保证窗口内每帧的卷积/RVQ 依赖都被满足,解码结果与”全量解码”一致。

5.3 holdback(暂留)

尾部多少帧先不提交为音频(:1040-1042)。流式默认 holdback = context(:1014-1015),即只提交距窗口尾足够远、已不会再变的稳定帧。调小 holdback 能降低感知延迟,但可能在窗口边界听到拼接伪音——README 明确提示了这个权衡(README.md:562)。

 ←──────────── window ────────────────────►
 [context 回看][────── 已稳定、emit ──────][holdback 不 emit]
 ▲window_start  ▲committed            total_frames▲

5.4 只吐增量样本

窗口每次都重解,但 emit 范围被精确钳制(:257-266):起点为”上次已提交帧在窗口中的位置”,终点为”本次稳定帧”。于是同一段已提交样本不会被重复播放,即使它们被重新计算了多次。这也是已知的结构性开销:重解码使流式的 CPU 时间偏高,codec 状态化之前无法根本消除(README.md:562)。

6. 流式合成主路径

synthesize_streaming_prompt_codes_locked() 的流程:

  1. 先发 44 字节 WAV 头(PCM16),再开始任何生成(:975-980)——客户端立刻知道采样率/格式,可以初始化播放器;
  2. build_prompt + init_kv_cache;
  3. 逐码本维护累积向量 accumulated_codes_by_cb(:997);
  4. 把 on_frame 回调挂到 generate:每帧把码推入累积向量,当总帧数 − last_decoded ≥ stride 时触发一次 decode_window_and_emit(:1100-1111),通过 sink.on_pcm_data 吐出 PCM;
  5. 生成结束后做一次 finalize 全量窗口收尾,补齐尾部 holdback(:1137-1142);
  6. 输出流式指标(批次数、解码毫秒等,:1161-1178)。

对外的 synthesize_streaming_raw()、_with_prompt_codes() 负责参考解析与互斥,最终都汇聚到这个 locked 实现。StreamingSink 抽象(on_header/on_pcm_data/on_done/on_error)让同一条管线能接 HTTP、文件、C 回调三种出口。

7. 低延迟预设

low_latency 模式(服务端参数)把三个旋钮全部推到极端:stride=1、holdback=0、启动缓冲 0,用可能的边界伪音换取最低首包延迟;服务端在解析参数时一次性套用(s2_server.cpp:670-679;导出库 apply_streaming_params 同理)。

8. 小结

Pipeline 的核心贡献是在非增量 codec 之上实现了窗口式流式:用 stride 控制重解码频率、context 保证重解码正确性、holdback 保证只提交不再变化的稳定帧;codec 后端则用 8 帧基准 + 10% 迟滞阈值自动选型。同一套 Pipeline 能力经由第 11 章的 HTTP 服务对外暴露。

关键文件

位置 职责
s2_pipeline.cpp:391 Pipeline::init 编排
s2_pipeline.cpp:306 单遍数据读取
s2_pipeline.cpp:155 codec 基准
s2_pipeline.cpp:198 窗口解码(非流式)
s2_pipeline.cpp:826 非流式合成核心
s2_pipeline.cpp:953 流式合成核心
s2_pipeline.cpp:1035 窗口解码与 emit
include/s2_pipeline.h PipelineParams / StreamingSink

This site uses Just the Docs, a documentation theme for Jekyll.