LoRA 微调完怎么推理:VibeVoice 的 inference_lora.py 加载路径

2026-08-18

训练脚本跑完,你打开输出目录,发现里面没有想象中那个几十 GB 的模型文件夹。这是 LoRA 的正常结果——它只存改动的那一小部分,基座还得你自己再加载一次。于是下一个问题立刻来了:这两半到底怎么拼回去,拼错了会怎样。

VibeVoice 仓库把这段拼装写在 finetuning-asr/inference_lora.py 里,篇幅不长,读完就能把加载路径完全搞清楚。下面按这个文件的实际写法走一遍。

一、这个脚本在解决什么

finetuning-asr/lora_finetune.py 训练结束后,--output_dir 指向的目录里放的是 peft 保存的适配器权重,外加训练脚本最后一步 processor.save_pretrained(training_args.output_dir) 存下的 processor 配置。它不是一个自包含的模型

所以推理时必须做三件事,缺一不可:加载基座模型、加载 processor、把适配器叠上去。inference_lora.py 里的 load_lora_model() 干的就是这三件事,签名是:

def load_lora_model(
    base_model_path: str,
    lora_path: str,
    device: str = "cuda",
    dtype: torch.dtype = torch.bfloat16,
):

base_model_pathlora_path 是两个独立参数——这一点是理解整件事的钥匙:适配器和基座是分开给的,脚本不会替你校验它们是不是配套的。

二、前置条件(这一段别跳)

finetuning-asr/README.md 的 Requirements 一节只写了两行:

# Install vibevoice first
pip install -e .

pip install peft

也就是说 peft 不在主包依赖里,是微调场景单独装的。仓库根目录的 pyproject.tomlrequires-python 写的是 >=3.10transformers 的约束是 >=4.51.3,<5.0.0(以上为仓库当前代码里的声明,随版本可能变动)。

比依赖更容易踩的是注意力实现。inference_lora.py 加载基座时是这么写的:

model = VibeVoiceASRForConditionalGeneration.from_pretrained(
    base_model_path,
    dtype=dtype,
    device_map=device if device == "auto" else None,
    attn_implementation="flash_attention_2",
    trust_remote_code=True,
)

attn_implementation="flash_attention_2" 在这里是写死的,没有对应的命令行开关。而同一个脚本的 --device 参数默认值是 "cuda" if torch.cuda.is_available() else "cpu"——把这两处放在一起看,就是:设备会自动回落到 CPU,注意力实现不会跟着回落。

作为对照,仓库里另一个脚本 demo/vibevoice_asr_inference_from_file.py 是把这个选项开放出来的,--attn_implementationchoices["flash_attention_2", "sdpa", "eager", "auto"],走 auto 时的分支里有一句注释写着 MPS/XPU/CPU 不支持 flash_attention_2,会落到 sdpaimport flash_attn 失败时同样落到 sdpa两个脚本的处理方式不一样,这是仓库里客观存在的差异,我们只指出来,不推断在你机器上会发生什么。

Windows 这边要多说一句。docs/vibevoice-asr.md 的 Installation 一节给的是 NVIDIA PyTorch 容器的路线(sudo docker run ... nvcr.io/nvidia/pytorch 镜像,具体 tag 以仓库文档当前写法为准),flash attention 的安装则指向 Dao-AILab 的 flash-attention 仓库,说明「如果你的 docker 环境里没有包含 flash attention,需要手动安装」。仓库里没有找到针对 Windows 原生环境的安装说明,也没有找到 WSL 相关的段落。如果你在 Windows 上做这件事,实际路线(容器、WSL 还是原生)要自己评估,仓库没有给答案。

三、加载路径拆开看

README 里给的推理命令原样是这样:

python inference_lora.py \
    --base_model microsoft/VibeVoice-ASR \
    --lora_path ./output \
    --audio_file ./toy_dataset/0.mp3 \
    --context_info "Tea Brew, Aiden Host"

--base_model 的默认值是 microsoft/VibeVoice-ASR--lora_path--audio_file 都是 required=True(以上是仓库当前代码里的定义,随版本可能变动)。

第一步是 processor,不是模型。 代码里写的是:

processor = VibeVoiceASRProcessor.from_pretrained(
    base_model_path,
    language_model_pretrained_name="Qwen/Qwen2.5-7B"
)

注意 processor 是从 base_model_path 加载的,不是从 lora_path——尽管训练脚本确实往输出目录里存过一份 processor。这里还显式传了 language_model_pretrained_name,而 vibevoice/processor/vibevoice_asr_processor.pyfrom_pretrained 里,这个值的取法是先看配置文件里有没有,没有才用传进来的 kwargs,最后兜底是另一个默认名。这条链路值得知道,因为 tokenizer 取错了,后面的特殊 token id 全都对不上。

第二步加载基座,就是上面那段 from_pretrained

第三步才是适配器

model = PeftModel.from_pretrained(model, lora_path)

PeftModel.from_pretrained 的第一个位置参数是已经加载好的基座实例,第二个是适配器目录。返回的是一个包了一层的对象,后面 model.generate(...) 是透过这层包装转发下去的。

