Kotonia
ログイン今すぐ始める

Kotonia Articles

LTX-2.3 在单张 96GB GPU 上与 TTS 共存的冷启动方案

在语音角色扮演产品中集成 LTX-2.3 时,持久化模式会占用 86 GiB VRAM,无法与 TTS/Ditto 共存。本文记录了切换到冷启动模式,实现空闲 0 GiB / 峰值 40 GiB 的实施方案。

作者 3分钟阅读
#LTX-2#VRAM#冷启动#bitsandbytes#Blackwell
其他语言日语

LTX-2.3 在单张 96GB GPU 上与 TTS 共存的冷启动方案

在尝试将 LTX-2.3(22B 的音频转视频模型)集成到语音角色扮演产品时,我在 VRAM 设计上遇到了难题。“如果以常驻服务器方式运行,它会吃掉 86 GiB,导致同一 GPU 上运行的 TTS / Ditto / MuseTalk 立即 OOM”——从这种典型的困境出发,我切换到了空闲 0 GiB / 峰值 40 GiB 的冷启动方案。

硬件是 RTX Pro 6000 Blackwell Max-Q(94.97 GiB),软件使用的是 LTX-2 官方仓库 和 bitsandbytes 0.49.1。

想要实现的目标

A2V(音频转视频)是一种从音频 + 参考图像 + 文本提示词生成唇形同步视频的模式。具体使用的是 A2VidPipelineTwoStage

prompt + audio_path + image
   ↓ stage_1 (低分辨率生成 video latent,音频固定)
   ↓ spatial upsample 2x
   ↓ stage_2 (高分辨率细化,应用 distilled LoRA-384)
   ↓ video VAE decode + 直接嵌入输入音频
mp4 output

官方 pipeline 在每次 __call__ 时都会构建 → 运行 → 释放各个组件,因此每次都会产生约 50 秒的磁盘 I/O。我原本想将其持久化。

困境 1: 持久化模式的 VRAM 占用明细

如果将 LTX-2 的所有组件都常驻 VRAM,其大小如下(均为 bf16)。

组件VRAM
embeddings processor5.91 GiB
Gemma3-12B text encoder22.78 GiB
stage_1 transformer35.38 GiB
stage_2 transformer (应用 distilled LoRA)35.38 GiB
video VAE encoder0.60 GiB
audio VAE encoder0.04 GiB
spatial upsampler0.92 GiB
video decoder0.76 GiB
总计101.77 GiB

只有 96 GiB 的 GPU 装不下 102 GiB。在加载 stage_2 transformer 的过程中,程序因 CUDA out of memory. Tried to allocate 128.00 MiB. 而崩溃。

困境 2: “Gemma 很小”是误解

直觉上会觉得“12B 的文本编码器应该很轻”,但实际加载后占用了 22.78 GiB。因为是 bf16 格式的 12B 参数,所以这理所当然。

文件名是 gemma-3-12b-it-qat-q4_0-unquantized,其中 qat-q4_0 表示“已针对 q4_0 进行 QAT(量化感知训练)”,unquantized 表示“权重以量化前的 bf16 格式存储”。这是一个应该按设计使用 q4_0 加载的模型,以 bf16 形式使用是一种 MoE 式的“正确但高成本”的运维方式。

解法 1: 使用 bitsandbytes 进行 4-bit 加载

LTX-2 的 Gemma 加载器内部使用了 transformers.Gemma3ForConditionalGeneration,因此 bnb 4-bit 可以直接生效。绕过 LTX-2 的自定义加载器路径,使用 from_pretrained 加载。

from transformers import BitsAndBytesConfig, Gemma3ForConditionalGeneration

quant_config = BitsAndBytesConfig(
    load_in_4bit=True,
    bnb_4bit_compute_dtype=torch.bfloat16,
    bnb_4bit_use_double_quant=True,
    bnb_4bit_quant_type="nf4",
)
model = Gemma3ForConditionalGeneration.from_pretrained(
    gemma_root,
    quantization_config=quant_config,
    device_map={"": "cuda:0"},
    torch_dtype=torch.bfloat16,  # ← 非量化层 (如 embedding) 的 dtype
    local_files_only=True,
)

如果不显式指定 torch_dtype,embedding 会以 fp16 加载,与 Linear4bitbnb_4bit_compute_dtype (bf16) 冲突:mat1 and mat2 must have the same dtype, but got Half and BFloat16。我也踩过这个坑。

LTX-2 对 Gemma 额外进行的 patch(RoPE inv_freq / embed_scale / position_ids 的 register_buffer),只需调用 create_and_populate(encoder) 即可直接应用。bnb 量化仅替换 nn.Linear,因此 Embedding 和 buffer 不受影响。

结果,Gemma 的 VRAM 从 22.78 GiB 压缩到 7.26 GiB。节省了 15 GiB。

困境 3: 即便如此,持久化模式仍无法共存

