Stockfish並列解析のスケーリング
結果: Stockfish解析パイプラインは、1台のWebサーバーでの毎秒 4.00 局面から、クラウドCPUワーカー上での安定した毎秒 47-60 局面へ向上した。この展開では Stockfish 18、深さ 18、MultiPV 3 という契約の下で 251,166 件の局面評価を完了した。
現在の本番設定はバッチサイズ 32、max_containers=100、1局面あたり 300s の上限、そしてバッファ付き書き戻しである。以前の直接書き戻し実行では1局面あたり約 $0.0000200 とモデル化されていたが、現在の cpu=0.25 バッファ経路では $0.0000053 に近い値になる。
興味深かったのは、Stockfishが並列実行できると証明することではなかった。チェスの局面はすでに独立したエンジン作業単位である。難しかったのは、コストを上限内に収め、失敗を回復可能にし、深さのセマンティクスを正確に保つ動作点を見つけることだった。
ここでの実装ではコントロールプレーンにRailway、StockfishワーカーにModalを使ったが、再利用できる部分はワークロードモデル、測定方法、そして失敗に関する制約である。
転用できる教訓: ワーカー数の最大化から始めてはいけない。まずエンジン契約、リトライ単位、書き戻し契約、測定台帳を定義すること。その後で並列度を上げる。
これが最も当てはまるのは大量のオフライン解析である。独立した局面が多数あり、品質要件が厳しく、生の速度と同じくらいリトライが重要になるほどの作業量がある場合だ。レイテンシが支配的な単一対局のインタラクティブ解析にはあまり関係しない。
実測した本番設定:
| 設定項目 | 値 |
|---|---|
| エンジン | Stockfish 18 |
| 深さ | 18 |
| MultiPV | 3 |
| ワーカーCPU | 0.25 |
| バッチサイズ | 32 |
| 最大コンテナ数 | 100 |
| 1局面あたりの上限 | 300s |
| 書き戻しモード | バッファ付き |
| 大規模実行のスループット | 毎秒 47-60 局面 |
| モデル化したコスト | 完了局面あたり約 $0.0000053 |
意思決定の記録:
| 決定事項 | 現在の値 |
|---|---|
| 作業単位 | 対局IDではなく position_key |
| 品質契約 | 要求は Stockfish 18、深さ 18、MultiPV 3 |
| 現在の設定 | バッチ 32、コンテナ 100、上限 300s、バッファ付き書き戻し |
| この動作点を選んだ理由 | コストが上限内、テールがリトライ可能、後片付けが確実、到達深さを全結果に記録 |
| 見直す条件 | プロバイダの上限変更、キャッシュヒット率の上昇、書き戻しのドレインが支配的になる、あるいはテール局面が実行時間を支配する場合 |
ワークロードの定義
ユーザーに見えるオブジェクトはチェスの対局。計算のオブジェクトは局面である。
システムは対局を取り込み、局面を抽出し、position_key で重複排除し、欠けている局面を評価し、その後で前後の局面が揃った時点で1手ごとの評価行を実体化する。
PGN games
-> positions
-> unique position keys
-> Stockfish queue
-> position eval cache
-> move-level eval rows
-> reports, drills, prep, review
本番の契約は Stockfish 18、要求深さ 18、MultiPV 3 で、position_key 単位でキューイングし、SQLiteを真実の源、クラウドCPUコンテナをワーカープールとした。
評価の契約はデータモデルの一部である。 深さ 18、MultiPV 3 の行は、別のエンジン、深さ、MultiPV形状による公開キャッシュ行と交換可能ではない。システムは出所を保存しており、将来のコードが計算した行、キャッシュヒット、より深い参照を区別できる。
方法論
ほとんどの表では短い実行ラベルを使う。25g、100g、500g はその数の対局から用意した作業を意味する。b32 はバッチサイズ 32。c50 はワーカー上限 50。t300 は1局面あたり 300s の上限を意味する。
測定値には3つのラベルを使う。ワーカーや台帳の行は実測、投入前のコスト見積もりは推定、CPU割り当てや書き戻し形状を変更した後の動作モデルはモデル値である。
スループットの数値は、その実行が記録していた内容に応じて、投入の実時間またはサーバー側バッチの期間を使っている。動作点の比較には十分だが、普遍的なStockfishベンチマークを主張するものではない。
元のワークロードは私自身のチェス対局アーカイブである。本番データベースはジョブ、バッチ、キュー状態、実体化した評価件数、ワーカー実行時間、書き戻し時間、コスト見積もりを記録した。プロファイル専用の深さ実験は同じワーカー経路を通ったが、結果を本番の評価テーブルには書き込んでいない。
件数について1点注意がある。ベースラインのステータス行と最終のワークロード件数は同じ単位ではない。ベースラインの行は旧来のユーザー単位バックフィル経路のフェーズカウンタだった。最終の 251,166 という数字には、position_keyによる重複排除、前後の局面の評価、再計算作業、リトライ、手評価の再利用が含まれる。対局数や手数ではなく、最終的に解析したワークロードの規模として扱ってほしい。
生の実験表はダウンロード可能な成果物として公開しない。この記事には論旨に必要な実測行を含めており、運用台帳の全体はプロジェクトのノートに残している。
ベースライン
ベースライン: 毎秒 4.00 局面。実行途中でも 9.1h の残り時間見積もりが見えていた。
元の経路はRailwayのWebランタイム内でStockfishを実行していた。小さなインポートには単純で十分正しかったが、大きなアーカイブには形が合っていなかった。
ライブのステータス行はこう見えた。
stockfish phase: 6350/137250 (cache=6043, errors=0) 4.00/s ETA 9.1h
その行は正確だった。同時に上限も示していた。より大きなWebマシンならスループットは線形に改善するが、作業は依然として1つの長時間プロセス、1つのデプロイライフサイクル、1台のマシンに縛られたままだ。
バッチのワークロードには、ワーカー容量で制限された並列性、バッチまたは局面単位に限定された失敗、ジョブごとのコスト上限、永続的なキュー状態が必要だった。最初のスケーリング目標は、回復に足る状態を備えた実測スループットだった。
コントロールプレーン
ワーカーを増やすなら台帳が必要になる。 台帳こそが主要なインフラだった。
コントロールプレーンは主に5つのテーブルを使った。
analysis_jobs: 親となるリクエスト、投入者、目的、エンジン契約、コスト上限、集約ステータス。analysis_job_batches: ワーカーのシャード状態、受理件数、実体化件数、ワーカー実行時間、書き戻しのタイミング。analysis_queue: position_key単位のステータスと正規化した評価結果。analysis_requests: 誰がどの局面をなぜ要求したか。evals: レポートや学習用途のための手単位の実体化行。
ワーカーは作業を自分で探さない。コントロールプレーンがposition_keyを確保し、ジョブ行とバッチ行を作成し、その確保済みキーをワーカープールに送る。所有権、ステータス、上限、リトライ、実体化については、データベースが真実の源であり続けた。
親ジョブのカウンタは最終的に正規のバッチ行から導出するようにした。書き戻しは実質的に少なくとも1回の配送であり、クライアントのリトライによって同じ完了済みバッチのペイロードが再送されうるからだ。
SELECT
SUM(accepted) AS accepted,
SUM(materialized) AS materialized,
SUM(CASE WHEN status != 'completed' THEN 1 ELSE 0 END) AS open_batches
FROM analysis_job_batches
WHERE job_id = ?
重複コールバックの手作業による監査が必要なら、そのスループットの数字は信用できない。
スループットの結果
安定した向上は単一サーバーのベースラインに対して概ね 12-15x だった。
信頼できるスループット向上は、複数の変数を同時に変えたことから得られた。バッチサイズ、コンテナ上限、1局面あたりの時間上限、データベースクエリの形、コールバックの冪等性である。
これは純粋なワーカー数ベンチマークではなく、チューニングの段階として読んでほしい。
| 実行形態 | 受理件数 | バッチ数 | 実時間/期間 | スループット | 推定コスト |
|---|---|---|---|---|---|
25g b32 c25 |
3,006 |
94 |
206.344s |
毎秒 14.57 局面 |
$0.06012 |
100g b32 c50 |
12,701 |
397 |
365.205s |
毎秒 34.78 局面 |
$0.25402 |
100g b32 c100 t300 |
10,282 |
322 |
216.439s |
毎秒 47.50 局面 |
$0.20564 |
500g b32 c100 t300 |
45,429 |
1,420 |
752.324s |
毎秒 60.39 局面 |
$0.90858 |
最終的な大規模チャンクも同じ帯域に収まった。
| ジョブ | 受理件数 | 期間 | スループット |
|---|---|---|---|
30 |
45,429 |
752.324s |
毎秒 60.39 局面 |
31 |
38,248 |
~663.0s |
毎秒 57.69 局面 |
32 |
32,857 |
~599.0s |
毎秒 54.85 局面 |
33 |
27,650 |
~562.0s |
毎秒 49.20 局面 |
34 |
22,371 |
~444.0s |
毎秒 50.39 局面 |
35 |
15,951 |
~326.0s |
毎秒 48.93 局面 |
36 |
3,403 |
~72.0s |
毎秒 47.26 局面 |
本番設定は、失敗の診断が可能で後片付けが確実に行える範囲で最も並列度が高い構成だった。内部の実行台帳にはバッチ数、実体化した評価数、書き戻し時間、コスト見積もりも記録されている。
スループットが改善したのは、周辺のキュー、書き戻し、リトライの各経路が本番規模の失敗に耐えられるようになった後だった。
バッチサイズ
バッチサイズはリトライの粒度である。 大きなバッチはスケジューリングとコールバックのオーバーヘッドを減らすが、テールの失敗を切り分けるコストを高くする。
この展開でそれを直接目にした。25g b64 c25 の実行は 44/45 バッチを完了し 2,767 局面を受理したが、1バッチが深さのガードで失敗した。その後のテール調査では、失敗範囲を 32 局面から 8、さらに1局面まで絞り込んだ。
最終的なバッチサイズ 32 は妥協点だった。バッチサイズ 1 は診断には最適だが、広範なオーケストレーションには高コストすぎる。8-16 はリトライの形として良い。64+ はバッチ数を減らすがテールの切り分けを悪化させる。リトライツールは失敗した作業を単一局面まで分割できたが、これはわずかなスケジューリングのオーバーヘッドを削るより重要だった。
目安: 無理なくリトライできる最大のバッチサイズを選ぶこと。
ワーカー数
ワーカー数が効いたのは、コントロールプレーンがコールバックのレートを吸収できるようになった後だった。
コンテナ 25 から 50 への引き上げは大きかった。50コンテナのクリーンな検証で毎秒 34.78 局面に達した。100コンテナで 300s の上限を設けた実行は100対局のチャンクで毎秒 47.50 局面、最初の500対局チャンクは毎秒 60.39 局面に達した。
実測した動作点は現在、プロバイダのワークスペース容量と単一の書き戻し先によって制限されている。数百のワーカーがほぼ同時に終わる場合、ワーカーからWebへの直接コールバックは長期的な書き込み経路として貧弱である。バッファ付きシャードはそのバーストをModal Volumeに移し、その後サーバーが制御されたフェーズで結果を吸い出す。
つまり max_containers だけが上限ではなかった。ある点を超えると、次のボトルネックはスケジューリング、シャードの処理、ドレイン速度、あるいはデータベースの実体化になる。
cpu=0.25 の設定が機能したのは、ワークロードに独立した局面が多数あり、正しさが到達深さの検証で担保されていたからだ。テール局面やプロバイダの制限がボトルネックになるまでは、十分な並列度を伴う安価なワーカーのほうが、少数の大きなワーカーより良い既定値だった。
コスト
コストの改善は、解析水準を下げたからではなく、ワーカーの形と書き戻しの形から得られた。 直接書き戻しの実行は投入局面あたり約 $0.0000200 に落ち着いた。新しいスケジュール実行では cpu=0.25 のコストスケーリングとバッファ付き書き戻しを使い、以前の見積もりの約4分の1である。
| フェーズ | 例 | 局面数 | 推定コスト | 局面あたりコスト |
|---|---|---|---|---|
| 大規模な直接書き戻しの展開 | ジョブ 30 |
45,429 |
$0.90858 |
$0.0000200 |
バッファ付き cpu=0.25 経路 |
ジョブ 63 |
235 |
$0.001175 |
$0.0000050 |
実験期間のModalダッシュボードでは、リソース費用の合計が $17.43 と表示された。CPUが $16.69、メモリが $0.74 である。これには失敗した試行、プロファイリング、リトライ、深さの実験、本番展開の作業が含まれる。「この探索にいくらかかったか」という問いに対する正しい数字であり、最終的な動作点の限界費用ではない。
規律として、ワーカー投入前にコスト上限を強制し、その見積もりをジョブ台帳に書き込んだ。
3つの数字を分けて扱うこと。投入前の推定コスト、実行後にプロバイダから実測された請求額、そしてリトライとキャッシュ再利用を経た受理局面あたりのコストである。これらは別の問いに答えている。
正しさに関する制約
エンジン契約には depth=18 以上のものが必要だった。
深さは実時間の予算ではない。深さのみを指定したStockfish呼び出しは、外側のプラットフォームのタイムアウトに殺されるまで走り続けうる。本番ワーカーには要求深さと1局面あたりの時間上限の両方が必要だった。
ワーカーには正しいエンジンライフサイクルの境界も必要だった。変化する game= トークンなしに1つのStockfishプロセスを無関係なFENで使い回すと、python-chess はシャード全体を暗黙の1対局として扱ってしまう。無関係な局面の間で ucinewgame を送らなかったのだ。あるワーカーシャードは 1800s でプラットフォームのタイムアウトに達したが、64 局面すべてをローカルで再実行すると約 91.8s で完了した。変化する game= トークンと ucinewgame を使うと、問題の局面は1秒未満で [18,18,18] に到達した。
境界の修正は小さかった。
info = engine.analyse(
board,
chess.engine.Limit(depth=18, time=300),
multipv=3,
game=position_key,
)
重要なのはラッパーの具体的な実装ではない。無関係な局面には無関係なエンジン側の対局アイデンティティが必要だという点である。
病的な局面
この展開では集約した失敗だけでなく、実際の局面も記録した。重要なケースは2つに分かれた。
第一に、実際に消費する契約に対して厳しすぎる局面があった。この節の盤面は手番側から見た向きで表示している。
18。代替手順は次で停止 17.
[15,15,14]のみ。低深さのフォールバックとして受理。
[17,17,17]。保存値は result_depth=17.
[17]。保存値は result_multipv=0/3.- MultiPVのテール。
失敗したバッチを
32 -> 8 -> 1とリトライして、この6駒のクイーンとポーンの終盤を切り分けた。Stockfishは深さ[18, 18, 17]を返した。最善手順は深さ18に到達したが、3番目のPVは1手浅いままだった。受理された行には最善手f3d5、評価値-8115cp、requested_multipv=3、result_multipv=2が記録されている。
FEN:8/5p2/3Kp1p1/8/8/5q2/5k2/8 b - -。 - Magnusの真に病的なテール。
120sと180sでの厳格なリトライでも深さ[15, 15, 14]しか返らなかった。最終的な非厳格フォールバックではresult_depth=15、最善手b7a7、評価値-933cp、および低深さである旨の明示的な出所を保存した。
FEN:1r4k1/1q3pb1/3p2p1/2p1p1Pp/2P1N3/1p2B2P/1P1QBP2/K5RR b - -。 - 低深さフォールバック1。
厳格な実行は
60.013s後に深さ[17, 17, 17]で停止した。フォールバック行にはresult_depth=17、result_multipv=2/3、最善手h3g4、評価値-847cp、ノード数90,863,715が記録されている。
FEN:b2r4/2QP2k1/p6p/1p2P1p1/2pP1P2/7K/PPB3P1/4q3 w - -。 - 低深さフォールバック2。
厳格な実行は
60.010s後に深さ[17]で停止した。フォールバック行にはresult_depth=17、result_multipv=0/3、最善手g7h8、評価値+761cp、ノード数89,579,084が記録されている。
FEN:r4r2/pb2bpk1/1p1qpn2/6Q1/1n1P4/2NB1N1P/PP3PP1/R3R1K1 b - -。
第二に、少なくとも1つの局面は悪いのではなく、最初の上限では単に遅かっただけだった。
[18,18,18] 、条件は 300s.- 真の
120sテール。120sでは、ある単一局面のリトライが最善手順の深さ[17, 17, 18]を返し、隣接するリトライは31/32が素早く完了した。300sの上限にすると、まったく同じ局面が140.171sで[18, 18, 18]に到達した。
FEN:r5k1/1r6/p1p5/3pB1Q1/P1nPp2p/2P1P3/5PPP/6K1 b - -。
これらの例は、受理した行がすべて深さ 18 とMultiPV 3 を満たしたと装うのではなく、実際の結果深さと実際のMultiPV数をシステムが保存する理由を説明している。
この展開では、いくつかの失敗の形も記録した。
| ケース | 兆候 | 対処 |
|---|---|---|
| エンジンのライフサイクル | あるシャードが 1800s のプラットフォームタイムアウトに達したが、64 局面のローカル再実行は 91.8s だった |
無関係なFENに別個の game= アイデンティティを与えて ucinewgame を発火させる |
| MultiPVの深さガード | 32 -> 8 -> 1 のリトライ分割で、代替PV手順が浅い合法局面を切り分けた |
実際に消費する最善手順を検証し、result_multipv を保存する |
| 真に遅いテール | 単一局面リトライの 31/32 は素早く終わった。1つは 120s に間に合わなかったが 300s なら 140.171s で完了した |
広範な契約では1局面あたりの上限を高めに保つ |
| 低深さフォールバック | 切り分けた合法局面でも、フォールバックの上限では深さとMultiPVの目標をすべて満たせなかった | result_depth と result_multipv の出所を明示した上でのみ受理する |
| 深さのコストのテール | サンプルでは深さ 36/MultiPV 1 は深さ 18/MultiPV 3 より 41.44x 遅かった |
キャッシュミスごとに深い再計算を既定経路にしない |
正しさのルール
最終的な正しさのルール。
- 無関係な局面ごとに新規対局の境界を送る。
- 1局面あたりの時間上限を使う。
- 実際に消費する最善手順が要求深さに到達したか検証する。
requested_multipvとresult_multipvを保存する。- 親のカウンタはバッチ行から導出する。
- 完了したバッチ行は確定として扱う。
最も非自明な教訓: エンジンがバッチワーカー内で走るとき、エンジンプロトコルのセマンティクスは本番の正しさそのものになる。
ベンチマークが最終スループットだけを測って到達深さを無視すると、解析品質を静かに劣化させながら成功したように見えてしまう。
深さと品質のトレードオフ
深さを上げることはコストの乗数であり、無料の品質つまみではない。
10 局面のプロファイル専用サンプルで、いくつかの契約を比較した。
| エンジン契約 | ワーカー実行時間 | 最も遅い局面 | 深さ18/MultiPV 3 との比 |
|---|---|---|---|
深さ 18、MultiPV 1 |
5.301s |
0.571s |
0.51x |
深さ 18、MultiPV 3 |
10.329s |
2.442s |
1.00x |
深さ 24、MultiPV 1 |
24.861s |
5.728s |
2.41x |
深さ 24、MultiPV 3 |
56.252s |
13.428s |
5.45x |
深さ 30、MultiPV 1 |
87.375s |
16.263s |
8.46x |
深さ 36、MultiPV 1 |
428.071s |
106.782s |
41.44x |
これで本番の選択は明確になった。キャッシュミスは Stockfish 18、深さ 18、MultiPV 3 のままにする。出所が製品要件に合致する場合はより深い公開キャッシュヒットを使う。個人対局のテール局面すべてに対して深さ 30+ や 36 を既定の再計算目標にはしない。
深さ 18 が普遍的に「十分」なのではない。曲線に価格をつけた上で、このワークロードに対して選ばれた既定の計算契約である。
より深い解析は有用かもしれないが、コストの形を数パーセントではなく数倍の単位で変えてしまう。
キャッシュの挙動
ローカルのposition_key再利用は価値があった。公開キャッシュへの問い合わせは、大規模な準備処理のインラインに置ける状態ではなかった。
大規模な準備処理では、ローカルキャッシュのリードスルーがクラウド計算の前に数千件の手評価を実体化した。これは有効なままにすべきだ。決定的で、ローカルで、同じ局面・評価契約に紐づいている。
Lichess/Stockpile系の公開キャッシュ問い合わせは、初期テストで2つの問題があった。最初にサンプルした 50 局面のヒット数が 0 だったこと、そして後の健全性チェックでレート制限の挙動に当たったことだ。つまり公開APIへの問い合わせは、大規模な本番準備処理をブロックしてはならない。
現在の方針。
- ローカル/グローバルの局面キャッシュのリードスルーを使う。
- 公開キャッシュの問い合わせは大規模実行では制限するかオフにする。
- 再挑戦する前に、増分的で永続的、バックオフを考慮した問い合わせを追加する。
- ソース、エンジンバージョン、深さ、MultiPV、取得時刻のステータス、データセットのメタデータを保存する。
- プライベートな深さ18の行を、より深い公開行と交換可能として扱わない。
キャッシュはその契約が合っているときに有用である。 キャッシュミスに対する上限のある経路の代わりにはならない。
製品上のルール。キャッシュヒットは出所を伴う計算結果である。ソース、エンジン、深さ、MultiPV、レイテンシが製品の契約に合わないなら、キャッシュは任意の高速化手段として扱う。
このベンチマークの限界
これらの数値は有用な運用データであり、普遍的なStockfishベンチマークではない。
ワークロードは個人のチェスアーカイブ1つである。エンジン契約は Stockfish 18、深さ 18、MultiPV 3 に固定されている。実装ではコントロールプレーンにRailwayとSQLite、ワーカーにこの展開期間中のアカウント上限下でのModal CPUコンテナを使った。深さ、MultiPV数、ハードウェア、局面の分布、キャッシュヒット率、プロバイダの制限が違えば結果は変わる。
転用できる部分は測定の方法である。評価契約を定義し、ジョブとバッチの状態を記録し、出所を保存し、投入前に支出を制限し、ボトルネックを1つずつ変えること。
見つかったボトルネック
規模が拡大するにつれてボトルネックは移動した。
| ボトルネック | 症状 | 対処 |
|---|---|---|
| 単一WebのCPU | 毎秒約 4 局面で、残り時間が数時間 |
独立したposition_keyにファンアウトする |
| バッチのテール | 遅い1局面がシャードを占有した | バッチサイズ 32、リトライ分割、1局面あたりの上限 |
| エンジンのライフサイクル | ローカル再実行は速いのにプラットフォームのタイムアウトが発生 | 局面ごとに ucinewgame の境界を入れる |
| MultiPVの深さガード | 妥当な最善手順が却下された | 消費する手順を検証し、結果のMultiPVを保存する |
| SQLiteのキー集合サイズ | 500対局の準備処理がSQL変数の上限に達した | クエリを分割し、一時テーブルを使う |
| 重複コールバック | 親ジョブが過大にカウントされた | カウンタを正規のバッチ行から導出する |
| 遅延したエラーコールバック | 完了済みバッチがエラー扱いになった | 完了した行は確定とする |
| 公開キャッシュのレイテンシ | 準備処理が停滞またはレート制限された | 公開問い合わせを制限し増分的にする |
| デプロイのフィードバックループ | イメージのpushに 163s かかった |
重いMLスタックをWebイメージから外す |
| ワークスペースの上限 | 100コンテナの実行が実用上の天井になった | 上限を受け入れるか、より大きな計算容量を求める |
イメージサイズの修正はStockfishの計算そのものではないが、反復速度には重要だった。Webイメージは 2.85 GB から 189.5 MB になり、イメージのpushは 163s から 8.1s に短縮した。
繰り返し現れたパターン: 1つのボトルネックが移動すると、次のボトルネックは大抵Stockfishの外にあった。
実測した動作点
現在の本番設定で期待される挙動:
| 指標 | 期待される範囲 |
|---|---|
| 大規模実行のスループット | 毎秒 47-60 局面 |
| 完了後のキューの後片付け | pending、running、error の行が残らない |
| 大きなチャンクでの書き戻しp95 | 記録された実行では1秒未満 |
| 完了局面あたりのコスト | cpu=0.25 のモデルで概ね $0.000005 |
これは安定した動作点であり、恒久的な最適点ではない。テール局面が実行時間を支配する、ドレインの実時間が無視できなくなる、プロバイダの請求がモデルから乖離する、キューの行が古くなる、キャッシュヒット率が高く信頼できるようになる、あるいはバッチプロバイダがアカウント上限を引き上げたときに変更すること。
次の10倍の飛躍は、おそらく同じ上限の中で max_containers を調整することからは来ない。 次の3つの変化のいずれかから来る。
- バッチプロバイダがワークスペース/コンテナの上限を引き上げる。
- ワークロードを独立した計算容量に分割する。
- 出所互換のキャッシュで満たせる局面が増える。
それまでは100コンテナのバッファ付き経路が本番の既定である。
転用すべきもの
長持ちする部分は、特定のプロバイダ、価格、バッチサイズではない。
| 原則 | 実測された具体例 |
|---|---|
| まず評価契約を定義する | Stockfish 18、深さ 18、MultiPV 3 |
| 不変な作業単位をキューに入れる | 対局IDではなく position_key |
| バッチサイズをリトライの粒度として扱う | バッチサイズ 32 が既定になった |
| 要求した品質と到達した品質を分ける | 要求深さ対到達深さ、要求MultiPV対結果MultiPV |
| 書き戻しを冪等にする | 親のカウンタをバッチ行から導出 |
| キャッシュの出所を見えるようにする | ソース、エンジン、深さ、MultiPV、取得時刻のメタデータ |
| 投入前に支出を制限する | 各ジョブにコスト上限を記録 |
別のシステムがエンジンバージョン、目標深さ、ハードウェア、プロバイダ、局面の分布を変えれば、実測値は変わるはずだ。原則は変わらないはずである。
このベンチマークの再実行
以下のいずれかが変わったら再実行すること。
- エンジンバージョンまたはUCIオプション。
- 要求深さまたはMultiPV数。
- 1局面あたりの時間上限。
- CPUの構成、ワーカー数、プロバイダの制限。
- バッチサイズ。
- 書き戻しの経路。
- キャッシュのソースとヒット率。
- 局面の分布。
ブリッツのアーカイブに最速の設定が、序盤データベース、終盤研究集、エンジン同士の対局コーパスでも最速とは限らない。最小限の有用なベンチマークのループはこうだ。
- 初期局面やテスト用FENだけでなく、実際のワークロードから局面をサンプリングする。
- 目標のエンジン契約と、少なくとも1つのより深い契約をプロファイルする。
- 小さなバッチの段階を試す。
1、8、16、32、64。 - リトライと書き戻しが信頼できるようになった後で、ワーカー数の段階を試す。
- ワーカー実行時間、投入期間、書き戻し時間、残ったキュー行を別々に測る。
- 投入前の推定コストと事後の実際のプロバイダ支出を追跡する。
- スループットだけでなく、到達深さと保存された出所を検証する。
- プロバイダ、深さ、ワーカーの構成を変えた後に同じサンプルで繰り返す。
教訓
- 計算をスケールする前にエンジン契約を定義する。
- 対局ではなくposition_keyをキューに入れる。
- バッチサイズをリトライの粒度として扱う。
- 実時間の上限を追加する。深さは予算ではない。
- エンジンのライフサイクル境界を正しさの要件として扱う。
- 冪等性を性能作業の一部にする。
- キャッシュの出所を見えるようにしておく。
- 本番チューニング中はデプロイのフィードバックループを最適化する。
後退させてはいけないこと。
- 無関係な局面には別個のエンジン側対局アイデンティティを送る。
- 要求深さと到達深さを別々に保存する。
- 要求MultiPVと到達MultiPVを別々に保存する。
position_keyでキューイングと重複排除を行う。- バッチの書き戻しを冪等にする。
- 失敗したバッチをより小さい粒度でリトライできるようにしておく。
- 低深さのフォールバックは明示的な出所として扱い、深さ
18を満たしたものとして扱わない。 - 本番実行は
pending=0、running=0、error=0で終える。