]> git.sesse.net Git - stockfish/blob - src/nnue/layers/affine_transform.h
Reuse existing functions to read/write array of network parameters
[stockfish] / src / nnue / layers / affine_transform.h
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 // Definition of layer AffineTransform of NNUE evaluation function
20
21 #ifndef NNUE_LAYERS_AFFINE_TRANSFORM_H_INCLUDED
22 #define NNUE_LAYERS_AFFINE_TRANSFORM_H_INCLUDED
23
24 #include <iostream>
25 #include <algorithm>
26 #include <type_traits>
27 #include "../nnue_common.h"
28 #include "simd.h"
29
30 /*
31   This file contains the definition for a fully connected layer (aka affine transform).
32   Two approaches are employed, depending on the sizes of the transform.
33
34   Approach 1:
35     - used when the PaddedInputDimensions >= 128
36     - uses AVX512 if possible
37     - processes inputs in batches of 2*InputSimdWidth
38       - so in batches of 128 for AVX512
39     - the weight blocks of size InputSimdWidth are transposed such that
40       access is sequential
41     - N columns of the weight matrix are processed a time, where N
42       depends on the architecture (the amount of registers)
43     - accumulate + hadd is used
44
45   Approach 2:
46     - used when the PaddedInputDimensions < 128
47     - does not use AVX512
48     - expected use-case is for when PaddedInputDimensions == 32 and InputDimensions <= 32.
49       - that's why AVX512 is hard to implement
50     - expected use-case is small layers
51       - not optimized as well as the approach 1
52     - inputs are processed in chunks of 4, weights are respectively transposed
53     - accumulation happens directly to int32s
54 */
55
56 namespace Stockfish::Eval::NNUE::Layers {
57
58 // Fallback implementation for older/other architectures.
59 // Identical for both approaches. Requires the input to be padded to at least 16 values.
60 #if !defined(USE_SSSE3)
61   template <IndexType InputDimensions, IndexType PaddedInputDimensions, IndexType OutputDimensions>
62   static void affine_transform_non_ssse3(std::int32_t* output, const std::int8_t* weights, const std::int32_t* biases, const std::uint8_t* input)
63   {
64 # if defined(USE_SSE2)
65     // At least a multiple of 16, with SSE2.
66     constexpr IndexType NumChunks = ceil_to_multiple<IndexType>(InputDimensions, 16) / 16;
67     const __m128i Zeros = _mm_setzero_si128();
68     const auto inputVector = reinterpret_cast<const __m128i*>(input);
69
70 # elif defined(USE_MMX)
71     constexpr IndexType NumChunks = ceil_to_multiple<IndexType>(InputDimensions, 8) / 8;
72     const __m64 Zeros = _mm_setzero_si64();
73     const auto inputVector = reinterpret_cast<const __m64*>(input);
74
75 # elif defined(USE_NEON_DOTPROD)
76     constexpr IndexType NumChunks = ceil_to_multiple<IndexType>(InputDimensions, 16) / 16;
77     const auto inputVector = reinterpret_cast<const int8x16_t*>(input);
78
79 # elif defined(USE_NEON)
80     constexpr IndexType NumChunks = ceil_to_multiple<IndexType>(InputDimensions, 16) / 16;
81     const auto inputVector = reinterpret_cast<const int8x8_t*>(input);
82 # endif
83
84     for (IndexType i = 0; i < OutputDimensions; ++i) {
85       const IndexType offset = i * PaddedInputDimensions;
86
87 # if defined(USE_SSE2)
88       __m128i sumLo = _mm_cvtsi32_si128(biases[i]);
89       __m128i sumHi = Zeros;
90       const auto row = reinterpret_cast<const __m128i*>(&weights[offset]);
91       for (IndexType j = 0; j < NumChunks; ++j) {
92         __m128i row_j = _mm_load_si128(&row[j]);
93         __m128i input_j = _mm_load_si128(&inputVector[j]);
94         __m128i extendedRowLo = _mm_srai_epi16(_mm_unpacklo_epi8(row_j, row_j), 8);
95         __m128i extendedRowHi = _mm_srai_epi16(_mm_unpackhi_epi8(row_j, row_j), 8);
96         __m128i extendedInputLo = _mm_unpacklo_epi8(input_j, Zeros);
97         __m128i extendedInputHi = _mm_unpackhi_epi8(input_j, Zeros);
98         __m128i productLo = _mm_madd_epi16(extendedRowLo, extendedInputLo);
99         __m128i productHi = _mm_madd_epi16(extendedRowHi, extendedInputHi);
100         sumLo = _mm_add_epi32(sumLo, productLo);
101         sumHi = _mm_add_epi32(sumHi, productHi);
102       }
103       __m128i sum = _mm_add_epi32(sumLo, sumHi);
104       __m128i sumHigh_64 = _mm_shuffle_epi32(sum, _MM_SHUFFLE(1, 0, 3, 2));
105       sum = _mm_add_epi32(sum, sumHigh_64);
106       __m128i sum_second_32 = _mm_shufflelo_epi16(sum, _MM_SHUFFLE(1, 0, 3, 2));
107       sum = _mm_add_epi32(sum, sum_second_32);
108       output[i] = _mm_cvtsi128_si32(sum);
109
110 # elif defined(USE_MMX)
111       __m64 sumLo = _mm_cvtsi32_si64(biases[i]);
112       __m64 sumHi = Zeros;
113       const auto row = reinterpret_cast<const __m64*>(&weights[offset]);
114       for (IndexType j = 0; j < NumChunks; ++j) {
115         __m64 row_j = row[j];
116         __m64 input_j = inputVector[j];
117         __m64 extendedRowLo = _mm_srai_pi16(_mm_unpacklo_pi8(row_j, row_j), 8);
118         __m64 extendedRowHi = _mm_srai_pi16(_mm_unpackhi_pi8(row_j, row_j), 8);
119         __m64 extendedInputLo = _mm_unpacklo_pi8(input_j, Zeros);
120         __m64 extendedInputHi = _mm_unpackhi_pi8(input_j, Zeros);
121         __m64 productLo = _mm_madd_pi16(extendedRowLo, extendedInputLo);
122         __m64 productHi = _mm_madd_pi16(extendedRowHi, extendedInputHi);
123         sumLo = _mm_add_pi32(sumLo, productLo);
124         sumHi = _mm_add_pi32(sumHi, productHi);
125       }
126       __m64 sum = _mm_add_pi32(sumLo, sumHi);
127       sum = _mm_add_pi32(sum, _mm_unpackhi_pi32(sum, sum));
128       output[i] = _mm_cvtsi64_si32(sum);
129
130 # elif defined(USE_NEON_DOTPROD)
131       int32x4_t sum = {biases[i]};
132       const auto row = reinterpret_cast<const int8x16_t*>(&weights[offset]);
133       for (IndexType j = 0; j < NumChunks; ++j) {
134         sum = vdotq_s32(sum, inputVector[j], row[j]);
135       }
136       output[i] = vaddvq_s32(sum);
137
138 # elif defined(USE_NEON)
139       int32x4_t sum = {biases[i]};
140       const auto row = reinterpret_cast<const int8x8_t*>(&weights[offset]);
141       for (IndexType j = 0; j < NumChunks; ++j) {
142         int16x8_t product = vmull_s8(inputVector[j * 2], row[j * 2]);
143         product = vmlal_s8(product, inputVector[j * 2 + 1], row[j * 2 + 1]);
144         sum = vpadalq_s16(sum, product);
145       }
146       output[i] = sum[0] + sum[1] + sum[2] + sum[3];
147
148 # else
149       std::int32_t sum = biases[i];
150       for (IndexType j = 0; j < InputDimensions; ++j) {
151         sum += weights[offset + j] * input[j];
152       }
153       output[i] = sum;
154 # endif
155     }
156
157 # if defined(USE_MMX)
158     _mm_empty();
159 # endif
160   }
161 #endif
162
163   template <IndexType InDims, IndexType OutDims, typename Enabled = void>
164   class AffineTransform;
165
166 #if defined (USE_AVX512)
167   constexpr IndexType LargeInputSize = 2 * 64;
168 #else
169   constexpr IndexType LargeInputSize = std::numeric_limits<IndexType>::max();
170 #endif
171
172   // A specialization for large inputs.
173   template <IndexType InDims, IndexType OutDims>
174   class AffineTransform<InDims, OutDims, std::enable_if_t<(ceil_to_multiple<IndexType>(InDims, MaxSimdWidth) >= LargeInputSize)>> {
175    public:
176     // Input/output type
177     using InputType = std::uint8_t;
178     using OutputType = std::int32_t;
179
180     // Number of input/output dimensions
181     static constexpr IndexType InputDimensions = InDims;
182     static constexpr IndexType OutputDimensions = OutDims;
183
184     static constexpr IndexType PaddedInputDimensions =
185       ceil_to_multiple<IndexType>(InputDimensions, MaxSimdWidth);
186     static constexpr IndexType PaddedOutputDimensions =
187       ceil_to_multiple<IndexType>(OutputDimensions, MaxSimdWidth);
188
189     using OutputBuffer = OutputType[PaddedOutputDimensions];
190
191     static_assert(PaddedInputDimensions >= LargeInputSize, "Something went wrong. This specialization should not have been chosen.");
192
193 #if defined (USE_AVX512)
194     static constexpr IndexType InputSimdWidth = 64;
195     static constexpr IndexType MaxNumOutputRegs = 16;
196 #elif defined (USE_AVX2)
197     static constexpr IndexType InputSimdWidth = 32;
198     static constexpr IndexType MaxNumOutputRegs = 8;
199 #elif defined (USE_SSSE3)
200     static constexpr IndexType InputSimdWidth = 16;
201     static constexpr IndexType MaxNumOutputRegs = 8;
202 #elif defined (USE_NEON_DOTPROD)
203     static constexpr IndexType InputSimdWidth = 16;
204     static constexpr IndexType MaxNumOutputRegs = 8;
205 #elif defined (USE_NEON)
206     static constexpr IndexType InputSimdWidth = 8;
207     static constexpr IndexType MaxNumOutputRegs = 8;
208 #else
209     // The fallback implementation will not have permuted weights.
210     // We define these to avoid a lot of ifdefs later.
211     static constexpr IndexType InputSimdWidth = 1;
212     static constexpr IndexType MaxNumOutputRegs = 1;
213 #endif
214
215     // A big block is a region in the weight matrix of the size [PaddedInputDimensions, NumOutputRegs].
216     // A small block is a region of size [InputSimdWidth, 1]
217
218     static constexpr IndexType NumOutputRegs = std::min(MaxNumOutputRegs, OutputDimensions);
219     static constexpr IndexType SmallBlockSize = InputSimdWidth;
220     static constexpr IndexType BigBlockSize = NumOutputRegs * PaddedInputDimensions;
221     static constexpr IndexType NumSmallBlocksInBigBlock = BigBlockSize / SmallBlockSize;
222     static constexpr IndexType NumSmallBlocksPerOutput = PaddedInputDimensions / SmallBlockSize;
223     static constexpr IndexType NumBigBlocks = OutputDimensions / NumOutputRegs;
224
225     static_assert(OutputDimensions % NumOutputRegs == 0);
226
227     // Hash value embedded in the evaluation file
228     static constexpr std::uint32_t get_hash_value(std::uint32_t prevHash) {
229       std::uint32_t hashValue = 0xCC03DAE4u;
230       hashValue += OutputDimensions;
231       hashValue ^= prevHash >> 1;
232       hashValue ^= prevHash << 31;
233       return hashValue;
234     }
235
236     /*
237       Transposes the small blocks within a block.
238       Effectively means that weights can be traversed sequentially during inference.
239     */
240     static IndexType get_weight_index(IndexType i)
241     {
242       const IndexType smallBlock = (i / SmallBlockSize) % NumSmallBlocksInBigBlock;
243       const IndexType smallBlockCol = smallBlock / NumSmallBlocksPerOutput;
244       const IndexType smallBlockRow = smallBlock % NumSmallBlocksPerOutput;
245       const IndexType bigBlock   = i / BigBlockSize;
246       const IndexType rest       = i % SmallBlockSize;
247
248       const IndexType idx =
249           bigBlock * BigBlockSize
250         + smallBlockRow * SmallBlockSize * NumOutputRegs
251         + smallBlockCol * SmallBlockSize
252         + rest;
253
254       return idx;
255     }
256
257     // Read network parameters
258     bool read_parameters(std::istream& stream) {
259       read_little_endian<BiasType>(stream, biases, OutputDimensions);
260
261       for (IndexType i = 0; i < OutputDimensions * PaddedInputDimensions; ++i)
262         weights[get_weight_index(i)] = read_little_endian<WeightType>(stream);
263
264       return !stream.fail();
265     }
266
267     // Write network parameters
268     bool write_parameters(std::ostream& stream) const {
269       write_little_endian<BiasType>(stream, biases, OutputDimensions);
270
271       for (IndexType i = 0; i < OutputDimensions * PaddedInputDimensions; ++i)
272         write_little_endian<WeightType>(stream, weights[get_weight_index(i)]);
273
274       return !stream.fail();
275     }
276
277     // Forward propagation
278     const OutputType* propagate(
279         const InputType* input, OutputType* output) const {
280
281 #if defined (USE_AVX512)
282       using acc_vec_t = __m512i;
283       using bias_vec_t = __m128i;
284       using weight_vec_t = __m512i;
285       using in_vec_t = __m512i;
286       #define vec_zero _mm512_setzero_si512()
287       #define vec_add_dpbusd_32x2 Simd::m512_add_dpbusd_epi32x2
288       #define vec_hadd Simd::m512_hadd
289       #define vec_haddx4 Simd::m512_haddx4
290 #elif defined (USE_AVX2)
291       using acc_vec_t = __m256i;
292       using bias_vec_t = __m128i;
293       using weight_vec_t = __m256i;
294       using in_vec_t = __m256i;
295       #define vec_zero _mm256_setzero_si256()
296       #define vec_add_dpbusd_32x2 Simd::m256_add_dpbusd_epi32x2
297       #define vec_hadd Simd::m256_hadd
298       #define vec_haddx4 Simd::m256_haddx4
299 #elif defined (USE_SSSE3)
300       using acc_vec_t = __m128i;
301       using bias_vec_t = __m128i;
302       using weight_vec_t = __m128i;
303       using in_vec_t = __m128i;
304       #define vec_zero _mm_setzero_si128()
305       #define vec_add_dpbusd_32x2 Simd::m128_add_dpbusd_epi32x2
306       #define vec_hadd Simd::m128_hadd
307       #define vec_haddx4 Simd::m128_haddx4
308 #elif defined (USE_NEON_DOTPROD)
309       using acc_vec_t = int32x4_t;
310       using bias_vec_t = int32x4_t;
311       using weight_vec_t = int8x16_t;
312       using in_vec_t = int8x16_t;
313       #define vec_zero {0}
314       #define vec_add_dpbusd_32x2 Simd::dotprod_m128_add_dpbusd_epi32x2
315       #define vec_hadd Simd::neon_m128_hadd
316       #define vec_haddx4 Simd::neon_m128_haddx4
317 #elif defined (USE_NEON)
318       using acc_vec_t = int32x4_t;
319       using bias_vec_t = int32x4_t;
320       using weight_vec_t = int8x8_t;
321       using in_vec_t = int8x8_t;
322       #define vec_zero {0}
323       #define vec_add_dpbusd_32x2 Simd::neon_m128_add_dpbusd_epi32x2
324       #define vec_hadd Simd::neon_m128_hadd
325       #define vec_haddx4 Simd::neon_m128_haddx4
326 #endif
327
328 #if defined (USE_SSSE3) || defined (USE_NEON)
329       const in_vec_t* invec = reinterpret_cast<const in_vec_t*>(input);
330
331       // Perform accumulation to registers for each big block
332       for (IndexType bigBlock = 0; bigBlock < NumBigBlocks; ++bigBlock)
333       {
334         acc_vec_t acc[NumOutputRegs] = { vec_zero };
335
336         // Each big block has NumOutputRegs small blocks in each "row", one per register.
337         // We process two small blocks at a time to save on one addition without VNNI.
338         for (IndexType smallBlock = 0; smallBlock < NumSmallBlocksPerOutput; smallBlock += 2)
339         {
340           const weight_vec_t* weightvec =
341             reinterpret_cast<const weight_vec_t*>(
342                 weights
343               + bigBlock * BigBlockSize
344               + smallBlock * SmallBlockSize * NumOutputRegs);
345
346           const in_vec_t in0 = invec[smallBlock + 0];
347           const in_vec_t in1 = invec[smallBlock + 1];
348
349           for (IndexType k = 0; k < NumOutputRegs; ++k)
350             vec_add_dpbusd_32x2(acc[k], in0, weightvec[k], in1, weightvec[k + NumOutputRegs]);
351         }
352
353         // Horizontally add all accumulators.
354         if constexpr (NumOutputRegs % 4 == 0)
355         {
356           bias_vec_t* outputvec = reinterpret_cast<bias_vec_t*>(output);
357           const bias_vec_t* biasvec = reinterpret_cast<const bias_vec_t*>(biases);
358
359           for (IndexType k = 0; k < NumOutputRegs; k += 4)
360           {
361             const IndexType idx = (bigBlock * NumOutputRegs + k) / 4;
362             outputvec[idx] = vec_haddx4(acc[k+0], acc[k+1], acc[k+2], acc[k+3], biasvec[idx]);
363           }
364         }
365         else
366         {
367           for (IndexType k = 0; k < NumOutputRegs; ++k)
368           {
369             const IndexType idx = (bigBlock * NumOutputRegs + k);
370             output[idx] = vec_hadd(acc[k], biases[idx]);
371           }
372         }
373       }
374
375 # undef vec_zero
376 # undef vec_add_dpbusd_32x2
377 # undef vec_hadd
378 # undef vec_haddx4
379 #else
380       // Use old implementation for the other architectures.
381       affine_transform_non_ssse3<
382         InputDimensions,
383         PaddedInputDimensions,
384         OutputDimensions>(output, weights, biases, input);
385
386 #endif
387
388       return output;
389     }
390
391    private:
392     using BiasType = OutputType;
393     using WeightType = std::int8_t;
394
395     alignas(CacheLineSize) BiasType biases[OutputDimensions];
396     alignas(CacheLineSize) WeightType weights[OutputDimensions * PaddedInputDimensions];
397   };
398
399   template <IndexType InDims, IndexType OutDims>
400   class AffineTransform<InDims, OutDims, std::enable_if_t<(ceil_to_multiple<IndexType>(InDims, MaxSimdWidth) < LargeInputSize)>> {
401    public:
402     // Input/output type
403     // Input/output type
404     using InputType = std::uint8_t;
405     using OutputType = std::int32_t;
406
407     // Number of input/output dimensions
408     static constexpr IndexType InputDimensions = InDims;
409     static constexpr IndexType OutputDimensions = OutDims;
410
411     static constexpr IndexType PaddedInputDimensions =
412       ceil_to_multiple<IndexType>(InputDimensions, MaxSimdWidth);
413     static constexpr IndexType PaddedOutputDimensions =
414       ceil_to_multiple<IndexType>(OutputDimensions, MaxSimdWidth);
415
416     using OutputBuffer = OutputType[PaddedOutputDimensions];
417
418     static_assert(PaddedInputDimensions < LargeInputSize, "Something went wrong. This specialization should not have been chosen.");
419
420 #if defined (USE_SSSE3)
421     static constexpr IndexType OutputSimdWidth = SimdWidth / 4;
422     static constexpr IndexType InputSimdWidth = SimdWidth;
423 #endif
424
425     // Hash value embedded in the evaluation file
426     static constexpr std::uint32_t get_hash_value(std::uint32_t prevHash) {
427       std::uint32_t hashValue = 0xCC03DAE4u;
428       hashValue += OutputDimensions;
429       hashValue ^= prevHash >> 1;
430       hashValue ^= prevHash << 31;
431       return hashValue;
432     }
433
434     static IndexType get_weight_index_scrambled(IndexType i)
435     {
436       return
437         (i / 4) % (PaddedInputDimensions / 4) * OutputDimensions * 4 +
438         i / PaddedInputDimensions * 4 +
439         i % 4;
440     }
441
442     static IndexType get_weight_index(IndexType i)
443     {
444 #if defined (USE_SSSE3)
445       return get_weight_index_scrambled(i);
446 #else
447       return i;
448 #endif
449     }
450
451     // Read network parameters
452     bool read_parameters(std::istream& stream) {
453       read_little_endian<BiasType>(stream, biases, OutputDimensions);
454       for (IndexType i = 0; i < OutputDimensions * PaddedInputDimensions; ++i)
455         weights[get_weight_index(i)] = read_little_endian<WeightType>(stream);
456
457       return !stream.fail();
458     }
459
460     // Write network parameters
461     bool write_parameters(std::ostream& stream) const {
462       write_little_endian<BiasType>(stream, biases, OutputDimensions);
463
464       for (IndexType i = 0; i < OutputDimensions * PaddedInputDimensions; ++i)
465         write_little_endian<WeightType>(stream, weights[get_weight_index(i)]);
466
467       return !stream.fail();
468     }
469     // Forward propagation
470     const OutputType* propagate(
471         const InputType* input, OutputType* output) const {
472
473 #if defined (USE_AVX2)
474       using vec_t = __m256i;
475       #define vec_setzero _mm256_setzero_si256
476       #define vec_set_32 _mm256_set1_epi32
477       #define vec_add_dpbusd_32 Simd::m256_add_dpbusd_epi32
478       #define vec_add_dpbusd_32x2 Simd::m256_add_dpbusd_epi32x2
479       #define vec_hadd Simd::m256_hadd
480 #elif defined (USE_SSSE3)
481       using vec_t = __m128i;
482       #define vec_setzero _mm_setzero_si128
483       #define vec_set_32 _mm_set1_epi32
484       #define vec_add_dpbusd_32 Simd::m128_add_dpbusd_epi32
485       #define vec_add_dpbusd_32x2 Simd::m128_add_dpbusd_epi32x2
486       #define vec_hadd Simd::m128_hadd
487 #endif
488
489 #if defined (USE_SSSE3)
490       const auto inputVector = reinterpret_cast<const vec_t*>(input);
491
492       static_assert(OutputDimensions % OutputSimdWidth == 0 || OutputDimensions == 1);
493
494       if constexpr (OutputDimensions % OutputSimdWidth == 0)
495       {
496         constexpr IndexType NumChunks = ceil_to_multiple<IndexType>(InputDimensions, 8) / 4;
497         constexpr IndexType NumRegs = OutputDimensions / OutputSimdWidth;
498
499         const auto input32 = reinterpret_cast<const std::int32_t*>(input);
500         const vec_t* biasvec = reinterpret_cast<const vec_t*>(biases);
501         vec_t acc[NumRegs];
502         for (IndexType k = 0; k < NumRegs; ++k)
503           acc[k] = biasvec[k];
504
505         for (IndexType i = 0; i < NumChunks; i += 2)
506         {
507           const vec_t in0 = vec_set_32(input32[i + 0]);
508           const vec_t in1 = vec_set_32(input32[i + 1]);
509           const auto col0 = reinterpret_cast<const vec_t*>(&weights[(i + 0) * OutputDimensions * 4]);
510           const auto col1 = reinterpret_cast<const vec_t*>(&weights[(i + 1) * OutputDimensions * 4]);
511           for (IndexType k = 0; k < NumRegs; ++k)
512             vec_add_dpbusd_32x2(acc[k], in0, col0[k], in1, col1[k]);
513         }
514
515         vec_t* outptr = reinterpret_cast<vec_t*>(output);
516         for (IndexType k = 0; k < NumRegs; ++k)
517           outptr[k] = acc[k];
518       }
519       else if constexpr (OutputDimensions == 1)
520       {
521         constexpr IndexType NumChunks = PaddedInputDimensions / SimdWidth;
522         vec_t sum0 = vec_setzero();
523         const auto row0 = reinterpret_cast<const vec_t*>(&weights[0]);
524
525         for (int j = 0; j < (int)NumChunks; ++j)
526         {
527           const vec_t in = inputVector[j];
528           vec_add_dpbusd_32(sum0, in, row0[j]);
529         }
530         output[0] = vec_hadd(sum0, biases[0]);
531       }
532
533 # undef vec_setzero
534 # undef vec_set_32
535 # undef vec_add_dpbusd_32
536 # undef vec_add_dpbusd_32x2
537 # undef vec_hadd
538 #else
539       // Use old implementation for the other architectures.
540       affine_transform_non_ssse3<
541         InputDimensions,
542         PaddedInputDimensions,
543         OutputDimensions>(output, weights, biases, input);
544 #endif
545
546       return output;
547     }
548
549    private:
550     using BiasType = OutputType;
551     using WeightType = std::int8_t;
552
553     alignas(CacheLineSize) BiasType biases[OutputDimensions];
554     alignas(CacheLineSize) WeightType weights[OutputDimensions * PaddedInputDimensions];
555   };
556
557 }  // namespace Stockfish::Eval::NNUE::Layers
558
559 #endif // #ifndef NNUE_LAYERS_AFFINE_TRANSFORM_H_INCLUDED