前回までのおさらいと今回のビルドの目的
前回記事で、Sonar(Aphrodite Engine、vLLMのフォーク)+ Ornith-1.0-9Bのreasoning/tool-callタグ断片漏れバグをvLLM本家の未マージ統一パーサーエンジンを移植して修正した話を書いた。この移植・検証の過程で使っていたのが、フォーク元リポジトリの最新コミット(283コミット分、公式リリースタグより先行)をソースから丸ごとビルドした開発環境sonar-nativeだった。今回はこのソースビルド自体が本番運用中のマシン上でどれだけ苦戦したかという話。
発端: WSLが突然クラッシュした
ある朝、sonar-nativeのフルビルドを走らせていたところ、WSL2インスタンスが突然フリーズして再起動していた。原因調査から始めることになった。
journalctl -b -1で直前ブートのログを追うと、犯人はすぐに見つかった。
oom-kill:constraint=CONSTRAINT_NONE,...task=chrome,pid=10887,uid=1000
Out of memory: Killed process 10887 (chrome) ...
カーネルのglobal OOM killerが発動し、chromeを殺したが焼け石に水で(実RSSはわずか3.9MB)、約90秒後にsystemdがinit.scope配下の全プロセスをSIGKILLで強制終了、WSLインスタンスごと再起動していた。
原因は単純で、Sonar(自作フォークのAphrodite Engine)のソースフルビルド(nvcc並列コンパイル)と、本番のAphroditeモデルサーバーが完全に同時刻に動いていたことだった。ビルドのcicc/cudafe++/ptxasが1プロセスあたり数百MB〜1GB、本番のAPHRODITE::Workerが1プロセス2.7GB前後、これらが重なって物理RAM 24GB + スワップ16GB = 合計40GBの上限に到達していた。
以前にも似た「nvccビルドがメモリを食い潰す」事故を踏んでいて、その時の対策(常駐サービス5つにMemoryMax=1G、MAX_JOBS=8)は入れていたが、今回はビルドと本番プロセスが対話的に起動されたもので、その対策の対象外だった。教訓: 過去の対策は「次に同じ条件で起きたら防げる」だけで、条件が変われば普通に再発する。
フルビルド、まさかの6連続失敗
WSL復旧後、フルビルドを最初からやり直すことになった。「今度こそ一発で通したい」と思っていたが、結果的に多段階で失敗することになった。
失敗1: メモリ制限を入れたつもりが効いていなかった
まずsystemd-run --user --scope -p MemoryHigh=14G -p MemoryMax=16Gでビルドを囲って再開したが、これが全く効いていなかった。systemctl --user showで確認するとMemoryCurrent=[not set]——スコープ名の指定方法に問題があり、制限が適用されていなかった。
結果、スワップ16GBを使い切り、vmstatのiowaitが70〜86%という激しいスラッシング状態に陥った(waカラムがほぼ張り付いた状態はCPU時間の大半がI/O待ちで無駄になっていることを意味する)。
失敗2: スワップへの退避自体が諸悪の根源だった
MemoryHigh/MemoryMaxだけではOSが「メモリが足りなければスワップに逃がせばいい」と判断してしまい、スワップ自体を使い切ってスラッシングする。MemorySwapMax=0でこのcgroupにスワップを一切使わせないようにし、MAX_JOBSも8→4に下げたところ安定した。スワップ4.7GB/16GBまで回復、iowait 0%を確認。
この一件で学んだのは、「メモリを制限したい」ならMemoryMaxだけでなく**MemorySwapMax=0とセットで初めて機能する**ということ。スラッシングで無限に耐えてしまうより、cgroup内で即座にOOM killされる方が、システム全体を巻き込まない分ずっと安全。
失敗3: CUDA_nvrtc_LIBRARYが見つからない
安定した並列度でビルドを再開したところ、今度は別のエラーで停止した。
CMake Error: The following variables are used in this project, but they are set to NOTFOUND.
CUDA_nvrtc_LIBRARY (ADVANCED)
原因は/usr/local/cudaのシンボリックリンクが、ビルド対象のconda環境とは**全く無関係な別のconda環境(CUDA 12.9)**を指していたこと。ビルド先の環境は自前でCUDA 13.2系のツールチェーン(nvidia-cuda-nvccパッケージ)を持っていたが、CUDA_HOME環境変数を明示していなかったため、PyTorchのビルド設定がシステムのデフォルト(/usr/local/cuda)にフォールバックし、バージョンの合わない方のnvccを掴んでいた。
CUDA_HOMEをビルド対象環境自身のCUDAパッケージのパスに明示的に設定することで解決。同じマシンに複数バージョンのCUDAツールチェーンが共存している環境では、/usr/local/cudaのシンボリックリンクを無条件に信用してはいけない、という教訓。
失敗4: 自分のコマンドミスでシステムpipを呼んでいた
CUDA_HOMEを直すついでに、micromamba run -n <env> env CUDA_HOME=... PATH="$CUDA_HOME/bin:$PATH" pip install ...という形で環境変数を注入しようとしたところ、今度はexternally-managed-environment(Debianのシステムpip保護)エラーが出た。
原因は自分のシェルスクリプトの書き方のミス。env PATH="$CUDA_HOME/bin:$PATH"の$PATHが、micromambaがアクティベートした環境のPATHではなく、外側のホストシェルのPATHとして展開されてしまっていた(コマンドが実行される前に、外側のシェルが変数展開を済ませてしまうため)。結果、envのPATH指定がmicromambaの環境変数を丸ごと上書きし、システムのpipが呼ばれていた。
micromamba run -n <env> bash -c 'export PATH="$CUDA_HOME/bin:$PATH"; pip install ...'のように、変数展開をアクティベート後のシェルの内側で行うように書き直して解決。
失敗5: /tmpがtmpfsで容量不足
CUDA_HOME・PATHの問題が片付いたら、今度はNo space left on device。df -h /tmpを見ると/tmpは**tmpfs(RAM上に確保された12GBの一時ファイルシステム)**だった。
pip installのbuild isolationが、torch(526MB)を含む依存パッケージ一式(合計4.6GB超)を毎回この12GBのtmpfsに再ダウンロード・再インストールしていた。ビルド対象の環境には既に同じ依存関係が入っていたので、この再インストール自体が完全に無駄な作業だった。
--no-build-isolationで重複インストールを止め、さらにTMPDIRを実ディスク(1TB、300GB以上空き)に向けることで解決。tmpfsはRAMを直接消費するので、大きな一時ファイルを扱うビルドとは相性が悪い。
最終的に成功
以上を全部潰した状態(CUDA_HOME明示、MemorySwapMax=0、MAX_JOBS=4、--no-build-isolation、TMPDIRをディスクへ)で再実行し、388オブジェクトのCUDA拡張ビルドが1時間40分で完走した。
「最初からこうしとけばよかった」の中身
振り返ると、最終的にたどり着いた設定は特別なものではない:
systemd-run --user --scope -p MemoryMax=16G -p MemorySwapMax=0 -- \
micromamba run -n sonar-native bash -c '
export CUDA_HOME="/path/to/env/nvidia/cu13"
export PATH="$CUDA_HOME/bin:$PATH"
export TMPDIR=/path/to/disk-backed-tmp
pip install --no-build-isolation -e .
'
これを最初の1回目から使っていれば、5回の失敗(スワップ設定漏れ・CUDA_HOME未設定・PATH展開ミス・tmpfs枯渇)は全部避けられた。ただし裏を返せば、**この設定の必要性は「実際に踏んでみるまで分からなかった」**ものが大半だった。/usr/local/cudaが別環境を指していることも、/tmpがtmpfsで容量制限があることも、事前にドキュメントを読んでも気づける類のものではなく、実機でエラーメッセージを一つずつ潰していく過程で初めて発覚した。
複数のCUDA環境・複数のconda/micromamba環境が同居するマシンでソースビルドをする際は、以下を最初からチェックリストとして持っておくと同じ轍を踏まずに済みそうだ:
-
CUDA_HOMEは必ず明示する。/usr/local/cudaのシンボリックリンク先を無条件に信用しない - メモリ制限は
MemoryMax単体でなくMemorySwapMaxとセットで。スラッシングは制限なしより悪いことがある -
df -h /tmpで/tmpがtmpfsかどうか確認する。RAM上の一時領域に巨大なビルド成果物を置かない - 依存関係が既にインストール済みの環境でビルドし直す時は
--no-build-isolationを検討する - 環境変数の注入は、外側のシェルではなくアクティベート後のシェルの内側で変数展開させる
まとめ
- WSLクラッシュの原因は、ソースビルドと本番モデルサーバーの同時実行によるOOM。過去に導入していた対策は「常駐サービス」向けで、対話的なビルドプロセスは対象外だった
- 復旧後のフルビルドは、メモリ制限漏れ→スワップ設定漏れ→CUDA_HOME誤り→PATH展開ミス→tmpfs枯渇、と5段階の失敗を経てようやく成功した
- 最終的な設定自体はシンプルだが、その必要性のほとんどは実際にエラーを踏むまで分からないものだった。複数CUDA環境が同居するマシンでのソースビルドは、事前チェックリストを用意しておく価値がある