适配器改的是哪些层,要回到训练脚本看。lora_finetune.pyget_lora_config()target_modulesNone 时用的是这一组:q_projk_projv_projo_projgate_projup_projdown_projtask_typeTaskType.CAUSAL_LMbias="none"。函数 docstring 自述这是「应用在语言模型的注意力层与 MLP 上」。同一个文件的 setup_model_for_training() 里还有一段显式冻结:名字里含 acoustic_tokenizersemantic_tokenizer 的参数被设成 requires_grad = False

把这两处合起来读,结论很直接:适配器里不含语音 tokenizer 的权重。所以推理时 --base_model 必须还是那个基座,换一个模型去挂这份适配器,前提就不成立了。

四、边界:这些地方要照实知道

合并那一步是注释掉的。 load_lora_model() 里有这么两行:

# Optionally merge LoRA weights into base model for faster inference
# model = model.merge_and_unload()

默认路径是不合并的。README 的 Merging LoRA Weights 一节把它标成 Optional,给的是单独的示例代码(model.merge_and_unload()model.save_pretrained("./merged_model"))。要不要合并是你自己的选择,脚本默认不做。

--temperature 的分支要看清。 生成配置的组装是这样的:

gen_config = {
    "max_new_tokens": max_new_tokens,
    "pad_token_id": processor.pad_id,
    "eos_token_id": processor.tokenizer.eos_token_id,
    "do_sample": temperature > 0,
}
if temperature > 0:
    gen_config["temperature"] = temperature
    gen_config["top_p"] = 0.9

--temperature 默认 0.0,也就是 do_sample=Falsetop_p 那个 0.9 只在采样分支里出现,它是仓库示例代码里的取值,不是可配置项。--max_new_tokens 默认 4096(这些都是仓库当前代码里的默认值,随版本可能变动)。

解析失败是静默降级的,而且是两层。 里层在 vibevoice_asr_processor.py 自己身上:post_process_transcription 整个函数体包在 try 里,json.JSONDecodeError 走一条 logger.warning(文案是 Failed to parse JSON from transcription),其余异常走另一条 logger.warning(文案是 Error post-processing transcription),两条都 return []。外层在 inference_lora.pytranscribe() 里,调用外面又套了一次 try/except,走到时打印的是 Warning: Failed to parse structured output,然后把 segments 置成空列表继续走。

因为里层已经把绝大多数异常吞掉了,外层那句 Warning: Failed to parse structured output 实际上只在里层没拦住的情况下才会出现。不管走哪条路,结果都一样:segments 为空不等于模型什么都没输出,得回头看 raw_text

多适配器怎么办,仓库里没有找到相关说明——inference_lora.py 只处理单个 lora_path,没有 adapter 命名、切换、多个适配器叠加的代码。

另外,finetuning-asr/README.md 明确写了 toy_dataset/ 里是用 VibeVoice TTS 合成的音频,仅供演示,不是完整的微调数据集,并提示用真实录音时要按数据规模与领域调整超参数。拿它验证流程可以,别拿它当训练效果的依据。

五、怎么验证配对了

验证的抓手是输出格式这条链,它在三个文件里首尾相接:

第一环在 processor 组 prompt 的地方。vibevoice_asr_processor.py 里有一行 show_keys = ['Start time', 'End time', 'Speaker ID', 'Content'],这四个键会被拼进 user 消息,让模型按这个格式输出。传了 --context_info 时,拼出来的句子形如 This is a {duration} seconds audio, with extra info: {context_info},后面接上同样这四个键;没传就走另一个分支。所以 context_info 的作用是进 prompt,不是进模型结构。

第二环在 post_process_transcription。它先从文本里找 ```json 代码块,找不到就从第一个 [{ 开始做括号配对截取,然后 json.loads。解析出来之后有一张 key_mapping,把 Start time / Start 映射到 start_timeEnd time / Endend_timeSpeaker ID / Speakerspeaker_idContenttext——注意它同时接受两种写法,且只保留映射表里认识的键,其余丢弃。

第三环在 inference_lora.pymain()。打印结构化结果时取的正是 seg.get('start_time')seg.get('end_time')seg.get('speaker_id')seg.get('text')

于是自查的顺序就清楚了。segments 是空的时候,按代码里写着的分支去对:一是 json.loads 没过,日志里会有 Failed to parse JSON from transcription;二是解析过了但每条 item 里没有 key_mapping 认识的键——代码里 cleaned_item 为空就不会被追加进 cleaned_result,这一条不打任何日志;三是里层没拦住的异常,那会走到 Warning: Failed to parse structured output。三条都排除掉,再看 raw_text 里的时间与说话人是否成形。函数返回的字典就是 {"raw_text": ..., "segments": ...} 这两项,raw_text 永远是原始文本,任何时候都能拿它兜底。

还有一处细节:截取生成部分用的是 input_length = inputs['input_ids'].shape[1],然后 output_ids[0, input_length:]——只取了 batch 里的第 0 条。这个脚本是按单文件推理写的。

以上代码片段均原样来自仓库文件,未经实测,以仓库最新代码为准。该项目持续更新,文中涉及的模块路径、配置字段与接口写法随版本变动。


本文依据 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 生成内容时主动披露。

想系统学会用 AI?报名体系课或加入会员,照着学、照着用。