Gemma 4-bit + 其他常驻组件的总计分配为 86.26 GiB(reserved 88.27 GiB,nvidia-smi 显示 91 GiB)。剩余空间为 4 GiB。生成时的推理工作空间(含 CFG 约 +5 GiB)会超出这 4 GiB,达到峰值 91 GiB,因此如果让 TTS (3.4 GiB) + Ditto (3.0 GiB) 总共 6.4 GiB 共存,无论如何计算都会 OOM

当时有三个选择:

  1. 移除 TTS+Ditto(在 A2V 验证期间无法使用语音聊天)
  2. 仅常驻一个 transformer(不彻底,仍有 OOM 风险)
  3. 冷启动:每次请求都构建 → 运行 → 释放所有权重

由于我希望在保持实时对话(MuseTalk + TTS,TTFA 约 930ms)运行的同时,将 LTX-2 用于“演出用途”,因此选择了方案 3。

解法 2: 冷启动架构

关键在于,“pipeline 对象本身很轻量(Builder 仅使用 mmap,不加载实际权重)”。保持 A2VidPipelineTwoStage 的实例,直接使用官方实现,即每次 __call__ 时各组件通过 context manager 进行构建 → 运行 → 释放。

class PersistentA2VPipeline:
    def __init__(self, ..., cold_start: bool):
        self.pipeline = A2VidPipelineTwoStage(...)  # 仅 builder,VRAM 几乎为零
        if cold_start:
            return  # 在此结束
        # 仅在持久化模式下,从这里开始常驻加载

    def _generate_cold(self, ...):
        # pipeline.__call__ 内部循环处理组件的构建/释放
        video, audio = self.pipeline(prompt=..., audio_path=..., images=...)
        encode_video(video, audio, output_path, ...)

stage_1 和 stage_2 是串行执行的,因此同时占用 VRAM 的只有其中一个。实测峰值为 39.50 GiB。生成结束后完全释放,回到 allocated 0.01 GiB / nvidia-smi 显示 0.55 GiB(仅 CUDA 上下文)。

[mode] cold-start: components load per-request (slow first call, low idle VRAM)
[cuda] cold-start startup (no preload): allocated=0.00GiB
...
[cuda] after cold-start generate: allocated=0.01GiB peak=39.50GiB

在与语音聊天(TTS 3.4 + Ditto 3.0 = 6.4 GiB)并行运行时,LTX 占用 0 GiB;触发 A2V 的瞬间上升到 40 GiB,60 秒后回到 0 GiB,这是一种动态分配。

踩坑点:音频 VAE 的前处理

A2V 的 audio VAE encoder 要求输入为 2-channel(立体声)波形,而 TTS 的输出通常是单声道。如果传入单声道,Conv2d 会报错:expected input[1, 1, 207, 66] to have 2 channels, but got 1 channels instead

此外,如果输入音频的长度短于 num_frames / frame_rate,编码后的 audio latent 会比预期短,导致 transformer 输入 shape 不匹配。

两者都通过 ffmpeg 进行前处理:

# 单声道 → 立体声 + 静音填充,一步完成
ffmpeg -y -i input.wav -ac 2 -af apad -t 2.041667 output.wav

在服务端使用 av 检查输入的 channels 和 duration,仅在必要时通过 ffmpeg subprocess 进行标准化,然后传递临时文件。如果两者都不需要,则零拷贝直接传递输入文件。

数据与权衡

指标持久化模式冷启动模式
空闲 VRAM86 GiB0 GiB
生成时峰值 VRAM91 GiB40 GiB
单次请求耗时~17s(仅推理)~60s(含磁盘 I/O)
TTS+Ditto 共存不可(OOM)
OS page cache 效果第二次起 ~25-30s

冷启动的代价是磁盘 I/O 时间(从 NVMe 读取 73 GB,约 40 秒)。第一次需要 60 秒,之后 OS page cache 生效,大约 25-30 秒。不适合连续使用的场景,但对于“作为演出每 1-2 分钟一次”“在场景切换瞬间插入”这类情况则没有问题。

战略定位

实际上,最初我打算将 LTX-2 用作实时对话的主要虚拟形象。我曾认为通过低分辨率生成再 upscale 可以解决速度问题,但在实测中发现 256×256 分辨率下质量崩溃(偏离了训练 bucket 分布),因此放弃了实时路线。从信息丢失状态进行的 AI upscaler 无法恢复唇形同步精度。

取而代之的是:

  • 实时对话: MuseTalk + 多语言 TTS(TTFA ~930ms,现有方案)
  • 异步演出: LTX-2 用于“场景切换瞬间”“情感峰值”“移动位置虚拟形象”等可以容忍 60 秒生成等待的场合

我按此进行了角色划分。冷启动方案正是基于“这种等待可以作为演出的一部分被接受”的前提才成立的架构选择。


在 hage,我们持续开发语音角色扮演 × 多语言高质量 TTS × 唇形同步虚拟形象。关于本次的 LTX-2 集成,以及将 Qwen3-TTS 的 VRAM 从 15 GB 压缩到 7 GB 等工程类文章,请参阅 /articles

Kotonia 将语音 AI、AI 聊天、图像生成和团队协作整合到一个 AI 工作区中。

试用 Kotonia