From 8f5441583291c755595bac636821dab18a6b339f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=A0=95=EC=8B=9C=EC=9B=90?= Date: Thu, 7 May 2026 00:11:01 +0900 Subject: [PATCH] =?UTF-8?q?Deep=20CFR=20multiprocessing=20benchmark=20?= =?UTF-8?q?=EB=B3=B4=EA=B0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../games/classic/deep_cfr/benchmark.py | 13 +++++++++++ .../games/classic/deep_cfr/cli.py | 22 +++++++++++++------ .../games/classic/deep_cfr/config.py | 17 +++++++++++++- .../games/classic/deep_cfr/trainer.py | 4 ++-- tests/games/classic/test_deep_cfr_trainer.py | 15 ++++++++++++- 5 files changed, 60 insertions(+), 11 deletions(-) diff --git a/src/coolrl_lost_cities/games/classic/deep_cfr/benchmark.py b/src/coolrl_lost_cities/games/classic/deep_cfr/benchmark.py index 9fe1080..eafe3b1 100644 --- a/src/coolrl_lost_cities/games/classic/deep_cfr/benchmark.py +++ b/src/coolrl_lost_cities/games/classic/deep_cfr/benchmark.py @@ -32,3 +32,16 @@ def benchmark_traversal( "advantage_samples": metrics.advantage_samples, "strategy_samples": metrics.strategy_samples, } + + +def benchmark_traversal_modes( + config: DeepCFRConfig | None = None, +) -> dict[str, dict[str, float | int]]: + single = benchmark_traversal(config, num_workers=0) + multi = benchmark_traversal(config, num_workers=2) + speedup = float(multi["nodes_per_second"]) / max(float(single["nodes_per_second"]), 1.0e-12) + return { + "single": single, + "multi": multi, + "summary": {"speedup": speedup}, + } diff --git a/src/coolrl_lost_cities/games/classic/deep_cfr/cli.py b/src/coolrl_lost_cities/games/classic/deep_cfr/cli.py index 5328504..6597000 100644 --- a/src/coolrl_lost_cities/games/classic/deep_cfr/cli.py +++ b/src/coolrl_lost_cities/games/classic/deep_cfr/cli.py @@ -5,7 +5,10 @@ import json from dataclasses import replace from pathlib import Path -from coolrl_lost_cities.games.classic.deep_cfr.benchmark import benchmark_traversal +from coolrl_lost_cities.games.classic.deep_cfr.benchmark import ( + benchmark_traversal, + benchmark_traversal_modes, +) from coolrl_lost_cities.games.classic.deep_cfr.config import DeepCFRConfig, config_from_dict from coolrl_lost_cities.games.classic.deep_cfr.evaluate import ( evaluate_strategy_network, @@ -61,13 +64,17 @@ def eval_command(args: argparse.Namespace) -> None: def benchmark_command(args: argparse.Namespace) -> None: + config = DeepCFRConfig( + traversals_per_iteration=args.traversals, + max_traversal_depth=args.depth, + seed=args.seed, + save_every_iteration=False, + ) + if args.compare: + print(json.dumps(benchmark_traversal_modes(config), indent=2, sort_keys=True)) + return result = benchmark_traversal( - DeepCFRConfig( - traversals_per_iteration=args.traversals, - max_traversal_depth=args.depth, - seed=args.seed, - save_every_iteration=False, - ), + config, num_workers=args.workers, ) print(json.dumps(result, indent=2, sort_keys=True)) @@ -104,6 +111,7 @@ def main(argv: list[str] | None = None) -> None: benchmark.add_argument("--traversals", type=int, default=8) benchmark.add_argument("--depth", type=int, default=4) benchmark.add_argument("--seed", type=int, default=1) + benchmark.add_argument("--compare", action="store_true") benchmark.set_defaults(func=benchmark_command) args = parser.parse_args(argv) diff --git a/src/coolrl_lost_cities/games/classic/deep_cfr/config.py b/src/coolrl_lost_cities/games/classic/deep_cfr/config.py index f419a4c..4344058 100644 --- a/src/coolrl_lost_cities/games/classic/deep_cfr/config.py +++ b/src/coolrl_lost_cities/games/classic/deep_cfr/config.py @@ -45,7 +45,7 @@ class DeepCFRConfig: eval_games: int = 10 eval_opponents: tuple[str, ...] = ("random",) eval_max_steps: int = 10_000 - num_workers: int = 0 + num_workers: int | str = 0 traversal_worker_chunk_size: int = 4 def to_dict(self) -> dict[str, Any]: @@ -55,9 +55,24 @@ class DeepCFRConfig: def checkpoint_path(self) -> Path: return Path(self.checkpoint_dir) + def resolved_num_workers(self, batches: int | None = None) -> int: + if isinstance(self.num_workers, str): + token = self.num_workers.strip().lower() + if token == "auto": + guess = max(1, (os_cpu_count() or 2) // 2) + return min(guess, batches) if batches is not None and batches > 0 else guess + return max(0, int(token)) + return max(0, int(self.num_workers)) + def config_from_dict(data: dict[str, Any]) -> DeepCFRConfig: values = dict(data) if "eval_opponents" in values: values["eval_opponents"] = tuple(values["eval_opponents"]) return DeepCFRConfig(**values) + + +def os_cpu_count() -> int | None: + import os + + return os.cpu_count() diff --git a/src/coolrl_lost_cities/games/classic/deep_cfr/trainer.py b/src/coolrl_lost_cities/games/classic/deep_cfr/trainer.py index 75bafc0..aa676c5 100644 --- a/src/coolrl_lost_cities/games/classic/deep_cfr/trainer.py +++ b/src/coolrl_lost_cities/games/classic/deep_cfr/trainer.py @@ -138,7 +138,7 @@ class DeepCFRTrainer: def run_iteration(self, iteration: int) -> IterationMetrics: self.iteration = iteration - if self.config.num_workers > 1: + if self.config.resolved_num_workers() > 1: total_stats = self._run_traversals_parallel(iteration) else: total_stats = self._run_traversals_single_process(iteration) @@ -206,7 +206,7 @@ class DeepCFRTrainer: total_stats = TraversalStats() if not batches: return total_stats - max_workers = min(self.config.num_workers, len(batches)) + max_workers = self.config.resolved_num_workers(len(batches)) with ProcessPoolExecutor( max_workers=max_workers, mp_context=mp.get_context("spawn"), diff --git a/tests/games/classic/test_deep_cfr_trainer.py b/tests/games/classic/test_deep_cfr_trainer.py index 5e52a46..eb3473a 100644 --- a/tests/games/classic/test_deep_cfr_trainer.py +++ b/tests/games/classic/test_deep_cfr_trainer.py @@ -3,7 +3,10 @@ from __future__ import annotations import numpy as np from coolrl_lost_cities.games.classic.game import GameState, LostCitiesConfig -from coolrl_lost_cities.games.classic.deep_cfr.benchmark import benchmark_traversal +from coolrl_lost_cities.games.classic.deep_cfr.benchmark import ( + benchmark_traversal, + benchmark_traversal_modes, +) from coolrl_lost_cities.games.classic.deep_cfr.config import DeepCFRConfig from coolrl_lost_cities.games.classic.deep_cfr.memory import ReservoirMemory, TrainingSample from coolrl_lost_cities.games.classic.deep_cfr.trainer import DeepCFRTrainer @@ -234,6 +237,16 @@ def test_deep_cfr_traversal_benchmark_smoke() -> None: assert result["traversal_nodes"] > 0 assert result["nodes_per_second"] > 0.0 + comparison = benchmark_traversal_modes( + DeepCFRConfig( + traversals_per_iteration=1, + max_traversal_depth=2, + hidden_size=16, + save_every_iteration=False, + seed=48, + ) + ) + assert comparison["summary"]["speedup"] > 0.0 def test_deep_cfr_self_play_league_records_snapshots(tmp_path) -> None: