scm2cpp-lasso 0.12.0__tar.gz → 0.13.1__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.12.0 → scm2cpp_lasso-0.13.1}/PKG-INFO +119 -5
  2. {scm2cpp_lasso-0.12.0 → scm2cpp_lasso-0.13.1}/README.md +117 -3
  3. {scm2cpp_lasso-0.12.0 → scm2cpp_lasso-0.13.1}/pyproject.toml +3 -3
  4. {scm2cpp_lasso-0.12.0 → scm2cpp_lasso-0.13.1}/setup.py +46 -1
  5. {scm2cpp_lasso-0.12.0 → scm2cpp_lasso-0.13.1}/src/scm2cpp_lasso/__init__.py +20 -7
  6. scm2cpp_lasso-0.13.1/src/scm2cpp_lasso/_generated/_loader_gpu_cuda.py +294 -0
  7. {scm2cpp_lasso-0.12.0 → scm2cpp_lasso-0.13.1}/src/scm2cpp_lasso/_generated/_loader_gramops_blas.py +3 -6
  8. {scm2cpp_lasso-0.12.0 → scm2cpp_lasso-0.13.1}/src/scm2cpp_lasso/_generated/_loader_gramops_loops.py +3 -6
  9. scm2cpp_lasso-0.13.1/src/scm2cpp_lasso/_generated/gpu_cuda/gpu.cpp +35 -0
  10. scm2cpp_lasso-0.13.1/src/scm2cpp_lasso/_generated/gpu_cuda/gpu.hpp +3715 -0
  11. scm2cpp_lasso-0.13.1/src/scm2cpp_lasso/_generated/gpu_cuda/gpu_capi.cpp +289 -0
  12. {scm2cpp_lasso-0.12.0 → scm2cpp_lasso-0.13.1}/src/scm2cpp_lasso/_generated/gramops_blas/gramops.hpp +36 -22
  13. {scm2cpp_lasso-0.12.0/src/scm2cpp_lasso/_generated/gramops_loops → scm2cpp_lasso-0.13.1/src/scm2cpp_lasso/_generated/gramops_blas}/gramops_capi.cpp +2 -2
  14. {scm2cpp_lasso-0.12.0 → scm2cpp_lasso-0.13.1}/src/scm2cpp_lasso/_generated/gramops_loops/gramops.hpp +170 -146
  15. {scm2cpp_lasso-0.12.0/src/scm2cpp_lasso/_generated/gramops_blas → scm2cpp_lasso-0.13.1/src/scm2cpp_lasso/_generated/gramops_loops}/gramops_capi.cpp +2 -2
  16. scm2cpp_lasso-0.13.1/src/scm2cpp_lasso/_generated/scm2cpp-cuda.hpp +285 -0
  17. {scm2cpp_lasso-0.12.0 → scm2cpp_lasso-0.13.1}/src/scm2cpp_lasso/_generated/scm2cpp.hpp +49 -7
  18. {scm2cpp_lasso-0.12.0 → scm2cpp_lasso-0.13.1}/src/scm2cpp_lasso/_libfind.py +12 -0
  19. {scm2cpp_lasso-0.12.0 → scm2cpp_lasso-0.13.1}/src/scm2cpp_lasso.egg-info/PKG-INFO +119 -5
  20. {scm2cpp_lasso-0.12.0 → scm2cpp_lasso-0.13.1}/src/scm2cpp_lasso.egg-info/SOURCES.txt +5 -0
  21. {scm2cpp_lasso-0.12.0 → scm2cpp_lasso-0.13.1}/setup.cfg +0 -0
  22. {scm2cpp_lasso-0.12.0 → scm2cpp_lasso-0.13.1}/src/scm2cpp_lasso/_generated/__init__.py +0 -0
  23. {scm2cpp_lasso-0.12.0 → scm2cpp_lasso-0.13.1}/src/scm2cpp_lasso/_generated/_loader.py +0 -0
  24. {scm2cpp_lasso-0.12.0 → scm2cpp_lasso-0.13.1}/src/scm2cpp_lasso/_generated/_loader_accel_cublas.py +0 -0
  25. {scm2cpp_lasso-0.12.0 → scm2cpp_lasso-0.13.1}/src/scm2cpp_lasso/_generated/_loader_classifiers.py +0 -0
  26. {scm2cpp_lasso-0.12.0 → scm2cpp_lasso-0.13.1}/src/scm2cpp_lasso/_generated/_loader_derived_blas.py +0 -0
  27. {scm2cpp_lasso-0.12.0 → scm2cpp_lasso-0.13.1}/src/scm2cpp_lasso/_generated/_loader_derived_cublas.py +0 -0
  28. {scm2cpp_lasso-0.12.0 → scm2cpp_lasso-0.13.1}/src/scm2cpp_lasso/_generated/_loader_derived_loops.py +0 -0
  29. {scm2cpp_lasso-0.12.0 → scm2cpp_lasso-0.13.1}/src/scm2cpp_lasso/_generated/_loader_nmf_blas.py +0 -0
  30. {scm2cpp_lasso-0.12.0 → scm2cpp_lasso-0.13.1}/src/scm2cpp_lasso/_generated/_loader_nmf_cublas.py +0 -0
  31. {scm2cpp_lasso-0.12.0 → scm2cpp_lasso-0.13.1}/src/scm2cpp_lasso/_generated/_loader_nmf_loops.py +0 -0
  32. {scm2cpp_lasso-0.12.0 → scm2cpp_lasso-0.13.1}/src/scm2cpp_lasso/_generated/accel_cublas/accel.cpp +0 -0
  33. {scm2cpp_lasso-0.12.0 → scm2cpp_lasso-0.13.1}/src/scm2cpp_lasso/_generated/accel_cublas/accel.hpp +0 -0
  34. {scm2cpp_lasso-0.12.0 → scm2cpp_lasso-0.13.1}/src/scm2cpp_lasso/_generated/accel_cublas/accel_capi.cpp +0 -0
  35. {scm2cpp_lasso-0.12.0 → scm2cpp_lasso-0.13.1}/src/scm2cpp_lasso/_generated/classifiers.cpp +0 -0
  36. {scm2cpp_lasso-0.12.0 → scm2cpp_lasso-0.13.1}/src/scm2cpp_lasso/_generated/classifiers.hpp +0 -0
  37. {scm2cpp_lasso-0.12.0 → scm2cpp_lasso-0.13.1}/src/scm2cpp_lasso/_generated/classifiers_capi.cpp +0 -0
  38. {scm2cpp_lasso-0.12.0 → scm2cpp_lasso-0.13.1}/src/scm2cpp_lasso/_generated/derived_blas/lasso_design.cpp +0 -0
  39. {scm2cpp_lasso-0.12.0 → scm2cpp_lasso-0.13.1}/src/scm2cpp_lasso/_generated/derived_blas/lasso_design.hpp +0 -0
  40. {scm2cpp_lasso-0.12.0 → scm2cpp_lasso-0.13.1}/src/scm2cpp_lasso/_generated/derived_blas/lasso_design_capi.cpp +0 -0
  41. {scm2cpp_lasso-0.12.0 → scm2cpp_lasso-0.13.1}/src/scm2cpp_lasso/_generated/derived_cublas/lasso_design.cpp +0 -0
  42. {scm2cpp_lasso-0.12.0 → scm2cpp_lasso-0.13.1}/src/scm2cpp_lasso/_generated/derived_cublas/lasso_design.hpp +0 -0
  43. {scm2cpp_lasso-0.12.0 → scm2cpp_lasso-0.13.1}/src/scm2cpp_lasso/_generated/derived_cublas/lasso_design_capi.cpp +0 -0
  44. {scm2cpp_lasso-0.12.0 → scm2cpp_lasso-0.13.1}/src/scm2cpp_lasso/_generated/derived_loops/lasso_design.cpp +0 -0
  45. {scm2cpp_lasso-0.12.0 → scm2cpp_lasso-0.13.1}/src/scm2cpp_lasso/_generated/derived_loops/lasso_design.hpp +0 -0
  46. {scm2cpp_lasso-0.12.0 → scm2cpp_lasso-0.13.1}/src/scm2cpp_lasso/_generated/derived_loops/lasso_design_capi.cpp +0 -0
  47. {scm2cpp_lasso-0.12.0 → scm2cpp_lasso-0.13.1}/src/scm2cpp_lasso/_generated/gramops_blas/gramops.cpp +0 -0
  48. {scm2cpp_lasso-0.12.0 → scm2cpp_lasso-0.13.1}/src/scm2cpp_lasso/_generated/gramops_loops/gramops.cpp +0 -0
  49. {scm2cpp_lasso-0.12.0 → scm2cpp_lasso-0.13.1}/src/scm2cpp_lasso/_generated/lasso_cov.cpp +0 -0
  50. {scm2cpp_lasso-0.12.0 → scm2cpp_lasso-0.13.1}/src/scm2cpp_lasso/_generated/lasso_cov.hpp +0 -0
  51. {scm2cpp_lasso-0.12.0 → scm2cpp_lasso-0.13.1}/src/scm2cpp_lasso/_generated/lasso_cov_capi.cpp +0 -0
  52. {scm2cpp_lasso-0.12.0 → scm2cpp_lasso-0.13.1}/src/scm2cpp_lasso/_generated/nmf_blas/nmf_cd.cpp +0 -0
  53. {scm2cpp_lasso-0.12.0 → scm2cpp_lasso-0.13.1}/src/scm2cpp_lasso/_generated/nmf_blas/nmf_cd.hpp +0 -0
  54. {scm2cpp_lasso-0.12.0 → scm2cpp_lasso-0.13.1}/src/scm2cpp_lasso/_generated/nmf_blas/nmf_cd_capi.cpp +0 -0
  55. {scm2cpp_lasso-0.12.0 → scm2cpp_lasso-0.13.1}/src/scm2cpp_lasso/_generated/nmf_cublas/nmf_cd.cpp +0 -0
  56. {scm2cpp_lasso-0.12.0 → scm2cpp_lasso-0.13.1}/src/scm2cpp_lasso/_generated/nmf_cublas/nmf_cd.hpp +0 -0
  57. {scm2cpp_lasso-0.12.0 → scm2cpp_lasso-0.13.1}/src/scm2cpp_lasso/_generated/nmf_cublas/nmf_cd_capi.cpp +0 -0
  58. {scm2cpp_lasso-0.12.0 → scm2cpp_lasso-0.13.1}/src/scm2cpp_lasso/_generated/nmf_loops/nmf_cd.cpp +0 -0
  59. {scm2cpp_lasso-0.12.0 → scm2cpp_lasso-0.13.1}/src/scm2cpp_lasso/_generated/nmf_loops/nmf_cd.hpp +0 -0
  60. {scm2cpp_lasso-0.12.0 → scm2cpp_lasso-0.13.1}/src/scm2cpp_lasso/_generated/nmf_loops/nmf_cd_capi.cpp +0 -0
  61. {scm2cpp_lasso-0.12.0 → scm2cpp_lasso-0.13.1}/src/scm2cpp_lasso/_generated/scm2cpp-blas.hpp +0 -0
  62. {scm2cpp_lasso-0.12.0 → scm2cpp_lasso-0.13.1}/src/scm2cpp_lasso/_generated/scm2cpp-cublas.hpp +0 -0
  63. {scm2cpp_lasso-0.12.0 → scm2cpp_lasso-0.13.1}/src/scm2cpp_lasso/batch_capi.cu +0 -0
  64. {scm2cpp_lasso-0.12.0 → scm2cpp_lasso-0.13.1}/src/scm2cpp_lasso.egg-info/dependency_links.txt +0 -0
  65. {scm2cpp_lasso-0.12.0 → scm2cpp_lasso-0.13.1}/src/scm2cpp_lasso.egg-info/requires.txt +0 -0
  66. {scm2cpp_lasso-0.12.0 → scm2cpp_lasso-0.13.1}/src/scm2cpp_lasso.egg-info/top_level.txt +0 -0
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: scm2cpp-lasso
3
- Version: 0.12.0
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 included
3
+ Version: 0.13.1
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
7
7
  Project-URL: Homepage, https://github.com/niitsuma/scm2cpp
