Merged the training data generator and the machine learning logic from YaneuraOu.

This commit is contained in:
Hisayori Noda
2019-06-18 08:48:05 +09:00
parent 87445881ec
commit bcd6985871
37 changed files with 6306 additions and 139 deletions

View File

@@ -0,0 +1 @@
// just a place holder

133
src/learn/half_float.h Normal file
View File

@@ -0,0 +1,133 @@
#ifndef __HALF_FLOAT_H__
#define __HALF_FLOAT_H__
// Half Float Library by yaneurao
// (16-bit float)
// 16bit型による浮動小数点演算
// コンパイラの生成するfloat型のコードがIEEE 754の形式であると仮定して、それを利用する。
#include "../types.h"
namespace HalfFloat
{
// IEEE 754 float 32 format is :
// sign(1bit) + exponent(8bits) + fraction(23bits) = 32bits
//
// Our float16 format is :
// sign(1bit) + exponent(5bits) + fraction(10bits) = 16bits
union float32_converter
{
int32_t n;
float f;
};
// 16-bit float
struct float16
{
// --- constructors
float16() {}
float16(int16_t n) { from_float((float)n); }
float16(int32_t n) { from_float((float)n); }
float16(float n) { from_float(n); }
float16(double n) { from_float((float)n); }
// build from a float
void from_float(float f) { *this = to_float16(f); }
// --- implicit converters
operator int32_t() const { return (int32_t)to_float(*this); }
operator float() const { return to_float(*this); }
operator double() const { return double(to_float(*this)); }
// --- operators
float16 operator += (float16 rhs) { from_float(to_float(*this) + to_float(rhs)); return *this; }
float16 operator -= (float16 rhs) { from_float(to_float(*this) - to_float(rhs)); return *this; }
float16 operator *= (float16 rhs) { from_float(to_float(*this) * to_float(rhs)); return *this; }
float16 operator /= (float16 rhs) { from_float(to_float(*this) / to_float(rhs)); return *this; }
float16 operator + (float16 rhs) const { return float16(*this) += rhs; }
float16 operator - (float16 rhs) const { return float16(*this) -= rhs; }
float16 operator * (float16 rhs) const { return float16(*this) *= rhs; }
float16 operator / (float16 rhs) const { return float16(*this) /= rhs; }
float16 operator - () const { return float16(-to_float(*this)); }
bool operator == (float16 rhs) const { return this->v_ == rhs.v_; }
bool operator != (float16 rhs) const { return !(*this == rhs); }
static void UnitTest() { unit_test(); }
private:
// --- entity
uint16_t v_;
// --- conversion between float and float16
static float16 to_float16(float f)
{
float32_converter c;
c.f = f;
u32 n = c.n;
// The sign bit is MSB in common.
uint16_t sign_bit = (n >> 16) & 0x8000;
// The exponent of IEEE 754's float 32 is biased +127 , so we change this bias into +15 and limited to 5-bit.
uint16_t exponent = (((n >> 23) - 127 + 15) & 0x1f) << 10;
// The fraction is limited to 10-bit.
uint16_t fraction = (n >> (23-10)) & 0x3ff;
float16 f_;
f_.v_ = sign_bit | exponent | fraction;
return f_;
}
static float to_float(float16 v)
{
u32 sign_bit = (v.v_ & 0x8000) << 16;
u32 exponent = ((((v.v_ >> 10) & 0x1f) - 15 + 127) & 0xff) << 23;
u32 fraction = (v.v_ & 0x3ff) << (23 - 10);
float32_converter c;
c.n = sign_bit | exponent | fraction;
return c.f;
}
// unit testになってないが、一応計算が出来ることは確かめた。コードはあとでなおす(かも)。
static void unit_test()
{
float16 a, b, c, d;
a = 1;
std::cout << (float)a << std::endl;
b = -118.625;
std::cout << (float)b << std::endl;
c = 2.5;
std::cout << (float)c << std::endl;
d = a + c;
std::cout << (float)d << std::endl;
c *= 1.5;
std::cout << (float)c << std::endl;
b /= 3;
std::cout << (float)b << std::endl;
float f1 = 1.5;
a += f1;
std::cout << (float)a << std::endl;
a += f1 * (float)a;
std::cout << (float)a << std::endl;
}
};
}
#endif // __HALF_FLOAT_H__

