Skip to content

Parallelize and optimize dwarf body-generation setup (bit-identical results)#238

Open
IanSteveC wants to merge 1 commit into
Milkyway-at-home:devfrom
IanSteveC:perf/parallel-phase1
Open

Parallelize and optimize dwarf body-generation setup (bit-identical results)#238
IanSteveC wants to merge 1 commit into
Milkyway-at-home:devfrom
IanSteveC:perf/parallel-phase1

Conversation

@IanSteveC

Copy link
Copy Markdown

Parallelize and optimize dwarf body-generation setup (bit-identical results)

Motivation

The v1.96 cored/NFW-with-rcut dwarf has no analytic distribution function, so body generation rejection-samples against numerical gauss_quad integrals. Profiling shows dist_fun (one gauss_quad per call) is essentially 100% of the setup phase, which runs single-threaded for ~10-16 minutes on a 40k-body workunit before the simulation starts.

Changes (all bit-neutral: identical operations in identical order, only scheduling/reuse changes)

  1. gauss_quad: enumerate the integration nodes first (verbatim original arithmetic), evaluate the pure integrand at all ~330 nodes in parallel with OpenMP (honors -n/--nthreads), then accumulate the terms in the original sequential order. Results are identical for any thread count.
  2. Constant hoisting: the prefix 4π·pcut·rcut^-δ·e^(rcut/rdecay)·rdecay^(δ+3) is computed once per component in set_model_params (same left-associated product) instead of re-evaluating three crlibm transcendentals per integrand call in the r > rcut branches.
  3. gammln memo: thread-local single-entry cache; it is called millions of times with only ~2 distinct arguments (delta+2, delta+3).

Verification

Full 40k-body v1.96 workunit: all 8 <search_likelihood*> metrics bit-identical to the unmodified build at --nthreads 1, 4, and 16.

Measured (40k v1.96 workunit)

threads setup time
unmodified 985s
4 301s → 244s with (2)+(3)
16 108s

Diff is 4 files, +68/−13.

…ults)

The v1.96 cored/NFW-with-rcut dwarf has no analytic distribution
function, so body generation rejection-samples against numerical
gauss_quad integrals. Profiling shows dist_fun (one gauss_quad per
call) is essentially 100% of the single-threaded setup phase: ~10-16
minutes per 40k-body workunit before any simulation starts.

Three changes, all provably bit-neutral (identical operations in the
identical order; only scheduling/reuse changes):

1. gauss_quad: enumerate the integration nodes first (verbatim
   original arithmetic), evaluate the pure integrand at all ~330 nodes
   in parallel with OpenMP (honors --nthreads / -n), then accumulate
   the terms in the original sequential order. Results are identical
   for any thread count.

2. Hoist the constant prefix 4*pi*pcut*rcut^-delta*e^(rcut/rdecay)*
   rdecay^(delta+3) into the Dwarf struct (computed once in
   set_model_params as the same left-associated product) instead of
   re-evaluating three transcendentals per integrand call.

3. Memoize gammln(z) with a thread-local single-entry cache: it is
   called millions of times with only ~2 distinct arguments
   (delta+2, delta+3).

Measured on a 40k-body v1.96 workunit (Ryzen, --nthreads 4): setup
985s -> 244s (4.0x); 16 threads: 108s (9.1x). Full-workunit results
bit-identical at 1, 4, and 16 threads (all 8 likelihood metrics).
@ahorek

ahorek commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

The ST phase currently (1.96) accounts for a significant portion of the total task runtime, making the use of MT impractical. Parallelizing this phase helps a lot.

@kgimmy thanks for considering it!

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