"""Profile GPU forward-pass throughput for the Deep CFR trainer network. Builds the same DeepCFRMLP that ``DeepCFRTrainer.__init__`` constructs from ``legacy/deep-cfr/configs/default.yaml``, then measures average forward-pass time on CUDA across a sweep of batch sizes. The goal is to decide whether batched traversal inference (Optimization Priorities #5) is worth implementing. """ from __future__ import annotations import time from pathlib import Path import torch from coolrl_lost_cities.games.classic.deep_cfr.encoding import input_dim from coolrl_lost_cities.games.classic.game import GameState from coolrl_lost_cities.games.classic.deep_cfr.config import load_config from coolrl_lost_cities.games.classic.deep_cfr.networks import DeepCFRMLP REPO_ROOT = Path(__file__).resolve().parent.parent CONFIG_PATH = REPO_ROOT / "legacy" / "deep-cfr" / "configs" / "default.yaml" BATCH_SIZES = [1, 4, 16, 64, 256, 1024] WARMUP_ITERS = 10 MEASURE_ITERS = 1000 def main() -> None: if not torch.cuda.is_available(): raise SystemExit("CUDA is not available; this script requires a CUDA-capable GPU.") cfg = load_config(CONFIG_PATH) game_config = cfg.rules.to_lost_cities_config(seed=cfg.run.seed) probe = GameState.new_game(game_config, seed=cfg.run.seed) in_dim = input_dim(probe, cfg.encoding) action_size = 2 * probe.config.hand_size + 1 + probe.config.n_colors device = torch.device("cuda") torch.manual_seed(cfg.run.seed) network = DeepCFRMLP.from_config(in_dim, action_size, cfg.network).to(device) network.eval() print( f"Network: DeepCFRMLP input_dim={in_dim} output_dim={action_size} " f"hidden_size={cfg.network.hidden_size} num_layers={cfg.network.num_layers} " f"activation={cfg.network.activation}" ) print(f"Device: {torch.cuda.get_device_name(0)}") print(f"Warmup iters: {WARMUP_ITERS} Measure iters: {MEASURE_ITERS}") print() results: list[tuple[int, float, float]] = [] with torch.inference_mode(): for bs in BATCH_SIZES: x = torch.randn(bs, in_dim, device=device) # Warm-up for _ in range(WARMUP_ITERS): network(x) torch.cuda.synchronize() start = time.perf_counter() for _ in range(MEASURE_ITERS): network(x) torch.cuda.synchronize() elapsed = time.perf_counter() - start us_per_call = (elapsed / MEASURE_ITERS) * 1e6 us_per_state = us_per_call / bs results.append((bs, us_per_call, us_per_state)) bs1_us_per_state = results[0][2] print(f"{'batch_size':>10} | {'μs/call':>10} | {'μs/state':>10} | {'speedup_vs_bs1':>14}") print("-" * 56) for bs, us_call, us_state in results: speedup = bs1_us_per_state / us_state print(f"{bs:>10} | {us_call:>10.2f} | {us_state:>10.3f} | {speedup:>13.2f}x") if __name__ == "__main__": main()