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
677 changes: 676 additions & 1 deletion megatron/core/datasets/data_schedule.py

Large diffs are not rendered by default.

952 changes: 952 additions & 0 deletions megatron/core/datasets/data_schedule_utils.py

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions megatron/core/datasets/gpt_dataset.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,8 @@ class GPTDatasetConfig(BlendedMegatronDatasetConfig):
Set to 0 if sequence parallel is not enabled regardless of TP size.
"""

hybrid_context_parallel: bool = False
"""Option to enable hybrid context parallelism. When setting this to True,
dynamic_context_parallel: bool = False
"""Option to enable dynamic context parallelism. When setting this to True,
each sample should be divisible by the data parallel size * context parallel size * 2.
If sequence parallel is enabled, it should be divisible by the
data parallel size * context parallel size * sequence parallel size * 2.
Expand Down
60 changes: 60 additions & 0 deletions megatron/core/datasets/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,66 @@ To query the `BlendedDataset` for the _k_-th sample we do the following

To save time during initialization, each index is built/cached sequentially on one process rank and subsequently loaded in parallel on other process ranks. The cached indices are unique to a hash generated in the `BlendedDataset.__init__` function.

## Packing Scheduler

The packing scheduler re-schedules variable-length sequences across DP×CP ranks to improve GPU utilization. It is built around two modules: `data_schedule.py` (high-level logic and entry points) and `data_schedule_utils.py` (utility functions).

### Call Hierarchy

The scheduling pipeline has two phases connected by the data iterator: `wrap_data_iterator` consumes the **original** data iterator, performs global-batch scheduling, and produces a **wrapped** (packed) data iterator; `get_batch_on_this_rank_for_sequence_packing` then consumes this **wrapped** data iterator to fetch individual packed microbatches during training.

```
original wrapped (packed)
data_iterator data_iterator
│ │
▼ ▼
┌────────────────────────┐ ┌────────────────────────────────────┐
│ wrap_data_iterator() │ │ get_batch_on_this_rank_for_ │
Phase 1 │ (once per global │ ────────► │ sequence_packing() │ Phase 2
(scheduling) │ batch) │ returns │ (once per microbatch, │ (fetching)
│ │ wrapped │ called by training loop) │
└───────────┬────────────┘ iterator └──────────────┬─────────────────────┘
│ │
▼ ▼
DpBalancedScheduler.run() next(wrapped_data_iterator)
│ ├─ get_thd_partitioned_indices() [TE]
├─ get_batch_and_global_seqlens() [utils] ├─ broadcast_tensor() [utils]
├─ get_groups_and_subsamples() └─ PackedSeqParams(...)
├─ reroute_samples_to_dcp_ranks() [utils]
├─ build_packed_microbatches() [utils]
├─ broadcast_scalars() [utils]
└─ create_data_iterator() [utils]
```

### `data_schedule.py`

#### Entry Points

- **`wrap_data_iterator(original_data_iterator) → wrapped_data_iterator`** — Top-level entry point called once per global batch. Takes the **original** data iterator as input, resolves the scheduler class from `scheduler_map`, instantiates it, and delegates to `scheduler.run()` which consumes all microbatches from the original iterator, re-schedules them, and produces a **wrapped** (packed) data iterator along with the updated `num_microbatches` and FLOPs statistics.

- **`get_batch_on_this_rank_for_sequence_packing(wrapped_data_iterator)`** — Per-microbatch entry point called by the training loop. Takes the **wrapped** data iterator returned by `wrap_data_iterator` as input. Fetches one packed microbatch via `next(wrapped_data_iterator)`, broadcasts batch fields across TP ranks, optionally partitions sequences across CP ranks using Transformer Engine's `thd_get_partitioned_indices`, and constructs `PackedSeqParams` (with `cu_seqlens`, `max_seqlen`, `qkv_format=thd`).

#### Scheduler Classes

- **`BasePackingScheduler`** — Abstract base class. Defines the interface:
- `get_groups_and_subsamples()` — pure scheduling algorithm (must be overridden).
- `run()` — full pipeline: fetch → schedule → reroute → pack → broadcast → VPP handling.

- **`DpBalancedScheduler(BasePackingScheduler)`** — Concrete scheduler that packs sequences in their original order until reaching `max_seqlen_per_dp_cp_rank × cp_size`. Aligns the number of microbatches to `dp_size` (and VPP stage multiples when applicable).

### `data_schedule_utils.py`

Utility functions consumed by the schedulers above:

| Function | Role |
|---|---|
| `get_batch_and_global_seqlens()` | Fetch `num_microbatches` batches from the data iterator and all-gather sequence lengths across DP ranks. |
| `reroute_samples_to_dcp_ranks()` | All-to-all communication to transfer sub-samples to their scheduled DP×CP rank. |
| `build_packed_microbatches()` | Concatenate sub-samples within each microbatch group and produce `cu_seqlens`. |
| `broadcast_scalars()` | Broadcast scalar values (e.g. `num_microbatches`, FLOPs stats) across a process group. |
| `broadcast_tensor()` | Broadcast a single tensor within a process group. |
| `create_data_iterator()` | Wrap packed sample lists into a data iterator; handles VPP stage splitting. |

## Offline cache preparation

For GPT-style training, the dataset caches described above can be prepared ahead of time with `tools/prepare_cache.py` instead of waiting for rank 0 to build them during training startup.
Expand Down
52 changes: 37 additions & 15 deletions megatron/core/extensions/transformer_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -1791,21 +1791,22 @@ def forward(
"""Forward."""
if packed_seq_params is not None:
# If Dynamic CP group is provided, update TE DPA CP group
if packed_seq_params.cp_group is not None:
self.cp_group = packed_seq_params.cp_group
super().set_context_parallel_group(
self.cp_group,
torch.distributed.get_process_group_ranks(self.cp_group),
TEDotProductAttention.cp_stream,
self.cp_comm_type,
)
# If cp_group is None but local_cp_size is provided,
# Indicates to turn off CP dynamically
elif packed_seq_params.local_cp_size is not None:
assert (
packed_seq_params.local_cp_size == 1
), "local_cp_size must be == 1 if provided without cp_group"
super().set_context_parallel_group(None, None, None, self.cp_comm_type)
if packed_seq_params.local_cp_size is not None:
if packed_seq_params.local_cp_size == 1:
super().set_context_parallel_group(None, None, None, self.cp_comm_type)
else:
assert (
packed_seq_params.cp_group is not None
), "cp_group is not set in packed_seq_params for dynamic CP"
self.cp_group = packed_seq_params.cp_group
if TEDotProductAttention.cp_stream is None:
TEDotProductAttention.cp_stream = torch.cuda.Stream()
super().set_context_parallel_group(
self.cp_group,
torch.distributed.get_process_group_ranks(self.cp_group),
TEDotProductAttention.cp_stream,
self.cp_comm_type,
)
self.kept_packed_seq_params.discard("cp_group")
self.kept_packed_seq_params.discard("local_cp_size")

Expand Down Expand Up @@ -3383,3 +3384,24 @@ def set_save_original_input(module):
from transformer_engine.pytorch.float8_tensor import Float8Tensor
except ImportError:
Float8Tensor = None


def get_thd_partitioned_indices(cu_seqlens, total_tokens, cp_size, cp_rank):
"""Get partitioned indices for THD format data in context parallel.

