TadaoYamaokaの開発日記

個人開発しているスマホアプリや将棋AIの開発ネタを中心に書いていきます。

入力特徴量作成の速度改善

年末にDiscordのやりとりで、dlshogiの入力特徴量作成で、各マスの利きを求めるために、各マス×手番ごとにattackersToを呼び出すのは無駄と、やねうら氏からご指摘をいただいた。
その通りなので、改善を行った。

改善したコード

各マスの利きをattackersToで調べると、処理の重い飛車・角の飛び利きを81マス×2(手番)回呼び出すことになる。
盤上の駒ごとに利きを調べて、各駒が利いているマスごとに処理することで、飛び利きの呼び出し回数を盤上にある駒の数に減らすことができる。

改善前のコード
	const Bitboard occupied_bb = position.occupiedBB();

	// 駒の利き(駒種でマージ)
	Bitboard attacks[ColorNum][PieceTypeNum] = {
		{ { 0, 0 },{ 0, 0 },{ 0, 0 },{ 0, 0 },{ 0, 0 },{ 0, 0 },{ 0, 0 },{ 0, 0 },{ 0, 0 },{ 0, 0 },{ 0, 0 },{ 0, 0 },{ 0, 0 },{ 0, 0 },{ 0, 0 } },
		{ { 0, 0 },{ 0, 0 },{ 0, 0 },{ 0, 0 },{ 0, 0 },{ 0, 0 },{ 0, 0 },{ 0, 0 },{ 0, 0 },{ 0, 0 },{ 0, 0 },{ 0, 0 },{ 0, 0 },{ 0, 0 },{ 0, 0 } },
	};
	for (Square sq = SQ11; sq < SquareNum; sq++) {
		const Piece p = position.piece(sq);
		if (p != Empty) {
			const Color pc = pieceToColor(p);
			const PieceType pt = pieceToPieceType(p);
			const Bitboard bb = position.attacksFrom(pt, pc, sq, occupied_bb);
			attacks[pc][pt] |= bb;
		}
	}

	for (Color c = Black; c < ColorNum; ++c) {
		// 白の場合、色を反転
		const Color c2 = turn == Black ? c : oppositeColor(c);

		// 駒の配置
		Bitboard bb[PieceTypeNum];
		for (PieceType pt = Pawn; pt < PieceTypeNum; ++pt) {
			bb[pt] = position.bbOf(pt, c);
		}

		for (Square sq = SQ11; sq < SquareNum; ++sq) {
			// 白の場合、盤面を180度回転
			const Square sq2 = turn == Black ? sq : SQ99 - sq;

			for (PieceType pt = Pawn; pt < PieceTypeNum; ++pt) {
				// 駒の配置
				if (bb[pt].isSet(sq)) {
					(*features1)[c2][pt - 1][sq2] = 1;
				}

				// 駒の利き
				if (attacks[c][pt].isSet(sq)) {
					(*features1)[c2][PIECETYPE_NUM + pt - 1][sq2] = 1;
				}
			}

			// 利き数
			const int num = std::min(MAX_ATTACK_NUM, position.attackersTo(c, sq, occupied_bb).popCount());
			for (int k = 0; k < num; k++) {
				(*features1)[c2][PIECETYPE_NUM + PIECETYPE_NUM + k][sq2] = 1;
			}
		}
	}
