EasyMagpieTTS 基于 vLLM-Omni 的两阶段流式 TTS 推理指南
【免费下载链接】SpeechA scalable generative AI framework built for researchers and developers working on Large Language Models, Multimodal, and Speech AI (Automatic Speech Recognition and Text-to-Speech)项目地址: https://gitcode.com/GitHub_Trending/nem/Speech
本文档面向需要在 NeMo Speech 中把 EasyMagpieTTS(Nemotron-H 骨干 + 逐码本局部 Transformer + 25 fps 谱编码器)部署为高吞吐流式语音服务的开发者。文章围绕仓库内 tools/easymagpie_vllm_omni/README.md 展开,完整覆盖 NeMo 检查点转换、vLLM-Omni 0.24 服务环境搭建、离线与流式合成、请求时零样本音色克隆、HTTP/WebSocket 服务接入与基准测试,并辅以源码级原理讲解。读完你将掌握一条从.nemo权重到可对外服务的端到端推理链路。
架构总览:两阶段推理流水线
EasyMagpieTTS 在 vLLM-Omni 中被拆解为两个可独立部署的阶段,整体采用"文本 → 堆叠声学码 → 波形"的管线:
| 阶段 | 职责 |
|---|---|
| Stage 0 — EasyMagpie LM | EasyMagpie_LM_Backbone(Nemotron-H)+EasyMagpie_LM_LT(局部 Transformer)→ 输出堆叠声学码(stacked acoustic codes) |
| Stage 1 — SpectralCodec-BWE-22kHz | 有状态的(stateful)原生 vLLM codec → 解码为 22.05 kHz 波形 |
模型定义与流水线注册位于 tools/easymagpie_vllm_omni/easymagpie_vllm_omni/ 与 tools/easymagpie_vllm_omni/vllm_plugin_easymagpie_omni/,部署参数集中在 tools/easymagpie_vllm_omni/deploy/easymagpie.yaml。
从源码看,两阶段的编排在 pipeline.py 中定义为EASYMAGPIE_PIPELINE:Stage 0 以model_stage="easymagpie"运行 LLM 自回归解码,把声学码作为 latent 输出;Stage 1 以model_arch="EasyMagpieCodecForConditionalGeneration"、model_subdir="codec_native"消费这些码并产出波形。由于 EasyMagpie LM 上报的model_type是通用的nemotron_h,部署时必须通过pipeline: easymagpie显式指定路由。此外仓库还提供了EASYMAGPIE_LM_PIPELINE(对应pipeline: easymagpie_lm),它是去掉 codec 的单阶段拓扑,用于单独评测 LM 的声学码预测吞吐(benchmark_model.py 默认使用该配置)。
部署配置中的关键字段
deploy/easymagpie.yaml 的要点如下:
pipeline: easymagpie:必须显式指定,否则路由器无法识别nemotron_h报告类型。async_chunk: true:两个阶段间需要流式码传输,必须开启。dtype:Stage 0 默认float16;Stage 1 默认float32——配置注释明确说明 FP16 更快但会引入可感知的 codec 伪影。- 两阶段通过
connectors中的SharedMemoryConnector通信,其中codec_chunk_frames: 8表示原生 vLLM 状态页保留解码器的完整因果历史,codec_startup_chunk_frames: [6, 6]提供稳态流式开始前的两个启动块作为播放余量。 - Stage 0 参数:
max_num_seqs: 32、gpu_memory_utilization: 0.5、enable_chunked_prefill: true、max_num_batched_tokens: 4096、max_model_len: 4096,并挂载自定义 workereasymagpie_vllm_omni.runner.EasyMagpieGPUARWorker;mamba_ssm_cache_dtype: float32用于 Mamba 选择性状态更新缓存。 - Stage 1 参数:
gpu_memory_utilization: 0.35、enforce_eager: true、enable_chunked_prefill: false——codec 块是原子的,绝不能在一步中拆分某个请求的状态更新;max_num_batched_tokens: 512、max_model_len: 512。 - 注意:引擎设置必须扁平地放在 stage 层级,嵌套设置不会被解析。
第一步:将 NeMo 检查点转换为 vLLM 模型目录
转换脚本 scripts/convert_to_vllm.py 会把 EasyMagpie LM 转换为自包含的 vLLM-Omni 模型目录,同时预计算文本嵌入查找表、保存分词器与可选的命名说话人嵌入。Stage-1 codec 解码器总是被打包;而 codec 编码器与参考说话人 Transformer默认不复制,因此默认产物只接受已知的speaker_id。
转换必须在NeMo 环境中、从仓库根目录执行。当环境里装有指向其他 checkouts 的可编辑 NeMo 安装时,把仓库根目录前置到PYTHONPATH很重要:
PYTHONPATH="$PWD" python tools/easymagpie_vllm_omni/scripts/convert_to_vllm.py \ --nemo_file /path/to/emptts.nemo \ --codec_model_path /path/to/25fps_spectral_codec.nemo \ --phoneme_tokenizer_path /path/to/bpe_ipa_tokenizer.json \ --outdir tools/easymagpie_vllm_omni/converted_model \ --context_audio /path/to/reference_voice.wav \ --speaker_name eng常用参数说明(来自脚本的parse_args):
| 参数 | 默认值 | 说明 |
|---|---|---|
--nemo_file | 必填 | EasyMagpieTTS.nemo检查点路径 |
--codec_model_path | 必填 | 25 fps 谱编码器.nemo路径 |
--outdir | 必填 | 输出的 vLLM 模型目录 |
--phoneme_tokenizer_path | 检查点内置 | 覆盖 IPA BPE 音素分词器路径 |
--disable_cas_for_context_text | 关 | 针对未用 CAS 嵌入训练 context text 的旧检查点 |
--text_tokenizer | 检查点内置 | 要导出的 HF 文本分词器名称/路径 |
--context_audio/--speaker_name | — /default | 预计算命名说话人嵌入,保存为speaker_embeddings/<name>.pt |
--bundle-audio-encoders | 关 | 显式选择打包 codec 编码器与参考说话人 Transformer |
--max-audio-seconds | 30.0 | 每条原始参考/用户音频的最大时长,超长直接拒绝 |
--context_audio_duration | 5.0 | 参考音频裁剪时长 |
--dtype | float32 | 可选bfloat16/float16/float32,bf16 匹配参考推理设置 |
--precompute_batch_size | 1024 | 预计算逐子词文本嵌入的批大小 |
转换产物包含(见 convert_to_vllm.py 的 docstring):
config.json—— 扁平 HF 风格配置(Nemotron-H 骨干字段 +EasyMagpieOmniArch读取的 EasyMagpie 标量);model.safetensors(+ index)—— 按 vLLMload_weights期望的键布局(decoder.*骨干 + 顶层 TTS 子模块)转换的权重;- 文本条件分词器(
AutoTokenizer.save_pretrained导出,用于引擎内按请求 tokenizecontext_text); speaker_embeddings/<name>.pt(可选)—— 预计算的说话人编码,推理时按speaker_id选择;codec_native/—— 转换为有状态 vLLM 模型的因果 codec 解码器;codec_encoder.safetensors+codec_encoder.json(仅--bundle-audio-encoders时)—— 零样本音色克隆与多轮用户历史所需的 codec 编码器与参考说话人 Transformer。
原理细节:转换时把字符感知子词(CAS)编码器坍缩为一张subword_id -> embedding的预计算查找表(CAS 对每个 subword id 完全确定,因此在转换期烘焙一次、引擎内不再运行);decoder中未被使用的 token 嵌入表被替换为一个宽度为 2 的哑表——模型始终通过inputs_embeds喂入,但 vLLM profiling 的_dummy_sampler_run会设置top_k = vocab_size - 1并回采索引,宽度为 1 会导致越界断言,故必须 ≥ 2。此外脚本会校验:骨干必须是decoder_type='nemotron_h'、hidden_dim == embedding_dim、nemotron_h_config.hidden_size == embedding_dim(骨干直接消费inputs_embeds,无输入投影)、local_transformer_type为ar/autoregressive、默认推理模式为text_input_mode='streaming'。codec 部分仅支持 GroupFiniteScalarQuantizer(FSQ)布局,且各 FSQ 组需共享同一num_levels_per_group。
第二步:搭建服务环境
服务阶段需要一块 GPU、与vLLM 0.24 / vLLM-Omni 0.24严格匹配的版本,以及本包。转换完成后不再需要 NeMo:
cd tools/easymagpie_vllm_omni conda create -n easymagpie-vllm python=3.12 -y conda activate easymagpie-vllm pip install -r requirements.txt pip install -e . # 可选:为 notebook 安装内核 pip install ipykernel python -m ipykernel install --user \ --name easymagpie-vllm \ --display-name "Python (easymagpie-vllm)"版本约束写死在 requirements.txt 中:vllm==0.24.0与vllm-omni==0.24.0必须保持相同主次版本,此外依赖numpy>=1.26、PyYAML>=6.0、safetensors>=0.8.0、soundfile>=0.13.1、tokenizers、transformers>=5.5.3。
Mamba 内核缓存提示:Mamba 的选择性状态更新内核需要按形状和 GPU 做针对性调优,未调优的缓存可能给出次优性能。建议跨多次启动复用同一套 Triton/vLLM 缓存目录,让重复运行累积更好的内核;如需显式扫描,运行python scripts/tune_mamba_ssu.py --model converted_model后重启服务。
快速开始:离线合成
离线合成示例见教程 offline_demo.ipynb,其中演示了AsyncOmni的初始化与使用方式:先创建单阶段或两阶段的异步引擎,再提交 prompt 拿到合成结果。
请求时参考音频与用户音频:音频标记机制
每个源 EasyMagpie NeMo 检查点都包含 codec 编码器与参考说话人 Transformer,但默认从转换产物中剔除,只有--bundle-audio-encoders才会把它们与(总是打包的)Stage-1 codec 解码器一起捆绑。没有这个显式选择,请求只能按speaker_id选择预计算嵌入。
prompt_token_ids中的arch.audio_input_token_id是一个音频标记。每个标记按顺序与multi_modal_data["audio"]中的一项配对;处理器会拒绝标记/条目数量不匹配的请求。非末尾的标记选择参考条件(reference conditioning),末尾标记选择用户历史(user history)。每一项会被编码成两种表示各一次,由标记决定插入哪种表示。
每一项以(waveform, sample_rate)形式传入,必须是单声道且采样率等于arch.codec_input_sample_rate——服务路径刻意不做降混或重采样。每条原始音频限制为arch.max_audio_seconds(默认 30 秒),超长输入会被拒绝而非截断或分块;可通过转换时的--max-audio-seconds修改启动 profiling 上限。
第一轮同时带原始参考音频与用户语音时,把参考标记放在上下文行之前、用户标记放在最后:
prompt = { "prompt_token_ids": ( [0] * task_rows + [arch.audio_input_token_id] + [0] * len(context_token_ids) + [arch.audio_input_token_id] ), "multi_modal_data": { "audio": [ (reference_waveform, reference_sample_rate), (user_waveform, user_sample_rate), ], }, "additional_information": { "context_text": "[EN]", "text": response_text, "text_prefill_num": arch.text_prefill_num, "temperature": 0.7, "top_k": 80, "reset_codec_on_segment": True, }, }标记放置规则:非末尾标记 = 参考条件;末尾标记 = 用户历史。
- 仅参考合成:把末尾用户标记替换为
arch.text_prefill_num个零行,并提供prefill_text_tokens。 - 已知音色:设置
speaker_id,此时唯一的末尾标记即用户历史。 - 同一 Stage-0 请求的后续轮次:只提交末尾标记与新用户波形即可——模型保留先前轮次的原始参考条件。
- 用同一个异步输入生成器以
StreamingInput逐轮产出,可以保持 Stage-0 的因果/Mamba 状态;而一次新的omni.generate(...)调用会开启新请求,必须重新提供speaker_id或原始参考条件。 - 每轮回复都应设置
reset_codec_on_segment=True,让 Stage 1 冲刷并重置其响应局部的 codec 状态。
多轮前置条件的限制:用户历史 prefill 额外要求检查点以use_multiturn_dataset、condition_on_user_speech与用户说话标记训练;而仅参考的原始音频不需要这些多轮特性。EasyMagpieOmniArch中相应的校验逻辑(config.py)会抛出明确错误,例如condition_on_user_speech requires use_multiturn_dataset=True、use_user_speaking_end_token requires use_user_speaking_token=True。
另外注意两种服务端点能力的差异:OpenAI 兼容的 speech 端点接受请求时参考音频用于零样本 TTS;增量式 WebSocket 端点目前只接受已知voice的文本输入——多轮原始用户音频历史请走直接AsyncOmni路径。
通过 HTTP 与 WebSocket 提供服务
bash ./scripts/run_server.sh ./converted_model 8091脚本 run_server.sh 实际执行的是带 EasyMagpie 插件的vllm serve,端口默认 8091(可用第二个参数覆盖),核心命令为:
VLLM_PLUGINS=easymagpie_omni vllm serve "$MODEL" \ --deploy-config "$DEPLOY_CONFIG" \ --host 0.0.0.0 --port "$PORT" \ --trust-remote-code --disable-log-stats \ --disable-uvicorn-access-log --uvicorn-log-level warning \ --omni两个服务 API:
POST /v1/audio/speech:完整文本输入 + 已知voice或一条请求时ref_audio。WS /v1/audio/speech/stream:增量文本/token 更新 + 异步 PCM 音频输出。
音素文本输入(发音控制)
以enable_phoneme_text_input=true转换的检查点接受内联 IPA 片段,例如Turn <bop>lɛft<eop> here。标记只是语法层面的:普通片段用导出的文本分词器,片段内容用打包的 IPA 分词器与检查点预留的文本 token 区间。
延迟流式(delayed-stream)检查点
对于延迟流检查点,适配器会把已知的文本主导位置折叠进因果 prefill。当前phoneme_delay=3、speech_delay=5的模型因此 prefill 四个目标位置:纯文本位置 0–2,以及携带已知音素 BOS 输入的位置 3。整段文本的 HTTP 请求会自动满足这一点;增量式 WebSocket 输入会缓冲初始更新,直到至少有phoneme_delay + 1个 token 可用。标记字符串与 IPA 片段可以跨input.text消息;未闭合的 IPA 片段会在input.done时被拒绝;input.tokens仍是精确 token 化绕过通道,仅在没有未完成的文本标记或 IPA 片段时被接受。
text_prefill_num的计算逻辑见 config.py 的text_prefill_num属性:音素流启用且streaming_speech_delay > streaming_phonemes_delay时返回streaming_phonemes_delay + 1,否则为 0(锁步/全模式)。
用 OpenAI 兼容客户端查询 HTTP 端点
curl -X POST http://localhost:8091/v1/audio/speech \ -H 'Content-Type: application/json' \ -d '{"input":"This is a TTS service test.","voice":"eng","response_format":"wav","stream":true,"stream_format":"audio"}' \ --output out.wav两种服务 API 的完整示例见教程 server_request.ipynb。
零样本 TTS:请求时参考音频
零样本 TTS 时省略voice,改为发送ref_audio——可以是 HTTP(S) URL、base64 data URL 或file://URI。本地文件必须位于服务器的--allowed-local-media-path之下。EasyMagpie不需要ref_text。音频由 HTTP 媒体加载器降混为单声道但不重采样:其采样率必须等于转换模型中的codec_input_sample_rate。前提是模型必须用--bundle-audio-encoders转换过。
curl -X POST http://localhost:8091/v1/audio/speech \ -H 'Content-Type: application/json' \ -d '{ "input":"This voice is conditioned from request-time reference audio.", "ref_audio":"file:///absolute/path/to/reference.wav", "response_format":"wav", "stream":true, "stream_format":"audio" }' \ --output out_zero_shot.wav服务端实现位于 serving_adapter.py:EasyMagpieTTSAdapter会校验voice与ref_audio互斥、参考音频格式、以及产物是否已捆绑音频编码器(ensure_reference_audio_available,未捆绑时提示"reconvert with --bundle-audio-encoders");然后构造带audio_input_token_id标记与multi_modal_data["audio"]的 prompt,其中context_text默认[EN]、temperature默认 0.7、top_k默认 80、默认说话人为eng。
基准测试
仓库在 scripts/ 提供四类基准脚本:
# 仅评测声学 token 预测(不含 codec),单阶段 easymagpie_lm 拓扑 python scripts/benchmark_model.py --model ./converted_model -n 128 -c 1 32 \ [--streaming --tokens-per-chunk 5] # 用已知说话人评测服务的 HTTP API python scripts/benchmark_server.py --text-file vctk_subset.txt -n 128 -c 1 32 \ --speaker-id eng # 带一条请求时参考音频的零样本合成评测 python scripts/benchmark_server.py --text-file vctk_subset.txt -n 128 -c 1 32 \ --reference-audio /path/to/reference.wav # 每条请求都扰动参考音频,绕过音频缓存 python scripts/benchmark_server.py --text-file vctk_subset.txt -n 128 -c 1 32 \ --reference-audio /path/to/reference.wav \ --randomize-reference-audio # 通过 WebSocket API 评测服务的增量合成 python scripts/benchmark_incremental_server.py --model ./converted_model \ --text-file vctk_subset.txt --tokens-per-chunk 5 -n 128 -c 1 32-c 1 32表示并发数集合,-n 128为请求数。从 benchmark_model.py 的 docstring 看,--streaming切换两种输入模式:默认整段文本一次性提交;流式模式下按--tokens-per-chunk个 subword id 分批推送(先 prefill 块,再每个 chunk 一条StreamingInput消息,携带max_tokens == len(chunk)让引擎一次自由运行相应帧数,最后跟一段自由运行的声学尾巴)。脚本报告吞吐、TTFT、ITL(均值 + p95)、EOS 命中率与整体 RTF(按 codec 帧率估算,而非解码后音频)。
总结与后续阅读
本文覆盖了 EasyMagpieTTS 在 vLLM-Omni 上从权重转换、环境搭建、离线/流式合成到对外服务与基准测试的完整链路。核心要点可归纳为:
- 两阶段拓扑:Stage 0 自回归预测堆叠声学码,Stage 1 有状态 codec 解码出 22.05 kHz 波形,二者通过共享内存连接器流式衔接;
- 转换期决定能力边界:
--bundle-audio-encoders是启用请求时原始音频(零样本音色克隆与多轮用户历史)的唯一开关,默认产物仅支持已知speaker_id; - 音频标记配对:
audio_input_token_id与multi_modal_data["audio"]按顺序一一配对,非末尾标记 = 参考条件、末尾标记 = 用户历史,原始音频必须为单声道且采样率匹配codec_input_sample_rate; - 双服务端点:HTTP
/v1/audio/speech支持完整文本 +voice或ref_audio(零样本);WebSocket/v1/audio/speech/stream支持增量文本与 PCM 流式输出。
如需继续深入,建议依次阅读:离线教程 offline_demo.ipynb、服务端教程 server_request.ipynb、架构配置 config.py、流水线编排 pipeline.py 与服务适配器 serving_adapter.py。
【免费下载链接】SpeechA scalable generative AI framework built for researchers and developers working on Large Language Models, Multimodal, and Speech AI (Automatic Speech Recognition and Text-to-Speech)项目地址: https://gitcode.com/GitHub_Trending/nem/Speech
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考