Skip to content

perf(nsga): 2.6x faster Pareto front sorting (multi-objective GA run) - #387

Open
dexhunter wants to merge 1 commit into
ahmedfgad:masterfrom
dexhunter:perf/nsga-pareto-sorting
Open

dexhunter wants to merge 1 commit into
ahmedfgad:masterfrom
dexhunter:perf/nsga-pareto-sorting

Conversation

@dexhunter

Copy link
Copy Markdown

TL;DR

  • This PR makes NSGA.get_non_dominated_set in pygad/utils/nsga.py faster. non_dominated_sorting calls it for every front, so it gets faster too.
  • The code now compares small fitness vectors as plain Python lists, not with a new numpy array per pair.
  • Benchmark wall time for a multi-objective GA workload, median of 9 timed pairs: 58.90 ms -> 21.99 ms, 62.7% faster.
  • All 596 tests in the NSGA, adaptive mutation, save solutions and gene space test files pass.

What & Why

Before, get_non_dominated_set checked every pair of solutions. For each pair it built a new numpy array from zip(). Then it called .all() and .any() on two more arrays. The fitness vectors are short, so this numpy work cost much more than the comparison itself. non_dominated_sorting also called this loop once for each Pareto front.

Now, when all fitness vectors are 1D numeric numpy arrays of the same dtype and shape, the code converts each vector to a list once. Then it compares the lists with plain Python. Solution j dominates solution i when all values of j are >= and the two rows are not equal. This is the same rule as before: "all >= and any >". non_dominated_sorting calls this function once per front, so it gets faster too. The same pair objects go into the same lists in the same order. Other inputs still use the old loop.

Results

Times are per call, best of 7 runs, one CPU core.

Situation Before After Change
GA run like test_adaptive_mutation_mutation_probability_multi_objective (10 solutions, 2 objectives) 4.90 ms 2.16 ms 2.3x faster
non_dominated_sorting, 50 solutions x 2 objectives 7.42 ms 0.43 ms 17x faster
non_dominated_sorting, 200 solutions x 3 objectives 118.8 ms 5.11 ms 23x faster
get_non_dominated_set, fitness vectors are Python lists (old loop is used) 0.089 ms 0.089 ms no change
get_non_dominated_set, 1 solution (old loop is used) 0.3 us 0.4 us +0.1 us (one type check)

Why this is safe and easy to maintain

The change is small and stays inside get_non_dominated_set. The new path is a short double loop with one clear domination test. It returns the same pair objects in the same order. The old loop stays for all other inputs. The NSGA-III tests, the adaptive mutation tests and the save solutions tests cover this code.

Validation

  • Workload: 10 multi-objective pygad.GA runs that copy test_adaptive_mutation_mutation_probability_multi_objective, plus 13 direct non_dominated_sorting calls with ties, duplicates, int fitness and one objective. Before 58.90 ms, after 21.99 ms (3 repeat runs: 0.378, 0.353, 0.388 of the old time). The output was checked to be identical to the unmodified code.
  • Tests: python -m pytest -q tests/test_adaptive_mutation.py tests/test_save_solutions.py tests/test_gene_space.py tests/test_gene_space_allow_duplicate_genes.py tests/test_nsga3.py tests/test_nsga3_dtlz2.py tests/test_nsga3_pipeline.py tests/test_nsga3_population_growth.py -> 596 passed (same as on master).
  • Quick check:
import timeit, numpy, pygad
from pygad.utils.nsga import NSGA
s = NSGA(); s.supported_int_float_types = pygad.GA.supported_int_float_types
f = numpy.random.RandomState(0).rand(50, 2)
print(min(timeit.repeat(lambda: s.non_dominated_sorting(f), number=1, repeat=7)))

AI assistance

The code change was generated by Codex (gpt-6.1-sol) candidates and tracked with Weco Observe. Each candidate was timed and its output was compared with the original code by an automated benchmark harness. The tests were run on the final patch.

Weco share link: https://dashboard.weco.ai/share/PYAiMz5MRfbwe0rWTcdOuZYmr0ssct2v

get_non_dominated_set built a new numpy array from zip() for every pair
of solutions and called .all()/.any() on it. The fitness vectors are
tiny, so this cost far more than the comparison itself.

When all fitness vectors are 1D numeric numpy arrays of the same dtype
and shape, convert them to lists once and compare them with plain
Python. non_dominated_sorting calls this function once per front,
so it gets faster too. Output (pair objects and order) is unchanged;
other inputs keep the old loop.

Multi-objective GA workload: 58.90 ms -> 21.99 ms (62.7% faster).

Co-Authored-By: Aiden <aiden@weco.ai>
@github-actions

github-actions Bot commented Nov 1, 2026

Copy link
Copy Markdown

📊 Change summary

1 files changed · +40 / −0 lines

pie showData
    title Lines changed per file (40 total)
    "pygad/utils/nsga.py" : 40
Loading
Per-file breakdown
File + − total
pygad/utils/nsga.py 40 0 40
Total (1 files) 40 0 40

This branch has not been deployed

No deployments
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.

1 participant