并发树搜索有一种常见浪费:第一个worker选中叶节点、正在等评估器,其他worker看到的统计量还没更新,于是继续挤向同一条路径。虚拟损失会临时降低正在探索路径的吸引力,让后来的worker有机会选择其他分支。
问题在于“临时”二字。worker报错或退出时,预约若没有撤销,这条分支就可能长期背着一笔不存在的损失。吞吐没有提高,统计量反而坏了。
把真实访问和在途预约分开
有些实现直接增加访问次数、减少累计价值,评估结束后再反向补偿。这种写法可以成立,但每个异常出口都得记得补回。教学实现里,我倾向于分别保存visits、total_value和inflight。选择公式读取它们的组合,真实结果与临时预约各自有账。
下面只实现单节点预约生命周期,不声称是一套完整MCTS。它不训练网络、不调用GPU,0.5是测试时指定的值,不是模型预测。
from concurrent.futures import ThreadPoolExecutor from dataclasses import dataclass, field from threading import Lock @dataclass class Node: visits: int = 0 total_value: float = 0.0 inflight: int = 0 lock: Lock = field(default_factory=Lock, repr=False) class Ticket: def __init__(self, node): self.node = node self.closed = False with node.lock: node.inflight += 1 def finish(self, value=None): with self.node.lock: if self.closed: return False self.closed = True self.node.inflight -= 1 if value is not None: self.node.visits += 1 self.node.total_value += value return True def work(ticket, mode): try: if mode == "error": raise ValueError("intentional evaluator failure") if mode == "cancel": # 合作式取消在评估前被观察到。 return ticket.finish(0.5) finally: ticket.finish() # 已提交的ticket不会被再次回滚。 node = Node() tickets = [Ticket(node) for _ in range(3)] assert node.inflight == 3 errors = 0 with ThreadPoolExecutor(max_workers=3) as pool: futures = [pool.submit(work, t, mode) for t, mode in zip( tickets, ("success", "error", "cancel"), )] for future in futures: try: future.result() except ValueError: errors += 1 assert (node.visits, node.total_value, node.inflight) == (1, 0.5, 0) assert errors == 1 assert all(not t.finish() for t in tickets) print("reservation checks passed")closed与节点统计量由同一把锁保护,完成操作只能发生一次。成功先提交,finally再调用时不做事;异常或合作式取消则只减少在途数。这比“每条路径都减一遍”多了一点状态,却能直接表达不变量。
线程没结束时,不能假装预约已经撤销
Future.result(timeout=...)只限制调用者等待,并不保证正在运行的worker停止。如果调用者超时就回滚,worker稍后又提交,计数就可能重复。因此,真正接入异步评估器时,还要规定谁拥有ticket、谁有权宣布终态,以及迟到结果是否仍然可提交。
一个选择是为每次预约分配代号,取消时把它置为终态,迟到结果只能写审计记录,不能再次改树。另一个选择是等待worker确认取消。两种方式都有代价,不能把本例的合作式检查当成远端推理任务已经停止。
完整搜索还缺状态转移、终止节点、策略先验、价值视角、路径回传、重复状态处理与扩展互斥。用于双人零和博弈时,相邻层价值视角也要核对,不能把一个正值原样传给所有节点就算完成AlphaZero复现。AlphaZero原始论文是历史背景,不证明当前闭源推理模型使用了这套内部算法。
如果以后加入GPU批处理,再单独测真实评估器的预热、同步、batch等待时间和队列积压。PyTorch的CUDA语义文档可以帮助核对异步计时与生命周期。sleep(0.005)只是在模拟等待,不能据此给出4090利用率或每秒节点数。
这篇先交付一个更小的保证:成功、异常和已观察到的取消结束后,临时预约都不再残留。搜索质量与GPU吞吐需要下一层实验,不能由计数守恒代替。
本地核验:将上方代码保存为example.py,在 Python 3.9.6、仅标准库的临时目录执行python3 example.py,退出码为 0,输出reservation checks passed。这验证了教学示例中的断言,不代表模型、GPU或线上系统实测。
本文未运行神经网络、GPU批处理或完整MCTS基准。