Skip to content

Commit

Permalink
New NNUE architecture and net
Browse files Browse the repository at this point in the history
Introduces a new NNUE network architecture and associated network parameters,
as obtained by a new pytorch trainer.

The network is already very strong at short TC, without regression at longer TC,
and has potential for further improvements.

https://tests.stockfishchess.org/tests/view/60a159c65085663412d0921d
TC: 10s+0.1s, 1 thread
ELO: 21.74 +-3.4 (95%) LOS: 100.0%
Total: 10000 W: 1559 L: 934 D: 7507
Ptnml(0-2): 38, 701, 2972, 1176, 113

https://tests.stockfishchess.org/tests/view/60a187005085663412d0925b
TC: 60s+0.6s, 1 thread
ELO: 5.85 +-1.7 (95%) LOS: 100.0%
Total: 20000 W: 1381 L: 1044 D: 17575
Ptnml(0-2): 27, 885, 7864, 1172, 52

https://tests.stockfishchess.org/tests/view/60a2beede229097940a03806
TC: 20s+0.2s, 8 threads
LLR: 2.93 (-2.94,2.94) <0.50,3.50>
Total: 34272 W: 1610 L: 1452 D: 31210
Ptnml(0-2): 30, 1285, 14350, 1439, 32

https://tests.stockfishchess.org/tests/view/60a2d687e229097940a03c72
TC: 60s+0.6s, 8 threads
LLR: 2.94 (-2.94,2.94) <-2.50,0.50>
Total: 45544 W: 1262 L: 1214 D: 43068
Ptnml(0-2): 12, 1129, 20442, 1177, 12

The network has been trained (by vondele) using the https://github.com/glinscott/nnue-pytorch/ trainer (started by glinscott),
specifically the branch https://github.com/Sopel97/nnue-pytorch/tree/experiment_56.
The data used are in 64 billion positions (193GB total) generated and scored with the current master net
d8: https://drive.google.com/file/d/1hOOYSDKgOOp38ZmD0N4DV82TOLHzjUiF/view?usp=sharing
d9: https://drive.google.com/file/d/1VlhnHL8f-20AXhGkILujnNXHwy9T-MQw/view?usp=sharing
d10: https://drive.google.com/file/d/1ZC5upzBYMmMj1gMYCkt6rCxQG0GnO3Kk/view?usp=sharing
fishtest_d9: https://drive.google.com/file/d/1GQHt0oNgKaHazwJFTRbXhlCN3FbUedFq/view?usp=sharing

This network also contains a few architectural changes with respect to the current master:

    Size changed from 256x2-32-32-1 to 512x2-16-32-1
        ~15-20% slower
        ~2x larger
        adds a special path for 16 valued ClippedReLU
        fixes affine transform code for 16 inputs/outputs, buy using InputDimensions instead of PaddedInputDimensions
            this is safe now because the inputs are processed in groups of 4 in the current affine transform code
    The feature set changed from HalfKP to HalfKAv2
        Includes information about the kings like HalfKA
        Packs king features better, resulting in 8% size reduction compared to HalfKA
    The board is flipped for the black's perspective, instead of rotated like in the current master
    PSQT values for each feature
        the feature transformer now outputs a part that is fowarded directly to the output and allows learning piece values more directly than the previous network architecture. The effect is visible for high imbalance positions, where the current master network outputs evaluations skewed towards zero.
        8 PSQT values per feature, chosen based on (popcount(pos.pieces()) - 1) / 4
        initialized to classical material values on the start of the training
    8 subnetworks (512x2->16->32->1), chosen based on (popcount(pos.pieces()) - 1) / 4
        only one subnetwork is evaluated for any position, no or marginal speed loss

A diagram of the network is available: https://user-images.githubusercontent.com/8037982/118656988-553a1700-b7eb-11eb-82ef-56a11cbebbf2.png
A more complete description: https://github.com/glinscott/nnue-pytorch/blob/master/docs/nnue.md

closes #3474

Bench: 3806488
  • Loading branch information
Sopel97 authored and vondele committed May 18, 2021
1 parent f90274d commit e8d64af
Show file tree
Hide file tree
Showing 13 changed files with 265 additions and 173 deletions.
2 changes: 1 addition & 1 deletion src/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ endif
SRCS = benchmark.cpp bitbase.cpp bitboard.cpp endgame.cpp evaluate.cpp main.cpp \
material.cpp misc.cpp movegen.cpp movepick.cpp pawns.cpp position.cpp psqt.cpp \
search.cpp thread.cpp timeman.cpp tt.cpp uci.cpp ucioption.cpp tune.cpp syzygy/tbprobe.cpp \
nnue/evaluate_nnue.cpp nnue/features/half_kp.cpp
nnue/evaluate_nnue.cpp nnue/features/half_ka_v2.cpp

OBJS = $(notdir $(SRCS:.cpp=.o))

Expand Down
13 changes: 5 additions & 8 deletions src/evaluate.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ namespace Eval {

if (filename.has_value())
actualFilename = filename.value();
else
else
{
if (eval_file_loaded != EvalFileDefaultName)
{
Expand Down Expand Up @@ -1116,10 +1116,8 @@ Value Eval::evaluate(const Position& pos) {
// Scale and shift NNUE for compatibility with search and classical evaluation
auto adjusted_NNUE = [&]()
{
int material = pos.non_pawn_material() + 4 * PawnValueMg * pos.count<PAWN>();
int scale = 580
+ material / 32
- 4 * pos.rule50_count();

int scale = 903 + 28 * pos.count<PAWN>() + 28 * pos.non_pawn_material() / 1024;

Value nnue = NNUE::evaluate(pos) * scale / 1024 + Time.tempoNNUE;

Expand All @@ -1134,7 +1132,7 @@ Value Eval::evaluate(const Position& pos) {
Value psq = Value(abs(eg_value(pos.psq_score())));
int r50 = 16 + pos.rule50_count();
bool largePsq = psq * 16 > (NNUEThreshold1 + pos.non_pawn_material() / 64) * r50;
bool classical = largePsq || (psq > PawnValueMg / 4 && !(pos.this_thread()->nodes & 0xB));
bool classical = largePsq;

// Use classical evaluation for really low piece endgames.
// One critical case is the draw for bishop + A/H file pawn vs naked king.
Expand All @@ -1151,8 +1149,7 @@ Value Eval::evaluate(const Position& pos) {
&& !lowPieceEndgame
&& ( abs(v) * 16 < NNUEThreshold2 * r50
|| ( pos.opposite_bishops()
&& abs(v) * 16 < (NNUEThreshold1 + pos.non_pawn_material() / 64) * r50
&& !(pos.this_thread()->nodes & 0xB))))
&& abs(v) * 16 < (NNUEThreshold1 + pos.non_pawn_material() / 64) * r50)))
v = adjusted_NNUE();
}

Expand Down
2 changes: 1 addition & 1 deletion src/evaluate.h
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ namespace Eval {
// The default net name MUST follow the format nn-[SHA256 first 12 digits].nnue
// for the build process (profile-build and fishtest) to work. Do not change the
// name of the macro, as it is used in the Makefile.
#define EvalFileDefaultName "nn-62ef826d1a6d.nnue"
#define EvalFileDefaultName "nn-8a08400ed089.nnue"

namespace NNUE {

Expand Down
24 changes: 16 additions & 8 deletions src/nnue/evaluate_nnue.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ namespace Stockfish::Eval::NNUE {
LargePagePtr<FeatureTransformer> featureTransformer;

// Evaluation function
AlignedPtr<Network> network;
AlignedPtr<Network> network[LayerStacks];

// Evaluation function file name
std::string fileName;
Expand Down Expand Up @@ -83,7 +83,8 @@ namespace Stockfish::Eval::NNUE {
void initialize() {

Detail::initialize(featureTransformer);
Detail::initialize(network);
for (std::size_t i = 0; i < LayerStacks; ++i)
Detail::initialize(network[i]);
}

// Read network header
Expand All @@ -92,7 +93,7 @@ namespace Stockfish::Eval::NNUE {
std::uint32_t version, size;

version = read_little_endian<std::uint32_t>(stream);
*hashValue = read_little_endian<std::uint32_t>(stream);
*hashValue = read_little_endian<std::uint32_t>(stream);
size = read_little_endian<std::uint32_t>(stream);
if (!stream || version != Version) return false;
desc->resize(size);
Expand All @@ -117,7 +118,8 @@ namespace Stockfish::Eval::NNUE {
if (!read_header(stream, &hashValue, &netDescription)) return false;
if (hashValue != HashValue) return false;
if (!Detail::read_parameters(stream, *featureTransformer)) return false;
if (!Detail::read_parameters(stream, *network)) return false;
for (std::size_t i = 0; i < LayerStacks; ++i)
if (!Detail::read_parameters(stream, *(network[i]))) return false;
return stream && stream.peek() == std::ios::traits_type::eof();
}

Expand All @@ -126,7 +128,8 @@ namespace Stockfish::Eval::NNUE {

if (!write_header(stream, HashValue, netDescription)) return false;
if (!Detail::write_parameters(stream, *featureTransformer)) return false;
if (!Detail::write_parameters(stream, *network)) return false;
for (std::size_t i = 0; i < LayerStacks; ++i)
if (!Detail::write_parameters(stream, *(network[i]))) return false;
return (bool)stream;
}

Expand Down Expand Up @@ -154,10 +157,15 @@ namespace Stockfish::Eval::NNUE {
ASSERT_ALIGNED(transformedFeatures, alignment);
ASSERT_ALIGNED(buffer, alignment);

featureTransformer->transform(pos, transformedFeatures);
const auto output = network->propagate(transformedFeatures, buffer);
const std::size_t bucket = (pos.count<ALL_PIECES>() - 1) / 4;

return static_cast<Value>(output[0] / OutputScale);
const auto [psqt, lazy] = featureTransformer->transform(pos, transformedFeatures, bucket);
if (lazy) {
return static_cast<Value>(psqt / OutputScale);
} else {
const auto output = network[bucket]->propagate(transformedFeatures, buffer);
return static_cast<Value>((output[0] + psqt) / OutputScale);
}
}

// Load eval, from a file stream or a memory stream
Expand Down
25 changes: 12 additions & 13 deletions src/nnue/features/half_kp.cpp → src/nnue/features/half_ka_v2.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,32 +16,32 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

//Definition of input features HalfKP of NNUE evaluation function
//Definition of input features HalfKAv2 of NNUE evaluation function

#include "half_kp.h"
#include "half_ka_v2.h"

#include "../../position.h"

namespace Stockfish::Eval::NNUE::Features {

// Orient a square according to perspective (rotates by 180 for black)
inline Square HalfKP::orient(Color perspective, Square s) {
return Square(int(s) ^ (bool(perspective) * 63));
inline Square HalfKAv2::orient(Color perspective, Square s) {
return Square(int(s) ^ (bool(perspective) * 56));
}

// Index of a feature for a given king position and another piece on some square
inline IndexType HalfKP::make_index(Color perspective, Square s, Piece pc, Square ksq) {
inline IndexType HalfKAv2::make_index(Color perspective, Square s, Piece pc, Square ksq) {
return IndexType(orient(perspective, s) + PieceSquareIndex[perspective][pc] + PS_NB * ksq);
}

// Get a list of indices for active features
void HalfKP::append_active_indices(
void HalfKAv2::append_active_indices(
const Position& pos,
Color perspective,
ValueListInserter<IndexType> active
) {
Square ksq = orient(perspective, pos.square<KING>(perspective));
Bitboard bb = pos.pieces() & ~pos.pieces(KING);
Bitboard bb = pos.pieces();
while (bb)
{
Square s = pop_lsb(bb);
Expand All @@ -52,7 +52,7 @@ namespace Stockfish::Eval::NNUE::Features {

// append_changed_indices() : get a list of indices for recently changed features

void HalfKP::append_changed_indices(
void HalfKAv2::append_changed_indices(
Square ksq,
StateInfo* st,
Color perspective,
Expand All @@ -63,23 +63,22 @@ namespace Stockfish::Eval::NNUE::Features {
Square oriented_ksq = orient(perspective, ksq);
for (int i = 0; i < dp.dirty_num; ++i) {
Piece pc = dp.piece[i];
if (type_of(pc) == KING) continue;
if (dp.from[i] != SQ_NONE)
removed.push_back(make_index(perspective, dp.from[i], pc, oriented_ksq));
if (dp.to[i] != SQ_NONE)
added.push_back(make_index(perspective, dp.to[i], pc, oriented_ksq));
}
}

int HalfKP::update_cost(StateInfo* st) {
int HalfKAv2::update_cost(StateInfo* st) {
return st->dirtyPiece.dirty_num;
}

int HalfKP::refresh_cost(const Position& pos) {
return pos.count<ALL_PIECES>() - 2;
int HalfKAv2::refresh_cost(const Position& pos) {
return pos.count<ALL_PIECES>();
}

bool HalfKP::requires_refresh(StateInfo* st, Color perspective) {
bool HalfKAv2::requires_refresh(StateInfo* st, Color perspective) {
return st->dirtyPiece.piece[0] == make_piece(perspective, KING);
}

Expand Down
51 changes: 26 additions & 25 deletions src/nnue/features/half_kp.h → src/nnue/features/half_ka_v2.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@

//Definition of input features HalfKP of NNUE evaluation function

#ifndef NNUE_FEATURES_HALF_KP_H_INCLUDED
#define NNUE_FEATURES_HALF_KP_H_INCLUDED
#ifndef NNUE_FEATURES_HALF_KA_V2_H_INCLUDED
#define NNUE_FEATURES_HALF_KA_V2_H_INCLUDED

#include "../nnue_common.h"

Expand All @@ -32,33 +32,34 @@ namespace Stockfish {

namespace Stockfish::Eval::NNUE::Features {

// Feature HalfKP: Combination of the position of own king
// and the position of pieces other than kings
class HalfKP {
// Feature HalfKAv2: Combination of the position of own king
// and the position of pieces
class HalfKAv2 {

// unique number for each piece type on each square
enum {
PS_NONE = 0,
PS_W_PAWN = 1,
PS_B_PAWN = 1 * SQUARE_NB + 1,
PS_W_KNIGHT = 2 * SQUARE_NB + 1,
PS_B_KNIGHT = 3 * SQUARE_NB + 1,
PS_W_BISHOP = 4 * SQUARE_NB + 1,
PS_B_BISHOP = 5 * SQUARE_NB + 1,
PS_W_ROOK = 6 * SQUARE_NB + 1,
PS_B_ROOK = 7 * SQUARE_NB + 1,
PS_W_QUEEN = 8 * SQUARE_NB + 1,
PS_B_QUEEN = 9 * SQUARE_NB + 1,
PS_NB = 10 * SQUARE_NB + 1
PS_W_PAWN = 0,
PS_B_PAWN = 1 * SQUARE_NB,
PS_W_KNIGHT = 2 * SQUARE_NB,
PS_B_KNIGHT = 3 * SQUARE_NB,
PS_W_BISHOP = 4 * SQUARE_NB,
PS_B_BISHOP = 5 * SQUARE_NB,
PS_W_ROOK = 6 * SQUARE_NB,
PS_B_ROOK = 7 * SQUARE_NB,
PS_W_QUEEN = 8 * SQUARE_NB,
PS_B_QUEEN = 9 * SQUARE_NB,
PS_KING = 10 * SQUARE_NB,
PS_NB = 11 * SQUARE_NB
};

static constexpr IndexType PieceSquareIndex[COLOR_NB][PIECE_NB] = {
// convention: W - us, B - them
// viewed from other side, W and B are reversed
{ PS_NONE, PS_W_PAWN, PS_W_KNIGHT, PS_W_BISHOP, PS_W_ROOK, PS_W_QUEEN, PS_NONE, PS_NONE,
PS_NONE, PS_B_PAWN, PS_B_KNIGHT, PS_B_BISHOP, PS_B_ROOK, PS_B_QUEEN, PS_NONE, PS_NONE },
{ PS_NONE, PS_B_PAWN, PS_B_KNIGHT, PS_B_BISHOP, PS_B_ROOK, PS_B_QUEEN, PS_NONE, PS_NONE,
PS_NONE, PS_W_PAWN, PS_W_KNIGHT, PS_W_BISHOP, PS_W_ROOK, PS_W_QUEEN, PS_NONE, PS_NONE }
{ PS_NONE, PS_W_PAWN, PS_W_KNIGHT, PS_W_BISHOP, PS_W_ROOK, PS_W_QUEEN, PS_KING, PS_NONE,
PS_NONE, PS_B_PAWN, PS_B_KNIGHT, PS_B_BISHOP, PS_B_ROOK, PS_B_QUEEN, PS_KING, PS_NONE },
{ PS_NONE, PS_B_PAWN, PS_B_KNIGHT, PS_B_BISHOP, PS_B_ROOK, PS_B_QUEEN, PS_KING, PS_NONE,
PS_NONE, PS_W_PAWN, PS_W_KNIGHT, PS_W_BISHOP, PS_W_ROOK, PS_W_QUEEN, PS_KING, PS_NONE }
};

// Orient a square according to perspective (rotates by 180 for black)
Expand All @@ -69,17 +70,17 @@ namespace Stockfish::Eval::NNUE::Features {

public:
// Feature name
static constexpr const char* Name = "HalfKP(Friend)";
static constexpr const char* Name = "HalfKAv2(Friend)";

// Hash value embedded in the evaluation file
static constexpr std::uint32_t HashValue = 0x5D69D5B8u;
static constexpr std::uint32_t HashValue = 0x5f234cb8u;

// Number of feature dimensions
static constexpr IndexType Dimensions =
static_cast<IndexType>(SQUARE_NB) * static_cast<IndexType>(PS_NB);

// Maximum number of simultaneously active features. 30 because kins are not included.
static constexpr IndexType MaxActiveDimensions = 30;
// Maximum number of simultaneously active features.
static constexpr IndexType MaxActiveDimensions = 32;

// Get a list of indices for active features
static void append_active_indices(
Expand Down Expand Up @@ -107,4 +108,4 @@ namespace Stockfish::Eval::NNUE::Features {

} // namespace Stockfish::Eval::NNUE::Features

#endif // #ifndef NNUE_FEATURES_HALF_KP_H_INCLUDED
#endif // #ifndef NNUE_FEATURES_HALF_KA_V2_H_INCLUDED
Loading

0 comments on commit e8d64af

Please sign in to comment.