-
Notifications
You must be signed in to change notification settings - Fork 444
feat: Enhance dataset loading efficiency with tensor parallelism #2405
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 4 commits
720715d
2fcee68
ffcb3a5
67b7faa
fc80103
4baae7b
63c7e53
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,104 @@ | ||
| # Broadcast Data Across Tensor-Parallel Ranks | ||
|
|
||
| ## Feature overview | ||
|
|
||
| The `broadcast_data_across_tp` flag controls how data is loaded across | ||
| tensor-parallel (TP) ranks during pretraining. | ||
|
|
||
| | Mode | `broadcast_data_across_tp` | Behaviour | | ||
| |---|---|---| | ||
| | Replicated (default) | `False` | Every TP rank independently builds a DataLoader and reads from storage. | | ||
| | Broadcast | `True` | Only TP-rank-0 reads from storage; the batch is broadcast to the remaining TP ranks via NCCL. | | ||
|
|
||
| **When to enable broadcast mode:** | ||
| Replicated loading works well on low-latency parallel file-systems such as | ||
| Lustre. On high-latency, network-attached storage (e.g. VAST) the redundant | ||
| I/O from all TP ranks causes severe contention -- `open()` syscall latency | ||
| degrades to ~1 s per call with 64+ concurrent processes. Enabling | ||
| `broadcast_data_across_tp` reduces VAST readers by a factor of TP and | ||
| eliminates the stalls. | ||
|
|
||
| ### Configuration | ||
|
|
||
| ```yaml | ||
| dataset: | ||
| broadcast_data_across_tp: true | ||
| ``` | ||
|
|
||
| ## Implementation details | ||
|
|
||
| When the flag is enabled two things change: | ||
|
|
||
| 1. **Dataset building** (`data/utils.py`): `BlendedMegatronDatasetBuilder` | ||
| receives `is_dataset_built_on_rank` instead of `lambda: True`, so the | ||
| dataset is only constructed on TP-rank-0 of first/last PP stages. | ||
|
|
||
| 2. **Batch loading** (`gpt_step.py` / `batch_utils.py`): | ||
| `get_batch_on_this_tp_rank` is called instead of `get_batch_from_iterator`. | ||
| The five standard fixed-shape tensors (tokens, labels, loss_mask, | ||
| attention_mask, position_ids) are broadcast via direct NCCL `broadcast` | ||
| calls. Any additional keys (e.g. packed-sequence metadata) are forwarded | ||
| via `broadcast_object_list`, guarded by a lightweight boolean flag so the | ||
| heavy path is skipped when there are no extra keys. | ||
|
|
||
| ## Performance benchmark | ||
|
|
||
| ### Methodology | ||
|
|
||
| - **Hardware:** 8x NVIDIA B200 (single node, NVLink interconnect) | ||
| - **Software:** PyTorch with NCCL backend, `torchrun --nproc_per_node=8` | ||
| - **Measurement:** Median of 50 iterations after 10 warmup iterations per | ||
| configuration. Each iteration includes data loading on rank 0, NCCL | ||
| broadcast, and `.cuda()` transfer on receivers. `torch.cuda.synchronize()` | ||
| bookends each iteration. | ||
|
|
||
| ### How to reproduce | ||
|
|
||
| ```bash | ||
| torchrun --nproc_per_node=8 tests/benchmarks/bench_broadcast_tp.py | ||
| ``` | ||
|
|
||
| ### Results (8x B200) | ||
|
|
||
| | Config | Data | Direct (ms) | ObjList (ms) | Overhead | | ||
| |--------------------------|--------|--------------|---------------|-----------| | ||
| | mbs=1 seq=8K | 0.2 MB | 0.14 | 0.62 | +0.48 | | ||
| | mbs=1 seq=8K +extra | 0.2 MB | 0.16 | 0.64 | +0.49 | | ||
| | mbs=1 seq=32K | 0.9 MB | 0.18 | 0.89 | +0.70 | | ||
| | mbs=1 seq=32K +extra | 0.9 MB | 0.21 | 0.90 | +0.70 | | ||
| | mbs=1 seq=128K | 3.7 MB | 0.36 | 1.86 | +1.50 | | ||
| | mbs=1 seq=256K | 7.3 MB | 0.57 | 3.58 | +3.01 | | ||
|
|
||
| **Direct broadcast** is the path taken for the standard batch keys. | ||
| **ObjList** is the fallback used only for extra keys (packed-sequence | ||
| metadata); in the common pretraining case this path is skipped entirely. | ||
|
|
||
| At 256K sequence length the direct broadcast adds < 0.6 ms per step -- | ||
| negligible compared to a typical 22 s compute-bound step. | ||
|
|
||
| ## Test coverage | ||
|
|
||
| ### Unit tests | ||
|
|
||
| ```bash | ||
| pytest tests/unit_tests/training/utils/test_batch_utils.py -v | ||
| ``` | ||
|
|
||
| | Test | What it validates | | ||
| |---|---| | ||
| | `test_standard_keys_returned` | TP rank 0 loads data; all 5 standard keys present in result. | | ||
| | `test_extra_keys_broadcast` | Extra keys (e.g. `cu_seqlens`) forwarded via `broadcast_object_list`. | | ||
| | `test_no_extra_keys_skips_heavy_broadcast` | When only standard keys exist, the heavy `broadcast_object_list` is skipped (only the boolean flag is broadcast). | | ||
|
|
||
| All tests use mocked `torch.distributed` and run on CPU without GPUs. | ||
|
|
||
| ### Regression checklist | ||
|
|
||
| Manual validation steps for multi-GPU environments: | ||
|
|
||
| - [ ] Standard pretraining (TP=8, CP=1) -- no deadlocks, loss matches baseline | ||
| - [ ] Pretraining with context parallelism (TP=4, CP=2) -- no deadlocks | ||
| - [ ] Pretraining with MTP enabled -- tokens/position_ids broadcast to last PP stage | ||
| - [ ] `broadcast_data_across_tp=False` (default) -- behaviour unchanged from main | ||
| - [ ] Single DataLoader worker per rank on VAST storage -- no I/O stalls | ||
| - [ ] Lustre-backed storage -- no regression in step time | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -12,9 +12,12 @@ | |
| # See the License for the specific language governing permissions and | ||
| # limitations under the License. | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| from dataclasses import fields | ||
| from typing import Any, Callable, Dict, Optional, Type, Union | ||
|
|
||
| from megatron.core import parallel_state | ||
| from megatron.core.datasets.blended_megatron_dataset_builder import BlendedMegatronDatasetBuilder | ||
| from megatron.core.datasets.blended_megatron_dataset_config import BlendedMegatronDatasetConfig | ||
| from megatron.core.datasets.gpt_dataset import GPTDataset, MockGPTDataset | ||
|
|
@@ -37,17 +40,27 @@ | |
| from megatron.bridge.utils.common_utils import print_rank_0 | ||
|
|
||
|
|
||
| def is_dataset_built_on_rank(pg_collection: ProcessGroupCollection) -> bool: | ||
| def is_dataset_built_on_rank(pg_collection: ProcessGroupCollection | None = None) -> bool: | ||
| """Determines whether the dataset should be built on the current rank. | ||
|
|
||
| Datasets are typically built only on the first and last pipeline stages | ||
| and the first tensor parallel rank to save memory and avoid redundancy. | ||
|
|
||
| Args: | ||
| pg_collection: Process group collection. When provided, uses the | ||
| explicit process groups. When ``None``, falls back to the global | ||
| parallel state, which allows this function to be passed directly | ||
| as a zero-argument callable to ``BlendedMegatronDatasetBuilder``. | ||
|
|
||
| Returns: | ||
| True if the dataset should be built on the current rank, False otherwise. | ||
| """ | ||
| return (is_pp_first_stage(pg_collection.pp) or is_pp_last_stage(pg_collection.pp)) and ( | ||
| pg_collection.tp.rank() == 0 | ||
| if pg_collection is not None: | ||
| return (is_pp_first_stage(pg_collection.pp) or is_pp_last_stage(pg_collection.pp)) and ( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Check if rank contains MTP layer, required to support placing MTP layers into standalone stages (Not the last PP stage)
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. added |
||
| pg_collection.tp.rank() == 0 | ||
| ) | ||
| return (parallel_state.is_pipeline_first_stage() or parallel_state.is_pipeline_last_stage()) and ( | ||
| parallel_state.get_tensor_model_parallel_rank() == 0 | ||
| ) | ||
|
|
||
|
|
||
|
|
@@ -76,9 +89,11 @@ def pretrain_train_valid_test_datasets_provider( | |
|
|
||
| print_rank_0("> building train, validation, and test datasets for GPT ...") | ||
|
|
||
| # Build the dataset on all ranks for TP-replicated loading | ||
| broadcast_data = getattr(dataset_config, "broadcast_data_across_tp", False) | ||
| is_built_on_rank = is_dataset_built_on_rank if broadcast_data else (lambda: True) | ||
|
|
||
| train_ds, valid_ds, test_ds = BlendedMegatronDatasetBuilder( | ||
| dataset_type, train_val_test_num_samples, lambda: True, dataset_config | ||
| dataset_type, train_val_test_num_samples, is_built_on_rank, dataset_config | ||
| ).build() | ||
|
|
||
| print_rank_0("> finished creating GPT datasets ...") | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -32,6 +32,7 @@ | |
| from megatron.bridge.training.losses import masked_next_token_loss | ||
| from megatron.bridge.training.post_training.distillation import loss_func_kd | ||
| from megatron.bridge.training.state import GlobalState | ||
| from megatron.bridge.training.utils.batch_utils import get_batch_on_this_tp_rank | ||
| from megatron.bridge.training.utils.packed_seq_utils import get_packed_seq_params | ||
| from megatron.bridge.training.utils.pg_utils import get_pg_collection | ||
|
|
||
|
|
@@ -169,13 +170,19 @@ def get_batch( | |
| if (not is_first) and (not is_last): | ||
| return None, None, None, None, None, None, None, None, None, None | ||
|
|
||
| batch = get_batch_from_iterator( | ||
| data_iterator, | ||
| use_mtp, | ||
| getattr(cfg.dataset, "skip_getting_attention_mask_from_dataset", True), | ||
| is_first_pp_stage=is_first, | ||
| is_last_pp_stage=is_last, | ||
| ) | ||
| broadcast_data = getattr(cfg.dataset, "broadcast_data_across_tp", False) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. since the dataloader config is shared, this setting is also exposed for vlm datasets. there needs to be handling here for
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. added |
||
| if broadcast_data: | ||
| # TP-rank-0 loads data and broadcasts to other TP ranks. | ||
| # Reduces I/O by a factor of TP on high-latency storage. | ||
| batch = get_batch_on_this_tp_rank(data_iterator, cfg, use_mtp, pg_collection=pg_collection) | ||
| else: | ||
| batch = get_batch_from_iterator( | ||
| data_iterator, | ||
| use_mtp, | ||
| getattr(cfg.dataset, "skip_getting_attention_mask_from_dataset", True), | ||
| is_first_pp_stage=is_first, | ||
| is_last_pp_stage=is_last, | ||
| ) | ||
|
|
||
| cp_size = pg_collection.cp.size() | ||
| has_packed = batch.get("cu_seqlens") is not None | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thank you for adding this documentation! I believe it'd be useful to have this be part of a general data loading page, which can later be expanded to cover different datasets supported in megatron bridge and how to plug in custom datasets, rather than a page specicially dedicated to TP broadcast vs replicated load, but I'll defer to @yaoyu-33 and @cuichenx for this