Development documentation — 0.3.0a4 / main
This is the development branch, not a published stable release. Use the source installation guide. Functional checks cover Ubuntu, Windows and macOS; interpreter, artifact and optional-feature limits still apply. Read support, release gates and known limitations before relying on results.
Native threads in numerical callbacks
n_jobs limits RKO search processes, not BLAS/OpenMP threads. A four-worker
portfolio with four OpenBLAS threads per worker can request sixteen BLAS threads
on four usable CPUs. Configured team sizes are not proof that every thread is
active; idle native pools alone do not demonstrate CPU oversubscription.
Configure the application launch, before importing NumPy
For a process-parallel numerical workload, one native thread per search worker is a useful starting point to measure, not a universal optimum. In Bash/WSL, prefix your normal command with the relevant backend settings. For example, this fully runnable inspection command starts NumPy with one-thread settings:
OPENBLAS_NUM_THREADS=1 OMP_NUM_THREADS=1 MKL_NUM_THREADS=1 BLIS_NUM_THREADS=1 \
uv run --locked --with threadpoolctl python -m threadpoolctl -i numpy
Replace the diagnostic command after the environment prefix with your existing
uv run --locked python your_solver.py or uv run --locked python -m examples.run_kp_cpp_instances ... invocation.
Those are templates for your own script/arguments, not bundled filenames. The
prefix applies only to that command and its descendants; it does not persistently
export settings to the shell or change the RKO default. threadpoolctl is needed
only for inspection/the benchmark, not for normal solves.
For Python applications or Windows shells, launch a fresh interpreter with an
explicit environment rather than changing a live application's native pools.
This template preserves the caller's environment; replace your_solver_module
with your application's guarded module:
import os
import subprocess
import sys
if __name__ == "__main__":
settings = dict.fromkeys(
("OPENBLAS_NUM_THREADS", "OMP_NUM_THREADS", "MKL_NUM_THREADS", "BLIS_NUM_THREADS"),
"1",
)
subprocess.run(
[sys.executable, "-m", "your_solver_module"],
env=os.environ | settings,
check=True,
)
This approach also applies when the caller has already imported NumPy: the new
interpreter imports its own libraries using the supplied environment, and spawned
RKO children inherit it. Changing os.environ after importing a native library
is not a reliable way to reconfigure the library already loaded in that process.
Do not put unguarded process creation in the solver module.
OpenBLAS pthread builds prioritize OPENBLAS_NUM_THREADS; OpenMP builds use
OMP_NUM_THREADS instead. MKL/BLIS controls are backend-specific; domain-specific
settings or other libraries can require additional configuration. Inspect the
actual runtime and verify your workload. These settings do not limit arbitrary
callback thread pools, GPU runtimes, other applications or concurrent RKO solves.
Keep n_jobs within the resources allocated to this application as well.
Why RKO does not silently change native pools
Wrapping only the parent solve() in threadpoolctl.threadpool_limits(...) does
not configure fresh spawned workers. Our negative control reports one BLAS
thread in the parent but four in each worker under the inherited environment.
Runtime controllers act on libraries loaded in their process; libraries imported
later also need separate consideration. With the measured OpenBLAS pthread build,
changing a runtime limit affects the entire current process, potentially disturbing
other numerical work or simultaneous thread-backend solves.
Consequently this change adds no native-thread keyword, runtime dependency,
parent-environment mutation, private vendor ctypes calls or implicit global
policy to RKO. Launch-time controls are explicit and inherited without those
cross-solve races. A future automatic controller needs a separately supported
scope/vendor contract; it is not implemented here.
Reproduce and verify workers
uv run --locked --with threadpoolctl==3.7.0 python -m benchmarks.runtime.native_threads
The default benchmark alternates inherited/one-thread launch environments over three repeats, three neutral numerical objectives and one/four dedicated workers. It verifies actual worker native-team counts, assigned initial seeds, identical candidate-stream hashes, finite completed evaluation counts and independent result costs. Parent environment/runtime limits are checked unchanged. Existing inherited caps are honored, not cleared to manufacture a baseline. On a machine already limited to one native thread, the two launch policies may be identical.
Populations are deliberately larger than the 64-evaluation prefix: no full generation or asynchronous elite feedback is measured. Dense objectives use 512-dimensional keys and fixed matrices. Warm-up is excluded from callback timing but included in search/end-to-end timing. The private stopping signal in this benchmark is instrumentation, not a supported user callback interface.
The identified Linux reference records OpenBLAS 0.3.31.dev, NumPy 2.4.3, Python 3.14.8 and four usable CPUs. In its four-worker dense-matrix prefix, inherited/one-thread medians were:
| Metric | Inherited (4 BLAS threads/worker) | One thread/worker |
|---|---|---|
| Slowest worker search prefix | 6.016 s | 3.491 s |
| End-to-end solve | 7.564 s | 4.860 s |
| Sum of measured callback process CPU | 18.574 s | 7.967 s |
A separate full repetition also improved this dense workload, but absolute timings varied. Single-worker dense callbacks also improved at this matrix size, so the measurements do not isolate outer-process oversubscription as the sole cause: per-kernel team overhead also matters. Cheap/vector objectives showed mixed search timings. These single-worker results are not evidence that single-threaded BLAS always wins. These are fixed initialization-prefix measurements, not full-portfolio speedups, solution-quality improvements or C++/Rust parity. BLAS summation order can change floating-point costs, so numerical checks use tolerance. Windows CI runs only a tiny correctness/configuration smoke case, not this performance experiment.
Sources: - NumPy 2.4 global performance configuration. - NumPy runtime inspection. - OpenBLAS threading controls and precedence. - threadpoolctl scope and limitations.