引擎运行与通信
界面负责棋盘、按钮和棋钟,引擎负责接收局面、计算并返回结果。两者用一行行文字命令交流。点击“分析”不只是调用一个算棋函数,还涉及设置局面、启动线程、持续输出和结束通知。
本文按 official Pikafish 的固定提交 1c66b9b 核对。这里解释各部分如何配合;参数怎么设置另见 UCI 选项,算棋方法见 搜索算法。
从启动到返回走法
main.cpp 先初始化攻击表和局面所需的数据,再创建 UCIEngine 并进入命令循环。uci.cpp 负责读写协议;Engine 统一管理局面、选项、线程、置换表和网络,实际搜索交给工作线程。
| 命令或输出 | 在这次思考中做什么 |
|---|---|
uci → uciok | 报告引擎名称和支持的选项,完成握手。 |
setoption | 修改选项;某些设置还会重新分配线程或内存。 |
position ... | 设置起始局面,并按 moves 后的着法逐步走到目标位置。 |
go ... | 读取时间、深度或节点限制,检查网络,准备根局面并启动搜索。 |
info ... | 搜索过程中持续报告深度、评分、节点数和主要变化。 |
stop | 请求结束本次搜索;工作线程收尾后返回结果。 |
bestmove ... | 本次搜索的最终走法通知,不等于界面已经落子。 |
启动搜索后,命令循环还能继续接收 stop。stop 只是设置停止标记,不会强行杀掉线程;搜索主线程会等待其他工作线程结束,再通过回调输出 bestmove。quit 则请求停止并退出程序。
bestmove 后可能附带 ponder,表示预计的对手回应。真正后台思考由界面发送 go ponder 启动;对手确实走了预计着法,再用 ponderhit 转为正常思考。只看到一个预计回应,并不代表引擎已经自动开始后台搜索。
局面、历史与等待:不能只看棋盘
每次 position 都重新建立局面状态;附带的 moves 会逐步验证并落子,留下历史链。只发送最后一张 FEN,不能恢复此前怎么走过来,因此可能影响重复、长将和长捉判定。
ucinewgame 清理置换表和搜索历史,不负责把棋盘摆回开局。新局面仍需用 position 设置。当前代码遇到无法解析的局面或非法历史着法,会输出 CRITICAL ERROR 并退出;接入程序需要保留错误消息。
readyok 不是“搜索已结束”。 当前 isready 会直接应答,搜索仍可继续;要结束正在运行的搜索,应发送 stop 并等待 bestmove。改变选项前也应先结束搜索,因为 setoption 的处理会等待当前搜索完成。选项代码会检查类型和数值范围,再执行相应更新。
线程与内存:共享有用信息,各算各的分支
工作线程各自保存搜索中的局面和进度,启动前准备自己的根状态;更早的局面历史只读共享。同一引擎实例的线程共享置换表,部分历史统计按 NUMA 节点组织,其他统计保留在线程内部。它不是“每个线程只负责一枚棋子”,也不是每增加一个线程就复制一整份 Hash。
NUMA 可以理解为多核机器中“访问附近内存与远处内存的代价不同”。相关代码负责安排线程和网络副本的位置;内存代码还提供对齐分配、大页尝试和相应回退。这些机制服务于数据访问效率,不改变象棋走法规则。
线程数影响总内存,但 Hash 只指定置换表预算,不代表进程全部内存。网络、历史表、线程栈等也占空间。多线程有重复工作,所以线程翻倍并不意味着棋力翻倍。
时间管理根据剩余时间、加秒等给搜索预算,再随搜索表现调整,详见 多线程与时间分配。
cp、mate、wdl:三种不同含义
原始 UCI 搜索评分从根局面当前行棋方看,正数有利于这一方。界面可能另行换算成红方视角,读日志时要分清。
| 输出 | 应怎样理解 |
|---|---|
score cp 100 | 普通局面评分经过显示尺度换算后的数值;不是胜率 100%,也不是领先 100 Elo。 |
score mate 3 | 用 UCI 的步数计法报告决定性胜负距离;正数表示己方胜,负数表示己方败。 |
wdl 300 600 100 | 模型给出的胜/和/负估计,单位是千分比;这个示例对应 30%/60%/10%。 |
当前普通分数的换算是 round(100 × v / a),其中 v 为内部分数,a 随盘上子力构成变化。因此,100 cp 不能机械地理解为固定“多一个兵”。WDL 则由拟合模型计算,三项合计 1000;它不是 NNUE 直接输出的三个分类,也不是这盘棋必然发生的结果。
内部决定性分数先转成剩余层数,再换成 mate 步数。例如正的三层距离会显示 mate 2。象棋的困毙和部分棋规判负也使用这套胜负分,所以看到 mate 不应自动认定为“只能靠将死结束”。若还带 lowerbound 或 upperbound,应按分数界限理解。
评估如何进入搜索,见 局面评估与 NNUE。源码:分数分类、cp/mate/WDL 换算、根搜索输出、棋规胜负分。
检查运行状态:各工具测什么
下列调试命令应在搜索停止后使用,部分界面可能不提供直接输入入口。
| 工具 | 用途与边界 |
|---|---|
d | 查看引擎实际持有的局面,排查界面与引擎是否一致。 |
eval | 查看当前局面的评估拆解;不是重新进行完整搜索。 |
compiler | 显示编译器、编译架构与启用的指令集等信息。 |
bench | 按指定局面和限制跑一组测试,统计节点与耗时;适合比较运行行为。 |
speedtest | 预热后运行测速序列,并报告线程、Hash、时间和节点速度等信息。 |
go perft 3 | 数到指定层数的合法着法路径,主要核验走法生成与落子撤销。 |
perft 的递归不调用局面评估,也不按完整长将/长捉规则截断;但当前 Engine::perft() 入口仍会检查网络。它数的是路径,不能把不同路径到达的同一盘面合并为一个,也不能代替棋规回归或棋力测试。
NPS 是每秒搜索节点数。比较编译版本时,应固定代码、网络、局面、线程和测试条件;不同搜索策略的节点工作量可能不同,单看 NPS 不能判断谁更强。
源码:诊断命令、bench 参数、speedtest 执行与报告、perft。
编译版本与压缩依赖各管什么
Makefile 决定目标架构、编译器和优化方式。普通版本在编译时选定指令集;x86-64 universal 把多个版本装进同一文件,启动时检查 CPU 特性再选择入口。当前分发代码还会避开部分旧 AMD 上较慢的 BMI2 路径,并不是只看指令名字谁更新。
想确认这次运行采用了什么,可输入 compiler,看 Compilation architecture 和 Compilation settings。universal 进入所选版本后,也由该版本返回编译信息;它不是启动后把所有版本都跑一次测速选最快。
外部压缩网络的读取使用 Zstandard:misc.cpp 解压字节流,网络加载代码再解析并校验参数。压缩依赖负责文件读取,NNUE 负责评估,搜索代码负责选棋,不能因为仓库里有压缩库就把它看成另一套棋力算法。
