Read NNUE net faster
authorTomasz Sobczyk <tomasz.sobczyk1997@gmail.com>
Wed, 9 Jun 2021 09:21:55 +0000 (11:21 +0200)
committerStéphane Nicolet <cassio@free.fr>
Sun, 13 Jun 2021 07:39:03 +0000 (09:39 +0200)
Load feature transformer weights in bulk on little-endian machines.
This is in particular useful to test new nets with c-chess-cli,
see https://github.com/lucasart/c-chess-cli/issues/44

```
$ time ./stockfish.exe uci

Before : 0m0.914s
After  : 0m0.483s
```

No functional change

src/misc.h
src/nnue/nnue_common.h
src/nnue/nnue_feature_transformer.h
src/syzygy/tbprobe.cpp

index 59ca6e376bda6b12a736d61f3ee92b9c621d1b6e..dae37cd98f05a10921a5903cddb4f7e47f6db311 100644 (file)
@@ -66,9 +66,10 @@ std::ostream& operator<<(std::ostream&, SyncCout);
 #define sync_cout std::cout << IO_LOCK
 #define sync_endl std::endl << IO_UNLOCK
 
-// `ptr` must point to an array of size at least
-// `sizeof(T) * N + alignment` bytes, where `N` is the
-// number of elements in the array.
+
+// align_ptr_up() : get the first aligned element of an array.
+// ptr must point to an array of size at least `sizeof(T) * N + alignment` bytes,
+// where N is the number of elements in the array.
 template <uintptr_t Alignment, typename T>
 T* align_ptr_up(T* ptr)
 {
@@ -78,6 +79,12 @@ T* align_ptr_up(T* ptr)
   return reinterpret_cast<T*>(reinterpret_cast<char*>((ptrint + (Alignment - 1)) / Alignment * Alignment));
 }
 
