先に実測の結論を書きます。RTX 4070 (12GB) で音声生成モデル 2 つを動かした結果です。
- ACE-Step 1.5 (BGM・楽曲生成、MIT): 60 秒のインストが生成 19.6 秒。リアルタイムの約 3 倍速
- MOSS-SoundEffect v2.0 (効果音生成、Apache 2.0): 5 秒の効果音に 4 分半 (steps を下限の 25 に削った場合。推奨値 100 だと 17 分)。パイプラインが VRAM 14.5GB を要求し、12GB では常に共有メモリへ溢れるため
同じ「音を作るモデル」でここまで差が出ます。この記事では両方のセットアップ手順と実測値、そして途中で踏んだ罠を 5 つ書きます。罠には「生成 AI が完全無音の WAV を吐いた犯人が bash の 1 行だった」という、GPU もモデルも無関係のものが含まれます。
なお「そもそもなぜ音までローカルで作るのか」「画像生成 (Wan2GP) まで含めた GPU 1 枚の排他運用」といった全体像は kenimoto.dev 側に書いたので、背景はそちらをどうぞ:https://kenimoto.dev/ja/blog/rtx4070-local-bgm-sfx-ace-step-moss-soundeffect/
検証環境
| 項目 | 値 |
|---|---|
| GPU | RTX 4070 12GB |
| OS | Windows 11 + WSL2 (Ubuntu 24.04) |
| Python | 3.12 (uv で venv 管理) |
| 操作 | 別マシンから Tailscale 経由の ssh |
ACE-Step 1.5: セットアップと実測
ACE-Step 1.5 は 2B の DiT と 0.6B の言語モデルを組み合わせた音楽生成モデルです (v1 とはリポジトリが別なので注意)。REST API サーバーが同梱されているので、常駐させて curl で叩く運用にしました。
git clone --depth 1 https://github.com/ACE-Step/ACE-Step-1.5.git ~/ace-step/repo
cd ~/ace-step/repo
uv sync
# .env で turbo 構成を指定
cat > .env <<'EOF'
ACESTEP_CONFIG_PATH=acestep-v15-turbo
ACESTEP_LM_MODEL_PATH=acestep-5Hz-lm-0.6B
ACESTEP_DEVICE=cuda
ACESTEP_LM_BACKEND=pt
ACESTEP_INIT_LLM=auto
ACESTEP_API_HOST=0.0.0.0
ACESTEP_API_PORT=8010
EOF
uv run acestep-api
モデル (計 11GB) は初回リクエスト時に自動ダウンロードされます。ダウンロード中は API が完全に無応答になりますが、壊れているわけではないので待ちます。
API は非同期型で、/release_task にジョブを投げて /query_result をポーリングします。インスト曲は lyrics に [instrumental] を渡します。
curl -s -X POST http://localhost:8010/release_task -H "Content-Type: application/json" -d '{
"prompt": "lofi hip hop instrumental, mellow piano, vinyl crackle",
"lyrics": "[instrumental]",
"audio_duration": 60,
"thinking": false
}'
# → {"task_id": "..."} を /query_result で status=1 までポーリング
生成時間が曲の長さに対してどうスケールするかの実測です (turbo、thinking なし、同一プロンプト、API ポーリング込みの wall clock)。
| 曲の長さ | 生成時間 | 実時間比 | 備考 |
|---|---|---|---|
| 30 秒 | 34 秒 | 0.9 倍速 | サーバー起動後の 1 発目 (ウォームアップ込み) |
| 60 秒 | 15 秒 | 4.0 倍速 | ウォーム状態 |
| 120 秒 | 15 秒 | 8.0 倍速 | ウォーム状態 |
ウォームアップ後は 60 秒でも 120 秒でも約 15 秒。生成時間は曲の長さにほぼ比例せず、長い曲ほど 1 秒あたりのコストが下がります。
thinking: true にすると 0.6B LM が曲の構成を先に計画します。120 秒のシティポップで試したところ生成 104 秒でした。倍近く遅くなりますが、展開のある仕上がりになるので 1 曲聴かせる用途ならこちらです。
MOSS-SoundEffect v2.0: セットアップと実測
MOSS-SoundEffect v2.0 は復旦大学 OpenMOSS チームの効果音生成モデル (1.3B DiT) です。テキスト記述から最大 30 秒の 48kHz WAV を作ります。プロンプトは英語か中国語のみです。
MOSS-TTS リポジトリ 内の moss_soundeffect_v2 パッケージを editable install します。
git clone --depth 1 https://github.com/OpenMOSS/MOSS-TTS.git ~/moss-sfx/repo
cd ~/moss-sfx/repo/moss_soundeffect_v2
uv venv ~/moss-sfx/.venv --python 3.12
VIRTUAL_ENV=~/moss-sfx/.venv uv pip install --index-strategy unsafe-best-match \
--extra-index-url https://download.pytorch.org/whl/cu128 -e ".[torch-cu128]"
生成は公式パイプラインを数行で呼ぶだけです。
import torch, torchaudio
from moss_soundeffect_v2 import MossSoundEffectPipeline
pipe = MossSoundEffectPipeline.from_pretrained(
"OpenMOSS-Team/MOSS-SoundEffect-v2.0",
torch_dtype=torch.bfloat16, device="cuda")
audio = pipe(prompt="rain falling on a tin roof, distant thunder rumble",
seconds=5, num_inference_steps=100, cfg_scale=4.0,
sigma_shift=5.0, seed=0)
torchaudio.save("out.wav", audio[0].detach().cpu(), pipe.sample_rate)
問題は速度です。torch.cuda.max_memory_allocated() で測るとパイプライン全体のピークが 14.5GB あり、12GB の 4070 では常時スピルします。1 ステップあたり約 10 秒。そこでステップ数と生成秒数を掃引して、実用の下限を探りました (同一プロンプト・seed 0・cfg 4.0)。
| 音の長さ | steps | 生成時間 | 1 step あたり |
|---|---|---|---|
| 5 秒 | 100 (推奨値) | 1,024.9 秒 (17.1 分) | 10.2 秒 |
| 5 秒 | 50 | 524.1 秒 (8.7 分) | 10.5 秒 |
| 5 秒 | 25 | 266.7 秒 (4.4 分) | 10.7 秒 |
| 3 秒 | 25 | 262.8 秒 (4.4 分) | 10.5 秒 |
| 10 秒 | 25 | 260.7 秒 (4.3 分) | 10.4 秒 |
全 run で VRAM ピーク 14.5GB (max_memory_allocated)、モデルロードは別途 12〜19 秒です。結果は 2 行に要約できます。
- 生成時間はステップ数に完全線形 (約 10.3 秒/step)。100→50→25 で誤差 1% 以内の綺麗な半減・1/4
- 音の長さは生成時間に影響しない。3 秒でも 10 秒でも 4.4 分 (latent 長よりパイプラインの固定コストが支配的)
実用上の帰結: 時間コストが長さと無関係なので、毎回長め (最大 30 秒) に生成して欲しい部分を切り出すのが最適です。3 秒のジングル 3 回 = 13 分、30 秒 1 回から 3 箇所切り出し = 4.4 分。品質面は、25 steps でも波形とスペクトログラム上の破綻はありませんでした (mean -21〜-23dB、max -0.7〜-2.6dB)。細部の質感は最終的に耳での確認が要りますが、環境音系はまず 25 steps で試す運用にしています。
音の確認には ffmpeg -af volumedetect と showspectrumpic によるスペクトログラムを併用しました。後述の罠 4 を踏んで以来、「聴く前に波形を見る」が習慣です。
踏んだ罠 5 連発
ここからが本題という説もあります。5 つとも別のレイヤで起きました。先にまとめを 1 枚に。
罠 1: ポート競合を「別サービスの /health」が隠す
ACE-Step を最初ポート 8001 で起動したところ、ヘルスチェックは通るのに生成タスクが一切受け付けられませんでした。
原因はポートの先客です。同じマシンに常駐していた別の FastAPI サービスが、8001 を先に握っていました。ACE-Step 側は bind に失敗してログに [Errno 98] Address already in use を残して落ちていたのですが、curl localhost:8001/health には先住サービスが {"status":"healthy"} を返すため、一見「起動成功」に見えます。
対策はヘルスチェックでサービス名まで検証することです。
# ×: これは「誰かが8001でhealthyと言っている」ことしか確認していない
curl -s localhost:8001/health | grep healthy
# ○: どのサービスが応えているかまで見る
curl -s localhost:8010/health | grep '"ACE-Step API"'
罠 2: uv が PyTorch の extra index で解決を拒否する
MOSS の uv pip install は素直に流すと tqdm の依存解決エラーで止まります。PyTorch の extra index には tqdm などの汎用パッケージの古い版が置いてあり、uv はデフォルトで「最初に見つけた index の版しか使わない」dependency confusion 対策を取るためです。今回は両方とも信頼できる index なので、--index-strategy unsafe-best-match (全 index から最良版を選ぶ) で通しました。
罠 3: ssh host 'pkill -f "acestep"' が自分自身を殺す
サーバー再起動のために ssh 越しに pkill を打つと、exit 255 で落ちて後続の nohup が実行されませんでした。毎回です。
pkill -f はプロセスのコマンドライン全体を検索します。ssh がリモートに作るシェルのコマンドラインには検索パターン自身 (pkill -f "acestep") が含まれるため、pkill が自分の親シェルにマッチして殺すわけです。ssh セッションごと死ぬので exit 255 になります。
古典的な回避策はブラケット記法です。
# ×: パターンが自分のコマンドラインにマッチする
ssh host 'pkill -f "acestep-api"'
# ○: [a]cestep は正規表現として acestep にマッチするが、
# 文字列 "[a]cestep" 自身にはマッチしない
ssh host 'pkill -f "[a]cestep-api"'
罠 4: 完全無音の WAV — 犯人は VAR=$(cat) cmd "$VAR"
一番のお気に入りです。MOSS で生成した WAV が、エラーもなく、サイズも正常で、完全な無音でした。volumedetect で mean_volume -90.3dB。デジタル的にほぼゼロです。
モデルのパラメータを疑い、公式スクリプトと差分を取り、VRAM の状態を疑い、最後にプロセスのコマンドラインを見て気づきました。
python gen-sfx.py --prompt --seconds 5 ...
^^ プロンプトが空
プロンプトをクォート事故なしで渡すために stdin 経由にしていたのですが、その受けがこう書かれていました。
# ×: SFXPROMPT は空文字で渡る
SFXPROMPT=$(cat) python gen-sfx.py --prompt "$SFXPROMPT"
VAR=value cmd 形式の一時環境変数代入では、コマンドライン上の "$SFXPROMPT" の展開が代入より先に行われます。代入は cmd の環境変数にしか効かないので、引数の "$SFXPROMPT" は代入前の値、つまり空文字です。bash の仕様通りの動きです。名誉のために書いておくと、shellcheck はこの形を SC2097 / SC2098 でちゃんと警告します。ssh に渡すリモートコマンド文字列の中に埋めていたため、shellcheck の目が届いていませんでした。
# ○: 代入とコマンドを ; で分ける
SFXPROMPT=$(cat); python gen-sfx.py --prompt "$SFXPROMPT"
空プロンプトを渡された MOSS は「無」を条件に生成し、律儀に無音を出力していました。エラーにならないのが今回の学びで、以後、生成スクリプトには max_volume < -60dB なら警告 の無音ガードを入れています。
MAXVOL=$(ffmpeg -i out.wav -af volumedetect -f null - 2>&1 | grep -oP 'max_volume: \K[-0-9.]+')
awk -v v="$MAXVOL" 'BEGIN{exit !(v < -60)}' && echo "⚠️ ほぼ無音です"
罠 5: GPU 共有ソフトの watchdog が生成中に VRAM を奪い返す
このマシンはアイドル時間に GPU を貸し出すサービス (Salad) を常駐させています。生成前にサービスを止めて VRAM を空ける運用なのですが、ある生成が 17 分経っても終わりませんでした。相手は 5 秒の効果音です。
nvidia-smi を見ると、止めたはずの貸し出しワークロードが VRAM を 7.4GB 確保しています。サービス (SaladBowl) を net stop で止めても、常駐 UI (Salad.exe) が watchdog としてサービスを自動再起動していました。生成途中で VRAM を奪われ、スピルが悪化して事実上進まなくなっていたわけです。
確実に止めるには UI 側も含めて落とす必要がありました。
Stop-Process -Name Salad,Salad.Bootstrapper -Force
net stop SaladBowl
# VRAM 解放までさらに 20 秒〜2 分かかる
この手の「サービスを止めたのに何かが戻す」は、GPU 共有系に限らず常駐エージェント全般で起きます。生成ジョブの前に「VRAM が実際に空いたか」を数値で確認するステップを挟むのが安全です。
まとめ
- ACE-Step 1.5 (turbo) は 12GB GPU で実用的に速く、BGM 量産に使えます。60 秒 = 19.6 秒
- MOSS-SoundEffect v2.0 は音は良いものの 14.5GB 要求で、12GB ではスピル前提。ステップ数を削っても数分単位です
- 罠は GPU 層 (watchdog)、パッケージ層 (uv index)、シェル層 (pkill 自滅、一時代入の展開順序)、サービス層 (health の偽陽性) に分散していました。音声生成固有の問題は実はひとつもありません
- 完全無音の WAV が出たら、モデルを疑う前にプロンプトが本当に渡っているかを見てください
📘 この記事の内容をさらに詳しく知りたい方へ
ハーネス・エンジニアリング: AIを"使う"から"操る"へ -- 生成モデルを「スキル + ガード + 自動復帰」で運用に組み込む設計論。今回の無音ガードや Salad ペア運用の背後にある考え方です
全体のストーリー (なぜローカルで作るのか、画像生成込みの GPU 1 枚運用、生成した曲と効果音がどうなったか) は kenimoto.dev の記事に書きました:https://kenimoto.dev/ja/blog/rtx4070-local-bgm-sfx-ace-step-moss-soundeffect/
X に投稿した生成物のサンプルはこちら: https://x.com/kenimo49/status/2083931739173662792


