Skip to content

Homogenize nthreads handling across the framework - #550

Open
paganol wants to merge 6 commits into
masterfrom
homogenize_nthreads
Open

Homogenize nthreads handling across the framework#550
paganol wants to merge 6 commits into
masterfrom
homogenize_nthreads

Conversation

@paganol

@paganol paganol commented Aug 25, 2026

Copy link
Copy Markdown
Member

Fixes #544.

Standardizes every nthreads parameter on int | None = None + resolve_nthreads(), removing the previous mix of 0/1 literal defaults and duplicated OMP_NUM_THREADS-reading code that motivated the issue.

Summary

  • utilities.py: NUM_THREADS/NUMBA_NUM_THREADS are now resolved once at import time (mirrors lbs.MPI_COMM_WORLD) instead of re-reading the environment on every call. resolve_nthreads()/resolve_numba_nthreads() become cheap lookups against the cached value, and both globals are exposed at the top-level lbs namespace.
  • constants.py: NUMBA_NUM_THREADS_ENVVAR was accidentally aliased to "OMP_NUM_THREADS" (identical to NUM_THREADS_ENVVAR) — fixed to point at its own NUMBA_NUM_THREADS variable, so Numba and ducc0 thread counts can be tuned independently, with OMP_NUM_THREADS as the shared fallback when NUMBA_NUM_THREADS isn't set.
  • simulations.py: Simulation.numba_threads now resolves via resolve_numba_nthreads(), called after _init_missing_params() so a numba_threads value from a parameter file still takes precedence over the environment (precedence: explicit constructor arg > parameter file > NUMBA_NUM_THREADS > OMP_NUM_THREADS > hardware default).
  • hwp_harmonics.py: replaced an inline reimplementation of resolve_nthreads()'s logic with the shared helper.
  • beam_convolution.py, maps_and_harmonics.py (interpolate_alm, pixelize_alm, estimate_alm, rotate_alm), scan_map.py, pointings_in_obs.py, input_sky.py: replaced literal nthreads=0 defaults with None + resolve_nthreads().
  • Also threaded already-resolved nthreads values into internal _get_pointings_array() calls in beam_convolution.py, scan_map.py, hwp_harmonics.py, and mapmaking/common.py that had a resolved value in scope but weren't passing it through, silently falling back to ducc0's own default instead.
  • grasp2alm.py: PseudoAnalysisSHT.to_alm() gained an nthreads parameter (was hardcoded to 0 in two direct ducc0 calls, with no way to override).
  • mueller_convolver.py intentionally left untouched: it's vendored from upstream ("maintained externally... to make updating from outside sources easier") and its only call site already passes an explicit resolved value.

Testing

  • Full test suite: 277 passed, 2 skipped (non-MPI) + test_mpi_n4.py passed.
  • Manually verified env-var precedence (OMP_NUM_THREADS only, OMP_NUM_THREADS + NUMBA_NUM_THREADS together) and Simulation precedence (explicit arg > parameter-file value > env > hardware default).
  • ruff check clean.

Test plan

  • pytest test/ (non-MPI)
  • pytest test/test_mpi_n4.py
  • Manual precedence checks for resolve_nthreads/resolve_numba_nthreads
  • Manual precedence check for Simulation(numba_threads=...) vs parameter-file numba_threads vs env vars

Standardize every nthreads parameter on `int | None = None` +
resolve_nthreads(), removing the previous mix of 0/1 literal defaults
and duplicated OMP_NUM_THREADS-reading code:

- utilities.py: resolve NUM_THREADS/NUMBA_NUM_THREADS once at import
  time (mirrors lbs.MPI_COMM_WORLD) instead of re-reading the
  environment on every call. resolve_nthreads()/resolve_numba_nthreads()
  become cheap lookups against the cached value.
- constants.py: NUMBA_NUM_THREADS_ENVVAR now points at its own
  NUMBA_NUM_THREADS variable (was accidentally aliased to
  OMP_NUM_THREADS), so Numba and ducc0 can be sized independently;
  OMP_NUM_THREADS remains the shared fallback.
- simulations.py: Simulation.numba_threads now resolves via
  resolve_numba_nthreads(), after _init_missing_params() so a
  parameter-file value still takes precedence over the environment.
- hwp_harmonics.py: replaced an inline reimplementation of
  resolve_nthreads() with the shared helper.
- beam_convolution.py, maps_and_harmonics.py, scan_map.py,
  pointings_in_obs.py, input_sky.py: replaced literal nthreads=0
  defaults with None + resolve_nthreads(), and threaded already-resolved
  nthreads values into internal _get_pointings_array() calls that were
  silently falling back to ducc0's own default instead of the caller's
  resolved value.
- grasp2alm.py: PseudoAnalysisSHT.to_alm() gained an nthreads parameter
  (was hardcoded to 0 in two direct ducc0 calls).
