Skip to content

Improve threading defaults, docs, and sys_info - #14103

Merged
drammock merged 6 commits into
mne-tools:mainfrom
larsoner:blas
Jul 28, 2026
Merged

Improve threading defaults, docs, and sys_info#14103
drammock merged 6 commits into
mne-tools:mainfrom
larsoner:blas

Conversation

@larsoner

@larsoner larsoner commented Jul 26, 2026

Copy link
Copy Markdown
Member

Some modest progress toward #13766 -- better docs about how to control threads (our docs were wrong for anyone using our installers!) and add control mechanism to sys_info. Implemented and figured out with the help of Claude Opus 5.

Closes #13766

@larsoner larsoner changed the title Improve threading docs and add to sys_info Improve threading defaults, docs, and sys_info Jul 27, 2026
@larsoner
larsoner requested a review from mscheltienne as a code owner July 27, 2026 17:18
@larsoner

Copy link
Copy Markdown
Member Author

@cbrnr @drammock feel free to try! Ready for review/merge from my end.

Test script to run while on this PR (mocks main behavior too)
"""Tiny main-vs-PR benchmark for mne-tools/mne-python#14103.

Run with the PR branch checked out. It A/Bs inside one process by neutering the
new thread limiting (which is exactly main's behavior), interleaving the two
conditions so any machine drift hits both equally, and reporting medians.
"""

import os
import statistics
import time
import warnings
from contextlib import contextmanager, nullcontext

warnings.filterwarnings("ignore")
import mne  # noqa: E402
from mne.preprocessing import ICA, maxwell_filter  # noqa: E402
from mne.utils import linalg  # noqa: E402
from mne.utils.config import _get_numpy_libs  # noqa: E402

mne.set_log_level("ERROR")
N_ROUNDS = 5

if already := [v for v in linalg._BLAS_THREAD_ENV_VARS if os.getenv(v)]:
    raise SystemExit(
        f"unset {' '.join(already)} first -- they disable the PR's limiting"
    )

path = mne.datasets.sample.data_path()
raw = mne.io.read_raw_fif(path / "MEG/sample/sample_audvis_raw.fif")
raw.crop(tmax=60.0).load_data()
raw.fix_mag_coil_types()
raw2 = raw.copy().set_eeg_reference(projection=True)
epochs = mne.Epochs(raw2, mne.find_events(raw2), tmin=-0.2, tmax=0.5, preload=True)
raw_ica = raw.copy().pick("eeg").filter(l_freq=1.0, h_freq=None)

OPS = {
    "Maxwell tSSS": lambda: maxwell_filter(
        raw.copy(),
        cross_talk=path / "SSS/ct_sparse_mgh.fif",
        calibration=path / "SSS/sss_cal_mgh.dat",
        st_duration=10.0,
    ),
    "Cov shrunk": lambda: mne.compute_covariance(epochs, tmax=0.0, method="shrunk"),
    "ICA": lambda: ICA(random_state=97).fit(raw_ica),
}


@contextmanager
def as_main():
    """Make the PR's limiting inert, i.e. reproduce main."""
    orig = linalg._blas_thread_controller
    linalg._blas_thread_controller = lambda: None
    try:
        yield
    finally:
        linalg._blas_thread_controller = orig


times = {name: {"main": [], "PR": []} for name in OPS}
for round_ in range(N_ROUNDS + 1):  # first round is warmup
    for label in ("main", "PR"):
        for name, fn in OPS.items():
            with as_main() if label == "main" else nullcontext():
                t0 = time.perf_counter()
                fn()
                dt = time.perf_counter() - t0
            if round_:
                times[name][label].append(dt)

print(f"BLAS: {_get_numpy_libs()}")
print(f"CPUs: {linalg._n_available_cpus()}, PR caps at {linalg._MAX_BLAS_THREADS}")
print(f"median of {N_ROUNDS} interleaved rounds\n")
print(f"{'operation':<14} {'main':>9} {'PR':>9} {'speedup':>8}")
print("-" * 43)
for name in OPS:
    m = statistics.median(times[name]["main"])
    p = statistics.median(times[name]["PR"])
    print(f"{name:<14} {m:8.3f}s {p:8.3f}s {m / p:7.2f}x")

For me on my macOS laptop I get

BLAS: OpenBLAS 0.3.33 with 10 threads (openmp threading layer)
CPUs: 10, PR caps at 3
median of 5 interleaved rounds

operation           main        PR  speedup
-------------------------------------------
Maxwell tSSS      4.106s    2.995s    1.37x
Cov shrunk        1.977s    1.844s    1.07x
ICA               1.697s    1.330s    1.28x

@cbrnr

cbrnr commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Thanks @larsoner, I'll test tomorrow. Just one quick question, why are you using OpenBLAS on macOS and how did you install/enable it? From what I've read, Accelerate should be much faster in most scenarios.

@larsoner

Copy link
Copy Markdown
Member Author

It's what our installers use by default. We should probably switch to accelerate there, I'll look into it (https://conda-forge.org/news/2025/07/31/new-accelerate-macos/)

@cbrnr cbrnr 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.

Very nice! As expected, there's no effect when using Accelerate:

BLAS: accelerate (numpy build config, threads not introspectable)
CPUs: 12, PR caps at 3
median of 5 interleaved rounds

operation           main        PR  speedup
-------------------------------------------
Maxwell tSSS      2.751s    2.719s    1.01x
Cov shrunk        1.532s    1.520s    1.01x
ICA               1.362s    1.383s    0.99x

Just two minor comments:

  1. Please use Accelerate and not accelerate in the sys_info output (maybe also don't use all lower-case for all packages accordingly, e.g., NumPy, OpenMP, ...)
  2. Make sure comments and docstrings are wrapped at 88 characters (it seems like they wrap much earlier, something like 72).

@drammock
drammock merged commit 7ec1a35 into mne-tools:main Jul 28, 2026
29 checks passed
@drammock
drammock deleted the blas branch July 28, 2026 14:46
@drammock

Copy link
Copy Markdown
Member

thanks @larsoner

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.

PERF: Default OpenBLAS thread count causes up to 13x regression for decomposition-heavy operations

3 participants