chess
A recent development in NNUE architecture is "Threat Inputs" (TI). In addition to normal piece location information, threat inputs also provide additional feature inputs into the neural network. These additional features tell the network how pieces are being attacked or defended on the chess board in the current position.
I am unhappy with the current state of the art in TI feature indexing, which requires several look-up tables. I have been contemplating this issue for about a month, trying to come up with a computational way to compute feature indexes.. A recent realization allowed me to come up with a scheme to compute relative indexes for knight and king moves without large LUTs.
This was somewhat inspired (but not directly) by a previous article of mine.
We consider the bit patterns in 0x88 for the relative vector for each possible king and knight move:
| Hex | Binary | Move |
0x10 | 00010000 | King N |
0x11 | 00010001 | King NE |
0x01 | 00000001 | King E |
0xF1 | 11110001 | King SE |
0xF0 | 11110000 | King S |
0xEF | 11101111 | King SW |
0xFF | 11111111 | King W |
0x0F | 00001111 | King NW |
0x1F | 00011111 | Knight move |
0x21 | 00100001 | Knight move |
0x12 | 00010010 | Knight move |
0xF2 | 11110010 | Knight move |
0xE1 | 11100001 | Knight move |
0xDF | 11011111 | Knight move |
0xEE | 11101110 | Knight move |
0x0E | 00001110 | Knight move |
Our goal is to be able to assign a unique index from 0-7 for each possible king and knight move. I came to the realization last night that if we can select a discriminatory subset of the bits, we can compute indexes for the king and knight threats using that.
The initial obvious subset is bits 0, 1, 4 and 5. Unfortunately this results in bad collisions within the knight moves.
The subset I settled on was bits 0, 1, 4 and 6. This does have overlap between the king and knight moves, but that doesn't matter for our use case.
| Hex | Binary | Move | Bits | Bits (decimal) | Output index |
0x10 | 00010000 | King N | 0100 | 4 | 0 |
0x11 | 00010001 | King NE | 0101 | 5 | 1 |
0x01 | 00000001 | King E | 0001 | 1 | 2 |
0xF1 | 11110001 | King SE | 1101 | 13 | 3 |
0xF0 | 11110000 | King S | 1100 | 12 | 4 |
0xEF | 11101111 | King SW | 1011 | 11 | 5 |
0xFF | 11111111 | King W | 1111 | 15 | 6 |
0x0F | 00001111 | King NW | 0011 | 3 | 7 |
0x1F | 00011111 | Knight move | 0111 | 7 | 1 |
0x21 | 00100001 | Knight move | 0001 | 1 | 2 |
0x12 | 00010010 | Knight move | 0110 | 6 | 3 |
0xF2 | 11110010 | Knight move | 1110 | 14 | 4 |
0xE1 | 11100001 | Knight move | 1001 | 9 | 5 |
0xDF | 11011111 | Knight move | 1111 | 15 | 6 |
0xEE | 11101110 | Knight move | 1010 | 10 | 7 |
0x0E | 00001110 | Knight move | 0010 | 2 | 0 |
Notice that 0x21 collides with King E and 0xDF collides with King W, but also that this does not matter for our use case.
u8x16 coords_to_indexes(u8x16 coords, Square sq) {
// 00rrrfff → 0rrr0fff
const u8x16 expandedCoords {_mm_gf2p8affine_epi64_epi8(coords.raw, u64x2::splat(0x0102040008102000).raw, 0)};
const u8x16 expandedSq = u8x16::splat(expandSq(sq));
// 0x88 difference
const u8x16 diff = expandedCoords - expandedSq;
// extract subset of bits
// xdxcxxba → 0000dcba
const u8x16 bits {_mm_gf2p8affine_epi64_epi8(diff.raw, u64x2::splat(0x0102104000000000).raw, 0)};
// lookup table
const u8x16 perm {{0xFF, 2, 0, 7, 0, 1, 3, 1, 0xFF, 5, 7, 5, 4, 3, 4, 6}};
const u8x16 indexes = bits.swizzle(perm);
return indexes;
}
coords: d5 e5 e4 e3 d3 c3 c4 c5 e6 f5 f3 e2 c2 b3 b5 c6
expandedCoords: 43 44 34 24 23 22 32 42 54 45 25 14 12 21 41 52
expandedSq: 33 33 33 33 33 33 33 33 33 33 33 33 33 33 33 33
diff: 10 11 01 f1 f0 ef ff 0f 21 12 f2 e1 df ee 0e 1f
bits: 04 05 01 0d 0c 0b 0f 03 01 06 0e 09 0f 0a 02 07
indexes: 00 01 02 03 04 05 06 07 02 03 04 05 06 07 00 01