- mueller_convolver.py intentionally left untouched: it's vendored
  from upstream ("maintained externally... to make updating from
  outside sources easier") and its only call site already passes an
  explicit resolved value.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@paganol paganol linked an issue Aug 25, 2026 that may be closed by this pull request
@paganol paganol added bug Something isn't working enhancement New feature or request labels Aug 25, 2026
paganol and others added 2 commits August 25, 2026 15:52
- mpi.rst: fix constructor parameter name (numba_num_of_threads ->
  numba_threads, matching the actual Simulation.__init__ signature,
  including in the TOML example), document the
  NUMBA_NUM_THREADS/OMP_NUM_THREADS precedence, and add a new
  "Threads and ducc0" section explaining resolve_nthreads(),
  lbs.NUM_THREADS/lbs.NUMBA_NUM_THREADS, and the MPI thread-contention
  risk of leaving OMP_NUM_THREADS unset with multiple ranks per node.
- maps_and_harmonics.rst: replace the stale "nthreads=0 (default)"
  tip with a description of the new None + resolve_nthreads() default.
- beam_convolution.rst: drop the now-unnecessary explicit nthreads=0
  from the convolve_sky() example. Verified via `sphinx -b doctest`
  that the example's expected output is unchanged.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Aug 25, 2026

Copy link
Copy Markdown

Coverage report

Click to see where and how coverage changed

FileStatementsMissingCoverageCoverage
(new stmts)
Lines missing
  litebird_sim
  __init__.py
  beam_convolution.py
  constants.py
  grasp2alm.py 97
  input_sky.py
  maps_and_harmonics.py 2615, 2838
  pointings_in_obs.py
  scan_map.py
  simulations.py 451
  utilities.py 8, 20, 57
  litebird_sim/hwp_harmonics
  hwp_harmonics.py 233
  litebird_sim/mapmaking
  common.py 168, 203-216, 224-226
  h_maps.py 281-283, 343
Project Total  

This report was generated by python-coverage-comment-action

@anand-avinash anand-avinash left a comment

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.

Everything looks good except few missed threading opportunity and addressing the contention.

Comment thread litebird_sim/input_sky.py Outdated
Comment thread litebird_sim/mapmaking/common.py Outdated
Comment thread litebird_sim/utilities.py Outdated
def _compute_nthreads() -> int:
if NUM_THREADS_ENVVAR in os.environ:
return int(os.environ[NUM_THREADS_ENVVAR])
return _hardware_threads()

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.

This will lead to using all available processes for threading, causing contention. Would be better if we simply set it to return 1, when no envvar is set.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Hi @anand-avinash, I made some test here and it seems to work fine on clusters. Can we keep as it is and invest more on the documentation? misc.available_hardware_threads of ducc0 seems to work very well. I've also checked whether numba/ducc0 could conflict directly, but I could not find any case.

paganol and others added 3 commits August 25, 2026 16:36
Two more direct ducc0.healpix calls were missing nthreads entirely,
found by auditing every ducc0 call site in input_sky.py and
mapmaking/common.py:

- input_sky.py: BeamCommon._dipole_map_values() called hpx.pix2vec()
  without nthreads; now passes self.params.nthreads.
- mapmaking/common.py: _compute_pixel_indices_single_detector() had no
  nthreads parameter at all, so its hpx.ang2pix() call and its
  _get_pointings_array() call both used ducc0's/that function's own
  default instead of the caller's resolved value. Added
  nthreads: int | None = None, resolved via resolve_nthreads().
- mapmaking/h_maps.py: make_h_maps() (the only caller of
  _compute_pixel_indices_single_detector) had no nthreads parameter
  either; added one and threaded it through.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Resolves conflicts with PR #549 (fix input sky unit error), which
landed on master after this branch was opened. Both changes touch
litebird_sim/input_sky.py in non-overlapping regions and merged
cleanly; only CHANGELOG.md needed manual conflict resolution (kept
both entries). Verified: full test suite (289 passed, 6 skipped) +
test_mpi_n4.py + ruff check, all green.
#544)

Per Avinash's review comment on #550: with no OMP_NUM_THREADS (or
NUMBA_NUM_THREADS) set, resolve_nthreads()/resolve_numba_nthreads()
previously fell back to ducc0.misc.available_hardware_threads() --
every hardware thread available to the process. That risks
oversubscription on unconfigured multi-rank MPI/OpenMP jobs, which is
the more consequential failure mode: HPC jobs are expected to set
OMP_NUM_THREADS per rank anyway, so this fallback mainly mattered for
laptop/workstation runs, where using every core by default is a much
smaller risk than one rank silently grabbing a whole shared node.

- utilities.py: drop the ducc0.misc-based _hardware_threads() helper;
  _compute_nthreads() now falls back to 1 when NUM_THREADS_ENVVAR is
  unset. _compute_numba_nthreads() is unaffected in structure (still
  falls back to _compute_nthreads()), so it inherits the same change.
- mpi.rst, maps_and_harmonics.rst: updated to describe the new
  fallback and drop the now-obsolete MPI-oversubscription warning
  (the new default is safe by construction); the Multithreading intro
  now says explicitly that the Framework does not use every core by
  default and that OMP_NUM_THREADS must be set for that.
- CHANGELOG.md: flagged as a behavior change.

Verified: full test suite (289 passed) + test_mpi_n4.py + ruff check,
all green; doctests in the touched docs pages still pass unchanged
(their expected output was already thread-count independent).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Missing nthreads arguments and its inconsistent defaults

2 participants