Skip to content

Commit b2eb110

Browse files
committed
Support TE attention graphs for scheduler-packed THD
Adapt the fixed-capacity THD interface from dev #4359 and #5724 to main packing and graph scheduling. Preserve real and physical offsets, exclude padding from auxiliary losses, and keep replay bounded by the fixed source batch. Signed-off-by: Deyu Fu <deyuf@nvidia.com>
1 parent ebab48f commit b2eb110

13 files changed

Lines changed: 550 additions & 12 deletions

File tree

‎docs/user-guide/features/cuda_graph.md‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,18 @@ wired into custom training loops. The trade-off is that it requires more manual
9999
Megatron-LM's stock training loop already wires these calls in `megatron/training/training.py`,
100100
but custom training scripts must do the same work themselves.
101101

102+
For scheduler-produced THD batches, use `--use-varlen-dataset`,
103+
`--sequence-packing-scheduler dp_balanced`, `--cuda-graph-modules attn`,
104+
`--pad-packed-seq-alignment max`, and an explicit `--thd-max-packed-sequences`. The metadata capacity includes one
105+
reserved dummy document for token padding. `--max-seqlen-per-dp-cp-rank` sets the
106+
fixed local token capacity; the source global batch size and micro-batch size 1
107+
must stay fixed. PP/VPP also requires `--cuda-graph-dynamic-microbatches`, which
108+
keeps graph inputs alive until their corresponding backward even when packing
109+
reduces the number of microbatches. Dynamic CP, EP communication overlap, delayed
110+
wgrad, and paged stash are outside this path. mHC wrappers and attention graph
111+
splitting require their separate graph integrations; these packing options do
112+
not enable mHC capture by themselves.
113+
102114
### Usage
103115