@@ -28,9 +28,19 @@ path. The solver is Scheme, translated to C++ by
28
28
  $ pip install scm2cpp-lasso
29
29
  ```
30
30
 
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.
31
+ On Linux x86_64 and macOS arm64 (macOS 14 or later) this installs a wheel: compiled, with
32
+ OpenBLAS inside, nothing to build. Elsewhere pip builds the sdist,
33
+ which needs a C++17 compiler and nothing else. The wheels carry no
34
+ GPU code (a wheel cannot know the device); for the GPU builds, install
35
+ from the sdist on a machine with `nvcc` on the path:
36
+
37
+ ```console
38
+ $ pip install --no-binary scm2cpp-lasso scm2cpp-lasso
39
+ ```
40
+
41
+ The batched GPU solver, the cuBLAS variants and the `--cuda`
42
+ estimators are then built as well; without `nvcc` the package installs
43
+ and works exactly the same, minus those.
34
44
 
35
45
  Three of a hundred candidate columns carry the signal; the solver is
36
46
  asked which, and answers with them and nothing else:
@@ -89,6 +99,63 @@ at zero, and `lambda_grid()` walks down from it -- the construction
89
99
  scikit-learn uses. If your columns differ wildly in scale, standardize
90
100
  them first; this solver does not do it for you.
91
101
 
102
+ ## Estimators at a glance
103
+
104
+ Every estimator, the scikit-learn estimator it reproduces, the Scheme
105
+ kernel under `examples/kernel-only/` it is translated from, the builds
106
+ it runs on, and whether Intel's sklearnex accelerates the scikit-learn
107
+ side. Builds: *loops* is the plain translation, *BLAS* / *cuBLAS* the
108
+ products handed to (cu)BLAS, *--cuda* the translator's GPU kernels
109
+ (`gpu_cuda/`), *GPU batch* the hand-written CUDA batch solver; the
110
+ package picks the fastest one present. None of the classes derives
111
+ from scikit-learn's `BaseEstimator` yet, so `clone`, `GridSearchCV`
112
+ and `check_estimator` do not apply to them.
113
+
114
+ | class | scikit-learn | Scheme kernel | builds | sklearnex accelerates |
115
+ |---|---|---|---|---|
116
+ | `CovLasso` | Lasso, ElasticNet, lasso_path(precompute=True) | `lasso-cov.scm` | CPU; GPU batch (fit_path_batch, bootstrap) | no |
117
+ | `CovLassoCV` | LassoCV, ElasticNetCV | `lasso-cov.scm` | CPU; GPU batch | no |
118
+ | `DerivedLasso` | lasso_path, from the design | `lasso-design.scm (--derive)` | loops / BLAS / cuBLAS | no |
119
+ | `CovMultiTaskLasso` | MultiTaskLasso, MultiTaskElasticNet | `lasso-cov.scm` | CPU | no |
120
+ | `CovMultiTaskLassoCV` | MultiTaskLassoCV, MultiTaskElasticNetCV | `lasso-cov.scm` | CPU; GPU batch | no |
121
+ | `CovGroupLasso` | (none; skglm GroupLasso) | `lasso-cov.scm` | CPU | - |
122
+ | `CovLogistic` | LogisticRegression(penalty='l1'), by majorization | `lasso-cov.scm` | CPU | no |
123
+ | `CovRidge` | Ridge(fit_intercept=False), a whole alpha path | `lasso-cov.scm` | CPU | Ridge |
124
+ | `CovBayesianRidge` | BayesianRidge | `lasso-cov.scm` | CPU | no |
125
+ | `CovARDRegression` | ARDRegression (n >= p) | `ard.scm` | loops / BLAS | no |
126
+ | `GramLars` | Lars | `lars.scm` | CPU | no |
127
+ | `GramLassoLars` | LassoLars | `lars.scm` | CPU | no |
128
+ | `GramLassoLarsIC` | LassoLarsIC | `lars.scm` | CPU; device Gram | no |
129
+ | `GramLarsCV` | LarsCV | `lars.scm` | CPU | no |
130
+ | `GramLassoLarsCV` | LassoLarsCV | `lars.scm` | CPU; device Gram | no |
131
+ | `GramOMP` | OrthogonalMatchingPursuit | `omp.scm` | loops / BLAS | no |
132
+ | `GramOMPCV` | OrthogonalMatchingPursuitCV | `omp.scm` | loops / BLAS | no |
133
+ | `LBFGSLogisticRegression` | LogisticRegression(solver='lbfgs') | `logistic-lbfgs.scm, lbfgs.scm` | loops / BLAS / --cuda | LogisticRegression |
134
+ | `LBFGSLogisticRegressionCV` | LogisticRegressionCV | `logistic-lbfgs.scm, lbfgs.scm` | loops / BLAS / --cuda | no |
135
+ | `LBFGSHuberRegressor` | HuberRegressor | `huber.scm, lbfgs.scm` | loops / BLAS / --cuda | no |
136
+ | `LBFGSPoissonRegressor` | PoissonRegressor | `glm.scm, lbfgs.scm` | loops / BLAS / --cuda | no |
137
+ | `LBFGSGammaRegressor` | GammaRegressor | `glm.scm, lbfgs.scm` | loops / BLAS / --cuda | no |
138
+ | `LBFGSTweedieRegressor` | TweedieRegressor | `glm.scm, lbfgs.scm` | loops / BLAS / --cuda | no |
139
+ | `LinearSVM` | LinearSVC(loss='hinge') | `svm-liblinear.scm` | CPU | no |
140
+ | `L1LinearSVM` | LinearSVC(penalty='l1', dual=False) | `svm-l1.scm` | CPU | no |
141
+ | `KernelSVM` | SVC | `svm-smo.scm` | CPU | SVC |
142
+ | `LinearSVR` | LinearSVR | `svr-liblinear.scm` | CPU | no |
143
+ | `KernelSVR` | SVR | `svm-smo.scm` | CPU | SVR |
144
+ | `L1LogisticRegression` | LogisticRegression(penalty='l1', solver='liblinear') | `logistic-l1.scm` | CPU | no |
145
+ | `SGDLinearClassifier` | SGDClassifier(shuffle=False) | `sgd.scm` | CPU | no |
146
+ | `SGDLinearRegressor` | SGDRegressor(shuffle=False) | `sgd.scm` | CPU | no |
147
+ | `SGDPerceptron` | Perceptron(shuffle=False) | `sgd.scm` | CPU | no |
148
+ | `LinearRANSAC` | RANSACRegressor (given subsets) | `ransac.scm` | CPU | no |
149
+ | `WeiszfeldTheilSen` | TheilSenRegressor | `theilsen.scm` | CPU | no |
150
+ | `CDNMF` | NMF(solver='cd') | `nmf-cd.scm (--derive)` | loops / BLAS / cuBLAS | no |
151
+ | `CDGraphicalLasso` | GraphicalLasso | `graphical-lasso.scm` | loops / BLAS | no |
152
+ | `EMGaussianMixture` | GaussianMixture(covariance_type='full') | `gmm.scm` | loops / BLAS / --cuda | no |
153
+ | `ParallelFastICA` | FastICA | `fastica.scm` | loops / BLAS / --cuda | no |
154
+ | `CovPCA` | PCA(svd_solver='covariance_eigh') | `pca.scm` | loops / BLAS / cuBLAS | PCA |
155
+ | `f_regression, f_classif` | f_regression, f_classif | `fscore.scm` | loops / BLAS / cuBLAS | no |
156
+ | `SelectKBest, SelectPercentile` | SelectKBest, SelectPercentile | `fscore.scm` | loops / BLAS / cuBLAS | no |
157
+ | `GramRFE` | RFE(Ridge), RFE(LinearRegression) | `rfe.scm` | loops / BLAS / cuBLAS | inside: Ridge |
158
+
92
159
  ## Two kernels: `CovLasso` and `DerivedLasso`
93
160
 
94
161
  Everything above, and the cross-validation, bootstrap and GPU methods
@@ -733,6 +800,53 @@ estimators (`GramLassoLarsIC`, `GramLarsCV`, `GramLassoLarsCV`,
733
800
  `GramLars`, `GramLassoLars`, `CovPCA`) form a large X'X on the device
734
801
  when one answers. `SCM2CPP_NO_DEVICE=1` keeps everything on the CPU.
735
802
 
803
+ ## Against Intel's sklearnex
804
+
805
+ sklearnex (scikit-learn-intelex 2026.2, oneDAL underneath) replaces
806
+ some scikit-learn estimators with its own. `bench/sklearnex-compare.py`
807
+ times the ones with a counterpart here, sklearnex's estimators
808
+ imported directly (not patched in) so that scikit-learn itself is
809
+ timed in the same process. The ratios are scikit-learn's or
810
+ sklearnex's time over the package's best (its CPU build, or the device
811
+ build where one answers); the load average was 8 to 11, so the times
812
+ move by some tens of percent.
813
+
814
+ | estimator, n x p | iterations (sk / sklearnex / ours) | vs scikit-learn, one core | vs sklearnex, one core | vs sklearnex, 20 cores |
815
+ |---|---|---|---|---|
816
+ | LogisticRegression, 200,000 x 100 | 6 / 12 / 6 | 10.6x (device) | 8.4x (device) | 4.4x (device); on the CPU 0.5x |
817
+ | Ridge (alpha 1), 200,000 x 200 | - | 2.1x | 1.2x | 0.3x |
818
+ | PCA (10 components), 100,000 x 50 | - | 1.5x | 2.4x | 1.7x |
819
+ | PCA (10 components), 50,000 x 500 | - | 1.1x | 1.4x | 0.5x |
820
+ | SVC (rbf), 5,000 x 20 | 4,858 / 7 / 4,882 | 1.2x | 1.4x | 0.5x |
821
+ | SVR (rbf), 5,000 x 20 | 12,821 / 10 / 12,824 | 1.3x | 1.4x | 0.4x |
822
+ | RFE(Ridge), 20 of 200 kept, 20,000 x 200 | - | 216x | 128x | 78x |
823
+
824
+ On one core the package is ahead everywhere. sklearnex uses every
825
+ core by default, and the package's kernels are single-threaded, so
826
+ with 20 cores sklearnex is faster on Ridge, the larger PCA and the
827
+ SVMs; what stays ahead is the device build (logistic regression) and
828
+ an algorithm that does less work (RFE: the Gram once, the Cholesky
829
+ factor downdated per drop, where scikit-learn refits). sklearnex is
830
+ not scikit-learn's arithmetic: its logistic regression takes 12
831
+ iterations to scikit-learn's 6 and its coefficients differ from
832
+ scikit-learn's by 6e-3, where the package's agree to 4e-15; its SVMs
833
+ report a different iteration count. This sklearnex accelerates no
834
+ lasso, so the lasso tables above stand against scikit-learn alone.
835
+
836
+ ## The GPU build
837
+
838
+ Where the CUDA toolkit is at install time, `ParallelFastICA`,
839
+ `EMGaussianMixture`, `LBFGSLogisticRegression`,
840
+ `LBFGSLogisticRegressionCV`, `LBFGSHuberRegressor` and the `LBFGS*`
841
+ GLMs also get the same Scheme translated with `--cuda`
842
+ (`libscm2cpp_gpu.so`). Its loops are CUDA kernels and its products
843
+ cuBLAS, on arrays that stay on the device. The classes take it when a
844
+ device answers and the data has a million entries or more, and give
845
+ the CPU build's iterations and results to the last digits. Measured
846
+ against scikit-learn: 10-22x for the L-BFGS fits, 6.7x FastICA, 3x the
847
+ mixture and LogisticRegressionCV (the repository README's table).
848
+ `SCM2CPP_NO_DEVICE=1` keeps everything on the CPU.
849
+
736
850
  ## Logistic regression with L1
737
851
 
738
852
  `CovLogistic` solves L1-penalized logistic regression by
@@ -10,9 +10,19 @@ path. The solver is Scheme, translated to C++ by
10
10
  $ pip install scm2cpp-lasso
11
11
  ```