Args:
cu_seqlens: Cumulative sequence lengths tensor.
total_tokens: Total number of tokens.
cp_size: Context parallel world size.
cp_rank: Context parallel rank.

Returns:
Partitioned indices tensor.
"""
assert is_te_min_version("1.10.0"), (
"Please update Transformer Engine to >= 1.10 to use "
"Context Parallel with THD format data"
)
import transformer_engine_torch as tex

return tex.thd_get_partitioned_indices(cu_seqlens, total_tokens, cp_size, cp_rank)
51 changes: 47 additions & 4 deletions megatron/core/model_parallel_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,14 +59,29 @@ class ModelParallelConfig:
can handle without overflowing the memory. Typically, a good starting point is to set this
to maximum sequence length / context parallel size.
This is used to calculate the number and length of sub-samples assigned to
each rank when using hybrid_context_parallel.
each rank when sequence_packing_scheduler is not None.
"""

hybrid_context_parallel: bool = False
dynamic_context_parallel: bool = False
"""
If true, enables hybrid context parallel. This is used to balance the workload of
If true, enables dynamic context parallel. This is used to balance the workload of
each CP rank when we use packed samples with variable sequence lengths.
Please set max_seqlen_per_dp_cp_rank when using hybrid_context_parallel.
Dynamic CP forms variable-sized CP groups from the DPxCP ranks dynamically.
Please set max_seqlen_per_dp_cp_rank.
"""

