Thanks to visit codestin.com
Credit goes to github.com

Skip to content

PRF Don't force X to C-order for LogisticRegression's lbfgs solver - #34903

Draft
cakedev0 wants to merge 2 commits into
scikit-learn:mainfrom
cakedev0:lbfgs-preserve-x-order
Draft

PRF Don't force X to C-order for LogisticRegression's lbfgs solver#34903
cakedev0 wants to merge 2 commits into
scikit-learn:mainfrom
cakedev0:lbfgs-preserve-x-order

Conversation

@cakedev0

@cakedev0 cakedev0 commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

Reference Issues/PRs

Related to #32162 (that one's about OpenMP threads in the loss kernel; this is about BLAS threads in the GEMV calls around it).

What does this implement/fix? Explain your changes.

While investigating why LogisticRegression(solver="lbfgs") doesn't benefit from (and sometimes gets hurt by) multithreaded OpenBLAS, I traced it down to the backward-pass gradient computation (X.T @ grad_pointwise in LinearModelLoss.loss_gradient). For n_samples >> n_features (the common shape), that GEMV parallelizes much better for the F-order (for the C-order, on my machine, parallelization is usually neutral / slightly counter-productive)

Turns out LogisticRegression.fit() unconditionally forces X to C-order for solver="lbfgs", even though X is only read (never mutated) throughout all L-BFGS iterations. So if a caller already had F-order X, we were silently paying for a full copy and throwing away the layout that would've made BLAS threading actually work.

This PR just changes that to order=None (preserve caller's layout) for solver="lbfgs". This gives a nice speed-up and saves memory for users passing a F-ordered X.

AI usage disclosure

  • Research and understanding
  • Early benchmarks generation (the numbers below, and a scratch benchmark script I'm not including in the PR)

Benchmarks

LogisticRegression(max_iter=200).fit(X, y) on synthetic binary data, 9 fits, median, 14 BLAS threads (this machine's core count).

For X already Fortran-ordered:

n_samples=100,000,  n_features=200: before 0.24s -> after 0.17s
n_samples=500,000,  n_features=200: before 1.00s -> after 0.71s  (~1.4x)
n_samples=1,000,000, n_features=200: before 1.53s -> after 1.04s  (~1.5x)

Peak memory (1M x 200 float64, X is ~1.6GB):

before (forced C-order copy): 3.16 GB peak RSS
after  (layout preserved):    1.60 GB peak RSS

X is only read (never mutated) across all L-BFGS iterations, so forcing
a C-order copy discards any layout the caller already had. If X is
already Fortran-contiguous, the backward-pass GEMV in the loss/gradient
(X.T @ grad) lands on OpenBLAS's better-parallelizing kernel, and the
forced copy (which doubles peak memory and costs real time) is avoided
entirely. C-order input, the common case, is unaffected.
),
accept_large_sparse=solver not in ("liblinear", "newton-cd", "sag", "saga"),
)
n_samples, n_features = X.shape

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.

Both n_samples and n_features are unused...

@cakedev0 cakedev0 changed the title EFF Don't force X to C-order for LogisticRegression's lbfgs solver PRF Don't force X to C-order for LogisticRegression's lbfgs solver Sep 7, 2026
@cakedev0 cakedev0 added this to Labs Sep 8, 2026
@cakedev0 cakedev0 moved this to In progress in Labs Sep 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant