Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
445e107
fix docs
dimapihtar May 5, 2026
106a0be
Merge branch 'NVIDIA:main' into main
dimapihtar May 6, 2026
422590f
Merge branch 'NVIDIA:main' into main
dimapihtar May 6, 2026
818fd81
Merge branch 'NVIDIA:main' into main
dimapihtar May 7, 2026
4fc8774
Merge branch 'NVIDIA:main' into main
dimapihtar May 13, 2026
b6d4e6d
Merge branch 'NVIDIA:main' into main
dimapihtar May 18, 2026
f98b41d
Merge branch 'NVIDIA:main' into main
dimapihtar May 28, 2026
be77ec4
Merge branch 'NVIDIA:main' into main
dimapihtar Jun 2, 2026
eb7551f
Merge branch 'NVIDIA:main' into main
dimapihtar Jun 4, 2026
1479557
Merge branch 'NVIDIA:main' into main
dimapihtar Jun 10, 2026
6e1bf96
Merge branch 'NVIDIA:main' into main
dimapihtar Jun 24, 2026
14faa28
Merge branch 'NVIDIA:main' into main
dimapihtar Jul 3, 2026
ad1a9fe
Merge branch 'NVIDIA:main' into main
dimapihtar Jul 8, 2026
9a5d645
Merge branch 'NVIDIA:main' into main
dimapihtar Jul 16, 2026
5c1ed22
Merge branch 'NVIDIA:main' into main
dimapihtar Jul 17, 2026
1107294
Merge branch 'NVIDIA:main' into main
dimapihtar Jul 28, 2026
76e5d6c
Merge branch 'NVIDIA:main' into main
dimapihtar Jul 31, 2026
f0a960b
Merge branch 'NVIDIA:main' into main
dimapihtar Aug 4, 2026
d958d0a
Merge branch 'NVIDIA:main' into main
dimapihtar Aug 5, 2026
ca83f21
Merge branch 'NVIDIA:main' into main
dimapihtar Aug 6, 2026
25d9f9b
Merge branch 'NVIDIA:main' into main
dimapihtar Aug 17, 2026
013bdee
Merge branch 'NVIDIA:main' into main
dimapihtar Aug 18, 2026
ca9a6d0
Merge branch 'NVIDIA:main' into main
dimapihtar Aug 19, 2026
9888886
Merge branch 'NVIDIA:main' into main
dimapihtar Aug 21, 2026
751289d
Merge branch 'NVIDIA:main' into main
dimapihtar Sep 1, 2026
3d984a2
Merge branch 'NVIDIA:main' into main
dimapihtar Sep 7, 2026
452befd
Merge branch 'NVIDIA:main' into main
dimapihtar Sep 14, 2026
c99b2ab
Merge branch 'NVIDIA:main' into main
dimapihtar Sep 15, 2026
514cc62
Merge branch 'NVIDIA:main' into main
dimapihtar Sep 18, 2026
814c102
Merge branch 'NVIDIA:main' into main
dimapihtar Sep 28, 2026
35bacc6
Merge branch 'NVIDIA:main' into main
dimapihtar Sep 30, 2026
d575a69
Merge branch 'NVIDIA:main' into main
dimapihtar Oct 5, 2026
250999e
Merge branch 'NVIDIA:main' into main
dimapihtar Oct 6, 2026
9a08ed9
Merge branch 'NVIDIA:main' into main
dimapihtar Oct 8, 2026
9a14d24
Merge branch 'NVIDIA:main' into main
dimapihtar Oct 12, 2026
1312c04
Merge branch 'NVIDIA:main' into main
dimapihtar Oct 14, 2026
f0b0192
switch from args to cfg.scheduler for scheduler args
dimapihtar Oct 14, 2026
82c3205
revert changes
dimapihtar Oct 14, 2026
3fc3f48
revert changes
dimapihtar Oct 14, 2026
d40d018
use cfg.scheduler instead of args
dimapihtar Oct 14, 2026
8f27e22
Merge branch 'main' into scheduler_config_migration
dimapihtar Oct 15, 2026
a1d148c
Merge branch 'main' into scheduler_config_migration
dimapihtar Oct 16, 2026
390381c
move scheduler args validation to ShcedulerConfig
dimapihtar Oct 16, 2026
ccd0b8b
fix bugs and remove deprecated param
dimapihtar Oct 16, 2026
4894d0c
fix config finalization
dimapihtar Oct 16, 2026
4f49137
fix bugs
dimapihtar Oct 16, 2026
eb3df0e
minor fix
dimapihtar Oct 16, 2026
f8f5b4f
add unit test
dimapihtar Oct 16, 2026
a29ba81
Merge branch 'main' into scheduler_config_migration
maanug-nv Oct 20, 2026
14cb9c8
move scheduler runtime field calculations to finalize
maanug-nv Oct 20, 2026
1abfe6f
fix unit tests
maanug-nv Oct 20, 2026
2715d12
fix unit tests
dimapihtar Oct 20, 2026
bf2b9d3
Merge branch 'main' into scheduler_config_migration
dimapihtar Oct 20, 2026
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
58 changes: 1 addition & 57 deletions megatron/training/arguments.py
Original file line number Diff line number Diff line change
Expand Up @@ -1170,35 +1170,6 @@ def validate_args(args, defaults={}):
# data
assert args.num_dataset_builder_threads > 0

