Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
169 changes: 74 additions & 95 deletions megatron/core/optimizer/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import logging
import warnings
from dataclasses import astuple
from typing import Callable, Dict, List, Optional, Tuple, Union
from typing import Any, Callable, Dict, List, Optional, Tuple, Union

import torch
from torch.optim import SGD as CPUSGD
Expand Down Expand Up @@ -35,6 +35,11 @@

from megatron.core import parallel_state
from megatron.core.optimizer.cpu_offloading.hybrid_optimizer import HybridDeviceOptimizer
from megatron.core.optimizer_param_scheduler import (
ParamGroupOverride,
combine_param_group_overrides,
param_group_override_to_tuple,
)
from megatron.core.process_groups_config import ProcessGroupCollection
from megatron.core.transformer.fsdp_dtensor_checkpoint import get_global_unique_param_name

Expand All @@ -50,114 +55,75 @@
MegatronOptimizer,
param_group_identifier_keys,
)
from .optimizer_config import AdamOptimizerConfig, OptimizerConfig, ParamKey, SGDOptimizerConfig
from .optimizer_config import (
AdamOptimizerConfig,
OptimizerConfig,
ParamKey,
ParamPredicate,
SGDOptimizerConfig,
)

logger = logging.getLogger(__name__)


def _matches(param: torch.nn.Parameter, param_name: str, param_key: ParamKey) -> bool:
"""Returns true if passed-in parameter (with name) matches `param_key`.

Args:
param (torch.nn.Parameter): Handle to parameter object.
param_name (str): Name of parameter in underlying PyTorch module.
param_key (ParamKey): ParamKey object.

Returns:
bool: True if parameter matches passed-in param_key.
"""

# Check if name matches.
if isinstance(param_key.name, str):
target_names = [param_key.name]
else:
target_names = list(param_key.name)
for target_name in target_names:
if param_name in target_name:
return True

# Check if attribute matches.
if isinstance(param_key.attr, str):
target_attrs = [param_key.attr]
else:
target_attrs = list(param_key.attr)
for target_attr in target_attrs:
if getattr(param, target_attr, False):
return True

return False


def _get_param_groups(
model_chunks: List[MegatronModule],
config: OptimizerConfig,
config_overrides: Optional[Dict[ParamKey, OptimizerConfig]],
config_overrides: Optional[Dict[ParamKey, ParamGroupOverride]],
) -> List[Dict]:
"""Create parameter groups for optimizer.

Creates parameter groups from provided optimizer config object.

NOTE There can be more than one match between a ParamKey and a parameter.
What we do is merge all of the matching ParamKey overrides into a single ParamGroupOverride
for that parameter and use that as the key for that parameter. Any parameters that get
the same set of merged overrides will be mapped into the same parameter group.

Args:
model_chunks (List[MegatronModule]): model chunks to create parameter
groups for.
config (OptimizerConfig): optimizer configuration object.
config_overrides (Optional[Dict[LayerKey, OptimizerConfig]): optimizer overrides,
config_overrides (Optional[Dict[ParamKey, ParamGroupOverride]): optimizer overrides,
specified on a per-layer basis.
Returns:
List of parameter groups.
"""

# Map (wd_mult, is_expert_parallel, param_group_hyperparameters_config) to params.
# Map (pg_overrides, is_expert_parallel) to params.
params_map = {}
configs_map = {}

for model_chunk in model_chunks:
for name, param in model_chunk.named_parameters():
if not param.requires_grad:
continue

uses_default_config = False
# Get optimizer config for this parameter.
if config_overrides is None:
config_for_param = config
uses_default_config = True
# Get optimizer config overrides for this parameter.
param_overrides_list: list[ParamGroupOverride] = []
if config_overrides is not None:
for param_key, param_override in config_overrides.items():
if param_key.matches(param, name):
param_overrides_list.append(param_override)

if param_overrides_list:
param_override: ParamGroupOverride | None = combine_param_group_overrides(
param_overrides_list
)
else:
config_for_param = None
for param_key in config_overrides:
if _matches(param, name, param_key):
config_for_param = config_overrides[param_key]
break
# Fall back to default config.
if config_for_param is None:
config_for_param = config
uses_default_config = True
param_override = None

