作業日: 2026-08-20
環境: 自宅の RTX 3060 機(VRAM 12GB / システムRAM 30GB → 64GB)
前回のあらすじ: MiniMax-Music3 を RunPod で動かす — 実測メモ
スクリプト・計測データ・生成音源はGitHubに置いた:
masafykun/minimax-music3-local
3行まとめ
- システムRAMを 30GB → 64GB に増設したら、以前ホストごと固まった MiniMax-Music3 が RTX 3060(VRAM 12GB)でも完走した
- ただし 60秒の曲に37分44秒(リアルタイム比 37.7倍)。RunPod の A6000(0.47倍)と比べて 80倍遅い
- 「動くか」と「使えるか」は別問題だった。RAMの壁を越えたら、次はVRAMの壁が見えた(ピーク 8.79GiB / 12GiB)
0. 前回の失敗(なぜ増設したのか)
同じ機体で Music3 を動かそうとして、ホストごと70分無応答にしたことがある。
- Music3 のウェイトは bf16 で約 26GB。CPUオフロード構成では、これが VRAMではなくシステムRAM上に常駐する
- RAM 30GB に 26GB を載せると、OSとDockerコンテナの分が残らない
- swap 40GB が「滑走路」になり、OOM Killer が発火しないまま無限にスワップスラッシングした
- 巻き添えで、同じ機体で回していた地デジ全録の1時間ブロックが全損した
このとき出した結論が 「64GBに増設するまでローカルでは動かない」 だった。今回それを検証した。
1. 増設した内容
| 項目 | 内容 |
|---|---|
| 構成 | 16GB × 4枚(4スロット全埋まり) |
| モジュール | Samsung DDR4 M378A2K43EB1-CWE、2Rank |
| 動作速度 | 3200 MT/s(定格、クロック落ちなし) |
| チャネル | Controller0/1 に2枚ずつ=デュアルチャネル成立 |
| 結果 |
MemTotal: 64526624 kB(実効 61GiB) |
4枚挿しは速度が落ちることがあるが、今回は Configured Memory Speed も 3200 MT/s のままだった。
sudo dmidecode -t memory | grep -E "Size:|Speed:|Locator:"
実行環境
| 項目 | 値 |
|---|---|
| CPU | 12th Gen Core i7-12700(12コア / 20スレッド) |
| GPU | RTX 3060 12GB(driver 595.84) |
| RAM | 64GB DDR4-3200 |
| Python | 3.12.14 |
| PyTorch | 2.13.0+cu132(CUDA 13.2) |
| diffusers | 0.40.0.dev0 |
| transformers | 5.15.0 |
| numpy | 2.5.2 |
| soundfile | 0.14.0 |
重要: この機体は地デジ8局の常時録画とエンコードを同時に回している。つまり「アイドルのGPU機で測った綺麗な数字」ではなく、実運用中の裏で回した実測である。
2. 先に守りを固める(これが本題かもしれない)
前回の事故の本質は「ジョブが死ぬ代わりにホストが死んだ」ことだった。同じことを繰り返さないよう、3層で守ってから実行した。
層1: cgroup で上限を掛ける(主砦)
safe-run という小さなラッパを噛ませる。
#!/bin/bash
# safe-run [-m 上限|auto] [-s] -- コマンド...
# -m auto (既定) 実際のavailableから RESERVE(6GB) を引いて自動計算
# -s swapを許可(既定は禁止=swap thrashingを防ぐ)
set -u
RESERVE_GB=${SAFE_RUN_RESERVE_GB:-6}
AVAIL_GB=$(free -g | awk '/^Mem:/{print $7}')
LIMIT="$(( AVAIL_GB - RESERVE_GB ))G"
exec systemd-run --user --scope --quiet \
-p MemoryMax="$LIMIT" -p MemorySwapMax=0 -p OOMPolicy=kill -- "$@"
肝は MemorySwapMax=0。前回はswapが滑走路になって OOM Killer が発火しなかったので、swapを明示的に禁止して「溢れたら即死」にする。
今回は自動計算(約50GB)ではなく、明示的に32GBに絞った。Music3 の所要は26GBなので、
最悪ケース = 常駐5GB + ジョブ32GB = 37GB / 61GB
となり、録画側に24GB以上が必ず残ることが算数で保証できる。
safe-run -m 32G -- ./run.sh lowvram 60
層2: watchdog(保険)
cgroupが効けば不要だが、ホスト側の空きが危険域に落ちたらジョブを能動的に止める見張りも置いた。
while :; do
kill -0 "$PID" 2>/dev/null || exit 0
AVAIL=$(free -g | awk '/^Mem:/{print $7}')
if [ "$AVAIL" -lt "$FLOOR_GB" ]; then
kill "$PID"; sleep 5; kill -0 "$PID" 2>/dev/null && kill -9 "$PID"
exit 1
fi
sleep 5
done
ここで踏んだ罠: 最初は pgrep -f run_music3.py で生死を見ていた。ところが監視スクリプト自身のargvに run_music3.py という文字列が含まれるため、自分自身にマッチしてしまう。ジョブが死んでも「まだ生きている」と誤認して永久に終わらない。
さらに修正中、pkill -f "watchdog.sh run_music3.py" を実行したら、そのコマンドを実行しているシェル自身のargvにもパターンが含まれていて自分を撃った。
監視対象はPIDで指定すること。 パターンマッチは自己マッチする。
層3: earlyoom / systemd-oomd(最後の砦)
earlyoom -r 3600 -m 8 -s 50 \
--avoid '^(ffmpeg|curl|node|dockerd|containerd|sshd|systemd|tailscaled|rclone)$' \
--prefer '^(python|python3|blender|ollama)$'
録画パイプライン(ffmpeg / curl)を保護し、python を優先的に犠牲にする。閾値が割合ベース(8%)なので、RAM増設分が自動で効くのが良いところ。
3. 実行結果
TOTAL_SEC = 2264 (37分44秒)
EXIT = 1 (※メモリではなく書き出しのバグ。後述)
生成そのものは完走した。 2秒間隔で VRAM / RAM / swap を記録した結果がこちら。
フェーズ別の内訳
| フェーズ | 時刻 | 所要 | 割合 | VRAM |
|---|---|---|---|---|
| モデルロード | 22:28:49 → 22:29:27 | 38秒 | 1.7% | 173 → 3,013 MiB |
| 言語モデル(自己回帰) | 22:29:27 → 23:04:29 | 35分02秒 | 92.8% | 3,013 → 3,571 MiB |
| 拡散サンプリング(330ステップ) | 23:04:29 → 23:06:29 | 1分59秒 | 5.3% | 3,571 → 7,975 MiB |
| ボコーダ・デコード | 23:06:29 → 23:06:34 | 5秒 | 0.2% | 7,975 → 8,997 MiB |
時間の93%は言語モデルの自己回帰生成に消えていた。 拡散サンプリングは 330ステップを 2.78 it/s でわずか2分。ここを速くしても意味がない。
リソースのピーク
| 項目 | 実測 | 備考 |
|---|---|---|
| VRAMピーク | 8.79 GiB / 12 GiB (73%) | 最終のボコーダ段で発生 |
| ジョブのRSS | 26.3 GB(終始一定) | ウェイトそのもの |
| ホストRAMピーク | 32.75 GB / 61 GB | 録画パイプライン込み |
| swap使用 | 最大 300 MB / 8 GB | 前回の「40GB食い潰し」とは別世界 |
| 巻き添え | なし(録画8局は最後まで正常) |
8/15(30GB)との比較
| 30GB | 64GB | |
|---|---|---|
| 結末 |
OOM SIGKILL(92分後 EXIT=137) |
生成完走(37分44秒) |
| swap | 40GBを食い潰しホスト無応答 | 最大300MB |
| 録画への影響 | 2ブロック全損 | なし |
増設は明確に効いた。 RAMが律速だったという読みは正しかった。
4. 発見1: KVキャッシュの増加から生成速度を逆算できる
進捗バーが出ないフェーズが35分続くので、生きているのか固まっているのか分からない。そこで VRAMの増え方を手がかりにした。
言語モデルの構成(language_model/config.json)は Qwen3 系:
| パラメータ | 値 |
|---|---|
num_hidden_layers |
36 |
num_key_value_heads |
8 |
head_dim |
128 |
dtype |
bfloat16 |
max_position_embeddings |
10,240 |
1トークンあたりのKVキャッシュは:
2 (K,V) × 36層 × 8ヘッド × 128 × 2バイト = 147,456 バイト = 144 KiB/token
実測のVRAM増加は 13.8 MiB/分。したがって:
13.8 MiB/分 ÷ 144 KiB/token ≈ 98 token/分 ≈ 1.6 token/秒
進捗表示のないモデルでも、KVキャッシュの増加ペースから生成速度を推定できる。 「固まっているのか進んでいるのか」の判断材料として実用的だった。
5. 発見2: RAMの壁を越えたら、次はVRAMの壁が見えた
これが今回いちばん重要な発見かもしれない。
VRAMは生成中ずっと3.5GB程度しか使っていなかったのに、最後のボコーダ段で一気に 8,997 MiB (8.79 GiB) まで跳ねた。
23:04:29 VRAM 3,571 MiB ← 拡散開始
23:06:29 VRAM 7,975 MiB ← ボコーダ開始
23:06:31 VRAM 8,997 MiB ← ピーク(12GiB中73%)
これは 60秒の曲での話である。Music3 は公称「最大5分の楽曲」に対応する。ボコーダのVRAM消費が曲の長さに比例するなら、3分を超えたあたりで12GBのVRAMを超える計算になる。
つまり RTX 3060 でのローカル実行は、60秒なら通るが、フルサイズの曲では今度はVRAM側で詰まる可能性が高い。 RAMを増設しても、この機体で5分の曲を作れる保証はない。
(RunPod の A6000 では VRAM 23.3GB を使い、曲の尺に依存しなかった。オフロードなしのフル構成だとメモリの使われ方自体が変わる。)
6. で、実用的なのか? — 結論は「RunPodを借りろ」
| ローカル RTX 3060 + RAM 64GB | RunPod RTX A6000 | |
|---|---|---|
| 60秒の曲 | 37分44秒 | 約28秒 |
| リアルタイム比 | 37.7倍 | 0.47倍 |
| VRAM | 8.79GB(尺に比例、12GBが天井) | 23.3GB(尺に依存しない) |
| コスト | 電気代のみ | 5分の曲で 約14円 |
| 速度差 | — | 約80倍速い |
「動く」と「使える」は違った。
37分待つ間、GPUは使用率100%で張り付いているので他の作業もできない。5分の曲を作ろうとすれば3時間コースで、しかもVRAMが足りるか怪しい。14円払ってRunPodを借りたほうが、あらゆる面で合理的である。
ではRAM増設は無駄だったのか? そうではない。
- 「動かない」が「遅い」に変わった — 選択肢が生まれた。ネットワークが死んでいても、急がないなら回せる
- 同じ機体で重いジョブと常時録画を同居させられるようになった — 実はこちらの価値のほうが大きい。以前は重いジョブが録画を巻き添えにしていた
- 律速がRAMからVRAMに移った — 次に投資すべき場所がはっきりした
7. 最後に踏んだ罠: 37分かけて生成した音声を、書き出しの1行で失った
生成が完走した直後、これで落ちた。
File "run_music3.py", line 76, in main
sf.write(args.out, audio.T.float().cpu().numpy(), pipe.sampling_rate)
^^^^^^^^^^^^^
AttributeError: 'numpy.ndarray' object has no attribute 'float'
pipe(..., output="audios") の戻り値が torch.Tensor ではなく numpy.ndarray だった。.float().cpu() は torch のメソッドなので存在しない。diffusers のバージョンによって戻り値の型が変わる。
37分かけて生成した音声も、計測値のJSONも、この例外で丸ごと消えた。(幸い別プロセスでCSVを記録していたので、この記事の数字は残った。)
対策: 両方に対応し、計測値は必ず先に保存する
arr = audio
if hasattr(arr, "detach"): # torch.Tensor の場合
arr = arr.detach().float().cpu().numpy()
arr = np.asarray(arr)
def save_metrics():
with open(args.metrics, "w") as f:
json.dump(m, f, ensure_ascii=False, indent=2)
try:
sf.write(args.out, arr.T, pipe.sampling_rate)
m["out_mb"] = round(os.path.getsize(args.out) / 1024**2, 2)
except Exception as e:
m["write_error"] = f"{type(e).__name__}: {e}"
np.save(os.path.splitext(args.out)[0] + ".npy", arr) # 生配列だけでも残す
save_metrics()
raise
save_metrics()
教訓: 長時間ジョブでは「計測値と生成物の保存」を最優先で、例外に巻き込まれない形で書くこと。数十分かけた実測値は後から復元できない。別プロセスでリソースを記録しておくのも保険として有効だった。
修正後: ちゃんと曲ができた
上記の修正を入れて、子供向けの曲(60秒指定)を生成し直したところ、今度は最後まで通って wav が出力された。
| 項目 | 実測 |
|---|---|
| 出力 | 56.5秒 / 44.1kHz / ステレオ / 9.5MB |
| 生成時間 | 50分28秒(リアルタイム比 50.5倍) |
| VRAMピーク | 8.59 GiB / 12 GiB |
| RAMピーク | 31.92 GB |
| 無音区間 | 0秒(全域に音がある) |
実際に生成された音源がこれ(56.5秒 / 44.1kHz ステレオ。ブラウザでそのまま再生できる):
kids_song.wav
歌詞と曲想はコマンドライン引数で渡せるようにした。曲想は英語、歌詞は日本語で通る。
python run_music3.py --mode lowvram --duration 60 --seed 21 \
--lyrics-file lyrics_kids.txt --prompt-file prompt_kids.txt \
--out kids_song.wav
曲想プロンプトの例(この手のモデルは子供の歌声が苦手なので、あえて「大人の女性が優しく歌う」と指定している):
Genre: Japanese children's song, warm and gentle nursery style. BPM: 96. Key: C major.
Vocals: one soft adult female lead, singing slowly and very clearly with kind diction,
like a kindergarten teacher; no child voices.
Arrangement: acoustic piano, glockenspiel, music box bells, light nylon-string guitar,
soft shaker, warm strings pad entering in the chorus.
No heavy drums, no electric guitar, no synthesizer, no dark or sad mood.
おまけ: GPUを取り合うと1.3倍遅くなる
1回目(Music3のみ)と2回目(Blenderのレンダリングと並走)で、同じ60秒尺なのに所要時間が変わった。
| 1回目(単独) | 2回目(Blender並走) | |
|---|---|---|
| 生成時間 | 37分44秒 | 50分28秒 |
| リアルタイム比 | 37.7倍 | 50.5倍 |
| VRAMピーク | 8.79 GiB | 8.59 GiB |
1.34倍の悪化。 VRAMには収まっているので、効いているのは演算資源の取り合いのほう。RAMを増やして「同居できるようになった」とはいえ、同居させれば当然どちらも遅くなる。並列で回せることと、速く終わることは別問題だった。
まとめ
- RAM 30GB → 64GB で、ローカル実行は「不可能」から「可能」になった。 律速がシステムRAMだったという読みは正しかった
- ただし60秒の曲に37分44秒(リアルタイム比37.7倍)。RunPodのA6000より約80倍遅く、実用性では勝負にならない
- 時間の93%は言語モデルの自己回帰生成。拡散サンプリングは2分しかかかっていない
- **RAMの壁を越えたら、VRAMの壁(8.79GiB / 12GiB)が見えた。**長い曲では今度はVRAMで詰まる可能性が高い
- 増設の本当の価値は「速くなること」ではなく、重いジョブが他のサービスを巻き添えにしなくなったこと
- 長時間ジョブは cgroupで上限を掛け、swapを禁止し、計測値を例外に巻き込まれない形で保存する
- 書き出しバグを直して再実行したところ、60秒の曲が50分28秒で無事に完成した(Blenderと並走したため単独時より1.34倍遅い)
リポジトリ
この記事で使ったスクリプト(safe-run / watchdog / 計測)、2秒間隔の実測CSV、生成音源をまとめて公開している。
masafykun/minimax-music3-local
-
scripts/— 推論本体とcgroup上限ラッパ、見張り、計測 -
measurements/— 1,293サンプルの実測CSV(本記事の数字の元データ) -
samples/— 生成音源と、歌詞・曲想プロンプトの実例
モデル重みは同梱していない。生成音源は MiniMax-Music3 COMMUNITY LICENSE に従う。