项目文件夹

文件
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 12:49:20 +08:00

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,
)