Skip to content

Fix n_jobs=-1 oversubscribing cores under CPU affinity restrictions - #4724

Open
lochhh wants to merge 6 commits into
SpikeInterface:mainfrom
lochhh:cpu-count
Open

Fix n_jobs=-1 oversubscribing cores under CPU affinity restrictions#4724
lochhh wants to merge 6 commits into
SpikeInterface:mainfrom
lochhh:cpu-count

Conversation

@lochhh

@lochhh lochhh commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Closes #4723

This PR

  • adds get_usable_cpu_count() in job_tools.py, preferring os.process_cpu_count() (Python >= 3.13, cross-platform affinity-aware) or os.sched_getaffinity() (older Python on Linux) and falls back to os.cpu_count() when neither is available
  • adds unit test for get_usable_cpu_count()
  • uses get_usable_cpu_count() everywhere n_jobs was previously resolved from os.cpu_count()
  • updates tests to compute their expected boundary values with get_usable_cpu_count()
  • additionally fixes the order of installation in deepinterpolation CI workflow as installing spikeinterface was silently reupgrading numpy to 2.x which breaks tensorflow's complied extensions
  • additionally fixes a pre-existing test-order-dependency bug in test_fix_job_kwargs: set_global_job_kwargs merges via dict.update() and can't remove keys, so the manual "restore" step that uses set_global_job_kwargs left chunk_memory key in global_job_kwargs. The fix uses reset_global_job_kwargs() following the same pattern in test_globals.py

lochhh added 5 commits July 30, 2026 16:56
- `fix_job_kwargs` caps `n_jobs` using `get_usable_cpu_count` and tests
  need to compute their expected boundary with the same function
- `set_global_job_kwargs` merges via `dict.update()` and doesn't remove `chunk_memory` key
@lochhh
lochhh marked this pull request as draft July 30, 2026 16:01
@lochhh
lochhh marked this pull request as ready for review July 30, 2026 16:28
Comment on lines +74 to +84
def get_usable_cpu_count():
"""
Number of CPUs usable by this process.
"""
if hasattr(os, "process_cpu_count"):
# Python >= 3.13; respects both Windows job object and Linux cgroup/cpuset restrictions
return os.process_cpu_count()
if hasattr(os, "sched_getaffinity"):
# Respects Linux cgroup/cpuset restrictions
return len(os.sched_getaffinity(0))
return os.cpu_count()

@chrishalcrow chrishalcrow Jul 30, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Would be good to test this function out on as many real life systems as possible to try and find any edge cases. I've tested on mac, local linux desktop Edinburgh supercomputer.

@lochhh have you tested this on the HPC?
@zm711 could you quickly test on windows?
@alejoe91 could you check in your set up?

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.

requested 4 cpus on HPC: srun --cpus-per-task=4 --pty bash -i

System Python process_cpu_count sched_getaffinity cpu_count
HPC Linux 6.8 (Ubuntu) 3.11.5 None 4 24
HPC Linux 6.8 (Ubuntu) 3.13.2 4 4 24
Laptop Windows 11 3.14.4 16 AttributeError 16

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I also just want to make it clear in general that even working locally on a Windows 10/11 station setting n_jobs to be 7 (for 8 cores) would lead to all 8 cores being used at various stages because:

  1. n_jobs is ultimately setting the number of processes while the OS chooses to schedule across cores so even if you say 1 job per core that doesn't mean that you are telling the computer to use 1 process/core and all processes could go to one core or setting 2 processes could utilize all cores
  2. we don't control scipy, numpy, torch, sklearn allocation of resources so they can also can affect this behavior and lead to n_jobs =/= n_cores.

That being said I have no problem with the logic for -1 being changed to not lead to too many processes for the number of cores (ie trying to keep the semantic that -1 = n_cpus available). But I just want us to remember not to overpromise what n_jobs is actually setting.

@alejoe91

Copy link
Copy Markdown
Member

@lochhh thanks for this! I agree it was an hassle! Please let's give us a couple of weeks as many of us are off for vacation at the moment. I agree with @chrishalcrow that we should test on a bunch of platforms to make sure it works as expected everywhere

@alejoe91 alejoe91 added core Changes to core module concurrency Related to parallel processing labels Jul 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

concurrency Related to parallel processing core Changes to core module

Projects

None yet

Development

Successfully merging this pull request may close these issues.

n_jobs=-1 in job_tools.py oversubscribes cores under CPU affinity restrictions

4 participants