643. FIDE & Google Efficient Chess AI Challenge | fide-google-efficiency-chess-ai-challenge
源代码:https://github.com/linrock/minifish
特别感谢团队:Approvers, "Fix the bugs?" 他们的顶尖表现,以及激发我加入的兴趣。
起初,我没指望在 64kb 二进制大小限制下,nnue 能比手工 crafted 评估(HCE)好那么多。直到 12 月份我看到 Approvers 团队在排行榜上遥遥领先,我才意识到 nnue 具有巨大的潜力。
我大部分时间都花在 nnue 研究上,因为我把这次比赛视为提高 ML 水平的有趣方式。我的引擎代码最终比前 3 名中的其他引擎更基础、更简单。大部分实力隐藏在神经网络权重中。
我从 Cfish 开始,因为我假设在 C 代码库中最小化二进制大小会比 C++ 更容易。现代 Stockfish 针对长时间控制下的强 nnue(约 70mb 压缩权重)评估进行了优化。近年来的许多改进与受 64kb 大小限制的较弱评估无关,并且表现会更差。
早期,我首先专注于删除不必要的代码(表库等)以减小二进制大小,然后浏览 Stockfish 的 git 提交历史,决定尝试移植哪些补丁。2020 年引入的原始 nnue 架构太复杂,无法适应大小限制,所以我完全删除了代码,在使用 HCE 进行搜索补丁方面取得了初步进展,然后再后来研究如何从头开始让简单的 nnue 工作。
由于之前使用 Stockfish 工作,我已经有了一个运行中的 fishtest 开发服务器。我修改了它以适应 Cfish,主要用于 SPRT。Elo 测量对于衡量引擎实力的进展至关重要。
对于测试基本功能,例如让 nnue 完全工作,结合 ./cfish bench 和使用 fastchess 在固定节点下进行本地 Elo 测量,非常适合快速检查。
我使用脚本来构建 .tar.gz 提交,并密切关注各种可用工具(gzip, zopfli, bz2, xz, 7z)的压缩大小。压缩大小帮助决定 nnue 的方向,因为在大小和强度之间需要做出权衡。
我发现查看权重图像对于制定计划很重要。我使用的网络是标准的 768 国际象棋输入(64 格 x 6 种棋子类型 x 2 种颜色),双视角,具有水平王镜像,以及单个隐藏层。
特征变换器权重与特定类型的棋子存在于某个格子上相关联。以下是“我们的兵”权重的示例:

Y 轴代表格子,从 A1 = 0, A2 = 1, … H8 = 63。第一行和最后一行为零,因为兵不能存在于第 1 和第 8 横排。虽然完全可以移除 32 个兵输入(我们的兵 16 个,对方的兵 16 个),但我发现简单地将未使用的权重归零可以提高压缩性,而不会增加代码复杂性。
X 轴代表特征变换器中神经元的索引。这里有 64 个神经元。字节从左到右,从上到下排序。具有高相关性的顺序数据压缩得更多,因此转置这些权重可以通过提高压缩性来减小二进制大小。
如果我们绘制所有按棋子类型和视角(我们的,他们的)分组的特征变换器权重,更多的概念变得明显。
从上到下的每一行显示按以下顺序排列的棋子类型的权重:
兵,马,象,车,后,王
左列是“我们的棋子”,右列是“对方的棋子”

每条垂直线是一个特征向量,编码跨棋子类型的一些抽象评估概念。
左下角是“我们的王”,其中的水平零条纹代表水平王镜像。可以创建网络,使得当我们的王在垂直中心(D 和 E 文件之间)移动时,我们视角的权重被镜像以保持王在棋盘的一半。
这有效地将王的输入数量减少了 32 个,这进一步提高了压缩性,因为我们可以将更多未使用的权重归零。它还通过提高相对于我们王的特征相关性来增强网络的强度。
我使用 bullet 创建具有简单架构的网络。网络实力最重要的方面在于数据选择和处理。我修改了 Primer 以改进数据过滤,并用它过滤我之前为训练 Stockfish 网络创建的源数据。
有了好的数据,一个简单的网络甚至可以比 HCE 强很多(+100 elo),即使没有实现 nnue 的易更新(UE)部分。
最好的原始训练数据来源是 Leela Chess Zero (lc0) 强化学习训练运行。特别是,较小的 ResNet T77 和 T79 网络产生了最好的 nnue 数据,而从较大网络生成的数据表现较差。我从几年前转换为 binpack 格式用于 nnue 训练的数据开始。
首先从头开始在较弱的数据上训练,然后在较强的数据上训练,比直接在最强的数据上从头开始训练能产生更强的网络。
我使用了从头开始的 2 阶段训练过程:
每个训练阶段使用 AdamW 优化器,LR 计划线性衰减到零。阶段 2 训练从阶段 1 结束时的检查点恢复。
由于即使所有参数相同,网络强度也存在差异,从头开始多次重新运行训练会产生更强的网络。
完整的训练配置可以在这里找到。
nnue-pytorch 训练器创建最强的 nnue 网络。其部分实力在于数据加载器中实现的数据跳过方法。这使得能够使用高度压缩的 binpack 数据,同时以训练速度换取整体网络 elo。
如果我们查看按每个位置的棋子数量分组的 Leela binpack 训练数据的直方图,我们会看到分布不均匀:

32 处的大数量峰值是由于训练游戏都从起始位置开始。在开局之后,由于在此阶段棋子捕获后重新捕获很常见,因此偶数个棋子的位置要多得多。
由于位置数量达到数百亿,预处理整个数据集将非常计算密集。通过在训练时使用随机跳过针对训练批次中棋子数量的均匀分布,我们在训练时 flattening 了不均匀的分布。此前发现这能提高 Stockfish 网络的强度,在这里也被证明是有效的。
由于 nnue-pytorch 数据加载器和 primer 都是用 C++ 实现的,因此通过复制/粘贴很容易将数据加载器代码移植到 primer。
我准备了来自数据集多次迭代的数据,每次迭代应用了一些过滤方法。当与顺序数据加载器一起使用时,这些结果数据集可以堆叠在一起,以模拟通过数据集的多次随机遍历:
应用了一些过滤方法:
16 位特征变换器权重的直方图显示,在特定的量化常数 QA = 101 和 QB = 160 的选择下,大多数都在 8 位范围内。

由于大多数 16 位特征变换器权重可以 fit 在 8 位中,因此值得考虑可变长度压缩(如 LEB128)作为减小网络大小的无损压缩方法。然而,我发现它的压缩率仍然不够高。
特征变换器(QA)和输出层(QB)中量化常数的选择会影响网络权重中整数值的范围。量化越少,信息越多,这会提高强度,但需要更多空间。
当按棋子类型分组时,事实证明后的权重具有最高的范围,而某些棋子类型的权重无需修改即可 fit 在 8 位中。这意味着可以将 16 位权重 fit 在 8 位中,这既避免了直接量化为 8 位导致的强度损失,又通过将权重存储为 8 位数字最大化了压缩。

在每组棋子权重内,只要有 256 个或更少的唯一值,16 位权重就可以用 8 位表示而无需量化。如果唯一权重的数量不是多很多,例如“我们的后”有 300 个唯一权重,可以将类似的低频权重分组为更少的唯一值,然后存储为任何未使用的 8 位数字。
这些未使用的 8 位数字到 16 位权重的映射可以存储在 C++ 数组中,以便在加载权重时反转映射。这样,16 位特征变换器可以压缩为 8 位而不会导致 elo 回归。
默认情况下,量化常数是 QA = 255 和 QB = 64。我最终选择了 QA = 101 和 QB = 160,因为那是我能适应大小限制的最大 QA。比赛结束后不久,我意识到我忽略了一些想法,这些想法本可以将二进制大小再减少 1kb+,但事情就是这样。
7zip 导致了最好的压缩率。虽然 .7z 提交不能直接上传,但我看到比赛环境的 docker 容器镜像中可用 7z,所以我用 7z 压缩引擎二进制文件,并在 main.py 中运行时解压缩,同时使用 zopfli 进行外部 .tar.gz 压缩。
直到截止日期前 1.5 周,我终于有了一个压缩到小于 64kb 的 nnue 提交。我利用剩余时间测量比赛运行环境并尝试对其进行优化。由于排行榜分数有噪声,我主要查看与 Approvers 和 "Fix the bugs?" 的对抗胜率来量化变化的影响。
通过测量不同哈希大小对错误损失的影响,我注意到 5mb RAM 限制是一个梯度,错误损失随着 RAM 使用量的增加而增加。由于增加哈希大小可以提高 elo,我进行了几次测量来决定最终提交使用什么哈希大小。
即使在时间低时立即移动,由于简单延迟是随机的,也会发生时间损失。我最终没有做任何事情来最小化时间损失,因为我无法有效测量对策的 elo 影响。
时间增量很早就宣布了,但在截止日期前一周没有后续消息,所以我假设它不会发生。我使用 SPSA 针对突然死亡时间控制进行了优化,在稍长的时间控制下(20 秒而不是 10 秒)以考虑时间延迟和思考时间使用。
对于最终提交,我担心太接近 RAM 限制,以防环境再次更改。我假设它不会改变,并采取了一种风险方法,使用 896kb 哈希大小以最大化 RAM 使用的 elo。我认为 3-4% 的错误损失率处于边缘,起初它挺住了。不幸的是,截止日期一周后,环境发生了变化,增加了每个人的错误损失,尤其是那些接近 RAM 限制的人。然而,由于高方差,运气最终 favor 了我。
思考时间管理和简单延迟在引擎测试框架中都不是标准的,所以我测试了一些想法在生产中尝试优化它们。对于我的最后 2 个提交,我在 timeman.c 中将 Time.optimumTime scaled up +1/3x 和 +2/5x,高于默认的 +1/4x,后者多年来保持不变。
总的来说,我发现 nnue 研究是学习更多 ML 知识的有趣方式。研究往往着眼于更大的模型,所以很高兴能工作在训练非常快的微小模型上。事实证明,可压缩到约 20kb 的神经网络既比几十年来积累的所有人类评估棋子位置的知识更快,也更强大!