Tweak fwdOut Multipliers

Description

Use different fwdOut multipliers for each subnetwork. These are SPSA tuned alongside L1 biases.

To apply the tuned parameters, I just scanned bytes in NNUE files and replaced matching patterns with them, because it was too tedious for me to calculate offsets.

patch-net.py
import argparse
import json
import os
import shutil
import subprocess

SOURCE = """
#ifndef PATCHER_H_
#define PATCHER_H_

#include <cstdint>

std::int32_t gBigL1BiasesPatch[8][16] = {{
{big}
}};

std::int32_t gSmallL1BiasesPatch[8][16] = {{
{small}
}};

#endif  // PATCHER_H_
""".lstrip()


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("spsa", type=argparse.FileType("r"))
    args = parser.parse_args()

    spsa = json.load(args.spsa)

    biases_big = [
        [round(float(spsa[f"gBigL1Biases[{i}][{j}]"]["value"])) for j in range(16)]
        for i in range(8)
    ]
    biases_small = [
        [round(float(spsa[f"gSmallL1Biases[{i}][{j}]"]["value"])) for j in range(16)]
        for i in range(8)
    ]

    replace = SOURCE.format(
        big=",\n".join(
            [f"    \x7B {', '.join([str(n) for n in l])} \x7D" for l in biases_big]
        ),
        small=",\n".join(
            [f"    \x7B {', '.join([str(n) for n in l])} \x7D" for l in biases_small]
        ),
    )

    with open("patcher.h", "w") as f:
        f.write(replace)

    print("Building patcher...")
    p = subprocess.Popen(["clang++", "-o", "patcher", "patcher.cc"])
    p.wait()

    print("Running patcher...")
    p = subprocess.Popen(["./patcher"])
    p.wait()
    os.unlink("patcher")

    def rename(filename: str):
        p = subprocess.Popen(["sha256sum", filename], stdout=subprocess.PIPE)
        out, _ = p.communicate()
        new_filename = f"nn-{out.decode()[:12]}.nnue"
        os.rename(filename, new_filename)
        print(f"  {filename} -> {new_filename}")
        return new_filename

    print("Renaming patched networks...")
    big = rename("nn-big.nnue")
    small = rename("nn-small.nnue")

    print("Copying patched networks...")
    shutil.copy(big, f"../../src/{big}")
    shutil.copy(small, f"../../src/{small}")

    fwdout_big = [round(float(spsa[f"gBigFwdOutMultiplier[{i}]"]["value"])) for i in range(8)]
    fwdout_small = [round(float(spsa[f"gSmallFwdOutMultiplier[{i}]"]["value"])) for i in range(8)]

    print()
    print(f"FwdOutMultipliersBig = \x7B {', '.join([str(n) for n in fwdout_big])} \x7D")
    print(f"FwdOutMultipliersSmall = \x7B {', '.join([str(n) for n in fwdout_small])} \x7D")


if __name__ == "__main__":
    main()
patcher.cc
#include <cstdint>
#include <cstdlib>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <iostream>

#include <fcntl.h>
#include <unistd.h>

#include <sys/mman.h>

#define NNUE_BIG_FILENAME   "nn-1111cefa1111.nnue"
#define NNUE_SMALL_FILENAME "nn-37f18f62d772.nnue"

std::int32_t gBigL1Biases[8][16] = {
    { -2684, 7895, -6, 708, 6843, -100, 3483, -1489, 3302, -944, -2445, 1705, -1231, 4758, -5838, 1246 },
    { -2846, 1390, -1762, 2838, -384, 2369, 253, 525, 1352, -661, -984, 5167, 3024, -758, -2553, 691 },
    { -837, 1910, 449, -468, 583, 2462, -215, 466, 3934, -1540, -3219, 1274, 1022, -707, 2660, 904 },
    { 577, 183, 1145, 4290, -2356, -128, -1378, 1396, 5405, -2113, -2265, -2564, -3378, -3846, 2157, 115 },
    { -191, 4973, 1095, 627, -3551, -2123, -1055, 2521, 765, 1947, -1466, -165, -2599, -1511, -4311, 826 },
    { -264, -1084, 4379, -5117, -4194, -1648, 1042, 3994, 3221, 1521, -2092, 4079, -1167, -1418, 6122, 789 },
    { -700, -720, 5141, -3246, -4768, -1825, 1422, 608, 905, -781, -3121, 3333, 4825, -2090, -2882, 1186 },
    { -864, 301, 3064, -2015, -2131, -1115, 1467, 3108, 2178, -961, 666, 986, -1327, -2337, -1242, 162 }
};

