scm2cpp-lasso 0.13.0__tar.gz → 0.13.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. {scm2cpp_lasso-0.13.0 → scm2cpp_lasso-0.13.2}/PKG-INFO +107 -4
  2. {scm2cpp_lasso-0.13.0 → scm2cpp_lasso-0.13.2}/README.md +106 -3
  3. {scm2cpp_lasso-0.13.0 → scm2cpp_lasso-0.13.2}/pyproject.toml +1 -1
  4. {scm2cpp_lasso-0.13.0 → scm2cpp_lasso-0.13.2}/setup.py +43 -5
  5. {scm2cpp_lasso-0.13.0 → scm2cpp_lasso-0.13.2}/src/scm2cpp_lasso/__init__.py +1 -1
  6. {scm2cpp_lasso-0.13.0 → scm2cpp_lasso-0.13.2}/src/scm2cpp_lasso/_generated/scm2cpp.hpp +31 -0
  7. {scm2cpp_lasso-0.13.0 → scm2cpp_lasso-0.13.2}/src/scm2cpp_lasso.egg-info/PKG-INFO +107 -4
  8. {scm2cpp_lasso-0.13.0 → scm2cpp_lasso-0.13.2}/setup.cfg +0 -0
  9. {scm2cpp_lasso-0.13.0 → scm2cpp_lasso-0.13.2}/src/scm2cpp_lasso/_generated/__init__.py +0 -0
  10. {scm2cpp_lasso-0.13.0 → scm2cpp_lasso-0.13.2}/src/scm2cpp_lasso/_generated/_loader.py +0 -0
  11. {scm2cpp_lasso-0.13.0 → scm2cpp_lasso-0.13.2}/src/scm2cpp_lasso/_generated/_loader_accel_cublas.py +0 -0
  12. {scm2cpp_lasso-0.13.0 → scm2cpp_lasso-0.13.2}/src/scm2cpp_lasso/_generated/_loader_classifiers.py +0 -0
  13. {scm2cpp_lasso-0.13.0 → scm2cpp_lasso-0.13.2}/src/scm2cpp_lasso/_generated/_loader_derived_blas.py +0 -0
  14. {scm2cpp_lasso-0.13.0 → scm2cpp_lasso-0.13.2}/src/scm2cpp_lasso/_generated/_loader_derived_cublas.py +0 -0
  15. {scm2cpp_lasso-0.13.0 → scm2cpp_lasso-0.13.2}/src/scm2cpp_lasso/_generated/_loader_derived_loops.py +0 -0
  16. {scm2cpp_lasso-0.13.0 → scm2cpp_lasso-0.13.2}/src/scm2cpp_lasso/_generated/_loader_gpu_cuda.py +0 -0
  17. {scm2cpp_lasso-0.13.0 → scm2cpp_lasso-0.13.2}/src/scm2cpp_lasso/_generated/_loader_gramops_blas.py +0 -0
  18. {scm2cpp_lasso-0.13.0 → scm2cpp_lasso-0.13.2}/src/scm2cpp_lasso/_generated/_loader_gramops_loops.py +0 -0
  19. {scm2cpp_lasso-0.13.0 → scm2cpp_lasso-0.13.2}/src/scm2cpp_lasso/_generated/_loader_nmf_blas.py +0 -0
  20. {scm2cpp_lasso-0.13.0 → scm2cpp_lasso-0.13.2}/src/scm2cpp_lasso/_generated/_loader_nmf_cublas.py +0 -0
  21. {scm2cpp_lasso-0.13.0 → scm2cpp_lasso-0.13.2}/src/scm2cpp_lasso/_generated/_loader_nmf_loops.py +0 -0
  22. {scm2cpp_lasso-0.13.0 → scm2cpp_lasso-0.13.2}/src/scm2cpp_lasso/_generated/accel_cublas/accel.cpp +0 -0
  23. {scm2cpp_lasso-0.13.0 → scm2cpp_lasso-0.13.2}/src/scm2cpp_lasso/_generated/accel_cublas/accel.hpp +0 -0
  24. {scm2cpp_lasso-0.13.0 → scm2cpp_lasso-0.13.2}/src/scm2cpp_lasso/_generated/accel_cublas/accel_capi.cpp +0 -0
  25. {scm2cpp_lasso-0.13.0 → scm2cpp_lasso-0.13.2}/src/scm2cpp_lasso/_generated/classifiers.cpp +0 -0
  26. {scm2cpp_lasso-0.13.0 → scm2cpp_lasso-0.13.2}/src/scm2cpp_lasso/_generated/classifiers.hpp +0 -0
  27. {scm2cpp_lasso-0.13.0 → scm2cpp_lasso-0.13.2}/src/scm2cpp_lasso/_generated/classifiers_capi.cpp +0 -0
  28. {scm2cpp_lasso-0.13.0 → scm2cpp_lasso-0.13.2}/src/scm2cpp_lasso/_generated/derived_blas/lasso_design.cpp +0 -0
  29. {scm2cpp_lasso-0.13.0 → scm2cpp_lasso-0.13.2}/src/scm2cpp_lasso/_generated/derived_blas/lasso_design.hpp +0 -0
  30. {scm2cpp_lasso-0.13.0 → scm2cpp_lasso-0.13.2}/src/scm2cpp_lasso/_generated/derived_blas/lasso_design_capi.cpp +0 -0
  31. {scm2cpp_lasso-0.13.0 → scm2cpp_lasso-0.13.2}/src/scm2cpp_lasso/_generated/derived_cublas/lasso_design.cpp +0 -0
  32. {scm2cpp_lasso-0.13.0 → scm2cpp_lasso-0.13.2}/src/scm2cpp_lasso/_generated/derived_cublas/lasso_design.hpp +0 -0
  33. {scm2cpp_lasso-0.13.0 → scm2cpp_lasso-0.13.2}/src/scm2cpp_lasso/_generated/derived_cublas/lasso_design_capi.cpp +0 -0
  34. {scm2cpp_lasso-0.13.0 → scm2cpp_lasso-0.13.2}/src/scm2cpp_lasso/_generated/derived_loops/lasso_design.cpp +0 -0
  35. {scm2cpp_lasso-0.13.0 → scm2cpp_lasso-0.13.2}/src/scm2cpp_lasso/_generated/derived_loops/lasso_design.hpp +0 -0
  36. {scm2cpp_lasso-0.13.0 → scm2cpp_lasso-0.13.2}/src/scm2cpp_lasso/_generated/derived_loops/lasso_design_capi.cpp +0 -0
  37. {scm2cpp_lasso-0.13.0 → scm2cpp_lasso-0.13.2}/src/scm2cpp_lasso/_generated/gpu_cuda/gpu.cpp +0 -0
  38. {scm2cpp_lasso-0.13.0 → scm2cpp_lasso-0.13.2}/src/scm2cpp_lasso/_generated/gpu_cuda/gpu.hpp +0 -0
  39. {scm2cpp_lasso-0.13.0 → scm2cpp_lasso-0.13.2}/src/scm2cpp_lasso/_generated/gpu_cuda/gpu_capi.cpp +0 -0
  40. {scm2cpp_lasso-0.13.0 → scm2cpp_lasso-0.13.2}/src/scm2cpp_lasso/_generated/gramops_blas/gramops.cpp +0 -0
  41. {scm2cpp_lasso-0.13.0 → scm2cpp_lasso-0.13.2}/src/scm2cpp_lasso/_generated/gramops_blas/gramops.hpp +0 -0
  42. {scm2cpp_lasso-0.13.0 → scm2cpp_lasso-0.13.2}/src/scm2cpp_lasso/_generated/gramops_blas/gramops_capi.cpp +0 -0
  43. {scm2cpp_lasso-0.13.0 → scm2cpp_lasso-0.13.2}/src/scm2cpp_lasso/_generated/gramops_loops/gramops.cpp +0 -0
  44. {scm2cpp_lasso-0.13.0 → scm2cpp_lasso-0.13.2}/src/scm2cpp_lasso/_generated/gramops_loops/gramops.hpp +0 -0
  45. {scm2cpp_lasso-0.13.0 → scm2cpp_lasso-0.13.2}/src/scm2cpp_lasso/_generated/gramops_loops/gramops_capi.cpp +0 -0
  46. {scm2cpp_lasso-0.13.0 → scm2cpp_lasso-0.13.2}/src/scm2cpp_lasso/_generated/lasso_cov.cpp +0 -0
  47. {scm2cpp_lasso-0.13.0 → scm2cpp_lasso-0.13.2}/src/scm2cpp_lasso/_generated/lasso_cov.hpp +0 -0
  48. {scm2cpp_lasso-0.13.0 → scm2cpp_lasso-0.13.2}/src/scm2cpp_lasso/_generated/lasso_cov_capi.cpp +0 -0
  49. {scm2cpp_lasso-0.13.0 → scm2cpp_lasso-0.13.2}/src/scm2cpp_lasso/_generated/nmf_blas/nmf_cd.cpp +0 -0
  50. {scm2cpp_lasso-0.13.0 → scm2cpp_lasso-0.13.2}/src/scm2cpp_lasso/_generated/nmf_blas/nmf_cd.hpp +0 -0
  51. {scm2cpp_lasso-0.13.0 → scm2cpp_lasso-0.13.2}/src/scm2cpp_lasso/_generated/nmf_blas/nmf_cd_capi.cpp +0 -0
  52. {scm2cpp_lasso-0.13.0 → scm2cpp_lasso-0.13.2}/src/scm2cpp_lasso/_generated/nmf_cublas/nmf_cd.cpp +0 -0
  53. {scm2cpp_lasso-0.13.0 → scm2cpp_lasso-0.13.2}/src/scm2cpp_lasso/_generated/nmf_cublas/nmf_cd.hpp +0 -0
  54. {scm2cpp_lasso-0.13.0 → scm2cpp_lasso-0.13.2}/src/scm2cpp_lasso/_generated/nmf_cublas/nmf_cd_capi.cpp +0 -0
  55. {scm2cpp_lasso-0.13.0 → scm2cpp_lasso-0.13.2}/src/scm2cpp_lasso/_generated/nmf_loops/nmf_cd.cpp +0 -0
  56. {scm2cpp_lasso-0.13.0 → scm2cpp_lasso-0.13.2}/src/scm2cpp_lasso/_generated/nmf_loops/nmf_cd.hpp +0 -0
  57. {scm2cpp_lasso-0.13.0 → scm2cpp_lasso-0.13.2}/src/scm2cpp_lasso/_generated/nmf_loops/nmf_cd_capi.cpp +0 -0
  58. {scm2cpp_lasso-0.13.0 → scm2cpp_lasso-0.13.2}/src/scm2cpp_lasso/_generated/scm2cpp-blas.hpp +0 -0
  59. {scm2cpp_lasso-0.13.0 → scm2cpp_lasso-0.13.2}/src/scm2cpp_lasso/_generated/scm2cpp-cublas.hpp +0 -0
  60. {scm2cpp_lasso-0.13.0 → scm2cpp_lasso-0.13.2}/src/scm2cpp_lasso/_generated/scm2cpp-cuda.hpp +0 -0
  61. {scm2cpp_lasso-0.13.0 → scm2cpp_lasso-0.13.2}/src/scm2cpp_lasso/_libfind.py +0 -0
  62. {scm2cpp_lasso-0.13.0 → scm2cpp_lasso-0.13.2}/src/scm2cpp_lasso/batch_capi.cu +0 -0
  63. {scm2cpp_lasso-0.13.0 → scm2cpp_lasso-0.13.2}/src/scm2cpp_lasso.egg-info/SOURCES.txt +0 -0
  64. {scm2cpp_lasso-0.13.0 → scm2cpp_lasso-0.13.2}/src/scm2cpp_lasso.egg-info/dependency_links.txt +0 -0
  65. {scm2cpp_lasso-0.13.0 → scm2cpp_lasso-0.13.2}/src/scm2cpp_lasso.egg-info/requires.txt +0 -0
  66. {scm2cpp_lasso-0.13.0 → scm2cpp_lasso-0.13.2}/src/scm2cpp_lasso.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: scm2cpp-lasso