237
src/learn/learn.h Normal file
View File

@@ -0,0 +1,237 @@
#ifndef _LEARN_H_
#define _LEARN_H_
#if defined(EVAL_LEARN)
#include <vector>
// =====================
// 学習時の設定
// =====================
// 以下のいずれかを選択すれば、そのあとの細々したものは自動的に選択される。
// いずれも選択しない場合は、そのあとの細々したものをひとつひとつ設定する必要がある。
// elmo方式での学習設定。これをデフォルト設定とする。
// 標準の雑巾絞りにするためにはlearnコマンドで "lambda 1"を指定してやれば良い。
#define LEARN_ELMO_METHOD
// ----------------------
// 更新式
// ----------------------
// AdaGrad。これが安定しているのでお勧め。
// #define ADA_GRAD_UPDATE
// 勾配の符号だけ見るSGD。省メモリで済むが精度は…。
// #define SGD_UPDATE
// ----------------------
// 学習時の設定
// ----------------------
// mini-batchサイズ。
// この数だけの局面をまとめて勾配を計算する。
// 小さくするとupdate_weights()の回数が増えるので収束が速くなる。勾配が不正確になる。
// 大きくするとupdate_weights()の回数が減るので収束が遅くなる。勾配は正確に出るようになる。
// 多くの場合において、この値を変更する必要はないと思う。
#define LEARN_MINI_BATCH_SIZE (1000 * 1000 * 1)
// ファイルから1回に読み込む局面数。これだけ読み込んだあとshuffleする。
// ある程度大きいほうが良いが、この数×40byte×3倍ぐらいのメモリを消費する。10M局面なら400MB*3程度消費する。
// THREAD_BUFFER_SIZE(=10000)の倍数にすること。
#define LEARN_SFEN_READ_SIZE (1000 * 1000 * 10)
// 学習時の評価関数の保存間隔。この局面数だけ学習させるごとに保存。
// 当然ながら、保存間隔を長くしたほうが学習時間は短くなる。
// フォルダ名は 0/ , 1/ , 2/ ...のように保存ごとにインクリメントされていく。
// デフォルトでは10億局面に1回。
#define LEARN_EVAL_SAVE_INTERVAL (1000000000ULL)
// ----------------------
// 目的関数の選択
// ----------------------
// 目的関数が勝率の差の二乗和
// 詳しい説明は、learner.cppを見ること。
//#define LOSS_FUNCTION_IS_WINNING_PERCENTAGE
// 目的関数が交差エントロピー
// 詳しい説明は、learner.cppを見ること。
// いわゆる、普通の「雑巾絞り」
//#define LOSS_FUNCTION_IS_CROSS_ENTOROPY
// 目的関数が交差エントロピーだが、勝率の関数を通さない版
// #define LOSS_FUNCTION_IS_CROSS_ENTOROPY_FOR_VALUE
// elmo(WCSC27)の方式
// #define LOSS_FUNCTION_IS_ELMO_METHOD
// ※ 他、色々追加するかも。
// ----------------------
// 学習に関するデバッグ設定
// ----------------------
// 学習時のrmseの出力をこの回数に1回に減らす。
// rmseの計算は1スレッドで行なうためそこそこ時間をとられるので出力を減らすと効果がある。
#define LEARN_RMSE_OUTPUT_INTERVAL 1
// ----------------------
// ゼロベクトルからの学習
// ----------------------
// 評価関数パラメーターをゼロベクトルから学習を開始する。
// ゼロ初期化して棋譜生成してゼロベクトルから学習させて、
// 棋譜生成→学習を繰り返すとプロの棋譜に依らないパラメーターが得られる。(かも)
// (すごく時間かかる)
//#define RESET_TO_ZERO_VECTOR
// ----------------------
// 学習のときの浮動小数
// ----------------------
// これをdoubleにしたほうが計算精度は上がるが、重み配列絡みのメモリが倍必要になる。
// 現状、ここをfloatにした場合、評価関数ファイルに対して、重み配列はその4.5倍のサイズ。(KPPTで4.5GB程度)
// double型にしても収束の仕方にほとんど差異がなかったのでfloatに固定する。
// floatを使う場合
typedef float LearnFloatType;
// doubleを使う場合
//typedef double LearnFloatType;
// float16を使う場合
//#include "half_float.h"
//typedef HalfFloat::float16 LearnFloatType;
// ----------------------
// 省メモリ化
// ----------------------
// Weight配列(のうちのKPP)に三角配列を用いて省メモリ化する。
// これを用いると、学習用の重み配列は評価関数ファイルの3倍程度で済むようになる。
#define USE_TRIANGLE_WEIGHT_ARRAY
// ----------------------
// 次元下げ
// ----------------------
// ミラー(左右対称性)、インバース(先後対称性)に関して次元下げを行なう。
// デフォルトではすべてオン。
// KKに対してミラー、インバースを利用した次元下げを行なう。(効果のほどは不明)
// USE_KK_INVERSE_WRITEをオンにするときはUSE_KK_MIRROR_WRITEもオンでなければならない。
#define USE_KK_MIRROR_WRITE
#define USE_KK_INVERSE_WRITE
// KKPに対してミラー、インバースを利用した次元下げを行なう。(インバースのほうは効果のほどは不明)
// USE_KKP_INVERSE_WRITEをオンにするときは、USE_KKP_MIRROR_WRITEもオンになっていなければならない。
#define USE_KKP_MIRROR_WRITE
#define USE_KKP_INVERSE_WRITE
// KPPに対してミラーを利用した次元下げを行なう。(これをオフにすると教師局面が倍ぐらい必要になる)
// KPPにはインバースはない。(先手側のKしかないので)
#define USE_KPP_MIRROR_WRITE
// KPPPに対してミラーを利用した次元下げを行なう。(これをオフにすると教師局面が倍ぐらい必要になる)
// KPPPにもインバースはない。(先手側のKしかないので)
#define USE_KPPP_MIRROR_WRITE
// KKPP成分に対して学習時にKPPによる次元下げを行なう。
// 学習、めっちゃ遅くなる。
// 未デバッグなので使わないこと。
//#define USE_KKPP_LOWER_DIM
// ======================
// 教師局面生成時の設定
// ======================
// ----------------------
// 引き分けを書き出す
// ----------------------
// 引き分けに至ったとき、それを教師局面として書き出す
// これをするほうが良いかどうかは微妙。
// #define LEARN_GENSFEN_USE_DRAW_RESULT
// ======================
// configure
// ======================
// ----------------------
// elmo(WCSC27)の方法での学習
// ----------------------
#if defined( LEARN_ELMO_METHOD )
#define LOSS_FUNCTION_IS_ELMO_METHOD
#define ADA_GRAD_UPDATE
#endif
// ----------------------
// Learnerで用いるstructの定義
// ----------------------
#include "../position.h"
namespace Learner
{
// PackedSfenと評価値が一体化した構造体
// オプションごとに書き出す内容が異なると教師棋譜を再利用するときに困るので
// とりあえず、以下のメンバーはオプションによらずすべて書き出しておく。
struct PackedSfenValue
{
// 局面
PackedSfen sfen;
// Learner::search()から返ってきた評価値
int16_t score;
// PVの初手
// 教師との指し手一致率を求めるときなどに用いる
uint16_t move;
// 初期局面からの局面の手数。
uint16_t gamePly;
// この局面の手番側が、ゲームを最終的に勝っているなら1。負けているなら-1。
// 引き分けに至った場合は、0。
// 引き分けは、教師局面生成コマンドgensfenにおいて、
// LEARN_GENSFEN_DRAW_RESULTが有効なときにだけ書き出す。
int8_t game_result;
// 教師局面を書き出したファイルを他の人とやりとりするときに
// この構造体サイズが不定だと困るため、paddingしてどの環境でも必ず40bytesになるようにしておく。
uint8_t padding;
// 32 + 2 + 2 + 2 + 1 + 1 = 40bytes
};
// 読み筋とそのときの評価値を返す型
// Learner::search() , Learner::qsearch()で用いる。
typedef std::pair<Value, std::vector<Move> > ValueAndPV;
// いまのところ、やねうら王2018 Otafukuしか、このスタブを持っていないが
// EVAL_LEARNをdefineするなら、このスタブが必須。
extern Learner::ValueAndPV search(Position& pos, int depth , size_t multiPV = 1 , uint64_t NodesLimit = 0);
extern Learner::ValueAndPV qsearch(Position& pos);
double calc_grad(Value shallow, const PackedSfenValue& psv);
}
#endif
#endif // ifndef _LEARN_H_

2922
src/learn/learner.cpp Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,256 @@
#include "learning_tools.h"
#if defined (EVAL_LEARN)
#if defined(_OPENMP)
#include <omp.h>
#endif
#include "../misc.h"
using namespace Eval;
namespace EvalLearningTools
{
// --- static variables
double Weight::eta;
double Weight::eta1;
double Weight::eta2;
double Weight::eta3;
uint64_t Weight::eta1_epoch;
uint64_t Weight::eta2_epoch;
std::vector<bool> min_index_flag;
// --- 個別のテーブルごとの初期化
void init_min_index_flag()
{
// mir_piece、inv_pieceの初期化が終わっていなければならない。
assert(mir_piece(Eval::f_pawn) == Eval::e_pawn);
// 次元下げ用フラグ配列の初期化
// KPPPに関しては関与しない。
KK g_kk;
g_kk.set(SQUARE_NB, Eval::fe_end, 0);
KKP g_kkp;
g_kkp.set(SQUARE_NB, Eval::fe_end, g_kk.max_index());
KPP g_kpp;
g_kpp.set(SQUARE_NB, Eval::fe_end, g_kkp.max_index());
uint64_t size = g_kpp.max_index();
min_index_flag.resize(size);
#pragma omp parallel
{
#if defined(_OPENMP)
// Windows環境下でCPUがつあるときに、論理64コアまでしか使用されないのを防ぐために
// ここで明示的にCPUに割り当てる
int thread_index = omp_get_thread_num(); // 自分のthread numberを取得
WinProcGroup::bindThisThread(thread_index);
#endif
#pragma omp for schedule(dynamic,20000)
for (int64_t index_ = 0; index_ < (int64_t)size; ++index_)
{
// OpenMPの制約からループ変数は符号型でないといけないらしいのだが、
// さすがに使いにくい。
uint64_t index = (uint64_t)index_;
if (g_kk.is_ok(index))
{
// indexからの変換と逆変換によって元のindexに戻ることを確認しておく。
// 起動時に1回しか実行しない処理なのでassertで書いておく。
assert(g_kk.fromIndex(index).toIndex() == index);
KK a[KK_LOWER_COUNT];
g_kk.fromIndex(index).toLowerDimensions(a);
// 次元下げの1つ目の要素が元のindexと同一であることを確認しておく。
assert(a[0].toIndex() == index);
uint64_t min_index = UINT64_MAX;
for (auto& e : a)
min_index = std::min(min_index, e.toIndex());
min_index_flag[index] = (min_index == index);
}
else if (g_kkp.is_ok(index))
{
assert(g_kkp.fromIndex(index).toIndex() == index);
KKP x = g_kkp.fromIndex(index);
KKP a[KKP_LOWER_COUNT];
x.toLowerDimensions(a);
assert(a[0].toIndex() == index);
uint64_t min_index = UINT64_MAX;
for (auto& e : a)
min_index = std::min(min_index, e.toIndex());
min_index_flag[index] = (min_index == index);
}
else if (g_kpp.is_ok(index))
{
assert(g_kpp.fromIndex(index).toIndex() == index);
KPP x = g_kpp.fromIndex(index);
KPP a[KPP_LOWER_COUNT];
x.toLowerDimensions(a);
assert(a[0].toIndex() == index);
uint64_t min_index = UINT64_MAX;
for (auto& e : a)
min_index = std::min(min_index, e.toIndex());
min_index_flag[index] = (min_index == index);
}
else
{
assert(false);
}
}
}
}
void learning_tools_unit_test_kpp()
{
// KPPの三角配列化にバグがないかテストする
// k-p0-p1のすべての組み合わせがきちんとKPPの扱う対象になっていかと、そのときの次元下げが
// 正しいかを判定する。
KK g_kk;
g_kk.set(SQUARE_NB, Eval::fe_end, 0);
KKP g_kkp;
g_kkp.set(SQUARE_NB, Eval::fe_end, g_kk.max_index());
KPP g_kpp;
g_kpp.set(SQUARE_NB, Eval::fe_end, g_kkp.max_index());
std::vector<bool> f;
f.resize(g_kpp.max_index() - g_kpp.min_index());
for(auto k = SQUARE_ZERO ; k < SQUARE_NB ; ++k)
for(auto p0 = BonaPiece::BONA_PIECE_ZERO; p0 < fe_end ; ++p0)
for (auto p1 = BonaPiece::BONA_PIECE_ZERO; p1 < fe_end; ++p1)
{
KPP kpp_org = g_kpp.fromKPP(k,p0,p1);
KPP kpp0;
KPP kpp1 = g_kpp.fromKPP(Mir(k), mir_piece(p0), mir_piece(p1));
KPP kpp_array[2];
auto index = kpp_org.toIndex();
assert(g_kpp.is_ok(index));
kpp0 = g_kpp.fromIndex(index);
//if (kpp0 != kpp_org)
// std::cout << "index = " << index << "," << kpp_org << "," << kpp0 << std::endl;
kpp0.toLowerDimensions(kpp_array);
assert(kpp_array[0] == kpp0);
assert(kpp0 == kpp_org);
assert(kpp_array[1] == kpp1);
auto index2 = kpp1.toIndex();
f[index - g_kpp.min_index()] = f[index2-g_kpp.min_index()] = true;
}
// 抜けてるindexがなかったかの確認。
for(size_t index = 0 ; index < f.size(); index++)
if (!f[index])
{
std::cout << index << g_kpp.fromIndex(index + g_kpp.min_index()) << std::endl;
}
}
void learning_tools_unit_test_kppp()
{
// KPPPの計算に抜けがないかをテストする
KPPP g_kppp;
g_kppp.set(15, Eval::fe_end,0);
uint64_t min_index = g_kppp.min_index();
uint64_t max_index = g_kppp.max_index();
// 最後の要素の確認。
//KPPP x = KPPP::fromIndex(max_index-1);
//std::cout << x << std::endl;
for (uint64_t index = min_index; index < max_index; ++index)
{
KPPP x = g_kppp.fromIndex(index);
//std::cout << x << std::endl;
#if 0
if ((index % 10000000) == 0)
std::cout << "index = " << index << std::endl;
// index = 9360000000
// done.
if (x.toIndex() != index)
{
std::cout << "assertion failed , index = " << index << std::endl;
}
#endif
assert(x.toIndex() == index);
// ASSERT((&kppp_ksq_pcpcpc(x.king(), x.piece0(), x.piece1(), x.piece2()) - &kppp[0][0]) == (index - min_index));
}
}
void learning_tools_unit_test_kkpp()
{
KKPP g_kkpp;
g_kkpp.set(SQUARE_NB, 10000 , 0);
uint64_t n = 0;
for (int k = 0; k<SQUARE_NB; ++k)
for (int i = 0; i<10000; ++i) // 試しに、かなり大きなfe_endを想定して10000で回してみる。
for (int j = 0; j < i; ++j)
{
auto kkpp = g_kkpp.fromKKPP(k, (BonaPiece)i, (BonaPiece)j);
auto r = kkpp.toRawIndex();
assert(n++ == r);
auto kkpp2 = g_kkpp.fromIndex(r + g_kkpp.min_index());
assert(kkpp2.king() == k && kkpp2.piece0() == i && kkpp2.piece1() == j);
}
}
// このEvalLearningTools全体の初期化
void init()
{
// 初期化は、起動後1回限りで良いのでそのためのフラグ。
static bool first = true;
if (first)
{
std::cout << "EvalLearningTools init..";
// mir_piece()とinv_piece()を利用可能にする。
// このあとmin_index_flagの初期化を行なうが、そこが
// これに依存しているので、こちらを先に行なう必要がある。
init_mir_inv_tables();
//learning_tools_unit_test_kpp();
//learning_tools_unit_test_kppp();
//learning_tools_unit_test_kkpp();
// UnitTestを実行するの最後でも良いのだが、init_min_index_flag()にとても時間がかかるので
// デバッグ時はこのタイミングで行いたい。
init_min_index_flag();
std::cout << "done." << std::endl;
first = false;
}
}
}
#endif

