DGX Spark(GB10)で動かしていたローカルLLMスタックが「コンテナ再起動ループ」「応答遅延」を起こしたときの、
原因究明の手順と切り分け方をまとめます。構築編の続編ですが、単体でも読める内容です。※ IPアドレス・ホスト名・認証情報はプレースホルダに置き換えています。
シリーズ
- 前編: 【DGX Spark】TensorFold + LiteLLM + opencode でローカルLLM環境を構築する
- 中編: 不安定化の切り分け(本記事)
- 後編: TensorFold で3モデル比較(量子化・形式の違い)
1. 症状
前提: 2つの推論サーバを併用していました。
tensorfold= Qwen3.8-27B(主に使う側)、tensorfold2= Qwen3.6-35B-A3B です。
- 推論コンテナの一方(Qwen3.6-35B-A3B 側)が数十秒周期で再起動を繰り返す。
- 応答が遅い・タイムアウトする。特に長いコンテキストで顕著。
- ホストのSSHは生きているので「ホスト全体の停止」ではない。
2. まず「どのコンテナが落ちているか」を特定する
# 稼働状況と再起動回数
docker ps -a --format 'table {{.Names}}\t{{.Status}}'
for c in tensorfold tensorfold2 tensorfold-litellm-1; do
printf '%s: ' "$c"
docker inspect -f '{{.State.Status}} restarts={{.RestartCount}} exit={{.State.ExitCode}} oom={{.State.OOMKilled}}' "$c"
done
実際には Qwen3.6-35B-A3B 側のコンテナが restarts=96 まで増えており、exit=1(アプリ異常終了)でした。
ライフサイクルイベントで「再起動の周期」も見えます。
docker events --since 1h --until 0s --filter type=container \
--format '{{.Time}} {{.Actor.Attributes.name}} {{.Action}} exit={{.Actor.Attributes.exitCode}}' \
| grep -vE 'exec_'
Docker の
restart: unless-stoppedは指数バックオフで再起動するため、約60秒周期で start→die を繰り返す挙動になります。
3. アプリのログとカーネルログを見る
3.1 アプリ側
docker logs --tail 80 tensorfold2
3.2 カーネル側(決定的な証拠)
sudo dmesg -T | grep -iE 'nvrm|no_memory|oom|killed|reset' | tail -40
# もしくは
sudo journalctl -k --since '7 days ago' | grep -iE 'nvrm|no_memory'
ここで GPUメモリの確保失敗 が出ていました。
NVRM: nvCheckOkFailedNoLog: Check failed: Out of memory [NV_ERR_NO_MEMORY]
returned from _memdescAllocInternal(pMemDesc) @ mem_desc.c:1359
3.3 日別に集計して「時系列」で切り分ける
これが最も効きました。 発生を日別に数えると、原因の時期がはっきりします。
sudo journalctl --since '45 days ago' -o short-iso \
| grep 'NV_ERR_NO_MEMORY' | cut -c1-10 | sort | uniq -c
結果(例):
| 日付 | 発生回数 |
|---|---|
| 10/05 | 7 |
| 10/06 | 129 |
| 10/07 | 75 |
| 10/08 | 17 |
| 10/09 | 177 |
カーネルは 10/09 に更新していたが、急増は 10/06 から。
→ 「カーネル更新が原因」ではなく、その前に始まったメモリ逼迫が原因、と切り分けられます。
4. メモリの実態を把握する
free -h
grep -E 'MemTotal|MemFree|MemAvailable|Cached|Committed' /proc/meminfo
nvidia-smi --query-compute-apps=pid,process_name,used_memory --format=csv
GB10 の注意点:
nvidia-smiのメモリ表示はNot Supportedになります(ユニファイドメモリのため)。
メモリはfree -hを見るのが正解です。
逼迫時の例:
total used free buff/cache available
Mem: 121Gi 86Gi 30Gi 6.0Gi 34Gi
-
freeが 30GiB しかない。 - さらに
freeの数字には**ディスクキャッシュ(解放可能)**が含まれるため、実質はavailableを見ます。
5. 原因
- GB10 は CPU/GPU がメモリを共有(
MemTotal ≈ 121GiB)。 -
2つのLLMサーバが同時稼働し、各コンテナのCUDAメモリ上限の合計が
64 + 50 = 114GiB。 - 加えて
shm_size: 16gb×2、OS・他プロセス分。 - ピーク時にオーバーサブスクライブ →
NV_ERR_NO_MEMORY→ コンテナexit 1→ 再起動ループ。
MemTotal 121GiB に対して:
tensorfold(27B) 上限64 + tensorfold2(35B) 上限50 + shm 16+16 + OS …
→ ピーク時に破綻
6. 対策
6.1 Qwen3.6-35B-A3B を縮小(応急処置)
| 項目 | 変更前 | 変更後 |
|---|---|---|
--context |
188657 | 131072 |
CUDA_MEMORY_LIMIT_GB |
50 | 44 |
shm_size |
16gb | 8gb |
起動見積りが 42.3GiB → 32.1GiB に低下。
6.2 Qwen3.6-35B-A3B を停止(根本)
「1種類しか動かさない」方針に合わせ、compose 定義を無効化して停止。
docker compose stop tensorfold2 # tensorfold2 = Qwen3.6-35B-A3B
# compose ファイル上のサービス定義をコメントアウトし、depends_on からも外す
docker compose up -d
6.3 結果
| 時点 | free |
|---|---|
| 変更前 | 30 GiB |
| Qwen3.6-35B-A3B 停止後 | 80〜90 GiB |
NV_ERR_NO_MEMORY は変更後 0件、コンテナは restarts=0 で安定。
7. 切り分けチェックリスト(再発時に)
-
docker inspectで どのコンテナが落ちているか / RestartCount / ExitCode を確認 -
docker logsでアプリ側のエラー -
dmesg/journalctlでNVRM/NO_MEMORY/ OOM -
journalctlの 日別集計で「いつからか」を時系列で確認(=カーネル更新等との切り分け) -
free -h(availableを見る)、nvidia-smi(GB10はN/A) - コンテナの CUDAメモリ上限の合計 vs 実メモリ を電卓で確認
-
shm_sizeの合計も忘れずに
8. 教訓
- ユニファイドメモリでは「上限の合計」が命取り。天井が高くても、同時に伸びれば破綻する。
- 不安定の切り分けは 「カーネル/ドライバ」より先に「メモリ」を疑う。日別集計が効く。
-
nvidia-smiが使えない GB10 では、free -hとコンテナ上限が一次情報。 - 併存は便利だが、余白20GiB以上を残すか、片方を止める設計にする。
シリーズ
- 前編: 【DGX Spark】TensorFold + LiteLLM + opencode でローカルLLM環境を構築する
- 中編: 不安定化の切り分け(本記事)
- 後編: TensorFold で3モデル比較(量子化・形式の違い)