# Iteration-based training.
# Skip these checks when skip_train is set: LR config is irrelevant.
if args.train_iters and not args.skip_train:
# If we use iteration-based training, make sure the
# sample-based options are off.
assert args.train_samples is None, \
'expected iteration-based training'
assert args.lr_decay_samples is None, \
'expected iteration-based learning rate decay'
assert args.lr_warmup_samples == 0, \
'expected iteration-based learning rate warmup'
if args.lr_warmup_fraction is not None:
assert args.lr_warmup_iters == 0, \
'can only specify one of lr-warmup-fraction and lr-warmup-iters'

# Sample-based training.
if args.train_samples and not args.skip_train:
# If we use sample-based training, make sure the
# iteration-based options are off.
assert args.train_iters is None, \
'expected sample-based training'
assert args.lr_decay_iters is None, \
'expected sample-based learning rate decay'
assert args.lr_warmup_iters == 0, \
'expected sample-based learnig rate warmup'
if args.lr_warmup_fraction is not None:
assert args.lr_warmup_samples == 0, \
'can only specify one of lr-warmup-fraction ' \
'and lr-warmup-samples'

if args.num_layers is not None:
assert args.encoder_num_layers is None, \
Expand Down Expand Up @@ -1277,26 +1248,6 @@ def validate_args(args, defaults={}):
dc = torch.cuda.get_device_capability()
assert dc[0] >= 8, "Unsupported compute capability for GroupedGEMM kernels."

if args.no_weight_decay_cond_type is not None:
print_rank_0(
'WARNING: --no-weight-decay-cond-type is deprecated. Please use --apply-wd-to-qk-layernorm instead.',
args.rank,
)
if args.no_weight_decay_cond_type == "apply_wd_to_qk_layernorm":
args.apply_wd_to_qk_layernorm = True
else:
raise ValueError(f"Invalid no_weight_decay_cond_type: {args.no_weight_decay_cond_type}")
args.no_weight_decay_cond_type = None

if args.weight_decay_incr_style == 'constant':
assert args.start_weight_decay is None
assert args.end_weight_decay is None
args.start_weight_decay = args.weight_decay
args.end_weight_decay = args.weight_decay
else:
assert args.start_weight_decay is not None
assert args.end_weight_decay is not None

# Persistent fused layer norm.
if not is_torch_min_version("1.11.0a0"):
args.no_persist_layer_norm = True
Expand Down Expand Up @@ -2798,13 +2749,6 @@ def _add_regularization_args(parser):
group.add_argument('--lion-beta2', type=float, default=0.98,
help='Second beta coefficient for Lion optimizer '
'(used in momentum EMA update). Default: 0.98.')

group.add_argument('--no-weight-decay-cond-type', type=str, choices=['apply_wd_to_qk_layernorm'],
help='Type of no weight decay condition. Choices: '
'None (default): apply weight decay to 1D weights and biases.'
'"apply_wd_to_qk_layernorm": additionally apply weight decay to '
'qk layernorm as a special case.'
'DEPRECATED. Please use --apply-wd-to-qk-layernorm instead. ')
return parser


Expand Down Expand Up @@ -2951,7 +2895,7 @@ def _add_initialization_args(parser):
def _add_learning_rate_args(parser):
from megatron.training.config import SchedulerConfig

sched_factory = ArgumentGroupFactory(SchedulerConfig, exclude=["no_weight_decay_cond_type"])
sched_factory = ArgumentGroupFactory(SchedulerConfig)
group = sched_factory.build_group(parser, title="learning rate and weight decay")

