scm2cpp-lasso 0.15.1__tar.gz → 0.17.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.1/src/scm2cpp_lasso.egg-info → scm2cpp_lasso-0.17.0}/PKG-INFO +147 -19
  2. scm2cpp_lasso-0.15.1/PKG-INFO → scm2cpp_lasso-0.17.0/README.md +145 -35
  3. {scm2cpp_lasso-0.15.1 → scm2cpp_lasso-0.17.0}/pyproject.toml +2 -2
  4. {scm2cpp_lasso-0.15.1 → scm2cpp_lasso-0.17.0}/setup.py +83 -10
  5. {scm2cpp_lasso-0.15.1 → scm2cpp_lasso-0.17.0}/src/scm2cpp_lasso/__init__.py +512 -23
  6. {scm2cpp_lasso-0.15.1 → scm2cpp_lasso-0.17.0}/src/scm2cpp_lasso/_generated/_loader_accel_cublas.py +19 -0
  7. {scm2cpp_lasso-0.15.1 → scm2cpp_lasso-0.17.0}/src/scm2cpp_lasso/_generated/_loader_classifiers.py +27 -0
  8. {scm2cpp_lasso-0.15.1 → scm2cpp_lasso-0.17.0}/src/scm2cpp_lasso/_generated/_loader_gpu_cuda.py +19 -0
  9. {scm2cpp_lasso-0.15.1 → scm2cpp_lasso-0.17.0}/src/scm2cpp_lasso/_generated/_loader_gramops_blas.py +157 -0
  10. {scm2cpp_lasso-0.15.1 → scm2cpp_lasso-0.17.0}/src/scm2cpp_lasso/_generated/_loader_gramops_loops.py +176 -0
  11. {scm2cpp_lasso-0.15.1 → scm2cpp_lasso-0.17.0}/src/scm2cpp_lasso/_generated/accel_cublas/accel.cpp +2 -0
  12. {scm2cpp_lasso-0.15.1 → scm2cpp_lasso-0.17.0}/src/scm2cpp_lasso/_generated/accel_cublas/accel.hpp +103 -17
  13. {scm2cpp_lasso-0.15.1 → scm2cpp_lasso-0.17.0}/src/scm2cpp_lasso/_generated/accel_cublas/accel_capi.cpp +6 -0
  14. {scm2cpp_lasso-0.15.1 → scm2cpp_lasso-0.17.0}/src/scm2cpp_lasso/_generated/classifiers.cpp +3 -0
  15. {scm2cpp_lasso-0.15.1 → scm2cpp_lasso-0.17.0}/src/scm2cpp_lasso/_generated/classifiers.hpp +1284 -364
  16. {scm2cpp_lasso-0.15.1 → scm2cpp_lasso-0.17.0}/src/scm2cpp_lasso/_generated/classifiers_capi.cpp +9 -0
  17. {scm2cpp_lasso-0.15.1 → scm2cpp_lasso-0.17.0}/src/scm2cpp_lasso/_generated/gpu_cuda/gpu.cpp +2 -0
  18. {scm2cpp_lasso-0.15.1 → scm2cpp_lasso-0.17.0}/src/scm2cpp_lasso/_generated/gpu_cuda/gpu.hpp +377 -246
  19. {scm2cpp_lasso-0.15.1 → scm2cpp_lasso-0.17.0}/src/scm2cpp_lasso/_generated/gpu_cuda/gpu_capi.cpp +19 -0
  20. {scm2cpp_lasso-0.15.1/src/scm2cpp_lasso/_generated/gramops_loops → scm2cpp_lasso-0.17.0/src/scm2cpp_lasso/_generated/gramops_blas}/gramops.cpp +20 -0
  21. scm2cpp_lasso-0.17.0/src/scm2cpp_lasso/_generated/gramops_blas/gramops.hpp +5219 -0
  22. {scm2cpp_lasso-0.15.1/src/scm2cpp_lasso/_generated/gramops_loops → scm2cpp_lasso-0.17.0/src/scm2cpp_lasso/_generated/gramops_blas}/gramops_capi.cpp +54 -0
  23. {scm2cpp_lasso-0.15.1/src/scm2cpp_lasso/_generated/gramops_blas → scm2cpp_lasso-0.17.0/src/scm2cpp_lasso/_generated/gramops_loops}/gramops.cpp +20 -0
  24. scm2cpp_lasso-0.17.0/src/scm2cpp_lasso/_generated/gramops_loops/gramops.hpp +6130 -0
  25. {scm2cpp_lasso-0.15.1/src/scm2cpp_lasso/_generated/gramops_blas → scm2cpp_lasso-0.17.0/src/scm2cpp_lasso/_generated/gramops_loops}/gramops_capi.cpp +60 -0
  26. {scm2cpp_lasso-0.15.1 → scm2cpp_lasso-0.17.0}/src/scm2cpp_lasso/_generated/scm2cpp-blas.hpp +72 -0
  27. scm2cpp_lasso-0.15.1/README.md → scm2cpp_lasso-0.17.0/src/scm2cpp_lasso.egg-info/PKG-INFO +163 -17
  28. scm2cpp_lasso-0.15.1/src/scm2cpp_lasso/_generated/gramops_blas/gramops.hpp +0 -3033
  29. scm2cpp_lasso-0.15.1/src/scm2cpp_lasso/_generated/gramops_loops/gramops.hpp +0 -3302
  30. {scm2cpp_lasso-0.15.1 → scm2cpp_lasso-0.17.0}/setup.cfg +0 -0
  31. {scm2cpp_lasso-0.15.1 → scm2cpp_lasso-0.17.0}/src/scm2cpp_lasso/_compat.py +0 -0
  32. {scm2cpp_lasso-0.15.1 → scm2cpp_lasso-0.17.0}/src/scm2cpp_lasso/_generated/__init__.py +0 -0
  33. {scm2cpp_lasso-0.15.1 → scm2cpp_lasso-0.17.0}/src/scm2cpp_lasso/_generated/_loader.py +0 -0
  34. {scm2cpp_lasso-0.15.1 → scm2cpp_lasso-0.17.0}/src/scm2cpp_lasso/_generated/_loader_derived_blas.py +0 -0
  35. {scm2cpp_lasso-0.15.1 → scm2cpp_lasso-0.17.0}/src/scm2cpp_lasso/_generated/_loader_derived_cublas.py +0 -0
  36. {scm2cpp_lasso-0.15.1 → scm2cpp_lasso-0.17.0}/src/scm2cpp_lasso/_generated/_loader_derived_loops.py +0 -0
  37. {scm2cpp_lasso-0.15.1 → scm2cpp_lasso-0.17.0}/src/scm2cpp_lasso/_generated/_loader_nmf_blas.py +0 -0
  38. {scm2cpp_lasso-0.15.1 → scm2cpp_lasso-0.17.0}/src/scm2cpp_lasso/_generated/_loader_nmf_cublas.py +0 -0
  39. {scm2cpp_lasso-0.15.1 → scm2cpp_lasso-0.17.0}/src/scm2cpp_lasso/_generated/_loader_nmf_loops.py +0 -0
  40. {scm2cpp_lasso-0.15.1 → scm2cpp_lasso-0.17.0}/src/scm2cpp_lasso/_generated/derived_blas/lasso_design.cpp +0 -0
  41. {scm2cpp_lasso-0.15.1 → scm2cpp_lasso-0.17.0}/src/scm2cpp_lasso/_generated/derived_blas/lasso_design.hpp +0 -0
  42. {scm2cpp_lasso-0.15.1 → scm2cpp_lasso-0.17.0}/src/scm2cpp_lasso/_generated/derived_blas/lasso_design_capi.cpp +0 -0
  43. {scm2cpp_lasso-0.15.1 → scm2cpp_lasso-0.17.0}/src/scm2cpp_lasso/_generated/derived_cublas/lasso_design.cpp +0 -0
  44. {scm2cpp_lasso-0.15.1 → scm2cpp_lasso-0.17.0}/src/scm2cpp_lasso/_generated/derived_cublas/lasso_design.hpp +0 -0
  45. {scm2cpp_lasso-0.15.1 → scm2cpp_lasso-0.17.0}/src/scm2cpp_lasso/_generated/derived_cublas/lasso_design_capi.cpp +0 -0
  46. {scm2cpp_lasso-0.15.1 → scm2cpp_lasso-0.17.0}/src/scm2cpp_lasso/_generated/derived_loops/lasso_design.cpp +0 -0
  47. {scm2cpp_lasso-0.15.1 → scm2cpp_lasso-0.17.0}/src/scm2cpp_lasso/_generated/derived_loops/lasso_design.hpp +0 -0
  48. {scm2cpp_lasso-0.15.1 → scm2cpp_lasso-0.17.0}/src/scm2cpp_lasso/_generated/derived_loops/lasso_design_capi.cpp +0 -0
  49. {scm2cpp_lasso-0.15.1 → scm2cpp_lasso-0.17.0}/src/scm2cpp_lasso/_generated/lasso_cov.cpp +0 -0
  50. {scm2cpp_lasso-0.15.1 → scm2cpp_lasso-0.17.0}/src/scm2cpp_lasso/_generated/lasso_cov.hpp +0 -0
  51. {scm2cpp_lasso-0.15.1 → scm2cpp_lasso-0.17.0}/src/scm2cpp_lasso/_generated/lasso_cov_capi.cpp +0 -0
  52. {scm2cpp_lasso-0.15.1 → scm2cpp_lasso-0.17.0}/src/scm2cpp_lasso/_generated/nmf_blas/nmf_cd.cpp +0 -0
  53. {scm2cpp_lasso-0.15.1 → scm2cpp_lasso-0.17.0}/src/scm2cpp_lasso/_generated/nmf_blas/nmf_cd.hpp +0 -0
  54. {scm2cpp_lasso-0.15.1 → scm2cpp_lasso-0.17.0}/src/scm2cpp_lasso/_generated/nmf_blas/nmf_cd_capi.cpp +0 -0
  55. {scm2cpp_lasso-0.15.1 → scm2cpp_lasso-0.17.0}/src/scm2cpp_lasso/_generated/nmf_cublas/nmf_cd.cpp +0 -0
  56. {scm2cpp_lasso-0.15.1 → scm2cpp_lasso-0.17.0}/src/scm2cpp_lasso/_generated/nmf_cublas/nmf_cd.hpp +0 -0
  57. {scm2cpp_lasso-0.15.1 → scm2cpp_lasso-0.17.0}/src/scm2cpp_lasso/_generated/nmf_cublas/nmf_cd_capi.cpp +0 -0
  58. {scm2cpp_lasso-0.15.1 → scm2cpp_lasso-0.17.0}/src/scm2cpp_lasso/_generated/nmf_loops/nmf_cd.cpp +0 -0
  59. {scm2cpp_lasso-0.15.1 → scm2cpp_lasso-0.17.0}/src/scm2cpp_lasso/_generated/nmf_loops/nmf_cd.hpp +0 -0
  60. {scm2cpp_lasso-0.15.1 → scm2cpp_lasso-0.17.0}/src/scm2cpp_lasso/_generated/nmf_loops/nmf_cd_capi.cpp +0 -0
  61. {scm2cpp_lasso-0.15.1 → scm2cpp_lasso-0.17.0}/src/scm2cpp_lasso/_generated/scm2cpp-cublas.hpp +0 -0
  62. {scm2cpp_lasso-0.15.1 → scm2cpp_lasso-0.17.0}/src/scm2cpp_lasso/_generated/scm2cpp-cuda.hpp +0 -0
  63. {scm2cpp_lasso-0.15.1 → scm2cpp_lasso-0.17.0}/src/scm2cpp_lasso/_generated/scm2cpp.hpp +0 -0
  64. {scm2cpp_lasso-0.15.1 → scm2cpp_lasso-0.17.0}/src/scm2cpp_lasso/_libfind.py +0 -0
  65. {scm2cpp_lasso-0.15.1 → scm2cpp_lasso-0.17.0}/src/scm2cpp_lasso/batch_capi.cu +0 -0
  66. {scm2cpp_lasso-0.15.1 → scm2cpp_lasso-0.17.0}/src/scm2cpp_lasso.egg-info/SOURCES.txt +0 -0
  67. {scm2cpp_lasso-0.15.1 → scm2cpp_lasso-0.17.0}/src/scm2cpp_lasso.egg-info/dependency_links.txt +0 -0
  68. {scm2cpp_lasso-0.15.1 → scm2cpp_lasso-0.17.0}/src/scm2cpp_lasso.egg-info/requires.txt +0 -0
  69. {scm2cpp_lasso-0.15.1 → scm2cpp_lasso-0.17.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.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
3
+ Version: 0.17.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, LDA, 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.
@@ -191,7 +201,8 @@ package picks the fastest one present.
191
201
  |---|---|---|---|---|
192
202
  | `GramLasso` | Lasso(precompute=True), fit(X, y) with the intercept | `lasso-cov.scm` | CPU | no |
193
203
  | `GramElasticNet` | ElasticNet(precompute=True), fit(X, y) with the intercept | `lasso-cov.scm` | CPU | no |
194
- | `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 |
195
206
  | `GramMultiTaskLasso` | MultiTaskLasso, fit(X, Y) with the intercept | `lasso-cov.scm` | CPU | no |
196
207
  | `GramMultiTaskElasticNet` | MultiTaskElasticNet, fit(X, Y) with the intercept | `lasso-cov.scm` | CPU | no |
197
208
  | `GramGroupLasso` | (none; skglm GroupLasso), fit(X, y) with the intercept | `lasso-cov.scm` | CPU | - |
@@ -236,6 +247,8 @@ package picks the fastest one present.
236
247
  | `EMGaussianMixture` | GaussianMixture(covariance_type='full') | `gmm.scm` | loops / BLAS / --cuda | no |
237
248
  | `ParallelFastICA` | FastICA | `fastica.scm` | loops / BLAS / --cuda | no |
238
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 |
251
+ | `GramLDA` | LinearDiscriminantAnalysis (svd, lsqr, eigen) | `lda.scm` | loops / BLAS | no |
239
252
  | `f_regression, f_classif` | f_regression, f_classif | `fscore.scm` | loops / BLAS / cuBLAS | no |
240
253
  | `SelectKBest, SelectPercentile` | SelectKBest, SelectPercentile | `fscore.scm` | loops / BLAS / cuBLAS | no |
241
254
  | `GramRFE` | RFE(Ridge), RFE(LinearRegression) | `rfe.scm` | loops / BLAS / cuBLAS | inside: Ridge |
@@ -558,6 +571,116 @@ on the GPU: the ridge is one eigendecomposition and the elastic net's
558
571
  sweeps are the lasso's; `fit_path_batch(l1_ratio=..)` is the GPU
559
572
  grid for the elastic net.
560
573
 
574
+ ## Leave-one-out ridge: `GramRidgeCV`
575
+
576
+ `GramRidgeCV` is scikit-learn's `RidgeCV` with `cv=None`: the alpha
577
+ whose leave-one-out mean squared error is least, without a refit per
578
+ row. With the centred design Xc and H = Xc (Xc'Xc + alpha I)^-1 Xc'
579
+ (plus 1/n for the intercept), the leave-one-out residual is
580
+ (y - yhat) / (1 - diag H). One eigenproblem gives H = Z diag(1 /
581
+ (lam + alpha)) Z' for every alpha: Z = Xc V from Xc'Xc = V diag(lam) V'
582
+ when n > p (X'X by dsyrk), Z = Q diag(sqrt mu) from Xc Xc' = Q diag(mu)
583
+ Q' otherwise. Then diag H and yhat at all the alphas are two products,
584
+ (Z o Z) W and Z (R o W) with W[k, a] = 1 / (lam_k + alpha_a): the
585
+ alphas cost one dgemm, where scikit-learn loops over them in numpy
586
+ (`examples/kernel-only/ridge-gcv.scm`; the eigenproblem is LAPACK's
587
+ dsyevd in the BLAS build).
588
+
589
+ ```python
590
+ from scm2cpp_lasso import GramRidgeCV
591
+ m = GramRidgeCV(alphas=np.logspace(-2, 4, 50)).fit(X, y)
592
+ m.alpha_, m.best_score_, m.coef_, m.intercept_
593
+ ```
594
+
595
+ It takes `fit_intercept`, `gcv_mode` (`"svd"` / `"eigen"` choose the
596
+ two Zs), `store_cv_results` and `alpha_per_target`, y one target or
597
+ several; `cv` and `scoring` must stay None. The chosen alpha is
598
+ scikit-learn's, the coefficients agree to 1e-12 and `cv_results_` to
599
+ 1e-7 relative (the wide case near interpolation). Measured
600
+ (`bench/eigen-compare.py`, the MKL build, best of three):
601
+
602
+ | case | scikit-learn `RidgeCV` | `GramRidgeCV` | ratio, one core | ratio, 20 cores |
603
+ |---|---|---|---|---|
604
+ | 200,000 x 200, 3 alphas | 2.04 s | 0.80 s | 2.6x | 3.6x |
605
+ | 200,000 x 200, 50 alphas | 26.6 s | 0.96 s | 27.7x | 29.0x |
606
+ | 20,000 x 500, 50 alphas | 9.64 s | 0.33 s | 28.9x | 29.2x |
607
+ | 2,000 x 5,000, 50 alphas | 3.16 s | 1.47 s | 2.1x | 4.5x |
608
+
609
+ ## Kernel PCA: `GramKernelPCA`
610
+
611
+ `GramKernelPCA` is scikit-learn's `KernelPCA`: the kernel matrix
612
+ (linear, rbf, poly, sigmoid, cosine or precomputed), centred in feature
613
+ space, its largest eigenpairs with svd_flip's signs
614
+ (`examples/kernel-only/kernel-pca.scm`). Where the eigenpairs come from
615
+ is what differs:
616
+
617
+ - fewer than 10 components of more than 200 rows, where scikit-learn
618
+ takes ARPACK (Lanczos, one matrix-vector product at a time): subspace
619
+ iteration on c + 25 columns, so each step is one dgemm, stopped when
620
+ every residual |K u - lam u| is under 1e-12 of the largest value
621
+ (falling back to the dense route if it is not reached);
622
+ - otherwise LAPACK's dsyevr over the wanted index range, which is what
623
+ scipy's `eigh(subset_by_index=..)` runs for scikit-learn;
624
+ - the linear kernel with no more components than features, n > p:
625
+ through Xc'Xc, p x p (the eigenvectors of Xc Xc' are Xc v / sqrt(lam)),
626
+ where scikit-learn forms the n x n kernel.
627
+
628
+ ```python
629
+ from scm2cpp_lasso import GramKernelPCA
630
+ Z = GramKernelPCA(5, kernel="rbf", gamma=0.05).fit_transform(X)
631
+ ```
632
+
633
+ Eigenvalues, eigenvectors and transforms agree with scikit-learn's to
634
+ 1e-12 on every kernel; no `inverse_transform`. Measured
635
+ (`bench/eigen-compare.py`, the MKL build, best of three):
636
+
637
+ | case | scikit-learn `KernelPCA` | `GramKernelPCA` | ratio, one core | ratio, 20 cores |
638
+ |---|---|---|---|---|
639
+ | rbf, 3,000 x 20, 5 components | 1.11 s (ARPACK) | 0.35 s (subspace) | 3.2x | 3.0x |
640
+ | rbf, 6,000 x 20, 5 components | 4.63 s (ARPACK) | 1.33 s (subspace) | 3.5x | 4.2x |
641
+ | rbf, 3,000 x 20, 50 components | 3.48 s (dense) | 3.63 s (dense) | 1.0x | 1.2x |
642
+ | 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 |
643
+
644
+ The linear row is the algorithm (an n x n problem against a p x p one),
645
+ not the code.
646
+
647
+ ## Linear discriminant analysis: `GramLDA`
648
+
649
+ `GramLDA` is scikit-learn's `LinearDiscriminantAnalysis` with the n x p
650
+ work moved to the Gram matrix (`examples/kernel-only/lda.scm`): one pass
651
+ gives the class means and X'X (dsyrk), and the within-class covariance
652
+ is Sw = (X'X - sum_k n_k mu_k mu_k') / n. Each solver then runs its own
653
+ steps on p x p matrices:
654
+
655
+ - `"svd"` (the default) takes the SVD of the standardised within-class
656
+ design Xc D^-1 / sqrt(n), n x p. Its right singular vectors and values
657
+ are the eigenvectors and square roots of the eigenvalues of
658
+ D^-1 Sw D^-1, so one p x p eigenproblem (dsyevd) replaces it; the
659
+ second, classes x rank SVD and the coefficients follow as in
660
+ scikit-learn;
661
+ - `"lsqr"` solves Sw W = M' by Cholesky;
662
+ - `"eigen"` solves Sb v = lam Sw v (`sym-eigen-gen`, LAPACK's dsygvd).
663
+
664
+ ```python
665
+ from scm2cpp_lasso import GramLDA
666
+ clf = GramLDA().fit(X, y) # solver="svd"
667
+ clf.predict(X), clf.transform(X)
668
+ ```
669
+
670
+ `shrinkage` takes None or a float (it is linear in Sw, so the Gram route
671
+ keeps it), not "auto"; custom `priors` form a Gram per class. The
672
+ coefficients, intercepts, probabilities and explained variance agree
673
+ with scikit-learn's to 1e-12, the predictions exactly, and `transform`
674
+ up to the sign of each column. Measured (`bench/eigen-compare.py`, the
675
+ MKL build, best of three):
676
+
677
+ | case | scikit-learn | `GramLDA` | ratio, one core | ratio, 20 cores |
678
+ |---|---|---|---|---|
679
+ | svd (default), 200,000 x 100, 5 classes | 4.80 s | 0.092 s | 52x | 35x |
680
+ | svd, 50,000 x 500, 10 classes | 4.07 s | 0.25 s | 16x | 14x |
681
+ | lsqr, 200,000 x 100, 5 classes | 0.42 s | 0.089 s | 4.7x | 8.9x |
682
+ | eigen, 200,000 x 100, 5 classes | 0.56 s | 0.094 s | 6.0x | 12x |
683
+
561
684
  ## Bayesian ridge
562
685
 
563
686
  `CovBayesianRidge` is scikit-learn's `BayesianRidge` -- the same model,
@@ -901,25 +1024,30 @@ times the ones with a counterpart here, sklearnex's estimators
901
1024
  imported directly (not patched in) so that scikit-learn itself is
902
1025
  timed in the same process. The ratios are scikit-learn's or
903
1026
  sklearnex's time over the package's best (its CPU build, or the device
904
- build where one answers); the load average was 8 to 11, so the times
905
- move by some tens of percent.
1027
+ build where one answers). The package was built against Intel MKL
1028
+ (see the build section), its loops under OpenMP; the load average was
1029
+ about 2.3, so the times move by some percent.
906
1030
 
907
1031
  | estimator, n x p | iterations (sk / sklearnex / ours) | vs scikit-learn, one core | vs sklearnex, one core | vs sklearnex, 20 cores |
908
1032
  |---|---|---|---|---|
909
- | LogisticRegression, 200,000 x 100 | 6 / 12 / 6 | 10.6x (device) | 8.4x (device) | 4.4x (device); on the CPU 0.5x |
910
- | Ridge (alpha 1), 200,000 x 200 | - | 2.1x | 1.2x | 0.3x |
911
- | PCA (10 components), 100,000 x 50 | - | 1.5x | 2.4x | 1.7x |
912
- | PCA (10 components), 50,000 x 500 | - | 1.1x | 1.4x | 0.5x |
913
- | SVC (rbf), 5,000 x 20 | 4,858 / 7 / 4,882 | 1.2x | 1.4x | 0.5x |
914
- | SVR (rbf), 5,000 x 20 | 12,821 / 10 / 12,824 | 1.3x | 1.4x | 0.4x |
915
- | RFE(Ridge), 20 of 200 kept, 20,000 x 200 | - | 216x | 128x | 78x |
916
-
917
- On one core the package is ahead everywhere. sklearnex uses every
918
- core by default, and the package's kernels are single-threaded, so
919
- with 20 cores sklearnex is faster on Ridge, the larger PCA and the
920
- SVMs; what stays ahead is the device build (logistic regression) and
921
- an algorithm that does less work (RFE: the Gram once, the Cholesky
922
- factor downdated per drop, where scikit-learn refits). sklearnex is
1033
+ | LogisticRegression, 200,000 x 100 | 6 / 12 / 6 | 6.8x (device) | 6.4x (device) | 1.8x (device); on the CPU 0.9x |
1034
+ | Ridge (alpha 1, no intercept), 200,000 x 200 | - | 1.7x | 0.8x | 2.3x |
1035
+ | PCA (10 components), 100,000 x 50 | - | 1.4x | 2.2x | 1.4x |
1036
+ | PCA (10 components), 50,000 x 500 | - | 1.1x | 1.4x | 1.6x (device); on the CPU 1.2x |
1037
+ | SVC (rbf), 5,000 x 20 | 4,858 / 7 / 4,882 | 1.4x | 1.6x | 0.5x |
1038
+ | SVR (rbf), 5,000 x 20 | 12,821 / 10 / 12,824 | 1.4x | 1.4x | 0.4x |
1039
+ | RFE(Ridge), 20 of 200 kept, 20,000 x 200 | - | 138x | 106x | 116x |
1040
+
1041
+ On one core the package is ahead everywhere but Ridge, where both
1042
+ sides are one symmetric rank-k update of the same size and sklearnex
1043
+ is 0.180 s to 0.219 s. With 20 cores both sides use every core: the
1044
+ package's loops run under OpenMP and its products in MKL's threads,
1045
+ so it is ahead on Ridge (the Gram by dsyrk and a Cholesky solve), PCA,
1046
+ the device build of logistic regression and RFE (an algorithm that
1047
+ does less work: the Gram once, the Cholesky factor downdated per drop,
1048
+ where scikit-learn refits), level on logistic regression on the CPU,
1049
+ and behind on the SVMs: SMO takes thousands of short steps, each
1050
+ waiting on the last, where sklearnex's solver takes 7 to 10 long ones. sklearnex is
923
1051
  not scikit-learn's arithmetic: its logistic regression takes 12
924
1052
  iterations to scikit-learn's 6 and its coefficients differ from
925
1053
  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.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
- 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.
@@ -191,7 +183,8 @@ package picks the fastest one present.
191
183
  |---|---|---|---|---|
192
184
  | `GramLasso` | Lasso(precompute=True), fit(X, y) with the intercept | `lasso-cov.scm` | CPU | no |
193
185
  | `GramElasticNet` | ElasticNet(precompute=True), fit(X, y) with the intercept | `lasso-cov.scm` | CPU | no |
194
- | `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 |
195
188
  | `GramMultiTaskLasso` | MultiTaskLasso, fit(X, Y) with the intercept | `lasso-cov.scm` | CPU | no |
196
189
  | `GramMultiTaskElasticNet` | MultiTaskElasticNet, fit(X, Y) with the intercept | `lasso-cov.scm` | CPU | no |
197
190
  | `GramGroupLasso` | (none; skglm GroupLasso), fit(X, y) with the intercept | `lasso-cov.scm` | CPU | - |
@@ -236,6 +229,8 @@ package picks the fastest one present.
236
229
  | `EMGaussianMixture` | GaussianMixture(covariance_type='full') | `gmm.scm` | loops / BLAS / --cuda | no |
237
230
  | `ParallelFastICA` | FastICA | `fastica.scm` | loops / BLAS / --cuda | no |
238
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 |
233
+ | `GramLDA` | LinearDiscriminantAnalysis (svd, lsqr, eigen) | `lda.scm` | loops / BLAS | no |
239
234
  | `f_regression, f_classif` | f_regression, f_classif | `fscore.scm` | loops / BLAS / cuBLAS | no |
240
235
  | `SelectKBest, SelectPercentile` | SelectKBest, SelectPercentile | `fscore.scm` | loops / BLAS / cuBLAS | no |
241
236
  | `GramRFE` | RFE(Ridge), RFE(LinearRegression) | `rfe.scm` | loops / BLAS / cuBLAS | inside: Ridge |
@@ -558,6 +553,116 @@ on the GPU: the ridge is one eigendecomposition and the elastic net's
558
553
  sweeps are the lasso's; `fit_path_batch(l1_ratio=..)` is the GPU
559
554
  grid for the elastic net.
560
555
 
556
+ ## Leave-one-out ridge: `GramRidgeCV`
557
+
558
+ `GramRidgeCV` is scikit-learn's `RidgeCV` with `cv=None`: the alpha
559
+ whose leave-one-out mean squared error is least, without a refit per
560
+ row. With the centred design Xc and H = Xc (Xc'Xc + alpha I)^-1 Xc'
561
+ (plus 1/n for the intercept), the leave-one-out residual is
562
+ (y - yhat) / (1 - diag H). One eigenproblem gives H = Z diag(1 /
563
+ (lam + alpha)) Z' for every alpha: Z = Xc V from Xc'Xc = V diag(lam) V'
564
+ when n > p (X'X by dsyrk), Z = Q diag(sqrt mu) from Xc Xc' = Q diag(mu)
565
+ Q' otherwise. Then diag H and yhat at all the alphas are two products,
566
+ (Z o Z) W and Z (R o W) with W[k, a] = 1 / (lam_k + alpha_a): the
567
+ alphas cost one dgemm, where scikit-learn loops over them in numpy
568
+ (`examples/kernel-only/ridge-gcv.scm`; the eigenproblem is LAPACK's
569
+ dsyevd in the BLAS build).
570
+
571
+ ```python
572
+ from scm2cpp_lasso import GramRidgeCV
573
+ m = GramRidgeCV(alphas=np.logspace(-2, 4, 50)).fit(X, y)
574
+ m.alpha_, m.best_score_, m.coef_, m.intercept_
575
+ ```
576
+
577
+ It takes `fit_intercept`, `gcv_mode` (`"svd"` / `"eigen"` choose the
578
+ two Zs), `store_cv_results` and `alpha_per_target`, y one target or
579
+ several; `cv` and `scoring` must stay None. The chosen alpha is
580
+ scikit-learn's, the coefficients agree to 1e-12 and `cv_results_` to
581
+ 1e-7 relative (the wide case near interpolation). Measured
582
+ (`bench/eigen-compare.py`, the MKL build, best of three):
583
+
584
+ | case | scikit-learn `RidgeCV` | `GramRidgeCV` | ratio, one core | ratio, 20 cores |
585
+ |---|---|---|---|---|
586
+ | 200,000 x 200, 3 alphas | 2.04 s | 0.80 s | 2.6x | 3.6x |
587
+ | 200,000 x 200, 50 alphas | 26.6 s | 0.96 s | 27.7x | 29.0x |
588
+ | 20,000 x 500, 50 alphas | 9.64 s | 0.33 s | 28.9x | 29.2x |
589
+ | 2,000 x 5,000, 50 alphas | 3.16 s | 1.47 s | 2.1x | 4.5x |
590
+
591
+ ## Kernel PCA: `GramKernelPCA`
592
+
593
+ `GramKernelPCA` is scikit-learn's `KernelPCA`: the kernel matrix
594
+ (linear, rbf, poly, sigmoid, cosine or precomputed), centred in feature
595
+ space, its largest eigenpairs with svd_flip's signs
596
+ (`examples/kernel-only/kernel-pca.scm`). Where the eigenpairs come from
597
+ is what differs:
598
+
599
+ - fewer than 10 components of more than 200 rows, where scikit-learn
600
+ takes ARPACK (Lanczos, one matrix-vector product at a time): subspace
601
+ iteration on c + 25 columns, so each step is one dgemm, stopped when
602
+ every residual |K u - lam u| is under 1e-12 of the largest value
603
+ (falling back to the dense route if it is not reached);
604
+ - otherwise LAPACK's dsyevr over the wanted index range, which is what
605
+ scipy's `eigh(subset_by_index=..)` runs for scikit-learn;
606
+ - the linear kernel with no more components than features, n > p:
607
+ through Xc'Xc, p x p (the eigenvectors of Xc Xc' are Xc v / sqrt(lam)),
608
+ where scikit-learn forms the n x n kernel.
609
+
610
+ ```python
611
+ from scm2cpp_lasso import GramKernelPCA
612
+ Z = GramKernelPCA(5, kernel="rbf", gamma=0.05).fit_transform(X)
613
+ ```
614
+
615
+ Eigenvalues, eigenvectors and transforms agree with scikit-learn's to
616
+ 1e-12 on every kernel; no `inverse_transform`. Measured
617
+ (`bench/eigen-compare.py`, the MKL build, best of three):
618
+
619
+ | case | scikit-learn `KernelPCA` | `GramKernelPCA` | ratio, one core | ratio, 20 cores |
620
+ |---|---|---|---|---|
621
+ | rbf, 3,000 x 20, 5 components | 1.11 s (ARPACK) | 0.35 s (subspace) | 3.2x | 3.0x |
622
+ | rbf, 6,000 x 20, 5 components | 4.63 s (ARPACK) | 1.33 s (subspace) | 3.5x | 4.2x |
623
+ | rbf, 3,000 x 20, 50 components | 3.48 s (dense) | 3.63 s (dense) | 1.0x | 1.2x |
624
+ | 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 |
625
+
626
+ The linear row is the algorithm (an n x n problem against a p x p one),
627
+ not the code.
628
+
629
+ ## Linear discriminant analysis: `GramLDA`
630
+
631
+ `GramLDA` is scikit-learn's `LinearDiscriminantAnalysis` with the n x p
632
+ work moved to the Gram matrix (`examples/kernel-only/lda.scm`): one pass
633
+ gives the class means and X'X (dsyrk), and the within-class covariance
634
+ is Sw = (X'X - sum_k n_k mu_k mu_k') / n. Each solver then runs its own
635
+ steps on p x p matrices:
636
+
637
+ - `"svd"` (the default) takes the SVD of the standardised within-class
638
+ design Xc D^-1 / sqrt(n), n x p. Its right singular vectors and values
639
+ are the eigenvectors and square roots of the eigenvalues of
640
+ D^-1 Sw D^-1, so one p x p eigenproblem (dsyevd) replaces it; the
641
+ second, classes x rank SVD and the coefficients follow as in
642
+ scikit-learn;
643
+ - `"lsqr"` solves Sw W = M' by Cholesky;
644
+ - `"eigen"` solves Sb v = lam Sw v (`sym-eigen-gen`, LAPACK's dsygvd).
645
+
646
+ ```python
647
+ from scm2cpp_lasso import GramLDA
648
+ clf = GramLDA().fit(X, y) # solver="svd"
649
+ clf.predict(X), clf.transform(X)
650
+ ```
651
+
652
+ `shrinkage` takes None or a float (it is linear in Sw, so the Gram route
653
+ keeps it), not "auto"; custom `priors` form a Gram per class. The
654
+ coefficients, intercepts, probabilities and explained variance agree
655
+ with scikit-learn's to 1e-12, the predictions exactly, and `transform`
656
+ up to the sign of each column. Measured (`bench/eigen-compare.py`, the
657
+ MKL build, best of three):
658
+
659
+ | case | scikit-learn | `GramLDA` | ratio, one core | ratio, 20 cores |
660
+ |---|---|---|---|---|
661
+ | svd (default), 200,000 x 100, 5 classes | 4.80 s | 0.092 s | 52x | 35x |
662
+ | svd, 50,000 x 500, 10 classes | 4.07 s | 0.25 s | 16x | 14x |
663
+ | lsqr, 200,000 x 100, 5 classes | 0.42 s | 0.089 s | 4.7x | 8.9x |
664
+ | eigen, 200,000 x 100, 5 classes | 0.56 s | 0.094 s | 6.0x | 12x |
665
+
561
666
  ## Bayesian ridge
562
667
 
563
668
  `CovBayesianRidge` is scikit-learn's `BayesianRidge` -- the same model,
@@ -901,25 +1006,30 @@ times the ones with a counterpart here, sklearnex's estimators
901
1006
  imported directly (not patched in) so that scikit-learn itself is
902
1007
  timed in the same process. The ratios are scikit-learn's or
903
1008
  sklearnex's time over the package's best (its CPU build, or the device
904
- build where one answers); the load average was 8 to 11, so the times
905
- move by some tens of percent.
1009
+ build where one answers). The package was built against Intel MKL
1010
+ (see the build section), its loops under OpenMP; the load average was
1011
+ about 2.3, so the times move by some percent.
906
1012
 
907
1013
  | estimator, n x p | iterations (sk / sklearnex / ours) | vs scikit-learn, one core | vs sklearnex, one core | vs sklearnex, 20 cores |
908
1014
  |---|---|---|---|---|
909
- | LogisticRegression, 200,000 x 100 | 6 / 12 / 6 | 10.6x (device) | 8.4x (device) | 4.4x (device); on the CPU 0.5x |
910
- | Ridge (alpha 1), 200,000 x 200 | - | 2.1x | 1.2x | 0.3x |
911
- | PCA (10 components), 100,000 x 50 | - | 1.5x | 2.4x | 1.7x |
912
- | PCA (10 components), 50,000 x 500 | - | 1.1x | 1.4x | 0.5x |
913
- | SVC (rbf), 5,000 x 20 | 4,858 / 7 / 4,882 | 1.2x | 1.4x | 0.5x |
914
- | SVR (rbf), 5,000 x 20 | 12,821 / 10 / 12,824 | 1.3x | 1.4x | 0.4x |
915
- | RFE(Ridge), 20 of 200 kept, 20,000 x 200 | - | 216x | 128x | 78x |
916
-
917
- On one core the package is ahead everywhere. sklearnex uses every
918
- core by default, and the package's kernels are single-threaded, so
919
- with 20 cores sklearnex is faster on Ridge, the larger PCA and the
920
- SVMs; what stays ahead is the device build (logistic regression) and
921
- an algorithm that does less work (RFE: the Gram once, the Cholesky
922
- factor downdated per drop, where scikit-learn refits). sklearnex is
1015
+ | LogisticRegression, 200,000 x 100 | 6 / 12 / 6 | 6.8x (device) | 6.4x (device) | 1.8x (device); on the CPU 0.9x |
1016
+ | Ridge (alpha 1, no intercept), 200,000 x 200 | - | 1.7x | 0.8x | 2.3x |
1017
+ | PCA (10 components), 100,000 x 50 | - | 1.4x | 2.2x | 1.4x |
1018
+ | PCA (10 components), 50,000 x 500 | - | 1.1x | 1.4x | 1.6x (device); on the CPU 1.2x |
1019
+ | SVC (rbf), 5,000 x 20 | 4,858 / 7 / 4,882 | 1.4x | 1.6x | 0.5x |
1020
+ | SVR (rbf), 5,000 x 20 | 12,821 / 10 / 12,824 | 1.4x | 1.4x | 0.4x |
1021
+ | RFE(Ridge), 20 of 200 kept, 20,000 x 200 | - | 138x | 106x | 116x |
1022
+
1023
+ On one core the package is ahead everywhere but Ridge, where both
1024
+ sides are one symmetric rank-k update of the same size and sklearnex
1025
+ is 0.180 s to 0.219 s. With 20 cores both sides use every core: the
1026
+ package's loops run under OpenMP and its products in MKL's threads,
1027
+ so it is ahead on Ridge (the Gram by dsyrk and a Cholesky solve), PCA,
1028
+ the device build of logistic regression and RFE (an algorithm that
1029
+ does less work: the Gram once, the Cholesky factor downdated per drop,
1030
+ where scikit-learn refits), level on logistic regression on the CPU,
1031
+ and behind on the SVMs: SMO takes thousands of short steps, each
1032
+ waiting on the last, where sklearnex's solver takes 7 to 10 long ones. sklearnex is
923
1033
  not scikit-learn's arithmetic: its logistic regression takes 12
924
1034
  iterations to scikit-learn's 6 and its coefficients differ from
925
1035
  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.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"
7
+ version = "0.17.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, LDA, 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