dpm_solver.py 在 VibeVoice 里做什么:扩散采样的调度那一段
翻 VibeVoice 仓库的时候,vibevoice/schedule/ 这个目录很容易被跳过——它只有三个文件,__init__.py 是空的,剩下 dpm_solver.py 和 timestep_sampler.py。但如果你想搞清楚”一段语音潜变量是怎么从纯噪声里被一步步解出来的”,绕不开这里。
先说清楚本文的边界:我们没有下载权重、没有跑过推理,也没有做过训练。下面全部是把仓库里的代码读出来讲一遍,不涉及任何运行表现。
从调用点倒着看:谁在用这个调度器
直接 grep 仓库,DPMSolverMultistepScheduler 的引用点只有三处,都在 vibevoice/modular/ 下:modeling_vibevoice.py、modeling_vibevoice_streaming.py、modeling_vibevoice_streaming_inference.py。
真正把整条采样循环写出来的是 modeling_vibevoice_streaming_inference.py 里的 sample_speech_tokens:
@torch.no_grad()
def sample_speech_tokens(self, condition, neg_condition, cfg_scale=3.0):
self.model.noise_scheduler.set_timesteps(self.ddpm_inference_steps)
condition = torch.cat([condition, neg_condition], dim=0).to(self.model.prediction_head.device)
speech = torch.randn(condition.shape[0], self.config.acoustic_vae_dim).to(condition)
for t in self.model.noise_scheduler.timesteps:
half = speech[: len(speech) // 2]
combined = torch.cat([half, half], dim=0)
eps = self.model.prediction_head(combined, t.repeat(combined.shape[0]).to(combined), condition=condition)
cond_eps, uncond_eps = torch.split(eps, len(eps) // 2, dim=0)
half_eps = uncond_eps + cfg_scale * (cond_eps - uncond_eps)
eps = torch.cat([half_eps, half_eps], dim=0)
speech = self.model.noise_scheduler.step(eps, t, speech).prev_sample
return speech[: len(speech) // 2]
这段是仓库代码原文。读法是:正条件和负条件在 batch 维拼在一起,prediction_head(也就是 diffusion head)一次前向同时算出两份输出,再按 classifier-free guidance 的写法合成一份 eps,最后交给 noise_scheduler.step。调度器在这里的角色很单纯——它不碰模型,只负责”走几步”和”这一步从 x_t 走到哪个 x_{t-1}“。签名里的 cfg_scale=3.0 是仓库当前代码里的默认值,随版本可能变动;它的实际取值由上游 generate 一路传下来(modeling_vibevoice_streaming_inference.py 里那个函数签名上写的是 cfg_scale: float = 1.0,两处默认值本身就不一致,这一点代码里没有解释)。
调度器是在哪儿被 new 出来的
VibeVoiceStreamingModel.__init__ 里只传了三个参数:
self.noise_scheduler = DPMSolverMultistepScheduler(
num_train_timesteps=config.diffusion_head_config.ddpm_num_steps,
beta_schedule=config.diffusion_head_config.ddpm_beta_schedule,
prediction_type=config.diffusion_head_config.prediction_type
)
modeling_vibevoice.py 里是同样的三行。也就是说,dpm_solver.py 那个长长的 __init__ 里除了这三项,其余全部吃默认值——solver_order、algorithm_type、solver_type、lower_order_final、final_sigmas_type、timestep_spacing 这些都没有从 config 走。想知道它们是什么,只能去 vibevoice/schedule/dpm_solver.py 的构造函数签名里看,那是仓库当前代码里的默认值,随版本可能变动。
三个来源字段定义在 vibevoice/modular/configuration_vibevoice.py 的 VibeVoiceDiffusionHeadConfig 里:ddpm_num_steps、ddpm_beta_schedule、prediction_type,同一个类里还有 ddpm_num_inference_steps 和 ddpm_batch_mul。注意 ddpm_num_inference_steps 并不流向调度器构造,它是被 VibeVoiceStreamingForConditionalGenerationInference.__init__ 读走存成 self.ddpm_inference_steps,再由 set_ddpm_inference_steps(num_steps=None) 覆盖,最后才在 sample_speech_tokens 里传给 set_timesteps。构造期和推理期的两个”步数”是两回事,混起来看会很晕。
set_timesteps 到底改了什么
set_timesteps(num_inference_steps, device, timesteps) 的文档串写明”要在推理前运行”,而 step() 开头有一段硬检查:num_inference_steps 为 None 时直接抛 ValueError,提示你先跑 set_timesteps。所以顺序是死的。
这个方法里值得记住的几件事:
num_inference_steps和自定义timesteps只能二选一,两个都传或都不传都会抛ValueError;传了自定义timesteps又开着use_karras_sigmas或use_lu_lambdas,也会各自抛错。- 时间步的排布由
timestep_spacing决定,代码里只接受linspace、leading、trailing三种,其它值抛ValueError。 final_sigmas_type只接受zero和sigma_min,前者把最后一个 sigma 设成 0。- 收尾时它会把
model_outputs重置成[None] * solver_order、lower_order_nums归零、_step_index与_begin_index置None,并把sigmas搬到 CPU(注释写的是避免过多 CPU/GPU 通信)。
把最后这条和上面那段采样循环放在一起看:sample_speech_tokens 每进来一次就重新调一次 set_timesteps,于是队列里上一段留下的 model_outputs 与两个下标计数器都被清成初始状态——多步求解器是带状态的,这一点代码里写得很清楚,至于为什么把清理动作放在这个位置,仓库里没有写明理由。
step() 内部走了哪几层
step(model_output, timestep, sample, generator=None, variance_noise=None, return_dict=True) 大致是四段:
第一段,定位。 self.step_index 为 None 时调 _init_step_index(timestep);如果 begin_index 没被 set_begin_index() 设过,就走 index_for_timestep() 在 self.timesteps 里找这个 timestep 的下标,命中多个时取第二个(注释说明这是为了从去噪中途起步的场景不漏掉 sigma)。之后每步结尾 self._step_index += 1,传进来的 timestep 实际上只在第一次调用时起作用。与之呼应的是,convert_model_output、dpm_solver_first_order_update 这几个方法里旧的位置参数 timestep / prev_timestep / timestep_list 都挂了 deprecate(...),提示信息逐字写明这些参数”已废弃且没有效果”,改由内部计数器 self.step_index 接管。
第二段,转换。 convert_model_output 按 algorithm_type 分两大支:dpmsolver++ / sde-dpmsolver++ 这支要的是数据预测(x0),dpmsolver / sde-dpmsolver 这支要的是噪声预测。每支内部再按 prediction_type 分 epsilon、sample、v_prediction 三种,其它值抛 ValueError。VibeVoice 的 VibeVoiceDiffusionHeadConfig 里 prediction_type 的默认值是 "v_prediction"(这是仓库当前代码里的默认值,随版本可能变动),走的是第三支。文档串里还挂了一个 <Tip>,写明”算法类型和模型类型是解耦的”。
第三段,滑窗与降阶。 转换后的输出被推进 self.model_outputs 这个长度等于 solver_order 的队列(老的往前挪,新的放队尾)。然后判定该用几阶:
lower_order_final = (self.step_index == len(self.timesteps) - 1) and (
self.config.euler_at_final
or (self.config.lower_order_final and len(self.timesteps) < 15)
or self.config.final_sigmas_type == "zero"
)
这里有一处把两段代码放在一起才看得出来的东西:final_sigmas_type 在构造函数签名里的默认值是 "zero"(同样是仓库当前代码里的默认值,随版本可能变动),而这个条件里 final_sigmas_type == "zero" 是个独立的 or 分支——只要不改这个默认值,最后一步就一定走一阶更新,跟推理步数是多是少无关,len(self.timesteps) < 15 那个条件在这种配置下并不是必要条件。代码就写在那里,仓库里没有对此做进一步说明。
第四段,更新。 按 solver_order、lower_order_nums 和上面两个降阶标志,分发到 dpm_solver_first_order_update(文档串写明等价于 DDIM)、multistep_dpm_solver_second_order_update、multistep_dpm_solver_third_order_update。二阶那支还会再按 solver_type 分 midpoint 与 heun 两种写法。SDE 那两种 algorithm_type 需要额外的 noise:step() 里会用 randn_tensor 生成,或者直接用你传进来的 variance_noise;两个 update 方法里对应位置是 assert noise is not None。返回值默认包成 SchedulerOutput(prev_sample=...),return_dict=False 时返回单元素 tuple——sample_speech_tokens 取的是 .prev_sample。
demo 里把调度器换了一遍
demo/web/app.py 加载完模型之后有这么一段:
self.model.model.noise_scheduler = self.model.model.noise_scheduler.from_config(
self.model.model.noise_scheduler.config,
algorithm_type="sde-dpmsolver++",
beta_schedule="squaredcos_cap_v2",
)
self.model.set_ddpm_inference_steps(num_steps=self.inference_steps)
两点值得对着 dpm_solver.py 读。一是 beta_schedule 换成了 squaredcos_cap_v2,而 VibeVoiceDiffusionHeadConfig 里 ddpm_beta_schedule 当前默认给的是 "cosine"(默认值随版本可能变动)——回到 dpm_solver.py 的构造函数,这两个字符串命中的是同一个分支(elif beta_schedule == "squaredcos_cap_v2" or beta_schedule == "cosine"),走的都是 betas_for_alpha_bar(..., alpha_transform_type="cosine")。二是 algorithm_type 换成 SDE 版本,于是每步会走上面说的 randn_tensor 那条路。
顺带一个文档与代码不一致的点:DPMSolverMultistepScheduler 的类文档串里 beta_schedule 只写了 linear、scaled_linear、squaredcos_cap_v2 三个可选值,但构造函数实际还接受 cosine、cauchy、laplace,后两者对应 betas_for_alpha_bar 里的 alpha_transform_type 分支。以代码为准。
另外必须照实标出来的:构造函数开头对 algorithm_type in ["dpmsolver", "sde-dpmsolver"] 调了 deprecate(...),消息逐字写明这两个类型已废弃、将在未来版本移除,让你改用 dpmsolver++ 或 sde-dpmsolver++。还有 deis 这个值不会报错,而是被静默 register_to_config(algorithm_type="dpmsolver++") 改掉;solver_type 传 logrho / bh1 / bh2 同样被静默改成 midpoint。这类”看着接受了、其实换了一个”的行为,调参时不看源码是发现不了的。
训练那一侧,以及 timestep_sampler.py 的位置
同一个调度器还有两个方法给训练用:add_noise(original_samples, noise, timesteps) 和 get_velocity(...)。modeling_vibevoice.py 里算 diffusion loss 时先用 add_noise 造带噪样本,再按 prediction_type 选目标——epsilon 时目标就是 noise,v_prediction 时目标由 get_velocity 算,其它值 raise NotImplementedError,最后取 F.mse_loss。
那里的时间步是现场抽的:
timesteps = torch.multinomial(
torch.ones(self.config.diffusion_head_config.ddpm_num_steps),
speech_len * ddpm_batch_mul,
replacement=True,
).to(hidden_states.device)
而 vibevoice/schedule/timestep_sampler.py 里定义了两个类,接口都是 sample(batch_size, device):UniformSampler 用 torch.randint(0, self.timesteps, ...),LogitNormalSampler 先在 [0, 1] 上取 linspace 算 logit、构造一个正态形状的概率向量 self.prob,再用 torch.multinomial(..., replacement=True) 抽索引。
把两处放在一起:这两个类在仓库里我们没有找到任何其它引用点,训练路径用的是上面那段就地写死的 multinomial。两者在形式上做的是同一类事(在 ddpm_num_steps 范围内抽一批时间步),但走的是两条独立实现。原因代码里没有写,这里就到此为止。
读这一块时容易踩的地方
vibevoice/schedule/__init__.py是空文件,导入要写全路径from vibevoice.schedule.dpm_solver import DPMSolverMultistepScheduler,仓库里三个引用点都是这么写的。- 这两个文件是纯 Python + PyTorch 计算,里面没有任何平台相关分支,Windows 与 Linux/macOS 在这一层没有差别;平台差异要看的是环境与依赖那一层,不在本文范围。
- 文件顶部的 DISCLAIMER 注释写明它强烈参考了
github.com/LuChengTHU/dpm-solver,多处方法上还带着# Copied from diffusers.schedulers...的标记。你在别的扩散项目里见过几乎一样的代码不是错觉。 - 想改采样行为,改点在三个地方:config 里那几个
ddpm_*字段、set_ddpm_inference_steps的入参、以及像 demo 那样用from_config重建调度器。绕开这三处直接改self.config.xxx未必生效——register_to_config的配置读的是self.config,而sigmas、lambda_t这些张量是在__init__里一次性算好的。
以上代码片段均摘自仓库文件原文(长函数有截断);未经实测,以仓库最新代码为准。
本文依据 github.com/microsoft/VibeVoice 仓库与 Hugging Face 模型卡于 2026-08-18 的公开内容整理,
事实来自仓库内的文档与源码。我们没有下载权重、没有跑过推理、也没有做过训练,
因此不涉及显存占用、推理速度、识别准确率与音质的任何描述,也不与其它模型做比较或排名。
该项目持续更新,文中涉及的模块路径、配置字段与接口写法随版本变动,请以仓库最新内容为准。
需要说明的是:仓库 README 记载,2025-09-05 微软因发现有与既定意图不符的使用方式,
基于负责任 AI 原则从该仓库移除了 VibeVoice-TTS 代码;
当前 vibevoice/modular/modeling_vibevoice.py 首行注释标明其来自社区 fork,
且该模块未被 vibevoice/modular/__init__.py 的 __all__ 导出。
本文只讲代码与架构,不构成 TTS 推理的可用性保证。
仓库 README 的风险与限制一节写明:该模型仅供研究与开发用途, 未经进一步测试与开发不建议用于商业或真实场景,并特别提示了合成语音被用于伪造与虚假信息的风险。 使用合成语音时应遵守所在司法辖区的法律法规,并在分享 AI 生成内容时主动披露。