NeuralGCM / model /legacy /optimization.py
yzt15806542928's picture
Upload folder using huggingface_hub
f4a39ee verified
Raw
History Blame Contribute Delete
6.36 kB
# Copyright 2024 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Configurable optimizers from JAX."""
import collections
import re
from typing import Sequence
import gin
import optax
gin.external_configurable(optax.adabelief, module='optax')
gin.external_configurable(optax.adam, module='optax')
gin.external_configurable(optax.adamw, module='optax')
gin.external_configurable(optax.constant_schedule, module='optax')
gin.external_configurable(optax.join_schedules, module='optax')
gin.external_configurable(optax.piecewise_constant_schedule, module='optax')
gin.external_configurable(optax.exponential_decay, module='optax')
gin.external_configurable(
optax.warmup_exponential_decay_schedule, module='optax'
)
class OptimizerError(Exception):
"""Raised if a custom Whirl optimizer encounters an error."""
@gin.configurable
def optimizer(value):
return value
OptState = collections.namedtuple('OptState', ['state', 'params'])
@gin.register
def piecewise_constant_schedule_specified_by_rates(
rates: Sequence[float],
boundaries: Sequence[int],
) -> optax.Schedule:
"""Schedule that is piecewise constant and specified by rates (not scales).
This is similar to optax.piecewise_constant_schedule, which requires users
to specify "scales" (ratio of old LR to new LR).
Args:
rates: Length K sequence of learning rates. `rates[i]` is used for steps
`0 <= step < boundaries[1]`, for i=0, and
`boundaries[i-1] <= step < boundaries[i]`, for 0 < i < len(boundaries)
`boundaries[i-1] <= step < ∞`, for i = len(boundaries)
boundaries: Length K-1 sequence of boundaries.
Returns:
Schedule to pass to optax optimizers.
"""
return optax.join_schedules(
schedules=[optax.constant_schedule(r) for r in rates],
boundaries=boundaries,
)
@gin.register
def delayed_constant_schedule(
turn_on_step: int,
rate: float,
) -> optax.Schedule:
"""Schedule that is zero until `turn_on_step` then `rate` thereafter."""
return piecewise_constant_schedule_specified_by_rates(
rates=[0., rate],
boundaries=[turn_on_step],
)
@gin.register
def top_level_multi_adam(
top_level_keys: Sequence[str] = (),
learning_rates: Sequence[optax.ScalarOrSchedule] = (),
default_learning_rate: optax.ScalarOrSchedule = 1e-4,
b1: float = 0.9,
b2: float = 0.95,
eps: float = 1e-6,
raise_if_keys_not_found: bool = True,
) -> optax.GradientTransformation:
"""Uses an Adam optimizer with different learning rates for different params.
Args:
top_level_keys: Keys to use non-default learning rates for. A key starting
with 'REGEX_', such as 'REGEX_cats' will use re.search to find keys, e.g.
re.search('cats', key).
learning_rates: Learning rates to use leafs under the `top_level_keys`.
default_learning_rate: Learning rate to use for keys not in `learning_rates`
b1: Exponential decay to track the first moment of past gradients.
b2: Exponential decay to track the second moment of past gradients.
eps: A small constant applied to denominator outside of the square root to
avoid dividing by zero when rescaling.
raise_if_keys_not_found: Whether to raise if some `top_level_keys` are not
found in params.
Returns:
optax optimizer with learning rate based on top level key in params dict.
"""
if len(top_level_keys) != len(learning_rates):
raise ValueError(
f'{top_level_keys=} had different length than {learning_rates=}'
)
if '' in top_level_keys:
raise ValueError('An empty string "" was found in `top_level_keys`.')
default_label = 'DEFAULT_LABEL'
if default_label in top_level_keys:
raise ValueError(f'{default_label=} should not be in `top_level_keys`')
def find_matching_top_level_key(param_name: str) -> str:
"""Searches for param_name in top_level_keys, returns the matching key."""
prefix = 'REGEX_'
matches = []
for k in top_level_keys:
if k.startswith(prefix) and re.search(k.lstrip(prefix), param_name):
matches.append(k)
elif k == param_name:
matches.append(k)
if not matches:
return default_label
elif len(matches) == 1:
return matches[0]
else:
raise ValueError(
f'{param_name=} had more than 1 ({len(matches)}) match '
f'({matches}). Only one `top_level_keys` should match, or else we '
'cannot choose a unique learning rate for these parameters.'
)
def get_prefix_labels(params):
"""Makes prefix labels to help optax match params with learning rates."""
# E.g. if top_level_keys = ['module_A', 'REGEX_special'],
# and params.keys() = ['module_A', 'special_A', 'special_B', 'module_C'],
# labels = {
# 'module_A': 'module_A',
# 'special_A': 'REGEX_special', 'special_B': 'REGEX_special',
# 'module_C': 'DEFAULT_LABEL', 'module_D': 'DEFAULT_LABEL',...
# }
# E.g. labels tells optax to use the learning rate 'REGEX_special' for
# parameters under the prefix 'module_C'.
labels = {
param_name: find_matching_top_level_key(param_name)
for param_name in params
}
top_level_keys_that_matched = [
k for k in labels.values() if k != default_label
]
missing_keys = set(top_level_keys).difference(top_level_keys_that_matched)
if raise_if_keys_not_found and missing_keys:
raise OptimizerError(
f'{missing_keys=} not found in params: {sorted(params)}'
)
return labels
def make_adam(lr):
return optax.adam(lr, b1=b1, b2=b2, eps=eps)
return optax.multi_transform(
transforms={ # pyrefly: ignore[bad-argument-type]
k: make_adam(lr) for k, lr in zip(top_level_keys, learning_rates)
}
| {default_label: make_adam(default_learning_rate)},
param_labels=get_prefix_labels,
)