min_dynamic_context_parallel_size: int = 1
"""Minimum CP group size for dynamic context parallel. Default 1 (no CP).
The maximum is dp_size * context_parallel_size (the full DPxCP group)."""

hybrid_context_parallel: bool = False
"""Deprecated. Use ``dynamic_context_parallel`` instead."""

sequence_packing_scheduler: Optional[Literal['dp_balanced', 'default_dynamic_cp']] = None
"""
Scheduler for sequence packing and dynamic context parallel.
dp_balanced: DP-balanced scheduler for sequence packing.
default_dynamic_cp: Dynamic-CP scheduler for packed sequence balancing.
"""

expert_model_parallel_size: int = 1
Expand Down Expand Up @@ -418,6 +433,34 @@ def __post_init__(self):
See https://docs.python.org/3/library/dataclasses.html#post-init-processing for more
details.
"""
if self.hybrid_context_parallel:
warnings.warn(
"hybrid_context_parallel is deprecated and will be removed in a future release. "
"Use dynamic_context_parallel instead.",
DeprecationWarning,
)
if self.dynamic_context_parallel:
raise ValueError(
"Cannot set both hybrid_context_parallel and dynamic_context_parallel. "
"Please use dynamic_context_parallel only."
)
self.dynamic_context_parallel = True

if self.dynamic_context_parallel:
if self.sequence_packing_scheduler is None:
self.sequence_packing_scheduler = 'default_dynamic_cp'
if self.sequence_packing_scheduler != 'default_dynamic_cp':
raise ValueError(
'Dynamic context parallelism requires '
'sequence_packing_scheduler=default_dynamic_cp'
)

if self.min_dynamic_context_parallel_size < 1:
raise ValueError(
f"min_dynamic_context_parallel_size must be >= 1, "
f"got {self.min_dynamic_context_parallel_size}"
)

if self.sequence_parallel:
if self.tensor_model_parallel_size <= 1:
raise ValueError("Cannot use sequence parallelism without tensor parallelism")
Expand Down
5 changes: 3 additions & 2 deletions megatron/core/models/gpt/gpt_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
RotaryEmbedding,
)
from megatron.core.models.common.language_module.language_module import LanguageModule
from megatron.core.packed_seq_params import PackedSeqParams
from megatron.core.packed_seq_params import PackedSeqParams, resolve_cp_group
from megatron.core.pipeline_parallel.fine_grained_activation_offload import (
FineGrainedActivationOffloadingInterface as off_interface,
)
Expand Down Expand Up @@ -671,6 +671,7 @@ def _postprocess(
self._decoder_hidden_states_cache = hidden_states
else:
# In training/eval, use the utility function for processing MTP loss/scaling.
mtp_cp_group = resolve_cp_group(self.pg_collection.cp, packed_seq_params)
hidden_states = process_mtp_loss(
hidden_states=hidden_states,
labels=labels,
Expand All @@ -681,7 +682,7 @@ def _postprocess(
is_training=self.training,
compute_language_model_loss=self.compute_language_model_loss,
config=self.config,
cp_group=self.pg_collection.cp,
cp_group=mtp_cp_group,
tp_group=self.tp_group,
packed_seq_params=packed_seq_params,
scale_logits_fn=self._scale_logits if self.config.use_mup else None,
Expand Down
14 changes: 14 additions & 0 deletions megatron/core/packed_seq_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,17 @@ def __post_init__(self):
.to(torch.int32)
.unsqueeze(0) # Add a batch dimension
)


def resolve_cp_group(
static_cp_group: dist.ProcessGroup, packed_seq_params: PackedSeqParams = None
) -> dist.ProcessGroup:
"""Return the dynamic CP group from packed_seq_params when available, else the static one.

Dynamic CP assigns a per-microbatch CP group that may differ from the
process-group stored at model construction time. This helper centralises
the resolution logic used by GPTModel, GatedDeltaNet, and MTP layers.
"""
if packed_seq_params is not None and packed_seq_params.cp_group is not None:
return packed_seq_params.cp_group
return static_cp_group
Loading