group.add_argument('--lr', type=float, default=None,
Expand Down
2 changes: 1 addition & 1 deletion megatron/training/checkpointing.py
Original file line number Diff line number Diff line change
Expand Up @@ -3333,7 +3333,7 @@ def load_model_state_dict(module, state_dict, strict: bool):
# Optimizer state dict can overwrite per-group max_lr/min_lr values.
# If scheduler override is requested, re-apply runtime lr bounds from
# args so scheduler math does not stay pinned to checkpoint lr settings.
if getattr(args, "override_opt_param_scheduler", False):
if getattr(cfg.scheduler, "override_opt_param_scheduler", False):
if (
optimizer is None
or getattr(optimizer, "is_stub_optimizer", False)
Expand Down
76 changes: 76 additions & 0 deletions megatron/training/config/container.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@

from megatron.core.distributed.distributed_data_parallel_config import DistributedDataParallelConfig
from megatron.core.msc_utils import maybe_msc
from megatron.core.num_microbatches_calculator import (
get_current_global_batch_size,
update_num_microbatches,
)
from megatron.core.optimizer import OptimizerConfig
from megatron.training.config.common_config import DistributedInitConfig, ProfilingConfig, RNGConfig
from megatron.training.config.inference_config import InferenceSetupConfig
Expand Down Expand Up @@ -341,6 +345,43 @@ def _resolve_batch_sizes(
)
return global_batch_size, eval_global_batch_size, eval_micro_batch_size

def _resolve_scheduler_step_fields(self):
"""Calculate user-unspecified scheduler configuration arguments from configurable fields."""

# Without a training horizon there is nothing to resolve; building the optimizer param
# scheduler (get_optimizer_param_scheduler) is what requires one.
if self.train.train_iters is None and self.train.train_samples is None:
return
# Iteration-based training.
if self.train.train_iters:
if self.scheduler.lr_decay_iters is None:
self.scheduler.lr_decay_iters = self.train.train_iters
self.scheduler.lr_decay_steps = self.scheduler.lr_decay_iters * self.train.global_batch_size
self.scheduler.wd_incr_steps = self.train.train_iters * self.train.global_batch_size
self.scheduler.wsd_decay_steps = None
if self.scheduler.lr_wsd_decay_iters is not None:
self.scheduler.wsd_decay_steps = self.scheduler.lr_wsd_decay_iters * self.train.global_batch_size
if self.scheduler.lr_warmup_fraction is not None:
self.scheduler.lr_warmup_steps = self.scheduler.lr_warmup_fraction * self.scheduler.lr_decay_steps
else:
self.scheduler.lr_warmup_steps = self.scheduler.lr_warmup_iters * self.train.global_batch_size
# Sample-based training.
elif self.train.train_samples:
# We need to set training iters for later use. Technically
# we need to adjust the training samples too (due to last
# batch being incomplete) but we leave it as is for now.
update_train_iters(self.train)
if self.scheduler.lr_decay_samples is None:
self.scheduler.lr_decay_samples = self.train.train_samples
self.scheduler.lr_decay_steps = self.scheduler.lr_decay_samples
self.scheduler.wd_incr_steps = self.train.train_samples
self.scheduler.wsd_decay_steps = self.scheduler.lr_wsd_decay_samples
if self.scheduler.lr_warmup_fraction is not None:
self.scheduler.lr_warmup_steps = self.scheduler.lr_warmup_fraction * self.scheduler.lr_decay_steps
else:
self.scheduler.lr_warmup_steps = self.scheduler.lr_warmup_samples


def validate(self) -> None:
"""Validate each section, then the RL section against the rest of the run."""
self.rl.validate_ddp_config(self.ddp)
Expand All @@ -361,6 +402,12 @@ def _validate_rl_config(self, global_batch_size: int | None) -> None:
save_interval=self.checkpoint.save_interval,
exit_interval=self.train.exit_interval,
)
self.scheduler.validate_run(
train_iters=self.train.train_iters,
train_samples=self.train.train_samples,
skip_train=self.validation.skip_train,
weight_decay=self.optimizer.weight_decay,
)

def _finalize(self) -> None:
from megatron.core.tensor_parallel.gtp_parallel_layout import get_batch_parallel_size
Expand All @@ -376,10 +423,39 @@ def _finalize(self) -> None:
self.validation.eval_micro_batch_size,
) = batch_sizes
super()._finalize()
self.scheduler.finalize_scheduler_config(self.optimizer.weight_decay)
self._resolve_scheduler_step_fields()
if self.rl.perform_rl_step:
self.rl.finalize_ddp_config(self.ddp)