std::int32_t gSmallL1Biases[8][16] = {
    { 4520, -224, -745, 2226, -379, 873, -862, 1802, -90, -969, -2685, -6127, 1663, 1524, 1182, 2867 },
    { 3322, -134, 689, 1822, 3909, 1769, -1781, -1741, 951, 736, 165, -6250, 1622, -3435, 2048, 2256 },
    { 3874, -1638, 1939, 7323, 305, 3074, -2712, -5057, -927, 4995, -2754, -12267, -2169, -937, 3790, 1843 },
    { 9299, -1797, 1208, 6096, 2377, 1987, -331, -1677, 273, 3748, -3183, -13408, 70, 3943, -1714, 1009 },
    { 10780, -2128, 1986, 5180, 382, 1401, 713, -5299, -283, 2682, 341, -14512, 347, 5684, -49, 965 },
    { 6527, -2984, -25, 6793, -751, 1099, 1796, -2767, -1368, 2182, 119, -9668, 1234, 3580, -26, 851 },
    { 7046, -2980, -1083, 6516, -1700, 953, 645, -2145, -3258, 1983, -898, -10751, 396, 2700, 0, 1067 },
    { 4711, -2034, -1082, 3914, 331, 1114, 845, -1524, -2016, 2820, -2159, -7452, 1536, 2796, 1246, 1635 }
};

#include "patcher.h"

void *find_patterns(const void *memory, const void *pattern, std::size_t mem_size, std::size_t size) {
    for (std::size_t i = 0; i < mem_size - size; i++) {
        const void *p = static_cast<const char *>(memory) + i;

        if (memcmp(p, pattern, size) == 0)
            return const_cast<void *>(p);
    }

    return nullptr;
}

void *load(const std::string &filename, std::size_t &size) {
    int fd = open(filename.c_str(), O_RDONLY);

    if (fd == -1)
        return nullptr;

    size = std::filesystem::file_size(filename);
    void *data = mmap(nullptr, size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);

    if (data == MAP_FAILED) {
        close(fd);
        return nullptr;
    }

    if (read(fd, data, size) != size) {
        close(fd);
        munmap(data, size);
        return nullptr;
    }

    close(fd);

    return data;
}

bool save(const std::string &filename, const void *data, std::size_t size) {
    int fd = open(filename.c_str(), O_WRONLY | O_CREAT | O_TRUNC, 0644);

    if (fd == -1)
        return false;

    if (write(fd, data, size) != size) {
        unlink(filename.c_str());
        return false;
    }

    close(fd);
}

void patch(const std::string &filename, const std::string &output,
           std::int32_t patterns[8][16], std::int32_t patch[8][16]) {
    std::size_t size;
    const void *data = load(filename, size);

    if (data == nullptr) {
        std::cerr << "Failed to load " << filename << std::endl;
        exit(1);
    }

    for (int i = 0; i < 8; ++i) {
        void *ptr = find_patterns(data, patterns[i], size, 64);

        if (ptr == nullptr) {
            std::cerr << "Failed to patch " << filename << std::endl;
            exit(1);
        }

        memcpy(ptr, &patch[i], 64);
    }

    if (!save(output, data, size)) {
        std::cerr << "Failed to patch " << filename << std::endl;
        exit(1);
    }
}

int main() {
    patch("../../src/" NNUE_BIG_FILENAME, "nn-big.nnue", gBigL1Biases, gBigL1BiasesPatch);
    patch("../../src/" NNUE_SMALL_FILENAME, "nn-small.nnue", gSmallL1Biases, gSmallL1BiasesPatch);

    return 0;
}

Branches

Tests

SPSA #1

Test #1

Failed

Parameters obtained after 15k iterations.

Test #2

Failed

Parameters obtained after 33k iterations.

SPSA #2

The first SPSA tune session was not good, presumably due to too high ckc_{k} values. Following linrock and Viren's suggestion, the second SPSA test is launched with much lower cendc_{\text{end}} values (128).

Test #3

Failed

Parameters obtained after 15,445 iterations.

Test #4

Failed

Parameters obtained after 31,369 iterations.

Test #5

Passed

LTC

Failed

Parameters obtained after 150,000 iterations. In some games it double kills master, but the overall strength seems equal.

Test #6

Failed

Same L1 bias values as Test #5 but average fwdOutMultiplier value (584) is applied.

Test #7

Passed

LTC

Failed

Same L1 bias values as Test #5 but keep the original fwdOutMultiplier value. It also yields more double kills but not as effective as Test #5.

Last updated