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.
@@ -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.
@@ -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,5 @@
1
+ from open_lars.__version__ import __version__ # noqa
2
+ from open_lars.functional import compute_adaptive_lr
3
+ from open_lars.lars import LARS
4
+
5
+ __all__ = ["LARS", "compute_adaptive_lr"]
@@ -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,4 @@
1
+ torch>=1.8
2
+
3
+ [test]
4
+ pytest>=4
@@ -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"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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))