Skip to content

Replace std::vector path tracking with SearchNode shadow tree#12

Draft
Copilot wants to merge 4 commits into
masterfrom
copilot/update-backprop-to-search-tree
Draft

Replace std::vector path tracking with SearchNode shadow tree#12
Copilot wants to merge 4 commits into
masterfrom
copilot/update-backprop-to-search-tree

Conversation

Copilot AI commented May 13, 2026

Copy link
Copy Markdown

Summary

Implements the SearchNode shadow tree to replace std::vector<Node*> path tracking. The shadow tree mirrors the parts of the NodeTree that have been explored during the search, providing explicit parent/child links that make path traversal natural and efficient. The tree is shared across all SearchWorkers and lives on the Search object for the duration of the entire search.

Changes

New SearchNode struct (search.h)

struct SearchNode {
  Node* node = nullptr;
  Edge* edge = nullptr;
  SearchNode* parent = nullptr;
  std::vector<std::unique_ptr<SearchNode>> children;
  SearchNode* GetOrSpawnAtIdx(int edge_idx, Edge* edge, Node* child_node);
};
  • children owns the subtree via unique_ptr, indexed by policy/edge index
  • GetOrSpawnAtIdx finds or creates a child SearchNode at the given edge index — reuses existing nodes for paths revisited in later iterations
  • Thread-safe without extra locking: all accesses are already serialized by Search::nodes_mutex_

Updated types

Field Before After
NodeToProcess node Node* node SearchNode* search_node + Node* node (derived)
TaskWorkspace::cur_iters std::array<Node::Iterator, 256> std::array<std::vector<std::unique_ptr<SearchNode>>::iterator, 256>
TaskWorkspace SearchNode* current_search_node
PickTask::start Node* start SearchNode* start_search_node
Search::shared_collisions_ pair<Node*, int> pair<SearchNode*, int>
Search SearchNode search_root_node_ (guarded by nodes_mutex_)

Updated traversal logic

All loops that previously used node->GetParent() to walk up to the search root now traverse the shadow tree via sn->parent:

  • DoBackupUpdateSingleNode — backup loop
  • CancelSharedCollisions — ancestor cancel-score-update
  • GatherMinibatch OOO revert — ancestor cancel-score-update
  • GatherMinibatch collision upsize — ancestor increment-n-in-flight
  • EnsureNodeTwoFoldCorrectForDepth — twofold revert loop

PickNodesToExtendTask

Maintains current_sn (alias into workspace->current_search_node) in sync with the existing node pointer. A single Node::Iterator edge_iter advances lazily alongside cache_filled_idx; GetOrSpawnAtIdx is called once per new edge index and cur_iters[idx] = current_sn->children.begin() + idx is stored. Descent uses current_sn = current_sn->GetOrSpawnAtIdx(idx, edge, child_node) and backtrack uses current_sn = current_sn->parent.

Node::Iterator best_edge / second_best_edge are replaced by int best_idx and bool second_best_valid. A best_spawn_iter snapshot of edge_iter is captured when best_idx is updated during the fill phase, avoiding O(idx) iterator reconstruction in the common case when spawning a new leaf.

Lifetime safety

SearchNode objects are owned by their parents; the root is owned by Search. The tree is initialized once in Search::Search() and persists for the lifetime of the search. shared_collisions_ holds SearchNode* pointers that are consumed by CancelSharedCollisions() before the Search object is destroyed, so no dangling pointers can exist.

Notes

  • children is indexed by policy/edge index and reserved to max_needed before the UCT while loop to prevent iterator invalidation when GetOrSpawnAtIdx resizes the vector.
  • NodeToProcess::node is kept as a concrete field (derived from search_node->node in the constructor) to avoid changing all existing call-sites that access picked_node.node.

@borg323

borg323 commented May 14, 2026

Copy link
Copy Markdown
Owner

@copilot the intention was for a single shadow tree for the whole search, not one per search worker. Revise accordingly.

Copilot AI commented May 14, 2026

Copy link
Copy Markdown
Author

@copilot the intention was for a single shadow tree for the whole search, not one per search worker. Revise accordingly.

Done in dbafdde. search_root_node_ is now a field on Search (guarded by nodes_mutex_), initialized once in the Search constructor and shared across all SearchWorkers. The per-iteration reset in InitializeIteration() has been removed — the tree accumulates across iterations since GetOrSpawn reuses existing nodes and the parent/child relationships in the NodeTree are stable throughout a search.

@borg323

borg323 commented May 28, 2026

Copy link
Copy Markdown
Owner

@copilot I would like to replace the std::array<Node::Iterator, 256> cur_iters in TaskWorkspace with an array of some kind of SearchNode iterators. Doesn't have to be a custom iterator, I expect an std::vector iterator will work just fine.

Copilot AI added 2 commits May 28, 2026 22:08
- Add Edge* field to SearchNode
- Replace GetOrSpawn(Node*) with GetOrSpawnAtIdx(int, Edge*, Node*)
  that uses index-based children vector (indexed by policy/edge index)
- Change TaskWorkspace::cur_iters from std::array<Node::Iterator, 256>
  to std::array<vector<unique_ptr<SearchNode>>::iterator, 256>
- Replace lazy-fill logic: use a single edge_iter (Node::Iterator) per
  node level, advance it once per new idx, call GetOrSpawnAtIdx to
  create/update the SearchNode child, then store children.begin()+idx
  in cur_iters[idx]
- Reserve children to max_needed before the UCT while loop to prevent
  iterator invalidation on resize
- Replace best_edge/second_best_edge Node::Iterators with bool
  second_best_valid flag; reconstruct a spawn_iter only when a new
  leaf needs to be spawned in the NodeTree (child node was nullptr)
…d comment

- Add best_spawn_iter variable that captures edge_iter when best_idx
  is set during the lazy-fill phase, avoiding O(idx) reconstruction
  in the common case where the best edge was just filled this iteration
- Add null check for child_node in task-split loop for defensive safety
- Add comment explaining that children are already reserved when
  GetOrSpawnAtIdx is called from the fall-through descent loop

Copilot AI commented May 28, 2026

Copy link
Copy Markdown
Author

@copilot I would like to replace the std::array<Node::Iterator, 256> cur_iters in TaskWorkspace with an array of some kind of SearchNode iterators. Doesn't have to be a custom iterator, I expect...

Done in the latest commits. TaskWorkspace::cur_iters is now std::array<std::vector<std::unique_ptr<SearchNode>>::iterator, 256>.

Key design points:

  • SearchNode gains an Edge* field and replaces GetOrSpawn with GetOrSpawnAtIdx(int edge_idx, Edge*, Node*), which indexes children by policy/edge index instead of linear-scanning by node pointer.
  • Before the UCT while loop, current_sn->children.reserve(max_needed) prevents reallocation from invalidating the stored iterators.
  • A single Node::Iterator edge_iter advances lazily alongside cache_filled_idx; GetOrSpawnAtIdx is called once per new idx and cur_iters[idx] = current_sn->children.begin() + idx is stored.
  • A best_spawn_iter snapshot captures edge_iter when best_idx is updated during the fill phase, avoiding the O(idx) iterator reconstruction in the common case. The fallback reconstruction path is only taken for previously-cached indices that have not yet been spawned in the NodeTree.
  • Node::Iterator best_edge / second_best_edge are replaced with int best_idx and bool second_best_valid.

Copilot AI changed the title Replace std::vector<Node*> path tracking with SearchNode shadow tree Replace std::vector path tracking with SearchNode shadow tree May 28, 2026
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