← 기사 목록
日本語https://zenn.dev/topics/llm/feed

【Irodori-TTS】DGX Spark (GB10) でOpenAI互換TTSサーバーを構築する

추출된 키워드

49
DGX Spark·5GB10·5OpenAI互換TTSサーバー·5Irodori-TTS·5Irodori-TTS-Server·4torchaudio·4NVIDIA NGC·4PyTorch·4SM_121·4CUDA compute capability·4Docker·4NVIDIA·4voiceGenServer·4Aratako/Semantic-DACVAE-Japanese-32dim·3Aratako/Irodori-TTS-500M-v3·3compose.yaml·3fastapi·3sentencepiece·3HTTP API·3HuggingFace·3soundfile·3scipy.signal.lfilter·3nvcr.io/nvidia/pytorch:26.01-py3·3Ubuntu·3Linux aarch64·3DACVAE codec·3cu128·3torchcodec·3CUDA 13.1·3audiotools·3Kウェイティングフィルタ·2LUFS·2webm·2aac·2opus·2LAN·2ogg·2m4a·2mp3·2flac·2wav·2fp32·2uvicorn·2pydantic-settings·2transformers·2peft·2accelerate·2libsndfile1·2ffmpeg·2

원문

16,302
【Irodori-TTS】DGX Spark (GB10) でOpenAI互換TTSサーバーを構築する

【Irodori-TTS】DGX Spark (GB10) でOpenAI互換TTSサーバーを構築する

はじめに

この記事は、Irodori-TTS を OpenAI 互換の HTTP API として LAN に公開するサーバー(voiceGenServer)を、NVIDIA DGX Spark(GB10)上に Docker で構築する手順をまとめたものです。

DGX Spark に搭載されている GB10 は CUDA compute capability が SM_121 と新しく、PyPI で配布されている通常の PyTorch wheel では動作しない問題があります。この記事では、その制約をどのように回避したかも含めて紹介します。

環境と前提

項目内容
ハードウェアNVIDIA DGX Spark (GB10 / SM_121)
ベースイメージ
nvcr.io/nvidia/pytorch:26.01-py3
OSUbuntu (Linux aarch64)
公開ポート8088

GB10 における PyTorch の制約

なぜ通常の wheel が使えないのか

GB10 は CUDA compute capability 12.1 (SM_121) のアーキテクチャです。PyPI で配布されている通常の PyTorch(cu128 対応版)を使って DACVAE codec を CUDA で実行しようとすると、以下のエラーが発生します。

RuntimeError: nvrtc: error: invalid value for --gpu-architecture (-arch)

この問題を回避するため、GB10 対応が進んでいる NVIDIA NGC の PyTorch コンテナを使用します。

nvcr.io/nvidia/pytorch:26.01-py3

torch:  2.10.0a0+a36e1d39eb.nv26.01.42222806
cuda:   True
GPU:    NVIDIA GB10 (compute capability 12.1)

torch.cuda.get_arch_list()
sm_121
は出ませんが、
sm_120
/
compute_120
互換として CUDA 実行は通ります。

pip から torch 系を入れてはいけない理由

Irodori-TTS の

requirements.txt
には以下が含まれています。
torch>=2.10.0
torchaudio>=2.10.0
torchcodec>=0.10.0

これらをそのまま pip でインストールすると、次の問題が起きます。

パッケージ問題
torch
NGC コンテナの GB10 対応 torch が pip の通常版に上書きされる
torchaudio
PyTorch cu128 の wheel が CUDA 12 系ライブラリを要求し、NGC 26.01(CUDA 13.1)環境と競合する
torchcodec
Linux aarch64 用 wheel が存在せずインストール不可

そのため Dockerfile では、

requirements.txt
からこれら 3 つを除外してインストールします。

torchaudio の shim について

torchaudio は NGC コンテナに含まれていませんが、Irodori-TTS 本体が

torchaudio.load
/
torchaudio.save
/
torchaudio.functional.resample
/
torchaudio.functional.lfilter
を呼んでいます。

Irodori-TTS 本体を編集せずに解決するため、サーバー起動スクリプト(

docker/run_server.py
)でサーバー起動前に最小限の shim を
sys.modules["torchaudio"]
に差し込んでいます。
API代替実装
torchaudio.load
soundfile.read
torchaudio.save
soundfile.write
torchaudio.functional.resample
torch.nn.functional.interpolate
torchaudio.functional.lfilter
scipy.signal.lfilter

