Replace std::vector path tracking with SearchNode shadow tree#12
Replace std::vector path tracking with SearchNode shadow tree#12Copilot wants to merge 4 commits into
Conversation
Agent-Logs-Url: https://github.com/borg323/lc0/sessions/7bef2a17-0daa-4f2f-b01b-2f32df106406 Co-authored-by: borg323 <39573933+borg323@users.noreply.github.com>
|
@copilot the intention was for a single shadow tree for the whole search, not one per search worker. Revise accordingly. |
…workers Agent-Logs-Url: https://github.com/borg323/lc0/sessions/93461f75-b4f6-4a28-946b-2524e476c43b Co-authored-by: borg323 <39573933+borg323@users.noreply.github.com>
Done in dbafdde. |
|
@copilot I would like to replace the |
- 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
Done in the latest commits. Key design points:
|
Summary
Implements the
SearchNodeshadow tree to replacestd::vector<Node*>path tracking. The shadow tree mirrors the parts of theNodeTreethat have been explored during the search, providing explicit parent/child links that make path traversal natural and efficient. The tree is shared across allSearchWorkers and lives on theSearchobject for the duration of the entire search.Changes
New
SearchNodestruct (search.h)childrenowns the subtree viaunique_ptr, indexed by policy/edge indexGetOrSpawnAtIdxfinds or creates a childSearchNodeat the given edge index — reuses existing nodes for paths revisited in later iterationsSearch::nodes_mutex_Updated types
NodeToProcessnodeNode* nodeSearchNode* search_node+Node* node(derived)TaskWorkspace::cur_itersstd::array<Node::Iterator, 256>std::array<std::vector<std::unique_ptr<SearchNode>>::iterator, 256>TaskWorkspaceSearchNode* current_search_nodePickTask::startNode* startSearchNode* start_search_nodeSearch::shared_collisions_pair<Node*, int>pair<SearchNode*, int>SearchSearchNode search_root_node_(guarded bynodes_mutex_)Updated traversal logic
All loops that previously used
node->GetParent()to walk up to the search root now traverse the shadow tree viasn->parent:DoBackupUpdateSingleNode— backup loopCancelSharedCollisions— ancestor cancel-score-updateGatherMinibatchOOO revert — ancestor cancel-score-updateGatherMinibatchcollision upsize — ancestor increment-n-in-flightEnsureNodeTwoFoldCorrectForDepth— twofold revert loopPickNodesToExtendTaskMaintains
current_sn(alias intoworkspace->current_search_node) in sync with the existingnodepointer. A singleNode::Iterator edge_iteradvances lazily alongsidecache_filled_idx;GetOrSpawnAtIdxis called once per new edge index andcur_iters[idx] = current_sn->children.begin() + idxis stored. Descent usescurrent_sn = current_sn->GetOrSpawnAtIdx(idx, edge, child_node)and backtrack usescurrent_sn = current_sn->parent.Node::Iterator best_edge/second_best_edgeare replaced byint best_idxandbool second_best_valid. Abest_spawn_itersnapshot ofedge_iteris captured whenbest_idxis updated during the fill phase, avoiding O(idx) iterator reconstruction in the common case when spawning a new leaf.Lifetime safety
SearchNodeobjects are owned by their parents; the root is owned bySearch. The tree is initialized once inSearch::Search()and persists for the lifetime of the search.shared_collisions_holdsSearchNode*pointers that are consumed byCancelSharedCollisions()before theSearchobject is destroyed, so no dangling pointers can exist.Notes
childrenis indexed by policy/edge index and reserved tomax_neededbefore the UCTwhileloop to prevent iterator invalidation whenGetOrSpawnAtIdxresizes the vector.NodeToProcess::nodeis kept as a concrete field (derived fromsearch_node->nodein the constructor) to avoid changing all existing call-sites that accesspicked_node.node.