perf(engine): vector-form flop forward walk with double chance compaction (v1.10 PR-3, #70)#198
Open
amaster97 wants to merge 2 commits into
Open
perf(engine): vector-form flop forward walk with double chance compaction (v1.10 PR-3, #70)#198amaster97 wants to merge 2 commits into
amaster97 wants to merge 2 commits into
Conversation
…0 PR-2, #70) Lands the FRAMEWORK for vector-form turn forward walk per docs/v1_10_postflop_optimization_plan.md §2B (Candidate B): adds chance-template extraction at tree-build time and a specialized chance-node dispatch hook on VectorDCFR. Changes ------- - exploit.rs: add BettingTreeMode enum (Standard / TemplateExtract) and BettingTree::build_with_mode. TemplateExtract additionally populates BettingTree::chance_templates with one entry per FlatNode::Chance whose children share structural identity (verified via a per-subtree structure_hash). The FlatNode list itself is unchanged — chance_templates is purely out-of-band metadata. - dcfr_vector.rs: add has_chance_template[] lookup on VectorDCFR (one bool per FlatNode index), populated from tree.chance_templates in with_init_noise. The chance arm of traverse() now dispatches to the new traverse_turn_chance specialized walker when the template flag is set, else falls through to the legacy per-branch recursion. - solve_range_vs_range_postflop_with_hands: choose TemplateExtract mode for Turn/Flop starting streets, Standard for River. Bit-identical gate (load-bearing) --------------------------------- The traverse_turn_chance walker performs the EXACT same arithmetic as the legacy chance arm: same DFS order over children, same `values[i] += prob * v` accumulator update. The IEEE 754 summation order is preserved. New unit tests verify this: - template_extract_finds_turn_chance_node — TemplateExtract mode populates chance_templates; Standard does not; FlatNode list is identical between modes. - template_extract_bit_identical_to_standard — solve outputs at 1e-12 strategy tolerance match byte-for-byte between modes. - river_solve_does_not_template_extract — negative control: river solve falls through to legacy path bit-identically. Existing diff tests (39+ across exploit / range_vs_range / w2_2 / aa_vs_aa / dcfr / leduc / true_path_b / asymmetric) all pass. All 70 lib::tests pass (1 ignored, 24 filtered). Perf framing ------------ PR-2 ships the framework only. Because traverse_turn_chance does the same arithmetic as the legacy chance arm (the only thing changed is the dispatch route), measured wall-clock is identical to main within noise (verified by 3×-replicated quick_bench runs on tiny/med fixtures). The actual perf win for turn comes when PR-3 (vector flop) extends template extraction to double-broadcast scratch buffers across the 45 river-card branches. No reordering of operations was performed in PR-2 because IEEE 754 summation order is not associative — hoisting the `prob` multiply out of the inner accumulator loop would break bit-identity for non-uniform chance probabilities (1/45 is not exactly representable). PR-3 will introduce template-broadcasting scratch buffers without changing arithmetic. Touched files ------------- - crates/cfr_core/src/exploit.rs (+166 LOC; BettingTreeMode, ChanceTemplate, build_with_mode, extract_chance_templates, structure_hash, hash_subtree) - crates/cfr_core/src/dcfr_vector.rs (+345 LOC; has_chance_template field, dispatch logic in chance arm, traverse_turn_chance, 3 unit tests) - scripts/v1_10_2_turn_bench.py (new; perf measurement driver) - scripts/v1_10_2_quick_bench.py (new; small-fixture timing comparison) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…tion (v1.10 PR-3, #70) Implement PR-3 of the v1.10 postflop optimization plan: extend PR-2's ChanceTemplate extraction to two-level chance compaction. The flop-rooted RvR's outer chance node (turn deal) now dispatches to a specialized traverse_flop_chance_recursive walker that pre-allocates scratch buffers in RunoutCache instead of allocating fresh vec![0.0; update_hands] per call. Key changes: - ChanceTemplate gains chance_depth: u8 recording max nested-chance depth below each template. Computed at extraction time via DFS. - RunoutCache struct with build/empty/is_active. Pre-allocates one outer scratch buffer per VectorDCFR::solve call, reused across all iterations. Inactive (no allocation) for turn/river-rooted solves or Standard build mode. - traverse_flop_chance_recursive walks turn-card children in DFS order matching the legacy chance arm exactly, but uses runout_cache.flop_values_scratch for the outer accumulator. - Dispatch in traverse_recursive_with_parallel: rayon (if enabled + multi-child) -> PR-3 flop walker (depth>=1 + active cache) -> PR-2 turn walker (any template) -> legacy chance loop. - traverse_with_infosets (rayon worker entry) constructs its own empty RunoutCache locally since workers run below the flop chance. - CFR_VECTOR_FLOP_TEMPLATE=0 env var forces Standard build mode for diff-test baselines (canonical legacy path). Bit-identity contract preserved: arithmetic + DFS order match the legacy chance arm exactly; the only behavioral change is scratch-buffer provenance (pre-allocated pool vs fresh vec!). F4_synth diff test shows byte-identical strategy output (max_diff=0.000e+00 vs canonical). Tests: - 3 new Rust unit tests (template_extract_assigns_correct_chance_depths, runout_cache_active_only_on_flop_root, flop_template_extract_bit_identical_to_standard). - 6 new Python diff tests in tests/test_v1_10_3_flop_diff.py (skip markers removed, real implementations added). 3 fast tests pass at 1e-12 strict bit-identity (F4_synth + turn/river non-regression); 3 slow tests (F4.1/F4.2/F4.3 8-class flop) marked with @pytest.mark.slow. - Rayon compose test passes at 1e-12 byte-identity across all three modes (canonical / PR-3 / PR-3+rayon). - All 94 existing cfr_core lib tests still pass. Perf (J7o flop, top_k=4 iter=5): - PR-3: 151.9s wall (was OOM>5min on main) - Canonical: 265.5s wall - Speedup: 1.75x The headline perf gates (top_k=15 iter=100 <60s, top_k=169 iter=100 <120s, RSS <1GB) are NOT met by PR-3 alone -- they require PR-1 (arena, separate PR) to refactor the Decision-arm allocations that dominate per-iteration memory churn. PR-3 addresses the outer chance-arm allocation pattern; PR-1 will compose with it. Per task brief: HOLD for user review since perf gate misses; all correctness gates pass strict bit-identity. Refs: docs/v1_10_pr3_flop_vector_design.md, docs/v1_10_postflop_optimization_plan.md sec 2 Candidate C. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements v1.10 PR-3 of the postflop optimization plan: extend PR-2's
ChanceTemplateextraction to two-level chance compaction. The flop-rooted RvR's outer chance node (turn deal) now dispatches to a specializedtraverse_flop_chance_recursivewalker that pre-allocates scratch buffers inRunoutCacheinstead of allocating freshvec![0.0; update_hands]per call.Status: HOLD for user review per task brief (perf gate misses; correctness gates pass strict bit-identity).
Base branch: This PR targets
feat-v1-10-2-vector-turn(PR #190) since PR-2'straverse_turn_chance_recursiveis the prerequisite. PR-2 needs to merge first.Key changes
ChanceTemplate.chance_depth: u8— records max nested-chance depth below each template. Computed at extraction time via DFS inBettingTree::max_nested_chance_depth.RunoutCachestruct withbuild/empty/is_active. Pre-allocates outer scratch buffer perVectorDCFR::solve, reused across all iterations. Inactive (no allocation) for turn/river-rooted solves orStandardbuild mode.traverse_flop_chance_recursivewalks turn-card children in DFS order matching the legacy chance arm exactly, but usesrunout_cache.flop_values_scratchfor the outer accumulator.traverse_recursive_with_parallel: rayon (if enabled + multi-child) → PR-3 flop walker (depth>=1 + active cache) → PR-2 turn walker (any template) → legacy chance loop.traverse_with_infosets(rayon worker entry) constructs its own emptyRunoutCachelocally since workers run below the flop chance.CFR_VECTOR_FLOP_TEMPLATE=0env var forcesStandardbuild mode for diff-test baselines (canonical legacy path).Bit-identity contract
Preserved: arithmetic + DFS order match the legacy chance arm exactly; the only behavioral change is scratch-buffer provenance (pre-allocated pool vs fresh
vec!). F4_synth diff test shows byte-identical strategy output (max_diff=0.000e+00) vs canonical at 1e-12 tolerance.Test plan
template_extract_assigns_correct_chance_depths,runout_cache_active_only_on_flop_root,flop_template_extract_bit_identical_to_standard)cfr_corelib tests still passtests/test_range_vs_range_rust_diff.pypasstests/test_vector_rayon_diff.pypass (1 timeout in unrelated exploit_walk; pre-existing)cargo clippy --all-targets -- -D warningsclean@pytest.mark.slow— too slow for tight loop, deferred to full PR gate.Perf measurement (J7o A♦8♥9♦ 40-BB flop, top_k=4 iter=5)
Speedup: 1.75x. The fixture was OOM-killed at 5+ min on main per
docs/flop_subgame_perf_measurement_2026-05-28.md; PR-3 unblocks completion.Perf gates NOT met (the HOLD reason)
The headline perf gates from the task brief require PR-1 (arena, separate PR) to land first:
Per the v1.10 plan flop subgame profile (
docs/flop_subgame_profile_baseline_2026-05-28.mdsection 2), the dominant per-iteration allocation traffic is in the Decision arm (dcfr_vector.rs:661 strategy+dcfr_vector.rs:741 action_values, ~99 MB/iter combined). PR-3 only targets the chance-arm outer accumulator (~2 MB/iter). The Decision-arm refactor is PR-1's scope.DFS-order preservation verified
The bit-identity contract is the load-bearing correctness gate. PR-3 preserves DFS order by:
flop_chance_childrenin source index order (matches legacy).traverse_recursive_with_parallel, preserving the entire turn-subtree DFS shape.values[h] += prob * v[h]arithmetic in the same loop nest order as legacy.The F4_synth fixture (
board=2c3d4h, 4 hand classes, 1 bet size, raise_cap=1) is small enough to enumerate analytically — any DFS-order drift produces visible (>1e-6) strategy differences. Observedmax_diff=0.000e+00confirms byte-identical IEEE-754 summation across all 10 iterations.Files touched
crates/cfr_core/src/dcfr_vector.rs(+750/-90)crates/cfr_core/src/exploit.rs(+115/-2)tests/test_v1_10_3_flop_diff.py(rewrite: skip markers removed, real implementations added)References
docs/v1_10_pr3_flop_vector_design.md— design + scaffold (commit6f5b52b)docs/v1_10_postflop_optimization_plan.md§2 Candidate Cdocs/flop_subgame_profile_baseline_2026-05-28.md— allocation hot-path measurementdocs/w2_3_vector_br_walk_test_fixtures.md— F4_synth fixture spec🤖 Generated with Claude Code