12
12
 
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.
13
+ On Linux x86_64 and macOS arm64 (macOS 14 or later) this installs a wheel: compiled, with
14
+ OpenBLAS inside, nothing to build. Elsewhere pip builds the sdist,
15
+ which needs a C++17 compiler and nothing else. The wheels carry no
16
+ GPU code (a wheel cannot know the device); for the GPU builds, install
17
+ from the sdist on a machine with `nvcc` on the path:
18
+
19
+ ```console
20
+ $ pip install --no-binary scm2cpp-lasso scm2cpp-lasso
21
+ ```
22
+
23
+ The batched GPU solver, the cuBLAS variants and the `--cuda`
24
+ estimators are then built as well; without `nvcc` the package installs
25
+ and works exactly the same, minus those.
16
26
 
17
27
  Three of a hundred candidate columns carry the signal; the solver is
18
28
  asked which, and answers with them and nothing else:
@@ -71,6 +81,63 @@ at zero, and `lambda_grid()` walks down from it -- the construction
71
81
  scikit-learn uses. If your columns differ wildly in scale, standardize
72
82
  them first; this solver does not do it for you.
73
83
 
84
+ ## Estimators at a glance
85
+
86
+ Every estimator, the scikit-learn estimator it reproduces, the Scheme
87
+ kernel under `examples/kernel-only/` it is translated from, the builds
88
+ it runs on, and whether Intel's sklearnex accelerates the scikit-learn
89
+ side. Builds: *loops* is the plain translation, *BLAS* / *cuBLAS* the
90
+ products handed to (cu)BLAS, *--cuda* the translator's GPU kernels
91
+ (`gpu_cuda/`), *GPU batch* the hand-written CUDA batch solver; the
92
+ package picks the fastest one present. None of the classes derives
93
+ from scikit-learn's `BaseEstimator` yet, so `clone`, `GridSearchCV`
94
+ and `check_estimator` do not apply to them.
95
+
96
+ | class | scikit-learn | Scheme kernel | builds | sklearnex accelerates |
97
+ |---|---|---|---|---|
98
+ | `CovLasso` | Lasso, ElasticNet, lasso_path(precompute=True) | `lasso-cov.scm` | CPU; GPU batch (fit_path_batch, bootstrap) | no |
99
+ | `CovLassoCV` | LassoCV, ElasticNetCV | `lasso-cov.scm` | CPU; GPU batch | no |
100
+ | `DerivedLasso` | lasso_path, from the design | `lasso-design.scm (--derive)` | loops / BLAS / cuBLAS | no |
101
+ | `CovMultiTaskLasso` | MultiTaskLasso, MultiTaskElasticNet | `lasso-cov.scm` | CPU | no |
102
+ | `CovMultiTaskLassoCV` | MultiTaskLassoCV, MultiTaskElasticNetCV | `lasso-cov.scm` | CPU; GPU batch | no |
103
+ | `CovGroupLasso` | (none; skglm GroupLasso) | `lasso-cov.scm` | CPU | - |
104
+ | `CovLogistic` | LogisticRegression(penalty='l1'), by majorization | `lasso-cov.scm` | CPU | no |
105
+ | `CovRidge` | Ridge(fit_intercept=False), a whole alpha path | `lasso-cov.scm` | CPU | Ridge |
106
+ | `CovBayesianRidge` | BayesianRidge | `lasso-cov.scm` | CPU | no |
107
+ | `CovARDRegression` | ARDRegression (n >= p) | `ard.scm` | loops / BLAS | no |
108
+ | `GramLars` | Lars | `lars.scm` | CPU | no |
109
+ | `GramLassoLars` | LassoLars | `lars.scm` | CPU | no |
110
+ | `GramLassoLarsIC` | LassoLarsIC | `lars.scm` | CPU; device Gram | no |
111
+ | `GramLarsCV` | LarsCV | `lars.scm` | CPU | no |
112
+ | `GramLassoLarsCV` | LassoLarsCV | `lars.scm` | CPU; device Gram | no |
113
+ | `GramOMP` | OrthogonalMatchingPursuit | `omp.scm` | loops / BLAS | no |
114
+ | `GramOMPCV` | OrthogonalMatchingPursuitCV | `omp.scm` | loops / BLAS | no |
115
+ | `LBFGSLogisticRegression` | LogisticRegression(solver='lbfgs') | `logistic-lbfgs.scm, lbfgs.scm` | loops / BLAS / --cuda | LogisticRegression |
116
+ | `LBFGSLogisticRegressionCV` | LogisticRegressionCV | `logistic-lbfgs.scm, lbfgs.scm` | loops / BLAS / --cuda | no |
117
+ | `LBFGSHuberRegressor` | HuberRegressor | `huber.scm, lbfgs.scm` | loops / BLAS / --cuda | no |
118
+ | `LBFGSPoissonRegressor` | PoissonRegressor | `glm.scm, lbfgs.scm` | loops / BLAS / --cuda | no |
119
+ | `LBFGSGammaRegressor` | GammaRegressor | `glm.scm, lbfgs.scm` | loops / BLAS / --cuda | no |
120
+ | `LBFGSTweedieRegressor` | TweedieRegressor | `glm.scm, lbfgs.scm` | loops / BLAS / --cuda | no |
121
+ | `LinearSVM` | LinearSVC(loss='hinge') | `svm-liblinear.scm` | CPU | no |
122
+ | `L1LinearSVM` | LinearSVC(penalty='l1', dual=False) | `svm-l1.scm` | CPU | no |
123
+ | `KernelSVM` | SVC | `svm-smo.scm` | CPU | SVC |
124
+ | `LinearSVR` | LinearSVR | `svr-liblinear.scm` | CPU | no |
125
+ | `KernelSVR` | SVR | `svm-smo.scm` | CPU | SVR |
126
+ | `L1LogisticRegression` | LogisticRegression(penalty='l1', solver='liblinear') | `logistic-l1.scm` | CPU | no |
127
+ | `SGDLinearClassifier` | SGDClassifier(shuffle=False) | `sgd.scm` | CPU | no |
128
+ | `SGDLinearRegressor` | SGDRegressor(shuffle=False) | `sgd.scm` | CPU | no |
129
+ | `SGDPerceptron` | Perceptron(shuffle=False) | `sgd.scm` | CPU | no |
130
+ | `LinearRANSAC` | RANSACRegressor (given subsets) | `ransac.scm` | CPU | no |
131
+ | `WeiszfeldTheilSen` | TheilSenRegressor | `theilsen.scm` | CPU | no |
132
+ | `CDNMF` | NMF(solver='cd') | `nmf-cd.scm (--derive)` | loops / BLAS / cuBLAS | no |
133
+ | `CDGraphicalLasso` | GraphicalLasso | `graphical-lasso.scm` | loops / BLAS | no |
134
+ | `EMGaussianMixture` | GaussianMixture(covariance_type='full') | `gmm.scm` | loops / BLAS / --cuda | no |
135
+ | `ParallelFastICA` | FastICA | `fastica.scm` | loops / BLAS / --cuda | no |
136
+ | `CovPCA` | PCA(svd_solver='covariance_eigh') | `pca.scm` | loops / BLAS / cuBLAS | PCA |
137
+ | `f_regression, f_classif` | f_regression, f_classif | `fscore.scm` | loops / BLAS / cuBLAS | no |
138
+ | `SelectKBest, SelectPercentile` | SelectKBest, SelectPercentile | `fscore.scm` | loops / BLAS / cuBLAS | no |
139
+ | `GramRFE` | RFE(Ridge), RFE(LinearRegression) | `rfe.scm` | loops / BLAS / cuBLAS | inside: Ridge |
140
+
74
141
  ## Two kernels: `CovLasso` and `DerivedLasso`