+
+// IsLittleEndian : true if and only if the binary is compiled on a little endian machine
+static inline const union { uint32_t i; char c[4]; } Le = { 0x01020304 };
+static inline const bool IsLittleEndian = (Le.c[0] == 4);
+
+
 template <typename T>
 class ValueListInserter {
 public:
index dc70006120dca4bef8502d7e6428c63425f106ce..390f61c35ad4c1c7e0119fc3b83eed17e39a2fc3 100644 (file)
@@ -24,6 +24,8 @@
 #include <cstring>
 #include <iostream>
 
+#include "../misc.h"  // for IsLittleEndian
+
 #if defined(USE_AVX2)
 #include <immintrin.h>
 
@@ -86,37 +88,77 @@ namespace Stockfish::Eval::NNUE {
   // necessary to return a result with the byte ordering of the compiling machine.
   template <typename IntType>
   inline IntType read_little_endian(std::istream& stream) {
-
       IntType result;
-      std::uint8_t u[sizeof(IntType)];
-      typename std::make_unsigned<IntType>::type v = 0;
 
-      stream.read(reinterpret_cast<char*>(u), sizeof(IntType));
-      for (std::size_t i = 0; i < sizeof(IntType); ++i)
-          v = (v << 8) | u[sizeof(IntType) - i - 1];
+      if (IsLittleEndian)
+          stream.read(reinterpret_cast<char*>(&result), sizeof(IntType));
+      else
+      {
+          std::uint8_t u[sizeof(IntType)];
+          typename std::make_unsigned<IntType>::type v = 0;
+
+          stream.read(reinterpret_cast<char*>(u), sizeof(IntType));
+          for (std::size_t i = 0; i < sizeof(IntType); ++i)
+              v = (v << 8) | u[sizeof(IntType) - i - 1];
+
+          std::memcpy(&result, &v, sizeof(IntType));
+      }
 
-      std::memcpy(&result, &v, sizeof(IntType));
       return result;
   }
 
+  // write_little_endian() is our utility to write an integer (signed or unsigned, any size)
+  // to a stream in little-endian order. We swap the byte order before the write if
+  // necessary to always write in little endian order, independantly of the byte
+  // ordering of the compiling machine.
   template <typename IntType>
   inline void write_little_endian(std::ostream& stream, IntType value) {
 
-      std::uint8_t u[sizeof(IntType)];
-      typename std::make_unsigned<IntType>::type v = value;
-
-      std::size_t i = 0;
-      // if constexpr to silence the warning about shift by 8
-      if constexpr (sizeof(IntType) > 1) {
-        for (; i + 1 < sizeof(IntType); ++i) {
-            u[i] = v;
-            v >>= 8;
-        }
+      if (IsLittleEndian)
+          stream.write(reinterpret_cast<const char*>(&value), sizeof(IntType));
+      else
+      {
+          std::uint8_t u[sizeof(IntType)];
+          typename std::make_unsigned<IntType>::type v = value;
+
+          std::size_t i = 0;
+          // if constexpr to silence the warning about shift by 8
+          if constexpr (sizeof(IntType) > 1)
+          {
+            for (; i + 1 < sizeof(IntType); ++i)
+            {
+                u[i] = v;
+                v >>= 8;
+            }
+          }
+          u[i] = v;
+
+          stream.write(reinterpret_cast<char*>(u), sizeof(IntType));
       }
-      u[i] = v;
+  }
+
+  // read_little_endian(s, out, N) : read integers in bulk from a little indian stream.
+  // This reads N integers from stream s and put them in array out.
+  template <typename IntType>
+  inline void read_little_endian(std::istream& stream, IntType* out, std::size_t count) {
+      if (IsLittleEndian)
+          stream.read(reinterpret_cast<char*>(out), sizeof(IntType) * count);
+      else
+          for (std::size_t i = 0; i < count; ++i)
+              out[i] = read_little_endian<IntType>(stream);
+  }
 
-      stream.write(reinterpret_cast<char*>(u), sizeof(IntType));
+  // write_little_endian(s, out, N) : write integers in bulk to a little indian stream.
+  // This takes N integers from array values and writes them on stream s.
+  template <typename IntType>
+  inline void write_little_endian(std::ostream& stream, const IntType* values, std::size_t count) {
+      if (IsLittleEndian)
+          stream.write(reinterpret_cast<const char*>(values), sizeof(IntType) * count);
+      else
+          for (std::size_t i = 0; i < count; ++i)
+              write_little_endian<IntType>(stream, values[i]);
   }
+
 }  // namespace Stockfish::Eval::NNUE
 
 #endif // #ifndef NNUE_COMMON_H_INCLUDED
index 10b226b31130d802155ef34a273807b28470bd34..300ce3671c256e67c8ba5aec7a3582ae542aa7be 100644 (file)
@@ -24,8 +24,6 @@
 #include "nnue_common.h"
 #include "nnue_architecture.h"
 
-#include "../misc.h"
-
 #include <cstring> // std::memset()
 
 namespace Stockfish::Eval::NNUE {
@@ -150,23 +148,21 @@ namespace Stockfish::Eval::NNUE {
 
     // Read network parameters
     bool read_parameters(std::istream& stream) {
-      for (std::size_t i = 0; i < HalfDimensions; ++i)
-        biases[i] = read_little_endian<BiasType>(stream);
-      for (std::size_t i = 0; i < HalfDimensions * InputDimensions; ++i)
-        weights[i] = read_little_endian<WeightType>(stream);
-      for (std::size_t i = 0; i < PSQTBuckets * InputDimensions; ++i)
-        psqtWeights[i] = read_little_endian<PSQTWeightType>(stream);
+
+      read_little_endian<BiasType      >(stream, biases     , HalfDimensions                  );
+      read_little_endian<WeightType    >(stream, weights    , HalfDimensions * InputDimensions);
+      read_little_endian<PSQTWeightType>(stream, psqtWeights, PSQTBuckets    * InputDimensions);
+
       return !stream.fail();
     }
 
     // Write network parameters
     bool write_parameters(std::ostream& stream) const {
-      for (std::size_t i = 0; i < HalfDimensions; ++i)
-        write_little_endian<BiasType>(stream, biases[i]);
-      for (std::size_t i = 0; i < HalfDimensions * InputDimensions; ++i)
-        write_little_endian<WeightType>(stream, weights[i]);
-      for (std::size_t i = 0; i < PSQTBuckets * InputDimensions; ++i)
-        write_little_endian<PSQTWeightType>(stream, psqtWeights[i]);
+
+      write_little_endian<BiasType      >(stream, biases     , HalfDimensions                  );
+      write_little_endian<WeightType    >(stream, weights    , HalfDimensions * InputDimensions);
+      write_little_endian<PSQTWeightType>(stream, psqtWeights, PSQTBuckets    * InputDimensions);
+
       return !stream.fail();
     }
 
index 831c8259c538c103b6d4eb4a15fe1577ca8fbd79..a0ac727f600197be3516eaa902e9a632125778ee 100644 (file)
@@ -105,9 +105,6 @@ template<> inline void swap_endian<uint8_t>(uint8_t&) {}
 
 template<typename T, int LE> T number(void* addr)
 {
-    static const union { uint32_t i; char c[4]; } Le = { 0x01020304 };
-    static const bool IsLittleEndian = (Le.c[0] == 4);
-
     T v;
 
     if ((uintptr_t)addr & (alignof(T) - 1)) // Unaligned pointer (very rare)