Skip to content

feat: Add parallel match execution to tournament graph algorithm#8

Open
bohan-contextual wants to merge 14 commits intomainfrom
parallel-matches-clean
Open

feat: Add parallel match execution to tournament graph algorithm#8
bohan-contextual wants to merge 14 commits intomainfrom
parallel-matches-clean

Conversation

@bohan-contextual
Copy link
Copy Markdown
Contributor

Summary

  • Implemented parallel execution of tournament matches to reduce total rounds
  • Added max_parallel_matches parameter to BlitzRank and TournamentGraphSortConfig (default: 5)
  • Fixed reachability calculation to exclude same-SCC members, preventing inflated counts for cycle members
  • Maintains oracle call parity with sequential mode while significantly reducing round count
  • Changes

Core Algorithm Changes:

  • Modified TournamentGraphSort to chunk nodes into multiple non-overlapping matches and execute them in parallel using asyncio.gather
  • Updated scheduling logic to collect up to k * max_parallel_matches representatives for parallel execution
  • Added adaptive parallelism: scales down to sequential mode once top item is being compared, preventing scattered comparisons across non-competitive items

Reachability Fix:

  • Fixed in_reach and out_reach calculation in TournamentGraph to exclude same-SCC members
  • This prevents inflated reachability counts for nodes in cycles (they were previously counting internal cycle edges)
  • Updated tests and comments to reflect the corrected external reachability semantics

API Changes:

  • Added max_parallel_matches parameter to BlitzRank constructor (default: 5)
  • Updated README example to show new parameter usage
  • TournamentGraphSortConfig now accepts max_parallel_matches parameter
  • Testing:

Added comprehensive test suite (test_parallel_tournament.py) with 540+ lines covering:

  • Correctness verification across various sizes (25 horses puzzle, 100 items, etc.)
  • Oracle call parity between sequential and parallel modes
  • Edge cases (n < k, top_m = 1, k = n, etc.)
  • Large SCC handling with cyclic comparisons
  • Stress tests with up to 3000 items and 100 parallel matches
  • Updated test_non_transitive.py to reflect corrected reachability semantics
  • Added test_max_parallel_matches.py for parameter validation

Add parallel execution of tournament matches using asyncio.gather to reduce
round count. Introduces max_parallel_matches parameter (default 5) to control
parallelism. Includes comprehensive test suite with 540+ lines covering
correctness, oracle call parity, and edge cases up to 3000 items.

Made-with: Cursor
Show max_parallel_matches=2 in BlitzRank usage example to demonstrate
the new parallel execution feature.

Made-with: Cursor
Change example to show default value of 5 parallel matches, matching
the implementation default.

Made-with: Cursor
Introduce _interleave_cycle_reps to distribute cycle representatives at
the front of each match. This ensures items in cycles (multi-node SCCs)
get compared against singletons, preventing cycles from being starved
due to their high in_reach values.

Made-with: Cursor
Replace per-node descendants/ancestors with SCC-level transitive closure
for O(S^2) complexity where S=num_sccs, typically << V. Precompute
per-SCC reach and map back to nodes for significant speedup on large graphs.

Made-with: Cursor
Revert performance optimization due to correctness issues. Return to
simpler per-node descendants/ancestors calculation.

Made-with: Cursor
Remove complex cycle prioritization in favor of simpler representative
collection. Prepares for alternative approach using reach calculation fix.

Made-with: Cursor
Correct in_reach and out_reach to exclude same-SCC members, preventing
inflated counts for nodes in cycles. They were incorrectly counting
internal cycle edges. Update tests and comments to reflect external
reachability semantics. Simplifies scheduling by removing need for
cycle-first ordering.

Made-with: Cursor
Increment version for parallel tournament execution release.

Made-with: Cursor
on_round_complete: Callable[["RoundLog"], Awaitable[None] | None] | None = None

max_num_rounds: int = DEFAULT_MAX_NUM_ROUNDS
max_parallel_matches: int = 5 # number of matches to run in parallel per round
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why should the default be 5? should we just set it to 1 for stability and backward compatibility and then let users decide to increase this when needed?

break
return TournamentProgress(representatives, [], sorted_node_infos)

# Determine how many parallel matches to run
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we refactor this into a function? also how was the parallel matches designed?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe we should discuss this a bit more?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, maybe we can jump on a huddle or board for this.

ancestors = nx.ancestors(self.G, node)
out_reach[node] = len(descendants)
in_reach[node] = len(ancestors)
# Exclude same-SCC members from in_reach/out_reach to avoid inflated
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for this fix! Can we decouple this fix + the test into its own separate PR?


class BlitzRank(Ranker):
def __init__(self, window_size: int = 20, top_m: int = 10):
def __init__(self, window_size: int = 20, top_m: int = 10, max_parallel_matches: int = 5):
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same with default max_parallel_matches here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's go with 1. i don't feel strongly, but i do think some parallelism really help to speed up the algo thus default

do you have a sense what average / median sample size we should aim for out of the box?

Resolve conflict in tournament_graph.py: keep main's formal notation
for reachability comments (R⁺_G(u), R⁻_G(u)).

Made-with: Cursor
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants