scm2cpp-lasso 0.5.3__tar.gz → 0.7.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 (20) hide show
  1. {scm2cpp_lasso-0.5.3 → scm2cpp_lasso-0.7.0}/PKG-INFO +92 -16
  2. {scm2cpp_lasso-0.5.3 → scm2cpp_lasso-0.7.0}/README.md +91 -15
  3. {scm2cpp_lasso-0.5.3 → scm2cpp_lasso-0.7.0}/pyproject.toml +1 -1
  4. {scm2cpp_lasso-0.5.3 → scm2cpp_lasso-0.7.0}/src/scm2cpp_lasso/__init__.py +344 -64
  5. {scm2cpp_lasso-0.5.3 → scm2cpp_lasso-0.7.0}/src/scm2cpp_lasso/_generated/_loader.py +8 -26
  6. {scm2cpp_lasso-0.5.3 → scm2cpp_lasso-0.7.0}/src/scm2cpp_lasso/_generated/lasso_cov.cpp +0 -2
  7. {scm2cpp_lasso-0.5.3 → scm2cpp_lasso-0.7.0}/src/scm2cpp_lasso/_generated/lasso_cov.hpp +24 -45
  8. {scm2cpp_lasso-0.5.3 → scm2cpp_lasso-0.7.0}/src/scm2cpp_lasso/_generated/lasso_cov_capi.cpp +3 -9
  9. {scm2cpp_lasso-0.5.3 → scm2cpp_lasso-0.7.0}/src/scm2cpp_lasso/_libfind.py +11 -0
  10. scm2cpp_lasso-0.7.0/src/scm2cpp_lasso/batch_capi.cu +428 -0
  11. {scm2cpp_lasso-0.5.3 → scm2cpp_lasso-0.7.0}/src/scm2cpp_lasso.egg-info/PKG-INFO +92 -16
  12. scm2cpp_lasso-0.5.3/src/scm2cpp_lasso/batch_capi.cu +0 -80
  13. {scm2cpp_lasso-0.5.3 → scm2cpp_lasso-0.7.0}/setup.cfg +0 -0
  14. {scm2cpp_lasso-0.5.3 → scm2cpp_lasso-0.7.0}/setup.py +0 -0
  15. {scm2cpp_lasso-0.5.3 → scm2cpp_lasso-0.7.0}/src/scm2cpp_lasso/_generated/__init__.py +0 -0
  16. {scm2cpp_lasso-0.5.3 → scm2cpp_lasso-0.7.0}/src/scm2cpp_lasso/_generated/scm2cpp.hpp +0 -0
  17. {scm2cpp_lasso-0.5.3 → scm2cpp_lasso-0.7.0}/src/scm2cpp_lasso.egg-info/SOURCES.txt +0 -0
  18. {scm2cpp_lasso-0.5.3 → scm2cpp_lasso-0.7.0}/src/scm2cpp_lasso.egg-info/dependency_links.txt +0 -0
  19. {scm2cpp_lasso-0.5.3 → scm2cpp_lasso-0.7.0}/src/scm2cpp_lasso.egg-info/requires.txt +0 -0
  20. {scm2cpp_lasso-0.5.3 → scm2cpp_lasso-0.7.0}/src/scm2cpp_lasso.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: scm2cpp-lasso
3
- Version: 0.5.3
3
+ Version: 0.7.0
4
4
  Summary: Lasso by covariance-update coordinate descent, translated from Scheme to C++ by scm2cpp; GPU batch solver included
5
5
  Author: Hirotaka Niitsuma
6
6
  License: MIT
@@ -114,18 +114,89 @@ celer and skglm are built for very large sparse designs, where their
114
114
  screening rules dominate; at this size they pay their setup per fit.
115
115
  cuML parallelises *within* one fit, which wins when a fit is large; at
116
116
  this size its launch overhead dominates, while our GPU row