3
- Version: 0.13.0
3
+ Version: 0.13.2
4
4
  Summary: scikit-learn estimators (lasso, elastic net, ridge, ARD, SVM, SVR, logistic regression, GLMs, Huber, Theil-Sen, F-score selection, RFE, NMF, Gaussian mixture, FastICA, PCA, graphical lasso, RANSAC, SGD, OMP, LARS) translated from Scheme to C++ by scm2cpp; GPU batch solver and CUDA kernels included
5
5
  Author: Hirotaka Niitsuma
6
6
  License: MIT
@@ -18,6 +18,8 @@ Requires-Dist: numpy>=1.17
18
18
 
19
19
  # scm2cpp-lasso
20
20
 
21
+ [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/niitsuma/scm2cpp/blob/master/notebooks/compare-sklearn.ipynb)
22
+
21
23
  *[Japanese (README.ja.md)](README.ja.md)*
22
24
 
23
25
  Lasso by coordinate descent over a Gram matrix, with an optional GPU
@@ -28,9 +30,20 @@ path. The solver is Scheme, translated to C++ by
28
30
  $ pip install scm2cpp-lasso
29
31
  ```
30
32
 
31
- Installing needs a C++17 compiler and nothing else. If `nvcc` is on
32
- the path, the batched GPU solver is built as well; if it is not, the
33
- package installs and works exactly the same, minus that one method.
33
+ On Linux x86_64, macOS arm64 (macOS 14 or later) and Windows AMD64
34
+ this installs a wheel: compiled, nothing to build (with OpenBLAS
35
+ inside on Linux and macOS; Windows has the loop variants). Elsewhere pip builds the sdist,
36
+ which needs a C++17 compiler and nothing else. The wheels carry no
37
+ GPU code (a wheel cannot know the device); for the GPU builds, install
38
+ from the sdist on a machine with `nvcc` on the path:
39
+
40
+ ```console
41
+ $ pip install --no-binary scm2cpp-lasso scm2cpp-lasso
42
+ ```
43
+
44
+ The batched GPU solver, the cuBLAS variants and the `--cuda`
45
+ estimators are then built as well; without `nvcc` the package installs
46
+ and works exactly the same, minus those.
34
47
 
35
48
  Three of a hundred candidate columns carry the signal; the solver is
36
49
  asked which, and answers with them and nothing else:
@@ -89,6 +102,63 @@ at zero, and `lambda_grid()` walks down from it -- the construction
89
102
  scikit-learn uses. If your columns differ wildly in scale, standardize
90
103
  them first; this solver does not do it for you.
91
104
 
105
+ ## Estimators at a glance
106
+
107
+ Every estimator, the scikit-learn estimator it reproduces, the Scheme
108
+ kernel under `examples/kernel-only/` it is translated from, the builds
109
+ it runs on, and whether Intel's sklearnex accelerates the scikit-learn
110
+ side. Builds: *loops* is the plain translation, *BLAS* / *cuBLAS* the
111
+ products handed to (cu)BLAS, *--cuda* the translator's GPU kernels
112
+ (`gpu_cuda/`), *GPU batch* the hand-written CUDA batch solver; the
113
+ package picks the fastest one present. None of the classes derives
114
+ from scikit-learn's `BaseEstimator` yet, so `clone`, `GridSearchCV`
115
+ and `check_estimator` do not apply to them.
116
+
117
+ | class | scikit-learn | Scheme kernel | builds | sklearnex accelerates |
118
+ |---|---|---|---|---|
119
+ | `CovLasso` | Lasso, ElasticNet, lasso_path(precompute=True) | `lasso-cov.scm` | CPU; GPU batch (fit_path_batch, bootstrap) | no |
120
+ | `CovLassoCV` | LassoCV, ElasticNetCV | `lasso-cov.scm` | CPU; GPU batch | no |
121
+ | `DerivedLasso` | lasso_path, from the design | `lasso-design.scm (--derive)` | loops / BLAS / cuBLAS | no |
122
+ | `CovMultiTaskLasso` | MultiTaskLasso, MultiTaskElasticNet | `lasso-cov.scm` | CPU | no |
123
+ | `CovMultiTaskLassoCV` | MultiTaskLassoCV, MultiTaskElasticNetCV | `lasso-cov.scm` | CPU; GPU batch | no |
124
+ | `CovGroupLasso` | (none; skglm GroupLasso) | `lasso-cov.scm` | CPU | - |
125
+ | `CovLogistic` | LogisticRegression(penalty='l1'), by majorization | `lasso-cov.scm` | CPU | no |
126
+ | `CovRidge` | Ridge(fit_intercept=False), a whole alpha path | `lasso-cov.scm` | CPU | Ridge |
127
+ | `CovBayesianRidge` | BayesianRidge | `lasso-cov.scm` | CPU | no |
128
+ | `CovARDRegression` | ARDRegression (n >= p) | `ard.scm` | loops / BLAS | no |
129
+ | `GramLars` | Lars | `lars.scm` | CPU | no |
130
+ | `GramLassoLars` | LassoLars | `lars.scm` | CPU | no |
131
+ | `GramLassoLarsIC` | LassoLarsIC | `lars.scm` | CPU; device Gram | no |
132
+ | `GramLarsCV` | LarsCV | `lars.scm` | CPU | no |
133
+ | `GramLassoLarsCV` | LassoLarsCV | `lars.scm` | CPU; device Gram | no |
134
+ | `GramOMP` | OrthogonalMatchingPursuit | `omp.scm` | loops / BLAS | no |
135
+ | `GramOMPCV` | OrthogonalMatchingPursuitCV | `omp.scm` | loops / BLAS | no |
136
+ | `LBFGSLogisticRegression` | LogisticRegression(solver='lbfgs') | `logistic-lbfgs.scm, lbfgs.scm` | loops / BLAS / --cuda | LogisticRegression |
137
+ | `LBFGSLogisticRegressionCV` | LogisticRegressionCV | `logistic-lbfgs.scm, lbfgs.scm` | loops / BLAS / --cuda | no |
138
+ | `LBFGSHuberRegressor` | HuberRegressor | `huber.scm, lbfgs.scm` | loops / BLAS / --cuda | no |
139
+ | `LBFGSPoissonRegressor` | PoissonRegressor | `glm.scm, lbfgs.scm` | loops / BLAS / --cuda | no |
140
+ | `LBFGSGammaRegressor` | GammaRegressor | `glm.scm, lbfgs.scm` | loops / BLAS / --cuda | no |
141
+ | `LBFGSTweedieRegressor` | TweedieRegressor | `glm.scm, lbfgs.scm` | loops / BLAS / --cuda | no |
142
+ | `LinearSVM` | LinearSVC(loss='hinge') | `svm-liblinear.scm` | CPU | no |
143
+ | `L1LinearSVM` | LinearSVC(penalty='l1', dual=False) | `svm-l1.scm` | CPU | no |
144
+ | `KernelSVM` | SVC | `svm-smo.scm` | CPU | SVC |
145
+ | `LinearSVR` | LinearSVR | `svr-liblinear.scm` | CPU | no |
146
+ | `KernelSVR` | SVR | `svm-smo.scm` | CPU | SVR |
147
+ | `L1LogisticRegression` | LogisticRegression(penalty='l1', solver='liblinear') | `logistic-l1.scm` | CPU | no |
148
+ | `SGDLinearClassifier` | SGDClassifier(shuffle=False) | `sgd.scm` | CPU | no |
149
+ | `SGDLinearRegressor` | SGDRegressor(shuffle=False) | `sgd.scm` | CPU | no |
150
+ | `SGDPerceptron` | Perceptron(shuffle=False) | `sgd.scm` | CPU | no |
151
+ | `LinearRANSAC` | RANSACRegressor (given subsets) | `ransac.scm` | CPU | no |
152
+ | `WeiszfeldTheilSen` | TheilSenRegressor | `theilsen.scm` | CPU | no |
153
+ | `CDNMF` | NMF(solver='cd') | `nmf-cd.scm (--derive)` | loops / BLAS / cuBLAS | no |
154
+ | `CDGraphicalLasso` | GraphicalLasso | `graphical-lasso.scm` | loops / BLAS | no |
155
+ | `EMGaussianMixture` | GaussianMixture(covariance_type='full') | `gmm.scm` | loops / BLAS / --cuda | no |
156
+ | `ParallelFastICA` | FastICA | `fastica.scm` | loops / BLAS / --cuda | no |
157
+ | `CovPCA` | PCA(svd_solver='covariance_eigh') | `pca.scm` | loops / BLAS / cuBLAS | PCA |
158
+ | `f_regression, f_classif` | f_regression, f_classif | `fscore.scm` | loops / BLAS / cuBLAS | no |
159
+ | `SelectKBest, SelectPercentile` | SelectKBest, SelectPercentile | `fscore.scm` | loops / BLAS / cuBLAS | no |
160
+ | `GramRFE` | RFE(Ridge), RFE(LinearRegression) | `rfe.scm` | loops / BLAS / cuBLAS | inside: Ridge |
161
+
92
162
  ## Two kernels: `CovLasso` and `DerivedLasso`
93
163
 
94
164
  Everything above, and the cross-validation, bootstrap and GPU methods
@@ -733,6 +803,39 @@ estimators (`GramLassoLarsIC`, `GramLarsCV`, `GramLassoLarsCV`,
733
803
  `GramLars`, `GramLassoLars`, `CovPCA`) form a large X'X on the device
734
804
  when one answers. `SCM2CPP_NO_DEVICE=1` keeps everything on the CPU.
735
805
 
806
+ ## Against Intel's sklearnex
807
+
808
+ sklearnex (scikit-learn-intelex 2026.2, oneDAL underneath) replaces
809
+ some scikit-learn estimators with its own. `bench/sklearnex-compare.py`
810
+ times the ones with a counterpart here, sklearnex's estimators
811
+ imported directly (not patched in) so that scikit-learn itself is
812
+ timed in the same process. The ratios are scikit-learn's or
813
+ sklearnex's time over the package's best (its CPU build, or the device
814
+ build where one answers); the load average was 8 to 11, so the times
815
+ move by some tens of percent.
816
+
817
+ | estimator, n x p | iterations (sk / sklearnex / ours) | vs scikit-learn, one core | vs sklearnex, one core | vs sklearnex, 20 cores |
818
+ |---|---|---|---|---|
819
+ | LogisticRegression, 200,000 x 100 | 6 / 12 / 6 | 10.6x (device) | 8.4x (device) | 4.4x (device); on the CPU 0.5x |
820
+ | Ridge (alpha 1), 200,000 x 200 | - | 2.1x | 1.2x | 0.3x |
821
+ | PCA (10 components), 100,000 x 50 | - | 1.5x | 2.4x | 1.7x |
822
+ | PCA (10 components), 50,000 x 500 | - | 1.1x | 1.4x | 0.5x |
823
+ | SVC (rbf), 5,000 x 20 | 4,858 / 7 / 4,882 | 1.2x | 1.4x | 0.5x |
824
+ | SVR (rbf), 5,000 x 20 | 12,821 / 10 / 12,824 | 1.3x | 1.4x | 0.4x |
825
+ | RFE(Ridge), 20 of 200 kept, 20,000 x 200 | - | 216x | 128x | 78x |
826
+
827
+ On one core the package is ahead everywhere. sklearnex uses every
828
+ core by default, and the package's kernels are single-threaded, so
829
+ with 20 cores sklearnex is faster on Ridge, the larger PCA and the
830
+ SVMs; what stays ahead is the device build (logistic regression) and
831
+ an algorithm that does less work (RFE: the Gram once, the Cholesky
832
+ factor downdated per drop, where scikit-learn refits). sklearnex is
833
+ not scikit-learn's arithmetic: its logistic regression takes 12
834
+ iterations to scikit-learn's 6 and its coefficients differ from
835
+ scikit-learn's by 6e-3, where the package's agree to 4e-15; its SVMs
836
+ report a different iteration count. This sklearnex accelerates no
837
+ lasso, so the lasso tables above stand against scikit-learn alone.
838
+
736
839
  ## The GPU build
737
840
 
738
841
  Where the CUDA toolkit is at install time, `ParallelFastICA`,
@@ -1,5 +1,7 @@
1
1
  # scm2cpp-lasso
2
2
 
3
+ [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/niitsuma/scm2cpp/blob/master/notebooks/compare-sklearn.ipynb)
4
+
3
5
  *[Japanese (README.ja.md)](README.ja.md)*
4
6
 
5
7
  Lasso by coordinate descent over a Gram matrix, with an optional GPU
@@ -10,9 +12,20 @@ path. The solver is Scheme, translated to C++ by
10
12
  $ pip install scm2cpp-lasso
11
13
  ```
12
14
 
13
- Installing needs a C++17 compiler and nothing else. If `nvcc` is on
14
- the path, the batched GPU solver is built as well; if it is not, the
15
- package installs and works exactly the same, minus that one method.
15
+ On Linux x86_64, macOS arm64 (macOS 14 or later) and Windows AMD64
16
+ this installs a wheel: compiled, nothing to build (with OpenBLAS
17
+ inside on Linux and macOS; Windows has the loop variants). Elsewhere pip builds the sdist,
18
+ which needs a C++17 compiler and nothing else. The wheels carry no
19
+ GPU code (a wheel cannot know the device); for the GPU builds, install
20
+ from the sdist on a machine with `nvcc` on the path:
21
+
22
+ ```console
23
+ $ pip install --no-binary scm2cpp-lasso scm2cpp-lasso
24
+ ```
25
+
26
+ The batched GPU solver, the cuBLAS variants and the `--cuda`
27
+ estimators are then built as well; without `nvcc` the package installs
28
+ and works exactly the same, minus those.
16
29
 
17
30
  Three of a hundred candidate columns carry the signal; the solver is
18
31
  asked which, and answers with them and nothing else:
@@ -71,6 +84,63 @@ at zero, and `lambda_grid()` walks down from it -- the construction
71
84
  scikit-learn uses. If your columns differ wildly in scale, standardize
