Skip to content

Commit 14bc4ef

Browse files
committed
feat(conversion): expose local refit views
Expose per-parameter canonical HF-compatible views for M-to-N refit, preserve the normal Bridge fallback for transformed mappings, and document BF16/MXFP8 transport boundaries. Signed-off-by: wdykas <wdykas@nvidia.com>
1 parent d352ace commit 14bc4ef

7 files changed

Lines changed: 478 additions & 12 deletions

File tree

docs/bridge-rl-integration.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,14 @@ Stream tensors from the training side to your inference runtime without writing
364364
- **Device routing:** Handles are returned under a `device_uuid` key (NVML UUID of the CUDA device). The inference side should map handles on the same device (or coordinate via your communicator). For collective updates, the worker can also broadcast tensors directly (`broadcast_weights_for_collective`).
365365
- **Parallelism nuances:** With TP/EP, exported HF tensors are reassembled from shards; with CP/sequence packing, shapes/dtypes are already consistent at export time. FP8 or mixed precision can affect size estimates; the worker accounts for dtype scaling when estimating bytes.
366366

367+
**Local-view API and model/transport boundary:**
368+
369+
`WeightConversionTask.local_hf_param_specs()` is a per-parameter optimization hint. A non-empty tuple describes canonical HF-compatible views of that one local logical Megatron tensor that may be transferred without first running Bridge collectives or layout conversion. It does not certify whole-model HF conversion or M-to-N refit support. An empty tuple means that parameter must use the normal Bridge conversion/packed-broadcast path, not that the model is unsupported.
370+
371+
Mappings that require transpose, permutation, interleaving, or grouped-export transforms return no specs unless they provide an explicit safe override. A transport must validate support across all parameters and separately qualify the destination backend and source/destination topology before selecting an M-to-N path.
372+
373+
This contract is not BF16-only. For MXFP8 refit, a transport may materialize canonical logical views from quantized training storage and requantize persistent MXFP8 inference destinations in place. Direct transfer of packed MXFP8 data and scales is a different optimization and is valid only when the source and destination storage layouts, quantization backends, and topology are explicitly compatible.
374+
367375
```python
368376
import os
369377
import torch

docs/fern/versions/nightly/pages/bridge-rl-integration.mdx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,14 @@ Stream tensors from the training side to your inference runtime without writing
356356
- **Device routing:** Handles are returned under a `device_uuid` key (NVML UUID of the CUDA device). The inference side should map handles on the same device (or coordinate via your communicator). For collective updates, the worker can also broadcast tensors directly (`broadcast_weights_for_collective`).
357357
- **Parallelism nuances:** With TP/EP, exported HF tensors are reassembled from shards; with CP/sequence packing, shapes/dtypes are already consistent at export time. FP8 or mixed precision can affect size estimates; the worker accounts for dtype scaling when estimating bytes.
358358

359+
**Local-view API and model/transport boundary:**
360+
361+
`WeightConversionTask.local_hf_param_specs()` is a per-parameter optimization hint. A non-empty tuple describes canonical HF-compatible views of that one local logical Megatron tensor that may be transferred without first running Bridge collectives or layout conversion. It does not certify whole-model HF conversion or M-to-N refit support. An empty tuple means that parameter must use the normal Bridge conversion/packed-broadcast path, not that the model is unsupported.
362+
363+
Mappings that require transpose, permutation, interleaving, or grouped-export transforms return no specs unless they provide an explicit safe override. A transport must validate support across all parameters and separately qualify the destination backend and source/destination topology before selecting an M-to-N path.
364+
365+
This contract is not BF16-only. For MXFP8 refit, a transport may materialize canonical logical views from quantized training storage and requantize persistent MXFP8 inference destinations in place. Direct transfer of packed MXFP8 data and scales is a different optimization and is valid only when the source and destination storage layouts, quantization backends, and topology are explicitly compatible.
366+
359367
```python
360368
import os
361369
import torch

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

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
import dataclasses
1818
import logging
1919
import os
20-
from collections.abc import Callable
20+
from collections.abc import Callable, Mapping
2121
from contextlib import nullcontext
2222
from functools import cached_property, partial
2323
from pathlib import Path
@@ -45,6 +45,7 @@
4545
from megatron.bridge.models.conversion.model_bridge import (
4646
HFWeightTuple,
4747
MegatronModelBridge,
48+
MegatronWeightTuple,
4849
WeightConversionTask,
4950
)
5051
from megatron.bridge.models.conversion.utils import get_causal_lm_class_name_via_auto_map
@@ -701,6 +702,12 @@ def load_hf_weights(
701702
self.unquantized_state_dict = getattr(bridge, "unquantized_state_dict", None)
702703
return model
703704

705+
def get_export_fp8_tasks(self, model: MegatronModelT | list[MegatronModelT]) -> list[WeightConversionTask | None]:
706+
"""Build physical FP8 data and scale export tasks."""
707+
if not isinstance(model, list):
708+
model = [model]
709+
return self._model_bridge.build_export_fp8_tasks(self.hf_pretrained, model)
710+
704711
def export_hf_weights(
705712
self,
706713
model: list[MegatronModelT],
@@ -2091,6 +2098,25 @@ def get_conversion_tasks(
20912098

20922099
return self._model_bridge.build_conversion_tasks(pre_trained, megatron_model)
20932100

2101+
def stream_weights_hf_to_megatron(
2102+
self,
2103+
megatron_model: MegatronModelT | list[MegatronModelT],
2104+
conversion_tasks: list[WeightConversionTask] | None = None,
2105+
*,
2106+
hf_state_dict: Mapping[str, torch.Tensor] | None = None,
2107+
) -> Iterable[MegatronWeightTuple]:
2108+
"""Stream HF-to-Megatron conversions from the configured or external state."""
2109+
return self._model_bridge.stream_weights_hf_to_megatron(
2110+
self._provider_bridge_input,
2111+
megatron_model,
2112+
conversion_tasks,
2113+
hf_state_dict=hf_state_dict,
2114+
)
2115+
2116+
def finalize_hf_import(self, megatron_model: MegatronModelT | list[MegatronModelT]) -> None:
2117+
"""Finalize tied parameters and parameter-derived caches after import."""
2118+
self._model_bridge.finalize_hf_import(megatron_model)
2119+
20942120
@property
20952121
def transformer_config(self) -> TransformerConfig:
20962122
_model_provider = self.to_megatron_provider(load_weights=False)

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

Lines changed: 123 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353

5454
from megatron.bridge.models.conversion.mapping_registry import MegatronMappingRegistry
5555
from megatron.bridge.models.conversion.param_mapping import (
56+
LocalHFParamSpec,
5657
MegatronParamMapping,
5758
)
5859
from megatron.bridge.models.conversion.peft_bridge import (
@@ -167,6 +168,8 @@ class WeightConversionTask(Generic[MappingT]):
167168
dtype; bridges that requantize on export skip it (no scale companions).
168169
export_hook: Export-only transformation applied after mapping conversion and
169170
before final device placement.
171+
required_hf_param_names: Import-only source tensors consumed by the loading
172+
hook. Defaults to the parameter names declared by ``mapping.hf_param``.
170173
171174
"""
172175

@@ -181,6 +184,41 @@ class WeightConversionTask(Generic[MappingT]):
181184
export_hook: Optional[Callable[[str, torch.Tensor], Iterable[HFWeightTuple]]] = field(
182185
default=None, compare=False, repr=False
183186
)
187+
required_hf_param_names: tuple[str, ...] | None = field(default=None, compare=False)
188+
189+
@property
190+
def hf_param_names(self) -> tuple[str, ...]:
191+
"""HF tensors required to import this Megatron parameter."""
192+
if self.required_hf_param_names is not None:
193+
return self.required_hf_param_names
194+
hf_param = self.mapping.hf_param
195+
names = (hf_param,) if isinstance(hf_param, str) else tuple(hf_param.values())
196+
return tuple(dict.fromkeys(names))
197+
198+
def local_hf_param_specs(self) -> tuple[LocalHFParamSpec, ...]:
199+
"""Return this parameter's canonical local HF-compatible views.
200+
201+
An empty tuple means the caller must use the normal Bridge conversion
202+
path; it does not declare the model unsupported for HF conversion or
203+
M-to-N refit. Destination backend and topology compatibility are outside
204+
this per-parameter contract, including whether MXFP8 is transferred as
205+
qualified physical data/scales or requantized from a logical view.
206+
"""
207+
return self.mapping.local_hf_param_specs(self.global_param_name)
208+
209+
def combine_local_hf_weights(self, weights: Mapping[str, torch.Tensor]) -> torch.Tensor:
210+
"""Reassemble transferred local HF views into this Megatron parameter."""
211+
specs = self.local_hf_param_specs()
212+
if not specs:
213+
raise ValueError(f"{self.param_name!r} has no local HF parameter views.")
214+
if len(specs) == 1:
215+
return weights[specs[0].name]
216+
217+
split_dims = {spec.split_dim for spec in specs}
218+
if len(split_dims) != 1 or None in split_dims:
219+
raise ValueError(f"{self.param_name!r} local HF views cannot be combined generically.")
220+
ordered = sorted(specs, key=lambda spec: spec.split_index)
221+
return torch.cat([weights[spec.name] for spec in ordered], dim=ordered[0].split_dim)
184222

185223

186224
class _HFNameSuffixMapping:
@@ -1013,6 +1051,70 @@ def maybe_modify_loaded_hf_weight(
10131051
hf_weights = {k: hf_state_dict[v] for k, v in hf_param.items()}
10141052
return hf_weights
10151053

1054+
@staticmethod
1055+
def get_hf_import_param_names(
1056+
hf_param: str | dict[str, str],
1057+
available_hf_param_names: set[str] | None = None,
1058+
) -> tuple[str, ...]:
1059+
"""Declare HF tensors that the import preprocessing hook may consume.
1060+
1061+
When checkpoint keys are available, this includes common quantization
1062+
companions and alternative packed representations. This keeps incremental
1063+
import callers from running a task before scale sidecars have arrived.
1064+
Bridges whose preprocessing hook reads other companion keys must override
1065+
this method to declare them.
1066+
1067+
Args:
1068+
hf_param: Parameter name or role-to-name mapping from the conversion mapping.
1069+
available_hf_param_names: Complete source schema, when known.
1070+
1071+
Returns:
1072+
Ordered, deduplicated source tensor names required for the task.
1073+
"""
1074+
mapped_names = (hf_param,) if isinstance(hf_param, str) else tuple(hf_param.values())
1075+
if available_hf_param_names is None:
1076+
return tuple(dict.fromkeys(mapped_names))
1077+
1078+
required_names: list[str] = []
1079+
for name in mapped_names:
1080+
if name in available_hf_param_names:
1081+
required_names.append(name)
1082+
companion_names = [f"{name}_scale_inv"]
1083+
if name.endswith(".weight"):
1084+
companion_names.append(f"{name[: -len('.weight')]}.scale")
1085+
required_names.extend(
1086+
companion_name for companion_name in companion_names if companion_name in available_hf_param_names
1087+
)
1088+
continue
1089+
1090+
packed_representations = (
1091+
(f"{name}_packed", f"{name}_scale", f"{name}_shape"),
1092+
(f"{name}_packed", f"{name}_scale"),
1093+
(f"{name}_blocks", f"{name}_scales"),
1094+
)
1095+
representation = next(
1096+
(
1097+
candidate
1098+
for candidate in packed_representations
1099+
if all(part in available_hf_param_names for part in candidate)
1100+
),
1101+
None,
1102+
)
1103+
if representation is not None:
1104+
required_names.extend(representation)
1105+
1106+
return tuple(dict.fromkeys(required_names))
1107+
1108+
@staticmethod
1109+
def _convert_loaded_hf_weight(
1110+
task: WeightConversionTask,
1111+
hf_weights: torch.Tensor | dict[str, torch.Tensor],
1112+
) -> Optional[torch.Tensor]:
1113+
"""Convert already-loaded HF inputs for one Bridge task."""
1114+
if task.megatron_module is None:
1115+
return None
1116+
return task.mapping.hf_to_megatron(hf_weights, task.megatron_module)
1117+
10161118
def maybe_modify_converted_hf_weight(
10171119
self,
10181120
task: WeightConversionTask,
@@ -1313,7 +1415,7 @@ def load_weights_hf_to_megatron(
13131415
_hf_import_cache[hf_param_key] = hf_weights
13141416

13151417
# 2) Delegate conversion & distribution to the bridge
1316-
converted_weights = task.mapping.hf_to_megatron(hf_weights, task.megatron_module)
1418+
converted_weights = self._convert_loaded_hf_weight(task, hf_weights)
13171419

13181420
# 3) Copy into Megatron param if this rank received a shard
13191421
if converted_weights is not None:
@@ -1360,7 +1462,7 @@ def load_weights_hf_to_megatron(
13601462
# "a leaf Variable that requires grad is being used in an in-place operation."
13611463
with torch.no_grad():
13621464
task.param_weight.copy_(converted_weights)
1363-
self._broadcast_shared_embeddings(megatron_model)
1465+
self.finalize_hf_import(megatron_model)
13641466
if use_megatron_fsdp:
13651467
for m in original_megatron_model:
13661468
m.module.install_optimized_model_weights()
@@ -1378,6 +1480,8 @@ def stream_weights_hf_to_megatron(
13781480
hf_pretrained: HFPreTrained,
13791481
megatron_model: Union[MegatronModel, List[MegatronModel]],
13801482
conversion_tasks: Optional[List[WeightConversionTask]] = None,
1483+
*,
1484+
hf_state_dict: Mapping[str, torch.Tensor] | None = None,
13811485
) -> Iterable[MegatronWeightTuple]:
13821486
"""Generator variant of load_weights_hf_to_megatron for streaming weight conversion.
13831487
@@ -1392,6 +1496,8 @@ def stream_weights_hf_to_megatron(
13921496
or list of model instances to extract configuration from.
13931497
conversion_tasks (Optional[List[WeightConversionTask]]): Pre-built conversion tasks.
13941498
If not provided, tasks will be built automatically from the models.
1499+
hf_state_dict: Optional external HF-style state mapping. When omitted,
1500+
weights are read from ``hf_pretrained.state``.
13951501
13961502
Yields:
13971503
MegatronWeightTuple: Named tuples containing:
@@ -1425,18 +1531,15 @@ def stream_weights_hf_to_megatron(
14251531
# Use provided conversion tasks or build them
14261532
if conversion_tasks is None:
14271533
conversion_tasks = self.build_conversion_tasks(hf_pretrained, megatron_model)
1534+
if hf_state_dict is None:
1535+
hf_state_dict = hf_pretrained.state
14281536

14291537
for task in conversion_tasks:
14301538
# None means megatron module not on current rank, skip if this task is not going to happen
14311539
if task.megatron_module is None:
14321540
continue
1433-
hf_state_dict: Mapping[str, torch.Tensor] = hf_pretrained.state
1434-
if isinstance(task.mapping.hf_param, str):
1435-
hf_weights = hf_state_dict[task.mapping.hf_param]
1436-
else:
1437-
hf_weights = {k: hf_state_dict[v] for k, v in task.mapping.hf_param.items()}
1438-
1439-
converted_weights = task.mapping.hf_to_megatron(hf_weights, task.megatron_module)
1541+
hf_weights = self.maybe_modify_loaded_hf_weight(task.mapping.hf_param, hf_state_dict)
1542+
converted_weights = self._convert_loaded_hf_weight(task, hf_weights)
14401543
if converted_weights is not None:
14411544
# Assert that vp_stage is not None for HF->Megatron tasks
14421545
yield MegatronWeightTuple(task.param_name, converted_weights, task.vp_stage)
@@ -1822,6 +1925,13 @@ def _broadcast_shared_embeddings(self, megatron_model: Union[MegatronModel, List
18221925
if hasattr(unwrapped_model, "output_layer"):
18231926
unwrapped_model.output_layer.weight.data.copy_(embd_weights)
18241927

1928+
def finalize_hf_import(self, megatron_model: Union[MegatronModel, List[MegatronModel]]) -> None:
1929+
"""Finalize tied parameters and parameter-derived caches after import."""
1930+
from megatron.core.resharding import refresh_module_caches
1931+
1932+
self._broadcast_shared_embeddings(megatron_model)
1933+
refresh_module_caches(megatron_model)
1934+
18251935
def _should_skip_mtp_duplicate_embedding_export(
18261936
self,
18271937
task: WeightConversionTask,
@@ -1899,6 +2009,7 @@ def build_conversion_tasks(
18992009
self.hf_config = hf_pretrained.config if hasattr(hf_pretrained, "config") else hf_pretrained
19002010

19012011
hf_keys: Optional[Iterable[str]] = hf_pretrained.state.source.get_all_keys() if has_hf_state else None
2012+
hf_key_set = set(hf_keys) if hf_keys is not None else None
19022013

19032014
mapping_registry = self.mapping_registry()
19042015
pg_collection = _get_pg_collection_from_model(megatron_model)
@@ -1918,7 +2029,7 @@ def build_conversion_tasks(
19182029
mappings_by_global_name = self._validate_conversion_mappings(
19192030
mapping_registry,
19202031
sorted_global_param_names_all_pp_ranks,
1921-
hf_keys,
2032+
hf_key_set,
19222033
)
19232034

19242035
global_names_index_dict = {name: idx for idx, name in enumerate(sorted_global_param_names_all_pp_ranks)}
@@ -1954,6 +2065,7 @@ def build_conversion_tasks(
19542065
param_weight=local_weights,
19552066
mapping=mapping,
19562067
weight_dtype=weight_dtype,
2068+
required_hf_param_names=self.get_hf_import_param_names(mapping.hf_param, hf_key_set),
19572069
)
19582070

19592071
# Fill the remaining ones for pp communications
@@ -1972,6 +2084,7 @@ def build_conversion_tasks(
19722084
param_weight=None,
19732085
mapping=mapping,
19742086
weight_dtype=weight_dtype,
2087+
required_hf_param_names=self.get_hf_import_param_names(mapping.hf_param, hf_key_set),
19752088
)
19762089

19772090
return self._require_concrete_tasks(pending_tasks)

0 commit comments

Comments
 (0)