Files
coorl-lost-cities/docs/research/deep-cfr-architecture.md
coolguyandClaude Opus 4.7 004b913a7b Rename bot family, curate analyze plots, tier evaluation cadence
Three coordinated hygiene changes; none target the diagnosed
selection-bias bottleneck. They make the codebase honestly reflect the
pure-self-play stance and reduce dashboard noise.

Bot rename (drop the unhelpful safe_ prefix; suffixes describe behaviour):
- safe_heuristic_loose -> heuristic_aggressive
- safe_heuristic       -> heuristic_balanced
- safe_heuristic_strict -> heuristic_cautious
- noisy_safe           -> heuristic_noisy
- passive_discard      -> discard_only

Class renames in bots/: SafeHeuristicBot -> HeuristicBot,
SafeHeuristicParams -> HeuristicParams, PassiveDiscardBot -> DiscardOnlyBot,
plus loose/strict parameter constants. Backwards compatibility was dropped
intentionally per user instruction; no aliases. Active configs, docs,
scripts, tests updated. Archive directories (configs/archive,
docs/archive, runs/archive) left intact and may still reference old
names per their read-only policy. The src/.../bots/passive.py module was
renamed to discard_only.py via git mv.

Analyze plot curation (deep_cfr/analyze.py):
- Added analysis_00_core.png as the canonical daily dashboard with 10
  heuristic-free metrics (loss/{advantage,strategy}; vs heuristic_cautious:
  avg_score_diff0, win_rate0, avg_opened_colors, positive_expedition_rate,
  bonus_expedition_rate, score_per_opened_color, policy_entropy; vs random:
  win_rate0).
- Removed analysis_05_open_quality.png (bad/weak/good open rates,
  recoverable score) and analysis_07_calibration.png (calibration gap,
  recoverable mean) - both relied on the heuristic recoverable_score
  classifier already dropped from inputs.
- Removed SELECTIVITY_PLOTS and plot_selectivity (heuristic-laden).
- SUMMARY_EVAL_METRICS no longer includes bad_open_rate or
  calibration_gap.
- PlotSpec gained an opponents allowlist so the new core section can pin
  a specific opponent per panel without restructuring plot_section.

Tiered evaluation cadence (EvaluationConfig):
- Added extended_opponents and extended_eval_every (default 0 = disabled).
- opponents_for_iteration(iteration) returns the core list every
  eval_every and appends extended_opponents (de-duplicated) when
  iteration is also a multiple of extended_eval_every.
- default.yaml now uses 3 core opponents (random, discard_only,
  heuristic_cautious) every 5 iterations and 3 extended opponents
  (heuristic_balanced, heuristic_aggressive, heuristic_noisy) every 50
  iterations. random is the floor sanity. discard_only is the
  zero-pit detector / absolute-score reference (its score is always 0,
  so eval/discard_only/avg_score_diff0 directly equals the model's raw
  average score). heuristic_cautious is the ceiling and the
  archive-comparable benchmark used in the prior diagnostic sections.

Net eval cost reduction: roughly 50% (3 opponents x every 5 iter, plus
6 opponents x every 50 iter, vs the prior 6 x every 5).

Documented in docs/plans/deep-cfr-selectivity.md section 9.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 15:32:55 +09:00

150 lines
7.3 KiB
Markdown

