]> git.sesse.net Git - stockfish/blob - src/nnue/evaluate_nnue.cpp
Fix compilation after recent merge.
[stockfish] / src / nnue / evaluate_nnue.cpp
1 /*
2   Stockfish, a UCI chess playing engine derived from Glaurung 2.1
3   Copyright (C) 2004-2023 The Stockfish developers (see AUTHORS file)
4
5   Stockfish is free software: you can redistribute it and/or modify
6   it under the terms of the GNU General Public License as published by
7   the Free Software Foundation, either version 3 of the License, or
8   (at your option) any later version.
9
10   Stockfish is distributed in the hope that it will be useful,
11   but WITHOUT ANY WARRANTY; without even the implied warranty of
12   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13   GNU General Public License for more details.
14
15   You should have received a copy of the GNU General Public License
16   along with this program.  If not, see <http://www.gnu.org/licenses/>.
17 */
18
19 // Code for calculating NNUE evaluation function
20
21 #include "evaluate_nnue.h"
22
23 #include <cmath>
24 #include <cstdlib>
25 #include <cstring>
26 #include <fstream>
27 #include <iomanip>
28 #include <iostream>
29 #include <sstream>
30 #include <string_view>
31
32 #include "../evaluate.h"
33 #include "../misc.h"
34 #include "../position.h"
35 #include "../types.h"
36 #include "../uci.h"
37 #include "nnue_accumulator.h"
38 #include "nnue_common.h"
39
40 namespace Stockfish::Eval::NNUE {
41
42 // Input feature converter
43 LargePagePtr<FeatureTransformer> featureTransformer;
44
45 // Evaluation function
46 AlignedPtr<Network> network[LayerStacks];
47
48 // Evaluation function file name
49 std::string fileName;
50 std::string netDescription;
51
52 namespace Detail {
53
54 // Initialize the evaluation function parameters
55 template<typename T>
56 void initialize(AlignedPtr<T>& pointer) {
57
58     pointer.reset(reinterpret_cast<T*>(std_aligned_alloc(alignof(T), sizeof(T))));
59     std::memset(pointer.get(), 0, sizeof(T));
60 }
61
62 template<typename T>
63 void initialize(LargePagePtr<T>& pointer) {
64
65     static_assert(alignof(T) <= 4096,
66                   "aligned_large_pages_alloc() may fail for such a big alignment requirement of T");
67     pointer.reset(reinterpret_cast<T*>(aligned_large_pages_alloc(sizeof(T))));
68     std::memset(pointer.get(), 0, sizeof(T));
69 }
70
71 // Read evaluation function parameters
72 template<typename T>
73 bool read_parameters(std::istream& stream, T& reference) {
74
75     std::uint32_t header;
76     header = read_little_endian<std::uint32_t>(stream);
77     if (!stream || header != T::get_hash_value())
78         return false;
79     return reference.read_parameters(stream);
80 }
81
82 // Write evaluation function parameters
83 template<typename T>
84 bool write_parameters(std::ostream& stream, const T& reference) {
85
86     write_little_endian<std::uint32_t>(stream, T::get_hash_value());
87     return reference.write_parameters(stream);
88 }
89
90 }  // namespace Detail
91
92
93 // Initialize the evaluation function parameters
94 static void initialize() {
95
96     Detail::initialize(featureTransformer);
97     for (std::size_t i = 0; i < LayerStacks; ++i)
98         Detail::initialize(network[i]);
99 }
100
101 // Read network header
102 static bool read_header(std::istream& stream, std::uint32_t* hashValue, std::string* desc) {
103     std::uint32_t version, size;
104
105     version    = read_little_endian<std::uint32_t>(stream);
106     *hashValue = read_little_endian<std::uint32_t>(stream);
107     size       = read_little_endian<std::uint32_t>(stream);
108     if (!stream || version != Version)
109         return false;
110     desc->resize(size);
111     stream.read(&(*desc)[0], size);
112     return !stream.fail();
113 }
114
115 // Write network header
116 static bool write_header(std::ostream& stream, std::uint32_t hashValue, const std::string& desc) {
117     write_little_endian<std::uint32_t>(stream, Version);
118     write_little_endian<std::uint32_t>(stream, hashValue);
119     write_little_endian<std::uint32_t>(stream, std::uint32_t(desc.size()));
120     stream.write(&desc[0], desc.size());
121     return !stream.fail();
122 }
123
124 // Read network parameters
125 static bool read_parameters(std::istream& stream) {
126
127     std::uint32_t hashValue;
128     if (!read_header(stream, &hashValue, &netDescription))
129         return false;
130     if (hashValue != HashValue)
131         return false;
132     if (!Detail::read_parameters(stream, *featureTransformer))
133         return false;
134     for (std::size_t i = 0; i < LayerStacks; ++i)
135         if (!Detail::read_parameters(stream, *(network[i])))
136             return false;
137     return stream && stream.peek() == std::ios::traits_type::eof();
138 }
139
140 // Write network parameters
141 static bool write_parameters(std::ostream& stream) {
142
143     if (!write_header(stream, HashValue, netDescription))
144         return false;
145     if (!Detail::write_parameters(stream, *featureTransformer))
146         return false;
147     for (std::size_t i = 0; i < LayerStacks; ++i)
148         if (!Detail::write_parameters(stream, *(network[i])))
149             return false;
150     return bool(stream);
151 }
152
153 void hint_common_parent_position(const Position& pos) {
154     featureTransformer->hint_common_access(pos);
155 }
156
157 // Evaluation function. Perform differential calculation.
158 Value evaluate(const Position& pos, bool adjusted, int* complexity) {
159
160     // We manually align the arrays on the stack because with gcc < 9.3
161     // overaligning stack variables with alignas() doesn't work correctly.
162
163     constexpr uint64_t alignment = CacheLineSize;
164     constexpr int      delta     = 24;
165
166 #if defined(ALIGNAS_ON_STACK_VARIABLES_BROKEN)
167     TransformedFeatureType
168       transformedFeaturesUnaligned[FeatureTransformer::BufferSize
169                                    + alignment / sizeof(TransformedFeatureType)];
170
171     auto* transformedFeatures = align_ptr_up<alignment>(&transformedFeaturesUnaligned[0]);
172 #else
173     alignas(alignment) TransformedFeatureType transformedFeatures[FeatureTransformer::BufferSize];
174 #endif
175
176     ASSERT_ALIGNED(transformedFeatures, alignment);
177
178     const int  bucket     = (pos.count<ALL_PIECES>() - 1) / 4;
179     const auto psqt       = featureTransformer->transform(pos, transformedFeatures, bucket);
180     const auto positional = network[bucket]->propagate(transformedFeatures);
181
182     if (complexity)
183         *complexity = std::abs(psqt - positional) / OutputScale;
184
185     // Give more value to positional evaluation when adjusted flag is set
186     if (adjusted)
187         return static_cast<Value>(((1024 - delta) * psqt + (1024 + delta) * positional)
188                                   / (1024 * OutputScale));
189     else
190         return static_cast<Value>((psqt + positional) / OutputScale);
191 }
192
193 struct NnueEvalTrace {
194     static_assert(LayerStacks == PSQTBuckets);
195
196     Value       psqt[LayerStacks];
197     Value       positional[LayerStacks];
198     std::size_t correctBucket;
199 };
200
201 static NnueEvalTrace trace_evaluate(const Position& pos) {
202
203     // We manually align the arrays on the stack because with gcc < 9.3
204     // overaligning stack variables with alignas() doesn't work correctly.
205     constexpr uint64_t alignment = CacheLineSize;
206
207 #if defined(ALIGNAS_ON_STACK_VARIABLES_BROKEN)
208     TransformedFeatureType
209       transformedFeaturesUnaligned[FeatureTransformer::BufferSize
210                                    + alignment / sizeof(TransformedFeatureType)];
211
212     auto* transformedFeatures = align_ptr_up<alignment>(&transformedFeaturesUnaligned[0]);
213 #else
214     alignas(alignment) TransformedFeatureType transformedFeatures[FeatureTransformer::BufferSize];
215 #endif
216
217     ASSERT_ALIGNED(transformedFeatures, alignment);
218
219     NnueEvalTrace t{};
220     t.correctBucket = (pos.count<ALL_PIECES>() - 1) / 4;
221     for (IndexType bucket = 0; bucket < LayerStacks; ++bucket)
222     {
223         const auto materialist = featureTransformer->transform(pos, transformedFeatures, bucket);
224         const auto positional  = network[bucket]->propagate(transformedFeatures);
225
226         t.psqt[bucket]       = static_cast<Value>(materialist / OutputScale);
227         t.positional[bucket] = static_cast<Value>(positional / OutputScale);
228     }
229
230     return t;
231 }
232
233 constexpr std::string_view PieceToChar(" PNBRQK  pnbrqk");
234
235
236 // Converts a Value into (centi)pawns and writes it in a buffer.
237 // The buffer must have capacity for at least 5 chars.
238 static void format_cp_compact(Value v, char* buffer) {
239
240     buffer[0] = (v < 0 ? '-' : v > 0 ? '+' : ' ');
241
242     int cp = std::abs(UCI::to_cp(v));
243     if (cp >= 10000)
244     {
245         buffer[1] = '0' + cp / 10000;
246         cp %= 10000;
247         buffer[2] = '0' + cp / 1000;
248         cp %= 1000;
249         buffer[3] = '0' + cp / 100;
250         buffer[4] = ' ';
251     }
252     else if (cp >= 1000)
253     {
254         buffer[1] = '0' + cp / 1000;
255         cp %= 1000;
256         buffer[2] = '0' + cp / 100;
257         cp %= 100;
258         buffer[3] = '.';
259         buffer[4] = '0' + cp / 10;
260     }
261     else
262     {
263         buffer[1] = '0' + cp / 100;
264         cp %= 100;
265         buffer[2] = '.';
266         buffer[3] = '0' + cp / 10;
267         cp %= 10;
268         buffer[4] = '0' + cp / 1;
269     }
270 }
271
272
273 // Converts a Value into pawns, always keeping two decimals
274 static void format_cp_aligned_dot(Value v, std::stringstream& stream) {
275
276     const double pawns = std::abs(0.01 * UCI::to_cp(v));
277
278     stream << (v < 0   ? '-'
279                : v > 0 ? '+'
280                        : ' ')
281            << std::setiosflags(std::ios::fixed) << std::setw(6) << std::setprecision(2) << pawns;
282 }
283
284
285 // Returns a string with the value of each piece on a board,
286 // and a table for (PSQT, Layers) values bucket by bucket.
287 std::string trace(Position& pos) {
288
289     std::stringstream ss;
290
291     char board[3 * 8 + 1][8 * 8 + 2];
292     std::memset(board, ' ', sizeof(board));
293     for (int row = 0; row < 3 * 8 + 1; ++row)
294         board[row][8 * 8 + 1] = '\0';
295
296     // A lambda to output one box of the board
297     auto writeSquare = [&board](File file, Rank rank, Piece pc, Value value) {
298         const int x = int(file) * 8;
299         const int y = (7 - int(rank)) * 3;
300         for (int i = 1; i < 8; ++i)
301             board[y][x + i] = board[y + 3][x + i] = '-';
302         for (int i = 1; i < 3; ++i)
303             board[y + i][x] = board[y + i][x + 8] = '|';
304         board[y][x] = board[y][x + 8] = board[y + 3][x + 8] = board[y + 3][x] = '+';
305         if (pc != NO_PIECE)
306             board[y + 1][x + 4] = PieceToChar[pc];
307         if (value != VALUE_NONE)
308             format_cp_compact(value, &board[y + 2][x + 2]);
309     };
310
311     // We estimate the value of each piece by doing a differential evaluation from
312     // the current base eval, simulating the removal of the piece from its square.
313     Value base = evaluate(pos);
314     base       = pos.side_to_move() == WHITE ? base : -base;
315
316     for (File f = FILE_A; f <= FILE_H; ++f)
317         for (Rank r = RANK_1; r <= RANK_8; ++r)
318         {
319             Square sq = make_square(f, r);
320             Piece  pc = pos.piece_on(sq);
321             Value  v  = VALUE_NONE;
322
323             if (pc != NO_PIECE && type_of(pc) != KING)
324             {
325                 auto st = pos.state();
326
327                 pos.remove_piece(sq);
328                 st->accumulator.computed[WHITE] = false;
329                 st->accumulator.computed[BLACK] = false;
330
331                 Value eval = evaluate(pos);
332                 eval       = pos.side_to_move() == WHITE ? eval : -eval;
333                 v          = base - eval;
334
335                 pos.put_piece(pc, sq);
336                 st->accumulator.computed[WHITE] = false;
337                 st->accumulator.computed[BLACK] = false;
338             }
339
340             writeSquare(f, r, pc, v);
341         }
342
343     ss << " NNUE derived piece values:\n";
344     for (int row = 0; row < 3 * 8 + 1; ++row)
345         ss << board[row] << '\n';
346     ss << '\n';
347
348     auto t = trace_evaluate(pos);
349
350     ss << " NNUE network contributions "
351        << (pos.side_to_move() == WHITE ? "(White to move)" : "(Black to move)") << std::endl
352        << "+------------+------------+------------+------------+\n"
353        << "|   Bucket   |  Material  | Positional |   Total    |\n"
354        << "|            |   (PSQT)   |  (Layers)  |            |\n"
355        << "+------------+------------+------------+------------+\n";
356
357     for (std::size_t bucket = 0; bucket < LayerStacks; ++bucket)
358     {
359         ss << "|  " << bucket << "        ";
360         ss << " |  ";
361         format_cp_aligned_dot(t.psqt[bucket], ss);
362         ss << "  "
363            << " |  ";
364         format_cp_aligned_dot(t.positional[bucket], ss);
365         ss << "  "
366            << " |  ";
367         format_cp_aligned_dot(t.psqt[bucket] + t.positional[bucket], ss);
368         ss << "  "
369            << " |";
370         if (bucket == t.correctBucket)
371             ss << " <-- this bucket is used";
372         ss << '\n';
373     }
374
375     ss << "+------------+------------+------------+------------+\n";
376
377     return ss.str();
378 }
379
380
381 // Load eval, from a file stream or a memory stream
382 bool load_eval(std::string name, std::istream& stream) {
383
384     initialize();
385     fileName = name;
386     return read_parameters(stream);
387 }
388
389 // Save eval, to a file stream or a memory stream
390 bool save_eval(std::ostream& stream) {
391
392     if (fileName.empty())
393         return false;
394
395     return write_parameters(stream);
396 }
397
398 // Save eval, to a file given by its name
399 bool save_eval(const std::optional<std::string>& filename) {
400
401     std::string actualFilename;
402     std::string msg;
403
404     if (filename.has_value())
405         actualFilename = filename.value();
406     else
407     {
408         if (currentEvalFileName != EvalFileDefaultName)
409         {
410             msg = "Failed to export a net. "
411                   "A non-embedded net can only be saved if the filename is specified";
412
413             sync_cout << msg << sync_endl;
414             return false;
415         }
416         actualFilename = EvalFileDefaultName;
417     }
418
419     std::ofstream stream(actualFilename, std::ios_base::binary);
420     bool          saved = save_eval(stream);
421
422     msg = saved ? "Network saved successfully to " + actualFilename : "Failed to export a net";
423
424     sync_cout << msg << sync_endl;
425     return saved;
426 }
427
428
429 }  // namespace Stockfish::Eval::NNUE