1032
src/learn/learning_tools.h Normal file

File diff suppressed because it is too large Load Diff

123
src/learn/multi_think.cpp Normal file
View File

@@ -0,0 +1,123 @@
#include "../types.h"
#if defined(EVAL_LEARN)
#include "multi_think.h"
#include "../tt.h"
#include "../uci.h"
#include <thread>
void MultiThink::go_think()
{
// あとでOptionsの設定を復元するためにコピーで保持しておく。
auto oldOptions = Options;
// 定跡を用いる場合、on the flyで行なうとすごく時間がかかるファイルアクセスを行なう部分が
// thread safeではないので、メモリに丸読みされている状態であることをここで保証する。
Options["BookOnTheFly"] = std::string("false");
// 評価関数の読み込み等
// learnコマンドの場合、評価関数読み込み後に評価関数の値を補正している可能性があるので、
// メモリの破損チェックは省略する。
is_ready(true);
// 派生クラスのinit()を呼び出す。
init();
// ループ上限はset_loop_max()で設定されているものとする。
loop_count = 0;
done_count = 0;
// threadをOptions["Threads"]の数だけ生成して思考開始。
std::vector<std::thread> threads;
auto thread_num = (size_t)Options["Threads"];
// worker threadの終了フラグの確保
thread_finished.resize(thread_num);
// worker threadの起動
for (size_t i = 0; i < thread_num; ++i)
{
thread_finished[i] = 0;
threads.push_back(std::thread([i, this]
{
// プロセッサの全スレッドを使い切る。
WinProcGroup::bindThisThread(i);
// オーバーライドされている処理を実行
this->thread_worker(i);
// スレッドが終了したので終了フラグを立てる
this->thread_finished[i] = 1;
}));
}
// すべてのthreadの終了待ちを
// for (auto& th : threads)
// th.join();
// のように書くとスレッドがまだ仕事をしている状態でここに突入するので、
// その間、callback_func()が呼び出せず、セーブできなくなる。
// そこで終了フラグを自前でチェックする必要がある。
// すべてのスレッドが終了したかを判定する関数
auto threads_done = [&]()
{
// ひとつでも終了していなければfalseを返す
for (auto& f : thread_finished)
if (!f)
return false;
return true;
};
// コールバック関数が設定されているならコールバックする。
auto do_a_callback = [&]()
{
if (callback_func)
callback_func();
};
for (uint64_t i = 0 ; ; )
{
// 全スレッドが終了していたら、ループを抜ける。
if (threads_done())
break;
sleep(1000);
// callback_secondsごとにcallback_func()が呼び出される。
if (++i == callback_seconds)
{
do_a_callback();
// ↑から戻ってきてからカウンターをリセットしているので、
// do_a_callback()のなかでsave()などにどれだけ時間がかかろうと
// 次に呼び出すのは、そこから一定時間の経過を要する。
i = 0;
}
}
// 最後の保存。
std::cout << std::endl << "finalize..";
// do_a_callback();
// → 呼び出し元で保存するはずで、ここでは要らない気がする。
// 終了したフラグは立っているがスレッドの終了コードの実行中であるということはありうるので
// join()でその終了を待つ必要がある。
for (auto& th : threads)
th.join();
// 全スレッドが終了しただけでfileの書き出しスレッドなどはまだ動いていて
// 作業自体は完了していない可能性があるのでスレッドがすべて終了したことだけ出力する。
std::cout << "all threads are joined." << std::endl;
// Optionsを書き換えたので復元。
// 値を代入しないとハンドラが起動しないのでこうやって復元する。
for (auto& s : oldOptions)
Options[s.first] = std::string(s.second);
}
#endif // defined(EVAL_LEARN)