72
85
  them first; this solver does not do it for you.
73
86
 
87
+ ## Estimators at a glance
88
+
89
+ Every estimator, the scikit-learn estimator it reproduces, the Scheme
90
+ kernel under `examples/kernel-only/` it is translated from, the builds
91
+ it runs on, and whether Intel's sklearnex accelerates the scikit-learn
92
+ side. Builds: *loops* is the plain translation, *BLAS* / *cuBLAS* the
93
+ products handed to (cu)BLAS, *--cuda* the translator's GPU kernels
94
+ (`gpu_cuda/`), *GPU batch* the hand-written CUDA batch solver; the
95
+ package picks the fastest one present. None of the classes derives
96
+ from scikit-learn's `BaseEstimator` yet, so `clone`, `GridSearchCV`
97
+ and `check_estimator` do not apply to them.
98
+
99
+ | class | scikit-learn | Scheme kernel | builds | sklearnex accelerates |
100
+ |---|---|---|---|---|
101
+ | `CovLasso` | Lasso, ElasticNet, lasso_path(precompute=True) | `lasso-cov.scm` | CPU; GPU batch (fit_path_batch, bootstrap) | no |
102
+ | `CovLassoCV` | LassoCV, ElasticNetCV | `lasso-cov.scm` | CPU; GPU batch | no |
103
+ | `DerivedLasso` | lasso_path, from the design | `lasso-design.scm (--derive)` | loops / BLAS / cuBLAS | no |
104
+ | `CovMultiTaskLasso` | MultiTaskLasso, MultiTaskElasticNet | `lasso-cov.scm` | CPU | no |
105
+ | `CovMultiTaskLassoCV` | MultiTaskLassoCV, MultiTaskElasticNetCV | `lasso-cov.scm` | CPU; GPU batch | no |
106
+ | `CovGroupLasso` | (none; skglm GroupLasso) | `lasso-cov.scm` | CPU | - |
107
+ | `CovLogistic` | LogisticRegression(penalty='l1'), by majorization | `lasso-cov.scm` | CPU | no |
108
+ | `CovRidge` | Ridge(fit_intercept=False), a whole alpha path | `lasso-cov.scm` | CPU | Ridge |
109
+ | `CovBayesianRidge` | BayesianRidge | `lasso-cov.scm` | CPU | no |
110
+ | `CovARDRegression` | ARDRegression (n >= p) | `ard.scm` | loops / BLAS | no |
111
+ | `GramLars` | Lars | `lars.scm` | CPU | no |
112
+ | `GramLassoLars` | LassoLars | `lars.scm` | CPU | no |
113
+ | `GramLassoLarsIC` | LassoLarsIC | `lars.scm` | CPU; device Gram | no |
114
+ | `GramLarsCV` | LarsCV | `lars.scm` | CPU | no |
115
+ | `GramLassoLarsCV` | LassoLarsCV | `lars.scm` | CPU; device Gram | no |
116
+ | `GramOMP` | OrthogonalMatchingPursuit | `omp.scm` | loops / BLAS | no |
117
+ | `GramOMPCV` | OrthogonalMatchingPursuitCV | `omp.scm` | loops / BLAS | no |
118
+ | `LBFGSLogisticRegression` | LogisticRegression(solver='lbfgs') | `logistic-lbfgs.scm, lbfgs.scm` | loops / BLAS / --cuda | LogisticRegression |
119
+ | `LBFGSLogisticRegressionCV` | LogisticRegressionCV | `logistic-lbfgs.scm, lbfgs.scm` | loops / BLAS / --cuda | no |
120
+ | `LBFGSHuberRegressor` | HuberRegressor | `huber.scm, lbfgs.scm` | loops / BLAS / --cuda | no |
121
+ | `LBFGSPoissonRegressor` | PoissonRegressor | `glm.scm, lbfgs.scm` | loops / BLAS / --cuda | no |
122
+ | `LBFGSGammaRegressor` | GammaRegressor | `glm.scm, lbfgs.scm` | loops / BLAS / --cuda | no |
123
+ | `LBFGSTweedieRegressor` | TweedieRegressor | `glm.scm, lbfgs.scm` | loops / BLAS / --cuda | no |
124
+ | `LinearSVM` | LinearSVC(loss='hinge') | `svm-liblinear.scm` | CPU | no |
125
+ | `L1LinearSVM` | LinearSVC(penalty='l1', dual=False) | `svm-l1.scm` | CPU | no |
126
+ | `KernelSVM` | SVC | `svm-smo.scm` | CPU | SVC |
127
+ | `LinearSVR` | LinearSVR | `svr-liblinear.scm` | CPU | no |
128
+ | `KernelSVR` | SVR | `svm-smo.scm` | CPU | SVR |
129
+ | `L1LogisticRegression` | LogisticRegression(penalty='l1', solver='liblinear') | `logistic-l1.scm` | CPU | no |
130
+ | `SGDLinearClassifier` | SGDClassifier(shuffle=False) | `sgd.scm` | CPU | no |
131
+ | `SGDLinearRegressor` | SGDRegressor(shuffle=False) | `sgd.scm` | CPU | no |
132
+ | `SGDPerceptron` | Perceptron(shuffle=False) | `sgd.scm` | CPU | no |
133
+ | `LinearRANSAC` | RANSACRegressor (given subsets) | `ransac.scm` | CPU | no |
134
+ | `WeiszfeldTheilSen` | TheilSenRegressor | `theilsen.scm` | CPU | no |
135
+ | `CDNMF` | NMF(solver='cd') | `nmf-cd.scm (--derive)` | loops / BLAS / cuBLAS | no |
136
+ | `CDGraphicalLasso` | GraphicalLasso | `graphical-lasso.scm` | loops / BLAS | no |
137
+ | `EMGaussianMixture` | GaussianMixture(covariance_type='full') | `gmm.scm` | loops / BLAS / --cuda | no |
138
+ | `ParallelFastICA` | FastICA | `fastica.scm` | loops / BLAS / --cuda | no |
139
+ | `CovPCA` | PCA(svd_solver='covariance_eigh') | `pca.scm` | loops / BLAS / cuBLAS | PCA |
140
+ | `f_regression, f_classif` | f_regression, f_classif | `fscore.scm` | loops / BLAS / cuBLAS | no |
141
+ | `SelectKBest, SelectPercentile` | SelectKBest, SelectPercentile | `fscore.scm` | loops / BLAS / cuBLAS | no |
142
+ | `GramRFE` | RFE(Ridge), RFE(LinearRegression) | `rfe.scm` | loops / BLAS / cuBLAS | inside: Ridge |
143
+
74
144
  ## Two kernels: `CovLasso` and `DerivedLasso`
