プログラミング
Loongson CPUの失われたアトミックアップデート
The Lost Atomic Update on Loongson CPU (jia.je)
要約
LoongArch CPUで、OpenMPのアトミック命令が稀にアトミック性を失うというCPUの不具合(エラッタ)が発見されました。この問題は、Debianパッケージング中に発見され、AIの助けを借りて最小再現コードが作成され、原因が特定されました。Loongson社は迅速に修正ファームウェアを提供し、まもなくリリースされる予定です。
全文翻訳
CPUアトミック命令1つ、パッケージング無限ループ1つ:LoongArch CPUの失われたアップデートの物語
中文版
TL;DR
2026年2月、Wang MiaoはLoongArchサーバーでDebianのnormalizをパッケージング中に奇妙なことに遭遇しました。数学ソフトウェアの組み込みテストがタイムアウトし、脱出できない無限ループに陥りました。コードを追跡したところ、問題は非常に普通の操作、つまり共有変数へのOpenMPの#pragma omp atomicによる累積であることが判明しました。ループの終了条件は、累積値がある特定の値と等しくなることを要求していましたが、累積結果は常にその値より小さく、無限ループを引き起こしていました。プログラムが大きくコードが複雑だったため、人間が理解できる最小限の例にまで絞り込むことができず、この件は棚上げされました。
半年後の8月、Wang Miaoは再び私のもとへ来て、この問題に取り組みたいと述べました。今回は異なるアプローチを取りました。人間が問題を見つける代わりに、人間がAIの調査を指示する形で、AIに最小再現コードを見つけさせました。約2日後、安定した再現コードが得られ、そこで初めて根本原因が明らかになりました。CPUのアトミック加算命令が、時折アトミック性を失うのです。これにより、私たちは新しいCPUエラッタを発見したことになり、Loongson社がそれを知ってからわずか2週間で、パフォーマンスの低下はほとんどない修正が見つかり、テストファームウェアが提供されました。私たちは、テストファームウェアが問題を解決することを確認し、Loongson社によると、ファームウェアは建国記念日(10月1日)までにリリースされる予定であり、その時点で読者はファームウェアをアップグレードして問題を修正できるようになります。
それでは、最初から最後まで、この物語を語りましょう。
起源
loong13は、LoongArchへのDebian 13 stableのコミュニティメンテナンスポートであり、Wang Miaoはそのメンテナーの一人です。ビルドおよびパッケージングプロセス中に、normalizの組み込みテストがループに陥り、終了できずにパッケージングがタイムアウトすることが発見されました。当時、根本原因をすぐに見つけられなかったため、このパッケージをスキップせざるを得ませんでした。しかし、他のいくつかのパッケージがnormalizに依存しているため、永遠にスキップし続けるわけにはいかず、2月に問題の調査に焦点を当て始めました。
以前、他のパッケージをビルド中に、コードに隠れた競合状態やメモリ順序の問題を発見していましたが、このような問題は、弱いメモリモデルを使用するLoongArchで表面化しやすい傾向があります。そのため、当初は、このソフトウェアにおける同様の問題が原因ではないかと推測しました。しかし、調査が始まると、驚くべきことに、驚きがありました。
最初の調査ラウンド
最初のラウンドはnormalizのソースコードから始まりました。normalizはOpenMPを使用してデータを並列処理します。問題のあるコードスニペットは次のように要約できます。
func (std::list<std::vector<int>> LatticePoints) {
size_t nr_to_match = LatticePoints.size(); // 入力サイズ
size_t nr_points_matched = 0; // 処理済みの点の数
while (true) {
size_t nr_points_done_in_this_round = 0; // このラウンドで処理された点の数
#pragma omp parallel {
auto P = LatticePoints.begin(); // スレッドプライベートなリストポインタ
size_t ppos = 0; // スレッドプライベートなリストポインタ位置
#pragma omp for
for (ppp = 0...nr_to_match) {
if (skip_remaining) {
// 特定の場合、skip_remainingが設定され、未処理の点をスキップします
continue;
}
// pposとpppの違いに基づいて、Pをpppが指す位置に移動させ、pposを維持します
if ((*P)[0] == 0) { // 処理済みであることを意味します
continue;
}
#pragma omp atomic nr_points_matched++;
#pragma omp atomic nr_points_done_in_this_round++;
// Pが指すオブジェクトを処理します
(*P)[0] = 0;
}
}
// このbreakは決して実行されません
if (nr_points_matched == nr_to_match) break;
}
}
このコードの要点は次のとおりです。与えられたLatticePointsリストに対して、プログラムは各点を並列処理します。各データ点を処理中に、一部の点は一時的にスキップされる可能性があり、すべてのデータ点が処理されるまで繰り返しパスが必要になります。このコードでは、nr_to_matchはデータ点の総数、nr_points_matchedは既に処理された点の数、nr_points_done_in_this_roundはこのラウンドで処理された点の数です。ループの終了条件は、nr_points_matchedがnr_to_matchに等しくなること、つまりすべてのデータ点が処理されることです。
無限ループの直接の原因は、nr_points_matchedがnr_to_matchに決して到達しないため、ループが終了できないことです。gdbを使用すると、これが起こるとき、LatticePointsリストのすべての点が処理済みとしてマークされており、nr_points_matchedの増加が停止しているにもかかわらず、ループの終了条件が満たされないため、ループが継続していることがわかります。問題は、なぜカウンターnr_points_matchedの値が、実際に処理されたデータ点の数と一致しないのかということです。コードによると、ラウンドごとのnr_points_matchedのインクリメントは、常に一緒にアトミックにインクリメントされるため、nr_points_done_in_this_roundのインクリメントと等しくなるはずです。しかし、実際の出力はそうではありませんでした。2つのカウンターの値はわずかに異なり、その差は不安定で、結果は実行ごとに変動しました。
最初に除外されたのはメモリ順序の問題でした。このコードは、他の変数を同期するためにアトミック変数に依存していません。つまり、アトミック変数自体のみを操作および読み取り、常にアトミック変数のみを操作および読み取っているため、コードの観点からは論理的に正しいです。
次に疑われたのは、OpenMPの実装が原因かどうかでした。つまり、#pragma omp atomicで注釈付けされたアトミック操作が本当にアトミシティを保証しているかどうかです。ディスアセンブリを見ると、コンパイラが予想通り、これらのアトミック操作のためにLoongArch64のamadd.d命令を生成していることがわかります。これを調査するために、元の2つのカウンターに加えて、対照として2つの追加のstd::atomicカウンターを設定し、結果が一致するかどうかを確認しました。計算された4つのカウンターの値(ラウンドごとのインクリメントから計算)は一致するはずでしたが、実際にはランダムな不一致を示しました。これは、アトミック加算命令が特定の条件下で更新を失うことを示唆しています。
しかし、単純なアトミック加算プログラムでアトミック加算命令のアトミシティをテストしても、失われた更新を再現できませんでした。最小再現コードを見つけるために、前述のnormalizの処理ロジックを同様のテストプログラムに単純化しましたが、これも問題は再現できませんでした。そのため、失われたアトミック加算をトリガーする最小条件を見つけようと、実際に実行されているnormalizコードの計算ステップをコメントアウトし続けました。奇妙な現象は、ほとんどの計算ステップをコメントアウトした後でも、問題が持続したことでした。プログラムが複雑すぎたため、最終的に、失われたアトミック加算を確実に再現する最小コードスニペットを見つけることができませんでした。
2回目の調査ラウンド
6ヶ月後も問題は未解決のままでした。LoongLeak/LoongBleed脆弱性の開示に伴い、normalizにおける失われたアトミック加算が再び私たちの注目を集めました。今回は、調査を支援するためにAIを使用しようとしました。その方法は、まずAIに、上記のnormalizコードに無限ループの問題があることを指摘し、AIにそれを確認して再現させ、その後、可能な原因を見つけさせるというものでした。最初の会話ラウンドでは、AIは問題のあるループに気づきましたが、アトミック加算命令が原因であるという結論には至りませんでした。その後、AIに、問題はLoongArchでのみ発生し、他のアーキテクチャでは発生しないことを示唆しましたが、AIはまだ確実な結論を出すことができませんでした。最終的に、私たちは直接AIに、問題をアトミック加算に絞り込んだという事実を伝え、それを再現して最小再現コードを提供するように依頼しました。その会話ラウンドで、AIは最終的に、最初のラウンドで見落とされていたmemcpy呼び出しに焦点を当てました。memcpyの実装はglibcにあり、glibcは現在利用可能なハードウェア機能に基づいて最適な実装を選択します。