Skip to content

refactor: replace non-dominated sorter implementations with ndsort - #63

Merged
foolnotion merged 5 commits into
mainfrom
feat/rank-intersect-sorter-rewrite
May 26, 2026
Merged

refactor: replace non-dominated sorter implementations with ndsort#63
foolnotion merged 5 commits into
mainfrom
feat/rank-intersect-sorter-rewrite

Conversation

@foolnotion

@foolnotion foolnotion commented May 21, 2026

Copy link
Copy Markdown
Member

Summary

  • Removes 8 hand-rolled sorter implementations (1034 lines) and replaces them with a 29-line adapter over the ndsort library, which provides all nine algorithms through a uniform, well-tested interface
  • Uses sorted_unique tag to skip redundant preprocessing: NSGA2::Sort already lex-sorts and partitions out duplicates before calling the sorter, so ndsort's internal lex-sort and eps_dedup passes are bypassed entirely
  • Fixes float preservation in flatten(): std::common_type_t<float, double> was silently promoting single-precision fitness values to double, causing 6 radix passes instead of 4; fixed with std::conditional_t
  • Fixes sort_ms column missing from compare_operon.py _STAT_COLS, which was causing elapsed to be read as sort_ms

Validation

  • Determinism: 72/72 checks pass — bit-identical output vs pre-migration binaries for every seed, dataset, and symbol set
  • Statistical equivalence: 24/24 groups pass at α=0.01 (Mann-Whitney U, 20 seeds) across Poly-10, Pagie-1, Concrete, Vladislavleva-4 for both operon_gp and operon_nsgp
  • Sort overhead: ~3.5ms/generation, unchanged vs reference

Test plan

  • CI passes
  • Confirm operon_nsgp runs with each --sorter flag

@foolnotion foolnotion changed the title perf: rewrite RankIntersect sorter with radix sort, packed bitsets, and SIMD refactor: replace non-dominated sorter implementations with ndsort May 25, 2026
…nd SIMD

Replace the old cppsort-based RankIntersect implementation with a
ground-up rewrite optimised for NSGA-II workloads:

- LSB radix sort (11-bit passes) with IEEE 754 float→sortable-uint
  transformation, avoiding any cppsort dependency
- Structure-of-Arrays fitness layout (fvals[obj * n + i]) for
  sequential memory access during objective passes
- PackedPool: triangular bitset allocator giving individual j exactly
  (nb - j/DIGITS) words; total footprint ≈ n²/128 words (~16 MB at n=16k)
- SIMD bitset intersection via eve::algo::transform_to with std::bit_and
- Early-exit in ObjectiveLoop: tracks a `done` counter and breaks out
  of the objective loop once all individuals have empty domination sets

Also add infrastructure used during development/benchmarking:
- GeneticAlgorithmBase::SortTime() — cumulative non-dominated sort time
  instrumented in NSGA2::Sort()
- --sorter rs|ms CLI flag for operon_nsgp to select sorter at runtime
- sort_ms column in reporter output

Benchmarks on Poly-10 (n=500, 16 threads, 1M evaluations, seed=42):
pop=500:  old RS 271ms / new RS 186ms / MS 524ms  (1.5× vs old, 2.8× vs MS)
pop=1000: old RS 321ms / new RS 223ms / MS 776ms  (1.4× vs old, 3.5× vs MS)
pop=2000: old RS 481ms / new RS 277ms / MS 1270ms (1.7× vs old, 4.6× vs MS)
pop=5000: old RS 1080ms / new RS 502ms / MS 3400ms (2.2× vs old, 6.8× vs MS)

Correctness verified: all 14 [algorithms] tests pass; 200-seed sweep
confirms exact agreement with DeductiveSorter across n={100,1000,5000},
m={2,3,4,5,10}.
Adds heal-research/ndsort as a flake input (nixpkgs.follows wired),
injects it via its overlay, and lists it in operon.nix buildInputs.
Removes 8 hand-rolled sorter .cpp files (1034 lines) and replaces them
with a 29-line adapter over the ndsort library, which provides all
nine algorithms through a uniform, well-tested interface.

Key changes:
- Add ndsort as a flake input (git+ssh://github.com/foolnotion/ndsort)
  and wire it into CMakeLists.txt via find_package + target_link_libraries
- Delete: best_order_sort, deductive_sort, dominance_degree_sort,
  efficient_sort, hierarchical_sort, merge_sort, rank_intersect,
  rank_ordinal .cpp files
- Add ndsort.cpp: thin Wrap<S> template calling each ndsort sorter with
  the sorted_unique tag, skipping redundant lex-sort and eps-dedup since
  NSGA2::Sort already partitions out duplicates before calling the sorter
- Remove EfficientSortStrategy enum (was only used internally)
- Fix float preservation in flatten(): use conditional_t instead of
  common_type_t to avoid silently promoting float fitness to double
- Fix sort_ms column missing from compare_operon.py _STAT_COLS

Validated: 72/72 determinism checks pass (bit-identical output vs ref),
24/24 statistical equivalence groups pass at α=0.01 across four datasets
and both operon_gp and operon_nsgp.
@foolnotion
foolnotion force-pushed the feat/rank-intersect-sorter-rewrite branch from 0777a75 to 01ad6a9 Compare May 26, 2026 06:13
@foolnotion
foolnotion merged commit 2397e18 into main May 26, 2026
3 checks passed
@foolnotion
foolnotion deleted the feat/rank-intersect-sorter-rewrite branch May 31, 2026 11:33
foolnotion added a commit that referenced this pull request Jul 13, 2026
…rewrite

refactor: replace non-dominated sorter implementations with ndsort
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