is_expert_parallel = not getattr(param, 'allreduce', True)

# TODO: Make sure there is a way to support old no_weight_decay_func functionality
# and default_skip_embedding_weight_decay:
# or (default_skip_embedding_weight_decay and "embedding" in name)
no_wd = name.endswith(".bias") or len(param.shape) == 1
if not no_wd:
wd_mult = 1.0
else:
wd_mult = 0.0

# Create config_tuple that is hash-able. Remove timers object before
# creating config_tuple.
config_for_param_copy = copy.deepcopy(config_for_param)
config_for_param_copy.timers = None
config_tuple = astuple(config_for_param_copy)
key = (wd_mult, is_expert_parallel, config_tuple)
# Create config_tuple that is hash-able, and has a consistent ordering of the keys.
param_override_tuple: tuple[tuple[str, Any], ...] | None = (
param_group_override_to_tuple(param_override)
)
key = (param_override_tuple, is_expert_parallel)
if key not in params_map:
params_map[key] = []
params_map[key].append(param)

if key in configs_map:
assert (config_for_param, uses_default_config) == configs_map[key]
else:
configs_map[key] = (config_for_param, uses_default_config)

# Distributed checkpoint requires all ranks to have the same param groups,
# so we need to align the param groups across ranks, otherwise we may have
# runtime error when loading the checkpoint or numerical error when resuming training.
Expand All @@ -168,34 +134,47 @@ def _get_param_groups(
for key in keys:
if key not in params_key:
params_key.append(key)

# Need to pick one of the param_override_tuples to use for the param group.
param_groups = []
for key in params_key:
wd_mult, is_expert_parallel, _ = key
# Sort keys, None first.
for key in sorted(params_key, key=lambda x: (x[0] is not None, x[0])):
param_override_tuple, is_expert_parallel = key
params = params_map[key] if key in params_map else []
config, uses_default_config = None, True
if key not in configs_map:
assert params == []
if param_override_tuple is None:
param_override: ParamGroupOverride = {}
else:
config, uses_default_config = configs_map[key]
assert config is not None
param_override: ParamGroupOverride = {k: v for (k, v) in param_override_tuple}

# False if param_group_override is None or empty tuple or if we do not modify the
# LR schedule.
# NOTE: "default_config" is used for logging the learning rate in training.py.
# so set to True if we do not modify the learning rate.
# if param_group['default_config']:
# learning_rate = param_group['lr']
uses_default_lr_schedule: bool = (not bool(param_override_tuple)) or not any(
["lr" in k for k in param_override]
)

# TODO: Remove "backwards compatible" fields below eventually.
default_config: ParamGroupOverride = {
'wd_mult': 1.0,
'lr_mult': 1.0,
'is_decoupled_lr': False,
# The following two fields may be important to keep even when we remove the
# above "backwards compatible" fields.
"max_lr": config.lr, # user may override this in param_override
"min_lr": config.min_lr, # user may override this in param_override
}
assert (
"params" not in param_override
), "'params' should not be in param_override, this is a protected key"
param_group = {
'params': params,
'wd_mult': wd_mult, # For backwards compatibility.
'lr_mult': 1.0, # For backwards compatibility.
'is_expert_parallel': is_expert_parallel,
'is_decoupled_lr': False, # For backwards compatibility.
'default_config': uses_default_config,
'default_config': uses_default_lr_schedule,
**default_config,
**param_override, # keep **param_override last so that users can override other fields.
}

# Stick relevant fields into param_group from config object.
if config is not None:
param_group['max_lr'] = config.lr
param_group['min_lr'] = config.min_lr
# TODO: Add other relevant arguments (e.g., weight decay, optimizer)
# here as well.
param_groups.append(param_group)

return param_groups
Expand All @@ -205,7 +184,7 @@ def _get_param_groups_and_buffers(
model_chunks: List[MegatronModule],
model_chunk_offset: int,
config: OptimizerConfig,
config_overrides: Optional[Dict[ParamKey, OptimizerConfig]],
config_overrides: Optional[Dict[ParamKey, ParamGroupOverride]],
filter_fn: Callable,
buffer_name: str,
) -> Tuple[List[Dict], Dict[int, List[_ParamAndGradBuffer]]]:
Expand All @@ -216,8 +195,8 @@ def _get_param_groups_and_buffers(
groups for.
model_chunk_offset (int): offset of model_chunks in global model_chunks list.
config (OptimizerConfig): optimizer configuration object.
config_overrides (Optional[Dict[LayerKey, OptimizerConfig]): optimizer overrides,
specified on a per-layer basis.
config_overrides (Optional[Dict[ParamKey, ParamGroupOverride]): optimizer/scheduler
overrides, specified on the basis of ParamKey matches with each parameter.
lr (float): learning rate.
min_lr (float): minimum learning rate.
filter_fn (callable): filtering function for param_groups.
Expand Down Expand Up @@ -442,7 +421,7 @@ def init_state_fn(opt, config=None):
def get_megatron_optimizer(
config: OptimizerConfig,
model_chunks: List[MegatronModule],
config_overrides: Optional[Dict[ParamKey, OptimizerConfig]] = None,
config_overrides: Optional[Dict[ParamKey, ParamGroupOverride]] = None,
use_gloo_process_groups: bool = True,
pg_collection: Optional[ProcessGroupCollection] = None,
dump_param_to_param_group_map: Optional[str] = None,
Expand Down
69 changes: 68 additions & 1 deletion megatron/core/optimizer/optimizer_config.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Copyright (c) 2024, NVIDIA CORPORATION. All rights reserved.

import fnmatch
from dataclasses import dataclass, field
from typing import Callable, Optional, Tuple, Union

Expand All @@ -8,6 +9,30 @@
from ..utils import is_te_min_version


@dataclass(frozen=True)
class ParamPredicate:
"""Wraps a matching function to make it hashable for ParamKey.
Example:
>>> shape_1_param = ParamPredicate(name="s1", fn=lambda param: len(param.shape) == 1)
>>> shape_1_param(torch.empty(10))
True
>>> shape_1_param_copy = ParamPredicate(name="s1", fn=lambda param: len(param.shape) == 1)
>>> shape_1_param == shape_1_param_copy # name is used to match
True
>>> {shape_1_param, shape_1_param_copy} == {shape_1_param} # set hashing works properly

NOTE:
__hash__ and __eq__ are automatically generated by @dataclass(frozen=True)
based solely on 'name' because we set compare=False/hash=False on 'fn'.
"""

name: str
fn: Callable[[torch.nn.Parameter], bool] = field(compare=False, hash=False)

def __call__(self, param: torch.nn.Parameter) -> bool:
return self.fn(param)


@dataclass(frozen=True, slots=True)
class ParamKey:
"""Key to group parameters by. All such grouped parameters can share an
Expand All @@ -16,11 +41,53 @@ class ParamKey:
# TODO: Can add layer_id here later.

name: Union[str, Tuple[str]] = field(default_factory=tuple)
"""Parameter name(s)."""
"""Parameter name(s), will use unix filesystem path syntax for matching."""

attr: Union[str, Tuple[str]] = field(default_factory=tuple)
"""Parameter attribute(s)."""

predicate: Union[ParamPredicate, Tuple[ParamPredicate]] = field(default_factory=tuple)
"""Predicate(s) to match parameters by. If multiple predicates are provided, any must match."""

def matches(self, param: torch.nn.Parameter, param_name: str) -> bool:
"""Returns true if passed-in parameter (with name) matches `param_key`.

Args:
param (torch.nn.Parameter): Handle to parameter object.
param_name (str): Name of parameter in underlying PyTorch module.

Returns:
bool: True if parameter matches passed-in param_key.
"""

# Check if name matches.
if isinstance(self.name, str):
target_names = [self.name]
else:
target_names = list(self.name)
for target_name in target_names:
if fnmatch.fnmatch(param_name, target_name):
return True

# Check if attribute matches.
if isinstance(self.attr, str):
target_attrs = [self.attr]
else:
target_attrs = list(self.attr)
for target_attr in target_attrs:
if getattr(param, target_attr, False):
return True

# Check if predicate matches.
if isinstance(self.predicate, ParamPredicate):
if self.predicate(param):
return True
else:
for predicate in self.predicate:
if predicate(param):
return True
return False


@dataclass
class OptimizerConfig:
Expand Down
Loading