75
142
 
76
143
  Everything above, and the cross-validation, bootstrap and GPU methods
@@ -715,6 +782,53 @@ estimators (`GramLassoLarsIC`, `GramLarsCV`, `GramLassoLarsCV`,
715
782
  `GramLars`, `GramLassoLars`, `CovPCA`) form a large X'X on the device
716
783
  when one answers. `SCM2CPP_NO_DEVICE=1` keeps everything on the CPU.
717
784
 
785
+ ## Against Intel's sklearnex
786
+
787
+ sklearnex (scikit-learn-intelex 2026.2, oneDAL underneath) replaces
788
+ some scikit-learn estimators with its own. `bench/sklearnex-compare.py`
789
+ times the ones with a counterpart here, sklearnex's estimators
790
+ imported directly (not patched in) so that scikit-learn itself is
791
+ timed in the same process. The ratios are scikit-learn's or
792
+ sklearnex's time over the package's best (its CPU build, or the device
793
+ build where one answers); the load average was 8 to 11, so the times
794
+ move by some tens of percent.
795
+
796
+ | estimator, n x p | iterations (sk / sklearnex / ours) | vs scikit-learn, one core | vs sklearnex, one core | vs sklearnex, 20 cores |
797
+ |---|---|---|---|---|
798
+ | LogisticRegression, 200,000 x 100 | 6 / 12 / 6 | 10.6x (device) | 8.4x (device) | 4.4x (device); on the CPU 0.5x |
799
+ | Ridge (alpha 1), 200,000 x 200 | - | 2.1x | 1.2x | 0.3x |
800
+ | PCA (10 components), 100,000 x 50 | - | 1.5x | 2.4x | 1.7x |
801
+ | PCA (10 components), 50,000 x 500 | - | 1.1x | 1.4x | 0.5x |
802
+ | SVC (rbf), 5,000 x 20 | 4,858 / 7 / 4,882 | 1.2x | 1.4x | 0.5x |
803
+ | SVR (rbf), 5,000 x 20 | 12,821 / 10 / 12,824 | 1.3x | 1.4x | 0.4x |
804
+ | RFE(Ridge), 20 of 200 kept, 20,000 x 200 | - | 216x | 128x | 78x |
805
+
806
+ On one core the package is ahead everywhere. sklearnex uses every
807
+ core by default, and the package's kernels are single-threaded, so
808
+ with 20 cores sklearnex is faster on Ridge, the larger PCA and the
809
+ SVMs; what stays ahead is the device build (logistic regression) and
810
+ an algorithm that does less work (RFE: the Gram once, the Cholesky
811
+ factor downdated per drop, where scikit-learn refits). sklearnex is
812
+ not scikit-learn's arithmetic: its logistic regression takes 12
813
+ iterations to scikit-learn's 6 and its coefficients differ from
814
+ scikit-learn's by 6e-3, where the package's agree to 4e-15; its SVMs
815
+ report a different iteration count. This sklearnex accelerates no
816
+ lasso, so the lasso tables above stand against scikit-learn alone.
817
+
818
+ ## The GPU build
819
+
820
+ Where the CUDA toolkit is at install time, `ParallelFastICA`,
821
+ `EMGaussianMixture`, `LBFGSLogisticRegression`,
822
+ `LBFGSLogisticRegressionCV`, `LBFGSHuberRegressor` and the `LBFGS*`
823
+ GLMs also get the same Scheme translated with `--cuda`
824
+ (`libscm2cpp_gpu.so`). Its loops are CUDA kernels and its products
825
+ cuBLAS, on arrays that stay on the device. The classes take it when a
826
+ device answers and the data has a million entries or more, and give
827
+ the CPU build's iterations and results to the last digits. Measured
828
+ against scikit-learn: 10-22x for the L-BFGS fits, 6.7x FastICA, 3x the
829
+ mixture and LogisticRegressionCV (the repository README's table).
830
+ `SCM2CPP_NO_DEVICE=1` keeps everything on the CPU.
831
+
718
832
  ## Logistic regression with L1
719
833
 
720
834
  `CovLogistic` solves L1-penalized logistic regression by
@@ -4,8 +4,8 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "scm2cpp-lasso"
7
- version = "0.12.0"
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 included"
7
+ version = "0.13.1"
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"
11
11
  license = {text = "MIT"}
@@ -31,4 +31,4 @@ package-dir = {"" = "src"}
31
31
  where = ["src"]
32
32
 
33
33
  [tool.setuptools.package-data]
34
- scm2cpp_lasso = ["_generated/*.hpp", "_generated/*.cpp", "_generated/derived_*/*.hpp", "_generated/derived_*/*.cpp", "_generated/nmf_*/*.hpp", "_generated/nmf_*/*.cpp", "_generated/gramops_*/*.hpp", "_generated/gramops_*/*.cpp", "_generated/accel_*/*.hpp", "_generated/accel_*/*.cpp", "*.cu"]
34
+ scm2cpp_lasso = ["_generated/*.hpp", "_generated/*.cpp", "_generated/derived_*/*.hpp", "_generated/derived_*/*.cpp", "_generated/nmf_*/*.hpp", "_generated/nmf_*/*.cpp", "_generated/gramops_*/*.hpp", "_generated/gramops_*/*.cpp", "_generated/accel_*/*.hpp", "_generated/accel_*/*.cpp", "_generated/gpu_*/*.hpp", "_generated/gpu_*/*.cpp", "*.cu"]
@@ -120,6 +120,28 @@ class BuildWithCuda(build_ext):
120
120
  except (subprocess.CalledProcessError, OSError) as exc:
121
121
  print(f"scm2cpp_lasso: no CUDA batch path ({exc}); "
122
122
  "the CPU solver is unaffected", file=sys.stderr)
123
+ # the --cuda translation (kernels + device-resident cuBLAS):
124
+ # FastICA, the mixture, the L-BFGS fits
125
+ gpu = GEN / "gpu_cuda" / "gpu_capi.cpp"
126
+ if gpu.exists():
127
+ out2 = Path(self.build_lib) / "scm2cpp_lasso" / "libscm2cpp_gpu.so"
128
+ # the installed device's architecture when one answers
129
+ arch = []
130
+ try:
131
+ if subprocess.run(["nvidia-smi"], capture_output=True).returncode == 0:
132
+ arch = ["-arch=native"]
133
+ except OSError:
134
+ pass
135
+ cmd2 = [nvcc, "-O3", "-std=c++17", "-shared", "-x", "cu", *arch,
136
+ "-Xcompiler", "-fPIC", "-I", str(GEN / "gpu_cuda"), "-I", str(GEN),
137
+ str(gpu), "-o", str(out2), "-lcublas",
138
+ "-Wno-deprecated-gpu-targets", "-diag-suppress", "174,177,550"]
139
+ try:
140
+ subprocess.run(cmd2, check=True)
141
+ print("scm2cpp_lasso: built the --cuda estimators")
142
+ except (subprocess.CalledProcessError, OSError) as exc:
143
+ print(f"scm2cpp_lasso: no --cuda estimators ({exc}); "
144
+ "the CPU paths are unaffected", file=sys.stderr)
123
145
 
124
146
 
125
147
  # The cuBLAS variant of the derived kernel: host C++ against the CUDA
@@ -220,4 +242,27 @@ if _cuda is not None:
220
242
  extra_compile_args=CXXFLAGS + ["-O3", "-fstrict-overflow"],
221
243
  ))
