Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
28 changes: 21 additions & 7 deletions megatron/training/datasets/data_samplers.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,16 @@ def build_pretraining_data_loader(dataset, consumed_samples):
is_eval = split in (Split.valid, Split.test)
micro_batch_size = getattr(args, 'eval_micro_batch_size', args.micro_batch_size) if is_eval else args.micro_batch_size
global_batch_size = getattr(args, 'eval_global_batch_size', args.global_batch_size) if is_eval else args.global_batch_size

if split == Split.valid and args.full_validation:
batch_sampler = MegatronFullValidationSampler(
total_samples=len(dataset),
data_parallel_rank=mpu.get_data_parallel_rank(),
data_parallel_size=mpu.get_data_parallel_world_size())
elif args.dataloader_type == 'single':
if args.hybrid_context_parallel:
if (
getattr(args, "hybrid_context_parallel", False)
and getattr(args, "sequence_packing_scheduler", None) is None
):
batch_sampler = HybridCPMegatronPretrainingSampler(
total_samples=len(dataset),
consumed_samples=consumed_samples,
Expand All @@ -55,7 +57,8 @@ def build_pretraining_data_loader(dataset, consumed_samples):
data_parallel_rank=mpu.get_data_parallel_rank(),
data_parallel_size=mpu.get_data_parallel_world_size())
else:
# Megatron sampler
# Megatron sampler. Packing schedulers consume one microbatch at a
# time and form packed global batches themselves.
batch_sampler = MegatronPretrainingSampler(
total_samples=len(dataset),
consumed_samples=consumed_samples,
Expand Down Expand Up @@ -97,9 +100,21 @@ def close_nvidia_fds():
maybe_worker_init_fn = (
worker_init_fn if args.num_workers > 0 else None
)
# Torch dataloader.
if args.hybrid_context_parallel:
extra_kwargs = {"collate_fn": lambda x: x,}
# Identity collate for VarlenDataset and packing-scheduler paths;
# they emit one variable-length dict per sample, not stack-able by
# the default collate. --varlen-sbhd-validation is excluded: it bypasses
# packing and emits fixed-length [seq_length] samples that the default
# collate stacks normally.
if (
(
getattr(args, "use_varlen_dataset", False)
and not getattr(args, "varlen_sbhd_validation", False)
)
or getattr(args, "hybrid_context_parallel", False)
or getattr(args, "sequence_packing_scheduler", None) is not None
or getattr(args, "use_vanilla_collate_fn", False)
):
extra_kwargs = {"collate_fn": lambda x: x}
else:
extra_kwargs = {}
return torch.utils.data.DataLoader(
Expand Down Expand Up @@ -225,7 +240,6 @@ def __iter__(self):
global_batch_idx.extend(batch[start_idx[i]:end_idx[i]])
yield global_batch_idx


class MegatronFullValidationSampler:
"""Sampler for full validation that handles small datasets gracefully.

Expand Down
93 changes: 70 additions & 23 deletions megatron/training/training.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@

# First-party.
from megatron.core import mpu, nccl_allocator, tensor_parallel
from megatron.core.datasets.data_schedule import HybridCPDataLoaderWrapper
from megatron.core.datasets.data_schedule import HybridCPDataLoaderWrapper, wrap_data_iterator
from megatron.core.distributed import DistributedDataParallel as DDP
from megatron.core.distributed import (
DistributedDataParallelConfig,
Expand Down Expand Up @@ -238,22 +238,6 @@

stimer = StragglerDetector()

# Per-iteration packed-sequence (THD) accumulator. The tensor holds TWO stats,
# both computed from the REAL ``cu_seqlens`` (i.e. unpadded sub-sequence lengths
# -- ``cu_seqlens_padded`` is intentionally ignored so that neither the
# per-chunk CP alignment padding nor any end-of-sequence padding is counted as
# useful work):
# index 0 -> ``sum_i(L_i)`` (total real tokens; used for token-linear FLOPs:
# projections, MLP, MoE, MTP, logits)
# index 1 -> ``sum_i(L_i**2)`` (used for core-attention FLOPs)
# Lives on GPU as fp64 so per-micro-batch updates run as fused kernels with
# no host sync; the only host sync happens once at ``consume`` time after a
# single 2-element all-reduce. ``_seqlen_stats_active`` flips to ``True`` the
# first time an update lands this iteration and is the gate that decides
# whether ``consume_*`` issues a collective at all -- unpacked BSHD runs
# never call ``update_*`` so the flag stays ``False`` and no collective fires.
_seqlen_stats_in_iteration: Optional[torch.Tensor] = None
_seqlen_stats_active: bool = False

# Only report memory for first 3 checkpoint saves.
num_checkpoints_memory_reported = 0
Expand Down Expand Up @@ -297,6 +281,24 @@ def print_datetime(string, override_timestamp=None):
time_str = datetime.fromtimestamp(override_timestamp).strftime('%Y-%m-%d %H:%M:%S.%f')
print_rank_0(f'[{string}] datetime: {time_str} ')

# Per-iteration packed-sequence (THD) accumulator. The tensor holds TWO stats,
# both computed from the REAL ``cu_seqlens`` (i.e. unpadded sub-sequence lengths
# -- ``cu_seqlens_padded`` is intentionally ignored so that neither the
# per-chunk CP alignment padding nor any end-of-sequence padding is counted as
# useful work):
# index 0 -> ``sum_i(L_i)`` (total real tokens; used for token-linear FLOPs:
# projections, MLP, MoE, MTP, logits)
# index 1 -> ``sum_i(L_i**2)`` (used for core-attention FLOPs)
# Lives on GPU as fp64 so per-micro-batch updates run as fused kernels with
# no host sync; the only host sync happens once at ``consume`` time after a
# single 2-element all-reduce. ``_seqlen_stats_active`` flips to ``True`` the
# first time an update lands this iteration and is the gate that decides
# whether ``consume_*`` issues a collective at all -- unpacked BSHD runs
# never call ``update_*`` so the flag stays ``False`` and no collective fires.
_seqlen_stats_in_iteration: Optional[torch.Tensor] = None
_seqlen_stats_active: bool = False
_seqlen_stats_are_global: bool = False


def update_seqlen_stats_from_cu_seqlens(cu_seqlens):
"""Add ``sum(L_i)`` and ``sum(L_i ** 2)`` from one micro-batch's REAL ``cu_seqlens``.
Expand All @@ -315,7 +317,7 @@ def update_seqlen_stats_from_cu_seqlens(cu_seqlens):
the all-reduce; BSHD callers that never invoke this function leave the
flag at ``False`` and pay zero collective cost.
"""
global _seqlen_stats_in_iteration, _seqlen_stats_active
global _seqlen_stats_in_iteration, _seqlen_stats_active, _seqlen_stats_are_global
if cu_seqlens is None or cu_seqlens.numel() < 2:
return
# Pin the accumulator to the current CUDA device when available so the
Expand All @@ -334,6 +336,21 @@ def update_seqlen_stats_from_cu_seqlens(cu_seqlens):
_seqlen_stats_in_iteration[0] += seqlens.sum()
_seqlen_stats_in_iteration[1] += (seqlens * seqlens).sum()
_seqlen_stats_active = True
_seqlen_stats_are_global = False


def set_seqlen_stats_in_iteration(total_real_tokens, seqlen_squared_sum):
"""Seed per-iteration THD FLOPs stats that were already computed globally."""
global _seqlen_stats_in_iteration, _seqlen_stats_active, _seqlen_stats_are_global
if total_real_tokens is None or seqlen_squared_sum is None:
return
if _seqlen_stats_in_iteration is None:
device = torch.device(f'cuda:{torch.cuda.current_device()}') if torch.cuda.is_available() else 'cpu'
_seqlen_stats_in_iteration = torch.zeros(2, dtype=torch.float64, device=device)
_seqlen_stats_in_iteration[0] = float(total_real_tokens)
_seqlen_stats_in_iteration[1] = float(seqlen_squared_sum)
_seqlen_stats_active = True
_seqlen_stats_are_global = True


def consume_seqlen_stats_in_iteration() -> Tuple[Optional[float], Optional[float]]:
Expand Down Expand Up @@ -361,13 +378,15 @@ def consume_seqlen_stats_in_iteration() -> Tuple[Optional[float], Optional[float
replicated across TP/CP/PP); the world all-reduce therefore overcounts by a
factor of ``TP * CP * PP``, which we divide out.
"""
global _seqlen_stats_in_iteration, _seqlen_stats_active
global _seqlen_stats_in_iteration, _seqlen_stats_active, _seqlen_stats_are_global
if not _seqlen_stats_active:
# BSHD path: never allocated the tensor; tell the caller to use the
# closed-form defaults.
return None, None
t = _seqlen_stats_in_iteration
if torch.distributed.is_initialized() and mpu.model_parallel_is_initialized():
if _seqlen_stats_are_global:
dedup = 1
elif torch.distributed.is_initialized() and mpu.model_parallel_is_initialized():
torch.distributed.all_reduce(t)
tp_size = max(mpu.get_tensor_model_parallel_world_size(), 1)
cp_size = max(mpu.get_context_parallel_world_size(), 1)
Expand All @@ -383,6 +402,7 @@ def consume_seqlen_stats_in_iteration() -> Tuple[Optional[float], Optional[float
# iterations reuse it without reallocating.
t.zero_()
_seqlen_stats_active = False
_seqlen_stats_are_global = False
return total_real_tokens / dedup, seqlen_squared_sum / dedup


Expand Down Expand Up @@ -2351,6 +2371,20 @@ def train_step(forward_step_func, data_iterator, model, optimizer, opt_param_sch
if isinstance(optim_instance, DistributedOptimizer):
optim_instance._copy_main_params_to_param_buffer()

if getattr(config, "sequence_packing_scheduler", None) is not None:
(
data_iterator,
scheduled_num_microbatches,
total_real_tokens_in_batch,
seqlen_squared_sum_in_batch,
) = wrap_data_iterator(data_iterator, config, get_num_microbatches())
set_seqlen_stats_in_iteration(
total_real_tokens_in_batch,
seqlen_squared_sum_in_batch,
)
else:
scheduled_num_microbatches = get_num_microbatches()

# Forward pass.
if save_activations_in_this_iteration:
enable_activation_logging(model, args.save)
Expand All @@ -2362,7 +2396,7 @@ def train_step(forward_step_func, data_iterator, model, optimizer, opt_param_sch
forward_step_func=forward_step_func,
data_iterator=data_iterator,
model=model,
num_microbatches=get_num_microbatches(),
num_microbatches=scheduled_num_microbatches,
seq_length=args.seq_length,
micro_batch_size=args.micro_batch_size,
decoder_seq_length=args.decoder_seq_length,
Expand Down Expand Up @@ -3709,6 +3743,9 @@ def trace_handler(p):

# Completely skip iteration if needed.
if (iteration + 1) in args.iterations_to_skip:
assert (
getattr(config, "sequence_packing_scheduler", None) is None
), "Sequence packing scheduler is not supported in skip iteration mode"
# Dummy train_step to fast forward train_data_iterator.
dummy_train_step(train_data_iterator)
if iteration == start_iteration:
Expand Down Expand Up @@ -4141,11 +4178,21 @@ def evaluate(
# Don't care about timing during evaluation
config.timers = None
ft_integration.on_eval_step_start()
if getattr(config, "sequence_packing_scheduler", None) is not None:
try:
(packed_data_iterator, scheduled_eval_num_microbatches, _, _) = (
wrap_data_iterator(data_iterator, config, eval_num_microbatches)
)
except StopIteration:
break
else:
packed_data_iterator = data_iterator
scheduled_eval_num_microbatches = eval_num_microbatches
loss_dicts = forward_backward_func(
forward_step_func=forward_step_func,
data_iterator=data_iterator,
data_iterator=packed_data_iterator,
model=model,
num_microbatches=eval_num_microbatches,
num_microbatches=scheduled_eval_num_microbatches,
seq_length=args.seq_length,
micro_batch_size=eval_micro_batch_size,
decoder_seq_length=args.decoder_seq_length,
Expand Down
116 changes: 77 additions & 39 deletions pretrain_gpt.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
from gpt_builders import gpt_builder
from megatron.core import mpu
from megatron.core.datasets.blended_megatron_dataset_builder import BlendedMegatronDatasetBuilder
from megatron.core.datasets.data_schedule import get_batch_on_this_rank_for_sequence_packing
from megatron.core.datasets.gpt_dataset import GPTDataset, GPTDatasetConfig, MockGPTDataset
from megatron.core.enums import ModelType
from megatron.core.package_info import __version__ as mcore_version
Expand Down Expand Up @@ -100,6 +101,19 @@ def get_batch(data_iterator, vp_stage: Optional[int] = None):
args = get_args()
config = core_transformer_config_from_args(args)

if args.sequence_packing_scheduler is not None:
return get_batch_on_this_rank_for_sequence_packing(
data_iterator,
vpp_size=config.virtual_pipeline_model_parallel_size,
mtp_on_this_rank=mtp_on_this_rank_func(
layout=config.pipeline_model_parallel_layout,
mtp_num_layers=config.mtp_num_layers,
ignore_virtual=False,
vp_stage=vp_stage,
),
vp_stage=vp_stage,
)

cp_size = args.context_parallel_size
tp_rank = mpu.get_tensor_model_parallel_rank()
is_sft = args.sft
Expand Down Expand Up @@ -294,44 +308,61 @@ def forward_step(data_iterator, model: GPTModel, return_schedule_plan: bool = Fa
timers('batch-generator', log_level=2).start()
with stimer(bdata=True):
vp_stage = get_attr_wrapped_model(model, "vp_stage")
(
attention_mask,
cu_seqlens,
cu_seqlens_padded,
hybrid_cp_group,
labels,
local_cp_size,
loss_mask,
max_seqlen,
position_ids,
tokens,
) = get_batch(data_iterator, vp_stage)

packed_seq_params = None
if cu_seqlens is not None:
# Squeeze the batch dim: the batch dict keeps cu_seqlens as (1, N)
# for consistency, but PackedSeqParams and TE expect 1-D.
cu_seqlens = cu_seqlens.squeeze(0)
if cu_seqlens_padded is not None:
cu_seqlens_padded = cu_seqlens_padded.squeeze(0)
# Use real (unpadded) cu_seqlens to feed the FLOPs accounting: varlen
# attention only computes work for real tokens within each chunk.
update_seqlen_stats_from_cu_seqlens(cu_seqlens)
cu_seqlens_for_params = (
cu_seqlens_padded if cu_seqlens_padded is not None else cu_seqlens
) # TODO(asolergi-nv): Currently there is a bug forcing cu_seqlens to be cu_seqlens_padded
packed_seq_params = PackedSeqParams(
qkv_format="thd",
cu_seqlens_q=cu_seqlens_for_params,
cu_seqlens_kv=cu_seqlens_for_params,
cu_seqlens_q_padded=cu_seqlens_padded,
cu_seqlens_kv_padded=cu_seqlens_padded,
max_seqlen_q=int(max_seqlen.item()),
max_seqlen_kv=int(max_seqlen.item()),
local_cp_size=int(local_cp_size.item()) if local_cp_size is not None else None,
cp_group=hybrid_cp_group,
tokens_per_sample=args.seq_length,
)
batch = get_batch(data_iterator, vp_stage)

if len(batch) == 7:
(
tokens,
labels,
loss_mask,
attention_mask,
position_ids,
packed_seq_params,
padding_mask,
) = batch
elif len(batch) == 6:
tokens, labels, loss_mask, attention_mask, position_ids, packed_seq_params = batch
padding_mask = None
else:
(
attention_mask,
cu_seqlens,
cu_seqlens_padded,
hybrid_cp_group,
labels,
local_cp_size,
loss_mask,
max_seqlen,
position_ids,
tokens,
) = batch

padding_mask = None
packed_seq_params = None
if cu_seqlens is not None:
# Squeeze the batch dim: the batch dict keeps cu_seqlens as (1, N)
# for consistency, but PackedSeqParams and TE expect 1-D.
cu_seqlens = cu_seqlens.squeeze(0)
if cu_seqlens_padded is not None:
cu_seqlens_padded = cu_seqlens_padded.squeeze(0)
# Use real (unpadded) cu_seqlens to feed the FLOPs accounting: varlen
# attention only computes work for real tokens within each chunk.
update_seqlen_stats_from_cu_seqlens(cu_seqlens)
cu_seqlens_for_params = (
cu_seqlens_padded if cu_seqlens_padded is not None else cu_seqlens
) # TODO(asolergi-nv): Currently there is a bug forcing cu_seqlens to be cu_seqlens_padded
packed_seq_params = PackedSeqParams(
qkv_format="thd",
cu_seqlens_q=cu_seqlens_for_params,
cu_seqlens_kv=cu_seqlens_for_params,
cu_seqlens_q_padded=cu_seqlens_padded,
cu_seqlens_kv_padded=cu_seqlens_padded,
max_seqlen_q=int(max_seqlen.item()),
max_seqlen_kv=int(max_seqlen.item()),
local_cp_size=int(local_cp_size.item()) if local_cp_size is not None else None,
cp_group=hybrid_cp_group,
tokens_per_sample=args.seq_length,
)

timers('batch-generator').stop()

Expand All @@ -341,7 +372,13 @@ def forward_step(data_iterator, model: GPTModel, return_schedule_plan: bool = Fa
args.overlap_moe_expert_parallel_comm
), "overlap_moe_expert_parallel_comm must be enabled to return the schedule plan"
schedule_plan = model.build_schedule_plan(
tokens, position_ids, attention_mask, labels=labels, loss_mask=loss_mask
tokens,
position_ids,
attention_mask,
labels=labels,
loss_mask=loss_mask,
packed_seq_params=packed_seq_params,
padding_mask=padding_mask,
)
return schedule_plan, partial(loss_func, loss_mask, model=model)
else:
Expand All @@ -352,6 +389,7 @@ def forward_step(data_iterator, model: GPTModel, return_schedule_plan: bool = Fa
labels=labels,
loss_mask=loss_mask,
packed_seq_params=packed_seq_params,
padding_mask=padding_mask,
)

# [ModelOpt]: model is needed to access ModelOpt distillation losses
Expand Down
Loading