ludwig-ai--ludwig
593b94c120
pytest / Unit Tests (push) Has been cancelled
pytest / Integration (integration_tests_a) (push) Has been cancelled
pytest / Integration (integration_tests_b) (push) Has been cancelled
pytest / Integration (integration_tests_c) (push) Has been cancelled
pytest / Integration (integration_tests_d) (push) Has been cancelled
pytest / Integration (integration_tests_e) (push) Has been cancelled
pytest / Integration (integration_tests_f) (push) Has been cancelled
pytest / Integration (integration_tests_g) (push) Has been cancelled
pytest / Integration (integration_tests_h) (push) Has been cancelled
pytest / Integration (integration_tests_i) (push) Has been cancelled
pytest / Integration (integration_tests_j) (push) Has been cancelled
pytest / Distributed (distributed_a) (push) Has been cancelled
pytest / Distributed (distributed_b) (push) Has been cancelled
pytest / Distributed (distributed_c) (push) Has been cancelled
pytest / Distributed (distributed_d) (push) Has been cancelled
pytest / Distributed (distributed_e) (push) Has been cancelled
pytest / Distributed (distributed_f) (push) Has been cancelled
pytest / Minimal Install (push) Has been cancelled
pytest / Event File (push) Has been cancelled
pytest (slow) / py-slow (push) Has been cancelled
Publish JSON Schema / publish-schema (push) Has been cancelled
1358 行
48 KiB
Python
1358 行
48 KiB
Python
import math
|
|
from abc import ABC
|
|
from dataclasses import field
|
|
from typing import ClassVar
|
|
|
|
import pydantic
|
|
import torch
|
|
|
|
try:
|
|
import bitsandbytes as bnb
|
|
except ImportError:
|
|
bnb = None
|
|
|
|
try:
|
|
from transformers.optimization import Adafactor as _TransformersAdafactor
|
|
except ImportError:
|
|
_TransformersAdafactor = None
|
|
|
|
try:
|
|
from schedulefree import AdamWScheduleFree as _AdamWScheduleFree
|
|
except ImportError:
|
|
_AdamWScheduleFree = None
|
|
|
|
try:
|
|
import soap as _soap_module
|
|
|
|
_SOAPOptimizer = getattr(_soap_module, "SOAP", None)
|
|
except ImportError:
|
|
_SOAPOptimizer = None
|
|
|
|
import ludwig.schema.utils as schema_utils
|
|
from ludwig.api_annotations import DeveloperAPI
|
|
from ludwig.error import ConfigValidationError
|
|
from ludwig.schema.metadata import OPTIMIZER_METADATA
|
|
from ludwig.schema.metadata.parameter_metadata import convert_metadata_to_json, ParameterMetadata
|
|
from ludwig.utils.registry import Registry
|
|
|
|
optimizer_registry = Registry()
|
|
|
|
|
|
@DeveloperAPI
|
|
def register_optimizer(name: str):
|
|
def wrap(optimizer_config: BaseOptimizerConfig):
|
|
optimizer_registry[name] = (optimizer_config.optimizer_class, optimizer_config)
|
|
return optimizer_config
|
|
|
|
return wrap
|
|
|
|
|
|
@DeveloperAPI
|
|
def get_optimizer_cls(name: str):
|
|
"""Get the optimizer schema class from the optimizer schema class registry."""
|
|
return optimizer_registry[name][1]
|
|
|
|
|
|
@DeveloperAPI
|
|
class BaseOptimizerConfig(schema_utils.LudwigBaseConfig, ABC):
|
|
"""Base class for optimizers. Not meant to be used directly.
|
|
|
|
The dataclass format prevents arbitrary properties from being set. Consequently, in child classes, all properties
|
|
from the corresponding `torch.optim.Optimizer` class are copied over: check each class to check which attributes are
|
|
different from the torch-specified defaults.
|
|
"""
|
|
|
|
optimizer_class: ClassVar[torch.optim.Optimizer | None] = None
|
|
"Class variable pointing to the corresponding `torch.optim.Optimizer` class."
|
|
|
|
type: str
|
|
"""Name corresponding to an optimizer `ludwig.modules.optimization_modules.optimizer_registry`.
|
|
|
|
Technically mutable, but attempting to load a derived optimizer with `type` set to a mismatched value will result in
|
|
a `ValidationError`.
|
|
"""
|
|
|
|
@property
|
|
def is_paged(self) -> bool:
|
|
"""Returns True if the optimizer is a Paged optimizer."""
|
|
return False
|
|
|
|
@property
|
|
def is_8bit(self) -> bool:
|
|
"""Returns True if the optimizer is an 8-bit optimizer."""
|
|
return False
|
|
|
|
|
|
@DeveloperAPI
|
|
@register_optimizer(name="sgd")
|
|
class SGDOptimizerConfig(BaseOptimizerConfig):
|
|
"""Parameters for stochastic gradient descent."""
|
|
|
|
optimizer_class: ClassVar[torch.optim.Optimizer] = torch.optim.SGD
|
|
"""Points to `torch.optim.SGD`."""
|
|
|
|
type: str = schema_utils.ProtectedString("sgd")
|
|
"""Must be 'sgd' - corresponds to name in `ludwig.modules.optimization_modules.optimizer_registry` (default:
|
|
'sgd')"""
|
|
|
|
# Defaults taken from https://pytorch.org/docs/stable/generated/torch.optim.SGD.html#torch.optim.SGD :
|
|
momentum: float = schema_utils.NonNegativeFloat(
|
|
default=0.0,
|
|
description="Momentum factor.",
|
|
parameter_metadata=OPTIMIZER_METADATA["momentum"],
|
|
)
|
|
|
|
weight_decay: float = schema_utils.NonNegativeFloat(
|
|
default=0.0,
|
|
description="Weight decay ($L2$ penalty).",
|
|
parameter_metadata=OPTIMIZER_METADATA["weight_decay"],
|
|
)
|
|
|
|
dampening: float = schema_utils.NonNegativeFloat(
|
|
default=0.0,
|
|
description="Dampening for momentum.",
|
|
parameter_metadata=OPTIMIZER_METADATA["dampening"],
|
|
)
|
|
|
|
nesterov: bool = schema_utils.Boolean(
|
|
default=False,
|
|
description="Enables Nesterov momentum.",
|
|
parameter_metadata=OPTIMIZER_METADATA["nesterov"],
|
|
)
|
|
|
|
|
|
if bnb is not None:
|
|
|
|
@DeveloperAPI
|
|
@register_optimizer(name="sgd_8bit")
|
|
class SGD8BitOptimizerConfig(SGDOptimizerConfig):
|
|
"""Parameters for stochastic gradient descent."""
|
|
|
|
optimizer_class: ClassVar[torch.optim.Optimizer] = bnb.optim.SGD8bit
|
|
|
|
type: str = schema_utils.ProtectedString("sgd_8bit")
|
|
|
|
block_wise: bool = schema_utils.Boolean(
|
|
default=False,
|
|
description="Whether to use block wise update.",
|
|
)
|
|
|
|
percentile_clipping: int = schema_utils.IntegerRange(
|
|
default=100,
|
|
min=0,
|
|
max=100,
|
|
description="Percentile clipping.",
|
|
)
|
|
|
|
@property
|
|
def is_8bit(self) -> bool:
|
|
return True
|
|
|
|
|
|
@DeveloperAPI
|
|
@register_optimizer(name="lbfgs")
|
|
class LBFGSOptimizerConfig(BaseOptimizerConfig):
|
|
"""Parameters for stochastic gradient descent."""
|
|
|
|
optimizer_class: ClassVar[torch.optim.Optimizer] = torch.optim.LBFGS
|
|
"""Points to `torch.optim.LBFGS`."""
|
|
|
|
type: str = schema_utils.ProtectedString("lbfgs")
|
|
"""Must be 'lbfgs' - corresponds to name in `ludwig.modules.optimization_modules.optimizer_registry` (default:
|
|
'lbfgs')"""
|
|
|
|
# Defaults taken from https://pytorch.org/docs/stable/generated/torch.optim.LBFGS.html#torch.optim.LBFGS
|
|
max_iter: int = schema_utils.Integer(
|
|
default=20,
|
|
description="Maximum number of iterations per optimization step.",
|
|
parameter_metadata=OPTIMIZER_METADATA["max_iter"],
|
|
)
|
|
|
|
max_eval: int = schema_utils.Integer(
|
|
default=None,
|
|
allow_none=True,
|
|
description="Maximum number of function evaluations per optimization step. Default: `max_iter` * 1.25.",
|
|
parameter_metadata=OPTIMIZER_METADATA["max_eval"],
|
|
)
|
|
|
|
tolerance_grad: float = schema_utils.NonNegativeFloat(
|
|
default=1e-07,
|
|
description="Termination tolerance on first order optimality.",
|
|
parameter_metadata=OPTIMIZER_METADATA["tolerance_grad"],
|
|
)
|
|
|
|
tolerance_change: float = schema_utils.NonNegativeFloat(
|
|
default=1e-09,
|
|
description="Termination tolerance on function value/parameter changes.",
|
|
parameter_metadata=OPTIMIZER_METADATA["tolerance_change"],
|
|
)
|
|
|
|
history_size: int = schema_utils.Integer(
|
|
default=100, description="Update history size.", parameter_metadata=OPTIMIZER_METADATA["history_size"]
|
|
)
|
|
|
|
line_search_fn: str = schema_utils.StringOptions(
|
|
["strong_wolfe"],
|
|
default=None,
|
|
allow_none=True,
|
|
description="Line search function to use.",
|
|
parameter_metadata=OPTIMIZER_METADATA["line_search_fn"],
|
|
)
|
|
|
|
|
|
@DeveloperAPI
|
|
@register_optimizer(name="adam")
|
|
class AdamOptimizerConfig(BaseOptimizerConfig):
|
|
"""Parameters for adam optimization."""
|
|
|
|
optimizer_class: ClassVar[torch.optim.Optimizer] = torch.optim.Adam
|
|
"""Points to `torch.optim.Adam`."""
|
|
|
|
type: str = schema_utils.ProtectedString("adam")
|
|
"""Must be 'adam' - corresponds to name in `ludwig.modules.optimization_modules.optimizer_registry`
|
|
(default: 'adam')"""
|
|
|
|
# Defaults taken from https://pytorch.org/docs/stable/generated/torch.optim.Adam.html#torch.optim.Adam :
|
|
betas: tuple[float, float] = schema_utils.FloatRangeTupleDataclassField(
|
|
default=(0.9, 0.999),
|
|
description="Coefficients used for computing running averages of gradient and its square.",
|
|
parameter_metadata=OPTIMIZER_METADATA["betas"],
|
|
)
|
|
|
|
eps: float = schema_utils.NonNegativeFloat(
|
|
default=1e-08,
|
|
description="Term added to the denominator to improve numerical stability.",
|
|
parameter_metadata=OPTIMIZER_METADATA["eps"],
|
|
)
|
|
|
|
weight_decay: float = schema_utils.NonNegativeFloat(
|
|
default=0.0, description="Weight decay (L2 penalty).", parameter_metadata=OPTIMIZER_METADATA["weight_decay"]
|
|
)
|
|
|
|
amsgrad: bool = schema_utils.Boolean(
|
|
default=False,
|
|
description="Whether to use the AMSGrad variant of this algorithm from the paper 'On the Convergence of Adam "
|
|
"and Beyond'.",
|
|
parameter_metadata=OPTIMIZER_METADATA["amsgrad"],
|
|
)
|
|
|
|
|
|
if bnb is not None:
|
|
|
|
@DeveloperAPI
|
|
@register_optimizer(name="adam_8bit")
|
|
class Adam8BitOptimizerConfig(AdamOptimizerConfig):
|
|
optimizer_class: ClassVar[torch.optim.Optimizer] = bnb.optim.Adam8bit
|
|
|
|
type: str = schema_utils.ProtectedString("adam_8bit")
|
|
|
|
block_wise: bool = schema_utils.Boolean(
|
|
default=True,
|
|
description="Whether to use block wise update.",
|
|
)
|
|
|
|
percentile_clipping: int = schema_utils.IntegerRange(
|
|
default=100,
|
|
min=0,
|
|
max=100,
|
|
description="Percentile clipping.",
|
|
)
|
|
|
|
@property
|
|
def is_8bit(self) -> bool:
|
|
return True
|
|
|
|
@DeveloperAPI
|
|
@register_optimizer(name="paged_adam")
|
|
class PagedAdamOptimizerConfig(Adam8BitOptimizerConfig):
|
|
optimizer_class: ClassVar[torch.optim.Optimizer] = bnb.optim.PagedAdam
|
|
|
|
type: str = schema_utils.ProtectedString("paged_adam")
|
|
|
|
@property
|
|
def is_paged(self) -> bool:
|
|
return True
|
|
|
|
@property
|
|
def is_8bit(self) -> bool:
|
|
return False
|
|
|
|
@DeveloperAPI
|
|
@register_optimizer(name="paged_adam_8bit")
|
|
class PagedAdam8BitOptimizerConfig(PagedAdamOptimizerConfig):
|
|
optimizer_class: ClassVar[torch.optim.Optimizer] = bnb.optim.PagedAdam8bit
|
|
|
|
type: str = schema_utils.ProtectedString("paged_adam_8bit")
|
|
|
|
@property
|
|
def is_8bit(self) -> bool:
|
|
return True
|
|
|
|
|
|
@DeveloperAPI
|
|
@register_optimizer(name="adamw")
|
|
class AdamWOptimizerConfig(BaseOptimizerConfig):
|
|
"""Parameters for adamw optimization."""
|
|
|
|
optimizer_class: ClassVar[torch.optim.Optimizer] = torch.optim.AdamW
|
|
"""Points to `torch.optim.AdamW`."""
|
|
|
|
type: str = schema_utils.ProtectedString("adamw")
|
|
"""Must be 'adamw' - corresponds to name in `ludwig.modules.optimization_modules.optimizer_registry`
|
|
(default: 'adamw')"""
|
|
|
|
# Defaults taken from https://pytorch.org/docs/stable/generated/torch.optim.Adam.html#torch.optim.Adam :
|
|
betas: tuple[float, float] = schema_utils.FloatRangeTupleDataclassField(
|
|
default=(0.9, 0.999),
|
|
description="Coefficients used for computing running averages of gradient and its square.",
|
|
parameter_metadata=OPTIMIZER_METADATA["betas"],
|
|
)
|
|
|
|
eps: float = schema_utils.NonNegativeFloat(
|
|
default=1e-08,
|
|
description="Term added to the denominator to improve numerical stability.",
|
|
parameter_metadata=OPTIMIZER_METADATA["eps"],
|
|
)
|
|
|
|
weight_decay: float = schema_utils.NonNegativeFloat(
|
|
default=0.0, description="Weight decay ($L2$ penalty).", parameter_metadata=OPTIMIZER_METADATA["weight_decay"]
|
|
)
|
|
|
|
amsgrad: bool = schema_utils.Boolean(
|
|
default=False,
|
|
description="Whether to use the AMSGrad variant of this algorithm from the paper 'On the Convergence of Adam "
|
|
"and Beyond'. ",
|
|
parameter_metadata=OPTIMIZER_METADATA["amsgrad"],
|
|
)
|
|
|
|
|
|
if bnb is not None:
|
|
|
|
@DeveloperAPI
|
|
@register_optimizer(name="adamw_8bit")
|
|
class AdamW8BitOptimizerConfig(AdamWOptimizerConfig):
|
|
optimizer_class: ClassVar[torch.optim.Optimizer] = bnb.optim.AdamW8bit
|
|
|
|
type: str = schema_utils.ProtectedString("adamw_8bit")
|
|
|
|
block_wise: bool = schema_utils.Boolean(
|
|
default=True,
|
|
description="Whether to use block wise update.",
|
|
)
|
|
|
|
percentile_clipping: int = schema_utils.IntegerRange(
|
|
default=100,
|
|
min=0,
|
|
max=100,
|
|
description="Percentile clipping.",
|
|
)
|
|
|
|
@property
|
|
def is_8bit(self) -> bool:
|
|
return True
|
|
|
|
@DeveloperAPI
|
|
@register_optimizer(name="paged_adamw")
|
|
class PagedAdamWOptimizerConfig(AdamW8BitOptimizerConfig):
|
|
optimizer_class: ClassVar[torch.optim.Optimizer] = bnb.optim.PagedAdamW
|
|
|
|
type: str = schema_utils.ProtectedString("paged_adamw")
|
|
|
|
@property
|
|
def is_paged(self) -> bool:
|
|
return True
|
|
|
|
@property
|
|
def is_8bit(self) -> bool:
|
|
return False
|
|
|
|
@DeveloperAPI
|
|
@register_optimizer(name="paged_adamw_8bit")
|
|
class PagedAdamW8BitOptimizerConfig(PagedAdamWOptimizerConfig):
|
|
optimizer_class: ClassVar[torch.optim.Optimizer] = bnb.optim.PagedAdamW8bit
|
|
|
|
type: str = schema_utils.ProtectedString("paged_adamw_8bit")
|
|
|
|
@property
|
|
def is_8bit(self) -> bool:
|
|
return True
|
|
|
|
|
|
@DeveloperAPI
|
|
@register_optimizer(name="adadelta")
|
|
class AdadeltaOptimizerConfig(BaseOptimizerConfig):
|
|
"""Parameters for adadelta optimization."""
|
|
|
|
optimizer_class: ClassVar[torch.optim.Optimizer] = torch.optim.Adadelta
|
|
"""Points to `torch.optim.Adadelta`."""
|
|
|
|
type: str = schema_utils.ProtectedString("adadelta")
|
|
"""Must be 'adadelta' - corresponds to name in `ludwig.modules.optimization_modules.optimizer_registry`
|
|
(default: 'adadelta')"""
|
|
|
|
# Defaults taken from https://pytorch.org/docs/stable/generated/torch.optim.Adadelta.html#torch.optim.Adadelta :
|
|
rho: float = schema_utils.FloatRange(
|
|
default=0.9,
|
|
min=0,
|
|
max=1,
|
|
description="Coefficient used for computing a running average of squared gradients.",
|
|
parameter_metadata=OPTIMIZER_METADATA["rho"],
|
|
)
|
|
|
|
eps: float = schema_utils.NonNegativeFloat(
|
|
default=1e-06,
|
|
description="Term added to the denominator to improve numerical stability.",
|
|
parameter_metadata=OPTIMIZER_METADATA["eps"],
|
|
)
|
|
|
|
weight_decay: float = schema_utils.NonNegativeFloat(
|
|
default=0.0, description="Weight decay ($L2$ penalty).", parameter_metadata=OPTIMIZER_METADATA["weight_decay"]
|
|
)
|
|
|
|
|
|
@DeveloperAPI
|
|
@register_optimizer(name="adagrad")
|
|
class AdagradOptimizerConfig(BaseOptimizerConfig):
|
|
"""Parameters for adagrad optimization."""
|
|
|
|
# Example docstring
|
|
optimizer_class: ClassVar[torch.optim.Optimizer] = torch.optim.Adagrad
|
|
"""Points to `torch.optim.Adagrad`."""
|
|
|
|
type: str = schema_utils.ProtectedString("adagrad")
|
|
"""Must be 'adagrad' - corresponds to name in `ludwig.modules.optimization_modules.optimizer_registry`
|
|
(default: 'adagrad')"""
|
|
|
|
# Defaults taken from https://pytorch.org/docs/stable/generated/torch.optim.Adagrad.html#torch.optim.Adagrad :
|
|
initial_accumulator_value: float = schema_utils.NonNegativeFloat(
|
|
default=0, description="", parameter_metadata=OPTIMIZER_METADATA["initial_accumulator_value"]
|
|
)
|
|
|
|
lr_decay: float = schema_utils.FloatRange(
|
|
default=0, description="Learning rate decay.", parameter_metadata=OPTIMIZER_METADATA["lr_decay"]
|
|
)
|
|
|
|
weight_decay: float = schema_utils.FloatRange(
|
|
default=0, description="Weight decay ($L2$ penalty).", parameter_metadata=OPTIMIZER_METADATA["weight_decay"]
|
|
)
|
|
|
|
eps: float = schema_utils.FloatRange(
|
|
default=1e-10,
|
|
description="Term added to the denominator to improve numerical stability.",
|
|
parameter_metadata=OPTIMIZER_METADATA["eps"],
|
|
)
|
|
|
|
|
|
if bnb is not None:
|
|
|
|
@DeveloperAPI
|
|
@register_optimizer(name="adagrad_8bit")
|
|
class Adagrad8BitOptimizerConfig(AdagradOptimizerConfig):
|
|
optimizer_class: ClassVar[torch.optim.Optimizer] = bnb.optim.Adagrad8bit
|
|
|
|
type: str = schema_utils.ProtectedString("adagrad_8bit")
|
|
|
|
block_wise: bool = schema_utils.Boolean(
|
|
default=True,
|
|
description="Whether to use block wise update.",
|
|
)
|
|
|
|
percentile_clipping: int = schema_utils.IntegerRange(
|
|
default=100,
|
|
min=0,
|
|
max=100,
|
|
description="Percentile clipping.",
|
|
)
|
|
|
|
@property
|
|
def is_8bit(self) -> bool:
|
|
return True
|
|
|
|
|
|
@DeveloperAPI
|
|
@register_optimizer(name="adamax")
|
|
class AdamaxOptimizerConfig(BaseOptimizerConfig):
|
|
"""Parameters for adamax optimization."""
|
|
|
|
optimizer_class: ClassVar[torch.optim.Optimizer] = torch.optim.Adamax
|
|
"""Points to `torch.optim.Adamax`."""
|
|
|
|
type: str = schema_utils.ProtectedString("adamax")
|
|
"""Must be 'adamax' - corresponds to name in `ludwig.modules.optimization_modules.optimizer_registry`
|
|
(default: 'adamax')"""
|
|
|
|
# Defaults taken from https://pytorch.org/docs/stable/generated/torch.optim.Adamax.html#torch.optim.Adamax :
|
|
betas: tuple[float, float] = schema_utils.FloatRangeTupleDataclassField(
|
|
default=(0.9, 0.999),
|
|
description="Coefficients used for computing running averages of gradient and its square.",
|
|
parameter_metadata=OPTIMIZER_METADATA["betas"],
|
|
)
|
|
|
|
eps: float = schema_utils.NonNegativeFloat(
|
|
default=1e-08,
|
|
description="Term added to the denominator to improve numerical stability.",
|
|
parameter_metadata=OPTIMIZER_METADATA["eps"],
|
|
)
|
|
|
|
weight_decay: float = schema_utils.NonNegativeFloat(
|
|
default=0.0, description="Weight decay ($L2$ penalty).", parameter_metadata=OPTIMIZER_METADATA["weight_decay"]
|
|
)
|
|
|
|
|
|
@DeveloperAPI
|
|
@register_optimizer(name="nadam")
|
|
class NadamOptimizerConfig(BaseOptimizerConfig):
|
|
optimizer_class: ClassVar[torch.optim.Optimizer] = torch.optim.NAdam
|
|
"""Points to `torch.optim.NAdam`."""
|
|
|
|
type: str = schema_utils.ProtectedString("nadam")
|
|
|
|
# Defaults taken from https://pytorch.org/docs/stable/generated/torch.optim.NAdam.html#torch.optim.NAdam :
|
|
|
|
betas: tuple[float, float] = schema_utils.FloatRangeTupleDataclassField(
|
|
default=(0.9, 0.999),
|
|
description="Coefficients used for computing running averages of gradient and its square.",
|
|
parameter_metadata=OPTIMIZER_METADATA["betas"],
|
|
)
|
|
|
|
eps: float = schema_utils.NonNegativeFloat(
|
|
default=1e-08,
|
|
description="Term added to the denominator to improve numerical stability.",
|
|
parameter_metadata=OPTIMIZER_METADATA["eps"],
|
|
)
|
|
|
|
weight_decay: float = schema_utils.NonNegativeFloat(
|
|
default=0.0, description="Weight decay ($L2$ penalty).", parameter_metadata=OPTIMIZER_METADATA["weight_decay"]
|
|
)
|
|
|
|
momentum_decay: float = schema_utils.NonNegativeFloat(
|
|
default=4e-3, description="Momentum decay.", parameter_metadata=OPTIMIZER_METADATA["momentum_decay"]
|
|
)
|
|
|
|
|
|
@DeveloperAPI
|
|
@register_optimizer(name="rmsprop")
|
|
class RMSPropOptimizerConfig(BaseOptimizerConfig):
|
|
"""Parameters for rmsprop optimization."""
|
|
|
|
optimizer_class: ClassVar[torch.optim.Optimizer] = torch.optim.RMSprop
|
|
"""Points to `torch.optim.RMSprop`."""
|
|
|
|
type: str = schema_utils.ProtectedString("rmsprop")
|
|
"""Must be 'rmsprop' - corresponds to name in `ludwig.modules.optimization_modules.optimizer_registry`
|
|
(default: 'rmsprop')"""
|
|
|
|
# Defaults taken from https://pytorch.org/docs/stable/generated/torch.optim.RMSprop.html#torch.optim.RMSprop:
|
|
momentum: float = schema_utils.NonNegativeFloat(
|
|
default=0.0,
|
|
description="Momentum factor.",
|
|
parameter_metadata=OPTIMIZER_METADATA["momentum"],
|
|
)
|
|
|
|
alpha: float = schema_utils.NonNegativeFloat(
|
|
default=0.99,
|
|
description="Smoothing constant.",
|
|
parameter_metadata=OPTIMIZER_METADATA["alpha"],
|
|
)
|
|
|
|
eps: float = schema_utils.NonNegativeFloat(
|
|
default=1e-08,
|
|
description="Term added to the denominator to improve numerical stability.",
|
|
parameter_metadata=OPTIMIZER_METADATA["eps"],
|
|
)
|
|
|
|
centered: bool = schema_utils.Boolean(
|
|
default=False,
|
|
description="If True, computes the centered RMSProp, and the gradient is normalized by an estimation of its "
|
|
"variance.",
|
|
parameter_metadata=OPTIMIZER_METADATA["centered"],
|
|
)
|
|
|
|
weight_decay: float = schema_utils.NonNegativeFloat(default=0.0, description="Weight decay ($L2$ penalty).")
|
|
|
|
|
|
if bnb is not None:
|
|
|
|
@DeveloperAPI
|
|
@register_optimizer(name="rmsprop_8bit")
|
|
class RMSProp8BitOptimizerConfig(RMSPropOptimizerConfig):
|
|
optimizer_class: ClassVar[torch.optim.Optimizer] = bnb.optim.RMSprop8bit
|
|
|
|
type: str = schema_utils.ProtectedString("rmsprop_8bit")
|
|
|
|
block_wise: bool = schema_utils.Boolean(
|
|
default=True,
|
|
description="Whether to use block wise update.",
|
|
)
|
|
|
|
percentile_clipping: int = schema_utils.IntegerRange(
|
|
default=100,
|
|
min=0,
|
|
max=100,
|
|
description="Percentile clipping.",
|
|
)
|
|
|
|
@property
|
|
def is_8bit(self) -> bool:
|
|
return True
|
|
|
|
|
|
if bnb is not None:
|
|
|
|
@DeveloperAPI
|
|
@register_optimizer(name="lamb")
|
|
class LAMBOptimizerConfig(BaseOptimizerConfig):
|
|
"""Layer-wise Adaptive Moments optimizer for Batch training.
|
|
|
|
Paper: https://arxiv.org/pdf/1904.00962.pdf
|
|
"""
|
|
|
|
optimizer_class: ClassVar[torch.optim.Optimizer] = bnb.optim.LAMB
|
|
|
|
type: str = schema_utils.ProtectedString("lamb")
|
|
|
|
bias_correction: bool = schema_utils.Boolean(
|
|
default=True,
|
|
)
|
|
|
|
betas: tuple[float, float] = schema_utils.FloatRangeTupleDataclassField(
|
|
default=(0.9, 0.999),
|
|
description="Coefficients used for computing running averages of gradient and its square.",
|
|
parameter_metadata=OPTIMIZER_METADATA["betas"],
|
|
)
|
|
|
|
eps: float = schema_utils.NonNegativeFloat(
|
|
default=1e-08,
|
|
description="Term added to the denominator to improve numerical stability.",
|
|
parameter_metadata=OPTIMIZER_METADATA["eps"],
|
|
)
|
|
|
|
weight_decay: float = schema_utils.NonNegativeFloat(
|
|
default=0.0,
|
|
description="Weight decay (L2 penalty).",
|
|
parameter_metadata=OPTIMIZER_METADATA["weight_decay"],
|
|
)
|
|
|
|
amsgrad: bool = schema_utils.Boolean(
|
|
default=False,
|
|
description=(
|
|
"Whether to use the AMSGrad variant of this algorithm from the paper "
|
|
"'On the Convergence of Adam and Beyond'."
|
|
),
|
|
parameter_metadata=OPTIMIZER_METADATA["amsgrad"],
|
|
)
|
|
|
|
adam_w_mode: bool = schema_utils.Boolean(
|
|
default=True,
|
|
description="Whether to use the AdamW mode of this algorithm from the paper "
|
|
"'Decoupled Weight Decay Regularization'.",
|
|
)
|
|
|
|
percentile_clipping: int = schema_utils.IntegerRange(
|
|
default=100,
|
|
min=0,
|
|
max=100,
|
|
description="Percentile clipping.",
|
|
)
|
|
|
|
block_wise: bool = schema_utils.Boolean(
|
|
default=False,
|
|
description="Whether to use block wise update.",
|
|
)
|
|
|
|
max_unorm: float = schema_utils.FloatRange(
|
|
default=1.0,
|
|
min=0.0,
|
|
max=1.0,
|
|
)
|
|
|
|
@DeveloperAPI
|
|
@register_optimizer(name="lamb_8bit")
|
|
class LAMB8BitOptimizerConfig(LAMBOptimizerConfig):
|
|
optimizer_class: ClassVar[torch.optim.Optimizer] = bnb.optim.LAMB8bit
|
|
|
|
type: str = schema_utils.ProtectedString("lamb_8bit")
|
|
|
|
@property
|
|
def is_8bit(self) -> bool:
|
|
return True
|
|
|
|
|
|
if bnb is not None:
|
|
|
|
@DeveloperAPI
|
|
@register_optimizer(name="lars")
|
|
class LARSOptimizerConfig(BaseOptimizerConfig):
|
|
"""Layerwise Adaptive Rate Scaling.
|
|
|
|
Paper: https://arxiv.org/pdf/1708.03888.pdf
|
|
"""
|
|
|
|
optimizer_class: ClassVar[torch.optim.Optimizer] = bnb.optim.LARS
|
|
|
|
type: str = schema_utils.ProtectedString("lars")
|
|
|
|
# 0.9 taken from the original paper - momentum requires a non zero value
|
|
# https://arxiv.org/pdf/1708.03888v3.pdf
|
|
momentum: float = schema_utils.FloatRange(
|
|
default=0.9,
|
|
min=0.0,
|
|
max=1.0,
|
|
min_inclusive=False,
|
|
description="Momentum factor.",
|
|
parameter_metadata=OPTIMIZER_METADATA["momentum"],
|
|
)
|
|
|
|
dampening: float = schema_utils.FloatRange(
|
|
default=0.0,
|
|
min=0.0,
|
|
max=1.0,
|
|
description="Dampening for momentum.",
|
|
parameter_metadata=OPTIMIZER_METADATA["dampening"],
|
|
)
|
|
|
|
weight_decay: float = schema_utils.NonNegativeFloat(
|
|
default=0.0,
|
|
description="Weight decay (L2 penalty).",
|
|
parameter_metadata=OPTIMIZER_METADATA["weight_decay"],
|
|
)
|
|
|
|
nesterov: bool = schema_utils.Boolean(
|
|
default=False,
|
|
description="Enables Nesterov momentum.",
|
|
parameter_metadata=OPTIMIZER_METADATA["nesterov"],
|
|
)
|
|
|
|
percentile_clipping: int = schema_utils.IntegerRange(
|
|
default=100,
|
|
min=0,
|
|
max=100,
|
|
description="Percentile clipping.",
|
|
)
|
|
|
|
max_unorm: float = schema_utils.FloatRange(
|
|
default=1.0,
|
|
min=0.0,
|
|
max=1.0,
|
|
)
|
|
|
|
@DeveloperAPI
|
|
@register_optimizer(name="lars_8bit")
|
|
class LARS8BitOptimizerConfig(LARSOptimizerConfig):
|
|
optimizer_class: ClassVar[torch.optim.Optimizer] = bnb.optim.LARS8bit
|
|
|
|
type: str = schema_utils.ProtectedString("lars_8bit")
|
|
|
|
@property
|
|
def is_8bit(self) -> bool:
|
|
return True
|
|
|
|
|
|
if bnb is not None:
|
|
|
|
@DeveloperAPI
|
|
@register_optimizer(name="lion")
|
|
class LIONOptimizerConfig(BaseOptimizerConfig):
|
|
"""Evolved Sign Momentum.
|
|
|
|
Paper: https://arxiv.org/pdf/2302.06675.pdf
|
|
"""
|
|
|
|
optimizer_class: ClassVar[torch.optim.Optimizer] = bnb.optim.Lion
|
|
|
|
type: str = schema_utils.ProtectedString("lion")
|
|
|
|
betas: tuple[float, float] = schema_utils.FloatRangeTupleDataclassField(
|
|
default=(0.9, 0.999),
|
|
description="Coefficients used for computing running averages of gradient and its square.",
|
|
parameter_metadata=OPTIMIZER_METADATA["betas"],
|
|
)
|
|
|
|
weight_decay: float = schema_utils.NonNegativeFloat(
|
|
default=0.0,
|
|
description="Weight decay (L2 penalty).",
|
|
parameter_metadata=OPTIMIZER_METADATA["weight_decay"],
|
|
)
|
|
|
|
percentile_clipping: int = schema_utils.IntegerRange(
|
|
default=100,
|
|
min=0,
|
|
max=100,
|
|
description="Percentile clipping.",
|
|
)
|
|
|
|
block_wise: bool = schema_utils.Boolean(
|
|
default=True,
|
|
description="Whether to use block wise update.",
|
|
)
|
|
|
|
@DeveloperAPI
|
|
@register_optimizer(name="lion_8bit")
|
|
class LION8BitOptimizerConfig(LIONOptimizerConfig):
|
|
optimizer_class: ClassVar[torch.optim.Optimizer] = bnb.optim.Lion8bit
|
|
|
|
type: str = schema_utils.ProtectedString("lion_8bit")
|
|
|
|
@property
|
|
def is_8bit(self) -> bool:
|
|
return True
|
|
|
|
@DeveloperAPI
|
|
@register_optimizer(name="paged_lion")
|
|
class PagedLionOptimizerConfig(LIONOptimizerConfig):
|
|
optimizer_class: ClassVar[torch.optim.Optimizer] = bnb.optim.PagedLion
|
|
|
|
type: str = schema_utils.ProtectedString("paged_lion")
|
|
|
|
@property
|
|
def is_paged(self) -> bool:
|
|
return True
|
|
|
|
@DeveloperAPI
|
|
@register_optimizer(name="paged_lion_8bit")
|
|
class PagedLion8BitOptimizerConfig(PagedLionOptimizerConfig):
|
|
optimizer_class: ClassVar[torch.optim.Optimizer] = bnb.optim.PagedLion8bit
|
|
|
|
type: str = schema_utils.ProtectedString("paged_lion_8bit")
|
|
|
|
@property
|
|
def is_8bit(self) -> bool:
|
|
return True
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Modern optimizers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@DeveloperAPI
|
|
@register_optimizer(name="radam")
|
|
class RAdamOptimizerConfig(BaseOptimizerConfig):
|
|
"""Rectified Adam (RAdam) optimizer config (Liu et al., 2020).
|
|
|
|
Paper: https://arxiv.org/abs/1908.03265
|
|
|
|
Convergence: Warms up the adaptive learning rate by computing an analytical
|
|
approximation to the variance of the second moment estimate. This eliminates the
|
|
need for an explicit warmup schedule: training is stable from step 1 even with a
|
|
large learning rate. Converges to the same quality as Adam but with a wider range
|
|
of valid learning rates.
|
|
|
|
Memory footprint: Same as Adam -- two moment buffers per parameter (~2x model size
|
|
in optimizer state).
|
|
|
|
When to use: Any setting where Adam is unstable early in training without warmup.
|
|
Especially useful for experimentation where you do not want to tune the warmup
|
|
duration. Drop-in replacement for Adam/AdamW with no warmup scheduler needed.
|
|
|
|
Common pitfalls: RAdam provides no benefit over well-warmed-up Adam in late training.
|
|
The rectification term switches off after the variance stabilises (around step ~5-6
|
|
at default beta2=0.999), so expect identical behaviour to Adam from that point on.
|
|
"""
|
|
|
|
optimizer_class: ClassVar[torch.optim.Optimizer] = torch.optim.RAdam
|
|
"""Points to `torch.optim.RAdam`."""
|
|
|
|
type: str = schema_utils.ProtectedString("radam")
|
|
"""Must be 'radam' - corresponds to name in `ludwig.modules.optimization_modules.optimizer_registry`."""
|
|
|
|
# Defaults from https://pytorch.org/docs/stable/generated/torch.optim.RAdam.html
|
|
betas: tuple[float, float] = schema_utils.FloatRangeTupleDataclassField(
|
|
default=(0.9, 0.999),
|
|
description="Coefficients used for computing running averages of gradient and its square.",
|
|
parameter_metadata=OPTIMIZER_METADATA["betas"],
|
|
)
|
|
|
|
eps: float = schema_utils.NonNegativeFloat(
|
|
default=1e-08,
|
|
description="Term added to the denominator to improve numerical stability.",
|
|
parameter_metadata=OPTIMIZER_METADATA["eps"],
|
|
)
|
|
|
|
weight_decay: float = schema_utils.NonNegativeFloat(
|
|
default=0.0,
|
|
description="Weight decay (L2 penalty).",
|
|
parameter_metadata=OPTIMIZER_METADATA["weight_decay"],
|
|
)
|
|
|
|
|
|
@DeveloperAPI
|
|
@register_optimizer(name="adafactor")
|
|
class AdafactorOptimizerConfig(BaseOptimizerConfig):
|
|
"""Adafactor optimizer config (Shazeer & Stern, 2018).
|
|
|
|
Paper: https://arxiv.org/abs/1805.09843
|
|
Implementation: `transformers.optimization.Adafactor`
|
|
|
|
Convergence: Factorizes the second moment matrix into row and column factors instead
|
|
of storing a full per-parameter tensor, dramatically reducing memory. Convergence is
|
|
comparable to Adam on large Transformer models but can be slightly slower to converge
|
|
on smaller tasks.
|
|
|
|
Memory footprint: Very low -- O(n+m) per parameter matrix (row + column factors)
|
|
instead of O(n*m). For a 1B parameter model this can save ~8 GB of optimizer state
|
|
versus Adam, making it the go-to optimizer when GPU memory is the bottleneck.
|
|
|
|
When to use: Training or fine-tuning very large language models (T5, LLaMA, GPT)
|
|
where Adam's two-moment buffers exceed available GPU memory. Enabled by default in
|
|
many Hugging Face T5 training recipes.
|
|
|
|
Common pitfalls: When `relative_step=True` (default) Adafactor computes its own
|
|
learning rate schedule -- do NOT combine with an external LR scheduler (set
|
|
`lr=None`). When `relative_step=False` you must pass an explicit `lr`. Setting
|
|
`scale_parameter=False` and `relative_step=False` with a manual `lr` is the
|
|
standard recipe for fine-tuning.
|
|
"""
|
|
|
|
optimizer_class: ClassVar[torch.optim.Optimizer | None] = _TransformersAdafactor
|
|
"""Points to `transformers.optimization.Adafactor` (None if transformers not installed)."""
|
|
|
|
type: str = schema_utils.ProtectedString("adafactor")
|
|
"""Must be 'adafactor' - corresponds to name in `ludwig.modules.optimization_modules.optimizer_registry`."""
|
|
|
|
# Adafactor manages its own LR schedule when relative_step=True, so lr defaults to None.
|
|
lr: float | None = schema_utils.FloatRange(
|
|
default=None,
|
|
allow_none=True,
|
|
min=0.0,
|
|
description=(
|
|
"Learning rate. Set to None (default) when `relative_step=True` so that Adafactor manages "
|
|
"its own schedule. Must be provided when `relative_step=False`."
|
|
),
|
|
)
|
|
|
|
scale_parameter: bool = schema_utils.Boolean(
|
|
default=True,
|
|
description=(
|
|
"If True, the learning rate is scaled by the root mean square of the parameters. "
|
|
"Should be True when `relative_step=True`."
|
|
),
|
|
)
|
|
|
|
relative_step: bool = schema_utils.Boolean(
|
|
default=True,
|
|
description=(
|
|
"If True, a time-dependent learning rate is computed instead of using the external `lr`. "
|
|
"Do not combine with an external LR scheduler."
|
|
),
|
|
)
|
|
|
|
warmup_init: bool = schema_utils.Boolean(
|
|
default=False,
|
|
description=(
|
|
"If True, the time-dependent learning rate is linearly increased at initialization. "
|
|
"Only effective when `relative_step=True`."
|
|
),
|
|
)
|
|
|
|
def __post_init__(self):
|
|
if self.optimizer_class is None:
|
|
raise ImportError(
|
|
"The 'adafactor' optimizer requires the `transformers` package. "
|
|
"Install it with: pip install transformers"
|
|
)
|
|
|
|
|
|
@DeveloperAPI
|
|
@register_optimizer(name="schedule_free_adamw")
|
|
class ScheduleFreeAdamWOptimizerConfig(BaseOptimizerConfig):
|
|
"""Schedule-Free AdamW optimizer config (Defazio & Mishchenko, 2024).
|
|
|
|
Paper: https://arxiv.org/abs/2405.15682
|
|
Package: `schedulefree` (install with: pip install schedulefree)
|
|
|
|
Convergence: Eliminates the need for a learning rate scheduler by maintaining a
|
|
Polyak-Ruppert averaged iterate in addition to the standard momentum buffer. The
|
|
averaged iterate is used for evaluation while the momentum buffer drives the
|
|
optimization. Achieves performance comparable to or better than well-tuned
|
|
cosine/linear decay schedules on a wide range of tasks.
|
|
|
|
Memory footprint: Slightly higher than AdamW -- stores an extra averaged parameter
|
|
buffer (z), so ~3x model size in optimizer state (vs 2x for AdamW).
|
|
|
|
When to use: When you want to skip learning rate scheduler tuning entirely: no
|
|
cosine decay, no linear warmup schedule (beyond the built-in `warmup_steps`). Ideal
|
|
for rapid prototyping and hyperparameter sweeps where schedule tuning is expensive.
|
|
Also useful for online/continual learning without a fixed horizon.
|
|
|
|
Common pitfalls: Must call `optimizer.train()` before the training loop and
|
|
`optimizer.eval()` before evaluation/inference -- the model is in a different state
|
|
depending on which iterate (momentum vs averaged) is active. Forgetting these calls
|
|
leads to degraded evaluation metrics. The `warmup_steps` parameter is built into
|
|
the optimizer and replaces the external warmup scheduler.
|
|
"""
|
|
|
|
optimizer_class: ClassVar[torch.optim.Optimizer | None] = _AdamWScheduleFree
|
|
"""Points to `schedulefree.AdamWScheduleFree` (None if schedulefree not installed)."""
|
|
|
|
type: str = schema_utils.ProtectedString("schedule_free_adamw")
|
|
"""Must be 'schedule_free_adamw' - corresponds to name in optimizer_registry."""
|
|
|
|
betas: tuple[float, float] = schema_utils.FloatRangeTupleDataclassField(
|
|
default=(0.9, 0.999),
|
|
description="Coefficients used for computing running averages of gradient and its square.",
|
|
parameter_metadata=OPTIMIZER_METADATA["betas"],
|
|
)
|
|
|
|
weight_decay: float = schema_utils.NonNegativeFloat(
|
|
default=0.0,
|
|
description="Weight decay (decoupled L2 penalty).",
|
|
parameter_metadata=OPTIMIZER_METADATA["weight_decay"],
|
|
)
|
|
|
|
warmup_steps: int = schema_utils.Integer(
|
|
default=0,
|
|
description=(
|
|
"Number of linear warmup steps built into the optimizer. "
|
|
"Replaces an external warmup scheduler -- do not combine with one."
|
|
),
|
|
)
|
|
|
|
def __post_init__(self):
|
|
if self.optimizer_class is None:
|
|
raise ImportError(
|
|
"The 'schedule_free_adamw' optimizer requires the `schedulefree` package. "
|
|
"Install it with: pip install schedulefree"
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Muon: pure-Python Newton-Schulz implementation so no extra package required
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class _MuonOptimizer(torch.optim.Optimizer):
|
|
"""Muon -- Momentum + Orthogonalization via Newton-Schulz (Jordan et al., 2024).
|
|
|
|
Paper: https://arxiv.org/abs/2409.20325
|
|
"""
|
|
|
|
_NS_COEFFS = (3.4445, -4.7750, 2.0315)
|
|
|
|
def __init__(self, params, lr=0.02, momentum=0.95, nesterov=True):
|
|
defaults = {"lr": lr, "momentum": momentum, "nesterov": nesterov}
|
|
super().__init__(params, defaults)
|
|
|
|
@torch.no_grad()
|
|
def _zeropower_via_newtonschulz5(self, G: torch.Tensor, steps: int = 5) -> torch.Tensor:
|
|
"""Newton-Schulz iteration to approximate the orthogonal factor of G."""
|
|
if G.ndim < 2:
|
|
raise ValueError(f"_zeropower_via_newtonschulz5 requires a matrix (ndim >= 2), got ndim={G.ndim}.")
|
|
a, b, c = self._NS_COEFFS
|
|
X = G.bfloat16() if G.dtype not in (torch.float16, torch.bfloat16) else G
|
|
X = X / (X.norm() + 1e-7)
|
|
transposed = X.shape[-2] < X.shape[-1]
|
|
if transposed:
|
|
X = X.mT
|
|
for _ in range(steps):
|
|
A = X @ X.mT
|
|
X = a * X + b * (A @ X) + c * (A @ A @ X)
|
|
if transposed:
|
|
X = X.mT
|
|
return X.to(G.dtype)
|
|
|
|
@torch.no_grad()
|
|
def step(self, closure=None):
|
|
loss = None
|
|
if closure is not None:
|
|
with torch.enable_grad():
|
|
loss = closure()
|
|
|
|
for group in self.param_groups:
|
|
lr = group["lr"]
|
|
momentum = group["momentum"]
|
|
nesterov = group["nesterov"]
|
|
|
|
for p in group["params"]:
|
|
if p.grad is None:
|
|
continue
|
|
g = p.grad
|
|
state = self.state[p]
|
|
if len(state) == 0:
|
|
state["momentum_buffer"] = torch.zeros_like(g)
|
|
|
|
buf = state["momentum_buffer"]
|
|
buf.mul_(momentum).add_(g)
|
|
|
|
if nesterov:
|
|
update = g + momentum * buf
|
|
else:
|
|
update = buf
|
|
|
|
if update.ndim >= 2:
|
|
orig_shape = update.shape
|
|
mat = update.view(orig_shape[0], -1)
|
|
mat = self._zeropower_via_newtonschulz5(mat)
|
|
scale = math.sqrt(max(mat.shape[-2], mat.shape[-1]))
|
|
update = mat.view(orig_shape) * scale
|
|
|
|
p.add_(update, alpha=-lr)
|
|
|
|
return loss
|
|
|
|
|
|
@DeveloperAPI
|
|
@register_optimizer(name="muon")
|
|
class MuonOptimizerConfig(BaseOptimizerConfig):
|
|
"""Muon optimizer config -- Momentum + Orthogonalization via Newton-Schulz (Jordan et al., 2024).
|
|
|
|
Paper: https://arxiv.org/abs/2409.20325
|
|
|
|
Convergence: Applies Nesterov SGD momentum then orthogonalizes each parameter update
|
|
matrix via a fast Newton-Schulz iteration (5 steps). Keeps updates approximately
|
|
orthogonal for more isotropic parameter changes. Empirically outperforms AdamW on
|
|
large language model pre-training at equivalent compute.
|
|
|
|
Memory footprint: Low -- stores only one momentum buffer per parameter (~1x model
|
|
size), same as SGD with momentum. Significantly cheaper than Adam's two buffers.
|
|
|
|
When to use: Pre-training large Transformer language models where you want faster
|
|
convergence than AdamW at the same memory cost as SGD. Implemented in pure PyTorch
|
|
(no extra package required).
|
|
|
|
Common pitfalls: The NS iteration operates in bfloat16 by default for speed. The
|
|
default lr (0.02) is much higher than typical Adam lr (1e-3); always re-tune lr
|
|
when switching from Adam.
|
|
"""
|
|
|
|
optimizer_class: ClassVar[torch.optim.Optimizer] = _MuonOptimizer
|
|
"""Points to the built-in `_MuonOptimizer` implementation."""
|
|
|
|
type: str = schema_utils.ProtectedString("muon")
|
|
"""Must be 'muon' - corresponds to name in `ludwig.modules.optimization_modules.optimizer_registry`."""
|
|
|
|
momentum: float = schema_utils.FloatRange(
|
|
default=0.95,
|
|
min=0.0,
|
|
max=1.0,
|
|
description="Momentum factor for Nesterov SGD applied before orthogonalization.",
|
|
parameter_metadata=OPTIMIZER_METADATA["momentum"],
|
|
)
|
|
|
|
nesterov: bool = schema_utils.Boolean(
|
|
default=True,
|
|
description=(
|
|
"If True, use Nesterov momentum (look-ahead gradient) before orthogonalization. "
|
|
"The original Muon paper uses Nesterov."
|
|
),
|
|
parameter_metadata=OPTIMIZER_METADATA["nesterov"],
|
|
)
|
|
|
|
|
|
if _SOAPOptimizer is not None:
|
|
|
|
@DeveloperAPI
|
|
@register_optimizer(name="soap")
|
|
class SOAPOptimizerConfig(BaseOptimizerConfig):
|
|
"""SOAP optimizer config -- Shampoo as Adam's Preconditioner (Vyas et al., 2024).
|
|
|
|
Paper: https://arxiv.org/abs/2409.11321
|
|
Package: `soap-pytorch` (install with: pip install soap-pytorch)
|
|
|
|
Convergence: Maintains a Kronecker-factored (Shampoo-style) preconditioner for
|
|
each weight matrix and runs Adam in its eigenbasis. Converges faster than AdamW
|
|
in terms of iterations/tokens on large Transformer pre-training.
|
|
|
|
Memory footprint: High -- stores Kronecker factors (m x m) and (n x n) per weight
|
|
matrix (m, n) in addition to Adam's two moment buffers. 2-3x Adam's memory for
|
|
typical Transformer shapes.
|
|
|
|
When to use: Large-scale pre-training where compute is plentiful but wall-clock
|
|
time is at a premium.
|
|
|
|
Common pitfalls: The preconditioner update frequency trades off overhead vs
|
|
freshness; a frequency of 10-100 steps is typical. Not recommended for small
|
|
models where preconditioner overhead outweighs convergence gain.
|
|
"""
|
|
|
|
optimizer_class: ClassVar[torch.optim.Optimizer] = _SOAPOptimizer
|
|
"""Points to `soap.SOAP` from the `soap-pytorch` package."""
|
|
|
|
type: str = schema_utils.ProtectedString("soap")
|
|
"""Must be 'soap' - corresponds to name in `ludwig.modules.optimization_modules.optimizer_registry`."""
|
|
|
|
betas: tuple[float, float] = schema_utils.FloatRangeTupleDataclassField(
|
|
default=(0.95, 0.95),
|
|
description=(
|
|
"Coefficients for the first and second Adam moment estimates run in the "
|
|
"Shampoo eigenbasis. Note: SOAP typically uses higher beta1 (0.95) than standard Adam."
|
|
),
|
|
parameter_metadata=OPTIMIZER_METADATA["betas"],
|
|
)
|
|
|
|
weight_decay: float = schema_utils.NonNegativeFloat(
|
|
default=0.01,
|
|
description="Weight decay (decoupled L2 penalty, as in AdamW).",
|
|
parameter_metadata=OPTIMIZER_METADATA["weight_decay"],
|
|
)
|
|
|
|
|
|
@DeveloperAPI
|
|
def get_optimizer_conds():
|
|
"""Returns a JSON schema of conditionals to validate against optimizer types defined in
|
|
`ludwig.modules.optimization_modules.optimizer_registry`.
|
|
|
|
Returns:
|
|
List of JSON schema conditionals for all registered optimizer types.
|
|
"""
|
|
conds = []
|
|
for optimizer in optimizer_registry:
|
|
optimizer_cls = optimizer_registry[optimizer][1]
|
|
other_props = schema_utils.unload_jsonschema_from_config_class(optimizer_cls)["properties"]
|
|
schema_utils.remove_duplicate_fields(other_props)
|
|
preproc_cond = schema_utils.create_cond(
|
|
{"type": optimizer},
|
|
other_props,
|
|
)
|
|
conds.append(preproc_cond)
|
|
return conds
|
|
|
|
|
|
@DeveloperAPI
|
|
def OptimizerDataclassField(default="adam", description="", parameter_metadata: ParameterMetadata = None):
|
|
"""Custom dataclass field that when used inside of a dataclass will allow any optimizer in
|
|
`ludwig.modules.optimization_modules.optimizer_registry`.
|
|
|
|
Sets default optimizer to 'adam'.
|
|
|
|
Args:
|
|
default: Dict specifying an optimizer with a `type` field and its associated parameters. Will attempt
|
|
to use `type` to load optimizer from registry with given params. (default: {"type": "adam"}).
|
|
|
|
Returns:
|
|
Initialized dataclass field that converts untyped dicts with params to optimizer dataclass instances.
|
|
"""
|
|
|
|
class OptimizerSelection(schema_utils.TypeSelection):
|
|
"""Custom marshmallow field that deserializes a dict to a valid optimizer from
|
|
`ludwig.modules.optimization_modules.optimizer_registry` and creates a corresponding `oneOf` JSON schema
|
|
for external usage."""
|
|
|
|
def __init__(self):
|
|
super().__init__(
|
|
registry=optimizer_registry,
|
|
default_value=default,
|
|
description=description,
|
|
parameter_metadata=parameter_metadata,
|
|
)
|
|
|
|
def get_schema_from_registry(self, key: str) -> type[schema_utils.LudwigBaseConfig]:
|
|
return get_optimizer_cls(key)
|
|
|
|
def _jsonschema_type_mapping(self):
|
|
# Note that this uses the same conditional pattern as combiners:
|
|
return {
|
|
"type": "object",
|
|
"properties": {
|
|
"type": {
|
|
"type": "string",
|
|
"enum": list(optimizer_registry.keys()),
|
|
"default": default,
|
|
"description": "The type of optimizer to use during the learning process",
|
|
},
|
|
},
|
|
"title": "optimizer_options",
|
|
"allOf": get_optimizer_conds(),
|
|
"required": ["type"],
|
|
"description": description,
|
|
}
|
|
|
|
return OptimizerSelection().get_default_field()
|
|
|
|
|
|
@DeveloperAPI
|
|
class GradientClippingConfig(schema_utils.LudwigBaseConfig):
|
|
"""Dataclass that holds gradient clipping parameters."""
|
|
|
|
clipglobalnorm: float | None = schema_utils.FloatRange(
|
|
default=0.5,
|
|
allow_none=True,
|
|
description="Maximum allowed norm of the gradients",
|
|
parameter_metadata=OPTIMIZER_METADATA["gradient_clipping"],
|
|
)
|
|
|
|
# TODO(travis): is this redundant with `clipglobalnorm`?
|
|
clipnorm: float | None = schema_utils.FloatRange(
|
|
default=None,
|
|
allow_none=True,
|
|
description="Maximum allowed norm of the gradients",
|
|
parameter_metadata=OPTIMIZER_METADATA["gradient_clipping"],
|
|
)
|
|
|
|
clipvalue: float | None = schema_utils.FloatRange(
|
|
default=None,
|
|
allow_none=True,
|
|
description="Maximum allowed value of the gradients",
|
|
parameter_metadata=OPTIMIZER_METADATA["gradient_clipping"],
|
|
)
|
|
|
|
|
|
@DeveloperAPI
|
|
def GradientClippingDataclassField(description: str, default: dict = {}):
|
|
"""Returns custom dataclass field for `ludwig.modules.optimization_modules.GradientClippingConfig`. Allows
|
|
`None` by default.
|
|
|
|
Args:
|
|
description: Description of the gradient dataclass field.
|
|
default: Dict that specifies clipping param values that will be loaded by its schema class (default: {}).
|
|
"""
|
|
allow_none = True
|
|
|
|
class GradientClippingConfigField(schema_utils.SchemaField):
|
|
"""Custom field class for gradient clipping.
|
|
|
|
Deserializes a dict to a valid instance of `ludwig.modules.optimization_modules.GradientClippingConfig` and
|
|
creates a corresponding JSON schema for external usage.
|
|
"""
|
|
|
|
def _deserialize(self, value, attr, data, **kwargs):
|
|
if value is None:
|
|
return value
|
|
if isinstance(value, dict):
|
|
try:
|
|
return GradientClippingConfig.model_validate(value)
|
|
except (TypeError, ConfigValidationError):
|
|
raise ConfigValidationError(
|
|
f"Invalid params for gradient clipping: {value}, see GradientClippingConfig class."
|
|
)
|
|
raise ConfigValidationError("Field should be None or dict")
|
|
|
|
def _jsonschema_type_mapping(self):
|
|
return {
|
|
"oneOf": [
|
|
{"type": "null", "title": "disabled", "description": "Disable gradient clipping."},
|
|
{
|
|
**schema_utils.unload_jsonschema_from_config_class(GradientClippingConfig),
|
|
"title": "enabled_options",
|
|
},
|
|
],
|
|
"title": "gradient_clipping_options",
|
|
"description": description,
|
|
}
|
|
|
|
if not isinstance(default, dict):
|
|
raise ConfigValidationError(f"Invalid default: `{default}`")
|
|
|
|
def load_default():
|
|
return GradientClippingConfig.model_validate(default)
|
|
|
|
try:
|
|
dump_default = GradientClippingConfig.model_validate(default).to_dict()
|
|
except pydantic.ValidationError:
|
|
dump_default = default if isinstance(default, dict) else {}
|
|
|
|
return field(
|
|
metadata={
|
|
"marshmallow_field": GradientClippingConfigField(
|
|
allow_none=allow_none,
|
|
load_default=load_default,
|
|
dump_default=dump_default,
|
|
metadata={
|
|
"description": description,
|
|
"parameter_metadata": convert_metadata_to_json(OPTIMIZER_METADATA["gradient_clipping"]),
|
|
},
|
|
)
|
|
},
|
|
default_factory=load_default,
|
|
)
|