treequest 0.1.2__tar.gz → 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {treequest-0.1.2 → treequest-0.2.0}/.gitignore +3 -0
- {treequest-0.1.2 → treequest-0.2.0}/PKG-INFO +52 -1
- {treequest-0.1.2 → treequest-0.2.0}/README.md +49 -0
- treequest-0.2.0/docs/PROFILING.md +98 -0
- treequest-0.2.0/images/batch/abmcts_m_batch_size_1.jpg +0 -0
- treequest-0.2.0/images/batch/abmcts_m_batch_size_10.jpg +0 -0
- treequest-0.2.0/images/batch/abmcts_m_batch_size_2.jpg +0 -0
- treequest-0.2.0/images/batch/abmcts_m_batch_size_5.jpg +0 -0
- {treequest-0.1.2 → treequest-0.2.0}/pyproject.toml +3 -1
- {treequest-0.1.2 → treequest-0.2.0}/src/treequest/__init__.py +3 -3
- {treequest-0.1.2 → treequest-0.2.0}/src/treequest/algos/ab_mcts_a/algo.py +107 -102
- {treequest-0.1.2 → treequest-0.2.0}/src/treequest/algos/ab_mcts_m/algo.py +152 -91
- treequest-0.2.0/src/treequest/algos/ab_mcts_m/numpyro_utils.py +13 -0
- {treequest-0.1.2 → treequest-0.2.0}/src/treequest/algos/ab_mcts_m/pymc_interface.py +4 -12
- {treequest-0.1.2 → treequest-0.2.0}/src/treequest/algos/base.py +27 -0
- {treequest-0.1.2 → treequest-0.2.0}/src/treequest/algos/best_first_search.py +76 -27
- {treequest-0.1.2 → treequest-0.2.0}/src/treequest/algos/multi_armed_bandit_ucb.py +64 -28
- {treequest-0.1.2 → treequest-0.2.0}/src/treequest/algos/standard_mcts.py +61 -37
- {treequest-0.1.2 → treequest-0.2.0}/src/treequest/algos/tree_of_thought_bfs.py +66 -33
- treequest-0.2.0/src/treequest/trial.py +240 -0
- treequest-0.2.0/tests/profiling/ab_mcts_a.py +101 -0
- treequest-0.2.0/tests/profiling/ab_mcts_m.py +114 -0
- {treequest-0.1.2 → treequest-0.2.0}/tests/test_ab_mcts_a.py +16 -51
- treequest-0.2.0/tests/test_ask_batch.py +111 -0
- treequest-0.2.0/tests/test_e2e_batch_flow.py +71 -0
- treequest-0.2.0/tests/test_interleaved_ask_tell.py +123 -0
- treequest-0.2.0/tests/test_next_nodes_behavior.py +139 -0
- treequest-0.2.0/tests/test_reflection_logic.py +166 -0
- {treequest-0.1.2 → treequest-0.2.0}/tests/test_tot_bfs.py +8 -7
- treequest-0.2.0/tests/test_trial_tell_idempotent.py +77 -0
- treequest-0.2.0/uv.lock +1493 -0
- treequest-0.1.2/images/figure1.png +0 -0
- treequest-0.1.2/uv.lock +0 -1345
- {treequest-0.1.2 → treequest-0.2.0}/.github/workflows/checks.yml +0 -0
- {treequest-0.1.2 → treequest-0.2.0}/.github/workflows/release.yml +0 -0
- {treequest-0.1.2 → treequest-0.2.0}/.github/workflows/tests.yml +0 -0
- {treequest-0.1.2 → treequest-0.2.0}/.pre-commit-config.yaml +0 -0
- {treequest-0.1.2 → treequest-0.2.0}/.python-version +0 -0
- {treequest-0.1.2 → treequest-0.2.0}/CONTRIBUTING.md +0 -0
- {treequest-0.1.2 → treequest-0.2.0}/LICENSE +0 -0
- {treequest-0.1.2 → treequest-0.2.0}/pytest.ini +0 -0
- {treequest-0.1.2 → treequest-0.2.0}/src/treequest/algos/ab_mcts_a/prob_state.py +0 -0
- {treequest-0.1.2 → treequest-0.2.0}/src/treequest/algos/ab_mcts_m/_ab_mcts_m_imports.py +0 -0
- {treequest-0.1.2 → treequest-0.2.0}/src/treequest/algos/tree.py +0 -0
- {treequest-0.1.2 → treequest-0.2.0}/src/treequest/imports.py +0 -0
- {treequest-0.1.2 → treequest-0.2.0}/src/treequest/py.typed +0 -0
- {treequest-0.1.2 → treequest-0.2.0}/src/treequest/ranker.py +0 -0
- {treequest-0.1.2 → treequest-0.2.0}/src/treequest/types.py +0 -0
- {treequest-0.1.2 → treequest-0.2.0}/src/treequest/visualization.py +0 -0
- {treequest-0.1.2 → treequest-0.2.0}/tests/__init__.py +0 -0
- {treequest-0.1.2 → treequest-0.2.0}/tests/test_ab_mcts_a_visualization.py +0 -0
- {treequest-0.1.2 → treequest-0.2.0}/tests/test_ab_mcts_m_algo.py +0 -0
- {treequest-0.1.2 → treequest-0.2.0}/tests/test_ab_mcts_m_algo_advanced.py +0 -0
- {treequest-0.1.2 → treequest-0.2.0}/tests/test_best_first_search.py +0 -0
- {treequest-0.1.2 → treequest-0.2.0}/tests/test_interface.py +0 -0
- {treequest-0.1.2 → treequest-0.2.0}/tests/test_multi_armed_bandit_ucb.py +0 -0
- {treequest-0.1.2 → treequest-0.2.0}/tests/test_standard_mcts.py +0 -0
- {treequest-0.1.2 → treequest-0.2.0}/tests/test_tree.py +0 -0
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: treequest
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: A Tree Search Library with Flexible API for LLM Inference-Time Scaling
|
|
5
5
|
Author-email: Kou Misaki <kou.misaki@sakana.ai>, Takuya Akiba <takiba@sakana.ai>
|
|
6
6
|
License-File: LICENSE
|
|
7
7
|
Requires-Python: >=3.11
|
|
8
|
+
Requires-Dist: joblib>=1.5.2
|
|
8
9
|
Requires-Dist: scipy>=1.0.0
|
|
10
|
+
Requires-Dist: ulid-py>=1.1.0
|
|
9
11
|
Provides-Extra: abmcts-m
|
|
10
12
|
Requires-Dist: jax>=0.5.2; extra == 'abmcts-m'
|
|
11
13
|
Requires-Dist: numpy>=1.26.4; extra == 'abmcts-m'
|
|
@@ -62,6 +64,44 @@ best_state, best_node_score = tq.top_k(search_tree, algo, k=1)[0]
|
|
|
62
64
|
print(f"Best state: {best_state}, Score: {best_node_score}")
|
|
63
65
|
```
|
|
64
66
|
|
|
67
|
+
Alternatively, you can use an ask–tell interface with batched AB-MCTS sampling steps:
|
|
68
|
+
```python
|
|
69
|
+
import random
|
|
70
|
+
import treequest as tq
|
|
71
|
+
|
|
72
|
+
State = str
|
|
73
|
+
|
|
74
|
+
def generate(parent_state: State | None) -> tuple[State, float]:
|
|
75
|
+
...
|
|
76
|
+
|
|
77
|
+
generate_fns = {"Action A": generate}
|
|
78
|
+
actions = list(generate_fns.keys())
|
|
79
|
+
|
|
80
|
+
# We use batch_size=5 here
|
|
81
|
+
batch_size = 5
|
|
82
|
+
|
|
83
|
+
# It runs AB-MCTS sampling step with 5 processes in parallel
|
|
84
|
+
algo = tq.ABMCTSM(max_process_workers=batch_size)
|
|
85
|
+
search_tree = algo.init_tree()
|
|
86
|
+
|
|
87
|
+
total_budget = 50
|
|
88
|
+
num_steps = total_budget // batch_size
|
|
89
|
+
for _ in range(num_steps):
|
|
90
|
+
# ask_batch returns a list of `Trial` object, which has action, parent_state and trial_id attrs
|
|
91
|
+
search_tree, trials = algo.ask_batch(search_tree, batch_size, actions)
|
|
92
|
+
|
|
93
|
+
for trial in trials:
|
|
94
|
+
result = generate_fns[trial.action](trial.parent_state)
|
|
95
|
+
# Call tell method with trial_id to update search_tree
|
|
96
|
+
search_tree = algo.tell(search_tree, trial.trial_id, result)
|
|
97
|
+
|
|
98
|
+
best_state, best_node_score = tq.top_k(search_tree, algo, k=1)[0]
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
In particular for AB-MCTS-M, each `step` call can be slow. If you encounter slow execution, prefer `ask_batch` over `step`.
|
|
102
|
+
Please note that using a large `batch_size` can skew the search-tree shape (i.e., the tree may become too wide), so it is best to avoid overly large `batch_size`, see [PROFILING.md](./docs/PROFILING.md) for example trees.
|
|
103
|
+
We recommend `batch_size<=5` as a starting point.
|
|
104
|
+
|
|
65
105
|
## Features
|
|
66
106
|
- Easy-to-use API with customizable node generation and node scoring logic.
|
|
67
107
|
- **AB-MCTS-A** and **AB-MCTS-M**, as well as **Multi-LLM AB-MCTS** support (See [our paper](https://arxiv.org/abs/2503.04412) for algorithm details).
|
|
@@ -155,6 +195,17 @@ for _ in range(20):
|
|
|
155
195
|
```
|
|
156
196
|
The variation is not limited to LLM types; you can use different prompts, actions, scoring logic, etc. in `generate_fns`.
|
|
157
197
|
|
|
198
|
+
### Batch Semantics and Concurrency
|
|
199
|
+
- Algorithms are stateless objects; the evolving tree/search state is returned from `init_tree`, `step`, `ask`, and `tell`.
|
|
200
|
+
- `ask_batch(state, batch_size, actions)` returns exactly `batch_size` Trial objects to expand next.
|
|
201
|
+
- Non-queue algorithms (e.g., `ABMCTSM`, `ABMCTSA`, `MultiArmedBanditUCB`) return exactly `batch_size` Trials.
|
|
202
|
+
- Queue-based algorithms (e.g., `StandardMCTS`, `BestFirstSearchAlgo`, `TreeOfThoughtsBFS`) precompute a set of parent/action pairs and duplicate them if needed to fill `batch_size`.
|
|
203
|
+
- `tell(state, trial_id, (new_state, score))` reflects the result for the corresponding Trial.
|
|
204
|
+
- Order-independent: you can call `tell` in any order; reflection is tied to `trial_id`.
|
|
205
|
+
- Idempotent: calling `tell` twice on the same `trial_id` does not add extra nodes.
|
|
206
|
+
- For queue-based algorithms, over-told Trials beyond possible number of childs from a parent node (e.g., `(# actions)*samples_per_action` for StandardMCTS) become `INVALID` and are not reflected.
|
|
207
|
+
- Scores are expected to be normalized to the `[0, 1]` range.
|
|
208
|
+
|
|
158
209
|
## Algorithms
|
|
159
210
|
|
|
160
211
|
### ABMCTS-A: ABMCTS with Node Aggregation
|
|
@@ -44,6 +44,44 @@ best_state, best_node_score = tq.top_k(search_tree, algo, k=1)[0]
|
|
|
44
44
|
print(f"Best state: {best_state}, Score: {best_node_score}")
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
+
Alternatively, you can use an ask–tell interface with batched AB-MCTS sampling steps:
|
|
48
|
+
```python
|
|
49
|
+
import random
|
|
50
|
+
import treequest as tq
|
|
51
|
+
|
|
52
|
+
State = str
|
|
53
|
+
|
|
54
|
+
def generate(parent_state: State | None) -> tuple[State, float]:
|
|
55
|
+
...
|
|
56
|
+
|
|
57
|
+
generate_fns = {"Action A": generate}
|
|
58
|
+
actions = list(generate_fns.keys())
|
|
59
|
+
|
|
60
|
+
# We use batch_size=5 here
|
|
61
|
+
batch_size = 5
|
|
62
|
+
|
|
63
|
+
# It runs AB-MCTS sampling step with 5 processes in parallel
|
|
64
|
+
algo = tq.ABMCTSM(max_process_workers=batch_size)
|
|
65
|
+
search_tree = algo.init_tree()
|
|
66
|
+
|
|
67
|
+
total_budget = 50
|
|
68
|
+
num_steps = total_budget // batch_size
|
|
69
|
+
for _ in range(num_steps):
|
|
70
|
+
# ask_batch returns a list of `Trial` object, which has action, parent_state and trial_id attrs
|
|
71
|
+
search_tree, trials = algo.ask_batch(search_tree, batch_size, actions)
|
|
72
|
+
|
|
73
|
+
for trial in trials:
|
|
74
|
+
result = generate_fns[trial.action](trial.parent_state)
|
|
75
|
+
# Call tell method with trial_id to update search_tree
|
|
76
|
+
search_tree = algo.tell(search_tree, trial.trial_id, result)
|
|
77
|
+
|
|
78
|
+
best_state, best_node_score = tq.top_k(search_tree, algo, k=1)[0]
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
In particular for AB-MCTS-M, each `step` call can be slow. If you encounter slow execution, prefer `ask_batch` over `step`.
|
|
82
|
+
Please note that using a large `batch_size` can skew the search-tree shape (i.e., the tree may become too wide), so it is best to avoid overly large `batch_size`, see [PROFILING.md](./docs/PROFILING.md) for example trees.
|
|
83
|
+
We recommend `batch_size<=5` as a starting point.
|
|
84
|
+
|
|
47
85
|
## Features
|
|
48
86
|
- Easy-to-use API with customizable node generation and node scoring logic.
|
|
49
87
|
- **AB-MCTS-A** and **AB-MCTS-M**, as well as **Multi-LLM AB-MCTS** support (See [our paper](https://arxiv.org/abs/2503.04412) for algorithm details).
|
|
@@ -137,6 +175,17 @@ for _ in range(20):
|
|
|
137
175
|
```
|
|
138
176
|
The variation is not limited to LLM types; you can use different prompts, actions, scoring logic, etc. in `generate_fns`.
|
|
139
177
|
|
|
178
|
+
### Batch Semantics and Concurrency
|
|
179
|
+
- Algorithms are stateless objects; the evolving tree/search state is returned from `init_tree`, `step`, `ask`, and `tell`.
|
|
180
|
+
- `ask_batch(state, batch_size, actions)` returns exactly `batch_size` Trial objects to expand next.
|
|
181
|
+
- Non-queue algorithms (e.g., `ABMCTSM`, `ABMCTSA`, `MultiArmedBanditUCB`) return exactly `batch_size` Trials.
|
|
182
|
+
- Queue-based algorithms (e.g., `StandardMCTS`, `BestFirstSearchAlgo`, `TreeOfThoughtsBFS`) precompute a set of parent/action pairs and duplicate them if needed to fill `batch_size`.
|
|
183
|
+
- `tell(state, trial_id, (new_state, score))` reflects the result for the corresponding Trial.
|
|
184
|
+
- Order-independent: you can call `tell` in any order; reflection is tied to `trial_id`.
|
|
185
|
+
- Idempotent: calling `tell` twice on the same `trial_id` does not add extra nodes.
|
|
186
|
+
- For queue-based algorithms, over-told Trials beyond possible number of childs from a parent node (e.g., `(# actions)*samples_per_action` for StandardMCTS) become `INVALID` and are not reflected.
|
|
187
|
+
- Scores are expected to be normalized to the `[0, 1]` range.
|
|
188
|
+
|
|
140
189
|
## Algorithms
|
|
141
190
|
|
|
142
191
|
### ABMCTS-A: ABMCTS with Node Aggregation
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# Performance Investigation of `ask_batch` method
|
|
2
|
+
Here, we report the profiling result of `ask_batch` with different `batch_size`. As we increase `batch_size`, AB-MCTS runs are accelerated, while the search tree shapes get more wide.
|
|
3
|
+
|
|
4
|
+
I profiled TreeQuest on a 32-core x86-64 machine running Ubuntu 20.04. As for AB-MCTS-M, it runs 4 parallel MCMC chains per process, so I tested `batch_size` from 1 to 10.
|
|
5
|
+
|
|
6
|
+
## Profiling Commands
|
|
7
|
+
|
|
8
|
+
### AB-MCTS-M
|
|
9
|
+
```bash
|
|
10
|
+
hyperfine \
|
|
11
|
+
'uv run tests/profiling/ab_mcts_m.py -b 1' \
|
|
12
|
+
'uv run tests/profiling/ab_mcts_m.py -b 2' \
|
|
13
|
+
'uv run tests/profiling/ab_mcts_m.py -b 5' \
|
|
14
|
+
'uv run tests/profiling/ab_mcts_m.py -b 10' \
|
|
15
|
+
-w 0 -r 3 --export-markdown tests/profiling/benchmark_m.md
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
### AB-MCTS-A
|
|
19
|
+
```bash
|
|
20
|
+
hyperfine \
|
|
21
|
+
'uv run tests/profiling/ab_mcts_a.py -b 1' \
|
|
22
|
+
'uv run tests/profiling/ab_mcts_a.py -b 2' \
|
|
23
|
+
'uv run tests/profiling/ab_mcts_a.py -b 5' \
|
|
24
|
+
'uv run tests/profiling/ab_mcts_a.py -b 10' \
|
|
25
|
+
-w 0 -r 3 --export-markdown tests/profiling/benchmark_a.md
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Analysis for various `batch_size`
|
|
29
|
+
### AB-MCTS-M
|
|
30
|
+
#### Time Profiling
|
|
31
|
+
For a search tree with (# non-root nodes) = 50,
|
|
32
|
+
| Command | Mean [s] | Min [s] | Max [s] | Relative |
|
|
33
|
+
|:---|---:|---:|---:|---:|
|
|
34
|
+
| `uv run tests/profiling/ab_mcts_m.py -b 1` | 372.156 ± 37.808 | 342.916 | 414.852 | 6.98 ± 0.72 |
|
|
35
|
+
| `uv run tests/profiling/ab_mcts_m.py -b 2` | 202.324 ± 8.006 | 194.178 | 210.182 | 3.80 ± 0.16 |
|
|
36
|
+
| `uv run tests/profiling/ab_mcts_m.py -b 5` | 91.418 ± 2.391 | 90.026 | 94.179 | 1.71 ± 0.05 |
|
|
37
|
+
| `uv run tests/profiling/ab_mcts_m.py -b 10` | 53.307 ± 0.747 | 52.468 | 53.902 | 1.00 |
|
|
38
|
+
|
|
39
|
+
> ![NOTE]
|
|
40
|
+
> For more resource-constrained systems, we get more mild boost. I ran profiling for M3 MacBook Air with 4 Pcores and 4 Ecores:
|
|
41
|
+
> | Command | Mean [s] | Min [s] | Max [s] | Relative |
|
|
42
|
+
> |:---|---:|---:|---:|---:|
|
|
43
|
+
> | `uv run tests/profiling/ab_mcts_m.py -b 1` | 147.564 ± 4.527 | 144.297 | 152.731 | 1.90 ± 0.08 |
|
|
44
|
+
> | `uv run tests/profiling/ab_mcts_m.py -b 2` | 116.557 ± 3.015 | 113.391 | 119.395 | 1.50 ± 0.06 |
|
|
45
|
+
> | `uv run tests/profiling/ab_mcts_m.py -b 5` | 90.628 ± 1.760 | 89.299 | 92.624 | 1.17 ± 0.04 |
|
|
46
|
+
> | `uv run tests/profiling/ab_mcts_m.py -b 10` | 77.773 ± 2.050 | 75.819 | 79.908 | 1.00 |
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
We note that different batch size leads to different tree shape; The larger batch size generates wider search tree:
|
|
50
|
+
|
|
51
|
+
#### `batch_size=1`
|
|
52
|
+

|
|
53
|
+
|
|
54
|
+
#### `batch_size=2`
|
|
55
|
+

|
|
56
|
+
|
|
57
|
+
#### `batch_size=5`
|
|
58
|
+

|
|
59
|
+
|
|
60
|
+
#### `batch_size=10`
|
|
61
|
+

|
|
62
|
+
|
|
63
|
+
## Analysis for various `batch_size`
|
|
64
|
+
### AB-MCTS-A
|
|
65
|
+
#### Time Profiling
|
|
66
|
+
AB-MCTS-A is faster than AB-MCTS-M, so I tested (# non-root nodes) = 1000:
|
|
67
|
+
| Command | Mean [s] | Min [s] | Max [s] | Relative |
|
|
68
|
+
|:---|---:|---:|---:|---:|
|
|
69
|
+
| `uv run tests/profiling/ab_mcts_a.py -b 1` | 26.116 ± 0.656 | 25.677 | 26.871 | 1.00 |
|
|
70
|
+
| `uv run tests/profiling/ab_mcts_a.py -b 2` | 26.191 ± 0.709 | 25.599 | 26.976 | 1.00 ± 0.04 |
|
|
71
|
+
| `uv run tests/profiling/ab_mcts_a.py -b 5` | 26.404 ± 0.441 | 26.061 | 26.902 | 1.01 ± 0.03 |
|
|
72
|
+
| `uv run tests/profiling/ab_mcts_a.py -b 10` | 26.399 ± 0.364 | 25.998 | 26.709 | 1.01 ± 0.03 |
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
> [!NOTE]
|
|
76
|
+
> I didn't parallelize AB-MCTS-A, so the command running time is the same for different batch sizes.
|
|
77
|
+
> Intially I also implemented parallel run for AB-MCTS-A as well, but it didn't lead to much speedup:
|
|
78
|
+
> | Command | Mean [s] | Min [s] | Max [s] | Relative |
|
|
79
|
+
> |:---|---:|---:|---:|---:|
|
|
80
|
+
> | `uv run tests/profiling/ab_mcts_a.py -b 1` | 49.973 ± 2.909 | 47.645 | 53.235 | 2.79 ± 0.16 |
|
|
81
|
+
> | `uv run tests/profiling/ab_mcts_a.py -b 2` | 30.971 ± 0.546 | 30.528 | 31.581 | 1.73 ± 0.03 |
|
|
82
|
+
> | `uv run tests/profiling/ab_mcts_a.py -b 5` | 21.256 ± 0.706 | 20.732 | 22.058 | 1.18 ± 0.04 |
|
|
83
|
+
> | `uv run tests/profiling/ab_mcts_a.py -b 10` | 17.941 ± 0.110 | 17.867 | 18.068 | 1.00 |
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
We note that different batch size leads to different tree shape; The larger batch size generates wider search tree:
|
|
87
|
+
|
|
88
|
+
#### `batch_size=1`
|
|
89
|
+

|
|
90
|
+
|
|
91
|
+
#### `batch_size=2`
|
|
92
|
+

|
|
93
|
+
|
|
94
|
+
#### `batch_size=5`
|
|
95
|
+

|
|
96
|
+
|
|
97
|
+
#### `batch_size=10`
|
|
98
|
+

|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "treequest"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.2.0"
|
|
4
4
|
description = "A Tree Search Library with Flexible API for LLM Inference-Time Scaling"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [
|
|
@@ -9,7 +9,9 @@ authors = [
|
|
|
9
9
|
]
|
|
10
10
|
requires-python = ">=3.11"
|
|
11
11
|
dependencies = [
|
|
12
|
+
"joblib>=1.5.2",
|
|
12
13
|
"scipy>=1.0.0",
|
|
14
|
+
"ulid-py>=1.1.0",
|
|
13
15
|
]
|
|
14
16
|
|
|
15
17
|
[project.optional-dependencies]
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
from .algos.ab_mcts_m._ab_mcts_m_imports import _import as _ab_mcts_m_import
|
|
2
2
|
|
|
3
|
-
if _ab_mcts_m_import.is_successful():
|
|
4
|
-
from .algos.ab_mcts_m.algo import ABMCTSM
|
|
5
|
-
else:
|
|
3
|
+
if not _ab_mcts_m_import.is_successful():
|
|
6
4
|
# Create a placeholder that raises an informative error when accessed
|
|
7
5
|
class _ABMCTSMPlaceholder:
|
|
8
6
|
def __getattr__(self, name): # type: ignore
|
|
@@ -14,6 +12,8 @@ else:
|
|
|
14
12
|
raise ImportError("ABMCTSM import failed.")
|
|
15
13
|
|
|
16
14
|
ABMCTSM = _ABMCTSMPlaceholder() # type: ignore
|
|
15
|
+
else:
|
|
16
|
+
from .algos.ab_mcts_m.algo import ABMCTSM # type: ignore[assignment]
|
|
17
17
|
|
|
18
18
|
from .algos.ab_mcts_a.algo import ABMCTSA
|
|
19
19
|
from .algos.base import Algorithm
|
|
@@ -8,6 +8,7 @@ from typing import Dict, Generic, List, Literal, Optional, Tuple, TypeVar, Union
|
|
|
8
8
|
from treequest.algos.ab_mcts_a.prob_state import NodeProbState, PriorConfig
|
|
9
9
|
from treequest.algos.base import Algorithm
|
|
10
10
|
from treequest.algos.tree import Node, Tree
|
|
11
|
+
from treequest.trial import Trial, TrialId, TrialStore
|
|
11
12
|
from treequest.types import GenerateFnType, StateScoreType
|
|
12
13
|
|
|
13
14
|
# Type variable for state
|
|
@@ -20,7 +21,7 @@ logger = getLogger(__name__)
|
|
|
20
21
|
class ABMCTSAStateManager(Generic[StateT]):
|
|
21
22
|
"""Manager for ABMCTSAState instances associated with expand_idx values."""
|
|
22
23
|
|
|
23
|
-
states: Dict[int, NodeProbState] = field(default_factory=dict)
|
|
24
|
+
states: Dict[int, NodeProbState] = field(default_factory=dict[int, NodeProbState])
|
|
24
25
|
default_prior_config: Optional["PriorConfig"] = None
|
|
25
26
|
default_reward_average_priors: Optional[Union[float, Dict[str, float]]] = None
|
|
26
27
|
default_model_selection_strategy: str = "multiarm_bandit_thompson"
|
|
@@ -32,13 +33,13 @@ class ABMCTSAStateManager(Generic[StateT]):
|
|
|
32
33
|
def __len__(self) -> int:
|
|
33
34
|
return len(self.states)
|
|
34
35
|
|
|
35
|
-
def get(self, node: Node) -> Optional[NodeProbState]:
|
|
36
|
+
def get(self, node: Node[StateT]) -> Optional[NodeProbState]:
|
|
36
37
|
"""Get thompson state for the given expand_idx if it exists."""
|
|
37
38
|
return self.states.get(node.expand_idx)
|
|
38
39
|
|
|
39
40
|
def create(
|
|
40
41
|
self,
|
|
41
|
-
node: Node,
|
|
42
|
+
node: Node[StateT],
|
|
42
43
|
actions: List[str],
|
|
43
44
|
prior_config: Optional["PriorConfig"] = None,
|
|
44
45
|
reward_average_priors: Optional[Union[float, Dict[str, float]]] = None,
|
|
@@ -65,7 +66,7 @@ class ABMCTSAStateManager(Generic[StateT]):
|
|
|
65
66
|
|
|
66
67
|
def get_or_create(
|
|
67
68
|
self,
|
|
68
|
-
node: Node,
|
|
69
|
+
node: Node[StateT],
|
|
69
70
|
actions: List[str],
|
|
70
71
|
prior_config: Optional["PriorConfig"] = None,
|
|
71
72
|
reward_average_priors: Optional[Union[float, Dict[str, float]]] = None,
|
|
@@ -96,10 +97,13 @@ class ABMCTSAAlgoState(Generic[StateT]):
|
|
|
96
97
|
"""State for ABMCTSA algorithm."""
|
|
97
98
|
|
|
98
99
|
tree: Tree[StateT]
|
|
99
|
-
thompson_states: ABMCTSAStateManager = field(
|
|
100
|
+
thompson_states: ABMCTSAStateManager[StateT] = field(
|
|
101
|
+
default_factory=ABMCTSAStateManager[StateT]
|
|
102
|
+
)
|
|
100
103
|
all_rewards_store: Dict[str, List[float]] = field(
|
|
101
104
|
default_factory=build_default_dict_of_list
|
|
102
105
|
)
|
|
106
|
+
trial_store: TrialStore[StateT] = field(default_factory=TrialStore[StateT])
|
|
103
107
|
|
|
104
108
|
|
|
105
109
|
class ABMCTSA(Algorithm[StateT, ABMCTSAAlgoState[StateT]]):
|
|
@@ -155,14 +159,14 @@ class ABMCTSA(Algorithm[StateT, ABMCTSAAlgoState[StateT]]):
|
|
|
155
159
|
)
|
|
156
160
|
self.model_selection_strategy = model_selection_strategy
|
|
157
161
|
|
|
158
|
-
def init_tree(self) -> ABMCTSAAlgoState:
|
|
162
|
+
def init_tree(self) -> ABMCTSAAlgoState[StateT]:
|
|
159
163
|
"""
|
|
160
164
|
Initialize the algorithm state with an empty tree.
|
|
161
165
|
|
|
162
166
|
Returns:
|
|
163
167
|
Initial algorithm state
|
|
164
168
|
"""
|
|
165
|
-
tree: Tree = Tree.with_root_node()
|
|
169
|
+
tree: Tree[StateT] = Tree.with_root_node()
|
|
166
170
|
state_manager = ABMCTSAStateManager[StateT](
|
|
167
171
|
default_prior_config=self.prior_config,
|
|
168
172
|
default_reward_average_priors=self.reward_average_priors,
|
|
@@ -172,55 +176,56 @@ class ABMCTSA(Algorithm[StateT, ABMCTSAAlgoState[StateT]]):
|
|
|
172
176
|
|
|
173
177
|
def step(
|
|
174
178
|
self,
|
|
175
|
-
state: ABMCTSAAlgoState,
|
|
179
|
+
state: ABMCTSAAlgoState[StateT],
|
|
176
180
|
generate_fn: Mapping[str, GenerateFnType[StateT]],
|
|
177
181
|
inplace: bool = False,
|
|
178
|
-
) -> ABMCTSAAlgoState:
|
|
182
|
+
) -> ABMCTSAAlgoState[StateT]:
|
|
179
183
|
"""
|
|
180
|
-
Perform one step of the
|
|
181
|
-
|
|
182
|
-
Args:
|
|
183
|
-
state: Current algorithm state
|
|
184
|
-
generate_fn: Mapping of action names to generation functions
|
|
185
|
-
|
|
186
|
-
Returns:
|
|
187
|
-
Updated algorithm state
|
|
184
|
+
Perform one step of the AB-MCTS-A algorithm and generate a new node.
|
|
188
185
|
"""
|
|
189
186
|
if not inplace:
|
|
190
187
|
state = copy.deepcopy(state)
|
|
191
188
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
for action in generate_fn:
|
|
195
|
-
state.all_rewards_store[action] = []
|
|
189
|
+
actions = list(generate_fn.keys())
|
|
190
|
+
state, trial = self.ask(state, actions)
|
|
196
191
|
|
|
192
|
+
action = trial.action
|
|
193
|
+
node = state.tree.get_node(trial.node_to_expand)
|
|
194
|
+
result = generate_fn[action](node.state)
|
|
195
|
+
|
|
196
|
+
self.tell(state, trial.trial_id, result)
|
|
197
|
+
return state
|
|
198
|
+
|
|
199
|
+
def _get_expand_node_and_action(
|
|
200
|
+
self,
|
|
201
|
+
state: ABMCTSAAlgoState[StateT],
|
|
202
|
+
actions: list[str],
|
|
203
|
+
) -> tuple[Node[StateT], str]:
|
|
197
204
|
# If the tree is empty (only root), expand the root
|
|
198
205
|
if not state.tree.root.children:
|
|
199
|
-
|
|
200
|
-
|
|
206
|
+
return state.tree.root, self._get_generation_action(
|
|
207
|
+
state, state.tree.root, actions
|
|
208
|
+
)
|
|
201
209
|
|
|
202
210
|
# Run one simulation step
|
|
203
211
|
node = state.tree.root
|
|
204
212
|
|
|
205
213
|
# Selection phase: traverse tree until we reach a leaf node or need to create a new node
|
|
206
214
|
while node.children:
|
|
207
|
-
node,
|
|
208
|
-
|
|
209
|
-
# If action is not None, it means we've generated a new node
|
|
210
|
-
if action_used is not None:
|
|
211
|
-
return state
|
|
215
|
+
node, action = self._select_child(state, node, actions)
|
|
212
216
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
+
# If action is not None, we will generate a new node from `node``
|
|
218
|
+
if action is not None:
|
|
219
|
+
return node, action
|
|
220
|
+
action = self._get_generation_action(state, node, actions)
|
|
221
|
+
return node, action
|
|
217
222
|
|
|
218
223
|
def _select_child(
|
|
219
224
|
self,
|
|
220
|
-
state: ABMCTSAAlgoState,
|
|
221
|
-
node: Node,
|
|
222
|
-
|
|
223
|
-
) -> Tuple[Node, Optional[str]]:
|
|
225
|
+
state: ABMCTSAAlgoState[StateT],
|
|
226
|
+
node: Node[StateT],
|
|
227
|
+
actions: list[str],
|
|
228
|
+
) -> Tuple[Node[StateT], Optional[str]]:
|
|
224
229
|
"""
|
|
225
230
|
Select a child node using Thompson Sampling.
|
|
226
231
|
|
|
@@ -235,7 +240,7 @@ class ABMCTSA(Algorithm[StateT, ABMCTSAAlgoState[StateT]]):
|
|
|
235
240
|
# Get or create thompson state for this node
|
|
236
241
|
thompson_state = state.thompson_states.get_or_create(
|
|
237
242
|
node,
|
|
238
|
-
|
|
243
|
+
actions,
|
|
239
244
|
)
|
|
240
245
|
|
|
241
246
|
# Ask for next node or action using Thompson Sampling
|
|
@@ -243,8 +248,7 @@ class ABMCTSA(Algorithm[StateT, ABMCTSAAlgoState[StateT]]):
|
|
|
243
248
|
|
|
244
249
|
# If string returned, we need to generate a new node with that action
|
|
245
250
|
if isinstance(selection, str):
|
|
246
|
-
|
|
247
|
-
return new_node, selection
|
|
251
|
+
return node, selection
|
|
248
252
|
else:
|
|
249
253
|
# Otherwise, we return the existing child with that index
|
|
250
254
|
if selection >= len(node.children):
|
|
@@ -253,27 +257,13 @@ class ABMCTSA(Algorithm[StateT, ABMCTSAAlgoState[StateT]]):
|
|
|
253
257
|
)
|
|
254
258
|
return node.children[selection], None
|
|
255
259
|
|
|
256
|
-
def
|
|
257
|
-
self,
|
|
258
|
-
|
|
259
|
-
node: Node,
|
|
260
|
-
generate_fn: Mapping[str, GenerateFnType[StateT]],
|
|
261
|
-
) -> Tuple[Node, str]:
|
|
262
|
-
"""
|
|
263
|
-
Expand a leaf node by generating a new child.
|
|
264
|
-
|
|
265
|
-
Args:
|
|
266
|
-
state: Current algorithm state
|
|
267
|
-
node: Node to expand
|
|
268
|
-
generate_fn: Mapping of action names to generation functions
|
|
269
|
-
|
|
270
|
-
Returns:
|
|
271
|
-
Tuple of (new node, action used)
|
|
272
|
-
"""
|
|
260
|
+
def _get_generation_action(
|
|
261
|
+
self, state: ABMCTSAAlgoState[StateT], node: Node[StateT], actions: list[str]
|
|
262
|
+
) -> str:
|
|
273
263
|
# Create thompson state for this node if it doesn't exist
|
|
274
264
|
thompson_state = state.thompson_states.get_or_create(
|
|
275
265
|
node,
|
|
276
|
-
|
|
266
|
+
actions,
|
|
277
267
|
)
|
|
278
268
|
|
|
279
269
|
# Get action to use for generating child
|
|
@@ -285,53 +275,14 @@ class ABMCTSA(Algorithm[StateT, ABMCTSAAlgoState[StateT]]):
|
|
|
285
275
|
f"Something went wrong in ABMCTSA algorithm: selection should always be str when the expansion is from the leaf node, whle got {selection}"
|
|
286
276
|
)
|
|
287
277
|
|
|
288
|
-
|
|
289
|
-
return new_node, selection
|
|
278
|
+
return selection
|
|
290
279
|
|
|
291
|
-
def
|
|
280
|
+
def _backpropagate(
|
|
292
281
|
self,
|
|
293
|
-
state: ABMCTSAAlgoState,
|
|
294
|
-
node: Node,
|
|
295
|
-
|
|
282
|
+
state: ABMCTSAAlgoState[StateT],
|
|
283
|
+
node: Node[StateT],
|
|
284
|
+
score: float,
|
|
296
285
|
action: str,
|
|
297
|
-
) -> Node:
|
|
298
|
-
"""
|
|
299
|
-
Generate a new child node using the specified action.
|
|
300
|
-
|
|
301
|
-
Args:
|
|
302
|
-
state: Current algorithm state
|
|
303
|
-
node: Parent node
|
|
304
|
-
generate_fn: Mapping of action names to generation functions
|
|
305
|
-
action: Name of action to use for generation
|
|
306
|
-
|
|
307
|
-
Returns:
|
|
308
|
-
Newly created node
|
|
309
|
-
"""
|
|
310
|
-
# Generate new state and score using the selected action
|
|
311
|
-
node_state = None if node.is_root() else node.state
|
|
312
|
-
new_state, new_score = generate_fn[action](node_state)
|
|
313
|
-
|
|
314
|
-
# Add new node to the tree
|
|
315
|
-
new_node = state.tree.add_node((new_state, new_score), node)
|
|
316
|
-
|
|
317
|
-
# Update Thompson state with the new node
|
|
318
|
-
thompson_state = state.thompson_states.get(node)
|
|
319
|
-
if thompson_state:
|
|
320
|
-
thompson_state.register_new_child_node(
|
|
321
|
-
action, new_node, self.model_selection_strategy
|
|
322
|
-
)
|
|
323
|
-
else:
|
|
324
|
-
raise RuntimeError(
|
|
325
|
-
f"Internal Error in ABMCTSA: thompson_state should not be None for node {node}"
|
|
326
|
-
)
|
|
327
|
-
|
|
328
|
-
# Backpropagate the score through the parents
|
|
329
|
-
self._backpropagate(state, new_node, new_score, action)
|
|
330
|
-
|
|
331
|
-
return new_node
|
|
332
|
-
|
|
333
|
-
def _backpropagate(
|
|
334
|
-
self, state: ABMCTSAAlgoState, node: Node, score: float, action: str
|
|
335
286
|
) -> None:
|
|
336
287
|
"""
|
|
337
288
|
Update Thompson Sampling statistics for all nodes in the path from node to root.
|
|
@@ -369,7 +320,7 @@ class ABMCTSA(Algorithm[StateT, ABMCTSAAlgoState[StateT]]):
|
|
|
369
320
|
current = current.parent
|
|
370
321
|
|
|
371
322
|
def get_state_score_pairs(
|
|
372
|
-
self, state: ABMCTSAAlgoState
|
|
323
|
+
self, state: ABMCTSAAlgoState[StateT]
|
|
373
324
|
) -> List[StateScoreType[StateT]]:
|
|
374
325
|
"""
|
|
375
326
|
Get all the state-score pairs from the tree.
|
|
@@ -381,3 +332,57 @@ class ABMCTSA(Algorithm[StateT, ABMCTSAAlgoState[StateT]]):
|
|
|
381
332
|
List of (state, score) pairs
|
|
382
333
|
"""
|
|
383
334
|
return state.tree.get_state_score_pairs()
|
|
335
|
+
|
|
336
|
+
def ask_batch(
|
|
337
|
+
self, state: ABMCTSAAlgoState[StateT], batch_size: int, actions: list[str]
|
|
338
|
+
) -> tuple[ABMCTSAAlgoState[StateT], list[Trial[StateT]]]:
|
|
339
|
+
"""
|
|
340
|
+
ABMCTSA is lightweight, so we don't parallelize it.
|
|
341
|
+
TODO: If we need to optimize it, ProcessPoolExecutor seems suffice
|
|
342
|
+
"""
|
|
343
|
+
# initialize all_rewards_store
|
|
344
|
+
if len(state.all_rewards_store) == 0:
|
|
345
|
+
for a in actions:
|
|
346
|
+
state.all_rewards_store[a] = []
|
|
347
|
+
|
|
348
|
+
trials: list[Trial[StateT]] = []
|
|
349
|
+
for _ in range(batch_size):
|
|
350
|
+
node, action = self._get_expand_node_and_action(state, actions)
|
|
351
|
+
trials.append(state.trial_store.create_trial(node, action))
|
|
352
|
+
|
|
353
|
+
return state, trials
|
|
354
|
+
|
|
355
|
+
def tell(
|
|
356
|
+
self,
|
|
357
|
+
state: ABMCTSAAlgoState[StateT],
|
|
358
|
+
trial_id: TrialId,
|
|
359
|
+
result: tuple[StateT, float],
|
|
360
|
+
) -> ABMCTSAAlgoState[StateT]:
|
|
361
|
+
_new_state, new_score = result
|
|
362
|
+
|
|
363
|
+
finished_trial = state.trial_store.get_finished_trial(trial_id, new_score)
|
|
364
|
+
if (
|
|
365
|
+
finished_trial is None
|
|
366
|
+
): # Trial is no longer valid, so we do not reflect the result to state
|
|
367
|
+
return state
|
|
368
|
+
|
|
369
|
+
parent_node = state.tree.get_node(finished_trial.node_to_expand)
|
|
370
|
+
action = finished_trial.action
|
|
371
|
+
# Add new node to the tree
|
|
372
|
+
new_node = state.tree.add_node(result, parent_node)
|
|
373
|
+
|
|
374
|
+
# Update Thompson state with the new node
|
|
375
|
+
thompson_state = state.thompson_states.get(parent_node)
|
|
376
|
+
if thompson_state:
|
|
377
|
+
thompson_state.register_new_child_node(
|
|
378
|
+
action, new_node, self.model_selection_strategy
|
|
379
|
+
)
|
|
380
|
+
else:
|
|
381
|
+
raise RuntimeError(
|
|
382
|
+
f"Internal Error in ABMCTSA: thompson_state should not be None for node {parent_node}"
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
# Backpropagate the score through the parents
|
|
386
|
+
self._backpropagate(state, new_node, new_score, action)
|
|
387
|
+
|
|
388
|
+
return state
|