def update_train_iters(train: TrainingConfig) -> None:
"""Resolve the sample-based training horizon on the supplied training config."""
# Imported lazily: megatron.training.utils imports megatron.training.config.
from megatron.training.utils import print_rank_0

# For iteration-based training, we don't need to do anything
if train.train_iters:
return

if train.step_batch_size_schedule is not None:
# Sample based training with step batch size schedule.
iterations = 0
consumed_samples = 0
while consumed_samples < train.train_samples:
update_num_microbatches(consumed_samples, consistency_check=False)
consumed_samples += get_current_global_batch_size()
iterations += 1
# Reset
update_num_microbatches(0, consistency_check=False)
train.train_iters = iterations
else:
# Constant batch size with sample-based training.
train.train_iters = train.train_samples // train.global_batch_size

print_rank_0(f'setting training iterations to {train.train_iters}')


@dataclass(kw_only=True)
class InferenceConfigContainer(ConfigContainerBase):
"""Top-level container for inference entry points.
Expand Down
57 changes: 51 additions & 6 deletions megatron/training/config/training_config.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved.

import signal
from dataclasses import dataclass, field
from typing import List, Literal, Optional
Expand Down Expand Up @@ -226,18 +227,62 @@ class SchedulerConfig:
weight_decay_incr_style: Literal["constant", "linear", "cosine"] = "constant"
"""Weight decay increment function."""

no_weight_decay_cond_type: Literal["qwen3_next"] | None = None
"""Type of no weight decay condition. Choices:
None (default): param no weight decay if and only if it is 1D; or it is bias;
or it is embedding and embedding_init_method_std is not None.
"qwen3_next": In addition to the default rules, apply weight decay to qk layernorm as a special case."""

wd_incr_steps: int | None = field(init=False, default=None)
"""Number of samples to increment weight decay over. Calculated at runtime."""

wsd_decay_steps: int | None = field(init=False, default=None)
"""Number of samples to decay WSD weight decay. Calculated at runtime."""

def validate_run(
self,
*,
train_iters: int | None,
train_samples: int | None,
skip_train: bool,
weight_decay: float | None,
) -> None:
"""Check this section against the rest of the run it belongs to."""
if train_iters and not skip_train:
# If we use iteration-based training, make sure the
# sample-based options are off.
assert train_samples is None, \
'expected iteration-based training'
assert self.lr_decay_samples is None, \
'expected iteration-based learning rate decay'
assert self.lr_warmup_samples == 0, \
'expected iteration-based learning rate warmup'
if self.lr_warmup_fraction is not None:
assert self.lr_warmup_iters == 0, \
'can only specify one of lr-warmup-fraction and lr-warmup-iters'

# Sample-based training.
if train_samples and not skip_train:
# If we use sample-based training, make sure the
# iteration-based options are off.
assert train_iters is None, \
'expected sample-based training'
assert self.lr_decay_iters is None, \
'expected sample-based learning rate decay'
assert self.lr_warmup_iters == 0, \
'expected sample-based learning rate warmup'
if self.lr_warmup_fraction is not None:
assert self.lr_warmup_samples == 0, \
'can only specify one of lr-warmup-fraction ' \
'and lr-warmup-samples'

if self.weight_decay_incr_style == "constant":
for w in (self.start_weight_decay, self.end_weight_decay):
assert w is None or w == weight_decay
else:
assert self.start_weight_decay is not None
assert self.end_weight_decay is not None

def finalize_scheduler_config(self, weight_decay: float | None) -> None:
"""Finalize the scheduler configuration after validation and before training starts."""
if self.weight_decay_incr_style == "constant":
self.start_weight_decay = weight_decay
self.end_weight_decay = weight_decay


@dataclass(kw_only=True)
class LoggerConfig:
Expand Down
80 changes: 14 additions & 66 deletions megatron/training/training.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@
save_grads,
)
from megatron.training.config import FaultInjectorConfig, TrainingConfig
from megatron.training.config.container import PretrainConfigContainer
from megatron.training.config.container import PretrainConfigContainer, update_train_iters
from megatron.training.datasets.data_samplers import build_pretraining_data_loader
from megatron.training.initialize import (
initialize_megatron,
Expand Down Expand Up @@ -2248,30 +2248,6 @@ def _otel_sigterm_handler(signum, frame):
_end_otel_job_spans()


def update_train_iters(train: TrainingConfig) -> None:
"""Resolve the sample-based training horizon on the supplied training config."""
# For iteration-based training, we don't need to do anything
if train.train_iters:
return

if train.step_batch_size_schedule is not None:
# Sample based training with step batch size schedule.
iterations = 0
consumed_samples = 0
while consumed_samples < train.train_samples:
update_num_microbatches(consumed_samples, consistency_check=False)
consumed_samples += get_current_global_batch_size()
iterations += 1
# Reset
update_num_microbatches(0, consistency_check=False)
train.train_iters = iterations
else:
# Constant batch size with sample-based training.
train.train_iters = train.train_samples // train.global_batch_size

print_rank_0(f'setting training iterations to {train.train_iters}')


def resolve_ddp_bucket_size(ddp_config, dp_cp_group, overlap_grad_reduce, num_parameters):
"""Resolve the absolute DDP grad-bucket size.