151
src/learn/multi_think.h Normal file
View File

@@ -0,0 +1,151 @@
#ifndef _MULTI_THINK_
#define _MULTI_THINK_
#if defined(EVAL_LEARN)
#include <functional>
#include "../misc.h"
#include "../learn/learn.h"
#include "../thread_win32_osx.h"
#include <atomic>
// 棋譜からの学習や、自ら思考させて定跡を生成するときなど、
// 複数スレッドが個別にSearch::think()を呼び出したいときに用いるヘルパクラス。
// このクラスを派生させて用いる。
struct MultiThink
{
MultiThink() : prng(21120903)
{
loop_count = 0;
}
// マスタースレッドからこの関数を呼び出すと、スレッドがそれぞれ思考して、
// 思考終了条件を満たしたところで制御を返す。
// 他にやってくれること。
// ・各スレッドがLearner::search(),qsearch()を呼び出しても安全なように
//  置換表をスレッドごとに分離してくれる。(終了後、元に戻してくれる。)
// ・bookはon the flyモードだとthread safeではないので、このモードを一時的に
//  オフにしてくれる。
// [要件]
// 1) thread_worker()のオーバーライド
// 2) set_loop_max()でループ回数の設定
// 3) 定期的にcallbackされる関数を設定する(必要なら)
// callback_funcとcallback_interval
void go_think();
// 派生クラス側で初期化したいものがあればこれをoverrideしておけば、
// go_think()で初期化が終わったタイミングで呼び出される。
// 定跡の読み込みなどはそのタイミングで行うと良い。
virtual void init() {}
// go_think()したときにスレッドを生成して呼び出されるthread worker
// これをoverrideして用いる。
virtual void thread_worker(size_t thread_id) = 0;
// go_think()したときにcallback_seconds[秒]ごとにcallbackされる。
std::function<void()> callback_func;
uint64_t callback_seconds = 600;
// workerが処理する(Search::think()を呼び出す)回数を設定する。
void set_loop_max(uint64_t loop_max_) { loop_max = loop_max_; }
// set_loop_max()で設定した値を取得する。
uint64_t get_loop_max() const { return loop_max; }
// [ASYNC] ループカウンターの値を取り出して、取り出し後にループカウンターを加算する。
// もしループカウンターがloop_maxに達していたらUINT64_MAXを返す。
// 局面を生成する場合などは、局面を生成するタイミングでこの関数を呼び出すようにしないと、
// 生成した局面数と、カウンターの値が一致しなくなってしまうので注意すること。
uint64_t get_next_loop_count() {
std::unique_lock<Mutex> lk(loop_mutex);
if (loop_count >= loop_max)
return UINT64_MAX;
return loop_count++;
}
// [ASYNC] 処理した個数を返す用。呼び出されるごとにインクリメントされたカウンターが返る。
uint64_t get_done_count() {
std::unique_lock<Mutex> lk(loop_mutex);
return ++done_count;
}
// worker threadがI/Oにアクセスするときのmutex
Mutex io_mutex;
protected:
// 乱数発生器本体
AsyncPRNG prng;
private:
// workerが処理する(Search::think()を呼び出す)回数
std::atomic<uint64_t> loop_max;
// workerが処理した(Search::think()を呼び出した)回数
std::atomic<uint64_t> loop_count;
// 処理した回数を返す用。
std::atomic<uint64_t> done_count;
// ↑の変数を変更するときのmutex
Mutex loop_mutex;
// スレッドの終了フラグ。
// vector<bool>にすると複数スレッドから書き換えようとしたときに正しく反映されないことがある…はず。
typedef uint8_t Flag;
std::vector<Flag> thread_finished;
};
// idle時間にtaskを処理する仕組み。
// masterは好きなときにpush_task_async()でtaskを渡す。
// slaveは暇なときにon_idle()を実行すると、taskを一つ取り出してqueueがなくなるまで実行を続ける。
// MultiThinkのthread workerをmaster-slave方式で書きたいときに用いると便利。
struct TaskDispatcher
{
typedef std::function<void(size_t /* thread_id */)> Task;
// slaveはidle中にこの関数を呼び出す。
void on_idle(size_t thread_id)
{
Task task;
while ((task = get_task_async()) != nullptr)
task(thread_id);
sleep(1);
}
// [ASYNC] taskを一つ積む。
void push_task_async(Task task)
{
std::unique_lock<Mutex> lk(task_mutex);
tasks.push_back(task);
}
// task用の配列の要素をsize分だけ事前に確保する。
void task_reserve(size_t size)
{
tasks.reserve(size);
}
protected:
// taskの集合
std::vector<Task> tasks;
// [ASYNC] taskを一つ取り出す。on_idle()から呼び出される。
Task get_task_async()
{
std::unique_lock<Mutex> lk(task_mutex);
if (tasks.size() == 0)
return nullptr;
Task task = *tasks.rbegin();
tasks.pop_back();
return task;
}
// tasksにアクセスするとき用のmutex
Mutex task_mutex;
};
#endif // defined(EVAL_LEARN) && defined(YANEURAOU_2018_OTAFUKU_ENGINE)
#endif