222
244
 
223
- setup(ext_modules=extensions, cmdclass={"build_ext": BuildWithCuda})
245
+ # A wheel is tagged py3-none-<platform>: the extensions are shared
246
+ # libraries the package loads through ctypes, using no Python API, so
247
+ # one build serves every Python on that platform.
248
+ try:
249
+ from setuptools.command.bdist_wheel import bdist_wheel
250
+ except ImportError: # setuptools before 70.1
251
+ try:
252
+ from wheel.bdist_wheel import bdist_wheel
253
+ except ImportError:
254
+ bdist_wheel = None
255
+
256
+ cmdclass = {"build_ext": BuildWithCuda}
257
+ if bdist_wheel is not None:
258
+ class PlatformWheel(bdist_wheel):
259
+ def finalize_options(self):
260
+ super().finalize_options()
261
+ self.root_is_pure = False
262
+
263
+ def get_tag(self):
264
+ return ("py3", "none", super().get_tag()[2])
265
+
266
+ cmdclass["bdist_wheel"] = PlatformWheel
267
+
268
+ 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.12.0"
54
+ __version__ = "0.13.1"
55
55
 
56
56
  _BATCH = load_batch_lib()
57
57
  _DP = ctypes.POINTER(ctypes.c_double)
@@ -1350,7 +1350,7 @@ class LBFGSLogisticRegression:
1350
1350
  k = nc if multi else 1
