A zopen community port of scikit-learn for z/OS.
zopen install scikit-learnWheels for each supported interpreter, built from the upstream sources with
meson. It depends on the numpy and scipy ports at runtime and bundles
joblib, threadpoolctl and narwhals, which are pure Python and have no
ports of their own.
scikit-learn is built without OpenMP, and is therefore single-threaded.
z/OS has no OpenMP runtime. scikit-learn is written for this case: when
_OPENMP is undefined its _openmp_helpers.pxd supplies no-op replacements
for the OpenMP routines -- omp_get_max_threads() becomes 1 -- and Cython
lowers prange to an ordinary serial loop. Results are unchanged; work that
would be spread over threads runs on one.
You can confirm what a given install was built with:
>>> from sklearn.utils._openmp_helpers import _openmp_parallelism_enabled
>>> _openmp_parallelism_enabled()
FalseEstimators that parallelise through joblib rather than OpenMP -- anything
taking n_jobs -- are unaffected and still use multiple processes.
This requires a meson that link-tests OpenMP before reporting it available.
The compiler here accepts -fopenmp, defines _OPENMP and ships omp.h
while providing no runtime, so an unpatched meson reports OpenMP as found and
the build then fails at the bind with omp_get_max_threads unresolved. The
meson port carries that check.
LogisticRegression().fit(X, y) returns a model that scores at chance.
Its default solver is lbfgs, which calls
scipy.optimize.minimize(method="L-BFGS-B"). On this platform that returns
success=True after performing no iterations, handing back the starting point.
Nothing raises and nothing warns, so the model looks fitted and is not.
Use any other solver:
LogisticRegression(solver="liblinear").fit(X, y)liblinear, newton-cg, newton-cholesky, sag and saga are all
unaffected -- none of them routes through scipy.optimize. The same applies to
any other code that chooses L-BFGS-B, whether explicitly or by default.
This is a scipy problem, not a scikit-learn one, and it is specific to
big-endian: an integer whose width does not match what the BLAS expects reads
as zero here and reads correctly on little-endian machines, so 1/‖d‖ becomes
infinite and the first line-search step is clamped to its 1e10 ceiling. Every
other scipy solver tested is exact -- BFGS, CG, TNC, SLSQP, Powell and
Nelder-Mead.
The port's own check therefore fits with liblinear, and records on every
build whether the default solver has started working.
scikit-learn's pyproject.toml caps its build requirements at numpy<2.5.0
and scipy<1.18.0, both below what the zopen wheel index carries. Those caps
are overridden deliberately, with --skip-dependency-check on the build call.
(--no-isolation on its own is not enough: build still validates the
requirements it finds and refuses to call the backend when a cap is violated.)
What still enforces versions is scikit-learn's own meson.build, which checks
its declared minimums and errors out if they are not met. Only the upper bounds
are overridden, and those are upstream declining to promise something it has
not tested rather than a known incompatibility -- scipy 1.18 still ships the
cython_blas and cython_special interfaces scikit-learn cimports.