75
145
 
76
146
  Everything above, and the cross-validation, bootstrap and GPU methods
@@ -715,6 +785,39 @@ estimators (`GramLassoLarsIC`, `GramLarsCV`, `GramLassoLarsCV`,
715
785
  `GramLars`, `GramLassoLars`, `CovPCA`) form a large X'X on the device
716
786
  when one answers. `SCM2CPP_NO_DEVICE=1` keeps everything on the CPU.
717
787
 
788
+ ## Against Intel's sklearnex
789
+
790
+ sklearnex (scikit-learn-intelex 2026.2, oneDAL underneath) replaces
791
+ some scikit-learn estimators with its own. `bench/sklearnex-compare.py`
792
+ times the ones with a counterpart here, sklearnex's estimators
793
+ imported directly (not patched in) so that scikit-learn itself is
794
+ timed in the same process. The ratios are scikit-learn's or
795
+ sklearnex's time over the package's best (its CPU build, or the device
796
+ build where one answers); the load average was 8 to 11, so the times
797
+ move by some tens of percent.
798
+
799
+ | estimator, n x p | iterations (sk / sklearnex / ours) | vs scikit-learn, one core | vs sklearnex, one core | vs sklearnex, 20 cores |
800
+ |---|---|---|---|---|
801
+ | LogisticRegression, 200,000 x 100 | 6 / 12 / 6 | 10.6x (device) | 8.4x (device) | 4.4x (device); on the CPU 0.5x |
802
+ | Ridge (alpha 1), 200,000 x 200 | - | 2.1x | 1.2x | 0.3x |
803
+ | PCA (10 components), 100,000 x 50 | - | 1.5x | 2.4x | 1.7x |
804
+ | PCA (10 components), 50,000 x 500 | - | 1.1x | 1.4x | 0.5x |
805
+ | SVC (rbf), 5,000 x 20 | 4,858 / 7 / 4,882 | 1.2x | 1.4x | 0.5x |
806
+ | SVR (rbf), 5,000 x 20 | 12,821 / 10 / 12,824 | 1.3x | 1.4x | 0.4x |
807
+ | RFE(Ridge), 20 of 200 kept, 20,000 x 200 | - | 216x | 128x | 78x |
808
+
809
+ On one core the package is ahead everywhere. sklearnex uses every
810
+ core by default, and the package's kernels are single-threaded, so
811
+ with 20 cores sklearnex is faster on Ridge, the larger PCA and the
812
+ SVMs; what stays ahead is the device build (logistic regression) and
813
+ an algorithm that does less work (RFE: the Gram once, the Cholesky
814
+ factor downdated per drop, where scikit-learn refits). sklearnex is
815
+ not scikit-learn's arithmetic: its logistic regression takes 12
816
+ iterations to scikit-learn's 6 and its coefficients differ from
817
+ scikit-learn's by 6e-3, where the package's agree to 4e-15; its SVMs
818
+ report a different iteration count. This sklearnex accelerates no
819
+ lasso, so the lasso tables above stand against scikit-learn alone.
820
+
718
821
  ## The GPU build
