open-lars 0.1.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.
- open_lars-0.1.0/LICENSE +201 -0
- open_lars-0.1.0/PKG-INFO +132 -0
- open_lars-0.1.0/README.md +112 -0
- open_lars-0.1.0/open_lars/__init__.py +5 -0
- open_lars-0.1.0/open_lars/__version__.py +1 -0
- open_lars-0.1.0/open_lars/functional.py +50 -0
- open_lars-0.1.0/open_lars/lars.py +128 -0
- open_lars-0.1.0/open_lars.egg-info/PKG-INFO +132 -0
- open_lars-0.1.0/open_lars.egg-info/SOURCES.txt +16 -0
- open_lars-0.1.0/open_lars.egg-info/dependency_links.txt +1 -0
- open_lars-0.1.0/open_lars.egg-info/requires.txt +4 -0
- open_lars-0.1.0/open_lars.egg-info/top_level.txt +1 -0
- open_lars-0.1.0/pyproject.toml +35 -0
- open_lars-0.1.0/setup.cfg +4 -0
- open_lars-0.1.0/setup.py +9 -0
- open_lars-0.1.0/tests/test_compute_adaptive_lr.py +104 -0
- open_lars-0.1.0/tests/test_lars.py +284 -0
- open_lars-0.1.0/tests/test_pickle.py +36 -0
open_lars-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
6
|
+
|
|
7
|
+
1. Definitions.
|
|
8
|
+
|
|
9
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
10
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
11
|
+
|
|
12
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
13
|
+
the copyright owner that is granting the License.
|
|
14
|
+
|
|
15
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
16
|
+
other entities that control, are controlled by, or are under common
|
|
17
|
+
control with that entity. For the purposes of this definition,
|
|
18
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
19
|
+
direction or management of such entity, whether by contract or
|
|
20
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
21
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
22
|
+
|
|
23
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
24
|
+
exercising permissions granted by this License.
|
|
25
|
+
|
|
26
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
27
|
+
including but not limited to software source code, documentation
|
|
28
|
+
source, and configuration files.
|
|
29
|
+
|
|
30
|
+
"Object" form shall mean any form resulting from mechanical
|
|
31
|
+
transformation or translation of a Source form, including but
|
|
32
|
+
not limited to compiled object code, generated documentation,
|
|
33
|
+
and conversions to other media types.
|
|
34
|
+
|
|
35
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
36
|
+
Object form, made available under the License, as indicated by a
|
|
37
|
+
copyright notice that is included in or attached to the work
|
|
38
|
+
(an example is provided in the Appendix below).
|
|
39
|
+
|
|
40
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
41
|
+
form, that is based on (or derived from) the Work and for which the
|
|
42
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
43
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
44
|
+
of this License, Derivative Works shall not include works that remain
|
|
45
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
46
|
+
the Work and Derivative Works thereof.
|
|
47
|
+
|
|
48
|
+
"Contribution" shall mean any work of authorship, including
|
|
49
|
+
the original version of the Work and any modifications or additions
|
|
50
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
51
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
52
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
53
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
54
|
+
means any form of electronic, verbal, or written communication sent
|
|
55
|
+
to the Licensor or its representatives, including but not limited to
|
|
56
|
+
communication on electronic mailing lists, source code control systems,
|
|
57
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
58
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
59
|
+
excluding communication that is conspicuously marked or otherwise
|
|
60
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
61
|
+
|
|
62
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
63
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
64
|
+
subsequently incorporated within the Work.
|
|
65
|
+
|
|
66
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
67
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
68
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
69
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
70
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
71
|
+
Work and such Derivative Works in Source or Object form.
|
|
72
|
+
|
|
73
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
74
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
75
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
76
|
+
(except as stated in this section) patent license to make, have made,
|
|
77
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
78
|
+
where such license applies only to those patent claims licensable
|
|
79
|
+
by such Contributor that are necessarily infringed by their
|
|
80
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
81
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
82
|
+
institute patent litigation against any entity (including a
|
|
83
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
84
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
85
|
+
or contributory patent infringement, then any patent licenses
|
|
86
|
+
granted to You under this License for that Work shall terminate
|
|
87
|
+
as of the date such litigation is filed.
|
|
88
|
+
|
|
89
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
90
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
91
|
+
modifications, and in Source or Object form, provided that You
|
|
92
|
+
meet the following conditions:
|
|
93
|
+
|
|
94
|
+
(a) You must give any other recipients of the Work or
|
|
95
|
+
Derivative Works a copy of this License; and
|
|
96
|
+
|
|
97
|
+
(b) You must cause any modified files to carry prominent notices
|
|
98
|
+
stating that You changed the files; and
|
|
99
|
+
|
|
100
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
101
|
+
that You distribute, all copyright, patent, trademark, and
|
|
102
|
+
attribution notices from the Source form of the Work,
|
|
103
|
+
excluding those notices that do not pertain to any part of
|
|
104
|
+
the Derivative Works; and
|
|
105
|
+
|
|
106
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
107
|
+
distribution, then any Derivative Works that You distribute must
|
|
108
|
+
include a readable copy of the attribution notices contained
|
|
109
|
+
within such NOTICE file, excluding those notices that do not
|
|
110
|
+
pertain to any part of the Derivative Works, in at least one
|
|
111
|
+
of the following places: within a NOTICE text file distributed
|
|
112
|
+
as part of the Derivative Works; within the Source form or
|
|
113
|
+
documentation, if provided along with the Derivative Works; or,
|
|
114
|
+
within a display generated by the Derivative Works, if and
|
|
115
|
+
wherever such third-party notices normally appear. The contents
|
|
116
|
+
of the NOTICE file are for informational purposes only and
|
|
117
|
+
do not modify the License. You may add Your own attribution
|
|
118
|
+
notices within Derivative Works that You distribute, alongside
|
|
119
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
120
|
+
that such additional attribution notices cannot be construed
|
|
121
|
+
as modifying the License.
|
|
122
|
+
|
|
123
|
+
You may add Your own copyright statement to Your modifications and
|
|
124
|
+
may provide additional or different license terms and conditions
|
|
125
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
126
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
127
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
128
|
+
the conditions stated in this License.
|
|
129
|
+
|
|
130
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
131
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
132
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
133
|
+
this License, without any additional terms or conditions.
|
|
134
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
135
|
+
the terms of any separate license agreement you may have executed
|
|
136
|
+
with Licensor regarding such Contributions.
|
|
137
|
+
|
|
138
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
139
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
140
|
+
except as required for reasonable and customary use in describing the
|
|
141
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
142
|
+
|
|
143
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
144
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
145
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
146
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
147
|
+
implied, including, without limitation, any warranties or conditions
|
|
148
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
149
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
150
|
+
appropriateness of using or redistributing the Work and assume any
|
|
151
|
+
risks associated with Your exercise of permissions under this License.
|
|
152
|
+
|
|
153
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
154
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
155
|
+
unless required by applicable law (such as deliberate and grossly
|
|
156
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
157
|
+
liable to You for damages, including any direct, indirect, special,
|
|
158
|
+
incidental, or consequential damages of any character arising as a
|
|
159
|
+
result of this License or out of the use or inability to use the
|
|
160
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
161
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
162
|
+
other commercial damages or losses), even if such Contributor
|
|
163
|
+
has been advised of the possibility of such damages.
|
|
164
|
+
|
|
165
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
166
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
167
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
168
|
+
or other liability obligations and/or rights consistent with this
|
|
169
|
+
License. However, in accepting such obligations, You may act only
|
|
170
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
171
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
172
|
+
defend, and hold each Contributor harmless for any liability
|
|
173
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
174
|
+
of your accepting any such warranty or additional liability.
|
|
175
|
+
|
|
176
|
+
END OF TERMS AND CONDITIONS
|
|
177
|
+
|
|
178
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
179
|
+
|
|
180
|
+
To apply the Apache License to your work, attach the following
|
|
181
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
182
|
+
replaced with your own identifying information. (Don't include
|
|
183
|
+
the brackets!) The text should be enclosed in the appropriate
|
|
184
|
+
comment syntax for the file format. We also recommend that a
|
|
185
|
+
file or class name and description of purpose be included on the
|
|
186
|
+
same "printed page" as the copyright notice for easier
|
|
187
|
+
identification within third-party archives.
|
|
188
|
+
|
|
189
|
+
Copyright 2026 Ian Yang
|
|
190
|
+
|
|
191
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
192
|
+
you may not use this file except in compliance with the License.
|
|
193
|
+
You may obtain a copy of the License at
|
|
194
|
+
|
|
195
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
196
|
+
|
|
197
|
+
Unless required by applicable law or agreed to in writing, software
|
|
198
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
199
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
200
|
+
See the License for the specific language governing permissions and
|
|
201
|
+
limitations under the License.
|
open_lars-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: open-lars
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A pure-PyTorch LARS optimizer, compatible with modern CUDA
|
|
5
|
+
Author-email: Ian Yang <ian16yang@gmail.com>
|
|
6
|
+
License: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://github.com/GrignardReagent/open_lars
|
|
8
|
+
Classifier: Development Status :: 4 - Beta
|
|
9
|
+
Classifier: Intended Audience :: Science/Research
|
|
10
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
12
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
13
|
+
Requires-Python: >=3.9
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
License-File: LICENSE
|
|
16
|
+
Requires-Dist: torch>=1.8
|
|
17
|
+
Provides-Extra: test
|
|
18
|
+
Requires-Dist: pytest>=4; extra == "test"
|
|
19
|
+
Dynamic: license-file
|
|
20
|
+
|
|
21
|
+
# open_lars
|
|
22
|
+
|
|
23
|
+
A **pure-PyTorch implementation of the LARS optimizer** (Layer-wise Adaptive
|
|
24
|
+
Rate Scaling; [You, Gitman & Ginsburg 2017](https://arxiv.org/abs/1708.03888)).
|
|
25
|
+
|
|
26
|
+
LARS is the optimizer used by the SimCLR / SupCon lineage (Chen et al. 2020;
|
|
27
|
+
Khosla et al. 2020) for large-batch contrastive pretraining: at very large
|
|
28
|
+
batch sizes a single global learning rate is simultaneously too large for some
|
|
29
|
+
layers and too small for others, so LARS rescales each layer's effective LR by
|
|
30
|
+
a "trust ratio" - the ratio of that layer's weight norm to its gradient norm.
|
|
31
|
+
|
|
32
|
+
## Why pure PyTorch?
|
|
33
|
+
|
|
34
|
+
Older LARS packages computed the trust ratio in custom C++/CUDA kernels
|
|
35
|
+
compiled at install time. That approach ages badly: it needs an `nvcc`
|
|
36
|
+
matching the exact CUDA version your PyTorch wheel was built against (fragile
|
|
37
|
+
on shared or HPC machines, impossible where no CUDA toolkit is installed), it
|
|
38
|
+
pins you to one torch ABI so every torch upgrade means a rebuild, and once the
|
|
39
|
+
underlying C++/ATen APIs churn, the extension stops building altogether.
|
|
40
|
+
|
|
41
|
+
The trust ratio is a handful of scalar operations, so `open_lars` implements
|
|
42
|
+
it as plain tensor ops instead. There is nothing to compile: it runs on CPU
|
|
43
|
+
and on **any CUDA version supported by your PyTorch build** (also ROCm/MPS),
|
|
44
|
+
with any `torch >= 1.8`.
|
|
45
|
+
|
|
46
|
+
## Install
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pip install open_lars
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Or from source:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pip install . # from a checkout
|
|
56
|
+
pip install -e . # editable, for development
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
The only dependency is `torch`. Once installed in an environment, it is
|
|
60
|
+
importable from any directory you work in.
|
|
61
|
+
|
|
62
|
+
## Usage
|
|
63
|
+
|
|
64
|
+
`open_lars.LARS` is a standalone optimizer (not a wrapper around another
|
|
65
|
+
optimizer), with SGD-with-momentum as the inner update rule, exactly as in
|
|
66
|
+
the paper's Algorithm 1:
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
from open_lars import LARS
|
|
70
|
+
|
|
71
|
+
optimizer = LARS(model.parameters(), lr=1.6, momentum=0.9,
|
|
72
|
+
weight_decay=1e-4, trust_coefficient=0.001)
|
|
73
|
+
|
|
74
|
+
for x, y in loader:
|
|
75
|
+
optimizer.zero_grad()
|
|
76
|
+
loss = criterion(model(x), y)
|
|
77
|
+
loss.backward()
|
|
78
|
+
optimizer.step()
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
It is a standard `torch.optim.Optimizer`: parameter groups, `state_dict()` /
|
|
82
|
+
`load_state_dict()` checkpointing, closures, and LR schedulers (e.g.
|
|
83
|
+
linear-warmup + cosine decay, the schedule conventionally paired with LARS)
|
|
84
|
+
all work as usual.
|
|
85
|
+
|
|
86
|
+
### Update rule
|
|
87
|
+
|
|
88
|
+
```text
|
|
89
|
+
local_lr = trust_coefficient * ||w|| / (||∇L(w)|| + weight_decay * ||w|| + eps)
|
|
90
|
+
v_t = momentum * v_{t-1} + lr * local_lr * (∇L(w) + weight_decay * w)
|
|
91
|
+
w_{t+1} = w_t - v_t
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Following standard practice (the paper; SimCLR/BYOL reference code), the trust
|
|
95
|
+
ratio is **skipped for 1-D parameters** (biases, LayerNorm/BatchNorm weights),
|
|
96
|
+
which fall back to plain SGD+momentum — applying per-layer adaptation to those
|
|
97
|
+
is known to hurt convergence. If `||w||` or `||∇L(w)||` is zero, `local_lr`
|
|
98
|
+
falls back to `1.0` so the parameter still receives a plain update.
|
|
99
|
+
|
|
100
|
+
The trust-ratio computation is also exposed directly as a pure-torch function:
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
from open_lars import compute_adaptive_lr
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Tests
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
pip install -e '.[test]'
|
|
110
|
+
pytest
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
The suite covers the trust-ratio formula and its zero-norm guards across
|
|
114
|
+
dtypes and devices (including half precision), the optimizer contract
|
|
115
|
+
(pickling, checkpointing, schedulers, parameter groups, closures), and an
|
|
116
|
+
equivalence suite verifying that `open_lars.LARS` reproduces the reference
|
|
117
|
+
implementation in `tests/reference_lars.py` **bit-for-bit** over multi-step
|
|
118
|
+
training trajectories, on CPU and CUDA, across momentum/weight-decay
|
|
119
|
+
configurations. CUDA-specific tests skip automatically on CPU-only machines.
|
|
120
|
+
|
|
121
|
+
## References
|
|
122
|
+
|
|
123
|
+
- You, Gitman & Ginsburg, *Large Batch Training of Convolutional Networks*,
|
|
124
|
+
2017. <https://arxiv.org/abs/1708.03888>
|
|
125
|
+
- Chen et al., *A Simple Framework for Contrastive Learning of Visual
|
|
126
|
+
Representations* (SimCLR), 2020. <https://arxiv.org/abs/2002.05709>
|
|
127
|
+
- Khosla et al., *Supervised Contrastive Learning*, 2020.
|
|
128
|
+
<https://arxiv.org/abs/2004.11362>
|
|
129
|
+
|
|
130
|
+
## License
|
|
131
|
+
|
|
132
|
+
Apache License 2.0 — see `LICENSE`.
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# open_lars
|
|
2
|
+
|
|
3
|
+
A **pure-PyTorch implementation of the LARS optimizer** (Layer-wise Adaptive
|
|
4
|
+
Rate Scaling; [You, Gitman & Ginsburg 2017](https://arxiv.org/abs/1708.03888)).
|
|
5
|
+
|
|
6
|
+
LARS is the optimizer used by the SimCLR / SupCon lineage (Chen et al. 2020;
|
|
7
|
+
Khosla et al. 2020) for large-batch contrastive pretraining: at very large
|
|
8
|
+
batch sizes a single global learning rate is simultaneously too large for some
|
|
9
|
+
layers and too small for others, so LARS rescales each layer's effective LR by
|
|
10
|
+
a "trust ratio" - the ratio of that layer's weight norm to its gradient norm.
|
|
11
|
+
|
|
12
|
+
## Why pure PyTorch?
|
|
13
|
+
|
|
14
|
+
Older LARS packages computed the trust ratio in custom C++/CUDA kernels
|
|
15
|
+
compiled at install time. That approach ages badly: it needs an `nvcc`
|
|
16
|
+
matching the exact CUDA version your PyTorch wheel was built against (fragile
|
|
17
|
+
on shared or HPC machines, impossible where no CUDA toolkit is installed), it
|
|
18
|
+
pins you to one torch ABI so every torch upgrade means a rebuild, and once the
|
|
19
|
+
underlying C++/ATen APIs churn, the extension stops building altogether.
|
|
20
|
+
|
|
21
|
+
The trust ratio is a handful of scalar operations, so `open_lars` implements
|
|
22
|
+
it as plain tensor ops instead. There is nothing to compile: it runs on CPU
|
|
23
|
+
and on **any CUDA version supported by your PyTorch build** (also ROCm/MPS),
|
|
24
|
+
with any `torch >= 1.8`.
|
|
25
|
+
|
|
26
|
+
## Install
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pip install open_lars
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Or from source:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install . # from a checkout
|
|
36
|
+
pip install -e . # editable, for development
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
The only dependency is `torch`. Once installed in an environment, it is
|
|
40
|
+
importable from any directory you work in.
|
|
41
|
+
|
|
42
|
+
## Usage
|
|
43
|
+
|
|
44
|
+
`open_lars.LARS` is a standalone optimizer (not a wrapper around another
|
|
45
|
+
optimizer), with SGD-with-momentum as the inner update rule, exactly as in
|
|
46
|
+
the paper's Algorithm 1:
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
from open_lars import LARS
|
|
50
|
+
|
|
51
|
+
optimizer = LARS(model.parameters(), lr=1.6, momentum=0.9,
|
|
52
|
+
weight_decay=1e-4, trust_coefficient=0.001)
|
|
53
|
+
|
|
54
|
+
for x, y in loader:
|
|
55
|
+
optimizer.zero_grad()
|
|
56
|
+
loss = criterion(model(x), y)
|
|
57
|
+
loss.backward()
|
|
58
|
+
optimizer.step()
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
It is a standard `torch.optim.Optimizer`: parameter groups, `state_dict()` /
|
|
62
|
+
`load_state_dict()` checkpointing, closures, and LR schedulers (e.g.
|
|
63
|
+
linear-warmup + cosine decay, the schedule conventionally paired with LARS)
|
|
64
|
+
all work as usual.
|
|
65
|
+
|
|
66
|
+
### Update rule
|
|
67
|
+
|
|
68
|
+
```text
|
|
69
|
+
local_lr = trust_coefficient * ||w|| / (||∇L(w)|| + weight_decay * ||w|| + eps)
|
|
70
|
+
v_t = momentum * v_{t-1} + lr * local_lr * (∇L(w) + weight_decay * w)
|
|
71
|
+
w_{t+1} = w_t - v_t
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Following standard practice (the paper; SimCLR/BYOL reference code), the trust
|
|
75
|
+
ratio is **skipped for 1-D parameters** (biases, LayerNorm/BatchNorm weights),
|
|
76
|
+
which fall back to plain SGD+momentum — applying per-layer adaptation to those
|
|
77
|
+
is known to hurt convergence. If `||w||` or `||∇L(w)||` is zero, `local_lr`
|
|
78
|
+
falls back to `1.0` so the parameter still receives a plain update.
|
|
79
|
+
|
|
80
|
+
The trust-ratio computation is also exposed directly as a pure-torch function:
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
from open_lars import compute_adaptive_lr
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Tests
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
pip install -e '.[test]'
|
|
90
|
+
pytest
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
The suite covers the trust-ratio formula and its zero-norm guards across
|
|
94
|
+
dtypes and devices (including half precision), the optimizer contract
|
|
95
|
+
(pickling, checkpointing, schedulers, parameter groups, closures), and an
|
|
96
|
+
equivalence suite verifying that `open_lars.LARS` reproduces the reference
|
|
97
|
+
implementation in `tests/reference_lars.py` **bit-for-bit** over multi-step
|
|
98
|
+
training trajectories, on CPU and CUDA, across momentum/weight-decay
|
|
99
|
+
configurations. CUDA-specific tests skip automatically on CPU-only machines.
|
|
100
|
+
|
|
101
|
+
## References
|
|
102
|
+
|
|
103
|
+
- You, Gitman & Ginsburg, *Large Batch Training of Convolutional Networks*,
|
|
104
|
+
2017. <https://arxiv.org/abs/1708.03888>
|
|
105
|
+
- Chen et al., *A Simple Framework for Contrastive Learning of Visual
|
|
106
|
+
Representations* (SimCLR), 2020. <https://arxiv.org/abs/2002.05709>
|
|
107
|
+
- Khosla et al., *Supervised Contrastive Learning*, 2020.
|
|
108
|
+
<https://arxiv.org/abs/2004.11362>
|
|
109
|
+
|
|
110
|
+
## License
|
|
111
|
+
|
|
112
|
+
Apache License 2.0 — see `LICENSE`.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Functional core of the LARS optimizer.
|
|
2
|
+
|
|
3
|
+
The trust ratio ("adaptive learning rate") is a handful of scalar operations,
|
|
4
|
+
so it is expressed here as plain tensor ops: no compiled extension, so it runs
|
|
5
|
+
on CPU or any CUDA/ROCm/MPS device torch itself supports, in any floating
|
|
6
|
+
dtype, with no compilation step at install time.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import torch
|
|
10
|
+
|
|
11
|
+
__all__ = ["compute_adaptive_lr"]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def compute_adaptive_lr(param_norm, grad_norm, weight_decay, eps, trust_coef,
|
|
15
|
+
out=None):
|
|
16
|
+
"""Compute the LARS local ("adaptive") learning rate for one parameter.
|
|
17
|
+
|
|
18
|
+
Implements the trust ratio of You, Gitman & Ginsburg (2017)::
|
|
19
|
+
|
|
20
|
+
adaptive_lr = trust_coef * ||w|| / (||g|| + weight_decay * ||w|| + eps)
|
|
21
|
+
|
|
22
|
+
with two guard cases:
|
|
23
|
+
|
|
24
|
+
1. ``param_norm == 0`` (e.g. a parameter initialised to zero): the ratio
|
|
25
|
+
would be zero and freeze the parameter forever, so fall back to 1.0
|
|
26
|
+
(plain SGD-style update).
|
|
27
|
+
2. ``grad_norm == 0`` (no useful gradient signal): the ratio is
|
|
28
|
+
meaningless, so fall back to 1.0 and let the base update rule decide.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
param_norm (Tensor): scalar tensor, ||w|| of the parameter.
|
|
32
|
+
grad_norm (Tensor): scalar tensor, ||g|| of its (raw) gradient.
|
|
33
|
+
weight_decay (float): weight-decay coefficient folded into the
|
|
34
|
+
denominator (the paper's ``beta * ||w||`` term).
|
|
35
|
+
eps (float): small constant for numerical stability.
|
|
36
|
+
trust_coef (float): the paper's trust coefficient ``eta``.
|
|
37
|
+
out (Tensor, optional): tensor to write the result into.
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
Tensor: scalar tensor holding the adaptive lr (``out`` if given).
|
|
41
|
+
"""
|
|
42
|
+
adaptive_lr = torch.where(
|
|
43
|
+
(param_norm > 0) & (grad_norm > 0),
|
|
44
|
+
trust_coef * param_norm / (grad_norm + weight_decay * param_norm + eps),
|
|
45
|
+
torch.ones_like(param_norm),
|
|
46
|
+
)
|
|
47
|
+
if out is not None:
|
|
48
|
+
out.copy_(adaptive_lr)
|
|
49
|
+
return out
|
|
50
|
+
return adaptive_lr
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""
|
|
2
|
+
LARS (Layer-wise Adaptive Rate Scaling) optimizer, in pure PyTorch.
|
|
3
|
+
|
|
4
|
+
Reference
|
|
5
|
+
---------
|
|
6
|
+
You, Gitman & Ginsburg, "Large Batch Training of Convolutional Networks", 2017.
|
|
7
|
+
https://arxiv.org/abs/1708.03888
|
|
8
|
+
|
|
9
|
+
Why this exists: at very large batch sizes, a single global learning rate is
|
|
10
|
+
simultaneously too large for some layers (divergence) and too small for
|
|
11
|
+
others (slow convergence), because layers differ widely in parameter/gradient
|
|
12
|
+
scale. LARS rescales each layer's *effective* LR by a "trust ratio" -- the
|
|
13
|
+
ratio of that layer's weight norm to its gradient norm -- so every layer
|
|
14
|
+
takes a step proportional to its own parameter scale rather than one shared
|
|
15
|
+
LR. This is the optimizer Khosla et al. (SupCon) and the SimCLR lineage use
|
|
16
|
+
for large-batch contrastive pretraining.
|
|
17
|
+
|
|
18
|
+
Implements the paper's Algorithm 1 directly:
|
|
19
|
+
v_t = momentum * v_{t-1} + lr * local_lr * (grad + weight_decay * w)
|
|
20
|
+
w_t+1 = w_t - v_t
|
|
21
|
+
local_lr = trust_coefficient * ||w|| / (||grad|| + weight_decay * ||w|| + eps)
|
|
22
|
+
|
|
23
|
+
There is no custom CUDA kernel to compile at install time: the trust ratio is
|
|
24
|
+
plain tensor ops (see ``open_lars.functional.compute_adaptive_lr``), so this
|
|
25
|
+
runs against any current CUDA build of PyTorch -- or CPU/ROCm/MPS -- with no
|
|
26
|
+
nvcc and no ABI pinning.
|
|
27
|
+
|
|
28
|
+
Following standard practice (the paper; also SimCLR/BYOL reference code),
|
|
29
|
+
LARS scaling is skipped for 1-D parameters (biases, LayerNorm/BatchNorm
|
|
30
|
+
weights) -- applying a per-layer trust ratio to those is known to hurt
|
|
31
|
+
convergence -- and falls back to plain SGD+momentum for them instead.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
import torch
|
|
35
|
+
from torch.optim import Optimizer
|
|
36
|
+
|
|
37
|
+
from open_lars.functional import compute_adaptive_lr
|
|
38
|
+
|
|
39
|
+
__all__ = ["LARS"]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class LARS(Optimizer):
|
|
43
|
+
"""LARS as a self-contained :class:`torch.optim.Optimizer`.
|
|
44
|
+
|
|
45
|
+
LARS *is* the optimizer here (rather than a wrapper around another one),
|
|
46
|
+
with SGD-with-momentum as the inner update rule, exactly as in the
|
|
47
|
+
paper's Algorithm 1.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
params: iterable of parameters or dicts defining parameter groups.
|
|
51
|
+
lr (float): global learning rate.
|
|
52
|
+
momentum (float, optional): SGD-style momentum used inside LARS.
|
|
53
|
+
weight_decay (float, optional): L2 penalty, folded into both the
|
|
54
|
+
trust-ratio denominator and the update direction.
|
|
55
|
+
trust_coefficient (float, optional): the paper's ``eta``.
|
|
56
|
+
eps (float, optional): numerical-stability constant in the
|
|
57
|
+
trust-ratio denominator.
|
|
58
|
+
|
|
59
|
+
Example::
|
|
60
|
+
|
|
61
|
+
optimizer = LARS(model.parameters(), lr=1.6, momentum=0.9,
|
|
62
|
+
weight_decay=1e-4, trust_coefficient=0.001)
|
|
63
|
+
|
|
64
|
+
loss = loss_fn(model(input), target)
|
|
65
|
+
loss.backward()
|
|
66
|
+
optimizer.step()
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
def __init__(self, params, lr, momentum=0.9, weight_decay=0.0,
|
|
70
|
+
trust_coefficient=0.001, eps=1e-8):
|
|
71
|
+
if lr < 0.0:
|
|
72
|
+
raise ValueError(f"Invalid learning rate: {lr}")
|
|
73
|
+
if momentum < 0.0:
|
|
74
|
+
raise ValueError(f"Invalid momentum value: {momentum}")
|
|
75
|
+
if weight_decay < 0.0:
|
|
76
|
+
raise ValueError(f"Invalid weight_decay value: {weight_decay}")
|
|
77
|
+
if trust_coefficient < 0.0:
|
|
78
|
+
raise ValueError(f"Invalid trust_coefficient value: {trust_coefficient}")
|
|
79
|
+
if eps < 0.0:
|
|
80
|
+
raise ValueError(f"Invalid epsilon value: {eps}")
|
|
81
|
+
defaults = dict(lr=lr, momentum=momentum, weight_decay=weight_decay,
|
|
82
|
+
trust_coefficient=trust_coefficient, eps=eps)
|
|
83
|
+
super().__init__(params, defaults)
|
|
84
|
+
|
|
85
|
+
@torch.no_grad()
|
|
86
|
+
def step(self, closure=None):
|
|
87
|
+
loss = None
|
|
88
|
+
if closure is not None:
|
|
89
|
+
with torch.enable_grad():
|
|
90
|
+
loss = closure()
|
|
91
|
+
|
|
92
|
+
for group in self.param_groups:
|
|
93
|
+
weight_decay = group["weight_decay"]
|
|
94
|
+
momentum = group["momentum"]
|
|
95
|
+
trust_coefficient = group["trust_coefficient"]
|
|
96
|
+
eps = group["eps"]
|
|
97
|
+
global_lr = group["lr"]
|
|
98
|
+
|
|
99
|
+
for p in group["params"]:
|
|
100
|
+
if p.grad is None:
|
|
101
|
+
continue
|
|
102
|
+
grad = p.grad
|
|
103
|
+
if grad.is_sparse:
|
|
104
|
+
raise RuntimeError("LARS does not support sparse gradients")
|
|
105
|
+
|
|
106
|
+
# Trust ratio uses the RAW gradient norm in its denominator
|
|
107
|
+
# (matches the paper's ||∇L(w)|| term) -- weight decay is
|
|
108
|
+
# folded in separately, only for the velocity update itself.
|
|
109
|
+
if p.ndim > 1:
|
|
110
|
+
local_lr = compute_adaptive_lr(
|
|
111
|
+
torch.norm(p), torch.norm(grad),
|
|
112
|
+
weight_decay, eps, trust_coefficient).item()
|
|
113
|
+
else:
|
|
114
|
+
local_lr = 1.0 # biases / norm params: plain SGD+momentum, no trust ratio
|
|
115
|
+
|
|
116
|
+
d_p = grad if weight_decay == 0 else grad.add(p, alpha=weight_decay)
|
|
117
|
+
actual_lr = global_lr * local_lr
|
|
118
|
+
|
|
119
|
+
state = self.state[p]
|
|
120
|
+
if "momentum_buffer" not in state:
|
|
121
|
+
buf = state["momentum_buffer"] = d_p.mul(actual_lr)
|
|
122
|
+
else:
|
|
123
|
+
buf = state["momentum_buffer"]
|
|
124
|
+
buf.mul_(momentum).add_(d_p, alpha=actual_lr)
|
|
125
|
+
|
|
126
|
+
p.add_(buf, alpha=-1.0)
|
|
127
|
+
|
|
128
|
+
return loss
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: open-lars
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A pure-PyTorch LARS optimizer, compatible with modern CUDA
|
|
5
|
+
Author-email: Ian Yang <ian16yang@gmail.com>
|
|
6
|
+
License: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://github.com/GrignardReagent/open_lars
|
|
8
|
+
Classifier: Development Status :: 4 - Beta
|
|
9
|
+
Classifier: Intended Audience :: Science/Research
|
|
10
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
12
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
13
|
+
Requires-Python: >=3.9
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
License-File: LICENSE
|
|
16
|
+
Requires-Dist: torch>=1.8
|
|
17
|
+
Provides-Extra: test
|
|
18
|
+
Requires-Dist: pytest>=4; extra == "test"
|
|
19
|
+
Dynamic: license-file
|
|
20
|
+
|
|
21
|
+
# open_lars
|
|
22
|
+
|
|
23
|
+
A **pure-PyTorch implementation of the LARS optimizer** (Layer-wise Adaptive
|
|
24
|
+
Rate Scaling; [You, Gitman & Ginsburg 2017](https://arxiv.org/abs/1708.03888)).
|
|
25
|
+
|
|
26
|
+
LARS is the optimizer used by the SimCLR / SupCon lineage (Chen et al. 2020;
|
|
27
|
+
Khosla et al. 2020) for large-batch contrastive pretraining: at very large
|
|
28
|
+
batch sizes a single global learning rate is simultaneously too large for some
|
|
29
|
+
layers and too small for others, so LARS rescales each layer's effective LR by
|
|
30
|
+
a "trust ratio" - the ratio of that layer's weight norm to its gradient norm.
|
|
31
|
+
|
|
32
|
+
## Why pure PyTorch?
|
|
33
|
+
|
|
34
|
+
Older LARS packages computed the trust ratio in custom C++/CUDA kernels
|
|
35
|
+
compiled at install time. That approach ages badly: it needs an `nvcc`
|
|
36
|
+
matching the exact CUDA version your PyTorch wheel was built against (fragile
|
|
37
|
+
on shared or HPC machines, impossible where no CUDA toolkit is installed), it
|
|
38
|
+
pins you to one torch ABI so every torch upgrade means a rebuild, and once the
|
|
39
|
+
underlying C++/ATen APIs churn, the extension stops building altogether.
|
|
40
|
+
|
|
41
|
+
The trust ratio is a handful of scalar operations, so `open_lars` implements
|
|
42
|
+
it as plain tensor ops instead. There is nothing to compile: it runs on CPU
|
|
43
|
+
and on **any CUDA version supported by your PyTorch build** (also ROCm/MPS),
|
|
44
|
+
with any `torch >= 1.8`.
|
|
45
|
+
|
|
46
|
+
## Install
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pip install open_lars
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Or from source:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pip install . # from a checkout
|
|
56
|
+
pip install -e . # editable, for development
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
The only dependency is `torch`. Once installed in an environment, it is
|
|
60
|
+
importable from any directory you work in.
|
|
61
|
+
|
|
62
|
+
## Usage
|
|
63
|
+
|
|
64
|
+
`open_lars.LARS` is a standalone optimizer (not a wrapper around another
|
|
65
|
+
optimizer), with SGD-with-momentum as the inner update rule, exactly as in
|
|
66
|
+
the paper's Algorithm 1:
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
from open_lars import LARS
|
|
70
|
+
|
|
71
|
+
optimizer = LARS(model.parameters(), lr=1.6, momentum=0.9,
|
|
72
|
+
weight_decay=1e-4, trust_coefficient=0.001)
|
|
73
|
+
|
|
74
|
+
for x, y in loader:
|
|
75
|
+
optimizer.zero_grad()
|
|
76
|
+
loss = criterion(model(x), y)
|
|
77
|
+
loss.backward()
|
|
78
|
+
optimizer.step()
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
It is a standard `torch.optim.Optimizer`: parameter groups, `state_dict()` /
|
|
82
|
+
`load_state_dict()` checkpointing, closures, and LR schedulers (e.g.
|
|
83
|
+
linear-warmup + cosine decay, the schedule conventionally paired with LARS)
|
|
84
|
+
all work as usual.
|
|
85
|
+
|
|
86
|
+
### Update rule
|
|
87
|
+
|
|
88
|
+
```text
|
|
89
|
+
local_lr = trust_coefficient * ||w|| / (||∇L(w)|| + weight_decay * ||w|| + eps)
|
|
90
|
+
v_t = momentum * v_{t-1} + lr * local_lr * (∇L(w) + weight_decay * w)
|
|
91
|
+
w_{t+1} = w_t - v_t
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Following standard practice (the paper; SimCLR/BYOL reference code), the trust
|
|
95
|
+
ratio is **skipped for 1-D parameters** (biases, LayerNorm/BatchNorm weights),
|
|
96
|
+
which fall back to plain SGD+momentum — applying per-layer adaptation to those
|
|
97
|
+
is known to hurt convergence. If `||w||` or `||∇L(w)||` is zero, `local_lr`
|
|
98
|
+
falls back to `1.0` so the parameter still receives a plain update.
|
|
99
|
+
|
|
100
|
+
The trust-ratio computation is also exposed directly as a pure-torch function:
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
from open_lars import compute_adaptive_lr
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Tests
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
pip install -e '.[test]'
|
|
110
|
+
pytest
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
The suite covers the trust-ratio formula and its zero-norm guards across
|
|
114
|
+
dtypes and devices (including half precision), the optimizer contract
|
|
115
|
+
(pickling, checkpointing, schedulers, parameter groups, closures), and an
|
|
116
|
+
equivalence suite verifying that `open_lars.LARS` reproduces the reference
|
|
117
|
+
implementation in `tests/reference_lars.py` **bit-for-bit** over multi-step
|
|
118
|
+
training trajectories, on CPU and CUDA, across momentum/weight-decay
|
|
119
|
+
configurations. CUDA-specific tests skip automatically on CPU-only machines.
|
|
120
|
+
|
|
121
|
+
## References
|
|
122
|
+
|
|
123
|
+
- You, Gitman & Ginsburg, *Large Batch Training of Convolutional Networks*,
|
|
124
|
+
2017. <https://arxiv.org/abs/1708.03888>
|
|
125
|
+
- Chen et al., *A Simple Framework for Contrastive Learning of Visual
|
|
126
|
+
Representations* (SimCLR), 2020. <https://arxiv.org/abs/2002.05709>
|
|
127
|
+
- Khosla et al., *Supervised Contrastive Learning*, 2020.
|
|
128
|
+
<https://arxiv.org/abs/2004.11362>
|
|
129
|
+
|
|
130
|
+
## License
|
|
131
|
+
|
|
132
|
+
Apache License 2.0 — see `LICENSE`.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
setup.py
|
|
5
|
+
open_lars/__init__.py
|
|
6
|
+
open_lars/__version__.py
|
|
7
|
+
open_lars/functional.py
|
|
8
|
+
open_lars/lars.py
|
|
9
|
+
open_lars.egg-info/PKG-INFO
|
|
10
|
+
open_lars.egg-info/SOURCES.txt
|
|
11
|
+
open_lars.egg-info/dependency_links.txt
|
|
12
|
+
open_lars.egg-info/requires.txt
|
|
13
|
+
open_lars.egg-info/top_level.txt
|
|
14
|
+
tests/test_compute_adaptive_lr.py
|
|
15
|
+
tests/test_lars.py
|
|
16
|
+
tests/test_pickle.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
open_lars
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=64"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "open-lars"
|
|
7
|
+
description = "A pure-PyTorch LARS optimizer, compatible with modern CUDA"
|
|
8
|
+
readme = "README.md"
|
|
9
|
+
requires-python = ">=3.9"
|
|
10
|
+
license = { text = "Apache-2.0" }
|
|
11
|
+
authors = [{ name = "Ian Yang", email = "ian16yang@gmail.com" }]
|
|
12
|
+
dependencies = ["torch>=1.8"]
|
|
13
|
+
dynamic = ["version"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Intended Audience :: Science/Research",
|
|
17
|
+
"License :: OSI Approved :: Apache Software License",
|
|
18
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
19
|
+
"Topic :: Scientific/Engineering :: Artificial Intelligence",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
[project.urls]
|
|
23
|
+
Homepage = "https://github.com/GrignardReagent/open_lars"
|
|
24
|
+
|
|
25
|
+
[project.optional-dependencies]
|
|
26
|
+
test = ["pytest>=4"]
|
|
27
|
+
|
|
28
|
+
[tool.setuptools]
|
|
29
|
+
packages = ["open_lars"]
|
|
30
|
+
|
|
31
|
+
[tool.setuptools.dynamic]
|
|
32
|
+
version = { attr = "open_lars.__version__.__version__" }
|
|
33
|
+
|
|
34
|
+
[tool.pytest.ini_options]
|
|
35
|
+
testpaths = ["tests"]
|
open_lars-0.1.0/setup.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""Shim for pip versions whose PEP 660 editable-install support is broken
|
|
2
|
+
(e.g. Ubuntu 22.04's stock pip 22.0.2): with this file present they fall
|
|
3
|
+
back to the legacy `setup.py develop` path for `pip install -e .`.
|
|
4
|
+
All metadata lives in pyproject.toml; modern pip ignores this file.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from setuptools import setup
|
|
8
|
+
|
|
9
|
+
setup()
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""Tests for open_lars.compute_adaptive_lr.
|
|
2
|
+
|
|
3
|
+
The function is pure tensor ops, so every case is checked across floating
|
|
4
|
+
dtypes and (when available) across devices, including half precision.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
import torch
|
|
9
|
+
|
|
10
|
+
from open_lars import compute_adaptive_lr
|
|
11
|
+
|
|
12
|
+
DEVICES = ['cpu'] + (['cuda'] if torch.cuda.is_available() else [])
|
|
13
|
+
DTYPES = [torch.float, torch.double]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def expected_ratio(param_norm, grad_norm, weight_decay, eps, trust_coef):
|
|
17
|
+
return trust_coef * param_norm / (grad_norm + weight_decay * param_norm + eps)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@pytest.mark.parametrize('device', DEVICES)
|
|
21
|
+
@pytest.mark.parametrize('dtype', DTYPES)
|
|
22
|
+
@pytest.mark.parametrize('param_norm,grad_norm,weight_decay,eps,trust_coef', [
|
|
23
|
+
(2.5, 0.5, 1e-4, 1e-8, 0.001),
|
|
24
|
+
(0.037, 12.0, 1e-4, 1e-8, 0.001),
|
|
25
|
+
(1.0, 1.0, 1.0, 2.0, 1.0),
|
|
26
|
+
(3.0, 0.25, 0.0, 0.0, 0.02),
|
|
27
|
+
])
|
|
28
|
+
def test_matches_formula(device, dtype, param_norm, grad_norm, weight_decay,
|
|
29
|
+
eps, trust_coef):
|
|
30
|
+
result = compute_adaptive_lr(
|
|
31
|
+
torch.tensor(param_norm, dtype=dtype, device=device),
|
|
32
|
+
torch.tensor(grad_norm, dtype=dtype, device=device),
|
|
33
|
+
weight_decay, eps, trust_coef)
|
|
34
|
+
|
|
35
|
+
expected = expected_ratio(param_norm, grad_norm, weight_decay, eps, trust_coef)
|
|
36
|
+
assert torch.allclose(result, torch.tensor(expected, dtype=dtype, device=device))
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@pytest.mark.parametrize('device', DEVICES)
|
|
40
|
+
@pytest.mark.parametrize('dtype', DTYPES)
|
|
41
|
+
def test_zero_param_norm_falls_back_to_one(device, dtype):
|
|
42
|
+
# ||w|| == 0 would give a zero ratio and freeze the parameter forever.
|
|
43
|
+
result = compute_adaptive_lr(
|
|
44
|
+
torch.tensor(0., dtype=dtype, device=device),
|
|
45
|
+
torch.tensor(1., dtype=dtype, device=device),
|
|
46
|
+
1e-4, 1e-8, 0.001)
|
|
47
|
+
|
|
48
|
+
assert result == torch.tensor(1., dtype=dtype, device=device)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@pytest.mark.parametrize('device', DEVICES)
|
|
52
|
+
@pytest.mark.parametrize('dtype', DTYPES)
|
|
53
|
+
def test_zero_grad_norm_falls_back_to_one(device, dtype):
|
|
54
|
+
# ||g|| == 0 carries no signal; defer to the base update rule.
|
|
55
|
+
result = compute_adaptive_lr(
|
|
56
|
+
torch.tensor(1., dtype=dtype, device=device),
|
|
57
|
+
torch.tensor(0., dtype=dtype, device=device),
|
|
58
|
+
1e-4, 1e-8, 0.001)
|
|
59
|
+
|
|
60
|
+
assert result == torch.tensor(1., dtype=dtype, device=device)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@pytest.mark.skipif(not torch.cuda.is_available(), reason='cuda required')
|
|
64
|
+
@pytest.mark.parametrize('dtype', DTYPES)
|
|
65
|
+
def test_cpu_and_gpu_agree(dtype):
|
|
66
|
+
args = (0.8, 3.2, 1e-4, 1e-8, 0.001)
|
|
67
|
+
|
|
68
|
+
cpu = compute_adaptive_lr(
|
|
69
|
+
torch.tensor(args[0], dtype=dtype),
|
|
70
|
+
torch.tensor(args[1], dtype=dtype), *args[2:])
|
|
71
|
+
gpu = compute_adaptive_lr(
|
|
72
|
+
torch.tensor(args[0], dtype=dtype, device='cuda'),
|
|
73
|
+
torch.tensor(args[1], dtype=dtype, device='cuda'), *args[2:])
|
|
74
|
+
|
|
75
|
+
assert torch.allclose(cpu, gpu.cpu())
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@pytest.mark.skipif(not torch.cuda.is_available(), reason='cuda required')
|
|
79
|
+
@pytest.mark.parametrize('param_norm,grad_norm,expected', [
|
|
80
|
+
(2.5, 0.5, 0.001 * 2.5 / (0.5 + 1e-4 * 2.5 + 1e-8)),
|
|
81
|
+
(0.0, 1.0, 1.0),
|
|
82
|
+
(1.0, 0.0, 1.0),
|
|
83
|
+
])
|
|
84
|
+
def test_half_precision_on_gpu(param_norm, grad_norm, expected):
|
|
85
|
+
result = compute_adaptive_lr(
|
|
86
|
+
torch.tensor(param_norm, dtype=torch.half, device='cuda'),
|
|
87
|
+
torch.tensor(grad_norm, dtype=torch.half, device='cuda'),
|
|
88
|
+
1e-4, 1e-8, 0.001)
|
|
89
|
+
|
|
90
|
+
assert torch.allclose(result, torch.tensor(expected, dtype=torch.half, device='cuda'))
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@pytest.mark.parametrize('dtype', DTYPES)
|
|
94
|
+
def test_out_argument_is_written_in_place(dtype):
|
|
95
|
+
out = torch.tensor(0., dtype=dtype)
|
|
96
|
+
|
|
97
|
+
returned = compute_adaptive_lr(
|
|
98
|
+
torch.tensor(2.5, dtype=dtype),
|
|
99
|
+
torch.tensor(0.5, dtype=dtype),
|
|
100
|
+
1e-4, 1e-8, 0.001, out)
|
|
101
|
+
|
|
102
|
+
assert returned is out
|
|
103
|
+
expected = expected_ratio(2.5, 0.5, 1e-4, 1e-8, 0.001)
|
|
104
|
+
assert torch.allclose(out, torch.tensor(expected, dtype=dtype))
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
"""Behavioural tests for open_lars.LARS.
|
|
2
|
+
|
|
3
|
+
The centrepiece is the equivalence suite: open_lars.LARS must produce
|
|
4
|
+
*bit-identical* parameter trajectories to the reference implementation used
|
|
5
|
+
by IY036 (vendored verbatim in tests/reference_lars.py), on CPU and CUDA,
|
|
6
|
+
with and without weight decay / momentum, for both >1-D parameters (trust
|
|
7
|
+
ratio applied) and 1-D parameters (plain SGD+momentum fallback).
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import copy
|
|
11
|
+
import math
|
|
12
|
+
|
|
13
|
+
import pytest
|
|
14
|
+
import torch
|
|
15
|
+
from torch import nn
|
|
16
|
+
|
|
17
|
+
from open_lars import LARS
|
|
18
|
+
from tests.reference_lars import LARS as ReferenceLARS
|
|
19
|
+
|
|
20
|
+
DEVICES = ['cpu'] + (['cuda'] if torch.cuda.is_available() else [])
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def make_model(device):
|
|
24
|
+
# Mix of 2-D weights (trust ratio applied), biases and LayerNorm weights
|
|
25
|
+
# (1-D: plain SGD+momentum), mirroring the transformer trained in IY036.
|
|
26
|
+
return nn.Sequential(
|
|
27
|
+
nn.Linear(8, 16),
|
|
28
|
+
nn.LayerNorm(16),
|
|
29
|
+
nn.ReLU(),
|
|
30
|
+
nn.Linear(16, 4),
|
|
31
|
+
).to(device)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def train_steps(model, optimizer, device, n_steps=10, scheduler=None, seed=0):
|
|
35
|
+
torch.manual_seed(seed)
|
|
36
|
+
for _ in range(n_steps):
|
|
37
|
+
x = torch.randn(32, 8, device=device)
|
|
38
|
+
y = torch.randn(32, 4, device=device)
|
|
39
|
+
optimizer.zero_grad()
|
|
40
|
+
loss = nn.functional.mse_loss(model(x), y)
|
|
41
|
+
loss.backward()
|
|
42
|
+
optimizer.step()
|
|
43
|
+
if scheduler is not None:
|
|
44
|
+
scheduler.step()
|
|
45
|
+
return loss
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# ── Equivalence with the IY036 reference implementation ──────────────────────
|
|
49
|
+
|
|
50
|
+
@pytest.mark.parametrize('device', DEVICES)
|
|
51
|
+
@pytest.mark.parametrize('momentum,weight_decay', [
|
|
52
|
+
(0.9, 1e-4), # IY036's configuration
|
|
53
|
+
(0.9, 0.0),
|
|
54
|
+
(0.0, 1e-4),
|
|
55
|
+
(0.0, 0.0),
|
|
56
|
+
])
|
|
57
|
+
def test_matches_reference_implementation(device, momentum, weight_decay):
|
|
58
|
+
torch.manual_seed(42)
|
|
59
|
+
model_a = make_model(device)
|
|
60
|
+
model_b = copy.deepcopy(model_a)
|
|
61
|
+
|
|
62
|
+
opt_a = LARS(model_a.parameters(), lr=0.5, momentum=momentum,
|
|
63
|
+
weight_decay=weight_decay, trust_coefficient=0.001)
|
|
64
|
+
opt_b = ReferenceLARS(model_b.parameters(), lr=0.5, momentum=momentum,
|
|
65
|
+
weight_decay=weight_decay, trust_coefficient=0.001)
|
|
66
|
+
|
|
67
|
+
train_steps(model_a, opt_a, device, n_steps=10)
|
|
68
|
+
train_steps(model_b, opt_b, device, n_steps=10)
|
|
69
|
+
|
|
70
|
+
for p_a, p_b in zip(model_a.parameters(), model_b.parameters()):
|
|
71
|
+
assert torch.equal(p_a, p_b)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
# ── Update-rule unit checks ──────────────────────────────────────────────────
|
|
75
|
+
|
|
76
|
+
def test_single_step_matches_formula_2d():
|
|
77
|
+
torch.manual_seed(0)
|
|
78
|
+
p = torch.randn(4, 4, requires_grad=True)
|
|
79
|
+
p_init = p.detach().clone()
|
|
80
|
+
lr, wd, tc, eps = 0.1, 1e-4, 0.001, 1e-8
|
|
81
|
+
|
|
82
|
+
loss = (p ** 2).sum()
|
|
83
|
+
loss.backward()
|
|
84
|
+
grad = p.grad.detach().clone()
|
|
85
|
+
|
|
86
|
+
LARS([p], lr=lr, momentum=0.9, weight_decay=wd, trust_coefficient=tc).step()
|
|
87
|
+
|
|
88
|
+
w_norm = p_init.norm()
|
|
89
|
+
g_norm = grad.norm()
|
|
90
|
+
local_lr = (tc * w_norm / (g_norm + wd * w_norm + eps)).item()
|
|
91
|
+
expected = p_init - lr * local_lr * (grad + wd * p_init)
|
|
92
|
+
assert torch.allclose(p.detach(), expected, atol=1e-7)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def test_1d_params_use_plain_sgd_momentum():
|
|
96
|
+
# A bias-like 1-D parameter must be updated with local_lr == 1.0.
|
|
97
|
+
torch.manual_seed(0)
|
|
98
|
+
p = torch.randn(6, requires_grad=True)
|
|
99
|
+
p_init = p.detach().clone()
|
|
100
|
+
lr = 0.01
|
|
101
|
+
|
|
102
|
+
(p ** 2).sum().backward()
|
|
103
|
+
grad = p.grad.detach().clone()
|
|
104
|
+
|
|
105
|
+
LARS([p], lr=lr, momentum=0.9, weight_decay=0.0).step()
|
|
106
|
+
|
|
107
|
+
assert torch.allclose(p.detach(), p_init - lr * grad, atol=1e-7)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def test_momentum_accumulates_velocity():
|
|
111
|
+
# Two steps with a constant gradient: v2 = m*v1 + lr*g, w2 = w0 - v1 - v2.
|
|
112
|
+
p = torch.ones(3, 3, requires_grad=True)
|
|
113
|
+
lr, m, tc, eps = 0.1, 0.9, 0.001, 1e-8
|
|
114
|
+
grad = torch.full((3, 3), 0.5)
|
|
115
|
+
|
|
116
|
+
opt = LARS([p], lr=lr, momentum=m, weight_decay=0.0, trust_coefficient=tc)
|
|
117
|
+
|
|
118
|
+
w = p.detach().clone()
|
|
119
|
+
v = torch.zeros_like(w)
|
|
120
|
+
for _ in range(2):
|
|
121
|
+
local_lr = (tc * w.norm() / (grad.norm() + eps)).item()
|
|
122
|
+
v = m * v + lr * local_lr * grad
|
|
123
|
+
w = w - v
|
|
124
|
+
p.grad = grad.clone()
|
|
125
|
+
opt.step()
|
|
126
|
+
|
|
127
|
+
assert torch.allclose(p.detach(), w, atol=1e-7)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def test_zero_weight_param_still_updates():
|
|
131
|
+
# ||w|| == 0 would give trust ratio 0 and freeze the parameter forever;
|
|
132
|
+
# the guard must fall back to local_lr = 1.0 instead.
|
|
133
|
+
p = torch.zeros(4, 4, requires_grad=True)
|
|
134
|
+
p.grad = torch.ones(4, 4)
|
|
135
|
+
|
|
136
|
+
LARS([p], lr=0.1, momentum=0.0).step()
|
|
137
|
+
|
|
138
|
+
assert torch.allclose(p.detach(), torch.full((4, 4), -0.1))
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def test_zero_grad_is_a_noop():
|
|
142
|
+
p = torch.randn(4, 4, requires_grad=True)
|
|
143
|
+
p_init = p.detach().clone()
|
|
144
|
+
p.grad = torch.zeros(4, 4)
|
|
145
|
+
|
|
146
|
+
LARS([p], lr=0.1, momentum=0.9, weight_decay=0.0).step()
|
|
147
|
+
|
|
148
|
+
assert torch.equal(p.detach(), p_init)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
# ── torch.optim.Optimizer contract ───────────────────────────────────────────
|
|
152
|
+
|
|
153
|
+
def test_closure_returns_loss():
|
|
154
|
+
p = torch.randn(3, 3, requires_grad=True)
|
|
155
|
+
opt = LARS([p], lr=0.1)
|
|
156
|
+
|
|
157
|
+
def closure():
|
|
158
|
+
opt.zero_grad()
|
|
159
|
+
loss = (p ** 2).sum()
|
|
160
|
+
loss.backward()
|
|
161
|
+
return loss
|
|
162
|
+
|
|
163
|
+
loss = opt.step(closure)
|
|
164
|
+
assert loss is not None and loss.item() >= 0.0
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
@pytest.mark.parametrize('device', DEVICES)
|
|
168
|
+
def test_state_dict_roundtrip(device):
|
|
169
|
+
torch.manual_seed(1)
|
|
170
|
+
model_a = make_model(device)
|
|
171
|
+
opt_a = LARS(model_a.parameters(), lr=0.5, weight_decay=1e-4)
|
|
172
|
+
train_steps(model_a, opt_a, device, n_steps=5)
|
|
173
|
+
|
|
174
|
+
# Resume from a checkpoint into fresh objects. The deepcopy stands in for
|
|
175
|
+
# a torch.save/torch.load round trip -- loading a live state_dict directly
|
|
176
|
+
# would alias opt_a's momentum buffers.
|
|
177
|
+
model_b = copy.deepcopy(model_a)
|
|
178
|
+
opt_b = LARS(model_b.parameters(), lr=0.5, weight_decay=1e-4)
|
|
179
|
+
opt_b.load_state_dict(copy.deepcopy(opt_a.state_dict()))
|
|
180
|
+
|
|
181
|
+
# ...and both must continue on the exact same trajectory.
|
|
182
|
+
train_steps(model_a, opt_a, device, n_steps=5, seed=2)
|
|
183
|
+
train_steps(model_b, opt_b, device, n_steps=5, seed=2)
|
|
184
|
+
|
|
185
|
+
for p_a, p_b in zip(model_a.parameters(), model_b.parameters()):
|
|
186
|
+
assert torch.equal(p_a, p_b)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def test_lr_scheduler_compatibility():
|
|
190
|
+
# IY036 pairs LARS with linear-warmup + cosine decay
|
|
191
|
+
# (transformers.get_cosine_schedule_with_warmup); reproduce that schedule
|
|
192
|
+
# with a plain LambdaLR to keep this package dependency-free.
|
|
193
|
+
torch.manual_seed(3)
|
|
194
|
+
model = make_model('cpu')
|
|
195
|
+
opt = LARS(model.parameters(), lr=1.0, weight_decay=1e-4)
|
|
196
|
+
|
|
197
|
+
total, warmup = 20, 5
|
|
198
|
+
|
|
199
|
+
def warmup_cosine(step):
|
|
200
|
+
if step < warmup:
|
|
201
|
+
return step / warmup
|
|
202
|
+
progress = (step - warmup) / (total - warmup)
|
|
203
|
+
return 0.5 * (1.0 + math.cos(math.pi * progress))
|
|
204
|
+
|
|
205
|
+
scheduler = torch.optim.lr_scheduler.LambdaLR(opt, warmup_cosine)
|
|
206
|
+
|
|
207
|
+
train_steps(model, opt, 'cpu', n_steps=total, scheduler=scheduler)
|
|
208
|
+
assert opt.param_groups[0]['lr'] == pytest.approx(0.0, abs=1e-6)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def test_param_groups_with_distinct_hyperparams():
|
|
212
|
+
w = torch.randn(4, 4, requires_grad=True)
|
|
213
|
+
b = torch.randn(4, requires_grad=True)
|
|
214
|
+
opt = LARS([
|
|
215
|
+
{'params': [w], 'weight_decay': 1e-4},
|
|
216
|
+
{'params': [b], 'weight_decay': 0.0, 'lr': 0.01},
|
|
217
|
+
], lr=0.5)
|
|
218
|
+
|
|
219
|
+
assert opt.param_groups[0]['weight_decay'] == 1e-4
|
|
220
|
+
assert opt.param_groups[1]['lr'] == 0.01
|
|
221
|
+
|
|
222
|
+
w.grad = torch.ones_like(w)
|
|
223
|
+
b.grad = torch.ones_like(b)
|
|
224
|
+
opt.step() # must not raise
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def test_invalid_hyperparams_raise():
|
|
228
|
+
p = [torch.zeros(2, 2)]
|
|
229
|
+
with pytest.raises(ValueError):
|
|
230
|
+
LARS(p, lr=-0.1)
|
|
231
|
+
with pytest.raises(ValueError):
|
|
232
|
+
LARS(p, lr=0.1, momentum=-0.5)
|
|
233
|
+
with pytest.raises(ValueError):
|
|
234
|
+
LARS(p, lr=0.1, weight_decay=-1e-4)
|
|
235
|
+
with pytest.raises(ValueError):
|
|
236
|
+
LARS(p, lr=0.1, trust_coefficient=-0.001)
|
|
237
|
+
with pytest.raises(ValueError):
|
|
238
|
+
LARS(p, lr=0.1, eps=-1e-8)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
# ── End-to-end sanity ────────────────────────────────────────────────────────
|
|
242
|
+
|
|
243
|
+
@pytest.mark.parametrize('device', DEVICES)
|
|
244
|
+
def test_converges_on_toy_regression(device):
|
|
245
|
+
torch.manual_seed(7)
|
|
246
|
+
true_w = torch.randn(8, 1, device=device)
|
|
247
|
+
x = torch.randn(256, 8, device=device)
|
|
248
|
+
y = x @ true_w
|
|
249
|
+
|
|
250
|
+
model = nn.Linear(8, 1, bias=False).to(device)
|
|
251
|
+
opt = LARS(model.parameters(), lr=5.0, momentum=0.9,
|
|
252
|
+
trust_coefficient=0.01)
|
|
253
|
+
|
|
254
|
+
initial_loss = nn.functional.mse_loss(model(x), y).item()
|
|
255
|
+
for _ in range(200):
|
|
256
|
+
opt.zero_grad()
|
|
257
|
+
loss = nn.functional.mse_loss(model(x), y)
|
|
258
|
+
loss.backward()
|
|
259
|
+
opt.step()
|
|
260
|
+
|
|
261
|
+
assert loss.item() < 0.05 * initial_loss
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
@pytest.mark.skipif(not torch.cuda.is_available(), reason='cuda required')
|
|
265
|
+
def test_cuda_matches_cpu_trajectory():
|
|
266
|
+
torch.manual_seed(11)
|
|
267
|
+
model_cpu = make_model('cpu')
|
|
268
|
+
model_gpu = copy.deepcopy(model_cpu).cuda()
|
|
269
|
+
|
|
270
|
+
opt_cpu = LARS(model_cpu.parameters(), lr=0.5, weight_decay=1e-4)
|
|
271
|
+
opt_gpu = LARS(model_gpu.parameters(), lr=0.5, weight_decay=1e-4)
|
|
272
|
+
|
|
273
|
+
torch.manual_seed(0)
|
|
274
|
+
x = torch.randn(32, 8)
|
|
275
|
+
y = torch.randn(32, 4)
|
|
276
|
+
for _ in range(5):
|
|
277
|
+
for model, opt, xd, yd in [(model_cpu, opt_cpu, x, y),
|
|
278
|
+
(model_gpu, opt_gpu, x.cuda(), y.cuda())]:
|
|
279
|
+
opt.zero_grad()
|
|
280
|
+
nn.functional.mse_loss(model(xd), yd).backward()
|
|
281
|
+
opt.step()
|
|
282
|
+
|
|
283
|
+
for p_c, p_g in zip(model_cpu.parameters(), model_gpu.parameters()):
|
|
284
|
+
assert torch.allclose(p_c, p_g.cpu(), atol=1e-5)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""LARS must survive (de)serialisation with its hyperparameters intact."""
|
|
2
|
+
|
|
3
|
+
import copy
|
|
4
|
+
import pickle
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
import torch
|
|
8
|
+
|
|
9
|
+
from open_lars import LARS
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@pytest.fixture
|
|
13
|
+
def lars():
|
|
14
|
+
return LARS([torch.zeros(2, 2)], lr=0.1, momentum=0.8,
|
|
15
|
+
weight_decay=1e-4, trust_coefficient=0.42)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def assert_hyperparams_preserved(opt):
|
|
19
|
+
assert isinstance(opt, LARS)
|
|
20
|
+
group = opt.param_groups[0]
|
|
21
|
+
assert group['lr'] == 0.1
|
|
22
|
+
assert group['momentum'] == 0.8
|
|
23
|
+
assert group['weight_decay'] == 1e-4
|
|
24
|
+
assert group['trust_coefficient'] == 0.42
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_pickle_roundtrip(lars):
|
|
28
|
+
assert_hyperparams_preserved(pickle.loads(pickle.dumps(lars)))
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_copy(lars):
|
|
32
|
+
assert_hyperparams_preserved(copy.copy(lars))
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_deepcopy(lars):
|
|
36
|
+
assert_hyperparams_preserved(copy.deepcopy(lars))
|