1351
1351
  coef = np.zeros(k * (p + 1))
1352
1352
  out = np.zeros(2)
1353
- it = _gramops_kernels().logistic_lbfgs(
1353
+ it = _gpu_or_cpu(n * p, getattr(self, "backend", None)).logistic_lbfgs(
1354
1354
  X, yi.astype(np.float64), coef, out, float(self.C), int(self.max_iter),
1355
1355
  float(self.tol), multi, 1 if self.fit_intercept else 0, n, p, k)
1356
1356
  self.coef_ = coef[:k * p].reshape(k, p).copy()
@@ -1418,7 +1418,7 @@ class _LBFGSGLM:
1418
1418
  coef = np.zeros(p + 1)
1419
1419
  out = np.zeros(2)
1420
1420
  fam, power = self._family_power()
1421
- it = _gramops_kernels().glm(X, y, coef, out, fam, float(power), float(self.alpha),
1421
+ it = _gpu_or_cpu(n * p, getattr(self, "backend", None)).glm(X, y, coef, out, fam, float(power), float(self.alpha),
1422
1422
  int(self.max_iter), float(self.tol),
1423
1423
  1 if self.fit_intercept else 0, n, p)
1424
1424
  self.coef_, self.intercept_ = coef[:p].copy(), float(coef[p])
@@ -1499,7 +1499,7 @@ class LBFGSLogisticRegressionCV:
1499
1499
 
1500
1500
  def _path(self, X, yi, Cs, coef, multi, k):
1501
1501
  n, p = X.shape
1502
- lib = _gramops_kernels()
1502
+ lib = _gpu_or_cpu(n * p, getattr(self, "backend", None))
1503
1503
  coefs, iters = [], []
1504
1504
  out = np.zeros(2)
1505
1505
  for C in Cs:
@@ -1582,7 +1582,7 @@ class LBFGSHuberRegressor:
1582
1582
  coef = np.zeros(p + 2)
1583
1583
  coef[-1] = 1.0
1584
1584
  out = np.zeros(2)
1585
- it = _gramops_kernels().huber(
1585
+ it = _gpu_or_cpu(n * p, getattr(self, "backend", None)).huber(
1586
1586
  X, y, coef, out, float(self.epsilon), float(self.alpha), int(self.max_iter),
1587
1587
  float(self.tol), 1 if self.fit_intercept else 0, n, p)
1588
1588
  self.coef_ = coef[:p].copy()
@@ -1643,7 +1643,7 @@ class EMGaussianMixture:
1643
1643
  self.precisions_cholesky_ = np.zeros((k, p, p))
1644
1644
  labels = np.zeros(n)
1645
1645
  out = np.zeros(2)
1646
- it = _gramops_kernels().gmm(X, resp, self.weights_, self.means_,
1646
+ it = _gpu_or_cpu(n * p, getattr(self, "backend", None)).gmm(X, resp, self.weights_, self.means_,
1647
1647
  self.precisions_cholesky_, labels, out,
1648
1648
  int(self.max_iter), float(self.tol),
1649
1649
  float(self.reg_covar), n, p, k)
@@ -1744,7 +1744,7 @@ class ParallelFastICA:
1744
1744
  w0 = np.ascontiguousarray(w0)
1745
1745
  self.components_ = np.zeros((k, p)); self.whitening_ = np.zeros((k, p))
1746
1746
  self._unmixing = np.zeros((k, k)); self.mean_ = np.zeros(p)
1747
- self.n_iter_ = _gramops_kernels().fastica(
1747
+ self.n_iter_ = _gpu_or_cpu(n * p, getattr(self, "backend", None)).fastica(
1748
1748
  X, w0, self.components_, self.whitening_, self._unmixing, self.mean_,
1749
1749
  int(self.max_iter), float(self.tol), n, p, k)
1750
1750
  self.mixing_ = np.linalg.pinv(self.components_)
@@ -1803,6 +1803,19 @@ class CovARDRegression:
1803
1803
  return np.ascontiguousarray(X, dtype=np.float64) @ self.coef_ + self.intercept_
1804
1804
 
1805
1805
 
1806
+ def _gpu_or_cpu(size, backend=None):
1807
+ """The --cuda translation (kernels and device-resident cuBLAS) when it
1808
+ was built, a device answers, the backend allows it and the problem has
1809
+ at least a million entries; the CPU library otherwise."""
1810
+ import os
1811
+ from ._libfind import gpu_built
1812
+ if (backend in (None, "cuda") and size >= 1e6 and not os.environ.get("SCM2CPP_NO_DEVICE")
1813
+ and gpu_built() and cuda_available()):
1814
+ import importlib
1815
+ return importlib.import_module(f"{__name__}._generated._loader_gpu_cuda")
1816
+ return _gramops_kernels()
1817
+
1818
+
1806
1819
  def _accel(work):
1807
1820
  """The --cublas build of the selection and Gram products, when it was
1808
1821
  built, a device answers, and WORK (flops) is large enough to pay for