From 016ab6f52ad183094ccdf43dac371b68a2d67f2c Mon Sep 17 00:00:00 2001 From: Peter Lord Date: Thu, 9 Jul 2026 23:33:29 -0700 Subject: [PATCH] Skip the Elo pairwise build for composite brackets so the snapshot rebuilds MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The player x skill composite brackets (solo:wr50, ...) built the full reward pairwise + Codex Elo (a 200-iteration Bradley-Terry fit) for all 16 of them, on top of the 10 base brackets, over ~740k runs. That made the snapshot walk too heavy to finish, so prod was stuck serving snapshot version 10 (never advancing to 12) with every composite bracket empty — the tier-list "players + skill" combine and the metrics composite view returned nothing. Composites now skip the pairwise/Elo machinery (the heaviest part of the walk). They keep their picks/wins, so Score and Win% still work; they just carry no Codex Elo, which those views don't display anyway (reward-preference Elo sliced this finely is meaningless). Non-composite brackets keep full Elo. Bump the snapshot version to 13 to force the now-cheap rebuild. Verified locally: the build completes, composite brackets carry picks/wins with elo=None, and non-composite brackets still carry Elo. --- backend/app/services/run_entity_stats.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/backend/app/services/run_entity_stats.py b/backend/app/services/run_entity_stats.py index 6ad83320..e3b5986f 100644 --- a/backend/app/services/run_entity_stats.py +++ b/backend/app/services/run_entity_stats.py @@ -110,6 +110,12 @@ _PLAYER_BRACKETS = ("solo", "2p", "3p", "4p") _SKILL_BRACKETS = ("a10", "wr30", "wr50", "wr75") _COMPOSITE_BRACKETS = [f"{p}:{c}" for p in _PLAYER_BRACKETS for c in _SKILL_BRACKETS] +# Fast membership test in the hot per-run walk. Composite brackets skip the +# expensive reward-pairwise / Codex Elo machinery (see the card-reward loop): +# it's the heaviest part of the walk, and the composite tier-list / metrics +# views only surface Score + Win%, not the reward-preference Elo / Pick% (which +# are too thin sliced this finely to mean anything). +_COMPOSITE_BRACKETS_SET = frozenset(_COMPOSITE_BRACKETS) _BRACKET_KEYS = [ "solo", @@ -211,7 +217,11 @@ def _winrate_brackets(ascension: int, winrate: float | None) -> list[str]: # These landed in the same deploy as v11 but reused version 11, so the leader # considered its (composite-less) v11 snapshot current and never rebuilt them in; # the bump forces the rebuild. -SNAPSHOT_VERSION = 12 +# Version 13: composites skip the reward-pairwise / Codex Elo build (they keep +# Score + Win% only). The full-Elo v12 build over 26 brackets x ~740k runs was +# too heavy to finish, so the snapshot never advanced past v10; this cuts the +# walk back to a healthy duration. The bump forces the (now cheap) rebuild. +SNAPSHOT_VERSION = 13 # The oldest snapshot version readers can still serve. Bump SNAPSHOT_VERSION # on every shape change; bump this floor ONLY when a change actually breaks # readers (a removed/retyped field). Everything in between is additive, and @@ -1199,6 +1209,11 @@ def _build_cache_data() -> tuple[dict, dict, dict, dict]: pick_counts, pair_wins, act_index, picked_ids, skipped_ids ) for ck in extra_brackets: + # Composite (player x skill) brackets skip the pairwise/Elo work + # — the heaviest part of the walk. They keep Score + Win% from + # the picks/wins loop above; their views don't show reward Elo. + if ck in _COMPOSITE_BRACKETS_SET: + continue _accumulate_screen( bracket_accs[ck]["pick_counts"], bracket_accs[ck]["pair_wins"],