# Deep CFR Package Architecture and Design Rationale
**Last verified:** 2026-05-07, commit `ad0be89`
Source: `docs/archive/deep-cfr-v0-plan.md`
## Question
What is the package layout of this Deep CFR implementation, why are the
subsystems split the way they are, and what are the design choices that drive
the architecture?
Short answer: **the Cython/Python split mirrors the compute profile.** The
traversal hot path (`traversal.pyx`, `cfr_math.pyx`, `encoding.pyx`) calls
`GameState` C APIs directly without Python round-trips; everything that runs
once per iteration or is GPU/I/O-dominated (training, checkpointing, eval, CLI)
stays in Python. A Pydantic config tree bridges the two by handing typed
scalars and arrays across the boundary.
## Package layout (current as of `ad0be89`)
```text
src/coolrl_lost_cities/games/classic/deep_cfr/
# Cython hot-path modules
cfr_math.pyx / cfr_math.pxd — regret matching arithmetic (all-negative fallback, etc.)
encoding.pyx / encoding.pxd — information-state feature extraction; Python + C-buffer API
traversal.pyx / traversal.pxd — full Deep CFR tree walking; calls GameState C APIs directly
# Python training layer
config.py — Pydantic config tree for all subsystems
memory.py — reservoir memory buffers (advantage and strategy)
networks.py — PyTorch advantage and strategy network definitions
trainer.py — orchestration: traverse → sample → train → eval → checkpoint loop
checkpoints.py — save/load for networks, optimizer state, training counters
workers.py — multiprocess traversal worker pool and result merge
# Inference experiments (opt-in)
inference_server.py — batched policy inference server (Option A batched traversal)
inference_client.py — client stub for the inference server
inference_buffers.py — shared-memory buffer management for batched inference
# Runtime tooling
cli.py — training and evaluation CLI entry points
benchmark.py — traversal throughput benchmark CLI
evaluate.py — evaluation loop against registered classic bots
analyze.py — metrics.jsonl analysis helpers
tracking.py — run artifact helpers (metrics, runtime_progress.json)
traversal_stats.py — structured traversal diagnostic metrics
# Auxiliary training modes
imitation.py — heuristic imitation pretraining
policy_gradient.py — policy-gradient fine-tuning
```
The compiled `.c` and `.so` artifacts (`cfr_math.c`, `encoding.c`, `traversal.c`
and their `.cpython-311-x86_64-linux-gnu.so` counterparts) are build outputs of
the Cython modules and live alongside the sources.
The original plan proposed 11 files; the current package has grown to 20+
Python/Cython source files as experiments and tooling have been added.
## Why Cython for traversal
The Deep CFR inner loop is an alternating-player tree walk that visits thousands
to millions of nodes per iteration. Each node needs: legal action enumeration,
regret matching, policy sampling, state push/pop for action application and
undo, and value backpropagation. In Python, each of these operations carries
dict lookups, reference counting, and interpreter overhead that compound across
the call tree.
`traversal.pyx` calls the `GameState` C APIs (`_legal_actions_c`,
`_unified_legal_actions_c`, `push_action`, `pop_action`, score caches, deck
sampling helpers) directly without Python object construction at each step. This
keeps the game-state hot path in C-land. `cfr_math.pyx` and `encoding.pyx`
extend the same principle to regret arithmetic and feature extraction
respectively, so a traversal node that produces a training sample can encode its
information state and compute regrets without leaving Cython.
The one remaining boundary cost is the PyTorch policy call: the advantage network
forward pass requires moving data to GPU and back, which necessarily crosses into
Python/PyTorch. This is the primary remaining performance target. The `inference_server`
/ `inference_client` / `inference_buffers` trio is an opt-in experiment that batches
multiple traversal-paused states into a single GPU forward pass, amortizing that
boundary cost.
## Why PyTorch for networks
PyTorch is the standard choice for the training layer. The advantage and strategy
networks are straightforward MLPs with a legal-mask head; no exotic architecture
is needed. PyTorch's autograd, optimizer API, and GPU memory management handle
the training loop cleanly. The design deliberately keeps the network definitions
(in `networks.py`) thin and the training orchestration (in `trainer.py`) separate,
so the network architecture can be swapped without touching traversal.
## Why this module split
The Cython/Python split mirrors the compute profile:
- **Cython**: everything that runs inside the traversal loop and must be fast.
`traversal.pyx` is the main entry point; `cfr_math.pyx` and `encoding.pyx`
are helpers it calls. These modules expose C-level APIs (`.pxd` headers) so
they can call each other without Python object round-trips.
- **Python**: everything that runs once per iteration or is dominated by I/O or
GPU compute. Training, checkpointing, evaluation, and CLI tooling have no
reason to be in Cython and benefit from Python's ergonomics for development
speed.
- **Config**: all hyperparameters live in `config.py` as a Pydantic model tree.
This gives type checking and YAML deserialization for free, and means the
Cython modules receive plain scalars and typed arrays at call boundaries rather
than dict lookups.
## Non-goals (held from the original plan)
The following were explicitly out of scope for v0 and remain so:
- Multi-machine distributed training.
- Exploitability calculation.
- Full ISMCTS integration (particle-belief opponent sampling).
- Large experiment orchestration or W&B artifact integration.
These non-goals have not changed. The performance roadmap (explicit iterative
Cython traversal scheduler, C-level memory writes, fully batched policy
inference) is the next meaningful investment, not deeper experiment
infrastructure.
## Practical implication
- New training-layer features (memory variants, eval hooks, tracking) belong in
Python; do not push them into the Cython modules unless they sit inside the
per-node traversal loop.
- Anything called per-node (legal actions, regret update, encoding) must stay
in Cython and use the existing `.pxd` C-level APIs — adding a Python helper
here regresses throughput across the whole tree walk.
- The remaining boundary cost is the PyTorch policy forward pass; further
performance work should target it (batched inference, iterative scheduler)
rather than re-Cythonizing already-Python pieces.
- Hyperparameter additions go in `config.py` as Pydantic fields, not as
positional kwargs threaded through Cython signatures.
## Discrepancy note
The original plan (`docs/archive/deep-cfr-v0-plan.md`) listed `traverser.py`
as the Python fallback traversal path. As of `ad0be89`, that file has been
removed; `traversal.pyx` is the sole traversal implementation. The plan also
did not anticipate `inference_server.py`, `inference_client.py`,
`inference_buffers.py`, `analyze.py`, `tracking.py`, `traversal_stats.py`,
`imitation.py`, `policy_gradient.py`, or `workers.py`, all of which exist in
the current package. These additions reflect experiments and tooling that
accumulated after the initial plan was written.