Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce dual NNUE evaluation #4910

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 52 additions & 5 deletions src/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -791,6 +791,7 @@ help:
@echo "profile-build > standard build with profile-guided optimization"
@echo "build > skip profile-guided optimization"
@echo "net > Download the default nnue net"
@echo "net2 > Download the smaller nnue net"
@echo "strip > Strip executable"
@echo "install > Install executable"
@echo "clean > Clean up"
Expand Down Expand Up @@ -857,13 +858,13 @@ endif
clang-profile-use clang-profile-make FORCE \
format analyze

analyze: net config-sanity objclean
analyze: net net2 config-sanity objclean
$(MAKE) -k ARCH=$(ARCH) COMP=$(COMP) $(OBJS)

build: net config-sanity
build: net net2 config-sanity
$(MAKE) ARCH=$(ARCH) COMP=$(COMP) all

profile-build: net config-sanity objclean profileclean
profile-build: net net2 config-sanity objclean profileclean
@echo ""
@echo "Step 1/4. Building instrumented executable ..."
$(MAKE) ARCH=$(ARCH) COMP=$(COMP) $(profile_make)
Expand Down Expand Up @@ -907,7 +908,7 @@ profileclean:

# set up shell variables for the net stuff
netvariables:
$(eval nnuenet := $(shell grep EvalFileDefaultName evaluate.h | grep define | sed 's/.*\(nn-[a-z0-9]\{12\}.nnue\).*/\1/'))
$(eval nnuenet := $(shell grep EvalFileDefaultNameBig evaluate.h | grep define | sed 's/.*\(nn-[a-z0-9]\{12\}.nnue\).*/\1/'))
$(eval nnuedownloadurl1 := https://tests.stockfishchess.org/api/nn/$(nnuenet))
$(eval nnuedownloadurl2 := https://github.com/official-stockfish/networks/raw/master/$(nnuenet))
$(eval curl_or_wget := $(shell if hash curl 2>/dev/null; then echo "curl -skL"; elif hash wget 2>/dev/null; then echo "wget -qO-"; fi))
Expand Down Expand Up @@ -951,6 +952,52 @@ net: netvariables
fi; \
fi; \

netvariables2:
$(eval nnuenet := $(shell grep EvalFileDefaultNameSmall evaluate.h | grep define | sed 's/.*\(nn-[a-z0-9]\{12\}.nnue\).*/\1/'))
$(eval nnuedownloadurl1 := https://tests.stockfishchess.org/api/nn/$(nnuenet))
$(eval nnuedownloadurl2 := https://github.com/official-stockfish/networks/raw/master/$(nnuenet))
$(eval curl_or_wget := $(shell if hash curl 2>/dev/null; then echo "curl -skL"; elif hash wget 2>/dev/null; then echo "wget -qO-"; fi))
$(eval shasum_command := $(shell if hash shasum 2>/dev/null; then echo "shasum -a 256 "; elif hash sha256sum 2>/dev/null; then echo "sha256sum "; fi))

# evaluation network (nnue)
net2: netvariables2
@echo "Default net: $(nnuenet)"
@if [ "x$(curl_or_wget)" = "x" ]; then \
echo "Neither curl nor wget is installed. Install one of these tools unless the net has been downloaded manually"; \
fi
@if [ "x$(shasum_command)" = "x" ]; then \
echo "shasum / sha256sum not found, skipping net validation"; \
elif test -f "$(nnuenet)"; then \
if [ "$(nnuenet)" != "nn-"`$(shasum_command) $(nnuenet) | cut -c1-12`".nnue" ]; then \
echo "Removing invalid network"; rm -f $(nnuenet); \
fi; \
fi;
@for nnuedownloadurl in "$(nnuedownloadurl1)" "$(nnuedownloadurl2)"; do \
if test -f "$(nnuenet)"; then \
echo "$(nnuenet) available : OK"; break; \
else \
if [ "x$(curl_or_wget)" != "x" ]; then \
echo "Downloading $${nnuedownloadurl}"; $(curl_or_wget) $${nnuedownloadurl} > $(nnuenet);\
else \
echo "No net found and download not possible"; exit 1;\
fi; \
fi; \
if [ "x$(shasum_command)" != "x" ]; then \
if [ "$(nnuenet)" != "nn-"`$(shasum_command) $(nnuenet) | cut -c1-12`".nnue" ]; then \
echo "Removing failed download"; rm -f $(nnuenet); \
fi; \
fi; \
done
@if ! test -f "$(nnuenet)"; then \
echo "Failed to download $(nnuenet)."; \
fi;
@if [ "x$(shasum_command)" != "x" ]; then \
if [ "$(nnuenet)" = "nn-"`$(shasum_command) $(nnuenet) | cut -c1-12`".nnue" ]; then \
echo "Network validated"; break; \
fi; \
fi; \


format:
$(CLANG-FORMAT) -i $(SRCS) $(HEADERS) -style=file

Expand Down Expand Up @@ -1073,6 +1120,6 @@ icx-profile-use:
.depend: $(SRCS)
-@$(CXX) $(DEPENDFLAGS) -MM $(SRCS) > $@ 2> /dev/null

ifeq (, $(filter $(MAKECMDGOALS), help strip install clean net objclean profileclean config-sanity))
ifeq (, $(filter $(MAKECMDGOALS), help strip install clean net net2 objclean profileclean config-sanity))
-include .depend
endif
167 changes: 91 additions & 76 deletions src/evaluate.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -43,19 +43,25 @@
// const unsigned int gEmbeddedNNUESize; // the size of the embedded file
// Note that this does not work in Microsoft Visual Studio.
#if !defined(_MSC_VER) && !defined(NNUE_EMBEDDING_OFF)
INCBIN(EmbeddedNNUE, EvalFileDefaultName);
INCBIN(EmbeddedNNUEBig, EvalFileDefaultNameBig);
INCBIN(EmbeddedNNUESmall, EvalFileDefaultNameSmall);
#else
const unsigned char gEmbeddedNNUEData[1] = {0x0};
const unsigned char* const gEmbeddedNNUEEnd = &gEmbeddedNNUEData[1];
const unsigned int gEmbeddedNNUESize = 1;
const unsigned char gEmbeddedNNUEBigData[1] = {0x0};
const unsigned char* const gEmbeddedNNUEBigEnd = &gEmbeddedNNUEBigData[1];
const unsigned int gEmbeddedNNUEBigSize = 1;
const unsigned char gEmbeddedNNUESmallData[1] = {0x0};
const unsigned char* const gEmbeddedNNUESmallEnd = &gEmbeddedNNUESmallData[1];
const unsigned int gEmbeddedNNUESmallSize = 1;
#endif


namespace Stockfish {

namespace Eval {

std::string currentEvalFileName = "None";
std::string currentEvalFileName[2] = {"None", "None"};
const std::string EvFiles[2] = {"EvalFileBig", "EvalFileSmall"};
const std::string EvFileNames[2] = {EvalFileDefaultNameBig, EvalFileDefaultNameSmall};

// Tries to load a NNUE network at startup time, or when the engine
// receives a UCI command "setoption name EvalFile value nn-[a-z0-9]{12}.nnue"
Expand All @@ -66,9 +72,11 @@ std::string currentEvalFileName = "None";
// variable to have the engine search in a special directory in their distro.
void NNUE::init() {

std::string eval_file = std::string(Options["EvalFile"]);
if (eval_file.empty())
eval_file = EvalFileDefaultName;
for (bool small : {false, true})
{
std::string eval_file = std::string(Options[EvFiles[small]]);
if (eval_file.empty())
eval_file = EvFileNames[small];

#if defined(DEFAULT_NNUE_DIRECTORY)
std::vector<std::string> dirs = {"<internal>", "", CommandLine::binaryDirectory,
Expand All @@ -77,82 +85,79 @@ void NNUE::init() {
std::vector<std::string> dirs = {"<internal>", "", CommandLine::binaryDirectory};
#endif

for (const std::string& directory : dirs)
if (currentEvalFileName != eval_file)
for (const std::string& directory : dirs)
{
if (directory != "<internal>")
if (currentEvalFileName[small] != eval_file)
{
std::ifstream stream(directory + eval_file, std::ios::binary);
if (NNUE::load_eval(eval_file, stream))
currentEvalFileName = eval_file;
}

if (directory == "<internal>" && eval_file == EvalFileDefaultName)
{
// C++ way to prepare a buffer for a memory stream
class MemoryBuffer: public std::basic_streambuf<char> {
public:
MemoryBuffer(char* p, size_t n) {
setg(p, p, p + n);
setp(p, p + n);
}
};

MemoryBuffer buffer(
const_cast<char*>(reinterpret_cast<const char*>(gEmbeddedNNUEData)),
size_t(gEmbeddedNNUESize));
(void) gEmbeddedNNUEEnd; // Silence warning on unused variable

std::istream stream(&buffer);
if (NNUE::load_eval(eval_file, stream))
currentEvalFileName = eval_file;
if (directory != "<internal>")
{
std::ifstream stream(directory + eval_file, std::ios::binary);
if (NNUE::load_eval(eval_file, stream, small))
currentEvalFileName[small] = eval_file;
}

if (directory == "<internal>" && eval_file == EvFileNames[small])
{
// C++ way to prepare a buffer for a memory stream
class MemoryBuffer: public std::basic_streambuf<char> {
public:
MemoryBuffer(char* p, size_t n) {
setg(p, p, p + n);
setp(p, p + n);
}
};

MemoryBuffer buffer(
const_cast<char*>(reinterpret_cast<const char*>(
small ? gEmbeddedNNUESmallData : gEmbeddedNNUEBigData)),
size_t(small ? gEmbeddedNNUESmallSize : gEmbeddedNNUEBigSize));
(void) gEmbeddedNNUEBigEnd; // Silence warning on unused variable
(void) gEmbeddedNNUESmallEnd;

std::istream stream(&buffer);
if (NNUE::load_eval(eval_file, stream, small))
currentEvalFileName[small] = eval_file;
}
}
}
}
}

// Verifies that the last net used was loaded successfully
void NNUE::verify() {

std::string eval_file = std::string(Options["EvalFile"]);
if (eval_file.empty())
eval_file = EvalFileDefaultName;

if (currentEvalFileName != eval_file)
for (bool small : {false, true})
{
std::string eval_file = std::string(Options[EvFiles[small]]);
if (eval_file.empty())
eval_file = EvFileNames[small];

std::string msg1 =
"Network evaluation parameters compatible with the engine must be available.";
std::string msg2 = "The network file " + eval_file + " was not loaded successfully.";
std::string msg3 = "The UCI option EvalFile might need to specify the full path, "
"including the directory name, to the network file.";
std::string msg4 = "The default net can be downloaded from: "
"https://tests.stockfishchess.org/api/nn/"
+ std::string(EvalFileDefaultName);
std::string msg5 = "The engine will be terminated now.";

sync_cout << "info string ERROR: " << msg1 << sync_endl;
sync_cout << "info string ERROR: " << msg2 << sync_endl;
sync_cout << "info string ERROR: " << msg3 << sync_endl;
sync_cout << "info string ERROR: " << msg4 << sync_endl;
sync_cout << "info string ERROR: " << msg5 << sync_endl;

exit(EXIT_FAILURE);
}
if (currentEvalFileName[small] != eval_file)
{
std::string msg1 =
"Network evaluation parameters compatible with the engine must be available.";
std::string msg2 = "The network file " + eval_file + " was not loaded successfully.";
std::string msg3 = "The UCI option EvalFile might need to specify the full path, "
"including the directory name, to the network file.";
std::string msg4 = "The default net can be downloaded from: "
"https://tests.stockfishchess.org/api/nn/"
+ std::string(EvFileNames[small]);
std::string msg5 = "The engine will be terminated now.";

sync_cout << "info string ERROR: " << msg1 << sync_endl;
sync_cout << "info string ERROR: " << msg2 << sync_endl;
sync_cout << "info string ERROR: " << msg3 << sync_endl;
sync_cout << "info string ERROR: " << msg4 << sync_endl;
sync_cout << "info string ERROR: " << msg5 << sync_endl;

exit(EXIT_FAILURE);
}

sync_cout << "info string NNUE evaluation using " << eval_file << sync_endl;
}
sync_cout << "info string NNUE evaluation using " << eval_file << sync_endl;
}
}


// Returns a static, purely materialistic evaluation of the position from
// the point of view of the given color. It can be divided by PawnValue to get
// an approximation of the material advantage on the board in terms of pawns.
Value Eval::simple_eval(const Position& pos, Color c) {
return PawnValue * (pos.count<PAWN>(c) - pos.count<PAWN>(~c))
+ (pos.non_pawn_material(c) - pos.non_pawn_material(~c));
}


// Evaluate is the evaluator for the outer world. It returns a static evaluation
// of the position from the point of view of the side to move.
Value Eval::evaluate(const Position& pos) {
Expand All @@ -162,18 +167,28 @@ Value Eval::evaluate(const Position& pos) {
Value v;
Color stm = pos.side_to_move();
int shuffling = pos.rule50_count();
int simpleEval = simple_eval(pos, stm) + (int(pos.key() & 7) - 3);
int simpleEval = pos.simple_eval() + (int(pos.key() & 7) - 3);

int lazyThreshold = RookValue + KnightValue + 16 * shuffling * shuffling
+ abs(pos.this_thread()->bestValue)
+ abs(pos.this_thread()->rootSimpleEval);

bool lazy = abs(simpleEval) >= RookValue + KnightValue + 16 * shuffling * shuffling
+ abs(pos.this_thread()->bestValue)
+ abs(pos.this_thread()->rootSimpleEval);
bool lazy = abs(simpleEval) > lazyThreshold * 105 / 100;

if (lazy)
v = Value(simpleEval);
else
{
int nnueComplexity;
Value nnue = NNUE::evaluate(pos, true, &nnueComplexity);
int accBias = pos.state()->accumulatorBig.computed[0]
+ pos.state()->accumulatorBig.computed[1]
- pos.state()->accumulatorSmall.computed[0]
- pos.state()->accumulatorSmall.computed[1];

int nnueComplexity;
bool smallNet = abs(simpleEval) > lazyThreshold * (90 + accBias) / 100;

Value nnue = smallNet ? NNUE::evaluate<true>(pos, true, &nnueComplexity)
: NNUE::evaluate<false>(pos, true, &nnueComplexity);

Value optimism = pos.this_thread()->optimism[stm];

Expand Down Expand Up @@ -216,7 +231,7 @@ std::string Eval::trace(Position& pos) {
ss << std::showpoint << std::showpos << std::fixed << std::setprecision(2) << std::setw(15);

Value v;
v = NNUE::evaluate(pos, false);
v = NNUE::evaluate<false>(pos, false);
v = pos.side_to_move() == WHITE ? v : -v;
ss << "NNUE evaluation " << 0.01 * UCI::to_cp(v) << " (white side)\n";

Expand Down
6 changes: 3 additions & 3 deletions src/evaluate.h
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,15 @@ namespace Eval {

std::string trace(Position& pos);

Value simple_eval(const Position& pos, Color c);
Value evaluate(const Position& pos);

extern std::string currentEvalFileName;
extern std::string currentEvalFileName[2];

// 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-0000000000a0.nnue"
#define EvalFileDefaultNameBig "nn-0000000000a0.nnue"
#define EvalFileDefaultNameSmall "nn-9067e33176e8.nnue"

namespace NNUE {

Expand Down
Loading
Loading