scm2cpp-lasso 0.15.0__tar.gz → 0.16.0__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 (69) hide show
  1. {scm2cpp_lasso-0.15.0/src/scm2cpp_lasso.egg-info → scm2cpp_lasso-0.16.0}/PKG-INFO +119 -22
  2. scm2cpp_lasso-0.15.0/PKG-INFO → scm2cpp_lasso-0.16.0/README.md +117 -38
  3. {scm2cpp_lasso-0.15.0 → scm2cpp_lasso-0.16.0}/pyproject.toml +2 -2
  4. {scm2cpp_lasso-0.15.0 → scm2cpp_lasso-0.16.0}/setup.py +83 -10
  5. {scm2cpp_lasso-0.15.0 → scm2cpp_lasso-0.16.0}/src/scm2cpp_lasso/__init__.py +357 -30
  6. {scm2cpp_lasso-0.15.0 → scm2cpp_lasso-0.16.0}/src/scm2cpp_lasso/_generated/_loader_accel_cublas.py +9 -0
  7. {scm2cpp_lasso-0.15.0 → scm2cpp_lasso-0.16.0}/src/scm2cpp_lasso/_generated/_loader_classifiers.py +17 -0
  8. {scm2cpp_lasso-0.15.0 → scm2cpp_lasso-0.16.0}/src/scm2cpp_lasso/_generated/_loader_gpu_cuda.py +9 -0
  9. {scm2cpp_lasso-0.15.0 → scm2cpp_lasso-0.16.0}/src/scm2cpp_lasso/_generated/_loader_gramops_blas.py +121 -0
  10. {scm2cpp_lasso-0.15.0 → scm2cpp_lasso-0.16.0}/src/scm2cpp_lasso/_generated/_loader_gramops_loops.py +130 -0
  11. {scm2cpp_lasso-0.15.0 → scm2cpp_lasso-0.16.0}/src/scm2cpp_lasso/_generated/accel_cublas/accel.cpp +1 -0
  12. {scm2cpp_lasso-0.15.0 → scm2cpp_lasso-0.16.0}/src/scm2cpp_lasso/_generated/accel_cublas/accel.hpp +38 -17
  13. {scm2cpp_lasso-0.15.0 → scm2cpp_lasso-0.16.0}/src/scm2cpp_lasso/_generated/accel_cublas/accel_capi.cpp +3 -0
  14. {scm2cpp_lasso-0.15.0 → scm2cpp_lasso-0.16.0}/src/scm2cpp_lasso/_generated/classifiers.cpp +2 -0
  15. {scm2cpp_lasso-0.15.0 → scm2cpp_lasso-0.16.0}/src/scm2cpp_lasso/_generated/classifiers.hpp +1221 -366
  16. {scm2cpp_lasso-0.15.0 → scm2cpp_lasso-0.16.0}/src/scm2cpp_lasso/_generated/classifiers_capi.cpp +6 -0
  17. {scm2cpp_lasso-0.15.0 → scm2cpp_lasso-0.16.0}/src/scm2cpp_lasso/_generated/derived_blas/lasso_design.hpp +17 -14
  18. {scm2cpp_lasso-0.15.0 → scm2cpp_lasso-0.16.0}/src/scm2cpp_lasso/_generated/derived_cublas/lasso_design.hpp +18 -15
  19. {scm2cpp_lasso-0.15.0 → scm2cpp_lasso-0.16.0}/src/scm2cpp_lasso/_generated/derived_loops/lasso_design.hpp +29 -26
  20. {scm2cpp_lasso-0.15.0 → scm2cpp_lasso-0.16.0}/src/scm2cpp_lasso/_generated/gpu_cuda/gpu.cpp +1 -0
  21. {scm2cpp_lasso-0.15.0 → scm2cpp_lasso-0.16.0}/src/scm2cpp_lasso/_generated/gpu_cuda/gpu.hpp +299 -246
  22. {scm2cpp_lasso-0.15.0 → scm2cpp_lasso-0.16.0}/src/scm2cpp_lasso/_generated/gpu_cuda/gpu_capi.cpp +9 -0
  23. {scm2cpp_lasso-0.15.0/src/scm2cpp_lasso/_generated/gramops_loops → scm2cpp_lasso-0.16.0/src/scm2cpp_lasso/_generated/gramops_blas}/gramops.cpp +15 -0
  24. scm2cpp_lasso-0.16.0/src/scm2cpp_lasso/_generated/gramops_blas/gramops.hpp +4959 -0
  25. {scm2cpp_lasso-0.15.0/src/scm2cpp_lasso/_generated/gramops_loops → scm2cpp_lasso-0.16.0/src/scm2cpp_lasso/_generated/gramops_blas}/gramops_capi.cpp +42 -0
  26. {scm2cpp_lasso-0.15.0/src/scm2cpp_lasso/_generated/gramops_blas → scm2cpp_lasso-0.16.0/src/scm2cpp_lasso/_generated/gramops_loops}/gramops.cpp +15 -0
  27. scm2cpp_lasso-0.16.0/src/scm2cpp_lasso/_generated/gramops_loops/gramops.hpp +5860 -0
  28. {scm2cpp_lasso-0.15.0/src/scm2cpp_lasso/_generated/gramops_blas → scm2cpp_lasso-0.16.0/src/scm2cpp_lasso/_generated/gramops_loops}/gramops_capi.cpp +45 -0
  29. {scm2cpp_lasso-0.15.0 → scm2cpp_lasso-0.16.0}/src/scm2cpp_lasso/_generated/scm2cpp-blas.hpp +61 -0
  30. scm2cpp_lasso-0.15.0/README.md → scm2cpp_lasso-0.16.0/src/scm2cpp_lasso.egg-info/PKG-INFO +135 -20
  31. scm2cpp_lasso-0.15.0/src/scm2cpp_lasso/_generated/gramops_blas/gramops.hpp +0 -3033
  32. scm2cpp_lasso-0.15.0/src/scm2cpp_lasso/_generated/gramops_loops/gramops.hpp +0 -3302
  33. {scm2cpp_lasso-0.15.0 → scm2cpp_lasso-0.16.0}/setup.cfg +0 -0
  34. {scm2cpp_lasso-0.15.0 → scm2cpp_lasso-0.16.0}/src/scm2cpp_lasso/_compat.py +0 -0
  35. {scm2cpp_lasso-0.15.0 → scm2cpp_lasso-0.16.0}/src/scm2cpp_lasso/_generated/__init__.py +0 -0
  36. {scm2cpp_lasso-0.15.0 → scm2cpp_lasso-0.16.0}/src/scm2cpp_lasso/_generated/_loader.py +0 -0
  37. {scm2cpp_lasso-0.15.0 → scm2cpp_lasso-0.16.0}/src/scm2cpp_lasso/_generated/_loader_derived_blas.py +0 -0
  38. {scm2cpp_lasso-0.15.0 → scm2cpp_lasso-0.16.0}/src/scm2cpp_lasso/_generated/_loader_derived_cublas.py +0 -0
  39. {scm2cpp_lasso-0.15.0 → scm2cpp_lasso-0.16.0}/src/scm2cpp_lasso/_generated/_loader_derived_loops.py +0 -0
  40. {scm2cpp_lasso-0.15.0 → scm2cpp_lasso-0.16.0}/src/scm2cpp_lasso/_generated/_loader_nmf_blas.py +0 -0
  41. {scm2cpp_lasso-0.15.0 → scm2cpp_lasso-0.16.0}/src/scm2cpp_lasso/_generated/_loader_nmf_cublas.py +0 -0
  42. {scm2cpp_lasso-0.15.0 → scm2cpp_lasso-0.16.0}/src/scm2cpp_lasso/_generated/_loader_nmf_loops.py +0 -0
  43. {scm2cpp_lasso-0.15.0 → scm2cpp_lasso-0.16.0}/src/scm2cpp_lasso/_generated/derived_blas/lasso_design.cpp +0 -0
  44. {scm2cpp_lasso-0.15.0 → scm2cpp_lasso-0.16.0}/src/scm2cpp_lasso/_generated/derived_blas/lasso_design_capi.cpp +0 -0
  45. {scm2cpp_lasso-0.15.0 → scm2cpp_lasso-0.16.0}/src/scm2cpp_lasso/_generated/derived_cublas/lasso_design.cpp +0 -0
  46. {scm2cpp_lasso-0.15.0 → scm2cpp_lasso-0.16.0}/src/scm2cpp_lasso/_generated/derived_cublas/lasso_design_capi.cpp +0 -0
  47. {scm2cpp_lasso-0.15.0 → scm2cpp_lasso-0.16.0}/src/scm2cpp_lasso/_generated/derived_loops/lasso_design.cpp +0 -0
  48. {scm2cpp_lasso-0.15.0 → scm2cpp_lasso-0.16.0}/src/scm2cpp_lasso/_generated/derived_loops/lasso_design_capi.cpp +0 -0
  49. {scm2cpp_lasso-0.15.0 → scm2cpp_lasso-0.16.0}/src/scm2cpp_lasso/_generated/lasso_cov.cpp +0 -0
  50. {scm2cpp_lasso-0.15.0 → scm2cpp_lasso-0.16.0}/src/scm2cpp_lasso/_generated/lasso_cov.hpp +0 -0
  51. {scm2cpp_lasso-0.15.0 → scm2cpp_lasso-0.16.0}/src/scm2cpp_lasso/_generated/lasso_cov_capi.cpp +0 -0
  52. {scm2cpp_lasso-0.15.0 → scm2cpp_lasso-0.16.0}/src/scm2cpp_lasso/_generated/nmf_blas/nmf_cd.cpp +0 -0
  53. {scm2cpp_lasso-0.15.0 → scm2cpp_lasso-0.16.0}/src/scm2cpp_lasso/_generated/nmf_blas/nmf_cd.hpp +0 -0
  54. {scm2cpp_lasso-0.15.0 → scm2cpp_lasso-0.16.0}/src/scm2cpp_lasso/_generated/nmf_blas/nmf_cd_capi.cpp +0 -0
  55. {scm2cpp_lasso-0.15.0 → scm2cpp_lasso-0.16.0}/src/scm2cpp_lasso/_generated/nmf_cublas/nmf_cd.cpp +0 -0
  56. {scm2cpp_lasso-0.15.0 → scm2cpp_lasso-0.16.0}/src/scm2cpp_lasso/_generated/nmf_cublas/nmf_cd.hpp +0 -0
  57. {scm2cpp_lasso-0.15.0 → scm2cpp_lasso-0.16.0}/src/scm2cpp_lasso/_generated/nmf_cublas/nmf_cd_capi.cpp +0 -0
  58. {scm2cpp_lasso-0.15.0 → scm2cpp_lasso-0.16.0}/src/scm2cpp_lasso/_generated/nmf_loops/nmf_cd.cpp +0 -0
  59. {scm2cpp_lasso-0.15.0 → scm2cpp_lasso-0.16.0}/src/scm2cpp_lasso/_generated/nmf_loops/nmf_cd.hpp +0 -0
  60. {scm2cpp_lasso-0.15.0 → scm2cpp_lasso-0.16.0}/src/scm2cpp_lasso/_generated/nmf_loops/nmf_cd_capi.cpp +0 -0
  61. {scm2cpp_lasso-0.15.0 → scm2cpp_lasso-0.16.0}/src/scm2cpp_lasso/_generated/scm2cpp-cublas.hpp +0 -0
  62. {scm2cpp_lasso-0.15.0 → scm2cpp_lasso-0.16.0}/src/scm2cpp_lasso/_generated/scm2cpp-cuda.hpp +0 -0
  63. {scm2cpp_lasso-0.15.0 → scm2cpp_lasso-0.16.0}/src/scm2cpp_lasso/_generated/scm2cpp.hpp +0 -0
  64. {scm2cpp_lasso-0.15.0 → scm2cpp_lasso-0.16.0}/src/scm2cpp_lasso/_libfind.py +0 -0
  65. {scm2cpp_lasso-0.15.0 → scm2cpp_lasso-0.16.0}/src/scm2cpp_lasso/batch_capi.cu +0 -0
  66. {scm2cpp_lasso-0.15.0 → scm2cpp_lasso-0.16.0}/src/scm2cpp_lasso.egg-info/SOURCES.txt +0 -0
  67. {scm2cpp_lasso-0.15.0 → scm2cpp_lasso-0.16.0}/src/scm2cpp_lasso.egg-info/dependency_links.txt +0 -0
  68. {scm2cpp_lasso-0.15.0 → scm2cpp_lasso-0.16.0}/src/scm2cpp_lasso.egg-info/requires.txt +0 -0
  69. {scm2cpp_lasso-0.15.0 → scm2cpp_lasso-0.16.0}/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.15.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 and CUDA kernels included
