引擎執行與通訊
介面負責棋盤、按鈕和棋鍾,引擎負責接收局面、計算並返回結果。兩者用一行行文字命令交流。點選「分析」不只是呼叫一個算棋函式,還涉及設定局面、啟動執行緒、持續輸出和結束通知。
本文按 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 負責評估,搜尋程式碼負責選棋,不能因為倉庫裡有壓縮庫就把它看成另一套棋力演算法。
原始碼:構建目標、universal 的 x86 分發、編譯資訊、壓縮流讀取、外部網路載入。完整檔案分工見 原始碼導讀。