Expand Down Expand Up @@ -2724,53 +2700,25 @@ def get_optimizer_param_scheduler(optimizer):
cfg = get_run_config()
args = get_args()

# Iteration-based training.
if cfg.train.train_iters:
if args.lr_decay_iters is None:
args.lr_decay_iters = cfg.train.train_iters
lr_decay_steps = args.lr_decay_iters * cfg.train.global_batch_size
wd_incr_steps = cfg.train.train_iters * cfg.train.global_batch_size
wsd_decay_steps = None
if args.lr_wsd_decay_iters is not None:
wsd_decay_steps = args.lr_wsd_decay_iters * cfg.train.global_batch_size
if args.lr_warmup_fraction is not None:
lr_warmup_steps = args.lr_warmup_fraction * lr_decay_steps
else:
lr_warmup_steps = args.lr_warmup_iters * cfg.train.global_batch_size
# Sample-based training.
elif cfg.train.train_samples:
# We need to set training iters for later use. Technically
# we need to adjust the training samples too (due to last
# batch being incomplete) but we leave it as is for now.
update_train_iters(cfg.train)
if args.lr_decay_samples is None:
args.lr_decay_samples = cfg.train.train_samples
lr_decay_steps = args.lr_decay_samples
wd_incr_steps = cfg.train.train_samples
wsd_decay_steps = args.lr_wsd_decay_samples
if args.lr_warmup_fraction is not None:
lr_warmup_steps = args.lr_warmup_fraction * lr_decay_steps
else:
lr_warmup_steps = args.lr_warmup_samples
else:
if cfg.train.train_iters is None and cfg.train.train_samples is None:
raise Exception('either train-iters or train-samples should be provided.')

opt_param_scheduler = OptimizerParamScheduler(
optimizer,
init_lr=args.lr_warmup_init,
init_lr=cfg.scheduler.lr_warmup_init,
max_lr=args.lr,
min_lr=args.min_lr,
lr_warmup_steps=lr_warmup_steps,
lr_decay_steps=lr_decay_steps,
lr_decay_style=args.lr_decay_style,
start_wd=args.start_weight_decay,
end_wd=args.end_weight_decay,
wd_incr_steps=wd_incr_steps,
wd_incr_style=args.weight_decay_incr_style,
use_checkpoint_opt_param_scheduler=args.use_checkpoint_opt_param_scheduler,
override_opt_param_scheduler=args.override_opt_param_scheduler,
wsd_decay_steps=wsd_decay_steps,
lr_wsd_decay_style=args.lr_wsd_decay_style,
lr_warmup_steps=cfg.scheduler.lr_warmup_steps,
lr_decay_steps=cfg.scheduler.lr_decay_steps,
lr_decay_style=cfg.scheduler.lr_decay_style,
start_wd=cfg.scheduler.start_weight_decay,
end_wd=cfg.scheduler.end_weight_decay,
wd_incr_steps=cfg.scheduler.wd_incr_steps,
wd_incr_style=cfg.scheduler.weight_decay_incr_style,
use_checkpoint_opt_param_scheduler=cfg.scheduler.use_checkpoint_opt_param_scheduler,
override_opt_param_scheduler=cfg.scheduler.override_opt_param_scheduler,
wsd_decay_steps=cfg.scheduler.wsd_decay_steps,
lr_wsd_decay_style=cfg.scheduler.lr_wsd_decay_style,
)

return opt_param_scheduler
Expand Down
Loading
Loading