Skip to content

Commit 7a0c1fb

Browse files
trust region, mem save mode
1 parent c193190 commit 7a0c1fb

5 files changed

Lines changed: 119 additions & 124 deletions

File tree

README.md

Lines changed: 18 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,18 @@ For original PSGD repo, see [psgd_torch](https://github.com/lixilinx/psgd_torch)
44

55
For JAX version, see [psgd_jax](https://github.com/evanatyourservice/psgd_jax).
66

7-
Implementation of [PSGD Kron optimizer](https://github.com/lixilinx/psgd_torch) in PyTorch.
7+
Implementations of [PSGD optimizers](https://github.com/lixilinx/psgd_torch) in JAX (optax-style).
88
PSGD is a second-order optimizer originally created by Xi-Lin Li that uses either a hessian-based
99
or whitening-based (gg^T) preconditioner and lie groups to improve training convergence,
1010
generalization, and efficiency. I highly suggest taking a look at Xi-Lin's PSGD repo's readme linked
11-
to above for interesting details on how PSGD works and experiments using PSGD.
11+
to above for interesting details on how PSGD works and experiments using PSGD. There are also
12+
paper resources listed near the bottom of this readme.
1213

1314
### `kron`:
1415

1516
The most versatile and easy-to-use PSGD optimizer is `kron`, which uses a Kronecker-factored
1617
preconditioner. It has less hyperparameters that need tuning than adam, and can generally act as a
17-
drop-in replacement for adam.
18+
drop-in replacement.
1819

1920
## Installation
2021

@@ -26,7 +27,7 @@ pip install kron-torch
2627

2728
Kron schedules the preconditioner update probability by default to start at 1.0 and anneal to 0.03
2829
at the beginning of training, so training will be slightly slower at the start but will speed up
29-
to near adam's speed by around 3k steps.
30+
by around 4k steps.
3031

3132
For basic usage, use `kron` optimizer like any other pytorch optimizer:
3233

@@ -45,48 +46,34 @@ optimizer.step()
4546
TLDR: Learning rate and weight decay act similarly to adam's, start with adam-like settings and go
4647
from there. There is no b2 or epsilon.
4748

48-
These next settings control whether a dimension's preconditioner is diagonal or triangular.
49+
These next 3 settings control whether a dimension's preconditioner is diagonal or triangular.
4950
For example, for a layer with shape (256, 128), triagular preconditioners would be shapes (256, 256)
5051
and (128, 128), and diagonal preconditioners would be shapes (256,) and (128,). Depending on how
51-
these settings are chosen, `kron` can balance between memory/speed and effectiveness (see below).
52+
these settings are chosen, `kron` can balance between memory/speed and effectiveness. Defaults lead
53+
to most precoditioners being triangular except for 1-dimensional layers and very large dimensions.
5254

53-
`max_size_triangular`: Anything above this value will have a diagonal preconditioner, anything
54-
below will have a triangular preconditioner. So if you have a dim with size 16,384 that you want
55-
to use a diagonal preconditioner for, set `max_size_triangular` to something like 15,000. Default
56-
is 8192.
57-
58-
`max_skew_triangular`: Any tensor with skew above this value with make the larger dim diagonal.
59-
For example, if `max_skew_triangular` = 10, a bias layer of shape (256,) would be diagonal
60-
because 256/1 > 10, and an embedding layer with shape (50000, 768) would be (diag, tri)
61-
because 50000/768 is greater than 10. The default value is 'inf'.
55+
`max_size_triangular`: Any dimension with size above this value will have a diagonal preconditioner.
6256

6357
`min_ndim_triangular`: Any tensor with less than this number of dims will have all diagonal
64-
preconditioners. Default is 2, so single-dim tensors like bias and scale will use diagonal
58+
preconditioners. Default is 2, so single-dim layers like bias and scale will use diagonal
6559
preconditioners.
6660

67-
Interesting setups using these settings:
68-
69-
- Setting `max_size_triangular` to 0 will make all layers have diagonal preconditioners, which uses
70-
very little memory and runs the fastest, but the optimizer might be less effective.
71-
72-
- With `max_skew_triangular` set to 1, if a layer has one dim larger than the rest, it will use a diagonal
73-
preconditioner. This setup usually results in less memory usage than adam, and is more performant
74-
than having all diagonal preconditioners.
61+
`memory_save_mode`: Can be None, 'one_diag', or 'all_diag'. None is default and lets all
62+
preconditioners be triangular. 'one_diag' sets the largest or last dim per layer as diagonal
63+
using `np.argsort(shape)[::-1][0]`. 'all_diag' sets all preconditioners to be diagonal.
7564

7665
`preconditioner_update_probability`: Preconditioner update probability uses a schedule by default
7766
that works well for most cases. It anneals from 1 to 0.03 at the beginning of training, so training
78-
will be slightly slower at the start but will speed up to near adam's speed by around 3k steps. PSGD
79-
generally benefits from more preconditioner updates at the start of training, but once the preconditioner
80-
is learned it's okay to do them less often.
67+
will be slightly slower at the start but will speed up by around 4k steps. PSGD generally benefits
68+
from more preconditioner updates at the start of training, but once the preconditioner is learned
69+
it's okay to do them less often. An easy way to adjust update frequency is to adjust `min_prob`
70+
from `precond_update_prob_schedule`.
8171

82-
This is the default schedule in the `precond_update_prob_schedule` function at the top of kron.py:
72+
This is the default schedule from the `precond_update_prob_schedule` function at the top of kron.py:
8373

8474
<img src="assets/default_schedule.png" alt="Default Schedule" width="800" style="max-width: 100%; height: auto;" />
8575

8676

87-
See kron.py for more hyperparameter details.
88-
89-
9077
## Resources
9178

9279
PSGD papers and resources listed from Xi-Lin's repo

assets/default_schedule.png

82.3 KB
Loading

kron_torch/kron.py

Lines changed: 61 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
1+
import numpy as np
12
import string
23
import torch
34

45

56
torch._dynamo.config.cache_size_limit = 1_000_000
67

78
try:
8-
torch.backends.opt_einsum.strategy = 'dynamic-programming'
9+
torch.backends.opt_einsum.strategy = "dynamic-programming"
910
except AttributeError:
1011
# opt_einsum backend is not available, so we'll skip setting the strategy
1112
pass
1213

1314

1415
def precond_update_prob_schedule(
15-
max_prob=1.0, min_prob=0.03, decay=0.001, flat_start=200
16+
max_prob=1.0, min_prob=0.03, decay=0.001, flat_start=250
1617
):
1718
"""Anneal preconditioner update probability during beginning of training.
1819
@@ -45,21 +46,24 @@ class Kron(torch.optim.Optimizer):
4546
Args:
4647
params (iterable): Iterable of parameters to optimize or dicts defining
4748
parameter groups.
48-
lr (float, optional): Learning rate (default: 0.001).
49-
b1 (float, optional): Momentum parameter (default: 0.9).
50-
weight_decay (float, optional): Weight decay (L2 penalty) (default: 0.0).
49+
lr (float): Learning rate.
50+
b1 (float): Momentum parameter.
51+
weight_decay (float): Weight decay (L2 penalty).
5152
preconditioner_update_probability (callable or float, optional): Probability of
5253
updating the preconditioner. If None, defaults to a schedule that anneals
5354
from 1.0 to 0.03 by 4000 steps.
54-
max_size_triangular (int, optional): Max size for dim's preconditioner to be
55-
triangular (default: 8192).
56-
max_skew_triangular (float, optional): Max skew for dim's preconditioner to be
57-
triangular (default: inf).
58-
min_ndim_triangular (int, optional): Minimum number of dimensions a layer needs
59-
to have triangular preconditioners (default: 2).
60-
mu_dtype (torch.dtype, optional): Dtype of the momentum accumulator. Defaults
61-
to the same dtype as the parameters.
62-
precond_dtype (torch.dtype, optional): Dtype of the preconditioner (default: None).
55+
max_size_triangular (int): Max size for dim's preconditioner to be triangular.
56+
min_ndim_triangular (int): Minimum number of dimensions a layer needs
57+
to have triangular preconditioners.
58+
memory_save_mode: (string, optional), None, 'one_diag', or 'all_diag', None is default
59+
to set all preconditioners to be triangular, 'one_diag' sets the largest
60+
or last dim to be diagonal per layer, and 'all_diag' sets all preconditioners
61+
to be diagonal.
62+
mu_dtype (torch.dtype, optional): Dtype of the momentum accumulator.
63+
precond_dtype (torch.dtype, optional): Dtype of the preconditioner.
64+
trust_region_scale (float): Trust region on preconditioned grads. Normally this
65+
doesn't need to be changed but if things seem unstable you can try reducing
66+
this to 1.5.
6367
"""
6468

6569
def __init__(
@@ -70,10 +74,11 @@ def __init__(
7074
weight_decay=0.0,
7175
preconditioner_update_probability=None,
7276
max_size_triangular=8192,
73-
max_skew_triangular=float("inf"),
7477
min_ndim_triangular=2,
78+
memory_save_mode=None,
7579
mu_dtype=None,
7680
precond_dtype=None,
81+
trust_region_scale=2.0,
7782
):
7883
if not 0.0 <= lr:
7984
raise ValueError(f"Invalid learning rate: {lr}")
@@ -91,26 +96,17 @@ def __init__(
9196
weight_decay=weight_decay,
9297
preconditioner_update_probability=preconditioner_update_probability,
9398
max_size_triangular=max_size_triangular,
94-
max_skew_triangular=max_skew_triangular,
9599
min_ndim_triangular=min_ndim_triangular,
100+
memory_save_mode=memory_save_mode,
96101
precond_lr=0.1, # precond lr hardcoded to 0.1
97102
precond_init_scale=1.0, # precond init scale hardcoded to 1.0
98103
mu_dtype=mu_dtype,
99104
precond_dtype=precond_dtype,
105+
trust_region_scale=trust_region_scale,
100106
)
101107
super(Kron, self).__init__(params, defaults)
102108

103-
self._global_clip = (
104-
sum(
105-
p.numel()
106-
for group in self.param_groups
107-
for p in group["params"]
108-
if p.requires_grad
109-
)
110-
** 0.5
111-
)
112-
self._element_clip = 1.0
113-
self._tiny = 1e-30
109+
self._tiny = torch.finfo(torch.bfloat16).tiny
114110
self._prob_step = 0
115111

116112
@torch.no_grad()
@@ -132,7 +128,7 @@ def step(self, closure=None):
132128
device = self.param_groups[0]["params"][0].device
133129
do_update = torch.rand([], device=device) < update_prob
134130
self._prob_step += 1
135-
131+
136132
balance = torch.rand([], device=device) < 0.01 and do_update
137133

138134
for group in self.param_groups:
@@ -155,8 +151,8 @@ def step(self, closure=None):
155151
p,
156152
group["precond_init_scale"],
157153
group["max_size_triangular"],
158-
group["max_skew_triangular"],
159154
group["min_ndim_triangular"],
155+
group["memory_save_mode"],
160156
dtype=precond_dtype,
161157
)
162158

@@ -206,8 +202,10 @@ def step(self, closure=None):
206202
).to(dtype=p.dtype, non_blocking=True)
207203

208204
# Apply trust region
209-
torch.nn.utils.clip_grad_norm_(pre_grad, self._global_clip)
210-
pre_grad.clamp_(-self._element_clip, self._element_clip)
205+
pre_grad = (
206+
torch.tanh(pre_grad / group["trust_region_scale"])
207+
* group["trust_region_scale"]
208+
)
211209

212210
# Apply weight decay and update parameters
213211
if group["weight_decay"] != 0 and p.dim() >= 2:
@@ -231,7 +229,7 @@ def step(self, closure=None):
231229
return loss
232230

233231

234-
def init_Q_exprs(t, scale, max_size, max_skew, min_ndim_triangular, dtype=None):
232+
def init_Q_exprs(t, scale, max_size, min_ndim_triangular, memory_save_mode, dtype=None):
235233
"""For a scalar or tensor t, we initialize its preconditioner Q and
236234
reusable einsum expressions for updating Q and preconditioning gradient.
237235
"""
@@ -242,30 +240,40 @@ def init_Q_exprs(t, scale, max_size, max_skew, min_ndim_triangular, dtype=None):
242240
if len(shape) == 0: # scalar
243241
Q = [scale * torch.ones_like(t, dtype=dtype)]
244242
exprA = ",->,"
245-
exprP = ",,->,"
246243
exprGs = [",->"]
244+
exprP = ",,->,"
247245
else: # tensor
248246
if len(shape) > 13:
249247
raise ValueError(
250248
f"Got tensor with dim {len(t.shape)}; Einstein runs out of letters!"
251249
)
252250

253251
scale = scale ** (1 / len(shape))
254-
if len(shape) == 1:
255-
beta_size = 1 # 2nd largest size
252+
253+
if memory_save_mode is None:
254+
dim_diag = [False for _ in shape]
255+
elif memory_save_mode == "one_diag":
256+
rev_sorted_dims = np.argsort(shape)[::-1]
257+
dim_diag = [False for _ in shape]
258+
dim_diag[rev_sorted_dims[0]] = True
259+
elif memory_save_mode == "all_diag":
260+
dim_diag = [True for _ in shape]
256261
else:
257-
beta_size = sorted(list(shape))[-2]
262+
raise ValueError(
263+
f"Invalid memory_save_mode: {memory_save_mode}, must be one of "
264+
"[None, 'one_diag', 'all_diag']"
265+
)
258266

259267
Q = []
260-
exprGs = []
261268
piece1A, piece2A, piece3A = ([], "", "")
269+
exprGs = []
262270
piece1P, piece2P, piece3P, piece4P = ([], [], "", "")
263-
for i, size in enumerate(shape):
271+
for i, (size, dim_d) in enumerate(zip(shape, dim_diag)):
264272
if (
265273
size == 1
266274
or size > max_size
267-
or size > max_skew * beta_size
268275
or len(shape) < min_ndim_triangular
276+
or dim_d
269277
):
270278
# use diagonal matrix as preconditioner for this dim
271279
Q.append(scale * torch.ones(size, dtype=dtype, device=t.device))
@@ -274,19 +282,19 @@ def init_Q_exprs(t, scale, max_size, max_skew, min_ndim_triangular, dtype=None):
274282
piece2A = piece2A + letters[i]
275283
piece3A = piece3A + letters[i]
276284

277-
piece1P.append(letters[i + 13])
278-
piece2P.append(letters[i + 13])
279-
piece3P = piece3P + letters[i + 13]
280-
piece4P = piece4P + letters[i + 13]
281-
282285
piece1 = "".join(
283286
[
284-
(letters[j + 13] if j == i else letters[j])
287+
(letters[i + 13] if j == i else letters[j])
285288
for j in range(len(shape))
286289
]
287290
)
288291
subscripts = piece1 + "," + piece1 + "->" + letters[i + 13]
289292
exprGs.append(subscripts)
293+
294+
piece1P.append(letters[i + 13])
295+
piece2P.append(letters[i + 13])
296+
piece3P = piece3P + letters[i + 13]
297+
piece4P = piece4P + letters[i + 13]
290298
else:
291299
# use triangular matrix as preconditioner for this dim
292300
Q.append(scale * torch.eye(size, dtype=dtype, device=t.device))
@@ -295,21 +303,15 @@ def init_Q_exprs(t, scale, max_size, max_skew, min_ndim_triangular, dtype=None):
295303
piece2A = piece2A + letters[i + 13]
296304
piece3A = piece3A + letters[i]
297305

298-
a, b, c = (letters[i], letters[i + 13], letters[i + 26])
299-
piece1P.append(a + b)
300-
piece2P.append(a + c)
301-
piece3P = piece3P + c
302-
piece4P = piece4P + b
303-
304306
piece1 = "".join(
305307
[
306-
(letters[j + 13] if j == i else letters[j])
308+
(letters[i + 13] if j == i else letters[j])
307309
for j in range(len(shape))
308310
]
309311
)
310312
piece2 = "".join(
311313
[
312-
(letters[j + 26] if j == i else letters[j])
314+
(letters[i + 26] if j == i else letters[j])
313315
for j in range(len(shape))
314316
]
315317
)
@@ -318,6 +320,12 @@ def init_Q_exprs(t, scale, max_size, max_skew, min_ndim_triangular, dtype=None):
318320
)
319321
exprGs.append(subscripts)
320322

323+
a, b, c = (letters[i], letters[i + 13], letters[i + 26])
324+
piece1P.append(a + b)
325+
piece2P.append(a + c)
326+
piece3P = piece3P + c
327+
piece4P = piece4P + b
328+
321329
exprA = ",".join(piece1A) + "," + piece2A + "->" + piece3A
322330
exprP = (
323331
",".join(piece1P) + "," + ",".join(piece2P) + "," + piece3P + "->" + piece4P

0 commit comments

Comments
 (0)