Skip to content

Commit e4f8f96

Browse files
authored
fix(ckpt): support GTP checkpoint groups and HF conversion (#6116)
Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com> Signed-off-by: Yu Yao <yaoyu.094@gmail.com>
1 parent 3ccbdb8 commit e4f8f96

39 files changed

Lines changed: 2650 additions & 136 deletions

‎docs/training/checkpointing.md‎

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,59 @@ checkpoint = CheckpointConfig(
191191

192192
**Important**: When using Megatron FSDP (`use_megatron_fsdp=True`), you must set `ckpt_format="fsdp_dtensor"`. Other formats are not compatible with FSDP's sharded parameter layout. See {doc}`megatron-fsdp` for complete FSDP configuration details.
193193

194+
### Generalized Tensor Parallelism (GTP)
195+
196+
Use `torch_dist` checkpoints for BF16 GTP models with TransformerEngine 2.19 or newer.
197+
Checkpoint metadata and fully parallel save/load include the GTP rematerialization
198+
axis. RNG checkpoint entries preserve distinct streams on dense and expert GTP ranks.
199+
200+
HF import splits each mapped TP-local tensor into its stored GTP row shard. HF export
201+
gathers those rows and removes alignment padding before applying the existing TP/EP/PP
202+
mappings. Use `AutoBridge.export_hf_weights()` or `save_hf_weights()` for this export;
203+
the local HF parameter-view API cannot describe the extra GTP shard axis. Quantized
204+
HF export and LoRA adapter export/merge also gather before applying their mappings.
205+
Raw FP8 export tasks and local native MXFP8 parameter export reject GTP before
206+
yielding weights, because their storage views cannot preserve this layout.
207+
208+
When loading a native GTP checkpoint with `load_megatron_model()`, pass `mp_overrides`
209+
with the saved TP size and weight shard count for each axis that uses GTP:
210+
`tensor_model_parallel_size` / `tensor_parallel_num_weight_shards` for dense weights,
211+
and `expert_tensor_parallel_size` / `expert_tensor_parallel_num_weight_shards` for
212+
expert weights. Axes without GTP can still change TP independently. Changing a GTP
213+
weight-sharding topology is
214+
rejected because the current Megatron-Core SwiGLU checkpoint layout can reorder
215+
gate/up rows. Training resume and finetuning enforce the same restriction, including
216+
loading a non-GTP native checkpoint into a GTP model. To change layouts, export HF
217+
weights from the original topology first, then import the HF weights into the new
218+
topology.
219+
220+
Energon shards samples across DP and dense GTP ranks while CP peers consume the
221+
same samples. Dataloader checkpoints use the same group for file ownership and
222+
restore; old duplicated GTP states with a different stream count are rejected.
223+
With GTP, do not enable both `dist_ckpt_optim_fully_reshardable` and
224+
`distrib_optim_fully_reshardable_mem_efficient`: Core does not create the Gloo
225+
groups needed by that combination.
226+
227+
The focused two-GPU regression covers exact BF16 HF round-trips, model and named
228+
RNG-stream resume, and rejection of unsafe topology changes, with fully parallel
229+
checkpoint I/O enabled and disabled:
230+
231+
```bash
232+
uv run python -m torch.distributed.run --standalone --nproc_per_node=2 -m pytest \
233+
tests/functional_tests/test_groups/converter/test_gtp_checkpoint_conversion.py \
234+
tests/functional_tests/test_groups/converter/test_gtp_native_fp8_conversion.py \
235+
tests/functional_tests/test_groups/data/energon/test_gtp_checkpoint_state.py -q
236+
```
237+
238+
The H100 L1 launcher `L1_Launch_gtp_checkpoint_conversion.sh` runs this matrix in CI.
239+
GTP cases require TransformerEngine 2.19 or newer and skip on older versions; the
240+
ordinary TP baseline cases still run.
241+
242+
The additional tests check native FP8 HF import against an independent TE cast and
243+
exact Energon sample continuation after restore. Native MXFP8 requires Blackwell
244+
and skips on H100; this does not validate native MXFP8 training or Muon optimizer
245+
state. Quantized and LoRA GTP mapping regressions use controlled unit fixtures.
246+
194247
## Performance Optimizations
195248

196249
| Parameter | Type | Default | Description |

‎src/megatron/bridge/data/energon/base_energon_datamodule.py‎

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
from megatron.energon.flavors.webdataset.default_generic_webdataset import DefaultGenericWebdatasetFactory
2424
from megatron.energon.typed_converter import JsonParser
2525

26+
from megatron.bridge.training.gtp import _get_dataloader_process_group
27+
2628

2729
logger = logging.getLogger(__name__)
2830

@@ -254,16 +256,11 @@ def __init__(
254256
self.kwargs = kwargs
255257

256258
def _build_worker_config(self, num_workers: int, split: str = "train") -> WorkerConfig:
257-
"""Build a WorkerConfig using pg_collection, falling back to default_worker_config.
258-
259-
NOTE: We intentionally use the pure DP rank (pg_collection.dp)
260-
rather than the combined DP-CP rank. With Megatron's rank ordering
261-
(default "tp-cp-ep-dp-pp"), all CP ranks within the same DP replica
262-
already share the same pure DP rank. This ensures that CP ranks
263-
processing different sequence portions of the same batch receive
264-
identical data from the dataloader.
265-
Using dp_cp would be INCORRECT here — it would assign each CP rank
266-
a unique rank, causing them to read different data shards.
259+
"""Build workers for distinct microbatches across data and GTP ranks.
260+
261+
Context-parallel peers consume the same sample before splitting its
262+
sequence, so the worker group excludes CP. GTP peers consume different
263+
microbatches and must have distinct worker ranks, including on resume.
267264
"""
268265
if self.pg_collection is None or self.pg_collection.dp is None:
269266
logger.info(
@@ -272,9 +269,9 @@ def _build_worker_config(self, num_workers: int, split: str = "train") -> Worker
272269
)
273270
return WorkerConfig.default_worker_config(num_workers)
274271

275-
rank = self.pg_collection.dp.rank()
276-
world_size = self.pg_collection.dp.size()
277-
data_parallel_group = self.pg_collection.dp
272+
data_parallel_group = _get_dataloader_process_group(self.pg_collection)
273+
rank = data_parallel_group.rank()
274+
world_size = data_parallel_group.size()
278275
cp_rank = self.pg_collection.cp.rank() if self.pg_collection.cp is not None else 0
279276
cp_size = self.pg_collection.cp.size() if self.pg_collection.cp is not None else 1
280277

‎src/megatron/bridge/models/conversion/auto_bridge.py‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2038,11 +2038,15 @@ def get_model(
20382038
]
20392039

20402040
model_config.finalize()
2041+
from megatron.bridge.training.gtp import classify_gtp_remat_chains, configure_gtp_remat
2042+
2043+
configure_gtp_remat(model_config)
20412044
builder = model_config.get_builder_cls()(model_config)
20422045
if pg_collection is None:
20432046
pg_collection = self._get_or_initialize_pg_collection(transformer_config)
20442047
kwargs.setdefault("data_parallel_random_init", False)
20452048
models = builder.build_distributed_models(pg_collection=pg_collection, **kwargs)
2049+
classify_gtp_remat_chains(models, model_config)
20462050
for model in models:
20472051
model.model_config = model_config
20482052
succeeded = True
@@ -2083,6 +2087,8 @@ def _get_or_initialize_pg_collection(
20832087
context_parallel_size=transformer_config.context_parallel_size or 1,
20842088
expert_model_parallel_size=transformer_config.expert_model_parallel_size or 1,
20852089
expert_tensor_parallel_size=transformer_config.expert_tensor_parallel_size,
2090+
gtp_remat_size=transformer_config.gtp_weight_remat_size,
2091+
expert_gtp_remat_size=transformer_config.expert_gtp_weight_remat_size,
20862092
)
20872093
if torch.cuda.is_available():
20882094
from megatron.core.tensor_parallel import model_parallel_cuda_manual_seed
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
# Copyright (c) 2026, NVIDIA CORPORATION. All rights reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Translate between stored GTP shards and the TP-local tensors used by mappings."""
16+
17+
from collections.abc import Iterator
18+
from contextlib import contextmanager
19+
20+
import torch
21+
22+
23+
def _is_gtp_param(param: torch.Tensor | None) -> bool:
24+
return getattr(param, "is_gtp_weight_remat", False) is True
25+
26+
27+
def _get_mapping_shape(param: torch.Tensor) -> torch.Size:
28+
"""Return the logical TP-local shape, excluding GTP padding and sharding."""
29+
if _is_gtp_param(param):
30+
shape = list(param.shape)
31+
shape[0] = shape[0] * param.group.size() - param.pad_length
32+
return torch.Size(shape)
33+
return param.shape
34+
35+
36+
@contextmanager
37+
def _gtp_weight_load_context(param: torch.Tensor, module: torch.nn.Module) -> Iterator[None]:
38+
"""Let TE update native FP8 storage while preserving the GTP parameter subclass."""
39+
if _is_gtp_param(param) and getattr(param, "_gtp_native_fp8", False):
40+
from megatron.core.tensor_parallel.gtp_api import gtp_native_fp8_load_context
41+
42+
with gtp_native_fp8_load_context(module):
43+
yield
44+
else:
45+
yield
46+
47+
48+
def _slice_gtp_weight(weight: torch.Tensor, param: torch.Tensor) -> torch.Tensor:
49+
"""Take the stored GTP row shard after the mapping has applied TP splitting."""
50+
if not _is_gtp_param(param):
51+
return weight
52+
expected_shape = _get_mapping_shape(param)
53+
if weight.shape != expected_shape:
54+
raise ValueError(f"Expected TP-local GTP weight shape {expected_shape}, got {weight.shape}")
55+
if param.pad_length:
56+
weight = torch.cat((weight, weight.new_zeros(param.pad_length, *weight.shape[1:])), dim=0)
57+
return weight.narrow(0, param.group.rank() * param.shape[0], param.shape[0]).contiguous()
58+
59+
60+
def _gather_gtp_weight(param: torch.Tensor | None) -> torch.Tensor | None:
61+
"""Gather only the GTP axis, leaving TP/EP/PP handling to the mapping."""
62+
if not _is_gtp_param(param):
63+
return param
64+
if getattr(param, "_gtp_native_fp8", False):
65+
from megatron.core.tensor_parallel.gtp_api import dequantize_gtp_native_fp8
66+
67+
local_weight = dequantize_gtp_native_fp8(param)
68+
else:
69+
# GTP's detach preserves the subclass but drops its runtime attributes.
70+
# Collectives should operate on a plain tensor without triggering prefetch.
71+
local_weight = param.as_subclass(torch.Tensor).detach()
72+
local_weight = local_weight.contiguous()
73+
padded_shape = (param.shape[0] * param.group.size(), *param.shape[1:])
74+
weight = torch.empty(padded_shape, dtype=local_weight.dtype, device=local_weight.device)
75+
torch.distributed.all_gather_into_tensor(weight, local_weight, group=param.group)
76+
return weight[: _get_mapping_shape(param)[0]]

‎src/megatron/bridge/models/conversion/model_bridge.py‎

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
Generic,
3232
Iterable,
3333
List,
34+
Literal,
3435
Mapping,
3536
NamedTuple,
3637
Optional,
@@ -59,6 +60,13 @@
5960
detect_fp8_export_layout,
6061
get_fp8_export_tensors,
6162
)
63+
from megatron.bridge.models.conversion.gtp import (
64+
_gather_gtp_weight,
65+
_get_mapping_shape,
66+
_gtp_weight_load_context,
67+
_is_gtp_param,
68+
_slice_gtp_weight,
69+
)
6270
from megatron.bridge.models.conversion.mapping_registry import MegatronMappingRegistry
6371
from megatron.bridge.models.conversion.param_mapping import (
6472
LocalHFParam,
@@ -1529,7 +1537,8 @@ def load_weights_hf_to_megatron(
15291537
continue
15301538

15311539
# Check shape compatibility before copying
1532-
if converted_weights.shape != task.param_weight.shape:
1540+
expected_shape = _get_mapping_shape(task.param_weight)
1541+
if converted_weights.shape != expected_shape:
15331542
# Check whitelist
15341543
is_whitelisted = False
15351544
if allowed_mismatched_params:
@@ -1548,11 +1557,12 @@ def load_weights_hf_to_megatron(
15481557

15491558
raise ValueError(
15501559
f"Shape mismatch for megatron param {task.mapping.megatron_param}:\n"
1551-
f" Expected shape: {task.param_weight.shape}\n"
1560+
f" Expected shape: {expected_shape}\n"
15521561
f" Got shape: {converted_weights.shape}\n"
15531562
f" Bridge type: {type(task.mapping).__name__}\n"
15541563
f" HF mapping: {task.mapping.hf_param}"
15551564
)
1565+
converted_weights = _slice_gtp_weight(converted_weights, task.param_weight)
15561566
if capture_unquantized_state_dict:
15571567
vp_stage = task.vp_stage if task.vp_stage is not None else 0
15581568
chunk_key = f"model{vp_stage}"
@@ -1562,7 +1572,7 @@ def load_weights_hf_to_megatron(
15621572
# Float8BlockwiseQTensor) that is a leaf with requires_grad=True.
15631573
# In-place updates under grad mode will raise:
15641574
# "a leaf Variable that requires grad is being used in an in-place operation."
1565-
with torch.no_grad():
1575+
with torch.no_grad(), _gtp_weight_load_context(task.param_weight, task.megatron_module):
15661576
task.param_weight.copy_(converted_weights)
15671577
self.finalize_hf_import(megatron_model)
15681578
if use_megatron_fsdp:
@@ -1681,6 +1691,7 @@ def stream_weights_hf_to_megatron(
16811691
hf_weights = self.maybe_modify_loaded_hf_weight(task.mapping.hf_param, hf_state_dict)
16821692
converted_weights = self._convert_loaded_hf_weight(task, hf_weights)
16831693
if converted_weights is not None:
1694+
converted_weights = _slice_gtp_weight(converted_weights, task.param_weight)
16841695
# Assert that vp_stage is not None for HF->Megatron tasks
16851696
yield MegatronWeightTuple(task.param_name, converted_weights, task.vp_stage)
16861697

@@ -1800,7 +1811,7 @@ def stream_weights_megatron_to_hf(
18001811

18011812
megatron_weights = uneven_dtensor_to_full_tensor(task.param_weight)
18021813
else:
1803-
megatron_weights = task.param_weight
1814+
megatron_weights = _gather_gtp_weight(task.param_weight)
18041815
megatron_module = task.megatron_module
18051816
if self._should_skip_mtp_duplicate_embedding_export(task, megatron_model):
18061817
megatron_weights = None
@@ -2345,7 +2356,7 @@ def _detect_fp8_params(
23452356
Returns:
23462357
Dictionary mapping global parameter names to FP8 export layouts.
23472358
"""
2348-
local_fp8_layouts: Dict[str, FP8ExportLayout] = {}
2359+
local_fp8_layouts: Dict[str, FP8ExportLayout | Literal[-1]] = {}
23492360
global_name_set = set(sorted_global_param_names_all_pp_ranks)
23502361

23512362
for vp_stage, model in enumerate(megatron_model):
@@ -2368,17 +2379,24 @@ def _detect_fp8_params(
23682379
fp8_scale_inv_attr=fp8_scale_inv_attr,
23692380
)
23702381
if layout is not None:
2371-
local_fp8_layouts[global_name] = layout
2382+
# Reject on every PP rank before creating raw storage views,
2383+
# which do not retain the parameter's GTP sharding metadata.
2384+
local_fp8_layouts[global_name] = -1 if _is_gtp_param(local_weights) else layout
23722385

23732386
# Gather across PP ranks to ensure consistent insertion decisions
2374-
fp8_layouts_list: list[Dict[str, FP8ExportLayout]] = [None] * get_pg_size(pp_group)
2387+
fp8_layouts_list: list[Dict[str, FP8ExportLayout | Literal[-1]] | None] = [None] * get_pg_size(pp_group)
23752388
torch.distributed.all_gather_object(fp8_layouts_list, local_fp8_layouts, group=pp_group)
23762389
global_fp8_layouts: Dict[str, FP8ExportLayout] = {}
23772390
for layouts in fp8_layouts_list:
23782391
if not layouts:
23792392
continue
23802393
# Validate only after every PP rank has completed the collective.
23812394
for global_name, layout in layouts.items():
2395+
if layout == -1:
2396+
raise ValueError(
2397+
f"{global_name}: raw FP8 export does not support GTP sharding; "
2398+
"use gathered dequantized export or gathered quantized export instead"
2399+
)
23822400
try:
23832401
layout.validate()
23842402
except ValueError as error:

‎src/megatron/bridge/models/conversion/param_mapping.py‎

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
)
3333
from torch.distributed._tensor import DTensor
3434

35+
from megatron.bridge.models.conversion.gtp import _get_mapping_shape, _is_gtp_param
3536
from megatron.bridge.models.conversion.utils import (
3637
get_module_and_param_from_name,
3738
is_modelopt_dynamic_module,
@@ -360,6 +361,11 @@ def local_hf_params(
360361
)
361362
if isinstance(weight, DTensor) or _module_uses_fsdp(megatron_module):
362363
raise ValueError(f"{global_param_name}: local HF parameter iteration does not support DTensor/FSDP")
364+
if _is_gtp_param(weight):
365+
raise ValueError(
366+
f"{global_param_name}: local HF parameter views cannot represent GTP sharding; "
367+
"use the gathered HF weight export instead"
368+
)
363369

364370
specs = self.local_hf_param_specs(global_param_name)
365371
if not specs:
@@ -1297,7 +1303,7 @@ def hf_to_megatron(
12971303
actual_dim0_size = hf_weights.shape[0]
12981304
# DTensor.shape is already the global shape across TP ranks, while
12991305
# a regular Megatron parameter stores only its local TP shard.
1300-
expect_dim0_size = target_param.shape[0]
1306+
expect_dim0_size = _get_mapping_shape(target_param)[0]
13011307
if not isinstance(target_param, DTensor):
13021308
expect_dim0_size *= self.tp_size
13031309
if actual_dim0_size != expect_dim0_size:
@@ -1321,7 +1327,7 @@ def hf_to_megatron(
13211327
if isinstance(target_param, DTensor):
13221328
output_shape = target_param.orig_param.shape
13231329
else:
1324-
output_shape = target_param.shape
1330+
output_shape = _get_mapping_shape(target_param)
13251331
# Scatter to all ranks. Each rank gets its sharded shape from its module.
13261332
return self.scatter_to_tp_ranks(
13271333
splits,
@@ -1485,13 +1491,14 @@ def hf_to_megatron(
14851491

14861492
# HF fused expert weights (e.g. down_proj) may be stored in [in, out]
14871493
# layout while Megatron expects [out, in]. Detect via the unsharded dim:
1488-
# for RowParallel, dim 0 is never split, so hf_weights.shape[0] must
1489-
# equal target_param.shape[0].
1494+
# RowParallel TP does not split dim 0. GTP may split it further in storage,
1495+
# so compare against the logical TP-local shape.
1496+
target_shape = _get_mapping_shape(target_param)
14901497
if (
14911498
hf_weights is not None
14921499
and hf_weights.ndim == 2
1493-
and hf_weights.shape[0] != target_param.shape[0]
1494-
and hf_weights.shape[1] == target_param.shape[0]
1500+
and hf_weights.shape[0] != target_shape[0]
1501+
and hf_weights.shape[1] == target_shape[0]
14951502
):
14961503
hf_weights = hf_weights.t().contiguous()
14971504

@@ -1522,7 +1529,7 @@ def hf_to_megatron(
15221529
if isinstance(target_param, DTensor):
15231530
output_shape = target_param.orig_param.shape
15241531
else:
1525-
output_shape = target_param.shape
1532+
output_shape = _get_mapping_shape(target_param)
15261533
# Scatter to all ranks. Each rank gets its sharded shape from its module.
15271534
return self.scatter_to_tp_ranks(
15281535
splits,
@@ -3176,7 +3183,7 @@ def hf_to_megatron(
31763183
if isinstance(target_param, DTensor):
31773184
output_shape = target_param.orig_param.shape
31783185
else:
3179-
output_shape = target_param.shape
3186+
output_shape = _get_mapping_shape(target_param)
31803187
# Scatter the concatenated shards to each rank
31813188
return self.scatter_to_tp_ranks(
31823189
splits,
@@ -3581,7 +3588,7 @@ def hf_to_megatron(self, hf_weights: torch.Tensor, megatron_module: nn.Module) -
35813588

35823589
normalized_param = self._normalize_expert_param_name(self.megatron_param)
35833590
_, target_param = get_module_and_param_from_name(megatron_module, normalized_param)
3584-
target_shape = target_param.shape
3591+
target_shape = _get_mapping_shape(target_param)
35853592

35863593
if target_shape[0] % 2 != 0:
35873594
raise ValueError(f"Expected even fused dim for {self.megatron_param}, got {target_shape}.")

0 commit comments

Comments
 (0)