3
+ Version: 0.16.0
4
+ Summary: scikit-learn estimators (lasso, elastic net, ridge, RidgeCV, ARD, SVM, SVR, logistic regression, GLMs, Huber, Theil-Sen, F-score selection, RFE, NMF, Gaussian mixture, FastICA, PCA, KernelPCA, 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
@@ -41,6 +41,16 @@ from the sdist on a machine with `nvcc` on the path:
41
41
  $ pip install --no-binary scm2cpp-lasso scm2cpp-lasso
42
42
  ```
43
43
 
44
+ Built from the sdist, the products and the eigen- and singular value
45
+ problems go to Intel MKL where pkg-config knows it
46
+ (`mkl-dynamic-lp64-gomp`, GNU OpenMP threading, as Gentoo's
47
+ `sci-libs/mkl` installs it), else to an OpenBLAS with LAPACKE, else to
48
+ the translated loops; `SCM2CPP_NO_MKL=1` skips MKL. The translated
49
+ loops that the dependence test proves independent run on OpenMP where
50
+ the compiler has it (`SCM2CPP_NO_OPENMP=1` builds them serial). Both
51
+ use every core unless told otherwise: `OMP_NUM_THREADS` (and
52
+ `MKL_NUM_THREADS` / `OPENBLAS_NUM_THREADS`) set the threads.
53
+
44
54
  The batched GPU solver, the cuBLAS variants and the `--cuda`
45
55
  estimators are then built as well; without `nvcc` the package installs
46
56
  and works exactly the same, minus those.
@@ -109,9 +119,7 @@ from scikit-learn's `BaseEstimator` and the matching mixin, so `clone`,
109
119
  `get_params` / `set_params`, `GridSearchCV`, `Pipeline` and
110
120
  `cross_val_score` take them, and all 44 pass
111
121
  `sklearn.utils.estimator_checks.check_estimator` (`check-sklearn.py`
112
- runs it; the expected failures are `DesignLasso`'s missing `n_iter_`,
113
- since the derived kernel does not count its sweeps, and
114
- `LBFGSLogisticRegressionCV`'s
122
+ runs it; the one expected failure is `LBFGSLogisticRegressionCV`'s
115
123
  `n_iter_` of 0 for a C whose warm start is already converged, which
116
124
  scikit-learn's own `LogisticRegressionCV` reports on the same data).
117
125
  scikit-learn stays optional: without it a numpy-only stand-in gives
@@ -193,7 +201,8 @@ package picks the fastest one present.
193
201
  |---|---|---|---|---|
194
202
  | `GramLasso` | Lasso(precompute=True), fit(X, y) with the intercept | `lasso-cov.scm` | CPU | no |
195
203
  | `GramElasticNet` | ElasticNet(precompute=True), fit(X, y) with the intercept | `lasso-cov.scm` | CPU | no |
196
- | `GramRidge` | Ridge, fit(X, y) with the intercept, one or many targets | numpy (Cholesky) | CPU | Ridge |
204
+ | `GramRidge` | Ridge, fit(X, y) with the intercept, one or many targets | `rfe.scm` (Cholesky) | loops / BLAS | Ridge |
205
+ | `GramRidgeCV` | RidgeCV (cv=None), leave-one-out over every alpha from one eigenproblem | `ridge-gcv.scm` | loops / BLAS | no |
197
206
  | `GramMultiTaskLasso` | MultiTaskLasso, fit(X, Y) with the intercept | `lasso-cov.scm` | CPU | no |
198
207
  | `GramMultiTaskElasticNet` | MultiTaskElasticNet, fit(X, Y) with the intercept | `lasso-cov.scm` | CPU | no |
199
208
  | `GramGroupLasso` | (none; skglm GroupLasso), fit(X, y) with the intercept | `lasso-cov.scm` | CPU | - |
@@ -238,6 +247,7 @@ package picks the fastest one present.
238
247
  | `EMGaussianMixture` | GaussianMixture(covariance_type='full') | `gmm.scm` | loops / BLAS / --cuda | no |
239
248
  | `ParallelFastICA` | FastICA | `fastica.scm` | loops / BLAS / --cuda | no |
240
249
  | `CovPCA` | PCA(svd_solver='covariance_eigh') | `pca.scm` | loops / BLAS / cuBLAS | PCA |
250
+ | `GramKernelPCA` | KernelPCA (no inverse_transform) | `kernel-pca.scm` | loops / BLAS | no |
241
251
  | `f_regression, f_classif` | f_regression, f_classif | `fscore.scm` | loops / BLAS / cuBLAS | no |
242
252
  | `SelectKBest, SelectPercentile` | SelectKBest, SelectPercentile | `fscore.scm` | loops / BLAS / cuBLAS | no |
243
253
  | `GramRFE` | RFE(Ridge), RFE(LinearRegression) | `rfe.scm` | loops / BLAS / cuBLAS | inside: Ridge |
@@ -560,6 +570,79 @@ on the GPU: the ridge is one eigendecomposition and the elastic net's
560
570
  sweeps are the lasso's; `fit_path_batch(l1_ratio=..)` is the GPU
561
571
  grid for the elastic net.
562
572
 
573
+ ## Leave-one-out ridge: `GramRidgeCV`
574
+
575
+ `GramRidgeCV` is scikit-learn's `RidgeCV` with `cv=None`: the alpha
576
+ whose leave-one-out mean squared error is least, without a refit per
577
+ row. With the centred design Xc and H = Xc (Xc'Xc + alpha I)^-1 Xc'
578
+ (plus 1/n for the intercept), the leave-one-out residual is
579
+ (y - yhat) / (1 - diag H). One eigenproblem gives H = Z diag(1 /
580
+ (lam + alpha)) Z' for every alpha: Z = Xc V from Xc'Xc = V diag(lam) V'
581
+ when n > p (X'X by dsyrk), Z = Q diag(sqrt mu) from Xc Xc' = Q diag(mu)
582
+ Q' otherwise. Then diag H and yhat at all the alphas are two products,
583
+ (Z o Z) W and Z (R o W) with W[k, a] = 1 / (lam_k + alpha_a): the
584
+ alphas cost one dgemm, where scikit-learn loops over them in numpy
585
+ (`examples/kernel-only/ridge-gcv.scm`; the eigenproblem is LAPACK's
586
+ dsyevd in the BLAS build).
587
+
588
+ ```python
589
+ from scm2cpp_lasso import GramRidgeCV
590
+ m = GramRidgeCV(alphas=np.logspace(-2, 4, 50)).fit(X, y)
591
+ m.alpha_, m.best_score_, m.coef_, m.intercept_
592
+ ```
593
+
594
+ It takes `fit_intercept`, `gcv_mode` (`"svd"` / `"eigen"` choose the
595
+ two Zs), `store_cv_results` and `alpha_per_target`, y one target or
596
+ several; `cv` and `scoring` must stay None. The chosen alpha is
597
+ scikit-learn's, the coefficients agree to 1e-12 and `cv_results_` to
598
+ 1e-7 relative (the wide case near interpolation). Measured
599
+ (`bench/eigen-compare.py`, the MKL build, best of three):
600
+
601
+ | case | scikit-learn `RidgeCV` | `GramRidgeCV` | ratio, one core | ratio, 20 cores |
602
+ |---|---|---|---|---|
603
+ | 200,000 x 200, 3 alphas | 2.04 s | 0.80 s | 2.6x | 3.6x |
604
+ | 200,000 x 200, 50 alphas | 26.6 s | 0.96 s | 27.7x | 29.0x |
605
+ | 20,000 x 500, 50 alphas | 9.64 s | 0.33 s | 28.9x | 29.2x |
606
+ | 2,000 x 5,000, 50 alphas | 3.16 s | 1.47 s | 2.1x | 4.5x |
607
+
608
+ ## Kernel PCA: `GramKernelPCA`
609
+
610
+ `GramKernelPCA` is scikit-learn's `KernelPCA`: the kernel matrix
611
+ (linear, rbf, poly, sigmoid, cosine or precomputed), centred in feature
612
+ space, its largest eigenpairs with svd_flip's signs
613
+ (`examples/kernel-only/kernel-pca.scm`). Where the eigenpairs come from
614
+ is what differs:
615
+
616
+ - fewer than 10 components of more than 200 rows, where scikit-learn
617
+ takes ARPACK (Lanczos, one matrix-vector product at a time): subspace
618
+ iteration on c + 25 columns, so each step is one dgemm, stopped when
619
+ every residual |K u - lam u| is under 1e-12 of the largest value
620
+ (falling back to the dense route if it is not reached);
621
+ - otherwise LAPACK's dsyevr over the wanted index range, which is what
622
+ scipy's `eigh(subset_by_index=..)` runs for scikit-learn;
623
+ - the linear kernel with no more components than features, n > p:
624
+ through Xc'Xc, p x p (the eigenvectors of Xc Xc' are Xc v / sqrt(lam)),
625
+ where scikit-learn forms the n x n kernel.
626
+
627
+ ```python
628
+ from scm2cpp_lasso import GramKernelPCA
629
+ Z = GramKernelPCA(5, kernel="rbf", gamma=0.05).fit_transform(X)
630
+ ```
631
+
632
+ Eigenvalues, eigenvectors and transforms agree with scikit-learn's to
633
+ 1e-12 on every kernel; no `inverse_transform`. Measured
634
+ (`bench/eigen-compare.py`, the MKL build, best of three):
635
+
636
+ | case | scikit-learn `KernelPCA` | `GramKernelPCA` | ratio, one core | ratio, 20 cores |
637
+ |---|---|---|---|---|
638
+ | rbf, 3,000 x 20, 5 components | 1.11 s (ARPACK) | 0.35 s (subspace) | 3.2x | 3.0x |
639
+ | rbf, 6,000 x 20, 5 components | 4.63 s (ARPACK) | 1.33 s (subspace) | 3.5x | 4.2x |
640
+ | rbf, 3,000 x 20, 50 components | 3.48 s (dense) | 3.63 s (dense) | 1.0x | 1.2x |
641
+ | linear, 5,000 x 50, 10 components | 19.8 s (ARPACK on 5,000 x 5,000) | 0.002 s (X'X) | 10,000x | 2,100x |
642
+
643
+ The linear row is the algorithm (an n x n problem against a p x p one),
644
+ not the code.
645
+
563
646
  ## Bayesian ridge
564
647
 
565
648
  `CovBayesianRidge` is scikit-learn's `BayesianRidge` -- the same model,
@@ -645,6 +728,15 @@ matrix (`KernelSVM`) or transposing X into columns
645
728
  | `L1LogisticRegression`, 20,000 x 500, C=0.05 | 1.20 s | 2.23 s | 1.9x | 106.77913 / 106.77915 | 100% |
646
729
  | `L1LogisticRegression`, 100,000 x 200, C=0.05 | 9.06 s | 18.7 s | 2.1x | 349.61154 / 349.61216 | 100% |
647
730
 
731
+ On 2,000 x 20,000 neither side reaches the optimum at tol 1e-4 (the
732
+ objective at tol 1e-10 is 0.0519588), and where each stops depends on
733
+ the coordinate order: over eight seeds ours stops at 0.05240 to
734
+ 0.05251 (0.9 to 1.1% above the optimum), `LinearSVC`, whose order is
735
+ random unless `random_state` is given, at 0.05242 to 0.05319 (0.9 to
736
+ 2.4%). When `LinearSVC` draws one of its worse orders the gap passes
737
+ the script's 0.5% bound and the row prints `<-- disagrees`; ours is
738
+ then the better solution of the two, so the flag there is not a fault.
739
+
648
740
  The regressors, from the same script (C = 1, epsilon 0.1):
649
741
 
650
742
  | problem | ours | scikit-learn | ratio | agreement |
@@ -894,25 +986,30 @@ times the ones with a counterpart here, sklearnex's estimators
894
986
  imported directly (not patched in) so that scikit-learn itself is
895
987
  timed in the same process. The ratios are scikit-learn's or
896
988
  sklearnex's time over the package's best (its CPU build, or the device
897
- build where one answers); the load average was 8 to 11, so the times
898
- move by some tens of percent.
989
+ build where one answers). The package was built against Intel MKL
990
+ (see the build section), its loops under OpenMP; the load average was
991
+ about 2.3, so the times move by some percent.
899
992
 
900
993
  | estimator, n x p | iterations (sk / sklearnex / ours) | vs scikit-learn, one core | vs sklearnex, one core | vs sklearnex, 20 cores |
901
994
  |---|---|---|---|---|
902
- | LogisticRegression, 200,000 x 100 | 6 / 12 / 6 | 10.6x (device) | 8.4x (device) | 4.4x (device); on the CPU 0.5x |
903
- | Ridge (alpha 1), 200,000 x 200 | - | 2.1x | 1.2x | 0.3x |
904
- | PCA (10 components), 100,000 x 50 | - | 1.5x | 2.4x | 1.7x |
905
- | PCA (10 components), 50,000 x 500 | - | 1.1x | 1.4x | 0.5x |
906
- | SVC (rbf), 5,000 x 20 | 4,858 / 7 / 4,882 | 1.2x | 1.4x | 0.5x |
907
- | SVR (rbf), 5,000 x 20 | 12,821 / 10 / 12,824 | 1.3x | 1.4x | 0.4x |
908
- | RFE(Ridge), 20 of 200 kept, 20,000 x 200 | - | 216x | 128x | 78x |
909
-
910
- On one core the package is ahead everywhere. sklearnex uses every
911
- core by default, and the package's kernels are single-threaded, so
912
- with 20 cores sklearnex is faster on Ridge, the larger PCA and the
913
- SVMs; what stays ahead is the device build (logistic regression) and
914
- an algorithm that does less work (RFE: the Gram once, the Cholesky
915
- factor downdated per drop, where scikit-learn refits). sklearnex is
995
+ | LogisticRegression, 200,000 x 100 | 6 / 12 / 6 | 6.8x (device) | 6.4x (device) | 1.8x (device); on the CPU 0.9x |
996
+ | Ridge (alpha 1, no intercept), 200,000 x 200 | - | 1.7x | 0.8x | 2.3x |
997
+ | PCA (10 components), 100,000 x 50 | - | 1.4x | 2.2x | 1.4x |
998
+ | PCA (10 components), 50,000 x 500 | - | 1.1x | 1.4x | 1.6x (device); on the CPU 1.2x |
999
+ | SVC (rbf), 5,000 x 20 | 4,858 / 7 / 4,882 | 1.4x | 1.6x | 0.5x |
1000
+ | SVR (rbf), 5,000 x 20 | 12,821 / 10 / 12,824 | 1.4x | 1.4x | 0.4x |
1001
+ | RFE(Ridge), 20 of 200 kept, 20,000 x 200 | - | 138x | 106x | 116x |
1002
+
1003
+ On one core the package is ahead everywhere but Ridge, where both
1004
+ sides are one symmetric rank-k update of the same size and sklearnex
1005
+ is 0.180 s to 0.219 s. With 20 cores both sides use every core: the
1006
+ package's loops run under OpenMP and its products in MKL's threads,
1007
+ so it is ahead on Ridge (the Gram by dsyrk and a Cholesky solve), PCA,
1008
+ the device build of logistic regression and RFE (an algorithm that
1009
+ does less work: the Gram once, the Cholesky factor downdated per drop,
1010
+ where scikit-learn refits), level on logistic regression on the CPU,
1011
+ and behind on the SVMs: SMO takes thousands of short steps, each
1012
+ waiting on the last, where sklearnex's solver takes 7 to 10 long ones. sklearnex is
916
1013
  not scikit-learn's arithmetic: its logistic regression takes 12
917
1014
  iterations to scikit-learn's 6 and its coefficients differ from
918
1015
  scikit-learn's by 6e-3, where the package's agree to 4e-15; its SVMs
@@ -1,21 +1,3 @@
1
- Metadata-Version: 2.4
2
- Name: scm2cpp-lasso
3
- Version: 0.15.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 and CUDA kernels included
5
- Author: Hirotaka Niitsuma
6
- License: MIT
7
- Project-URL: Homepage, https://github.com/niitsuma/scm2cpp
8
- Project-URL: Source, https://github.com/niitsuma/scm2cpp/tree/master/python/scm2cpp-lasso
9
- Keywords: lasso,elastic-net,ridge,svm,logistic-regression,coordinate-descent,cuda,compiler
10
- Classifier: License :: OSI Approved :: MIT License
11
- Classifier: Programming Language :: Python :: 3
12
- Classifier: Programming Language :: C++
13
- Classifier: Topic :: Scientific/Engineering :: Mathematics
14
- Classifier: Intended Audience :: Science/Research
15
- Requires-Python: >=3.8
16
- Description-Content-Type: text/markdown
17
- Requires-Dist: numpy>=1.17
18
-
19
1
  # scm2cpp-lasso
20
2
 
21
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)
@@ -41,6 +23,16 @@ from the sdist on a machine with `nvcc` on the path:
41
23
  $ pip install --no-binary scm2cpp-lasso scm2cpp-lasso
42
24
  ```
43
25
 
26
+ Built from the sdist, the products and the eigen- and singular value
27
+ problems go to Intel MKL where pkg-config knows it
28
+ (`mkl-dynamic-lp64-gomp`, GNU OpenMP threading, as Gentoo's
29
+ `sci-libs/mkl` installs it), else to an OpenBLAS with LAPACKE, else to
30
+ the translated loops; `SCM2CPP_NO_MKL=1` skips MKL. The translated
31
+ loops that the dependence test proves independent run on OpenMP where
32
+ the compiler has it (`SCM2CPP_NO_OPENMP=1` builds them serial). Both
33
+ use every core unless told otherwise: `OMP_NUM_THREADS` (and
34
+ `MKL_NUM_THREADS` / `OPENBLAS_NUM_THREADS`) set the threads.
35
+
44
36
  The batched GPU solver, the cuBLAS variants and the `--cuda`
45
37
  estimators are then built as well; without `nvcc` the package installs
46
38
  and works exactly the same, minus those.
@@ -109,9 +101,7 @@ from scikit-learn's `BaseEstimator` and the matching mixin, so `clone`,
109
101
  `get_params` / `set_params`, `GridSearchCV`, `Pipeline` and
110
102
  `cross_val_score` take them, and all 44 pass
111
103
  `sklearn.utils.estimator_checks.check_estimator` (`check-sklearn.py`
112
- runs it; the expected failures are `DesignLasso`'s missing `n_iter_`,
113
- since the derived kernel does not count its sweeps, and
114
- `LBFGSLogisticRegressionCV`'s
104
+ runs it; the one expected failure is `LBFGSLogisticRegressionCV`'s
115
105
  `n_iter_` of 0 for a C whose warm start is already converged, which
116
106
  scikit-learn's own `LogisticRegressionCV` reports on the same data).
117
107
  scikit-learn stays optional: without it a numpy-only stand-in gives
@@ -193,7 +183,8 @@ package picks the fastest one present.
193
183
  |---|---|---|---|---|
194
184
  | `GramLasso` | Lasso(precompute=True), fit(X, y) with the intercept | `lasso-cov.scm` | CPU | no |
195
185
  | `GramElasticNet` | ElasticNet(precompute=True), fit(X, y) with the intercept | `lasso-cov.scm` | CPU | no |
196
- | `GramRidge` | Ridge, fit(X, y) with the intercept, one or many targets | numpy (Cholesky) | CPU | Ridge |
186
+ | `GramRidge` | Ridge, fit(X, y) with the intercept, one or many targets | `rfe.scm` (Cholesky) | loops / BLAS | Ridge |
187
+ | `GramRidgeCV` | RidgeCV (cv=None), leave-one-out over every alpha from one eigenproblem | `ridge-gcv.scm` | loops / BLAS | no |
197
188
  | `GramMultiTaskLasso` | MultiTaskLasso, fit(X, Y) with the intercept | `lasso-cov.scm` | CPU | no |
198
189
  | `GramMultiTaskElasticNet` | MultiTaskElasticNet, fit(X, Y) with the intercept | `lasso-cov.scm` | CPU | no |
199
190
  | `GramGroupLasso` | (none; skglm GroupLasso), fit(X, y) with the intercept | `lasso-cov.scm` | CPU | - |
@@ -238,6 +229,7 @@ package picks the fastest one present.
238
229
  | `EMGaussianMixture` | GaussianMixture(covariance_type='full') | `gmm.scm` | loops / BLAS / --cuda | no |
239
230
  | `ParallelFastICA` | FastICA | `fastica.scm` | loops / BLAS / --cuda | no |
240
231
  | `CovPCA` | PCA(svd_solver='covariance_eigh') | `pca.scm` | loops / BLAS / cuBLAS | PCA |
232
+ | `GramKernelPCA` | KernelPCA (no inverse_transform) | `kernel-pca.scm` | loops / BLAS | no |
241
233
  | `f_regression, f_classif` | f_regression, f_classif | `fscore.scm` | loops / BLAS / cuBLAS | no |
242
234
  | `SelectKBest, SelectPercentile` | SelectKBest, SelectPercentile | `fscore.scm` | loops / BLAS / cuBLAS | no |
243
235
  | `GramRFE` | RFE(Ridge), RFE(LinearRegression) | `rfe.scm` | loops / BLAS / cuBLAS | inside: Ridge |
@@ -560,6 +552,79 @@ on the GPU: the ridge is one eigendecomposition and the elastic net's
560
552
  sweeps are the lasso's; `fit_path_batch(l1_ratio=..)` is the GPU
561
553
  grid for the elastic net.
562
554
 
555
+ ## Leave-one-out ridge: `GramRidgeCV`
556
+
557
+ `GramRidgeCV` is scikit-learn's `RidgeCV` with `cv=None`: the alpha
558
+ whose leave-one-out mean squared error is least, without a refit per
559
+ row. With the centred design Xc and H = Xc (Xc'Xc + alpha I)^-1 Xc'
560
+ (plus 1/n for the intercept), the leave-one-out residual is
561
+ (y - yhat) / (1 - diag H). One eigenproblem gives H = Z diag(1 /
562
+ (lam + alpha)) Z' for every alpha: Z = Xc V from Xc'Xc = V diag(lam) V'
563
+ when n > p (X'X by dsyrk), Z = Q diag(sqrt mu) from Xc Xc' = Q diag(mu)
564
+ Q' otherwise. Then diag H and yhat at all the alphas are two products,
565
+ (Z o Z) W and Z (R o W) with W[k, a] = 1 / (lam_k + alpha_a): the
566
+ alphas cost one dgemm, where scikit-learn loops over them in numpy
567
+ (`examples/kernel-only/ridge-gcv.scm`; the eigenproblem is LAPACK's
568
+ dsyevd in the BLAS build).
569
+
570
+ ```python
571
+ from scm2cpp_lasso import GramRidgeCV
572
+ m = GramRidgeCV(alphas=np.logspace(-2, 4, 50)).fit(X, y)
573
+ m.alpha_, m.best_score_, m.coef_, m.intercept_
574
+ ```
575
+
576
+ It takes `fit_intercept`, `gcv_mode` (`"svd"` / `"eigen"` choose the
577
+ two Zs), `store_cv_results` and `alpha_per_target`, y one target or
578
+ several; `cv` and `scoring` must stay None. The chosen alpha is
579
+ scikit-learn's, the coefficients agree to 1e-12 and `cv_results_` to
580
+ 1e-7 relative (the wide case near interpolation). Measured
581
+ (`bench/eigen-compare.py`, the MKL build, best of three):
582
+
583
+ | case | scikit-learn `RidgeCV` | `GramRidgeCV` | ratio, one core | ratio, 20 cores |
584
+ |---|---|---|---|---|
585
+ | 200,000 x 200, 3 alphas | 2.04 s | 0.80 s | 2.6x | 3.6x |
586
+ | 200,000 x 200, 50 alphas | 26.6 s | 0.96 s | 27.7x | 29.0x |
587
+ | 20,000 x 500, 50 alphas | 9.64 s | 0.33 s | 28.9x | 29.2x |
588
+ | 2,000 x 5,000, 50 alphas | 3.16 s | 1.47 s | 2.1x | 4.5x |
589
+
590
+ ## Kernel PCA: `GramKernelPCA`
591
+
592
+ `GramKernelPCA` is scikit-learn's `KernelPCA`: the kernel matrix
593
+ (linear, rbf, poly, sigmoid, cosine or precomputed), centred in feature
594
+ space, its largest eigenpairs with svd_flip's signs
595
+ (`examples/kernel-only/kernel-pca.scm`). Where the eigenpairs come from
596
+ is what differs:
597
+
598
+ - fewer than 10 components of more than 200 rows, where scikit-learn
599
+ takes ARPACK (Lanczos, one matrix-vector product at a time): subspace
600
+ iteration on c + 25 columns, so each step is one dgemm, stopped when
601
+ every residual |K u - lam u| is under 1e-12 of the largest value
602
+ (falling back to the dense route if it is not reached);
603
+ - otherwise LAPACK's dsyevr over the wanted index range, which is what
604
+ scipy's `eigh(subset_by_index=..)` runs for scikit-learn;
605
+ - the linear kernel with no more components than features, n > p:
606
+ through Xc'Xc, p x p (the eigenvectors of Xc Xc' are Xc v / sqrt(lam)),
607
+ where scikit-learn forms the n x n kernel.
608
+
609
+ ```python
610
+ from scm2cpp_lasso import GramKernelPCA
611
+ Z = GramKernelPCA(5, kernel="rbf", gamma=0.05).fit_transform(X)
612
+ ```
613
+
614
+ Eigenvalues, eigenvectors and transforms agree with scikit-learn's to
615
+ 1e-12 on every kernel; no `inverse_transform`. Measured
616
+ (`bench/eigen-compare.py`, the MKL build, best of three):
617
+
618
+ | case | scikit-learn `KernelPCA` | `GramKernelPCA` | ratio, one core | ratio, 20 cores |
619
+ |---|---|---|---|---|
620
+ | rbf, 3,000 x 20, 5 components | 1.11 s (ARPACK) | 0.35 s (subspace) | 3.2x | 3.0x |
621
+ | rbf, 6,000 x 20, 5 components | 4.63 s (ARPACK) | 1.33 s (subspace) | 3.5x | 4.2x |
622
+ | rbf, 3,000 x 20, 50 components | 3.48 s (dense) | 3.63 s (dense) | 1.0x | 1.2x |
623
+ | linear, 5,000 x 50, 10 components | 19.8 s (ARPACK on 5,000 x 5,000) | 0.002 s (X'X) | 10,000x | 2,100x |
624
+
625
+ The linear row is the algorithm (an n x n problem against a p x p one),
626
+ not the code.
627
+
563
628
  ## Bayesian ridge
564
629
 
565
630
  `CovBayesianRidge` is scikit-learn's `BayesianRidge` -- the same model,
@@ -645,6 +710,15 @@ matrix (`KernelSVM`) or transposing X into columns
645
710
  | `L1LogisticRegression`, 20,000 x 500, C=0.05 | 1.20 s | 2.23 s | 1.9x | 106.77913 / 106.77915 | 100% |
646
711
  | `L1LogisticRegression`, 100,000 x 200, C=0.05 | 9.06 s | 18.7 s | 2.1x | 349.61154 / 349.61216 | 100% |
647
712
 
713
+ On 2,000 x 20,000 neither side reaches the optimum at tol 1e-4 (the
714
+ objective at tol 1e-10 is 0.0519588), and where each stops depends on
715
+ the coordinate order: over eight seeds ours stops at 0.05240 to
716
+ 0.05251 (0.9 to 1.1% above the optimum), `LinearSVC`, whose order is
717
+ random unless `random_state` is given, at 0.05242 to 0.05319 (0.9 to
718
+ 2.4%). When `LinearSVC` draws one of its worse orders the gap passes
719
+ the script's 0.5% bound and the row prints `<-- disagrees`; ours is
720
+ then the better solution of the two, so the flag there is not a fault.
721
+
648
722
  The regressors, from the same script (C = 1, epsilon 0.1):
649
723
 
650
724
  | problem | ours | scikit-learn | ratio | agreement |
@@ -894,25 +968,30 @@ times the ones with a counterpart here, sklearnex's estimators
894
968
  imported directly (not patched in) so that scikit-learn itself is
895
969
  timed in the same process. The ratios are scikit-learn's or
896
970
  sklearnex's time over the package's best (its CPU build, or the device
897
- build where one answers); the load average was 8 to 11, so the times
898
- move by some tens of percent.
971
+ build where one answers). The package was built against Intel MKL
972
+ (see the build section), its loops under OpenMP; the load average was
973
+ about 2.3, so the times move by some percent.
899
974
 
900
975
  | estimator, n x p | iterations (sk / sklearnex / ours) | vs scikit-learn, one core | vs sklearnex, one core | vs sklearnex, 20 cores |
901
976
  |---|---|---|---|---|
902
- | LogisticRegression, 200,000 x 100 | 6 / 12 / 6 | 10.6x (device) | 8.4x (device) | 4.4x (device); on the CPU 0.5x |
903
- | Ridge (alpha 1), 200,000 x 200 | - | 2.1x | 1.2x | 0.3x |
904
- | PCA (10 components), 100,000 x 50 | - | 1.5x | 2.4x | 1.7x |
905
- | PCA (10 components), 50,000 x 500 | - | 1.1x | 1.4x | 0.5x |
906
- | SVC (rbf), 5,000 x 20 | 4,858 / 7 / 4,882 | 1.2x | 1.4x | 0.5x |
907
- | SVR (rbf), 5,000 x 20 | 12,821 / 10 / 12,824 | 1.3x | 1.4x | 0.4x |
908
- | RFE(Ridge), 20 of 200 kept, 20,000 x 200 | - | 216x | 128x | 78x |
909
-
910
- On one core the package is ahead everywhere. sklearnex uses every
911
- core by default, and the package's kernels are single-threaded, so
912
- with 20 cores sklearnex is faster on Ridge, the larger PCA and the
913
- SVMs; what stays ahead is the device build (logistic regression) and
914
- an algorithm that does less work (RFE: the Gram once, the Cholesky
915
- factor downdated per drop, where scikit-learn refits). sklearnex is
977
+ | LogisticRegression, 200,000 x 100 | 6 / 12 / 6 | 6.8x (device) | 6.4x (device) | 1.8x (device); on the CPU 0.9x |
978
+ | Ridge (alpha 1, no intercept), 200,000 x 200 | - | 1.7x | 0.8x | 2.3x |
979
+ | PCA (10 components), 100,000 x 50 | - | 1.4x | 2.2x | 1.4x |
980
+ | PCA (10 components), 50,000 x 500 | - | 1.1x | 1.4x | 1.6x (device); on the CPU 1.2x |
981
+ | SVC (rbf), 5,000 x 20 | 4,858 / 7 / 4,882 | 1.4x | 1.6x | 0.5x |
982
+ | SVR (rbf), 5,000 x 20 | 12,821 / 10 / 12,824 | 1.4x | 1.4x | 0.4x |
983
+ | RFE(Ridge), 20 of 200 kept, 20,000 x 200 | - | 138x | 106x | 116x |
984
+
985
+ On one core the package is ahead everywhere but Ridge, where both
986
+ sides are one symmetric rank-k update of the same size and sklearnex
987
+ is 0.180 s to 0.219 s. With 20 cores both sides use every core: the
988
+ package's loops run under OpenMP and its products in MKL's threads,
989
+ so it is ahead on Ridge (the Gram by dsyrk and a Cholesky solve), PCA,
990
+ the device build of logistic regression and RFE (an algorithm that
991
+ does less work: the Gram once, the Cholesky factor downdated per drop,
992
+ where scikit-learn refits), level on logistic regression on the CPU,
993
+ and behind on the SVMs: SMO takes thousands of short steps, each
994
+ waiting on the last, where sklearnex's solver takes 7 to 10 long ones. sklearnex is
916
995
  not scikit-learn's arithmetic: its logistic regression takes 12
917
996
  iterations to scikit-learn's 6 and its coefficients differ from
918
997
  scikit-learn's by 6e-3, where the package's agree to 4e-15; its SVMs
@@ -4,8 +4,8 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "scm2cpp-lasso"
7
- version = "0.15.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 and CUDA kernels included"
7
+ version = "0.16.0"
8
+ description = "scikit-learn estimators (lasso, elastic net, ridge, RidgeCV, ARD, SVM, SVR, logistic regression, GLMs, Huber, Theil-Sen, F-score selection, RFE, NMF, Gaussian mixture, FastICA, PCA, KernelPCA, 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"}
@@ -41,6 +41,32 @@ kernel = Extension(
41
41
  )
42
42
 
43
43
 
44
+ def mkl_flags():
45
+ """Intel MKL where pkg-config knows it (mkl-dynamic-lp64-gomp: the GNU
46
+ OpenMP threading layer, so MKL's threads and the -P omp loops share
47
+ one libgomp) and a test program links, with SCM2CPP_MKL defined for
48
+ scm2cpp-blas.hpp's headers; else None. SCM2CPP_NO_MKL=1 skips it."""
49
+ if os.name == "nt" or os.environ.get("SCM2CPP_NO_MKL"):
50
+ return None
51
+ try:
52
+ pc = subprocess.run(["pkg-config", "--cflags", "--libs", "mkl-dynamic-lp64-gomp"],
53
+ capture_output=True, text=True, check=True)
54
+ except (subprocess.CalledProcessError, OSError):
55
+ return None
56
+ flags = pc.stdout.split() + ["-DSCM2CPP_MKL"]
57
+ probe = "#include <mkl_cblas.h>\n#include <mkl_lapacke.h>\nint main(){double a=1,b=0,w=0;" \
58
+ "cblas_dgemv(CblasRowMajor,CblasNoTrans,1,1,1.0,&a,1,&a,1,0.0,&b,1);" \
59
+ "LAPACKE_dsyevd(LAPACK_ROW_MAJOR,'V','U',1,&a,1,&w);" \
60
+ "return b==1?0:1;}\n"
61
+ cxx = os.environ.get("CXX", "c++")
62
+ try:
63
+ r = subprocess.run([cxx, "-x", "c++", "-", "-o", os.devnull] + flags,
64
+ input=probe, capture_output=True, text=True)
65
+ except OSError:
66
+ return None
67
+ return flags if r.returncode == 0 else None
68
+
69
+
44
70
  def cblas_flags():
45
71
  """How to compile and link against a CBLAS here, or None.
46
72
 
@@ -49,9 +75,25 @@ def cblas_flags():
49
75
  kernel's BLAS variant needs cblas.h and a library; without them the
50
76
  loop variant is built instead (same answers, the Gram as a scalar
51
77
  loop nest).
78
+
79
+ On Windows there is no pkg-config and no system OpenBLAS: the
80
+ directory of an unpacked OpenBLAS release (include/cblas.h,
81
+ lib/libopenblas.lib) is named by SCM2CPP_OPENBLAS_DIR, and the
82
+ wheel build bundles its bin/libopenblas.dll (delvewheel).
52
83
  """
53
- if os.name == "nt" or os.environ.get("SCM2CPP_NO_BLAS"):
84
+ if os.environ.get("SCM2CPP_NO_BLAS"):
54
85
  return None
86
+ mkl = mkl_flags()
87
+ if mkl is not None:
88
+ return mkl
89
+ if os.name == "nt":
90
+ root = os.environ.get("SCM2CPP_OPENBLAS_DIR")
91
+ if not root:
92
+ return None
93
+ inc, lib = Path(root) / "include", Path(root) / "lib" / "libopenblas.lib"
94
+ if not ((inc / "cblas.h").exists() and lib.exists()):
95
+ return None
96
+ return ["-I" + str(inc), str(lib)]
55
97
  cxx = os.environ.get("CXX", "c++")
56
98
  candidates = []
57
99
  try:
@@ -61,8 +103,11 @@ def cblas_flags():
61
103
  except (subprocess.CalledProcessError, OSError):
62
104
  pass
63
105
  candidates.append(["-lopenblas"])
64
- probe = "#include <cblas.h>\nint main(){double a=1,b=0;" \
106
+ # the BLAS variants call LAPACKE too (lapack-syevd! for sym-eigen),
107
+ # so a CBLAS without LAPACKE counts as none
108
+ probe = "#include <cblas.h>\n#include <lapacke.h>\nint main(){double a=1,b=0,w=0;" \
65
109
  "cblas_dgemv(CblasRowMajor,CblasNoTrans,1,1,1.0,&a,1,&a,1,0.0,&b,1);" \
110
+ "LAPACKE_dsyevd(LAPACK_ROW_MAJOR,'V','U',1,&a,1,&w);" \
66
111
  "return b==1?0:1;}\n"
67
112
  for flags in candidates:
68
113
  try:
@@ -75,6 +120,32 @@ def cblas_flags():
75
120
  return None
76
121
 
77
122
 
123
+ def openmp_flags():
124
+ """-fopenmp where the compiler takes the directives the translator
125
+ writes (-P omp: parallel for, reduction, an if clause on the work),
126
+ else nothing -- the same sources then compile serial. Not on
127
+ Windows (MSVC's OpenMP needs vcomp140.dll beside the wheel) nor
128
+ with SCM2CPP_NO_OPENMP set. OMP_NUM_THREADS sets the threads at
129
+ run time, all cores by default.
130
+ """
131
+ if os.name == "nt" or os.environ.get("SCM2CPP_NO_OPENMP"):
132
+ return []
133
+ cxx = os.environ.get("CXX", "c++")
134
+ probe = "int main(){double s=0;int n=4;\n" \
135
+ "#pragma omp parallel for reduction(+:s) if(n > 1)\n" \
136
+ "for(int i = 0 ; i < n ; i = (i+1)) s = (s+i);\n" \
137
+ "return s==6?0:1;}\n"
138
+ try:
139
+ r = subprocess.run([cxx, "-fopenmp", "-x", "c++", "-", "-o", os.devnull],
140
+ input=probe, capture_output=True, text=True)
141
+ except OSError:
142
+ return []
143
+ return ["-fopenmp"] if r.returncode == 0 else []
144
+
145
+
146
+ _omp = openmp_flags()
147
+
148
+
78
149
  # The derived kernel: lasso-design.scm, the plain residual-form
79
150
  # coordinate descent over X as numpy holds it, turned into the Gram form by the translator
80
151
  # (--derive) with its products as CBLAS calls (--blas). Which variant
@@ -185,7 +256,8 @@ classifiers = Extension(
185
256
  "scm2cpp_lasso._classifiers",
186
257
  sources=[GEN_REL + "/classifiers_capi.cpp"],
187
258
  include_dirs=[GEN_REL],
188
- extra_compile_args=_cls_flags,
259
+ extra_compile_args=_cls_flags + _omp,
260
+ extra_link_args=_omp,
189
261
  )
190
262
 
191
263
  # The derived NMF, in the same variants as the derived lasso.
@@ -194,7 +266,7 @@ if _blas is not None:
194
266
  "scm2cpp_lasso._nmf_blas",
195
267
  sources=[GEN_REL + "/nmf_blas/nmf_cd_capi.cpp"],
196
268
  include_dirs=[GEN_REL + "/nmf_blas", GEN_REL],
197
- extra_compile_args=CXXFLAGS + ["-O3", "-fstrict-overflow"]
269
+ extra_compile_args=_cls_flags
198
270
  + [f for f in _blas if f.startswith("-I") or f.startswith("-D")],
199
271
  extra_link_args=[f for f in _blas if not f.startswith("-I")
200
272
  and not f.startswith("-D")],
@@ -215,17 +287,18 @@ if _blas is not None:
215
287
  "scm2cpp_lasso._gramops_blas",
216
288
  sources=[GEN_REL + "/gramops_blas/gramops_capi.cpp"],
217
289
  include_dirs=[GEN_REL + "/gramops_blas", GEN_REL],
218
- extra_compile_args=CXXFLAGS + ["-O3", "-fstrict-overflow"]
290
+ extra_compile_args=_cls_flags + _omp
219
291
  + [f for f in _blas if f.startswith("-I") or f.startswith("-D")],
220
- extra_link_args=[f for f in _blas if not f.startswith("-I")
221
- and not f.startswith("-D")],
292
+ extra_link_args=_omp + [f for f in _blas if not f.startswith("-I")
293
+ and not f.startswith("-D")],
222
294
  )
223
295
  else:
224
296
  gramops = Extension(
225
297
  "scm2cpp_lasso._gramops_loops",
226
298
  sources=[GEN_REL + "/gramops_loops/gramops_capi.cpp"],
227
299
  include_dirs=[GEN_REL + "/gramops_loops", GEN_REL],
228
- extra_compile_args=_cls_flags,
300
+ extra_compile_args=_cls_flags + _omp,
301
+ extra_link_args=_omp,
229
302
  )
230
303
 
231
304
  _cuda = cuda_home()
@@ -245,7 +318,7 @@ if _cuda is not None:
245
318
  include_dirs=[GEN_REL + "/nmf_cublas", GEN_REL, str(_cuda / "include")],
246
319
  library_dirs=[str(_cuda / "lib64"), str(_cuda / "lib")],
247
320
  libraries=["cublas", "cudart"],
248
- extra_compile_args=CXXFLAGS + ["-O3", "-fstrict-overflow"],
321
+ extra_compile_args=_cls_flags,
249
322
  ))
250
323
 
251
324
  extensions.append(Extension(
@@ -254,7 +327,7 @@ if _cuda is not None:
254
327
  include_dirs=[GEN_REL + "/accel_cublas", GEN_REL, str(_cuda / "include")],
255
328
  library_dirs=[str(_cuda / "lib64"), str(_cuda / "lib")],
256
329
  libraries=["cublas", "cudart"],
257
- extra_compile_args=CXXFLAGS + ["-O3", "-fstrict-overflow"],
330
+ extra_compile_args=_cls_flags,
258
331
  ))
259
332
 
260
333
  # A wheel is tagged py3-none-<platform>: the extensions are shared