Engineering

The interesting part was not just the solver. It was keeping a C++ training backend provably identical to the Python reference while making it 74.2× faster, and learning which trust signals were real.

Problem → decision → result: the 74.2× backend

PROBLEM

Python trained at ~21.9 it/s, so one 2M-iteration run took more than a day. Profiling put ~60% of the time in hand evaluation and river percentile tables.

FAILED FIRST ATTEMPT

Moving only helper functions native was measured and found insufficient — crossing the language boundary per node ate the gains. A real lesson, kept.

DECISION

Move the ENTIRE hot path to C++17: rules, chance sampling, evaluator, feature tables, infoset encoding, regret matching, and the recursive traversal. Python crosses the pybind11 boundary once per training chunk, GIL released. Infosets are packed 64-bit keys over fixed-size native nodes.

PORT RESULT

~22 → 837 it/s warm (38.11×, median of 3 trials; historical benchmark). Multi-seed, 2M-iteration training became practical on a laptop, which is what made the uncertainty analysis affordable at all.

PROFILE AGAIN

A sampling profile of the native trainer found 69.5% of CPU rebuilding river percentile tables. Deduplicating that work, reusing evaluator board counts and a flat open-addressing infoset index gave a further 1.96× in an A/B test between native builds, with an identical strategy digest.

CURRENT

21.9 → 1,625 it/s warm (74.2×, fresh benchmark, median of 3 trials). This is MCCFR training throughput on the release configuration, not end-user analysis latency.

Measured throughput: Python reference vs native C++ MCCFR backend
Committed benchmark; regenerate with experiments/make_readme_figures.py.

Bit-exact across two languages

Faster isn’t useful if it silently becomes a different algorithm. Both backends consume a shared random tape; the parity suite requires identical trajectories, regret sums and average strategies, bitwise. CI runs 1,000 iterations; a stress run held for 10,000. The evaluator matches on every five-, six- and seven-card hand (133,784,560 seven-card hands), and checkpoint resume is bit-identical (100k continuous equals 10 × 10k).

It caught real bugs: clang’s FMA contraction fused a multiply-add in betting arithmetic and changed results in the last ulp — harmless-looking, but through MCCFR’s sampling it forked entire training trajectories. The build now requires -ffp-contract=off, and a platform-independent fsum dot product replaced BLAS dispatch for the same reason.

What breaks what: the test matrix

Poker rulesproperty/fuzz over engine-generated legal states + exhaustive reduced-variant enumeration
Hand evaluatorexhaustive 5-, 6- and 7-card parity (all 133,784,560 seven-card hands)
Native backendshared-random-tape bitwise parity, ASan/UBSan clean
Checkpointstrain(a)+save+load+train(b) ≡ train(a+b), bitwise
Confidence gatecalibrated + held-out against exact-game EV regret
Regret semanticspaired-SE and exact-size invariants on every review
APIcontract tests + fuzzed direct-vs-JSON parity
Webvitest incl. browser-CFR-vs-Python fixture; strict tsc; static build
Desktopsidecar smoke + packaged standalone smoke outside the repo
Observerexperimental: limited real PokerNow tuning, no independent held-out validation yet

1008 tests collected at this commit (counted by pytest when this page’s data was generated).

Code map

Hold'em enginepoker_alpha/holdem/
CFR / CFR+ / MCCFRpoker_alpha/solvers/
C++ training backendcpp/
Abstraction + encoderspoker_alpha/abstraction/
Confidence gatepoker_alpha/decision/solver_gate.py
Ranges & opponent modelspoker_alpha/opponent/
Analysis / study / red teampoker_alpha/analysis/
Agents & tournamentspoker_alpha/agents/
Local APIpoker_alpha/api/
This siteapps/web/ (data: scripts/build_web_research_data.py)
Desktop overlayapps/desktop/

The local API, in one request

POST http://127.0.0.1:8765/analyze
{"state": {…pokeralpha.observed/v1…}}
→ { "recommended_tag": "HIGHEST FREQUENCY",
    "solver_state": "ACCEPTED",
    "actions": [{"label": "check", "frequency": 0.83}, …],
    "receipt": {"receipt_id": "PA-…", "strategy_artifact_sha256": "…"} }

Binds 127.0.0.1 only; warm analysis ~0.1 s; every response can carry a reproducibility receipt.

Evidence & validation ladder Benchmarks Native solver deep dive