117
- parallelises *across* lambdas -- one CUDA thread per penalty, the axis
118
- a cross-validation grid actually offers.
119
-
120
- `CovLassoCV` is scikit-learn's `LassoCV` over this machinery: the same
121
- grid construction, the same contiguous folds, the same
122
- minimum-mean-MSE choice -- and where the two disagree it is by one
123
- grid step on a near-tie, which sklearn also resolves our way once its
124
- tolerance is tightened. A fold's training Gram is a subtraction
125
- (G - Xf'Xf; the Gram is additive over rows), and on the GPU every fold
126
- and every alpha is one thread of a single launch. At cv=10 with 1000
127
- alphas -- ten thousand independent problems -- that launch takes
128
- 1.2 s against sklearn's 1.7 s, with GPU and CPU agreeing to 1e-14.
117
+ parallelises *across* lambdas -- one block of CUDA threads per
118
+ penalty, the axis a cross-validation grid actually offers.
119
+
120
+ `CovLassoCV` is scikit-learn's `LassoCV` over this machinery -- same
121
+ grid construction, same contiguous folds, same minimum-mean-MSE choice
122
+ -- with two structural savings. A fold's training Gram is a
123
+ *subtraction* (the Gram is additive over rows, so G - Xf'Xf costs one
124
+ fold's work where rebuilding costs four folds'), and once the Grams
125
+ exist nothing downstream ever touches the n rows again -- which is why
126
+ the gap grows with n. With a GPU the whole grid is one launch: one
127
+ *block* of threads per (fold, alpha) cell, the fold's Gram read in
128
+ place (cv Gram matrices on the device, not cv x 100), thread 0 of the
129
+ block doing the coordinate step and the block spreading the O(p)
130
+ update of the correlations. cv x 100 = 500 cells are too few
131
+ problems to fill a device one thread each, and a block per problem is
132
+ what turns that count into enough parallel work; `bench/cv-grid-designs.py`
133
+ times the alternatives (one thread per cell, warm runs of several
134
+ alphas per thread or block) side by side, and the block per cell wins
135
+ at every size tried. The GPU is the default when a device answers
136
+ (`force_cpu` takes the warm CPU path). Measured at cv=5, 100 alphas,
137
+ one CPU core, sklearn 1.9.0, best of three runs, whole estimator
138
+ including the Grams:
139
+
140
+ | n | p | CPU | CUDA | sklearn `LassoCV` |
141
+ |---------|------|--------|--------|-------------------|
142
+ | 1,800 | 200 | 0.36 s | 0.03 s | 0.23 s |
143
+ | 5,000 | 1000 | 3.9 s | 0.39 s | 2.7 s |
144
+ | 100,000 | 200 | 0.75 s | 0.60 s | 3.9 s |
145
+ | 100,000 | 500 | 2.0 s | 1.8 s | 12.6 s |
146
+
147
+ (This table was taken on a machine sharing its cores with other jobs,
148
+ load 14 on 20 threads, so its absolute times run some 2-3x above the
149
+ tables earlier in this README; the ratios are the point.) The grid
150
+ launch itself takes 9 ms at n=1,800 and 64 ms at n=5,000, p=1000; at
151
+ n=100,000 the CUDA column is nearly all Gram products, which the CPU
152
+ path pays as well. Large n is where the machinery pays either way:
153
+ nothing downstream of the Grams touches the 100,000 rows again. At
154
+ n=5,000, p=1000 sklearn's raw-X descent beats our Gram-heavy CPU path,
155
+ and the GPU makes up the difference seven times over. CPU and CUDA
156
+ choose the same alpha everywhere and agree to 1e-14; against sklearn
157
+ the pick is exact at n=5,000 and one grid step apart on a near-tie
158
+ elsewhere (mean-MSE relative gap at most 2.6e-4), and sklearn at
159
+ tol=1e-10 picks our alpha at every size.
160
+
161
+ `CovMultiTaskLasso` and `CovMultiTaskLassoCV` are the multi-task
162
+ family over the same machinery: scikit-learn's `MultiTaskLasso`,
163
+ `MultiTaskElasticNet` (through `l1_ratio`), `MultiTaskLassoCV` and
164
+ `MultiTaskElasticNetCV`. The penalty ties each feature's row of W
165
+ together across tasks, so the coordinate update becomes a block soft
166
+ threshold on the row's L2 norm; C = X'Y - GW is maintained per task
167
+ exactly as the single-task C is, and nothing downstream of the Grams
168
+ touches the n rows -- the kernel is the same translated Scheme
169
+ (`mt-descend`). Folds are independent and both the descent (ctypes)
170
+ and the products (BLAS) release the GIL, so `n_jobs` threads scale the
171
+ CV across folds; the default is sequential, as scikit-learn's
172
+ `n_jobs=None` is, and the benchmark pins BLAS to one thread on both
173
+ sides either way. With a GPU the CV grid is one launch exactly as
174
+ `CovLassoCV`'s is, a block per (fold, alpha) cell with the p x T
175
+ coefficient block in shared memory (`force_cpu` opts out). Same
176
+ protocol as above, 8 tasks; single fits at alpha = 0.1 lambda_max;
177
+ the CV pairs share the 100-alpha grid:
178
+
179
+ | estimator | n | p | ours | ours `n_jobs=5` | ours CUDA | sklearn |
180
+ |-----------------------|---------|-----|--------|-----------------|-----------|---------|
181
+ | MultiTaskLasso | 100,000 | 200 | 0.15 s | -- | -- | 0.80 s |
182
+ | MultiTaskLasso | 100,000 | 500 | 0.53 s | -- | -- | 1.9 s |
183
+ | MultiTaskLassoCV | 1,800 | 200 | 1.5 s | 0.33 s | 0.08 s | 0.51 s |
184
+ | MultiTaskLassoCV | 100,000 | 200 | 1.6 s | 0.62 s | 1.7 s | 76 s |
185
+ | MultiTaskElasticNetCV | 1,800 | 200 | 1.6 s | 0.51 s | 0.10 s | 1.0 s |
186
+ | MultiTaskElasticNetCV | 100,000 | 200 | 1.6 s | 0.68 s | 1.9 s | 164 s |
187
+
188
+ (`MultiTaskElasticNet` single fits time the same as `MultiTaskLasso`'s.
189
+ The CUDA column was timed on the busier day of the `CovLassoCV` table
190
+ above, when the sequential CPU rows read 1.5 / 2.0 / 1.6 / 2.1 s and
191
+ sklearn's 0.56 / 114 / 0.99 / 241 s.) At n=1,800 our sequential CV
192
+ is slower than sklearn's: the default tol=1e-8 runs more sweeps than
193
+ sklearn's 1e-4 dual-gap stop, and at that size there are no rows to
194
+ save -- and the GPU grid takes the whole thing in a tenth of a second.
195
+ At n=100,000 the Gram route is 48x and 100x ahead, essentially
196
+ unchanged from n=1,800 in absolute time; the CUDA column there is
197
+ the Gram products, the same ones the CPU path forms. Coefficients agree with scikit-learn's to 1e-15 at tight
198
+ tolerance, and the CV pair picks the same alpha with coefficients to
199
+ 1e-13.
129
200
 
130
201
  ## Which method
131
202
 
@@ -192,9 +263,14 @@ groups it reduces exactly to the lasso -- verified against sklearn to
192
263
 
193
264
  `bootstrap` draws pairs-bootstrap resamples and refits them all at
194
265
  one lambda. Each resample's Gram matrix is `X' diag(m) X` for its
195
- multiplicity counts `m` -- one BLAS product -- and because the
196
- problems are independent, the descents run as one batch: on the GPU,
197
- one thread per resample, each reading its own Gram matrix.
266
+ multiplicity counts `m`, written as `Xs' Xs` with `Xs = diag(sqrt m) X`
267
+ so that numpy hands it to the symmetric rank-k product (`dsyrk`,
268
+ half the flops of `X' (diag(m) X)`) -- one BLAS call -- and because
269
+ the problems are independent, the descents run as one batch: on the
270
+ GPU, one block of threads per resample, each reading its own Gram
271
+ matrix. The products are the cost: at n=5000, p=1000 a hundred
272
+ resamples took 18.1 s as general products and 11.2 s as `dsyrk`,
273
+ descents included.
198
274
 
199
275
  ```python
200
276
  betas = model.bootstrap(lam, n_boot=500, seed=0) # (500, p)
@@ -96,18 +96,89 @@ celer and skglm are built for very large sparse designs, where their
96
96
  screening rules dominate; at this size they pay their setup per fit.
97
97
  cuML parallelises *within* one fit, which wins when a fit is large; at
98
98
  this size its launch overhead dominates, while our GPU row
99
- parallelises *across* lambdas -- one CUDA thread per penalty, the axis
100
- a cross-validation grid actually offers.
101
-
102
- `CovLassoCV` is scikit-learn's `LassoCV` over this machinery: the same
103
- grid construction, the same contiguous folds, the same
104
- minimum-mean-MSE choice -- and where the two disagree it is by one
105
- grid step on a near-tie, which sklearn also resolves our way once its
106
- tolerance is tightened. A fold's training Gram is a subtraction
107
- (G - Xf'Xf; the Gram is additive over rows), and on the GPU every fold
108
- and every alpha is one thread of a single launch. At cv=10 with 1000
109
- alphas -- ten thousand independent problems -- that launch takes
110
- 1.2 s against sklearn's 1.7 s, with GPU and CPU agreeing to 1e-14.
99
+ parallelises *across* lambdas -- one block of CUDA threads per
100
+ penalty, the axis a cross-validation grid actually offers.
101
+
102
+ `CovLassoCV` is scikit-learn's `LassoCV` over this machinery -- same
103
+ grid construction, same contiguous folds, same minimum-mean-MSE choice
104
+ -- with two structural savings. A fold's training Gram is a
105
+ *subtraction* (the Gram is additive over rows, so G - Xf'Xf costs one
106
+ fold's work where rebuilding costs four folds'), and once the Grams
107
+ exist nothing downstream ever touches the n rows again -- which is why
108
+ the gap grows with n. With a GPU the whole grid is one launch: one
109
+ *block* of threads per (fold, alpha) cell, the fold's Gram read in
110
+ place (cv Gram matrices on the device, not cv x 100), thread 0 of the
111
+ block doing the coordinate step and the block spreading the O(p)
112
+ update of the correlations. cv x 100 = 500 cells are too few
113
+ problems to fill a device one thread each, and a block per problem is
114
+ what turns that count into enough parallel work; `bench/cv-grid-designs.py`
115
+ times the alternatives (one thread per cell, warm runs of several
116
+ alphas per thread or block) side by side, and the block per cell wins
117
+ at every size tried. The GPU is the default when a device answers
118
+ (`force_cpu` takes the warm CPU path). Measured at cv=5, 100 alphas,
119
+ one CPU core, sklearn 1.9.0, best of three runs, whole estimator
120
+ including the Grams:
121
+
122
+ | n | p | CPU | CUDA | sklearn `LassoCV` |
123
+ |---------|------|--------|--------|-------------------|
124
+ | 1,800 | 200 | 0.36 s | 0.03 s | 0.23 s |
125
+ | 5,000 | 1000 | 3.9 s | 0.39 s | 2.7 s |
126
+ | 100,000 | 200 | 0.75 s | 0.60 s | 3.9 s |
127
+ | 100,000 | 500 | 2.0 s | 1.8 s | 12.6 s |
128
+
129
+ (This table was taken on a machine sharing its cores with other jobs,
130
+ load 14 on 20 threads, so its absolute times run some 2-3x above the
131
+ tables earlier in this README; the ratios are the point.) The grid
132
+ launch itself takes 9 ms at n=1,800 and 64 ms at n=5,000, p=1000; at
133
+ n=100,000 the CUDA column is nearly all Gram products, which the CPU
134
+ path pays as well. Large n is where the machinery pays either way:
135
+ nothing downstream of the Grams touches the 100,000 rows again. At
136
+ n=5,000, p=1000 sklearn's raw-X descent beats our Gram-heavy CPU path,
137
+ and the GPU makes up the difference seven times over. CPU and CUDA
138
+ choose the same alpha everywhere and agree to 1e-14; against sklearn
139
+ the pick is exact at n=5,000 and one grid step apart on a near-tie
140
+ elsewhere (mean-MSE relative gap at most 2.6e-4), and sklearn at
141
+ tol=1e-10 picks our alpha at every size.
142
+
143
+ `CovMultiTaskLasso` and `CovMultiTaskLassoCV` are the multi-task
144
+ family over the same machinery: scikit-learn's `MultiTaskLasso`,
145
+ `MultiTaskElasticNet` (through `l1_ratio`), `MultiTaskLassoCV` and
146
+ `MultiTaskElasticNetCV`. The penalty ties each feature's row of W
147
+ together across tasks, so the coordinate update becomes a block soft
148
+ threshold on the row's L2 norm; C = X'Y - GW is maintained per task
149
+ exactly as the single-task C is, and nothing downstream of the Grams
150
+ touches the n rows -- the kernel is the same translated Scheme
151
+ (`mt-descend`). Folds are independent and both the descent (ctypes)
152
+ and the products (BLAS) release the GIL, so `n_jobs` threads scale the
153
+ CV across folds; the default is sequential, as scikit-learn's
154
+ `n_jobs=None` is, and the benchmark pins BLAS to one thread on both
155
+ sides either way. With a GPU the CV grid is one launch exactly as
156
+ `CovLassoCV`'s is, a block per (fold, alpha) cell with the p x T
157
+ coefficient block in shared memory (`force_cpu` opts out). Same
158
+ protocol as above, 8 tasks; single fits at alpha = 0.1 lambda_max;
159
+ the CV pairs share the 100-alpha grid:
160
+
161
+ | estimator | n | p | ours | ours `n_jobs=5` | ours CUDA | sklearn |
162
+ |-----------------------|---------|-----|--------|-----------------|-----------|---------|
163
+ | MultiTaskLasso | 100,000 | 200 | 0.15 s | -- | -- | 0.80 s |
164
+ | MultiTaskLasso | 100,000 | 500 | 0.53 s | -- | -- | 1.9 s |
165
+ | MultiTaskLassoCV | 1,800 | 200 | 1.5 s | 0.33 s | 0.08 s | 0.51 s |
166
+ | MultiTaskLassoCV | 100,000 | 200 | 1.6 s | 0.62 s | 1.7 s | 76 s |
167
+ | MultiTaskElasticNetCV | 1,800 | 200 | 1.6 s | 0.51 s | 0.10 s | 1.0 s |
168
+ | MultiTaskElasticNetCV | 100,000 | 200 | 1.6 s | 0.68 s | 1.9 s | 164 s |
169
+
170
+ (`MultiTaskElasticNet` single fits time the same as `MultiTaskLasso`'s.
171
+ The CUDA column was timed on the busier day of the `CovLassoCV` table
172
+ above, when the sequential CPU rows read 1.5 / 2.0 / 1.6 / 2.1 s and
173
+ sklearn's 0.56 / 114 / 0.99 / 241 s.) At n=1,800 our sequential CV
174
+ is slower than sklearn's: the default tol=1e-8 runs more sweeps than
175
+ sklearn's 1e-4 dual-gap stop, and at that size there are no rows to
176
+ save -- and the GPU grid takes the whole thing in a tenth of a second.
177
+ At n=100,000 the Gram route is 48x and 100x ahead, essentially
178
+ unchanged from n=1,800 in absolute time; the CUDA column there is
179
+ the Gram products, the same ones the CPU path forms. Coefficients agree with scikit-learn's to 1e-15 at tight
180
+ tolerance, and the CV pair picks the same alpha with coefficients to
181
+ 1e-13.
111
182
 
112
183
  ## Which method
113
184
 
@@ -174,9 +245,14 @@ groups it reduces exactly to the lasso -- verified against sklearn to
174
245
 
175
246
  `bootstrap` draws pairs-bootstrap resamples and refits them all at
176
247
  one lambda. Each resample's Gram matrix is `X' diag(m) X` for its
177
- multiplicity counts `m` -- one BLAS product -- and because the
178
- problems are independent, the descents run as one batch: on the GPU,
179
- one thread per resample, each reading its own Gram matrix.
248
+ multiplicity counts `m`, written as `Xs' Xs` with `Xs = diag(sqrt m) X`
249
+ so that numpy hands it to the symmetric rank-k product (`dsyrk`,
250
+ half the flops of `X' (diag(m) X)`) -- one BLAS call -- and because
251
+ the problems are independent, the descents run as one batch: on the
252
+ GPU, one block of threads per resample, each reading its own Gram
253
+ matrix. The products are the cost: at n=5000, p=1000 a hundred
254
+ resamples took 18.1 s as general products and 11.2 s as `dsyrk`,
255
+ descents included.
180
256
 
181
257
  ```python
182
258
  betas = model.bootstrap(lam, n_boot=500, seed=0) # (500, p)
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "scm2cpp-lasso"
7
- version = "0.5.3"
7
+ version = "0.7.0"
8
8
  description = "Lasso by covariance-update coordinate descent, translated from Scheme to C++ by scm2cpp; GPU batch solver included"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.8"