lfilter
audiotools
がラウドネス正規化(LUFS計算のKウェイティングフィルタ)のために CPU 実行時に呼び出します。NGC コンテナには scipy が含まれているため、これで代替できます。

ディレクトリ構成

voiceGenServer/
├── Dockerfile
├── compose.yaml
├── .env
├── docker/
│   └── run_server.py       # torchaudio shim + サーバー起動スクリプト
├── Irodori-TTS/            # git clone
├── Irodori-TTS-Server/     # git clone
├── voices/                 # 参照音声置き場(.wav 等)
└── .cache/huggingface/     # モデルキャッシュ(自動生成)

セットアップ手順

1. リポジトリのクローン

作業ディレクトリを作成し、必要な 2 つのリポジトリをクローンします。

mkdir -p ~/Documents/voiceGenServer
cd ~/Documents/voiceGenServer

git clone https://github.com/Aratako/Irodori-TTS.git
git clone https://github.com/Aratako/Irodori-TTS-Server.git

動作確認済みのコミットは以下の通りです。

  • Irodori-TTS:
    256528abc65aa89eba5ff1388f8701a5b157076e
  • Irodori-TTS-Server:
    89f503cda41c30c09bcabd97b9d52f4128f4c9f3

2. 必要ディレクトリの作成

mkdir -p voices docker .cache/huggingface

3. ファイルの配置

以下の 4 つのファイルを作成します。

Dockerfile

FROM nvcr.io/nvidia/pytorch:26.01-py3

ENV DEBIAN_FRONTEND=noninteractive \
    PIP_NO_CACHE_DIR=1 \
    PYTHONUNBUFFERED=1 \
    HF_HOME=/workspace/.cache/huggingface \
    TORCH_FLOAT32_MATMUL_PRECISION=high

