ヒューリスティックコンのパラメータチューニングをGCPでやる

AtCoderヒューリスティックコン(https://atcoder.jp/contests/intro-heuristics)のような短時間のコンペのパラメータチューニングをGCPで行ってみました。
理想のラン環境を求めて - threecourse’s blog のつづきです。
ソースコードは、敵に塩を送りたくない整理できていないので公開せず、考え方のみまとめています。)

概要

  • GCPを使って、並列でOptunaによるパラメータチューニングを行う
  • Google Cloud Run(以下Cloud Run)というサービスを使うと、計算時間の短い計算を比較的簡単にスケーリングしてくれる
    (他のサービスだと、クラスタの構成が面倒だったり、起動時間がかかったりする)

設定と結果

設定は以下のとおりです:

  • Introduction to Heuristics Contest(https://atcoder.jp/contests/intro-heuristics)を問題とする
  • 焼きなまし解法の「終了温度」「開始温度/終了温度」「2点スワップの割合(残りは1点更新)」をチューニング対象のパラメータとする
    (参考:Introduction to Heuristics Contestの解法 https://img.atcoder.jp/intro-heuristics/editorial.pdf
  • パラメータチューニングのために100試行を行う。
  • パラメータの評価に用いるスコアは、10テストケースの平均をとする。
  • 10並列 x 5バッチ(2ケースごとのバッチにする)で並列処理させる

結果は以下のようになりました:

  • 並列なしだと2秒 x 10テストケース x 100試行で2000秒かかるところ、90秒程度で計算完了(なかなか実用的)
  • ベストのパラメータは、(開始温度・終了温度・2点スワップの割合)=(2488, 556, 0.79)となった。 解説を参考に作成した(開始温度・終了温度・2点スワップの割合)=(2000, 600, 0.5)と同程度の結果となった。

雑感:

  • AtCoder環境の方がCloud Runより2倍程度性能が良さそうなので、Cloud Runでの計算時間は2倍程度にすると良さそう。
  • チューニングを繰り返すと微妙なパラメータがベストとなることもあり、「最適なパラメータチューニング」となっているかは分からない。 引き続き要検討だが、試行回数を増やすのが良いような気もする。

構成

全体の流れ

パラメータチューニングの全体の流れは、以下のようになります。

  1. Optunaの初期化を行う
  2. Google Cloud Storage(以下GCS)にテストケースとバイナリをアップロードする
  3. Optunaでの並列パラメータチューニングの実行を開始する
  4. 各パラメータの試行では、
    ・10個のテストケースを実行し、その結果の平均をスコアにする
    ・10個のテストケースの実行は、2ケースごとのバッチにしてCloud Runに計算依頼を飛ばすことで行う
    ・Cloud Runの計算において、GCSからテストケースとバイナリをダウンロードする
    ・10並列で行う(つまり、10並列 x 5バッチ = 50個の計算依頼が飛んでいる状況になる)
  5. パラメータの試行100ケースが全て終わると、最も良いパラメータを選んで終了する

最適化プログラムの構造

パラメータを受け取り、計算結果を返せるように、最適化プログラムを組んでおく必要があります。

  • 引数として「名前」「テストケース名」「各パラメータ」を受け取り、
    結果である「スコア」や「焼きなましの計算回数/更新回数」などの補助情報をjsonファイルなどに出力する仕組みにします。
  • こうすることで、外部からPythonなどを用いて、最適化プログラムを並列で叩いてその結果を取得できるようになります。
  • このまま提出するとWrong Answerになってしまうので、コンパイル定数などを用いて手元でコンパイルしたときと提出したときの挙動を変える必要があります。

Optuna

Optunaは、改めて非常に使いやすいツールですね。

その他、以下の機能が便利でした。

Cloud RunにデプロイするDockerイメージ

Cloud Runは、リクエストを処理している間のみ課金され、また自動的にスケールしてくれるという素晴らしいサービスです。

Cloud Runを使うためには、httpリクエストを受け取り、レスポンスを返すDockerイメージを作成する必要があります。
今回は、flaskでhttpリクエストの処理を行い、その中でC#の最適化プログラムを動かすため、C#Python(Anaconda)を動かせる構成にしています。

Dockerイメージ内の処理は、以下のような流れになります。

  1. httpリクエストのデータとして、「ランの名前」「計算対象となるケース名のリスト」「パラメータ」を受け取る
  2. 「ランの名前」を基にGCSからテストケースとバイナリをダウンロードする
  3. 「計算対象となるケース名のリスト」の各ケースを「パラメータ」を指定して実行し、結果を取得する
  4. 結果のリストをレスポンスとして返す

Cloud Runの設定

最大100までインスタンスを立ち上げ、インスタンス内の同時実行数は1、実行時間制限は(一応)15分、インスタンスはCPU1個、メモリ512MBという設定にしました。

同時実行数を1にすることで、複数の計算で同時にCPUを使うことを防ぎ、正しく計測できるようにしています。 たまになぜか計算回数が半分くらいになることがあるようですが、概ね計測に問題は無さそうです。 もう少し攻めた設定にしても良いかもしれません。

なお、インスタンスがスケーリングしている最中には、429や500といったレスポンスを返すことがあるので、それらのときには少し待って再度リクエストを投げるようにする必要があります。

Cloud Runの料金はそれほどでもないのですが、ログには注意が必要です。 標準出力がログとして飛んでいってしまうので、最適化プログラムで一定間隔ごとに焼きなましの温度などを出力するようにしていると、1回のチューニングで数十GBのログが飛んでいくことがあるようです(怖)。GCPのログ取り込みで除外設定を作成するか、最適化プログラムを叩くときに標準出力・標準エラーをDEVNULLなどで抑止する必要があります。

追記:
threecourse.hatenablog.com