AMD GPUで発生するLinuxのサスペンド復帰フリーズを追跡して修正した過程
(nyanpasu64.gitlab.io)- AMD RX 570搭載のLinuxデスクトップで、RAM使用量が高い状態でサスペンドすると、復帰後に黒画面や入力不能状態が繰り返し発生し、単なる画面の問題を超えてカーネルの電源管理フローまで追跡する必要があった
- 根本原因は、S3サスペンド前にVRAMをシステムRAMへバックアップする必要があるamdgpuが、空きRAM不足の状況でディスクswapを適切に利用できず、OOMで破綻する流れにあった
- VRAM evictionを
dpm_suspend()からdpm_prepare()へ前倒しする最初のパッチは、SSDの電源断との競合を減らしたが、pm_restrict_gfp_mask()がすでにswapを無効化しているため、連続メモリ不足は依然として発生し得た - その後、
register_pm_notifier()でPM_SUSPEND_PREPARE時点にamdgpu_device_evict_resources()を呼ぶよう変更すると、swapとディスクが生きている間にVRAMをバックアップでき、テスト環境では高RAM・高VRAM使用量でもサスペンドが成功した - この変更はamdgpuツリーにマージされたものの、デッドロックの可能性のため2025-06にリバートされ、ユーザープロセスを先にfreezeしないサスペンド経路では、3DアプリがVRAMを再びGPUへ引き戻してevictionを妨げ得る
繰り返されたサスペンド失敗と初期の誤診
- デスクトップはWindowsとLinuxのデュアルブート構成で、LinuxでRAM使用量が高いときにサスペンドを試みると、システムが頻繁に壊れた
- 復帰後に黒画面と動くカーソルだけが見える、あるいは画面出力がなくmagic SysRqやハードリセットにしか反応しない
- 一部のケースではKDEロック画面の時計はリアルタイム更新されるが、ログインや操作を試みると停止する
- テスト環境はGigabyte B550M DS3H、AMD RX 570 GPU、Kingston A2000 1TB NVMe SSD、Arch Linux、systemd-boot、Linux 6.4だった
journalctl --system -b -1で前回起動ログを確認すると、一部のサスペンド試行でamdgpu_device_suspend配下のカーネルコードにOOMエラーが現れた- 多くの失敗事例は次のログで終わっており、壊れたシステムが再び起動した記録は残っていなかった
systemd-sleep:Entering sleep state 'suspend'...- kernel:
PM: suspend entry (deep)
- 当初はNVMe APST省電力モードを疑い、
nvme_core.default_ps_max_latency_us=0、iommu=soft、SSDファームウェア更新、2TB起動SSDへの交換を試したが、問題は解決しなかった
失敗位置を絞り込んだデバッグツール群
- systemdが複数のサスペンドモードを連続試行する動作がログノイズやカーネル状態の悪化を招き得るため、
/etc/systemd/sleep.confにSuspendState=memを追加した- デバッグは単純化したが、根本原因はそのままだった
echo 1 > /sys/power/pm_traceでサスペンド失敗位置を追跡したpm_traceはサスペンド・復帰の進行状態をシステム時刻に保存する- 非同期サスペンドが無効化される副作用のおかげで、amdgpuのsuspend失敗後にシステム全体がハングする代わりに回復するパターンも確認できた
systemd.debug_shellカーネルパラメータを追加し、KDEやTTYログインなしでもCtrl-Alt-F9でroot shellを開けるようにした- USBコントローラやキーボードが壊れる場合に備えてPS/2キーボードを使い、その後はシリアルコンソールを構成した
- カーネルパラメータ:
no_console_suspend console=tty0 console=ttyS0,115200 loglevel=8 - ノートPCで
sudo minicom --device /dev/ttyUSB0 --baudrate 115200を使ってログを継続捕捉した - シリアルコンソールはディスプレイとネットワークが壊れた後でもコマンド実行とログ収集を可能にしたが、転送速度や色・画面サイズ処理には制約があった
- カーネルパラメータ:
amdgpuのVRAM evictionとOOMのつながり
- クラッシュログは概ねamdgpuのTTMバッファeviction経路で発生していた
amdgpu_device_evict_resources()amdgpu_ttm_evict_resources()
- 関連バグレポートから、“evict”はVRAMをシステムRAMへコピーしたり、システムRAMをswapへ追い出したりする動作に関係すると分かった
- デスクトップがS3サスペンドに入るとPCIe GPUの電源が切れ、VRAMチップ上のデータは消える
- GPUドライバはサスペンド前に使用中のVRAMをシステムRAMへコピーする必要がある
- 復帰後はRAMに保存したデータを再度復元しなければならない
- Linuxのamdgpuドライバには、使用中VRAM全体を収める空きRAMがないとき、システムRAMをディスクベースのswapへ逃がす代わりに、メモリ不足でクラッシュする問題があった
- Linuxがsuspend中にOOMへ遭遇すると、サスペンドを中止してデバイスを再起動しようとするが、すでに中断されたサスペンドのため一部ドライバが壊れたり、suspend/resume中に再度OOMが起きたりする
- 非同期サスペンドが有効だと危険性はさらに高い
- サスペンド自体は成功しても、resume中のデバイス起動処理でOOMが発生することもある
最初の解決の試み: VRAMバックアップ時点の前倒し
- Mario Limoncielloは
/sys/power/pm_print_timesと/sys/power/pm_debug_messagesを有効にしてサスペンドログを確認するよう提案した - ログでは、NVMeとamdgpuドライバがサスペンド中に
pci_pm_suspendへ並列に入っていることが分かった - 当初はGPUのsuspendをSSDのsuspendより先にする方法を探し、Linuxのdevice suspend orderingメカニズムも調べた
- このメカニズムは、すべてのGPUをすべてのシステムディスクより先にsuspendする用途というより、密接に接続した周辺機器の同期に近いように見えた
- MarioはLinux suspendの
prepare段階でVRAMをevictする方式を提案し、VRAM evictionをdpm_suspend()からdpm_prepare()へ移すカーネルパッチを書いた - 変更前後の流れは次の通り
- 従来:
dpm_suspend()時点でVRAMバックアップが実行され、SSDの電源断の流れと重なり得る - 変更後:
dpm_prepare()で先にVRAMバックアップを試み、失敗したら他デバイスのsuspend前にサスペンドを中止する
- 従来:
- この変更は以前より改善したが、
pm_restrict_gfp_mask()がdpm_prepare()より前にswapを無効化するため、高RAM使用量では依然として失敗したamdgpu_ttm_evict_resources()は連続メモリ不足で失敗し得る- VRAMをRAMへすべて入れられても、その後のドライバ割り当てを処理する余裕が足りなければ、sleepまたはwake中にOOMが起きる可能性がある
Ghidraで見つけた別のamdgpuクラッシュ
- テスト中に
BUG: unable to handle page fault for address: fffffffffffffffcエラーが発生し、ほぼnullに近いポインタ参照のように見えた - クラッシュログは
dm_resume+0x200の位置を指していたが、ソースコードの行番号は示されていなかった amdgpu.koカーネルモジュールを保存・抽出したうえでGhidraで逆コンパイルし、dm_resume内のクラッシュ位置をカーネルソース行へ対応付けた- 問題は
for_each_new_crtc_in_state(dm->cached_state, crtc, new_crtc_state, i)マクロで発生していたdm->cached_stateは有効なポインタではなくfffffffffffffff4だった- その後
[RSI + 0x8]フィールドを読もうとしてfffffffffffffffcアドレスでpage faultが発生した
- 原因は、
dm_suspend()がdrm_atomic_helper_suspend()の戻り値をそのままポインタへ格納していた流れにあったdrm_atomic_helper_suspend()は有効ポインタまたはERR_PTR(err)を返し得る- OOMにより
-ENOMEM(-12)が返され、amdgpuのsuspendコードがそれをポインタとして参照したと判断された
- Marioは失敗戻り値を検査してsuspendを中止するコードを追加し、この問題を修正した
断念した方向: prepare()中にswapを許可する
- 高RAM使用量でサスペンド失敗が続いた理由は、amdgpuが
dpm_prepare()でVRAMをバックアップしても、この時点ではすでにpm_restrict_gfp_mask()がディスクswapを無効化しているためだった - ひとつの試みは、
dpm_prepare()ではswapを許可し、dpm_suspend()がディスクを止める直前にswapを無効化する構成だった - そのために、
pm_restrict_gfp_mask()呼び出しをenter_state()からより深いdpm_suspend_start()内へ移す実験を行った - 実務上の制約は大きかった
pm_restrict_gfp_mask()はkernel/power/power.hで宣言され、kernel/power/suspend.cで呼ばれている- 呼び出したかった場所は
drivers/base/power/main.cで、このファイルは通常kernel/power/ヘッダをインクルードしない - 一時的なハックとして
#include <../kernel/power/power.h>を追加したが、upstreamへ持ち込むには説得が必要な構造だった
- ハイブリッドサスペンドでも正しさの問題があった
- ハイブリッドサスペンドはシステムイメージを保存し、
pm_restrict_gfp_mask()を呼んだ後、swapが無効化された状態でsuspend_devices_and_enter()を呼ぶ - 同じ関数を再度呼ぶと警告が発生し、
pm_restore_gfp_mask()がswapを再有効化できない可能性がある
- ハイブリッドサスペンドはシステムイメージを保存し、
- テストでは失敗やクラッシュの頻度は減ったが完全には解決せず、core power managementに手を入れる脆い変更だったためupstream化は試みなかった
ユーザースペースの回避策: amdgpu-sleep
- 2024-10にSuperTuxKart終了後サスペンドし、復帰時に黒画面が発生した
- ログ上では最初のサスペンド試行が
dpm_prepare()でOOMになり、次の試行はresume中のamdgpuのbw_calcs()メモリ割り当てクラッシュへつながった
- ログ上では最初のサスペンド試行が
- NVIDIAのユーザースペースVRAMバックアップ方式を参考にした
- NVIDIAはsystemdがkernel suspendを呼ぶ前後に実行されるサービスから
/proc/driver/nvidia/suspendへ書き込み、VRAMバックアップ・復元を行う
- NVIDIAはsystemdがkernel suspendを呼ぶ前後に実行されるサービスから
- これをもとにArch Linux向けのamdgpu-sleepパッケージを作成した
- サスペンド前に
/sys/kernel/debug/dri/1/amdgpu_evict_vramを読み出す - このdebugエンドポイントはamdgpuに全VRAMをシステムRAMへ保存するよう指示する
- systemdはGPUのVRAM eviction完了まで待ってからkernel suspendを開始する
- サスペンド前に
- デスクトップでサスペンドすると、VRAMを素早くメモリへコピーし、必要ならRAMをswapへ押し出して成功した
- 複数の3Dアプリが動作中だと、アプリが継続してフレームを描画し、VRAMを再びGPUへ引き戻して
amdgpu_evict_vramと綱引きのようなライブロックが発生した- この状態は70秒以上続いた後、
amdgpu_evict_vramが断念した - その後systemdがkernel suspendを開始してuserspaceをfreezeすると、kernel段階ではVRAM evictionが成功した
- この状態は70秒以上続いた後、
- このスクリプトは、ライブロック発生頻度よりも、スクリプトを無効にしたときのkernelレベルクラッシュ発生頻度のほうが低くなかったため、使い続けられた
最終パッチ: power management notifier
- 2024-11、Marioはswapがまだ有効な間にevictionを許可するパッチのテストを依頼した
- パッチは
register_pm_notifier()を呼ぶ構造で、Linuxのpower management notifier APIを使っていた - コールバックは
PM_HIBERNATION_PREPAREとPM_SUSPEND_PREPAREメッセージを受け取り、amdgpu_device_evict_resources()を呼ぶ PM_SUSPEND_PREPAREはenter_state() → suspend_prepare()でpm_notifier_call_chain_robust(PM_SUSPEND_PREPARE, PM_POST_SUSPEND)を通じて発行される- notifierコールバックを持つドライバへ
PM_SUSPEND_PREPAREが渡される - 失敗が起きると、すでに準備済みのドライバへ
PM_POST_SUSPENDが渡され、サスペンドは中止される
- notifierコールバックを持つドライバへ
- この位置でVRAMをevictすると、
pm_restrict_gfp_mask()がswapを無効化する前であり、ディスクもまだfreezeされていない - 修正後のフローは次の通り
suspend_prepare()PM_SUSPEND_PREPAREamdgpu_device_pm_notifier() → amdgpu_device_evict_resources()pm_restrict_gfp_mask()dpm_prepare()dpm_suspend()
- 修正済みamdgpuドライバを含むカスタムカーネルをビルドしてテストした結果、高RAM・高VRAM使用量で何度サスペンドしてもエラーは発生しなかった
- ただし、amdgpuがPipeWireやkernelがスピーカーをミュートする前にVRAMをバックアップするため、数秒間オーディオがループ再生される現象が見られた
- 数回のコードレビューを経て、この変更はamdgpuツリーへマージされ、1年以上にわたる試行の末、このバグを解決する段階へ進んだ
2025-06アップデートとリバート
- 当初、この変更はstable Linux kernel 6.14に含まれると予想されていたが、その後デッドロックの可能性によりリバートされた
- 新たに持ち込まれた問題は、systemdがすべてのユーザースペースプロセスを先にfreezeせずにsystem suspendを呼ぶ場合に発生する
- kernelが自前でプロセスをfreezeする前にVRAMをシステムRAMへevictしようとする
- 同時に3DプログラムがVRAMを再びGPUへ戻すと、サスペンド時のeviction処理がデッドロックする可能性がある
- これはユーザースペースの
amdgpu_evict_vram回避策で経験したライブロックに似ている - Linux PMメンテナは、
pm_restrict_gfp_mask()呼び出しをsuspend_devices_and_enter()の内側へ移す案を提案した- これは先に試していた
prepare()中にswapを許可する方向とつながっている
- これは先に試していた
- 自分で実装・投稿する予定はない
- AMD GPUを、より遅いCPUと8GBメモリを持つ古いPCへ移したため、その環境でカーネルビルドを回したくないからだ
- メインPCはIntel Arc B570へアップグレードしたが、10GB VRAMでも同種の問題が発生した
- この問題はdrm/xe kernel bug trackerへ報告された
1件のコメント
Hacker Newsのコメント
デスクトップがS3スリープに入るとPCIe GPUの電源が切れる、という前提は確実ではない
S3はRAMを除くすべての電源を切るのが本来だが、たとえばGigabyte AorusのマザーボードはNVMe SSDのスリープバグで、システムが正常にスリープしたり復帰したりできないことで悪名高い
たいていは、すべてのPCIeポートのウェイクアップを無効にするudevルールで回避できる:
ACTION=="offline", SUBSYSTEM=="pci", DRIVER=="pcieport", ATTR{power/wakeup}="disabled"さらに絞るなら、問題のあるPCIeポートだけを
ATTR{vendor}=="0x8086", ATTR{device}=="0x43bc"のように指定でき、/proc/acpi/wakeup、/sys/class/pci_bus/*/*/yourWakeupDevicePci/uevent | grep PCI_ID、udevadm info --attribute-walk /dev/whateverで原因を探せるudevの代わりにsystemdサービスや自動化スクリプトで
/proc/acpi/wakeupをトグルすることもできるが、安定性は低く、こうしたLinuxのスリープ問題には本当にうんざりする/etc/udev/rules.d/にACTION=="add", KERNELS=="0000:00:01.1", ATTR{power/wakeup}="disabled"を入れて自発的なウェイクアップを防ぐ必要があったLogitech Boltレシーバーも複数のLinuxマシンを即座に起こしてしまうのに、Windowsではそうならない理由が分からないし、USBキャプチャを試すにはロジックアナライザやGlasgowのような機材が必要なのかも微妙
当面は
ACTION=="add", SUBSYSTEM=="usb", DRIVERS=="usb", ATTRS{idVendor}=="046d", ATTRS{idProduct}=="c548", ATTR{power/wakeup}="disabled"というルールを追加して防いでいるecho GPP0 >> /proc/acpi/wakeupを実行すると解決したただし起動後最初のスリープは必ず即座に復帰していたが、上のudevルールを適用したところ、その問題も解決したようだ
PCIeデバイスにスリープ命令を送った後でバス自体をスリープさせ、復帰時にはまずバスを戻してからデバイスを起こす、という形が可能であるべきだと思う
私の場合、ウェイクアップの原因は
.../devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A08:00/device:45/wakeup/wakeup6から来ているようだが、このパスが何を意味するのかよく分からない言及されているユーザー空間での回避策の一つであるmemreserverの作者として、数年前にこの問題をデバッグした
すぐ見つかる公開コメントは https://gitlab.freedesktop.org/drm/amd/-/issues/2125#note_17... で、メーリングリストでの議論もあったと記憶している
要点は、Linuxにはディスクとメモリサブシステムの一部が凍結される前に安定して実行される段階的なsuspendフックがなかったことだったが、今では可能になっているようだ
残念ながらFreedesktop Gitlabはインデックスされにくいようで、この知見は埋もれてしまったようだ
本当に素晴らしい仕事
Linuxでスリープへの移行をきちんと動かすのがなぜ難しく、デバッグも難しいのか気になっていたなら、この記事だけでも壊れ得る箇所がどれほど多いかがよく分かる
今でもThinkPad P1G4では、スリープ前にファンを手動で止めないと自動で止まらず、最近はスリープ復帰後にBluetoothヘッドフォンでノイズが出るようになり、PipeWireのノードスリープも無効にする必要があった: https://wiki.archlinux.org/title/PipeWire#Noticeable_audio_d...
初めてこういう問題に遭遇したのは、たぶん15年ほど前だった
さまざまな問題に対して提案される回避策の中には省電力モードをオフにするものがあるが、スリープを使う目的は普通バッテリー駆動時間を延ばすことなので、この方法は実使用時間が大きく短くなりかねず、あまり現実的ではない
それでもS0ixスリープを動かすのが不可能というわけではない
AMD 7840UとAMD 8840Uベースの携帯型デバイス、つまりGPD Win Max 2、GPD Win Mini、GPD Win 4、Minisforum V3、OneXPlayer X1 RyzenにArch Linuxをインストールしたが、これらの会社がLinuxサポートを念頭に置いて設計したりテストしたりした可能性は低そうに見える
ところが、
/proc/acpi/wakeupと/sys/devices/*/*/*/power/wakeupで偽のウェイクアップソースを少し調整する程度で、最新のOneXPlayer X1 Ryzenを除けば、ほぼ完璧なS0ixサポートが得られた標準のLinuxカーネルサポートも優秀で、タッチスクリーン、ペン入力、Wi-Fi、Bluetoothはうまく動き、見かけた唯一の穴は指紋リーダーのサポートだった
こうした小規模メーカーは部品の極端なカスタマイズや緻密な統合が少なく、実際にデバイスが数mmずつ厚い形で表れている
そのため、より保守的な部品選定になり、その結果Linuxサポートが意外に良いのかもしれない
自分の経験では、ビジネス向けThinkPadではかなり前から非常に安定しており、同じモデルのWindowsユーザーのほうがむしろスリープ問題に頻繁に遭っているように見える
個人的に本当にありがたい
メインのノートPCがLinuxを動かしているRyzenベースのThinkPadで、スリープとハイバネーションをよく使うのだが、この問題には断続的に遭遇してきた
Linux 6.14が楽しみ
dm->cached_stateがポインタではなく-12を保存していた理由は、おそらくsuspend中にdm_suspend()がdm.cached_state = drm_atomic_helper_suspend(adev_to_drm(adev))をそのまま代入したためである可能性が高い呼び出された
drm_atomic_helper_suspend()は、有効なポインタか、エラーを負のポインタとしてエンコードしたERR_PTR(err)を返すことがあり、呼び出し側がエラーを検査せずにそのままポインタへ入れ、resume時に逆参照したということカーネルにRustを入れるべき理由がまた一つ増えたし、
Result型の処理が強制されれば、こういうことは起きにくいしかしデフォルトが重要で、カーネルがコーディング慣行の近代化を長く先送りしてきた歴史は、C側の改善には不利に働く
皮肉なことに、同じ抵抗のせいでRust開発者たちも挫折している。各サブシステムを整理したり、動作方式を文書として残したりすることすら、なかなか受け入れられないからだ
https://github.com/llvm/llvm-project/issues/74205のようなものがカーネルまで降りてくれば助けになるかもしれないが、それでも型で安全性を確保するより、ポインタを手動でオーバーロードする方法を選び続けそうだ
GPU拡張モジュールを付けたFramework AMDノートPCでLinux/Windowsのデュアルブートを使っているので、この作業は役に立ちそう
直接支援するか、希望する慈善団体に寄付したい。連絡先はプロフィールにある
命名、キャッシュ無効化、off-by-oneエラーがコンピュータサイエンスにおける最大の2つの問題だと思っていたが、sleep/wake問題を知ってからは、これはNP完全に見える
すべての周辺機器が状態を持たないなら、おそらく問題ではなかったはず
Linuxにおけるメモリ管理、特にOOM状況は、いまだに信じがたいほど苦痛な悪夢である
こうした問題を常に経験しているわけではないが、似たような問題をデバッグしようとして失敗したことは確かにあり、結局OOMになるとたいていRAMを増設することになる
無駄で高くつくが、OOM状況を優雅に処理することは、今後もLinuxにとって解決の難しい問題であり続けそうだ
今回の作業は素晴らしく、今後似たような問題をデバッグするときの基準点になるだろう
systemdのdebug-shell機能もありがたいし、そんな機能があるとは知らなかった
ただ、自分のX670E Steel Legendマザーボードにはシリアルヘッダーがないようなのだが、最近のオンボードシリアルポートはどう動作しているのか気になる
チップセットのPCIeレーンにぶら下がっているのだろうか
Linuxカーネルを深く掘るときは、FOSDEMやLinux Plumbers Conferenceのような場でのカーネルサブシステム発表の録画が大いに役立つ。たとえば、ほとんどのデスクトップGPU DRMドライバーが使うTTMメモリサブシステムの動画はこちら: https://www.youtube.com/watch?v=MG7_tUNKSt0
DOS万歳
TTMの動画は時間があるときに見るつもり
Linuxがやっていることより優れた最新のOOM処理方式があるのかはよく分からないので、関連して読む価値のある資料があれば知りたい
LinuxはOOM状況をまともに処理できない
cgroupsでガードレールを設けられるし、earlyoomをインストールすることもできるし、swapを増やしたりzramを使ったりできることは知っている
だが結局どれも、たまに一度救ってくれるかもしれない汚いハックにすぎず、こうした状況の処理のされ方そのものは直せない
こういうものを解決策として提示しないでほしい
カーネルがdm_cryptでメモリを割り当てられず、LUKSボリュームが自分で読み取り専用としてマウントされるのを見たことがあるが、頼むからユーザー空間プロセスを1つ殺してくれればいい
現状は到底受け入れられないし、言い訳にも疲れた
zstdを使えば、8GB RAMを大きな問題なく20GBの「RAM」のように、16GBを40GBのように使える
さらに踏み込めばメモリを100%超でオーバーコミットすることもできるし、Androidもこうしているので、かなり安定した方式だ
朗報だ
AMDのLinuxグラフィックドライバーはおおむねよく動作していたが、この問題だけは何度も遭遇した例外だった
最近遭遇している問題は、サスペンドから復帰した後、ドライバーが
WARNING: CPU: 12 PID: 11871 at drivers/gpu/drm/amd/amdgpu/../display/dc/dc_helper.c:100 generic_reg_update_ex+0x1d2/0x290 [amdgpu]の後に"[drm] scheduler comp_1.0.n is not ready, skipping"をログへ延々と吐き続けるというものだhttps://gitlab.freedesktop.org/drm/amd/-/issues/3911
ただしノートPCなのでドライバー構成はかなり違い、PCIe GPUもない
amdgpu.koカーネルモジュールを保存して取り出し、Ghidraでデコンパイルしたうえで、dm_resumeのクラッシュ位置をカーネルソースの該当行にマッピングしたという部分は、デバッグの中でいつも一番好きな場面だ