Deep CFR evaluation 배칭 및 병렬화 추가
This commit is contained in:
@@ -72,6 +72,9 @@ memory:
|
|||||||
evaluation:
|
evaluation:
|
||||||
eval_every: 5
|
eval_every: 5
|
||||||
games: 100
|
games: 100
|
||||||
|
batch_size: 64
|
||||||
|
device: trainer
|
||||||
|
num_workers: 4
|
||||||
max_steps: 1000
|
max_steps: 1000
|
||||||
on_max_steps: score_diff
|
on_max_steps: score_diff
|
||||||
opponents:
|
opponents:
|
||||||
|
|||||||
@@ -0,0 +1,111 @@
|
|||||||
|
# Deep CFR Batched Evaluation 2026-05-07
|
||||||
|
|
||||||
|
Run directory:
|
||||||
|
|
||||||
|
`/mnt/2tbhdd/coolrl-lost-cities-runs/2026-05-07_032944_deep_cfr_batched_eval_cuda_postprocess_1iter`
|
||||||
|
|
||||||
|
Command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run python -m coolrl_lost_cities.games.classic.deep_cfr.cli train \
|
||||||
|
--config configs/deep_cfr/deep_cfr_selfplay_full_depth_slot_playability.yaml \
|
||||||
|
--checkpoint-dir /mnt/2tbhdd/coolrl-lost-cities-runs/2026-05-07_032944_deep_cfr_batched_eval_cuda_postprocess_1iter \
|
||||||
|
--max-iterations 1 \
|
||||||
|
--eval-every 1 \
|
||||||
|
--save-latest-only
|
||||||
|
```
|
||||||
|
|
||||||
|
The config used `evaluation.batch_size: 64` and `evaluation.device: trainer`.
|
||||||
|
The base run device was CUDA.
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
The run completed one training iteration and evaluated immediately.
|
||||||
|
|
||||||
|
| Metric | Value |
|
||||||
|
| --- | ---: |
|
||||||
|
| `iteration_seconds` | 21.322356 |
|
||||||
|
| `evaluation_seconds` | 14.834096 |
|
||||||
|
| `traversal_seconds` | 3.850308 |
|
||||||
|
| `advantage_train_seconds` | 1.778991 |
|
||||||
|
| `strategy_train_seconds` | 0.847232 |
|
||||||
|
|
||||||
|
Compared with the previous batch-size-1 CUDA check, evaluation time changed
|
||||||
|
from about 61.826s to about 14.834s.
|
||||||
|
|
||||||
|
## Opponent Timing
|
||||||
|
|
||||||
|
| Opponent | elapsed | avg len | policy turns | network | postprocess | encoding | legal mask | opponent act |
|
||||||
|
| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
|
||||||
|
| `safe_heuristic_strict` | 3.971 | 1000.0 | 50000 | 0.195 | 0.251 | 0.147 | 0.084 | 3.168 |
|
||||||
|
| `safe_heuristic` | 3.685 | 997.5 | 49876 | 0.199 | 0.257 | 0.148 | 0.084 | 2.868 |
|
||||||
|
| `safe_heuristic_loose` | 3.330 | 980.8 | 49038 | 0.194 | 0.250 | 0.145 | 0.082 | 2.536 |
|
||||||
|
| `noisy_safe` | 2.902 | 955.9 | 47784 | 0.195 | 0.250 | 0.144 | 0.079 | 2.109 |
|
||||||
|
| `random` | 0.797 | 640.4 | 31968 | 0.154 | 0.204 | 0.095 | 0.053 | 0.212 |
|
||||||
|
| `passive_discard` | 0.138 | 172.2 | 8562 | 0.032 | 0.042 | 0.025 | 0.014 | 0.004 |
|
||||||
|
|
||||||
|
## Totals
|
||||||
|
|
||||||
|
| Metric | Value |
|
||||||
|
| --- | ---: |
|
||||||
|
| `eval_elapsed_seconds` | 14.824448 |
|
||||||
|
| `eval_policy_turns` | 237228 |
|
||||||
|
| `eval_policy_network_seconds` | 0.968006 |
|
||||||
|
| `eval_policy_postprocess_seconds` | 1.253108 |
|
||||||
|
| `eval_policy_encoding_seconds` | 0.702534 |
|
||||||
|
| `eval_policy_legal_mask_seconds` | 0.396494 |
|
||||||
|
| `eval_opponent_act_seconds` | 10.896567 |
|
||||||
|
| `eval_apply_action_seconds` | 0.068075 |
|
||||||
|
| `eval_diagnostics_seconds` | 0.060940 |
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
Batched network inference removed the previous batch-size-1 CUDA network
|
||||||
|
bottleneck. After batching, safe heuristic opponent action time became the
|
||||||
|
largest remaining eval cost for safe heuristic opponents.
|
||||||
|
|
||||||
|
The first batched implementation exposed a postprocess synchronization cost
|
||||||
|
from per-row entropy calculation. Moving entropy calculation into torch batch
|
||||||
|
postprocess reduced that cost before this run.
|
||||||
|
|
||||||
|
## Opponent Parallel Evaluation
|
||||||
|
|
||||||
|
Run directory:
|
||||||
|
|
||||||
|
`/mnt/2tbhdd/coolrl-lost-cities-runs/2026-05-07_033925_deep_cfr_batched_eval_parallel_1iter`
|
||||||
|
|
||||||
|
Command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run python -m coolrl_lost_cities.games.classic.deep_cfr.cli train \
|
||||||
|
--config configs/deep_cfr/deep_cfr_selfplay_full_depth_slot_playability.yaml \
|
||||||
|
--checkpoint-dir /mnt/2tbhdd/coolrl-lost-cities-runs/2026-05-07_033925_deep_cfr_batched_eval_parallel_1iter \
|
||||||
|
--max-iterations 1 \
|
||||||
|
--eval-every 1 \
|
||||||
|
--save-latest-only
|
||||||
|
```
|
||||||
|
|
||||||
|
This run used `evaluation.num_workers: 4`, `evaluation.batch_size: 64`, and
|
||||||
|
`evaluation.device: trainer` on CUDA.
|
||||||
|
|
||||||
|
| Metric | Batched sequential | Batched opponent-parallel |
|
||||||
|
| --- | ---: | ---: |
|
||||||
|
| `iteration_seconds` | 21.322356 | 12.854383 |
|
||||||
|
| `evaluation_seconds` | 14.834096 | 6.420402 |
|
||||||
|
| `traversal_seconds` | 3.850308 | 3.852627 |
|
||||||
|
| `advantage_train_seconds` | 1.778991 | 1.746623 |
|
||||||
|
| `strategy_train_seconds` | 0.847232 | 0.819998 |
|
||||||
|
|
||||||
|
Opponent elapsed values from the parallel run:
|
||||||
|
|
||||||
|
| Opponent | elapsed | opponent act | network | avg len |
|
||||||
|
| --- | ---: | ---: | ---: | ---: |
|
||||||
|
| `safe_heuristic_strict` | 4.121 | 3.225 | 0.196 | 1000.0 |
|
||||||
|
| `safe_heuristic` | 4.029 | 2.863 | 0.281 | 997.5 |
|
||||||
|
| `safe_heuristic_loose` | 3.807 | 2.631 | 0.286 | 981.6 |
|
||||||
|
| `noisy_safe` | 3.026 | 2.141 | 0.211 | 951.7 |
|
||||||
|
| `random` | 1.130 | 0.212 | 0.253 | 639.0 |
|
||||||
|
| `passive_discard` | 0.417 | 0.004 | 0.114 | 172.2 |
|
||||||
|
|
||||||
|
Parallel eval reduced measured eval wall time from about 14.83s to about
|
||||||
|
6.42s, roughly `2.31x` faster for this one-iteration profile.
|
||||||
@@ -62,3 +62,92 @@ still smaller than policy network time.
|
|||||||
|
|
||||||
`apply_action_seconds`, `diagnostics_seconds`, and `final_scoring_seconds` were
|
`apply_action_seconds`, `diagnostics_seconds`, and `final_scoring_seconds` were
|
||||||
small in this run.
|
small in this run.
|
||||||
|
|
||||||
|
## CPU vs CUDA Evaluation Check
|
||||||
|
|
||||||
|
This check compared `--device cpu` and `--device cuda` on the same base
|
||||||
|
configuration after the evaluation breakdown metrics were available. The base
|
||||||
|
configuration was:
|
||||||
|
|
||||||
|
`configs/deep_cfr/deep_cfr_selfplay_full_depth_slot_playability.yaml`
|
||||||
|
|
||||||
|
Both runs used one training iteration and ran evaluation on that iteration.
|
||||||
|
The base configuration's evaluation settings were kept at 100 games per
|
||||||
|
opponent and the six configured opponents.
|
||||||
|
|
||||||
|
CPU run directory:
|
||||||
|
|
||||||
|
`/mnt/2tbhdd/coolrl-lost-cities-runs/2026-05-07_eval_device_cpu_1iter`
|
||||||
|
|
||||||
|
CPU command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run python -m coolrl_lost_cities.games.classic.deep_cfr.cli train \
|
||||||
|
--config configs/deep_cfr/deep_cfr_selfplay_full_depth_slot_playability.yaml \
|
||||||
|
--checkpoint-dir /mnt/2tbhdd/coolrl-lost-cities-runs/2026-05-07_eval_device_cpu_1iter \
|
||||||
|
--max-iterations 1 \
|
||||||
|
--eval-every 1 \
|
||||||
|
--save-latest-only \
|
||||||
|
--device cpu
|
||||||
|
```
|
||||||
|
|
||||||
|
CUDA run directory:
|
||||||
|
|
||||||
|
`/mnt/2tbhdd/coolrl-lost-cities-runs/2026-05-07_eval_device_cuda_1iter`
|
||||||
|
|
||||||
|
CUDA command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run python -m coolrl_lost_cities.games.classic.deep_cfr.cli train \
|
||||||
|
--config configs/deep_cfr/deep_cfr_selfplay_full_depth_slot_playability.yaml \
|
||||||
|
--checkpoint-dir /mnt/2tbhdd/coolrl-lost-cities-runs/2026-05-07_eval_device_cuda_1iter \
|
||||||
|
--max-iterations 1 \
|
||||||
|
--eval-every 1 \
|
||||||
|
--save-latest-only \
|
||||||
|
--device cuda
|
||||||
|
```
|
||||||
|
|
||||||
|
Top-level timing:
|
||||||
|
|
||||||
|
| Metric | CPU | CUDA | CUDA / CPU |
|
||||||
|
| --- | ---: | ---: | ---: |
|
||||||
|
| `iteration_seconds` | 50.969 | 72.501 | 1.42 |
|
||||||
|
| `evaluation_seconds` | 38.920 | 61.826 | 1.59 |
|
||||||
|
| `traversal_seconds` | 6.343 | 6.409 | 1.01 |
|
||||||
|
| `advantage_train_seconds` | 4.054 | 2.884 | 0.71 |
|
||||||
|
| `strategy_train_seconds` | 1.643 | 1.369 | 0.83 |
|
||||||
|
|
||||||
|
Opponent elapsed timing:
|
||||||
|
|
||||||
|
| Opponent | CPU elapsed | CUDA elapsed | CUDA / CPU |
|
||||||
|
| --- | ---: | ---: | ---: |
|
||||||
|
| `random` | 4.038 | 7.220 | 1.79 |
|
||||||
|
| `passive_discard` | 0.990 | 1.773 | 1.79 |
|
||||||
|
| `safe_heuristic` | 8.701 | 13.518 | 1.55 |
|
||||||
|
| `safe_heuristic_loose` | 8.334 | 13.145 | 1.58 |
|
||||||
|
| `safe_heuristic_strict` | 9.097 | 13.882 | 1.53 |
|
||||||
|
| `noisy_safe` | 7.751 | 12.281 | 1.58 |
|
||||||
|
|
||||||
|
Opponent breakdown for the CPU run:
|
||||||
|
|
||||||
|
| Opponent | elapsed | policy turns | network | network / turn | postprocess | encoding | legal mask | opponent act | avg len |
|
||||||
|
| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
|
||||||
|
| `random` | 4.038 | 32170 | 2.401 | 0.075 ms | 0.586 | 0.169 | 0.114 | 0.361 | 644.4 |
|
||||||
|
| `passive_discard` | 0.990 | 8562 | 0.633 | 0.074 ms | 0.155 | 0.044 | 0.034 | 0.010 | 172.2 |
|
||||||
|
| `safe_heuristic` | 8.701 | 49756 | 3.650 | 0.073 ms | 0.903 | 0.253 | 0.211 | 3.068 | 995.1 |
|
||||||
|
| `safe_heuristic_loose` | 8.334 | 48958 | 3.592 | 0.073 ms | 0.886 | 0.248 | 0.207 | 2.798 | 979.2 |
|
||||||
|
| `safe_heuristic_strict` | 9.097 | 50000 | 3.678 | 0.074 ms | 0.910 | 0.254 | 0.214 | 3.420 | 1000.0 |
|
||||||
|
| `noisy_safe` | 7.751 | 47276 | 3.501 | 0.074 ms | 0.864 | 0.244 | 0.195 | 2.354 | 945.7 |
|
||||||
|
| Total | 38.911 | 236722 | 17.456 | 0.074 ms | 4.304 | 1.212 | 0.975 | 12.011 | |
|
||||||
|
|
||||||
|
Opponent breakdown for the CUDA run:
|
||||||
|
|
||||||
|
| Opponent | elapsed | policy turns | network | network / turn | postprocess | encoding | legal mask | opponent act | avg len |
|
||||||
|
| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
|
||||||
|
| `random` | 7.220 | 32096 | 5.337 | 0.166 ms | 0.716 | 0.179 | 0.129 | 0.384 | 642.9 |
|
||||||
|
| `passive_discard` | 1.773 | 8562 | 1.364 | 0.159 ms | 0.185 | 0.047 | 0.039 | 0.012 | 172.2 |
|
||||||
|
| `safe_heuristic` | 13.518 | 49876 | 8.053 | 0.161 ms | 1.097 | 0.274 | 0.239 | 3.139 | 997.5 |
|
||||||
|
| `safe_heuristic_loose` | 13.145 | 49078 | 7.967 | 0.162 ms | 1.080 | 0.271 | 0.236 | 2.890 | 981.6 |
|
||||||
|
| `safe_heuristic_strict` | 13.882 | 50000 | 8.034 | 0.161 ms | 1.096 | 0.274 | 0.239 | 3.524 | 1000.0 |
|
||||||
|
| `noisy_safe` | 12.281 | 47266 | 7.650 | 0.162 ms | 1.044 | 0.262 | 0.219 | 2.421 | 945.5 |
|
||||||
|
| Total | 61.819 | 236878 | 38.406 | 0.162 ms | 5.218 | 1.307 | 1.101 | 12.370 | |
|
||||||
|
|||||||
@@ -0,0 +1,99 @@
|
|||||||
|
# Deep CFR Legacy Runtime Comparison 2026-05-07
|
||||||
|
|
||||||
|
This note records a runtime summary from the older `../coolrl` Lost Cities
|
||||||
|
Deep CFR implementation and compares it with the current profiling runs in
|
||||||
|
this repository.
|
||||||
|
|
||||||
|
## Legacy Run Summary
|
||||||
|
|
||||||
|
The older run completed metrics through iteration 387. The process had stopped
|
||||||
|
before completing iteration 388.
|
||||||
|
|
||||||
|
Total elapsed time through iteration 387:
|
||||||
|
|
||||||
|
`5879.17s`, or about `1h 37m 59s`.
|
||||||
|
|
||||||
|
| Segment | Mean | Median | Note |
|
||||||
|
| --- | ---: | ---: | --- |
|
||||||
|
| All iterations | 15.19s/iter | 11.44s | Includes eval iterations |
|
||||||
|
| Non-eval iterations | 11.51s/iter | 11.32s | Normal training iteration |
|
||||||
|
| Eval iterations | 30.00s/iter | 29.31s | Eval every 5 iterations |
|
||||||
|
| Evaluation only | 18.61s/eval | 18.04s | Early evals were slower |
|
||||||
|
| Traversal | 7.16s/iter | 6.98s | 140 traversals/iter |
|
||||||
|
| Advantage train | 2.81s/iter | 2.78s | Player 0 + player 1 |
|
||||||
|
| Strategy train | 1.44s/iter | 1.44s | |
|
||||||
|
| Overall throughput | 5387 nodes/s | 5437 nodes/s | |
|
||||||
|
| Traversal throughput | 19.8 traversals/s | 20.1 traversals/s | |
|
||||||
|
|
||||||
|
Recent 50 iteration window from that run:
|
||||||
|
|
||||||
|
| Segment | Mean |
|
||||||
|
| --- | ---: |
|
||||||
|
| All iterations | 15.57s/iter |
|
||||||
|
| Non-eval iterations | 12.21s/iter |
|
||||||
|
| Recent 20 evals, eval only | 17.31s/eval |
|
||||||
|
| Recent 20 eval iterations | 29.30s/iter |
|
||||||
|
|
||||||
|
Evaluation ran every 5 iterations: 5, 10, 15, ..., 385.
|
||||||
|
|
||||||
|
The practical legacy cadence was roughly:
|
||||||
|
|
||||||
|
`4 normal iterations + 1 eval iteration ~= 75s per 5 iterations`.
|
||||||
|
|
||||||
|
## Current Repo Reference Points
|
||||||
|
|
||||||
|
From `docs/deep-cfr-profile-advantage-memory-split-2026-05-07.md`:
|
||||||
|
|
||||||
|
| Segment | Current mean |
|
||||||
|
| --- | ---: |
|
||||||
|
| Non-eval iterations | 5.832958s/iter |
|
||||||
|
| Traversal | 3.160975s/iter |
|
||||||
|
| Advantage train | 1.742895s/iter |
|
||||||
|
| Strategy train | 0.912324s/iter |
|
||||||
|
|
||||||
|
From `docs/deep-cfr-batched-evaluation-2026-05-07.md`:
|
||||||
|
|
||||||
|
| Segment | Current value |
|
||||||
|
| --- | ---: |
|
||||||
|
| Batched CUDA evaluation | 14.834096s/eval |
|
||||||
|
| Batched CUDA 1-iter wall time with eval | 21.322356s |
|
||||||
|
| Batched opponent-parallel CUDA evaluation | 6.420402s/eval |
|
||||||
|
| Batched opponent-parallel CUDA 1-iter wall time with eval | 12.854383s |
|
||||||
|
|
||||||
|
## Rough Comparison
|
||||||
|
|
||||||
|
Normal training iterations improved from about `11.51s` to about `5.83s`,
|
||||||
|
roughly `1.97x` faster.
|
||||||
|
|
||||||
|
Traversal improved from about `7.16s` to about `3.16s`, roughly `2.27x`
|
||||||
|
faster.
|
||||||
|
|
||||||
|
Evaluation improved from about `18.61s` to about `14.83s`, roughly `1.25x`
|
||||||
|
faster for the measured batched CUDA profile. With opponent-parallel eval, the
|
||||||
|
measured eval time was about `6.42s`, roughly `2.90x` faster than the legacy
|
||||||
|
eval-only average.
|
||||||
|
|
||||||
|
Using the simple cadence model:
|
||||||
|
|
||||||
|
Legacy:
|
||||||
|
|
||||||
|
`4 * 11.51 + (11.51 + 18.61) = 76.16s per 5 iterations`
|
||||||
|
|
||||||
|
Current batched sequential:
|
||||||
|
|
||||||
|
`4 * 5.83 + (5.83 + 14.83) = 43.98s per 5 iterations`
|
||||||
|
|
||||||
|
Current batched opponent-parallel:
|
||||||
|
|
||||||
|
`4 * 5.83 + (5.83 + 6.42) = 35.57s per 5 iterations`
|
||||||
|
|
||||||
|
That implies about `1.73x` faster eval-included wall time for the batched
|
||||||
|
sequential rough comparison, and about `2.14x` faster for the batched
|
||||||
|
opponent-parallel rough comparison.
|
||||||
|
|
||||||
|
## Caveat
|
||||||
|
|
||||||
|
The legacy numbers came from a long run through iteration 387. The current
|
||||||
|
numbers are from targeted profiling runs. The comparison is useful for order of
|
||||||
|
magnitude and bottleneck direction, not as a strict benchmark under identical
|
||||||
|
runtime conditions.
|
||||||
@@ -232,6 +232,9 @@ class EvaluationConfig(StrictModel):
|
|||||||
opponents: tuple[str, ...] = ("random",)
|
opponents: tuple[str, ...] = ("random",)
|
||||||
max_steps: int = 10_000
|
max_steps: int = 10_000
|
||||||
on_max_steps: str = "score_diff"
|
on_max_steps: str = "score_diff"
|
||||||
|
batch_size: int = 64
|
||||||
|
device: str = "trainer"
|
||||||
|
num_workers: int = 4
|
||||||
|
|
||||||
@field_validator("on_max_steps")
|
@field_validator("on_max_steps")
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -241,6 +244,23 @@ class EvaluationConfig(StrictModel):
|
|||||||
raise ValueError("must be 'score_diff', 'loss', or 'draw'")
|
raise ValueError("must be 'score_diff', 'loss', or 'draw'")
|
||||||
return token
|
return token
|
||||||
|
|
||||||
|
@field_validator("device")
|
||||||
|
@classmethod
|
||||||
|
def _validate_device(cls, value: str) -> str:
|
||||||
|
token = value.strip().lower()
|
||||||
|
if token not in {"trainer", "auto", "cpu", "cuda"}:
|
||||||
|
raise ValueError("must be 'trainer', 'auto', 'cpu', or 'cuda'")
|
||||||
|
return token
|
||||||
|
|
||||||
|
def resolved_batch_size(self) -> int:
|
||||||
|
return max(1, int(self.batch_size))
|
||||||
|
|
||||||
|
def resolved_num_workers(self, opponent_count: int | None = None) -> int:
|
||||||
|
workers = max(1, int(self.num_workers))
|
||||||
|
if opponent_count is not None:
|
||||||
|
workers = min(workers, max(1, int(opponent_count)))
|
||||||
|
return workers
|
||||||
|
|
||||||
|
|
||||||
class DeepCFRConfig(StrictModel):
|
class DeepCFRConfig(StrictModel):
|
||||||
run: RunConfig = Field(default_factory=RunConfig)
|
run: RunConfig = Field(default_factory=RunConfig)
|
||||||
|
|||||||
@@ -159,6 +159,17 @@ class PolicyEvalDiagnostics:
|
|||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class _EvalGame:
|
||||||
|
state: GameState
|
||||||
|
policies: list[LostCitiesPolicy]
|
||||||
|
policy_player: int
|
||||||
|
diagnostics: PolicyEvalDiagnostics
|
||||||
|
first_open_recoverable_by_color: dict[int, float]
|
||||||
|
steps: int = 0
|
||||||
|
done: bool = False
|
||||||
|
|
||||||
|
|
||||||
class StrategyNetPolicy(LostCitiesPolicy):
|
class StrategyNetPolicy(LostCitiesPolicy):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@@ -176,6 +187,51 @@ class StrategyNetPolicy(LostCitiesPolicy):
|
|||||||
self.encoding = encoding
|
self.encoding = encoding
|
||||||
self.runtime = EvalRuntimeCounters()
|
self.runtime = EvalRuntimeCounters()
|
||||||
|
|
||||||
|
def select_actions_batch(self, states: list[GameState]) -> list[tuple[int, float]]:
|
||||||
|
if not states:
|
||||||
|
return []
|
||||||
|
started = time.perf_counter()
|
||||||
|
self.runtime.policy_turns += len(states)
|
||||||
|
|
||||||
|
legal_started = time.perf_counter()
|
||||||
|
legal_masks = [np.asarray(state.unified_legal_mask(), dtype=bool) for state in states]
|
||||||
|
legal_actions_list = [np.flatnonzero(legal) for legal in legal_masks]
|
||||||
|
self.runtime.policy_legal_mask_seconds += time.perf_counter() - legal_started
|
||||||
|
if any(len(legal_actions) == 0 for legal_actions in legal_actions_list):
|
||||||
|
raise RuntimeError("no legal action available")
|
||||||
|
|
||||||
|
encoding_started = time.perf_counter()
|
||||||
|
infos = [encode_info_state(state, state.current_player, self.encoding) for state in states]
|
||||||
|
self.runtime.policy_encoding_seconds += time.perf_counter() - encoding_started
|
||||||
|
|
||||||
|
network_started = time.perf_counter()
|
||||||
|
with torch.inference_mode():
|
||||||
|
x = torch.as_tensor(np.stack(infos), dtype=torch.float32, device=self.device)
|
||||||
|
logits = self.strategy_network(x)
|
||||||
|
self.runtime.policy_network_seconds += time.perf_counter() - network_started
|
||||||
|
|
||||||
|
postprocess_started = time.perf_counter()
|
||||||
|
legal_tensor = torch.as_tensor(np.stack(legal_masks), dtype=torch.bool, device=self.device)
|
||||||
|
masked = logits.masked_fill(~legal_tensor, torch.finfo(torch.float32).min)
|
||||||
|
probs_tensor = torch.softmax(masked, dim=-1).masked_fill(~legal_tensor, 0.0)
|
||||||
|
entropy_tensor = -(probs_tensor * probs_tensor.clamp_min(1.0e-12).log()).sum(dim=-1)
|
||||||
|
if self.sample:
|
||||||
|
probs_np = probs_tensor.detach().cpu().numpy()
|
||||||
|
unified_actions = [
|
||||||
|
int(self.rng.choice(legal_actions, p=probs_np[index][legal_actions]))
|
||||||
|
for index, legal_actions in enumerate(legal_actions_list)
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
unified_actions = [int(value) for value in torch.argmax(masked, dim=-1).cpu()]
|
||||||
|
entropies = [float(value) for value in entropy_tensor.cpu()]
|
||||||
|
actions = [
|
||||||
|
state.from_unified_action(unified)
|
||||||
|
for state, unified in zip(states, unified_actions, strict=True)
|
||||||
|
]
|
||||||
|
self.runtime.policy_postprocess_seconds += time.perf_counter() - postprocess_started
|
||||||
|
self.runtime.policy_select_seconds += time.perf_counter() - started
|
||||||
|
return list(zip(actions, entropies, strict=True))
|
||||||
|
|
||||||
def action_distribution(self, state: GameState) -> tuple[np.ndarray, np.ndarray]:
|
def action_distribution(self, state: GameState) -> tuple[np.ndarray, np.ndarray]:
|
||||||
started = time.perf_counter()
|
started = time.perf_counter()
|
||||||
legal = np.asarray(state.unified_legal_mask(), dtype=bool)
|
legal = np.asarray(state.unified_legal_mask(), dtype=bool)
|
||||||
@@ -236,6 +292,7 @@ def evaluate_strategy_network(
|
|||||||
device: torch.device | str = "cpu",
|
device: torch.device | str = "cpu",
|
||||||
max_steps: int = 10_000,
|
max_steps: int = 10_000,
|
||||||
encoding: EncodingConfig | None = None,
|
encoding: EncodingConfig | None = None,
|
||||||
|
batch_size: int = 64,
|
||||||
) -> dict[str, float | int]:
|
) -> dict[str, float | int]:
|
||||||
strategy_network.eval()
|
strategy_network.eval()
|
||||||
return _evaluate_strategy_network_with_diagnostics(
|
return _evaluate_strategy_network_with_diagnostics(
|
||||||
@@ -247,6 +304,7 @@ def evaluate_strategy_network(
|
|||||||
device=device,
|
device=device,
|
||||||
max_steps=max_steps,
|
max_steps=max_steps,
|
||||||
encoding=encoding,
|
encoding=encoding,
|
||||||
|
batch_size=batch_size,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -260,35 +318,119 @@ def _evaluate_strategy_network_with_diagnostics(
|
|||||||
device: torch.device | str,
|
device: torch.device | str,
|
||||||
max_steps: int,
|
max_steps: int,
|
||||||
encoding: EncodingConfig | None,
|
encoding: EncodingConfig | None,
|
||||||
|
batch_size: int,
|
||||||
) -> dict[str, float | int]:
|
) -> dict[str, float | int]:
|
||||||
if games <= 0:
|
if games <= 0:
|
||||||
raise ValueError(f"games must be positive, got {games}")
|
raise ValueError(f"games must be positive, got {games}")
|
||||||
diagnostics = PolicyEvalDiagnostics()
|
diagnostics = PolicyEvalDiagnostics()
|
||||||
|
policy = StrategyNetPolicy(
|
||||||
|
strategy_network,
|
||||||
|
device=device,
|
||||||
|
seed=seed * 2,
|
||||||
|
encoding=encoding,
|
||||||
|
)
|
||||||
started = time.perf_counter()
|
started = time.perf_counter()
|
||||||
|
active_games: list[_EvalGame] = []
|
||||||
for index in range(games):
|
for index in range(games):
|
||||||
game_seed = seed + index
|
game_seed = seed + index
|
||||||
swap = index % 2 == 1
|
swap = index % 2 == 1
|
||||||
policy_player = 1 if swap else 0
|
policy_player = 1 if swap else 0
|
||||||
policy = StrategyNetPolicy(
|
|
||||||
strategy_network,
|
|
||||||
device=device,
|
|
||||||
seed=game_seed * 2 + policy_player,
|
|
||||||
encoding=encoding,
|
|
||||||
)
|
|
||||||
opponent_policy = build_bot(opponent, seed=game_seed * 2 + (1 - policy_player))
|
opponent_policy = build_bot(opponent, seed=game_seed * 2 + (1 - policy_player))
|
||||||
policies = [opponent_policy, policy] if swap else [policy, opponent_policy]
|
policies = [opponent_policy, policy] if swap else [policy, opponent_policy]
|
||||||
game_diag = _evaluate_one_game(
|
active_games.append(
|
||||||
policies,
|
_EvalGame(
|
||||||
policy_player,
|
state=GameState.new_game(config, seed=game_seed),
|
||||||
config,
|
policies=policies,
|
||||||
seed=game_seed,
|
policy_player=policy_player,
|
||||||
max_steps=max_steps,
|
diagnostics=PolicyEvalDiagnostics(games=1),
|
||||||
|
first_open_recoverable_by_color={},
|
||||||
)
|
)
|
||||||
game_diag.runtime.accumulate(policy.runtime)
|
)
|
||||||
_accumulate_game_diagnostics(diagnostics, game_diag)
|
|
||||||
|
while active_games:
|
||||||
|
pending_policy_games: list[_EvalGame] = []
|
||||||
|
next_active_games: list[_EvalGame] = []
|
||||||
|
for game in active_games:
|
||||||
|
if _finalize_if_done(game, max_steps=max_steps):
|
||||||
|
_accumulate_game_diagnostics(diagnostics, game.diagnostics)
|
||||||
|
continue
|
||||||
|
state = game.state
|
||||||
|
if state.current_player == game.policy_player and isinstance(
|
||||||
|
game.policies[state.current_player], StrategyNetPolicy
|
||||||
|
):
|
||||||
|
pending_policy_games.append(game)
|
||||||
|
else:
|
||||||
|
_advance_opponent_turn(game)
|
||||||
|
if _finalize_if_done(game, max_steps=max_steps):
|
||||||
|
_accumulate_game_diagnostics(diagnostics, game.diagnostics)
|
||||||
|
else:
|
||||||
|
next_active_games.append(game)
|
||||||
|
|
||||||
|
for start in range(0, len(pending_policy_games), max(1, int(batch_size))):
|
||||||
|
chunk = pending_policy_games[start : start + max(1, int(batch_size))]
|
||||||
|
actions = policy.select_actions_batch([game.state for game in chunk])
|
||||||
|
for game, (action, entropy) in zip(chunk, actions, strict=True):
|
||||||
|
_advance_policy_turn(game, action, entropy)
|
||||||
|
if _finalize_if_done(game, max_steps=max_steps):
|
||||||
|
_accumulate_game_diagnostics(diagnostics, game.diagnostics)
|
||||||
|
else:
|
||||||
|
next_active_games.append(game)
|
||||||
|
|
||||||
|
active_games = next_active_games
|
||||||
|
diagnostics.runtime.accumulate(policy.runtime)
|
||||||
return diagnostics.to_dict(time.perf_counter() - started)
|
return diagnostics.to_dict(time.perf_counter() - started)
|
||||||
|
|
||||||
|
|
||||||
|
def _finalize_if_done(game: _EvalGame, *, max_steps: int) -> bool:
|
||||||
|
if game.done:
|
||||||
|
return True
|
||||||
|
if not game.state.terminal and game.steps < max_steps:
|
||||||
|
return False
|
||||||
|
timed_out = not game.state.terminal
|
||||||
|
if timed_out:
|
||||||
|
game.steps = max_steps
|
||||||
|
final_started = time.perf_counter()
|
||||||
|
_record_final_game_state(
|
||||||
|
game.diagnostics,
|
||||||
|
game.state,
|
||||||
|
game.policy_player,
|
||||||
|
game.steps,
|
||||||
|
timed_out,
|
||||||
|
game.first_open_recoverable_by_color,
|
||||||
|
)
|
||||||
|
game.diagnostics.runtime.final_scoring_seconds += time.perf_counter() - final_started
|
||||||
|
game.done = True
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def _advance_policy_turn(game: _EvalGame, action: int, entropy: float) -> None:
|
||||||
|
game.diagnostics.entropies.append(entropy)
|
||||||
|
diagnostics_started = time.perf_counter()
|
||||||
|
_record_policy_action(
|
||||||
|
game.diagnostics,
|
||||||
|
game.state,
|
||||||
|
action,
|
||||||
|
game.first_open_recoverable_by_color,
|
||||||
|
)
|
||||||
|
game.diagnostics.runtime.diagnostics_seconds += time.perf_counter() - diagnostics_started
|
||||||
|
apply_started = time.perf_counter()
|
||||||
|
game.state.apply_action(action)
|
||||||
|
game.diagnostics.runtime.apply_action_seconds += time.perf_counter() - apply_started
|
||||||
|
game.steps += 1
|
||||||
|
|
||||||
|
|
||||||
|
def _advance_opponent_turn(game: _EvalGame) -> None:
|
||||||
|
state = game.state
|
||||||
|
game.diagnostics.runtime.opponent_turns += 1
|
||||||
|
opponent_started = time.perf_counter()
|
||||||
|
action = game.policies[state.current_player].act(state)
|
||||||
|
game.diagnostics.runtime.opponent_act_seconds += time.perf_counter() - opponent_started
|
||||||
|
apply_started = time.perf_counter()
|
||||||
|
state.apply_action(action)
|
||||||
|
game.diagnostics.runtime.apply_action_seconds += time.perf_counter() - apply_started
|
||||||
|
game.steps += 1
|
||||||
|
|
||||||
|
|
||||||
def _evaluate_one_game(
|
def _evaluate_one_game(
|
||||||
policies: list[LostCitiesPolicy],
|
policies: list[LostCitiesPolicy],
|
||||||
policy_player: int,
|
policy_player: int,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import copy
|
||||||
import json
|
import json
|
||||||
import multiprocessing as mp
|
import multiprocessing as mp
|
||||||
import time
|
import time
|
||||||
@@ -15,7 +16,11 @@ from coolrl_lost_cities.games.classic.deep_cfr.checkpoints import (
|
|||||||
load_checkpoint,
|
load_checkpoint,
|
||||||
save_checkpoint,
|
save_checkpoint,
|
||||||
)
|
)
|
||||||
from coolrl_lost_cities.games.classic.deep_cfr.config import DeepCFRConfig
|
from coolrl_lost_cities.games.classic.deep_cfr.config import (
|
||||||
|
DeepCFRConfig,
|
||||||
|
EncodingConfig,
|
||||||
|
NetworkConfig,
|
||||||
|
)
|
||||||
from coolrl_lost_cities.games.classic.deep_cfr.encoding import input_dim
|
from coolrl_lost_cities.games.classic.deep_cfr.encoding import input_dim
|
||||||
from coolrl_lost_cities.games.classic.deep_cfr.evaluate import evaluate_strategy_network
|
from coolrl_lost_cities.games.classic.deep_cfr.evaluate import evaluate_strategy_network
|
||||||
from coolrl_lost_cities.games.classic.deep_cfr.memory import ReservoirMemory, TrainingSample
|
from coolrl_lost_cities.games.classic.deep_cfr.memory import ReservoirMemory, TrainingSample
|
||||||
@@ -42,6 +47,44 @@ def _resolve_torch_device(device: str) -> torch.device:
|
|||||||
return torch.device(token)
|
return torch.device(token)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class EvaluationWorkerJob:
|
||||||
|
opponent: str
|
||||||
|
strategy_state_dict: dict[str, torch.Tensor]
|
||||||
|
game_config: dict
|
||||||
|
network_config: dict
|
||||||
|
encoding_config: dict
|
||||||
|
input_dim: int
|
||||||
|
action_size: int
|
||||||
|
games: int
|
||||||
|
seed: int
|
||||||
|
device: str
|
||||||
|
max_steps: int
|
||||||
|
batch_size: int
|
||||||
|
|
||||||
|
|
||||||
|
def run_evaluation_worker(job: EvaluationWorkerJob) -> tuple[str, dict[str, float | int]]:
|
||||||
|
device = _resolve_torch_device(job.device)
|
||||||
|
game_config = LostCitiesConfig(**job.game_config)
|
||||||
|
network_config = NetworkConfig.model_validate(job.network_config)
|
||||||
|
encoding_config = EncodingConfig.model_validate(job.encoding_config)
|
||||||
|
network = DeepCFRMLP.from_config(job.input_dim, job.action_size, network_config).to(device)
|
||||||
|
network.load_state_dict(job.strategy_state_dict)
|
||||||
|
network.eval()
|
||||||
|
result = evaluate_strategy_network(
|
||||||
|
network,
|
||||||
|
game_config,
|
||||||
|
games=job.games,
|
||||||
|
seed=job.seed,
|
||||||
|
opponent=job.opponent,
|
||||||
|
device=device,
|
||||||
|
max_steps=job.max_steps,
|
||||||
|
encoding=encoding_config,
|
||||||
|
batch_size=job.batch_size,
|
||||||
|
)
|
||||||
|
return job.opponent, result
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class IterationMetrics:
|
class IterationMetrics:
|
||||||
iteration: int
|
iteration: int
|
||||||
@@ -540,21 +583,88 @@ class DeepCFRTrainer:
|
|||||||
):
|
):
|
||||||
return {}
|
return {}
|
||||||
results: dict[str, float | int] = {}
|
results: dict[str, float | int] = {}
|
||||||
|
eval_device = self._evaluation_device()
|
||||||
|
if self.config.evaluation.resolved_num_workers(len(self.config.evaluation.opponents)) > 1:
|
||||||
|
return self._evaluate_parallel(iteration, eval_device)
|
||||||
|
eval_network = self._evaluation_network(eval_device)
|
||||||
for opponent in self.config.evaluation.opponents:
|
for opponent in self.config.evaluation.opponents:
|
||||||
result = evaluate_strategy_network(
|
result = evaluate_strategy_network(
|
||||||
self.strategy_network,
|
eval_network,
|
||||||
self.game_config,
|
self.game_config,
|
||||||
games=self.config.evaluation.games,
|
games=self.config.evaluation.games,
|
||||||
seed=self.config.run.seed + iteration * 1000,
|
seed=self.config.run.seed + iteration * 1000,
|
||||||
opponent=opponent,
|
opponent=opponent,
|
||||||
device=self.device,
|
device=eval_device,
|
||||||
max_steps=self.config.evaluation.max_steps,
|
max_steps=self.config.evaluation.max_steps,
|
||||||
encoding=self.config.encoding,
|
encoding=self.config.encoding,
|
||||||
|
batch_size=self.config.evaluation.resolved_batch_size(),
|
||||||
)
|
)
|
||||||
for key, value in result.items():
|
for key, value in result.items():
|
||||||
results[f"eval_{opponent}_{key}"] = value
|
results[f"eval_{opponent}_{key}"] = value
|
||||||
return results
|
return results
|
||||||
|
|
||||||
|
def _evaluate_parallel(
|
||||||
|
self,
|
||||||
|
iteration: int,
|
||||||
|
eval_device: torch.device,
|
||||||
|
) -> dict[str, float | int]:
|
||||||
|
opponents = self.config.evaluation.opponents
|
||||||
|
max_workers = self.config.evaluation.resolved_num_workers(len(opponents))
|
||||||
|
self.tracker.log_event(
|
||||||
|
f"Evaluation multiprocessing enabled iteration={iteration} "
|
||||||
|
f"effective_workers={max_workers} opponents={len(opponents)} "
|
||||||
|
f"batch_size={self.config.evaluation.resolved_batch_size()} device={eval_device}"
|
||||||
|
)
|
||||||
|
state_dict = self._strategy_state_dict_cpu()
|
||||||
|
jobs = [
|
||||||
|
EvaluationWorkerJob(
|
||||||
|
opponent=opponent,
|
||||||
|
strategy_state_dict=state_dict,
|
||||||
|
game_config=self.game_config.to_snapshot(),
|
||||||
|
network_config=self.config.network.model_dump(mode="python"),
|
||||||
|
encoding_config=self.config.encoding.model_dump(mode="python"),
|
||||||
|
input_dim=self.input_dim,
|
||||||
|
action_size=self.action_size,
|
||||||
|
games=self.config.evaluation.games,
|
||||||
|
seed=self.config.run.seed + iteration * 1000,
|
||||||
|
device=str(eval_device),
|
||||||
|
max_steps=self.config.evaluation.max_steps,
|
||||||
|
batch_size=self.config.evaluation.resolved_batch_size(),
|
||||||
|
)
|
||||||
|
for opponent in opponents
|
||||||
|
]
|
||||||
|
results: dict[str, float | int] = {}
|
||||||
|
with ProcessPoolExecutor(
|
||||||
|
max_workers=max_workers,
|
||||||
|
mp_context=mp.get_context("spawn"),
|
||||||
|
) as executor:
|
||||||
|
futures = [executor.submit(run_evaluation_worker, job) for job in jobs]
|
||||||
|
for future in as_completed(futures):
|
||||||
|
opponent, result = future.result()
|
||||||
|
for key, value in result.items():
|
||||||
|
results[f"eval_{opponent}_{key}"] = value
|
||||||
|
return results
|
||||||
|
|
||||||
|
def _evaluation_device(self) -> torch.device:
|
||||||
|
token = self.config.evaluation.device
|
||||||
|
if token == "trainer":
|
||||||
|
return self.device
|
||||||
|
if token == "auto":
|
||||||
|
return torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
||||||
|
if token == "cuda" and not torch.cuda.is_available():
|
||||||
|
raise RuntimeError("evaluation.device=cuda requested but CUDA is unavailable")
|
||||||
|
return torch.device(token)
|
||||||
|
|
||||||
|
def _evaluation_network(self, device: torch.device) -> torch.nn.Module:
|
||||||
|
if device == self.device:
|
||||||
|
return self.strategy_network
|
||||||
|
return copy.deepcopy(self.strategy_network).to(device).eval()
|
||||||
|
|
||||||
|
def _strategy_state_dict_cpu(self) -> dict[str, torch.Tensor]:
|
||||||
|
return {
|
||||||
|
key: value.detach().cpu() for key, value in self.strategy_network.state_dict().items()
|
||||||
|
}
|
||||||
|
|
||||||
def _train_advantage_networks(self) -> float:
|
def _train_advantage_networks(self) -> float:
|
||||||
losses: list[float] = []
|
losses: list[float] = []
|
||||||
for player, (network, memory) in enumerate(
|
for player, (network, memory) in enumerate(
|
||||||
|
|||||||
@@ -19,7 +19,9 @@ from coolrl_lost_cities.games.classic.deep_cfr.cli import (
|
|||||||
_with_overrides,
|
_with_overrides,
|
||||||
)
|
)
|
||||||
from coolrl_lost_cities.games.classic.deep_cfr.config import DeepCFRConfig, load_config
|
from coolrl_lost_cities.games.classic.deep_cfr.config import DeepCFRConfig, load_config
|
||||||
|
from coolrl_lost_cities.games.classic.deep_cfr.evaluate import evaluate_strategy_network
|
||||||
from coolrl_lost_cities.games.classic.deep_cfr.memory import ReservoirMemory, TrainingSample
|
from coolrl_lost_cities.games.classic.deep_cfr.memory import ReservoirMemory, TrainingSample
|
||||||
|
from coolrl_lost_cities.games.classic.deep_cfr.networks import DeepCFRMLP
|
||||||
from coolrl_lost_cities.games.classic.deep_cfr.trainer import DeepCFRTrainer
|
from coolrl_lost_cities.games.classic.deep_cfr.trainer import DeepCFRTrainer
|
||||||
|
|
||||||
|
|
||||||
@@ -59,6 +61,9 @@ def test_deep_cfr_loads_mapped_legacy_reproduction_config() -> None:
|
|||||||
assert config.optimization.weight_decay == 0.0001
|
assert config.optimization.weight_decay == 0.0001
|
||||||
assert config.optimization.grad_clip == 1.0
|
assert config.optimization.grad_clip == 1.0
|
||||||
assert config.evaluation.on_max_steps == "score_diff"
|
assert config.evaluation.on_max_steps == "score_diff"
|
||||||
|
assert config.evaluation.resolved_batch_size() == 64
|
||||||
|
assert config.evaluation.device == "trainer"
|
||||||
|
assert config.evaluation.resolved_num_workers() == 4
|
||||||
assert config.checkpoint.save_iteration_interval == 10
|
assert config.checkpoint.save_iteration_interval == 10
|
||||||
assert (
|
assert (
|
||||||
config.checkpoint.directory == "runs/deep_cfr/deep_cfr_selfplay_full_depth_slot_playability"
|
config.checkpoint.directory == "runs/deep_cfr/deep_cfr_selfplay_full_depth_slot_playability"
|
||||||
@@ -128,6 +133,51 @@ def test_deep_cfr_train_cli_checkpoint_save_overrides() -> None:
|
|||||||
assert overridden.checkpoint.save_iteration_interval == 1
|
assert overridden.checkpoint.save_iteration_interval == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_deep_cfr_batched_evaluation_matches_batch_size_one() -> None:
|
||||||
|
config = _deep_cfr_config(
|
||||||
|
{
|
||||||
|
"network": {"hidden_size": 16},
|
||||||
|
"encoding": {"derived_playability": True, "slot_aware_playability": True},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
game_config = LostCitiesConfig(seed=123)
|
||||||
|
state = GameState.new_game(game_config, seed=123)
|
||||||
|
network = DeepCFRMLP.from_config(
|
||||||
|
input_dim(state, config.encoding),
|
||||||
|
game_config.action_size,
|
||||||
|
config.network,
|
||||||
|
)
|
||||||
|
network.eval()
|
||||||
|
kwargs = {
|
||||||
|
"strategy_network": network,
|
||||||
|
"config": game_config,
|
||||||
|
"games": 8,
|
||||||
|
"seed": 55,
|
||||||
|
"opponent": "random",
|
||||||
|
"device": "cpu",
|
||||||
|
"max_steps": 200,
|
||||||
|
"encoding": config.encoding,
|
||||||
|
}
|
||||||
|
|
||||||
|
batch_one = evaluate_strategy_network(**kwargs, batch_size=1)
|
||||||
|
batched = evaluate_strategy_network(**kwargs, batch_size=64)
|
||||||
|
|
||||||
|
for key in [
|
||||||
|
"games",
|
||||||
|
"wins0",
|
||||||
|
"wins1",
|
||||||
|
"draws",
|
||||||
|
"avg_score0",
|
||||||
|
"avg_score1",
|
||||||
|
"avg_score_diff0",
|
||||||
|
"avg_game_length",
|
||||||
|
"max_step_timeouts",
|
||||||
|
"play_action_rate",
|
||||||
|
]:
|
||||||
|
assert batched[key] == batch_one[key]
|
||||||
|
assert np.isclose(batched["policy_entropy"], batch_one["policy_entropy"])
|
||||||
|
|
||||||
|
|
||||||
def test_deep_cfr_resume_latest_resolution_uses_config_checkpoint_dir(tmp_path) -> None:
|
def test_deep_cfr_resume_latest_resolution_uses_config_checkpoint_dir(tmp_path) -> None:
|
||||||
config = _deep_cfr_config({"checkpoint": {"directory": str(tmp_path)}})
|
config = _deep_cfr_config({"checkpoint": {"directory": str(tmp_path)}})
|
||||||
latest = tmp_path / "latest.pt"
|
latest = tmp_path / "latest.pt"
|
||||||
|
|||||||
Reference in New Issue
Block a user