719
822
 
720
823
  Where the CUDA toolkit is at install time, `ParallelFastICA`,
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "scm2cpp-lasso"
7
- version = "0.13.0"
7
+ version = "0.13.2"
8
8
  description = "scikit-learn estimators (lasso, elastic net, ridge, ARD, SVM, SVR, logistic regression, GLMs, Huber, Theil-Sen, F-score selection, RFE, NMF, Gaussian mixture, FastICA, PCA, graphical lasso, RANSAC, SGD, OMP, LARS) translated from Scheme to C++ by scm2cpp; GPU batch solver and CUDA kernels included"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.8"
@@ -12,6 +12,7 @@ device, or a failed nvcc all leave a working CPU package behind, and
12
12
  the batch path notices at import and falls back.
13
13
  """
14
14
  import os
15
+ import re
15
16
  import shutil
16
17
  import subprocess
17
18
  import sys
@@ -28,7 +29,9 @@ GEN_REL = PKG_REL + "/_generated"
28
29
  PKG = HERE / "src" / "scm2cpp_lasso"
29
30
  GEN = PKG / "_generated"
30
31
 
31
- CXXFLAGS = ["-O2", "-std=c++17"] if os.name != "nt" else ["/O2", "/std:c++17"]
32
+ # MSVC: /EHsc for C++ exceptions, /bigobj for the large generated files
33
+ MSVC = ["/O2", "/std:c++17", "/EHsc", "/bigobj"]
34
+ CXXFLAGS = ["-O2", "-std=c++17"] if os.name != "nt" else MSVC
32
35
 
33
36
  kernel = Extension(
34
37
  "scm2cpp_lasso._lasso_cov",
@@ -92,12 +95,24 @@ else:
92
95
  "scm2cpp_lasso._lasso_derived_loops",
93
96
  sources=[GEN_REL + "/derived_loops/lasso_design_capi.cpp"],
94
97
  include_dirs=[GEN_REL + "/derived_loops", GEN_REL],
95
- extra_compile_args=(["-O3", "-std=c++17"] if os.name != "nt"
96
- else ["/O2", "/std:c++17"]),
98
+ extra_compile_args=(["-O3", "-std=c++17"] if os.name != "nt" else MSVC),
97
99
  )
98
100
 
99
101
 
100
102
  class BuildWithCuda(build_ext):
103
+ def get_export_symbols(self, ext):
104
+ # The extensions are plain shared libraries the package loads
105
+ # through ctypes, not Python modules: no PyInit_ entry point,
106
+ # and on Windows a DLL shows ctypes only what it exports, so the
107
+ # exports are the extern "C" wrappers of the -M C API. (Unix
108
+ # linkers export everything and ignore this list.)
109
+ names = []
110
+ for src in ext.sources:
111
+ if src.endswith("_capi.cpp"):
112
+ text = (HERE / src).read_text()
113
+ names += re.findall(r'extern "C"[^(\n]*?\b(scm2cpp_\w+)\(', text)
114
+ return names
115
+
101
116
  def build_extensions(self):
102
117
  # the kernel is C++ compiled through the usual machinery, so
103
118
  # wheels and cross-compilation behave as they always do
@@ -165,7 +180,7 @@ def cuda_home():
165
180
  # flags carry: with signed overflow defined the compiler cannot treat
166
181
  # the flat index i*p + j as an induction variable, and the linear SVM's
167
182
  # sweep runs 25% slower.
168
- _cls_flags = ["-O3", "-std=c++17", "-fstrict-overflow"] if os.name != "nt" else ["/O2", "/std:c++17"]
183
+ _cls_flags = ["-O3", "-std=c++17", "-fstrict-overflow"] if os.name != "nt" else MSVC
169
184
  classifiers = Extension(
170
185
  "scm2cpp_lasso._classifiers",
171
186
  sources=[GEN_REL + "/classifiers_capi.cpp"],
@@ -242,4 +257,27 @@ if _cuda is not None:
242
257
  extra_compile_args=CXXFLAGS + ["-O3", "-fstrict-overflow"],
243
258
  ))
244
259
 
245
- setup(ext_modules=extensions, cmdclass={"build_ext": BuildWithCuda})
260
+ # A wheel is tagged py3-none-<platform>: the extensions are shared
261
+ # libraries the package loads through ctypes, using no Python API, so
262
+ # one build serves every Python on that platform.
263
+ try:
264
+ from setuptools.command.bdist_wheel import bdist_wheel
265
+ except ImportError: # setuptools before 70.1
266
+ try:
267
+ from wheel.bdist_wheel import bdist_wheel
268
+ except ImportError:
269
+ bdist_wheel = None
270
+
271
+ cmdclass = {"build_ext": BuildWithCuda}
272
+ if bdist_wheel is not None:
273
+ class PlatformWheel(bdist_wheel):
274
+ def finalize_options(self):
275
+ super().finalize_options()
276
+ self.root_is_pure = False
277
+
278
+ def get_tag(self):
279
+ return ("py3", "none", super().get_tag()[2])
280
+
281
+ cmdclass["bdist_wheel"] = PlatformWheel
282
+
283
+ setup(ext_modules=extensions, cmdclass=cmdclass)
@@ -51,7 +51,7 @@ __all__ = ["CovLasso", "CovRidge", "CovBayesianRidge", "CovLogistic", "CovGroupL
51
51
  "SelectPercentile", "GramRFE",
52
52
  "CovMultiTaskLasso", "CovMultiTaskLassoCV", "DerivedLasso",
53
53
  "cuda_available", "derived_backend", "kernel"]
54
- __version__ = "0.13.0"
54
+ __version__ = "0.13.2"
55
55
 
56
56
  _BATCH = load_batch_lib()
57
57
  _DP = ctypes.POINTER(ctypes.c_double)
@@ -3,6 +3,7 @@
3
3
 
4
4
  #include <iostream>
5
5
  #include <vector>
6
+ #include <string>
6
7
  #include <list>
7
8
  #include <map>
8
9
  #include <algorithm>
@@ -1459,6 +1460,36 @@ namespace scm2cpp {
1459
1460
  std::size_t size() const { return n; }
1460
1461
  };
1461
1462
 
1463
+ // A memo for a pure call: (memo-call M E KEY ...) is
1464
+ // memo_get(M, memo_key().add(KEY)...str(), [&]() { return E; })
1465
+ // M a hash table from the key's bytes to E's value (the translator's
1466
+ // (make-hash), an std::unordered_map<std::string, V>), so E runs once
1467
+ // per distinct key. A key is scalars by value and, through
1468
+ // add_range, the first N elements of an array -- the index set a
1469
+ // gather read, say. The bytes are the values' object representation,
1470
+ // so 0.0 and -0.0 are different keys (a miss, never a wrong hit).
1471
+ struct memo_key {
1472
+ std::string b;
1473
+ template<typename T> memo_key& add(const T& v) {
1474
+ b.append(reinterpret_cast<const char*>(&v), sizeof(T));
1475
+ return *this;
1476
+ }
1477
+ template<typename V> memo_key& add_range(const V& v, long n) {
1478
+ add(n);
1479
+ for (long i = 0; i < n; ++i) add(v[i]);
1480
+ return *this;
1481
+ }
1482
+ const std::string& str() const { return b; }
1483
+ };
1484
+ template<typename M, typename F>
1485
+ inline typename M::mapped_type memo_get(M& m, const std::string& k, F f) {
1486
+ auto it = m.find(k);
1487
+ if (it != m.end()) return it->second;
1488
+ typename M::mapped_type v = f();
1489
+ m.emplace(k, v);
1490
+ return v;
1491
+ }
1492
+
1462
1493
  template<typename T, int N> struct integral_image {
1463
1494
  std::vector<T> table; // padded with a zero border: dim[k]+1 per axis
1464
1495
  int dim[N]; // extents of the source array
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: scm2cpp-lasso
3
- Version: 0.13.0
3
+ Version: 0.13.2
4
4
  Summary: scikit-learn estimators (lasso, elastic net, ridge, ARD, SVM, SVR, logistic regression, GLMs, Huber, Theil-Sen, F-score selection, RFE, NMF, Gaussian mixture, FastICA, PCA, graphical lasso, RANSAC, SGD, OMP, LARS) translated from Scheme to C++ by scm2cpp; GPU batch solver and CUDA kernels included
5
5
  Author: Hirotaka Niitsuma
6
6
  License: MIT
@@ -18,6 +18,8 @@ Requires-Dist: numpy>=1.17
18
18
 
19
19
  # scm2cpp-lasso
20
20
 
21
+ [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/niitsuma/scm2cpp/blob/master/notebooks/compare-sklearn.ipynb)
22
+
21
23
  *[Japanese (README.ja.md)](README.ja.md)*
22
24
 
23
25
  Lasso by coordinate descent over a Gram matrix, with an optional GPU
@@ -28,9 +30,20 @@ path. The solver is Scheme, translated to C++ by
28
30
  $ pip install scm2cpp-lasso
29
31
  ```
30
32
 
31
- Installing needs a C++17 compiler and nothing else. If `nvcc` is on
32
- the path, the batched GPU solver is built as well; if it is not, the
33
- package installs and works exactly the same, minus that one method.
33
+ On Linux x86_64, macOS arm64 (macOS 14 or later) and Windows AMD64
34
+ this installs a wheel: compiled, nothing to build (with OpenBLAS
35
+ inside on Linux and macOS; Windows has the loop variants). Elsewhere pip builds the sdist,
36
+ which needs a C++17 compiler and nothing else. The wheels carry no
37
+ GPU code (a wheel cannot know the device); for the GPU builds, install
38
+ from the sdist on a machine with `nvcc` on the path:
39
+
40
+ ```console
41
+ $ pip install --no-binary scm2cpp-lasso scm2cpp-lasso
42
+ ```
43
+
44
+ The batched GPU solver, the cuBLAS variants and the `--cuda`
45
+ estimators are then built as well; without `nvcc` the package installs
46
+ and works exactly the same, minus those.
34
47
 
35
48
  Three of a hundred candidate columns carry the signal; the solver is
36
49
  asked which, and answers with them and nothing else:
@@ -89,6 +102,63 @@ at zero, and `lambda_grid()` walks down from it -- the construction
89
102
  scikit-learn uses. If your columns differ wildly in scale, standardize
90
103
  them first; this solver does not do it for you.
91
104
 
105
+ ## Estimators at a glance
106
+
107
+ Every estimator, the scikit-learn estimator it reproduces, the Scheme
108
+ kernel under `examples/kernel-only/` it is translated from, the builds
109
+ it runs on, and whether Intel's sklearnex accelerates the scikit-learn
110
+ side. Builds: *loops* is the plain translation, *BLAS* / *cuBLAS* the
111
+ products handed to (cu)BLAS, *--cuda* the translator's GPU kernels
112
+ (`gpu_cuda/`), *GPU batch* the hand-written CUDA batch solver; the
113
+ package picks the fastest one present. None of the classes derives
114
+ from scikit-learn's `BaseEstimator` yet, so `clone`, `GridSearchCV`
115
+ and `check_estimator` do not apply to them.
116
+
117
+ | class | scikit-learn | Scheme kernel | builds | sklearnex accelerates |
118
+ |---|---|---|---|---|
119
+ | `CovLasso` | Lasso, ElasticNet, lasso_path(precompute=True) | `lasso-cov.scm` | CPU; GPU batch (fit_path_batch, bootstrap) | no |
120
+ | `CovLassoCV` | LassoCV, ElasticNetCV | `lasso-cov.scm` | CPU; GPU batch | no |
121
+ | `DerivedLasso` | lasso_path, from the design | `lasso-design.scm (--derive)` | loops / BLAS / cuBLAS | no |
122
+ | `CovMultiTaskLasso` | MultiTaskLasso, MultiTaskElasticNet | `lasso-cov.scm` | CPU | no |
123
+ | `CovMultiTaskLassoCV` | MultiTaskLassoCV, MultiTaskElasticNetCV | `lasso-cov.scm` | CPU; GPU batch | no |
124
+ | `CovGroupLasso` | (none; skglm GroupLasso) | `lasso-cov.scm` | CPU | - |
125
+ | `CovLogistic` | LogisticRegression(penalty='l1'), by majorization | `lasso-cov.scm` | CPU | no |
126
+ | `CovRidge` | Ridge(fit_intercept=False), a whole alpha path | `lasso-cov.scm` | CPU | Ridge |
127
+ | `CovBayesianRidge` | BayesianRidge | `lasso-cov.scm` | CPU | no |
128
+ | `CovARDRegression` | ARDRegression (n >= p) | `ard.scm` | loops / BLAS | no |
129
+ | `GramLars` | Lars | `lars.scm` | CPU | no |
130
+ | `GramLassoLars` | LassoLars | `lars.scm` | CPU | no |
131
+ | `GramLassoLarsIC` | LassoLarsIC | `lars.scm` | CPU; device Gram | no |
132
+ | `GramLarsCV` | LarsCV | `lars.scm` | CPU | no |
133
+ | `GramLassoLarsCV` | LassoLarsCV | `lars.scm` | CPU; device Gram | no |
134
+ | `GramOMP` | OrthogonalMatchingPursuit | `omp.scm` | loops / BLAS | no |
135
+ | `GramOMPCV` | OrthogonalMatchingPursuitCV | `omp.scm` | loops / BLAS | no |
136
+ | `LBFGSLogisticRegression` | LogisticRegression(solver='lbfgs') | `logistic-lbfgs.scm, lbfgs.scm` | loops / BLAS / --cuda | LogisticRegression |
137
+ | `LBFGSLogisticRegressionCV` | LogisticRegressionCV | `logistic-lbfgs.scm, lbfgs.scm` | loops / BLAS / --cuda | no |
138
+ | `LBFGSHuberRegressor` | HuberRegressor | `huber.scm, lbfgs.scm` | loops / BLAS / --cuda | no |
139
+ | `LBFGSPoissonRegressor` | PoissonRegressor | `glm.scm, lbfgs.scm` | loops / BLAS / --cuda | no |
140
+ | `LBFGSGammaRegressor` | GammaRegressor | `glm.scm, lbfgs.scm` | loops / BLAS / --cuda | no |
141
+ | `LBFGSTweedieRegressor` | TweedieRegressor | `glm.scm, lbfgs.scm` | loops / BLAS / --cuda | no |
142
+ | `LinearSVM` | LinearSVC(loss='hinge') | `svm-liblinear.scm` | CPU | no |
143
+ | `L1LinearSVM` | LinearSVC(penalty='l1', dual=False) | `svm-l1.scm` | CPU | no |
144
+ | `KernelSVM` | SVC | `svm-smo.scm` | CPU | SVC |
145
+ | `LinearSVR` | LinearSVR | `svr-liblinear.scm` | CPU | no |
146
+ | `KernelSVR` | SVR | `svm-smo.scm` | CPU | SVR |
147
+ | `L1LogisticRegression` | LogisticRegression(penalty='l1', solver='liblinear') | `logistic-l1.scm` | CPU | no |
148
+ | `SGDLinearClassifier` | SGDClassifier(shuffle=False) | `sgd.scm` | CPU | no |
149
+ | `SGDLinearRegressor` | SGDRegressor(shuffle=False) | `sgd.scm` | CPU | no |
150
+ | `SGDPerceptron` | Perceptron(shuffle=False) | `sgd.scm` | CPU | no |
151
+ | `LinearRANSAC` | RANSACRegressor (given subsets) | `ransac.scm` | CPU | no |
152
+ | `WeiszfeldTheilSen` | TheilSenRegressor | `theilsen.scm` | CPU | no |
153
+ | `CDNMF` | NMF(solver='cd') | `nmf-cd.scm (--derive)` | loops / BLAS / cuBLAS | no |
154
+ | `CDGraphicalLasso` | GraphicalLasso | `graphical-lasso.scm` | loops / BLAS | no |
155
+ | `EMGaussianMixture` | GaussianMixture(covariance_type='full') | `gmm.scm` | loops / BLAS / --cuda | no |
156
+ | `ParallelFastICA` | FastICA | `fastica.scm` | loops / BLAS / --cuda | no |
157
+ | `CovPCA` | PCA(svd_solver='covariance_eigh') | `pca.scm` | loops / BLAS / cuBLAS | PCA |
158
+ | `f_regression, f_classif` | f_regression, f_classif | `fscore.scm` | loops / BLAS / cuBLAS | no |
159
+ | `SelectKBest, SelectPercentile` | SelectKBest, SelectPercentile | `fscore.scm` | loops / BLAS / cuBLAS | no |
160
+ | `GramRFE` | RFE(Ridge), RFE(LinearRegression) | `rfe.scm` | loops / BLAS / cuBLAS | inside: Ridge |
161
+
92
162
  ## Two kernels: `CovLasso` and `DerivedLasso`
93
163
 
94
164
  Everything above, and the cross-validation, bootstrap and GPU methods
@@ -733,6 +803,39 @@ estimators (`GramLassoLarsIC`, `GramLarsCV`, `GramLassoLarsCV`,
733
803
  `GramLars`, `GramLassoLars`, `CovPCA`) form a large X'X on the device
734
804
  when one answers. `SCM2CPP_NO_DEVICE=1` keeps everything on the CPU.
735
805
 
806
+ ## Against Intel's sklearnex
807
+
808
+ sklearnex (scikit-learn-intelex 2026.2, oneDAL underneath) replaces
809
+ some scikit-learn estimators with its own. `bench/sklearnex-compare.py`
810
+ times the ones with a counterpart here, sklearnex's estimators
811
+ imported directly (not patched in) so that scikit-learn itself is
812
+ timed in the same process. The ratios are scikit-learn's or
813
+ sklearnex's time over the package's best (its CPU build, or the device
814
+ build where one answers); the load average was 8 to 11, so the times
815
+ move by some tens of percent.
816
+
817
+ | estimator, n x p | iterations (sk / sklearnex / ours) | vs scikit-learn, one core | vs sklearnex, one core | vs sklearnex, 20 cores |
818
+ |---|---|---|---|---|
819
+ | LogisticRegression, 200,000 x 100 | 6 / 12 / 6 | 10.6x (device) | 8.4x (device) | 4.4x (device); on the CPU 0.5x |
820
+ | Ridge (alpha 1), 200,000 x 200 | - | 2.1x | 1.2x | 0.3x |
821
+ | PCA (10 components), 100,000 x 50 | - | 1.5x | 2.4x | 1.7x |
822
+ | PCA (10 components), 50,000 x 500 | - | 1.1x | 1.4x | 0.5x |
823
+ | SVC (rbf), 5,000 x 20 | 4,858 / 7 / 4,882 | 1.2x | 1.4x | 0.5x |
824
+ | SVR (rbf), 5,000 x 20 | 12,821 / 10 / 12,824 | 1.3x | 1.4x | 0.4x |
825
+ | RFE(Ridge), 20 of 200 kept, 20,000 x 200 | - | 216x | 128x | 78x |
826
+
827
+ On one core the package is ahead everywhere. sklearnex uses every
828
+ core by default, and the package's kernels are single-threaded, so
829
+ with 20 cores sklearnex is faster on Ridge, the larger PCA and the
830
+ SVMs; what stays ahead is the device build (logistic regression) and
831
+ an algorithm that does less work (RFE: the Gram once, the Cholesky
832
+ factor downdated per drop, where scikit-learn refits). sklearnex is
833
+ not scikit-learn's arithmetic: its logistic regression takes 12
834
+ iterations to scikit-learn's 6 and its coefficients differ from
835
+ scikit-learn's by 6e-3, where the package's agree to 4e-15; its SVMs
836
+ report a different iteration count. This sklearnex accelerates no
837
+ lasso, so the lasso tables above stand against scikit-learn alone.
838
+
736
839
  ## The GPU build
737
840
 
738
841
  Where the CUDA toolkit is at install time, `ParallelFastICA`,
File without changes