104116
```bash

‎megatron/core/datasets/data_schedule.py‎

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
get_batch_and_global_seqlens,
1717
reroute_samples_to_dcp_ranks,
1818
)
19-
from megatron.core.packed_seq_params import PackedSeqParams
19+
from megatron.core.packed_seq_params import PackedSeqParams, pad_packed_batch_to_capacity
2020
from megatron.core.pipeline_parallel.hybrid_cp_schedule import BalancedCPScheduler
2121
from megatron.core.process_groups_config import ProcessGroupCollection
2222

@@ -361,6 +361,7 @@ def __init__(
361361
cp_size: int,
362362
dp_size: int,
363363
microbatch_group_size_per_vp_stage: Optional[int],
364+
max_num_seqs: Optional[int] = None,
364365
):
365366
"""
366367
Args:
@@ -374,6 +375,7 @@ def __init__(
374375
self.cp_size = cp_size
375376
self.dp_size = dp_size
376377
self.microbatch_group_size_per_vp_stage = microbatch_group_size_per_vp_stage
378+
self.max_num_seqs = max_num_seqs
377379

378380
def get_required_sample_keys(self):
379381
"""Return the required key of each batch."""
@@ -444,7 +446,9 @@ def get_groups_and_subsamples(self, sample_id_seqlens):
444446
single_microbatch = []
445447

446448
for i in range(len(sample_id_seqlens)):
447-
if sum_seqlen + sample_id_seqlens[i][1] <= self.max_seq_len_all_ranks:
449+
if sum_seqlen + sample_id_seqlens[i][1] <= self.max_seq_len_all_ranks and (
450+
self.max_num_seqs is None or len(single_microbatch) < self.max_num_seqs
451+
):
448452
single_microbatch.append(i)
449453
sum_seqlen += sample_id_seqlens[i][1]
450454
else:
@@ -720,6 +724,14 @@ def wrap_data_iterator(
720724
if config.virtual_pipeline_model_parallel_size is None
721725
else config.microbatch_group_size_per_vp_stage
722726
),
727+
# Reserve a document when token padding can append a dummy tail. Enforce
728+
# the metadata bound before routing instead of falling back mid-pipeline.
729+
max_num_seqs=(
730+
config.thd_max_packed_sequences
731+
- int(getattr(config, "pad_packed_seq_alignment", None) is not None)
732+
if getattr(config, "thd_max_packed_sequences", None) is not None
733+
else None
734+
),
723735
)
724736

725737
(
@@ -745,6 +757,7 @@ def get_batch_on_this_rank_for_sequence_packing(
745757
mtp_on_this_rank: bool = False,
746758
vp_stage: Optional[int] = None,
747759
pg_collection: Optional[ProcessGroupCollection] = None,
760+
config=None,
748761
):
749762
"""
750763
Get a batch of data for sequence packing.
@@ -921,5 +934,29 @@ def get_batch_on_this_rank_for_sequence_packing(
921934
max_seqlen_kv=max_seqlen,
922935
)
923936

937+
if config is not None and (
938+
config.pad_packed_seq_alignment is not None
939+
or getattr(config, "thd_max_packed_sequences", None) is not None
940+
):
941+
alignment = config.pad_packed_seq_alignment
942+
local_capacity = padding_mask.shape[-1]
943+
if alignment == "max":
944+
local_capacity = config.max_seqlen_per_dp_cp_rank
945+
elif alignment is not None:
946+
local_capacity = ((local_capacity + alignment - 1) // alignment) * alignment
947+
tokens, labels, loss_mask, position_ids, packed_seq_params, padding_mask = (
948+
pad_packed_batch_to_capacity(
949+
tokens,
950+
labels,
951+
loss_mask,
952+
position_ids,
953+
packed_seq_params,
954+
padding_mask,
955+
local_capacity=local_capacity,
956+
cp_size=cp_group.size(),
957+
max_num_seqs=getattr(config, "thd_max_packed_sequences", None),
958+
)
959+
)
960+
924961
# "attention_mask" is not valid for sequence packing, so set it to None.
925962
return tokens, labels, loss_mask, None, position_ids, packed_seq_params, padding_mask

‎megatron/core/model_parallel_config.py‎

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import warnings
44
from dataclasses import dataclass, field
5-
from typing import Callable, ContextManager, Literal, Optional
5+
from typing import Callable, ContextManager, Literal, Optional, Union
66

77
import torch
88

@@ -11,6 +11,11 @@
1111
)
1212

1313

14+
def _parse_pad_packed_seq_alignment(value):
15+
"""Parse an integer packed-token alignment or the fixed-capacity value 'max'."""
16+
return value if value == "max" else int(value)
17+
18+
1419
def resolve_tensor_parallel_weight_shards(
1520
tensor_model_parallel_size: int,
1621
tensor_parallel_num_weight_shards: Optional[int],
@@ -159,6 +164,16 @@ class ModelParallelConfig:
159164
dp_balanced: DP-balanced scheduler for sequence packing.
160165
"""
161166

167+
pad_packed_seq_alignment: Optional[Union[int, Literal["max"]]] = field(
168+
default=None, metadata={"argparse_meta": {"type": _parse_pad_packed_seq_alignment}}
169+
)
170+
"""Pad scheduler THD tensors to a local token alignment, or to capacity with 'max'.
171+
172+
Padding occupies a separate physical document with no valid tokens. It is
173+
excluded from loss, indexer training and MoE routing. Apply the same setting
174+
in eager mode when comparing against CUDA graph execution.
175+
"""
176+
162177
expert_model_parallel_size: int = 1
163178
"""Distributes Moe Experts across sub data parallel dimension."""
164179

@@ -545,6 +560,22 @@ def __post_init__(self):
545560
See https://docs.python.org/3/library/dataclasses.html#post-init-processing for more
546561
details.
547562
"""
563+
if self.pad_packed_seq_alignment is not None:
564+
self.pad_packed_seq_alignment = _parse_pad_packed_seq_alignment(
565+
self.pad_packed_seq_alignment
566+
)
567+
assert self.sequence_packing_scheduler == "dp_balanced"
568+
assert self.max_seqlen_per_dp_cp_rank is not None
569+
if self.pad_packed_seq_alignment != "max":
570+
assert 0 < self.pad_packed_seq_alignment <= self.max_seqlen_per_dp_cp_rank
571+
if self.context_parallel_size > 1:
572+
alignment = (
573+
self.max_seqlen_per_dp_cp_rank
574+
if self.pad_packed_seq_alignment == "max"
575+
else self.pad_packed_seq_alignment
576+
)
577+
assert alignment % 2 == 0, "Packed padding must preserve zigzag CP alignment."
578+
548579
if self.sequence_parallel:
549580
if self.tensor_model_parallel_size <= 1:
550581
raise ValueError("Cannot use sequence parallelism without tensor parallelism")

‎megatron/core/packed_seq_params.py‎

Lines changed: 91 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# Copyright (c) 2024, NVIDIA CORPORATION. All rights reserved.
2-
from dataclasses import dataclass
2+
from dataclasses import dataclass, replace
33
from typing import Optional
44

55
import torch
@@ -73,6 +73,96 @@ def __post_init__(self):
7373
)
7474

7575

76+
def pad_packed_batch_to_capacity(
77+
tokens,
78+
labels,
79+
loss_mask,
80+
position_ids,
81+
packed_seq_params,
82+
padding_mask,
83+
*,
84+
local_capacity: int,
85+
cp_size: int,
86+
max_num_seqs: Optional[int] = None,
87+
):
88+
"""Pad already CP-sliced scheduler tensors without changing real document boundaries.
89+
90+
The existing mask supplies the local storage length on intermediate PP stages,
91+
which do not own token or label tensors. Append a separate dummy document in
92+
global THD coordinates; each CP rank appends its equal share of that document.
93+
"""
94+
local_rows = padding_mask.shape[-1]
95+
if local_rows > local_capacity:
96+
raise ValueError(f"Packed local rows {local_rows} exceed capacity {local_capacity}.")
97+
local_tail = local_capacity - local_rows
98+
if cp_size > 1 and local_tail % 2:
99+
raise ValueError("Packed padding tail must preserve the two-way zigzag CP partition.")
100+
101+
def pad_tensor(tensor, fill=0):
102+
if tensor is None:
103+
return None
104+
if tensor.shape[-1] != local_rows:
105+
raise ValueError("Packed token tensors and padding_mask must have the same length.")
106+
return (
107+
tensor
108+
if local_tail == 0
109+
else torch.nn.functional.pad(tensor, (0, local_tail), value=fill)
110+
)
111+
112+
fields = {}
113+
if local_tail:
114+
for name in (
115+
'cu_seqlens_q',
116+
'cu_seqlens_kv',
117+
'cu_seqlens_q_padded',
118+
'cu_seqlens_kv_padded',
119+
):
120+
offsets = getattr(packed_seq_params, name)
121+
if offsets is None:
122+
offsets = getattr(packed_seq_params, name.removesuffix('_padded'))
123+
# The dummy document occupies physical storage but contains no real
124+
# tokens. In particular, CSA's indexer auxiliary loss must ignore it.
125+
tail = local_tail * cp_size if name.endswith('_padded') else 0
126+
fields[name] = torch.cat((offsets, offsets[-1:] + tail))
127+
if max_num_seqs is not None:
128+
for name in (
129+
'cu_seqlens_q',
130+
'cu_seqlens_kv',
131+
'cu_seqlens_q_padded',
132+
'cu_seqlens_kv_padded',
133+
):
134+
offsets = fields.get(name, getattr(packed_seq_params, name))
135+
if offsets is None:
136+
offsets = fields.get(
137+
name.removesuffix('_padded'),
138+
getattr(packed_seq_params, name.removesuffix('_padded')),
139+
)
140+
remaining = max_num_seqs + 1 - offsets.numel()
141+
if remaining < 0:
142+
raise ValueError("Packed documents exceed thd_max_packed_sequences.")
143+
if remaining:
144+
fields[name] = torch.cat((offsets, offsets[-1:].expand(remaining)))
145+
static_capacity = local_capacity * cp_size if max_num_seqs is not None else 0
146+
if fields or static_capacity:
147+
packed_seq_params = replace(
148+
packed_seq_params,
149+
**fields,
150+
max_seqlen_q=max(packed_seq_params.max_seqlen_q, local_tail * cp_size, static_capacity),
151+
max_seqlen_kv=max(
152+
packed_seq_params.max_seqlen_kv, local_tail * cp_size, static_capacity
153+
),
154+
pad_between_seqs=True,
155+
)
156+
return (
157+
pad_tensor(tokens),
158+
pad_tensor(labels),
159+
pad_tensor(loss_mask),
160+
pad_tensor(position_ids),
161+
packed_seq_params,
162+
pad_tensor(padding_mask, True),
163+
)
164+
165+
76166
def resolve_cp_group(
77167
static_cp_group: Optional[dist.ProcessGroup], packed_seq_params: PackedSeqParams = None
78168
) -> Optional[dist.ProcessGroup]:

‎megatron/core/transformer/cuda_graphs.py‎

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2249,9 +2249,16 @@ def __init__(
22492249
"Setting NCCL_GRAPH_REGISTER=0 to avoid illegal memory access when using "
22502250
"CUDA Graph with PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True."
22512251
)
2252+
if getattr(config, "sequence_packing_scheduler", None) is not None:
2253+
assert micro_batch_size == 1, "Scheduler THD graphs require micro_batch_size=1."
22522254
self.model = model
22532255
self.config = config
2254-
self.seq_length = seq_length
2256+
self.seq_length = (
2257+
config.max_seqlen_per_dp_cp_rank * config.context_parallel_size
2258+
if getattr(config, "sequence_packing_scheduler", None) is not None
2259+
and config.pad_packed_seq_alignment is not None
2260+
else seq_length
2261+
)
22552262
self.micro_batch_size = micro_batch_size
22562263
self.optimizers = optimizers
22572264
self.pg_collection = pg_collection
@@ -2454,7 +2461,9 @@ def get_rotary_pos_emb(transformer_module, transformer_input):
24542461
)
24552462
if rotary_seq_len not in rotary_pos_emb_cache:
24562463
rotary_pos_emb_cache[rotary_seq_len] = transformer_module.rotary_pos_emb(
2457-
rotary_seq_len
2464+
rotary_seq_len,
2465+
packed_seq=getattr(self.config, "sequence_packing_scheduler", None)
2466+
is not None,
24582467
)
24592468
return rotary_pos_emb_cache[rotary_seq_len]
24602469
else:
@@ -2652,6 +2661,10 @@ def _get_cuda_graph_input_data(self):
26522661
), "If PP is not enabled, there should be only one model chunk."
26532662
self.num_microbatches = 1
26542663
else:
2664+
# Keep the source-batch bound when dp_balanced produces fewer packs.
2665+
# With a fixed source batch and MBS=1, each aligned packed schedule is
2666+
# a subsequence of this capture order, preserving the existing input
2667+
# buffer lifetimes without wrapping around the captured graph list.
26552668
self.num_microbatches = get_num_microbatches()
26562669

26572670
_, _, num_warmup_microbatches, _ = get_pp_rank_microbatches(

‎megatron/core/transformer/module.py‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -349,7 +349,17 @@ def _te_cuda_graph_replay(self, *args, **kwargs):
349349
v, torch.Tensor
350350
), "CUDA graph accepts only Tensor inputs."
351351

352-
cg_index = getattr(self, 'current_microbatch', 0) % len(self.cuda_graphs)
352+
microbatch_idx = getattr(self, 'current_microbatch', 0)
353+
if (
354+
getattr(self.config, "cuda_graph_dynamic_microbatches", False)
355+
and self.config.pipeline_model_parallel_size > 1
356+
):
357+
if microbatch_idx >= len(self.cuda_graphs):
358+
raise RuntimeError(
359+
"Packed microbatch count exceeds the captured source-batch bound; "
360+
"recapture CUDA graphs before increasing the source batch size."
361+
)
362+
cg_index = microbatch_idx % len(self.cuda_graphs)
353363
cudagraph_args, cudagraph_kwargs = self._get_te_cuda_graph_replay_args(*args, **kwargs)
354364

355365
for hook, hook_args in self.cuda_graph_manual_hooks:

‎megatron/core/transformer/transformer_config.py‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1252,6 +1252,20 @@ class TransformerConfig(ModelParallelConfig):
12521252
cuda_graph_modules has no effect when cuda_graph_impl="none" and must be empty when
12531253
cuda_graph_impl="full_iteration"."""
12541254

1255+
thd_max_packed_sequences: Optional[int] = None
1256+
"""THD metadata capacity, including the dummy padding document.
1257+
1258+
Bounds real-document packing and fixes all four cumulative-offset tensor
1259+
shapes in eager and TE graph execution. Padding reserves one document slot.
1260+
"""
1261+
1262+
cuda_graph_dynamic_microbatches: bool = False
1263+
"""Allow TE graph replay when packing reduces the source microbatch count.
1264+
1265+
A fixed source global batch size bounds graph instances. Input slots stay
1266+
live until backward, independently of the number of packed microbatches.
1267+
"""
1268+
12551269
cuda_graph_modules: Union[str, CudaGraphModule, List[str], List[CudaGraphModule]] = "full"
12561270
"""Selects training capture coverage within per-layer CUDA graphs (local and
12571271
transformer_engine implementations).
@@ -3562,6 +3576,27 @@ def _scope_to_str(s):
35623576
"full_iteration",
35633577
], f"Invalid cuda graph implementation: {self.cuda_graph_impl}"
35643578

3579+
if self.thd_max_packed_sequences is not None:
3580+
assert self.thd_max_packed_sequences >= (
3581+
2 if self.pad_packed_seq_alignment is not None else 1
3582+
), "THD metadata capacity must leave room for the dummy padding document."
3583+
if self.cuda_graph_dynamic_microbatches:
3584+
assert self.cuda_graph_impl == "transformer_engine"
3585+
assert self.sequence_packing_scheduler == "dp_balanced"
3586+
assert not self.overlap_moe_expert_parallel_comm and not self.delay_wgrad_compute
3587+
assert not self.moe_paged_stash
3588+
if (
3589+
self.sequence_packing_scheduler is not None
3590+
and self.cuda_graph_impl == "transformer_engine"
3591+
):
3592+
assert self.pad_packed_seq_alignment in ("max", self.max_seqlen_per_dp_cp_rank)
3593+
assert self.thd_max_packed_sequences is not None
3594+
assert self.pipeline_model_parallel_size == 1 or self.cuda_graph_dynamic_microbatches
3595+
assert self.cuda_graph_modules == [
3596+
CudaGraphModule.attn
3597+
], "Scheduler-produced THD CUDA graphs currently support attention capture only."
3598+
assert is_te_min_version("2.18.0"), "Packed TE graphs require TE >= 2.18.0."
3599+
35653600
self.inference_cuda_graph_scope = normalize_inference_cuda_graph_scope(
35663601
self.inference_cuda_graph_scope, self.cuda_graph_impl
35673602
)

0 commit comments

Comments
 (0)