Как я добавил llama.cpp бэкенд в CosyVoice3 и ускорил инференс в 2.6x

от автора

CosyVoice3 — одна из лучших open source TTS моделей прямо сейчас, особенно для русского языка. Но у неё есть проблема: LLM-часть на PyTorch работает медленно на слабых GPU вроде T4. RTF (real-time factor) около 1.17 — это значит синтез одной секунды аудио занимает больше секунды реального времени.

Я решил это исправить, добавив поддержку llama.cpp через llama-cpp-python. Результат: RTF упал до ~0.45, то есть ускорение примерно в 2.6x.

В этой статье расскажу как это работает, почему это нетривиально, и как попробовать самому.


Почему CosyVoice LLM — не обычная LLM

CosyVoice3 использует LLM (на базе Qwen2.5-0.5B) для генерации речевых токенов. Но это не стандартная языковая модель — у неё две головы и два набора эмбеддингов:

  • Текстовые эмбеддинги (embed_tokens) — стандартный словарь Qwen, ~151936 токенов

  • Речевые эмбеддинги (speech_embedding) — 6561 токен для аудио

  • Речевая голова (llm_decoder) — отдельный выходной слой для речевых токенов

llama.cpp не знает об этой архитектуре. Он ожидает стандартную модель с одним словарём и одной lm_head.


Конвертер: объединяем всё в одну обычную LLM

Чтобы скормить модель llama.cpp, нужно привести её к стандартному виду. Идея простая: слить оба эмбеддинга в один большой словарь.

# Конкатенируем текстовые и речевые эмбеддингиinput_emb = torch.cat([txt_emb, spk_in], dim=0)  # Теперь vocab_size = 151936 + 6561+200 = ~158700# Собираем единую lm_headfull_output = torch.zeros((input_emb.shape[0], input_emb.shape[1]))full_output[:151936, :] = txt_emb        # текстовая частьfull_output[151936:, :] = spk_out        # речевая часть

После этого обновляем config.json (новый vocab_size, tie_word_embeddings: false) и расширяем vocab.json, добавляя токены вида <|s_0|>, <|s_1|>, … для речевых токенов.

Теперь convert_hf_to_gguf.py из llama.cpp работает без изменений и видит просто большой Qwen2.5.

Готовые сконвертированные модели опубликованы на HuggingFace: Ferraronp/CosyVoice3-qwen2.5-0.5b-speech-gguf

Доступные варианты квантизации: Q2_K, Q3_K_S, Q3_K_M, Q3_K_L, Q4_0, Q4_1, Q4_K_S, Q4_K_M, Q5_0, Q5_1, Q5_K_S, Q5_K_M, Q6_K, Q8_0, BF16, F16, F32.

Конвертер (Colab-ноутбук): Ferraronp/CosyVoice-gguf-converter


Инференс: как не дать модели генерировать текст вместо речи

После конвертации возникает другая проблема. Модель теперь видит огромный словарь из текстовых и речевых токенов. Ничто не мешает ей на выходе генерировать обычный текст — ведь с её точки зрения это просто токены с маленькими индексами.

Решение: маскировка логитов при сэмплинге. Перед выбором следующего токена берём сырые логиты из llama.cpp, ставим -inf на все позиции кроме речевых токенов и EOS:

logits = np.array(self.llm_gguf.scores[logit_pos], dtype=np.float32)# Разрешаем только речевые токены [151936, 151936+6561) и EOSvalid = np.full(n_vocab, False)valid[speech_offset : speech_offset + base_speech_size] = Truevalid[eos_token_id] = Truelogits[~valid] = -np.inf# Дальше стандартный softmax + top-k/top-p сэмплингprobs = np.exp((logits - logits[valid].max()) / temperature)probs /= probs.sum()next_token = np.random.choice(n_vocab, p=probs)

Встроенный sample() из llama-cpp-python тоже работает, но иногда может вернуть текстовый токен — в этом случае код автоматически переключается на маскировку.


А как насчёт TRT-LLM?

Существует форк FastCosyVoice с поддержкой TensorRT-LLM. Это мощный инструмент, но он требует Docker и конвертацию модели под конкретную GPU. Бенчмарки там только на RTX 3090 и RTX 5060ti. На слабых картах это либо не заведётся, либо потребует часов настройки.

llama-cpp-python бэкенд устанавливается одной командой:

pip install llama-cpp-python

Никакого Docker, никакого Triton, никакой конвертации под GPU. Работает на слабых GPU, работает на CPU. Если у вас нет топовой карты — это ваш вариант.


Результаты

Тестировалось на NVIDIA T4, fp16:

Бэкенд

Среднее RTF

PyTorch fp16 (оригинал)

~1.17

llama-cpp-python F16 GGUF

~0.45

RTF < 1.0 означает что синтез идёт быстрее реального времени — можно использовать в стриминге без накопления лага.


Как попробовать

Установка:

pip install llama-cpp-python

Использование:

from cosyvoice.cli.cosyvoice import AutoModelcosyvoice = AutoModel(    model_dir='pretrained_models/Fun-CosyVoice3-0.5B',    load_llama_cpp=True,    gguf_model_path='/path/to/cosyvoice_llm_f16.gguf')# Все стандартные методы работают как обычноfor output in cosyvoice.inference_zero_shot(text, prompt_text, prompt_wav):    audio = output['tts_speech']

Скачать готовую GGUF модель: HuggingFace

Форк с llama-cpp-python бэкендом (для быстрого старта): Ferraronp/CosyVoice

PR в оригинальный репозиторий: FunAudioLLM/CosyVoice#1872

ссылка на оригинал статьи https://habr.com/ru/articles/1027934/