改善後のコード
	const Bitboard occupied_bb = position.occupiedBB();

	// 歩と歩以外に分ける
	Bitboard pawns_bb = position.bbOf(Pawn);
	Bitboard without_pawns_bb = occupied_bb & ~pawns_bb;
	// 利き数集計用
	int attack_num[ColorNum][SquareNum] = {};

	// 歩以外
	FOREACH_BB(without_pawns_bb, Square sq, {
		const Piece pc = position.piece(sq);
		const PieceType pt = pieceToPieceType(pc);
		Color c = pieceToColor(pc);
		Bitboard attacks = Position::attacksFrom(pt, c, sq, occupied_bb);

		// 後手の場合、色を反転し、盤面を180度回転
		if (turn == White) {
			c = oppositeColor(c);
			sq = SQ99 - sq;
		}

		// 駒の配置
		(*features1)[c][pt - 1][sq] = 1.0f;

		FOREACH_BB(attacks, Square to, {
			// 後手の場合、盤面を180度回転
			if (turn == White) to = SQ99 - to;

			// 駒の利き
			(*features1)[c][PIECETYPE_NUM + pt - 1][to] = 1.0f;

			// 利き数
			auto& num = attack_num[c][to];
			if (num < MAX_ATTACK_NUM) {
				(*features1)[c][PIECETYPE_NUM + PIECETYPE_NUM + num][to] = 1.0f;
				num++;
			}
		});
	});

	for (Color c = Black; c < ColorNum; ++c) {
		// 後手の場合、色を反転
		const Color c2 = turn == Black ? c : oppositeColor(c);

		// 歩
		Bitboard pawns_bb2 = pawns_bb & position.bbOf(c2);
		const SquareDelta pawnDelta = c == Black ? DeltaN : DeltaS;
		FOREACH_BB(pawns_bb2, Square sq, {
			// 後手の場合、盤面を180度回転
			if (turn == White) sq = SQ99 - sq;

			// 駒の配置
			(*features1)[c][Pawn - 1][sq] = 1.0f;

			// 駒の利き
			const Square to = sq + pawnDelta; // 1マス先
			(*features1)[c][PIECETYPE_NUM + Pawn - 1][to] = 1.0f;

			// 利き数
			auto& num = attack_num[c][to];
			if (num < MAX_ATTACK_NUM) {
				(*features1)[c][PIECETYPE_NUM + PIECETYPE_NUM + num][to] = 1.0f;
				num++;
			}
		});
	}

速度比較

floodgateのR3500同士の棋譜から抽出した6997258局面を使用して、入力特徴量作成処理にかかった時間の合計を測定した。
20回測定した結果は以下の通り。モデルサイズは15ブロック。

改善前(ms) 改善後(ms) 改善後/改善前
平均 39533 5384 13.6%
中央値 39422 5378 13.6%
最大値 40597 5430 13.4%
最小値 39287 5363 13.7%

平均で、処理時間が13.6%になった。

NPS比較

floodgateの棋譜からサンプリングした100局面で、1秒探索した際のNPSを測定した。
10回測定した平均値を算出し、100局面について平均、中央値、最大値、最小値を算出した。

RTX3090、2スレッド
改善前(ms) 改善後(ms) 改善後/改善前
平均値 28862 29246 101.5%
中央値 30155 30196 100.1%
最大値 31660 31557 106.4%
最小値 17078 17746 99.1%

※改善後/改善前は同一局面での比

100局面の比の平均で1.5%NPSが向上した。

A100x8、GPUあたり4スレッド
改善前(ms) 改善後(ms) 改善後/改善前
平均値 295397 292695 99.1%
中央値 297469 294976 99.2%
最大値 343213 343739 102.3%
最小値 233813 229470 94.7%

※改善後/改善前は同一局面での比

100局面の比の平均で0.9%NPSが低下した。

入力特徴量作成処理の時間が短くなっているのに、NPSが低下する理由がわからない。
NPSはGPUの温度などで毎回ぶれるため、測定誤差と考えられる。
入力特徴量作成は、各プレイアウトで1回しか呼び出さないため、ボトルネックもなっていないため、NPSには影響がないと考えられる。

まとめ

入力特徴量作成処理に無駄な処理があっため、改善を行ったことで、処理時間が13.6%になった。
ただし、入力特徴量作成処理はボトルネックになっておらず、NPSにはほとんど影響がなかった。

学習時にも、入力特徴量作成処理を呼び出しているが、データローダの並列化を行っているため、影響はなさそうである。

追記

10ブロックのモデルでも確認した。

RTX3090、2スレッド
改善前(ms) 改善後(ms) 改善後/改善前
平均値 43589 46531 107.1%
中央値 46058 51137 108.0%
最大値 61426 57525 113.3%
最小値 19727 20366 90.7%

※改善後/改善前は同一局面での比

100局面の比の平均で7.1%NPSが向上した。

A100x8、GPUあたり5スレッド
改善前(ms) 改善後(ms) 改善後/改善前
平均値 306643 315190 102.8%
中央値 306638 313558 102.6%
最大値 419507 449810 107.2%
最小値 247300 243651 97.5%

※改善後/改善前は同一局面での比

100局面の比の平均で2.8%NPSが向上した。

2021/1/2 追記

改善後(ms)の数値が20回の平均値ではなく10回目の数値になっていたため修正した。