RUN apt-get update \
    && apt-get install -y --no-install-recommends ffmpeg libsndfile1 git \
    && rm -rf /var/lib/apt/lists/*

WORKDIR /workspace/voiceGenServer

# torch / torchaudio / torchcodec / sentencepiece を除外してインストール
COPY Irodori-TTS/requirements.txt /tmp/requirements.txt
RUN grep -v -E '^(torch|torchaudio|torchcodec|sentencepiece)([<>= @]|$)' /tmp/requirements.txt > /tmp/requirements-no-torch.txt \
    && python -m pip install --upgrade pip \
    && python -m pip install sentencepiece==0.2.1 \
    && python -m pip install --no-deps -r /tmp/requirements-no-torch.txt \
    && python -m pip install --no-deps \
        accelerate argbind descript-audiotools fire flatten-dict flask gradio-client \
        julius peft pyaml pydub pyloudnorm pystoi randomname safehttpx semantic-version \
        torch-stoi transformers blinker brotli ffmpy gitpython groovy hf-gradio \
        importlib-resources itsdangerous markdown2 orjson sentry-sdk starlette \
        termcolor tomlkit typer annotated_doc \
    && python -m pip install \
        "fastapi>=0.115.0" \
        "pydantic-settings>=2.6.0" \
        "python-dotenv>=1.0.1" \
        "python-multipart>=0.0.9" \
        "uvicorn[standard]>=0.32.0"

COPY Irodori-TTS /workspace/voiceGenServer/Irodori-TTS
RUN python -m pip install -e /workspace/voiceGenServer/Irodori-TTS --no-deps

COPY Irodori-TTS-Server /workspace/voiceGenServer/Irodori-TTS-Server
RUN python -m pip install -e /workspace/voiceGenServer/Irodori-TTS-Server --no-deps

COPY docker /workspace/voiceGenServer/docker

WORKDIR /workspace/voiceGenServer/Irodori-TTS-Server
EXPOSE 8088
CMD ["python", "/workspace/voiceGenServer/docker/run_server.py"]

requirements.txt
から
torch
/
torchaudio
/
torchcodec
を除外しているのが重要なポイントです。NGC コンテナの GB10 対応 torch を上書きしないようにするためです。

また、

sentencepiece>=0.1.99,<0.2
は Python 3.12 / Linux aarch64 で wheel がなく、ソースビルドも失敗するため
sentencepiece==0.2.1
を明示指定しています。動作上の問題は確認されていません。

compose.yaml

services:
  irodori-tts-server:
    build:
      context: .
      dockerfile: Dockerfile
    image: voicegen/irodori-tts-server:gb10
    working_dir: /workspace/voiceGenServer/Irodori-TTS-Server
    ipc: host
    user: "1000:1000"
    gpus: all
    ports:
      - "${IRODORI_PORT:-8088}:${IRODORI_PORT:-8088}"
    env_file:
      - .env
    environment:
      HF_HOME: /workspace/.cache/huggingface
      TORCH_FLOAT32_MATMUL_PRECISION: high
      IRODORI_MODEL_DEVICE: cuda
      IRODORI_CODEC_DEVICE: cuda
    volumes:
      - ./Irodori-TTS:/workspace/voiceGenServer/Irodori-TTS
      - ./Irodori-TTS-Server:/workspace/voiceGenServer/Irodori-TTS-Server
      - ./.cache/huggingface:/workspace/.cache/huggingface
      - ./voices:/workspace/voiceGenServer/voices
      - ./docker:/workspace/voiceGenServer/docker

Irodori-TTS
Irodori-TTS-Server
docker/
をボリュームマウントしているため、これらのファイルを変更した場合は
docker compose restart
だけで反映されます(ビルド不要)。

.env

# Server
IRODORI_HOST=0.0.0.0
IRODORI_PORT=8088
# Optional. When set, clients must send Authorization: Bearer <value>.
# IRODORI_API_KEY=change-me

# Model
IRODORI_HF_CHECKPOINT=Aratako/Irodori-TTS-500M-v3
IRODORI_CODEC_REPO=Aratako/Semantic-DACVAE-Japanese-32dim
IRODORI_MODEL_NAME=irodori-tts
IRODORI_MODEL_DEVICE=auto
IRODORI_CODEC_DEVICE=auto
IRODORI_MODEL_PRECISION=fp32
IRODORI_CODEC_PRECISION=fp32
IRODORI_COMPILE_MODEL=false
IRODORI_COMPILE_DYNAMIC=false
IRODORI_PRELOAD=false
IRODORI_MODEL_LOAD_TIMEOUT=300
IRODORI_MAX_CONCURRENT_SYNTHESIS=1
IRODORI_SYNTHESIS_WAIT_TIMEOUT=300

# Voices
IRODORI_VOICES_DIR=/workspace/voiceGenServer/voices
IRODORI_ALLOW_NO_REF_VOICE=true

# Defaults for /v1/audio/speech
IRODORI_DEFAULT_RESPONSE_FORMAT=wav
IRODORI_DEFAULT_NUM_STEPS=40
IRODORI_DEFAULT_T_SCHEDULE_MODE=linear
IRODORI_DEFAULT_SWAY_COEFF=-1.0
IRODORI_DEFAULT_DURATION_SCALE=1.0
IRODORI_DEFAULT_CFG_SCALE_TEXT=3.0
IRODORI_DEFAULT_CFG_SCALE_SPEAKER=5.0
IRODORI_DEFAULT_CFG_GUIDANCE_MODE=independent
IRODORI_DEFAULT_CHUNKING_ENABLED=true
IRODORI_DEFAULT_CHUNK_MIN_CHARS=80

IRODORI_VOICES_DIR
はコンテナ内の絶対パスで指定してください。

docker/run_server.py

以下のスクリプトを

docker/run_server.py
として保存します。torchaudio の shim を
sys.modules
に差し込んでから、サーバーを起動します。
import sys
import types
import importlib.machinery

import soundfile as sf
import torch
import torch.nn.functional as F


def _as_audio_array(audio):
    audio_cpu = audio.detach().to(device="cpu", dtype=torch.float32)
    return audio_cpu.squeeze(0).numpy() if audio_cpu.shape[0] == 1 else audio_cpu.T.numpy()


def _save(path, audio, sample_rate, *args, **kwargs):
    sf.write(str(path), _as_audio_array(audio), int(sample_rate))


def _resample(waveform, orig_freq, new_freq, *args, **kwargs):
    if int(orig_freq) == int(new_freq):
        return waveform
    original_shape = waveform.shape
    flat = waveform.reshape(-1, 1, original_shape[-1])
    new_length = max(1, round(original_shape[-1] * float(new_freq) / float(orig_freq)))
    resampled = F.interpolate(flat, size=new_length, mode="linear", align_corners=False)
    return resampled.reshape(*original_shape[:-1], new_length)


def _load(path, *args, **kwargs):
    data, sample_rate = sf.read(str(path), dtype="float32")
    wav = torch.from_numpy(data)
    if wav.ndim == 1:
        wav = wav.unsqueeze(0)
    else:
        wav = wav.T
    return wav, int(sample_rate)


def _lfilter(waveform, a_coeffs, b_coeffs, clamp=True, batching=False):
    import numpy as np
    from scipy.signal import lfilter as _scipy_lfilter

    device = waveform.device
    dtype = waveform.dtype
    w = waveform.detach().cpu().to(torch.float32).numpy()
    a = a_coeffs.detach().cpu().to(torch.float32).numpy()
    b = b_coeffs.detach().cpu().to(torch.float32).numpy()

    if a.ndim == 1:
        result = _scipy_lfilter(b, a, w, axis=-1)
    else:
        result = np.zeros_like(w)
        for i in range(a.shape[0]):
            result[i] = _scipy_lfilter(b[i], a[i], w[i], axis=-1)

    out = torch.from_numpy(result.copy()).to(dtype=dtype, device=device)
    if clamp:
        out = torch.clamp(out, -1.0, 1.0)
    return out


# torchaudio を使えないため soundfile + torch で shim を構築する
torchaudio = types.ModuleType("torchaudio")
torchaudio.__spec__ = importlib.machinery.ModuleSpec("torchaudio", loader=None)
torchaudio.__version__ = "0.0.soundfile-shim"
torchaudio.save = _save
torchaudio.load = _load
torchaudio.functional = types.SimpleNamespace(resample=_resample, lfilter=_lfilter)
sys.modules["torchaudio"] = torchaudio

from irodori_openai_tts.__main__ import main

main()

4. ファイアウォールの設定

LAN からアクセスできるよう、使用するポートを開放します。

sudo ufw allow 8088/tcp

5. Docker イメージのビルド

docker compose build

初回はベースイメージのプルも含むため、時間がかかります。

6. サーバーの起動

docker compose up irodori-tts-server

初回リクエスト時にモデルが HuggingFace からダウンロードされます(

.env
IRODORI_PRELOAD=false
の場合)。

API の使い方

ホストの LAN IP を

<HOST_IP>
に置き換えて使用してください。

ヘルスチェック

まず、以下のコマンドでサーバーが正常に起動しているか確認します。

curl http://<HOST_IP>:8088/health

音声生成(参照音声なし)

参照音声を用意しなくても音声生成できます。

.env
IRODORI_ALLOW_NO_REF_VOICE=true
が前提です。
curl http://<HOST_IP>:8088/v1/audio/speech \
  -H "Content-Type: application/json" \
  -d '{
    "model": "irodori-tts",
    "input": "こんにちは。これはテストです。",
    "voice": "none",
    "response_format": "wav"
  }' \
  --output speech.wav

音声生成(参照音声あり)

参照音声を指定すると、その声質に近い音声を生成できます。

# 1. voices/ に参照音声を置く
cp myvoice.wav voices/myvoice.wav

# 2. ファイル名(拡張子なし)を voice に指定する
curl http://<HOST_IP>:8088/v1/audio/speech \
  -H "Content-Type: application/json" \
  -d '{
    "model": "irodori-tts",
    "input": "こんにちは。",
    "voice": "myvoice",
    "response_format": "wav"
  }' \
  --output speech.wav

対応フォーマットは

.wav
.flac
.mp3
.m4a
.ogg
.opus
.aac
.webm
です。参照音声はサーバー再起動なしで即座に反映されます。

登録済みボイスの確認

curl http://<HOST_IP>:8088/v1/audio/voices

依存パッケージの問題と対処

--no-deps
でインストールしているパッケージが多いのは、ベースイメージの torch を壊さないようにするためです。構築中に発生した主な問題と対処をまとめておきます。
問題対処
starlette
が見つからない
fastapi
--no-deps
なしで通常インストール
audiotools
が見つからない
descript-audiotools
が抜けていたため追加
flatten_dict
が見つからない
flatten-dict
--no-deps
リストに追加
torchaudio.functional.lfilter
がない
run_server.py
の shim に
scipy.signal.lfilter
で実装した
_lfilter
を追加

注意点

参照音声ありで合成した場合、冒頭に短い異音が混入することがあります。LLM が出力したテキストの冒頭に不可視の文字列が入っていたことが原因と思われます。リクエスト側のスクリプトを見直すことで改善しました。

まとめ

今回は、Irodori-TTS を DGX Spark(GB10)上で OpenAI 互換の TTS API サーバーとして動かすための手順をまとめました。GB10 の CUDA 制約による torchaudio の shim 実装など、少し手間はかかりますが、一度構築してしまえば LAN 内から OpenAI 互換のインターフェースで日本語 TTS が使えるようになります。是非試してみてください。

私のXではLLMに限らず、AIを活用した業務改善情報の発信をしておりますのでご興味のある方は是非フォローをお願いします。https://x.com/Linus_lab