scm2cpp-lasso 0.5.3.1__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.1 → scm2cpp_lasso-0.7.0}/PKG-INFO +85 -23
  2. {scm2cpp_lasso-0.5.3.1 → scm2cpp_lasso-0.7.0}/README.md +84 -22
  3. {scm2cpp_lasso-0.5.3.1 → scm2cpp_lasso-0.7.0}/pyproject.toml +1 -1
  4. {scm2cpp_lasso-0.5.3.1 → scm2cpp_lasso-0.7.0}/src/scm2cpp_lasso/__init__.py +344 -70
  5. {scm2cpp_lasso-0.5.3.1 → scm2cpp_lasso-0.7.0}/src/scm2cpp_lasso/_generated/_loader.py +8 -26
  6. {scm2cpp_lasso-0.5.3.1 → scm2cpp_lasso-0.7.0}/src/scm2cpp_lasso/_generated/lasso_cov.cpp +0 -2
  7. {scm2cpp_lasso-0.5.3.1 → scm2cpp_lasso-0.7.0}/src/scm2cpp_lasso/_generated/lasso_cov.hpp +24 -45
  8. {scm2cpp_lasso-0.5.3.1 → scm2cpp_lasso-0.7.0}/src/scm2cpp_lasso/_generated/lasso_cov_capi.cpp +3 -9
  9. {scm2cpp_lasso-0.5.3.1 → 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.1 → scm2cpp_lasso-0.7.0}/src/scm2cpp_lasso.egg-info/PKG-INFO +85 -23
  12. scm2cpp_lasso-0.5.3.1/src/scm2cpp_lasso/batch_capi.cu +0 -80
  13. {scm2cpp_lasso-0.5.3.1 → scm2cpp_lasso-0.7.0}/setup.cfg +0 -0
  14. {scm2cpp_lasso-0.5.3.1 → scm2cpp_lasso-0.7.0}/setup.py +0 -0
  15. {scm2cpp_lasso-0.5.3.1 → scm2cpp_lasso-0.7.0}/src/scm2cpp_lasso/_generated/__init__.py +0 -0
  16. {scm2cpp_lasso-0.5.3.1 → scm2cpp_lasso-0.7.0}/src/scm2cpp_lasso/_generated/scm2cpp.hpp +0 -0
  17. {scm2cpp_lasso-0.5.3.1 → scm2cpp_lasso-0.7.0}/src/scm2cpp_lasso.egg-info/SOURCES.txt +0 -0
  18. {scm2cpp_lasso-0.5.3.1 → scm2cpp_lasso-0.7.0}/src/scm2cpp_lasso.egg-info/dependency_links.txt +0 -0
  19. {scm2cpp_lasso-0.5.3.1 → scm2cpp_lasso-0.7.0}/src/scm2cpp_lasso.egg-info/requires.txt +0 -0
  20. {scm2cpp_lasso-0.5.3.1 → 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.1
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,8 +114,8 @@ 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.
117
+ parallelises *across* lambdas -- one block of CUDA threads per
118
+ penalty, the axis a cross-validation grid actually offers.
119
119
 
120
120
  `CovLassoCV` is scikit-learn's `LassoCV` over this machinery -- same
121
121
  grid construction, same contiguous folds, same minimum-mean-MSE choice
@@ -123,23 +123,80 @@ grid construction, same contiguous folds, same minimum-mean-MSE choice
123
123
  *subtraction* (the Gram is additive over rows, so G - Xf'Xf costs one
124
124
  fold's work where rebuilding costs four folds'), and once the Grams
125
125
  exist nothing downstream ever touches the n rows again -- which is why
126
- the gap grows with n. On the GPU every fold and every alpha can run
127
- as one thread of a single launch; that launch replicates each fold's
128
- Gram per alpha, so it is chosen only while the replication stays under
129
- 512 MB, the CPU warm path taking over at large p. Measured at cv=5,
130
- 100 alphas, one CPU core:
131
-
132
- | n | p | `CovLassoCV` | sklearn `LassoCV` |
133
- |---------|------|--------------|-------------------|
134
- | 1,800 | 200 | 0.23 s | 0.20 s |
135
- | 5,000 | 1000 | 1.1 s | 3.4 s |
136
- | 100,000 | 200 | 0.44 s | 5.7 s |
137
- | 100,000 | 500 | 0.90 s | 8.0 s |
138
-
139
- At n=100,000 the chosen alpha is identical and the coefficients agree
140
- to 7e-7; at small n a near-tie can leave the picks one grid step
141
- apart, which sklearn resolves our way once its tolerance is
142
- tightened. GPU and CPU agree to 1e-14 wherever both run.
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.
143
200
 
144
201
  ## Which method
145
202
 
@@ -206,9 +263,14 @@ groups it reduces exactly to the lasso -- verified against sklearn to
206
263
 
207
264
  `bootstrap` draws pairs-bootstrap resamples and refits them all at
208
265
  one lambda. Each resample's Gram matrix is `X' diag(m) X` for its
209
- multiplicity counts `m` -- one BLAS product -- and because the
210
- problems are independent, the descents run as one batch: on the GPU,
211
- 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.
212
274
 
213
275
  ```python
214
276
  betas = model.bootstrap(lam, n_boot=500, seed=0) # (500, p)
@@ -96,8 +96,8 @@ 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.
99
+ parallelises *across* lambdas -- one block of CUDA threads per
100
+ penalty, the axis a cross-validation grid actually offers.
101
101
 
102
102
  `CovLassoCV` is scikit-learn's `LassoCV` over this machinery -- same
103
103
  grid construction, same contiguous folds, same minimum-mean-MSE choice
@@ -105,23 +105,80 @@ grid construction, same contiguous folds, same minimum-mean-MSE choice
105
105
  *subtraction* (the Gram is additive over rows, so G - Xf'Xf costs one
106
106
  fold's work where rebuilding costs four folds'), and once the Grams
107
107
  exist nothing downstream ever touches the n rows again -- which is why
108
- the gap grows with n. On the GPU every fold and every alpha can run
109
- as one thread of a single launch; that launch replicates each fold's
110
- Gram per alpha, so it is chosen only while the replication stays under
111
- 512 MB, the CPU warm path taking over at large p. Measured at cv=5,
112
- 100 alphas, one CPU core:
113
-
114
- | n | p | `CovLassoCV` | sklearn `LassoCV` |
115
- |---------|------|--------------|-------------------|
116
- | 1,800 | 200 | 0.23 s | 0.20 s |
117
- | 5,000 | 1000 | 1.1 s | 3.4 s |
118
- | 100,000 | 200 | 0.44 s | 5.7 s |
119
- | 100,000 | 500 | 0.90 s | 8.0 s |
120
-
121
- At n=100,000 the chosen alpha is identical and the coefficients agree
122
- to 7e-7; at small n a near-tie can leave the picks one grid step
123
- apart, which sklearn resolves our way once its tolerance is
124
- tightened. GPU and CPU agree to 1e-14 wherever both run.
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.
125
182
 
126
183
  ## Which method
127
184
 
@@ -188,9 +245,14 @@ groups it reduces exactly to the lasso -- verified against sklearn to
188
245
 
189
246
  `bootstrap` draws pairs-bootstrap resamples and refits them all at
190
247
  one lambda. Each resample's Gram matrix is `X' diag(m) X` for its
191
- multiplicity counts `m` -- one BLAS product -- and because the
192
- problems are independent, the descents run as one batch: on the GPU,
193
- 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.
194
256
 
195
257
  ```python
196
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.1"
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"