Skip to content

Conversation

@mooskagh
Copy link
Member

search(codex): add minimal PUCT search implementation and register it; hook into build via meson

@borg323
Copy link
Member

borg323 commented Oct 22, 2025

Quick fix for opponent move display and a few other issues I found:

--- a/src/search/codex/codex.cc
+++ b/src/search/codex/codex.cc
@@ -104,6 +104,7 @@ class CodexSearch : public SearchBase {
     params_ = params;
     // Compute simple time budget.
     ComputeTimeBudget();
+    if (worker_.joinable()) worker_.join();
     worker_ = std::thread(&CodexSearch::SearchLoop, this);
   }
 
@@ -270,6 +271,7 @@ class CodexSearch : public SearchBase {
   // Prepare and send a thinking info update (best-so-far line and score).
   void SendThinkingInfo() {
     if (!root_ || root_->edges.empty()) return;
+    if (responded_bestmove_) return;
     const int64_t elapsed_ms = move_start_time_ ?
         std::chrono::duration_cast<std::chrono::milliseconds>(
             std::chrono::steady_clock::now() - *move_start_time_).count() : 0;
@@ -283,6 +285,11 @@ class CodexSearch : public SearchBase {
           [](const Edge& a, const Edge& b) { return a.n < b.n; });
       if (it == node->edges.end() || it->n == 0) break;
       pv.push_back(it->move);
+      // Temporary compatibility: moves are encoded from current player
+      // perspective, flip before sending when black to move.
+      if (game_state_.CurrentPosition().IsBlackToMove() == (pv.size() & 1)) {
+        pv.back().Flip();
+      }
       node = it->child.get();
       if (pv.size() >= 16) break;
     }
@@ -338,6 +345,7 @@ class CodexSearch : public SearchBase {
     // perspective, flip before sending when black to move.
     if (game_state_.CurrentPosition().IsBlackToMove()) {
       if (!info.bestmove.is_null()) info.bestmove.Flip();
+    } else {
       if (!info.ponder.is_null()) info.ponder.Flip();
     }
     uci_responder_->OutputBestMove(&info);

@mooskagh mooskagh requested a review from Copilot October 25, 2025 11:24
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR introduces a minimal PUCT (Predictor + Upper Confidence bounds applied to Trees) search implementation named "Codex" for the Leela Chess Zero engine. This implementation provides a simple, single-threaded alternative to the main search engine for demonstration and experimentation purposes.

Key Changes:

  • Implements a lightweight PUCT tree search using direct Backend API calls with basic node selection, expansion, and backpropagation
  • Registers the new search implementation as "codex" via the search factory pattern
  • Integrates the implementation into the build system

Reviewed Changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.

File Description
src/search/codex/codex.cc Complete implementation of CodexSearch class with PUCT algorithm, node/edge structures, and UCI integration
meson.build Adds codex.cc to the build file list

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.


void SetBackend(Backend* backend) override {
// Wrap to respect backend maximum batch sizes automatically.
batchsplit_backend_ = CreateBatchSplitingBackend(backend);
Copy link

Copilot AI Oct 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Corrected spelling of 'CreateBatchSplitingBackend' to 'CreateBatchSplittingBackend'.

Suggested change
batchsplit_backend_ = CreateBatchSplitingBackend(backend);
batchsplit_backend_ = CreateBatchSplittingBackend(backend);

Copilot uses AI. Check for mistakes.
Comment on lines +227 to +232
int best_idx = -1;
float best_score = -std::numeric_limits<float>::infinity();
for (size_t i = 0; i < node->edges.size(); ++i) {
const Edge& e = node->edges[i];
const float q = (e.n > 0) ? (e.w / e.n) : 0.0f;
const float u = kCpuct * e.prior * sqrt_total / (1.0f + e.n);
Copy link

Copilot AI Oct 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Division operation inside the selection loop could be optimized by precomputing 1.0f / (1.0f + e.n) or by computing the common factor kCpuct * sqrt_total once before the loop, reducing redundant calculations per child edge.

Suggested change
int best_idx = -1;
float best_score = -std::numeric_limits<float>::infinity();
for (size_t i = 0; i < node->edges.size(); ++i) {
const Edge& e = node->edges[i];
const float q = (e.n > 0) ? (e.w / e.n) : 0.0f;
const float u = kCpuct * e.prior * sqrt_total / (1.0f + e.n);
const float cpuct_sqrt = kCpuct * sqrt_total;
int best_idx = -1;
float best_score = -std::numeric_limits<float>::infinity();
for (size_t i = 0; i < node->edges.size(); ++i) {
const Edge& e = node->edges[i];
const float q = (e.n > 0) ? (e.w / e.n) : 0.0f;
const float recip = 1.0f / (1.0f + e.n);
const float u = cpuct_sqrt * e.prior * recip;

Copilot uses AI. Check for mistakes.

// Counters/metrics
int max_depth_ = 0;
int64_t nodes_visited_ = 0;
Copy link

Copilot AI Oct 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The nodes_visited_ counter is accessed from both the worker thread (incrementing in RunOneSimulation) and potentially the main thread during shutdown/info updates, but is not protected by atomics or mutex. This creates a data race. Consider making it std::atomic<int64_t> or protecting access with the appropriate synchronization mechanism.

Suggested change
int64_t nodes_visited_ = 0;
std::atomic<int64_t> nodes_visited_{0};

Copilot uses AI. Check for mistakes.
std::optional<std::chrono::steady_clock::time_point> move_start_time_;

// Counters/metrics
int max_depth_ = 0;
Copy link

Copilot AI Oct 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The max_depth_ variable is updated in RunOneSimulation (line 251) on the worker thread and read in SendThinkingInfo without synchronization. This is a data race. Consider using std::atomic<int> for thread-safe access.

Suggested change
int max_depth_ = 0;
std::atomic<int> max_depth_{0};

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants