このブログは、株式会社フィックスターズのエンジニアが、あらゆるテーマについて自由に書いているブログです。
2020年9月から11月にかけて開催されたkaggleコンテスト 「Conway’s Reverse Game of Life 2020」 で、筆者 (kota.iizuka) を含むチームが 3位を獲得しました!
この記事では、今回のコンテストの概略を説明したのち、高速化という観点で私が行った手法を紹介します。
Conway’s Reverse Game of Life 2020 は、その名前の通り Conwayのライフゲーム の逆時間発展を計算するコンテストです。Conwayのライフゲームとは、格子上の値が一定のルールに従って動くセルオートマトンのひとつで、簡単なルールにもかかわらず下図のように複雑なパターンが得られることが特徴です。
各セルは「生きている」か「死んでいる」か2通りの状態を持っており(上図だとそれぞれ緑と黒)、生きているセルは周囲(8近傍)に2個か3個のセルがあるときに、死んでいるセルは周囲に3個のセルがあるときに、次の時刻に生きたセルになります。
コンテストの問題設定を大まかに言うと、「25×25の盤面が5万個与えられるので、t (1≦t≦5) ステップ計算したときにその盤面になるようなものを探せ」というものです。評価基準は、提出した盤面をtステップ計算したものと与えられた盤面とのセルごとの正解率、とわかりやすい内容になっています。
ルールから分かる通りこのコンテストは機械学習というよりは最適化の手法が問われています。kaggleといえば機械学習コンテスト、という印象があるかと思いますが、「Santa’s Workshop Tour 2019」など最適化系のコンテストもときどき開催されます。
私の解法は kaggle notebookでも公開 したのですが、焼きなまし法をできる限り高速化するという方法をとりました。実装例はkaggle notebook のほうに書いた ので詳しくはそちらを確認してほしいのですが、今回のように最適化する対象がはっきりしている場合、性能向上のために重要となるのはなるべくたくさん探索すること、今回で言えば盤面の更新や評価を高速化することです。とくに、1か所盤面を変化させたときにtステップ後の盤面がどうなるか、を大量に計算する必要があります。
定義からも分かる通り、1ステップの更新では隣接セルにしか影響を与えないので、tステップの更新後も周囲tセルだけ計算すれば十分と分かります(下図はそれぞれ1ステップで1/4ずつ進むgliderと1/2ずつ進むlightweight spaceship, 1ずつ進むlightspeed signalsというパターンで、いずれも速度は1以下になっています)。
また、状態が2通りしかないので更新をうまく書くことで1bitでセルを管理することができ、これも高速化につながります。詳細はnotebookに記載しましたが、盤面の各行は25bitなので uint32_t
で管理して、25列あるテーブルは std::array<uint32_t, 25>
とすればよいです。
さらに、25×25と盤面が小さいので、これを(手元のpcがryzenなのでavx2を使って)8並列でSIMD化できます(雰囲気としては array<uint256_t, 25>
を32bitずつ区切って8個の盤面だと思って更新するような形)。焼きなまし法の並列化については問題ごとにタスク並列にもできますが、温度並列焼きなまし法というのがあるのを知ったのでそれを使用しました。
これらの速度改善によって更新にかかる時間は以下のようになりました(測定環境は手元のryzen 5 3600, 測定コード全体は未公開)。それぞれの手法で数倍ずつの高速化ができていることがわかります。
手法 | t=1での速度[it/s] | t=1での倍率[倍] | t=5での速度[it/s] | t=5での倍率[倍] |
---|---|---|---|---|
高速化前 | 70648 | 1.0 | 15553 | 1.0 |
局所計算(1) | 4734848 | 67.0 | 159474 | 10.2 |
bit管理(2) | 6110975 | 86.4 | 1357957 | 87.3 |
bit局所更新(1+2) | 36608581 | 518.1 | 3038654 | 195.3 |
SIMD化(1+2+3) | 326743996 | 4624.9 | 43357425 | 2787.7 |
SIMD化(3)について、8並列なので理論値は8倍のはずですが実測値はそれぞれ8.9倍・14.2倍となっています。コンパイル後の命令列を見ると、コンパイラがSIMD化した結果ではレジスタスピルが発生しているほか、手でSIMD化したもの(リンク先の update_partial_x8
)のほうが命令列としても短くなっており、これらが影響したものと考えられます。
このコードをさらにOpenMPで並列化して、PC2台(先ほどのRyzen 5 3600 (6C12T)と Core i7-8500U (4C8T))を使って1問あたり約14分、5万問あわせて25日ほど回し続けました。
このコンテストはスコアが簡単にわかり複数手法のマージが効果的なうえ、3位までしか商品(kaggleのロゴ入りTシャツ)が出ない、という事情があり、上位陣がチームを組んでさらにスコアを上げていく展開になりました。自分も3位以内に入れるかどうかで一喜一憂という毎日でしたが、最終日に markuskarmann さんとチームを組み、188チーム中3位を獲得しました。上位解法として多く使われたのはSAT(充足可能性問題)ソルバを使う方法でしたが、 markskarmann さんは CNN を使用した解法 で興味深かったです。
kaggleで高速化が役に立つやや特異な例として今回の話を紹介しましたが、機械学習コンペにおいても前処理がボトルネックになっていたり、提出時の計算時間制限に間に合う範囲で精度の良いモデルを作る必要があったりして、高速化という切り口で考えることがたくさんあるなあと感じています。みなさんも気軽にkaggleコンペに参加してみてはいかがでしょうか?
ffmpeg -f lavfi -i life=s=60x60:r=5:life_color=00ff00:ratio=.5 -sws_flags neighbor -vf scale=360:360,drawgrid=w=6:h=6 -vframes 100 lifegame.gif
ffmpeg -f lavfi -i life=s=60x60:r=5:life_color=00ff00:ratio=.5:f=spaceships.txt -sws_flags neighbor -vf scale=360:360,drawgrid=w=6:h=6 -vframes 31 spaceships.gif
ffmpeg -f lavfi -i life=s=110x30:r=5:life_color=00ff00:ratio=.5:f=lightspeed_signals.txt -sws_flags neighbor -vf scale=660:180,drawgrid=w=6:h=6 -vframes 62 lightspeed_signals.gif
spaceships.txt
, lightspeed_signals.txt
は空白文字とそれ以外で初期状態のセルを表現したテキストファイル
コンピュータビジョンセミナーvol.2 開催のお知らせ - ニュース一覧 - 株式会社フィックスターズ in Realizing Self-Driving Cars with General-Purpose Processors 日本語版
[…] バージョンアップに伴い、オンラインセミナーを開催します。 本セミナーでは、...
【Docker】NVIDIA SDK Managerでエラー無く環境構築する【Jetson】 | マサキノート in NVIDIA SDK Manager on Dockerで快適なJetsonライフ
[…] 参考:https://proc-cpuinfo.fixstars.com/2019/06/nvidia-sdk-manager-on-docker/ […]...
Windowsカーネルドライバを自作してWinDbgで解析してみる① - かえるのほんだな in Windowsデバイスドライバの基本動作を確認する (1)
[…] 参考:Windowsデバイスドライバの基本動作を確認する (1) - Fixstars Tech Blog /proc/cpuinfo ...
2021年版G検定チートシート | エビワークス in ニューラルネットの共通フォーマット対決! NNEF vs ONNX
[…] ONNX(オニキス):Open Neural Network Exchange formatフレームワーク間のモデル変換ツー...
YOSHIFUJI Naoki in CUDAデバイスメモリもスマートポインタで管理したい
ありがとうございます。別に型にこだわる必要がないので、ユニバーサル参照を受けるよ...