GPU1枚でのfloodgate対局と学習の両立【コンピュータ将棋】

12月24日、来年の世界コンピュータ将棋選手権(WCSC29)の募集が開始されました。 ねね将棋もこの大会に向けて徐々にですが改良を続けています。

ねね将棋はDeep Learningベースの評価関数を用いており、その学習にも対局エンジンの探索部からの評価もGPUを必要とします。 いろいろな評価関数のモデルパラメータを試していますが、1つのモデルを学習するのには数日かかります(強化学習はまだやっていません)。 学習と同時に対局エンジンからもGPUを使おうとすると、GPUが1枚しかないので競合して速度が下がってしまい、正しく強さを測れません。 対局エンジンを動かすたびに手動で学習を一時停止するのは面倒なので、これを自動化してみました。 特に、floodgateのように30分毎に対局が開始し、対局が終わると暇になるような状況だと、対局が開始した際に学習を一時停止、終了した際に再開できるので便利です。

環境はWindowsで、このような用途にはMutexを使うことができます。 Mutexは次のような性質を持ちます。

  • CreateMutexで、指定した名前のMutexを作成するか、すでにあれば開く。
  • OpenMutexで、指定したMutexがすでにあれば開く。存在しなければエラー。
  • CloseHandleで、既に開いてあるMutexを閉じる。これを用いなくても、プロセスが終了すれば自動的に閉じられる。
  • すべてのプロセスがMutexを閉じると、そのMutexは存在しなくなる。

Mutex本来の用途とは少しずれるのですが、二重起動防止などで使われるテクニックに似ています。

これを用いると、次のような処理が書けます。

  • 学習プロセスでは、定期的にOpenMutexを実行し、Mutexが存在するかどうか確認する。存在すれば、存在しなくなるまで待機。
  • 対局エンジンでは、GPUを使い始める時にCreateMutexを実行する。使い終わったらCloseHandleで閉じる。もしクラッシュしても自動で閉じられるので問題ない。

学習プロセス側では、次のコード(python)で、1バッチ進むごとにwait()を呼び出します。もしその際Mutexが存在していればブロックすることにより、学習が一時停止するわけです。

"""
Mutexを用いて、将棋エンジンが動いている間は学習を止めるユーティリティ。
"""
import win32api
import win32con
import win32event
import pywintypes
import time

MUTEX_ALL_ACCESS = win32con.STANDARD_RIGHTS_REQUIRED | win32con.SYNCHRONIZE | win32con.MUTANT_QUERY_STATE


class MutexStopper:
    def __init__(self, mutex_name="NENESHOGI_GPU_LOCK"):
        self.mutex_name = mutex_name

    def wait(self):
        n_wait_seconds = 0
        while self._is_mutex_exist():
            n_wait_seconds += 1
            time.sleep(1)
        return n_wait_seconds

    def _is_mutex_exist(self):
        try:
            hMutex = win32event.OpenMutex(MUTEX_ALL_ACCESS, False, self.mutex_name)
            win32api.CloseHandle(hMutex)
        except:
            return False
        return True

対局エンジン(C++, やねうら王を改造)では、次のように関数を定義しています。

#include "../../extra/all.h"
#include <Windows.h>

// GPUロックタイムアウト
// epochからの秒数がこの値以上の時、ロックを解放する。
static atomic<std::chrono::seconds> gpu_lock_timeout(std::chrono::seconds(0));
static std::thread* gpu_lock_thread = nullptr;

// GPUをロックするスレッド。
// 特定のMutexを作成することで、学習プロセスはそれを察知してGPU利用を一旦停止する。
// タイムアウトでMutexを削除する。
static void gpu_lock_thread_main()
{
    HANDLE hMutex = NULL;
    while (true)
    {
        std::this_thread::sleep_for(std::chrono::seconds(1));
        auto current = chrono::duration_cast<chrono::seconds>(chrono::system_clock::now().time_since_epoch());
        if (hMutex && gpu_lock_timeout.load() < current)
        {
            // Mutex削除
            sync_cout << "info string release gpu lock" << sync_endl;
            CloseHandle(hMutex);
            hMutex = NULL;
        }
        else if (!hMutex && gpu_lock_timeout.load() >= current)
        {
            // Mutex作成
            sync_cout << "info string acquire gpu lock" << sync_endl;
            hMutex = CreateMutex(NULL, FALSE, TEXT("NENESHOGI_GPU_LOCK"));
            if (!hMutex)
            {
                sync_cout << "info string FAILED acquire gpu lock" << sync_endl;
            }
        }
    }

}

// GPUをロックするスレッドを開始する。
void gpu_lock_thread_start()
{
    if (!gpu_lock_thread)
    {
        gpu_lock_thread = new std::thread(gpu_lock_thread_main);
        gpu_lock_thread->detach();//プロセス終了時に自動的に終了させる
    }
}

// GPUのロックタイムアウトを延長する。
void gpu_lock_extend()
{
    sync_cout << "info string extending gpu lock" << sync_endl;
    auto next_timeout = chrono::system_clock::now() + chrono::seconds(60);//1手60秒以上はめったにないのでこれぐらいで
    gpu_lock_timeout.store(chrono::duration_cast<chrono::seconds>(next_timeout.time_since_epoch()));
}

まず対局エンジン開始時にgpu_lock_thread_start()を呼んでおきます。そして、1手ごとの思考開始時にgpu_lock_extend()を呼びます。そうすると、その時点から60秒間Mutexが存在するようになり、学習側が一時停止するという仕掛けになります。USIのgameoverを受信した際にMutexを閉じるという実装でもよいかと思います。

この記事のコードについては、特に権利を主張しませんので適当に組み込んでご利用ください。

こんな感じで、限られたリソースを便利に使う機構を作ると作業がはかどります。棋力には関係ないですが。