Skip to content

Commit 5666907

Browse files
committed
Emit a map for parameters AutoTP leaves untouched
An untouched parameter is identical on every rank, so it is describable as one replicated piece -- but building the map needs the tp degree, which only the partitioned layers carry. Take it from one of those and fill in the rest after the walk. Add the coverage invariant: every parameter placed by a name category must also carry a map, or conversion still depends on the category. Only the layouts AutoTP refuses to describe are exempt. Signed-off-by: Achyuthan Sivasankar <achyuthan.sivasankar@gmail.com>
1 parent 2a8ce10 commit 5666907

2 files changed

Lines changed: 98 additions & 1 deletion

File tree

deepspeed/module_inject/layers.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -665,7 +665,7 @@ def collect_autotp_universal_checkpoint_info(model: nn.Module) -> Dict[str, Any]
665665
restore-time per-parameter details such as `sub_param_sizes` or
666666
`target_partition_shape`, which stay on the parameter metadata object.
667667
"""
668-
from deepspeed.checkpoint.affine import AFFINE_MAP_FORMAT_VERSION
668+
from deepspeed.checkpoint.affine import AFFINE_MAP_FORMAT_VERSION, replicated_map
669669
from deepspeed.checkpoint.constants import (AFFINE_MAP, AFFINE_MAP_PARAMS, AFFINE_MAP_VERSION,
670670
AUTOTP_UNSUPPORTED_PARAMETER_PATTERNS, ORIGINAL_VOCAB_SIZE,
671671
PARAMETER_WITH_ROW_PARALLELISM_PATTERNS, PARAMETER_WITH_SUB_PARAMS,
@@ -680,6 +680,8 @@ def collect_autotp_universal_checkpoint_info(model: nn.Module) -> Dict[str, Any]
680680
parameter_with_sub_params = []
681681
unsupported_parameter_patterns = {}
682682
affine_maps = {}
683+
untouched_shapes = {}
684+
tp_world_size = None
683685
original_vocab_size = None
684686

685687
# Tied parameters are reachable under several module attributes, but the optimizer -- and
@@ -691,6 +693,8 @@ def collect_autotp_universal_checkpoint_info(model: nn.Module) -> Dict[str, Any]
691693
marker = getattr(module, "_mark_uc_metadata", None)
692694
if marker is not None:
693695
marker()
696+
if tp_world_size is None:
697+
tp_world_size = getattr(module, 'tp_world_size', None)
694698

695699
for param_name, param in module.named_parameters(recurse=False):
696700
full_name = f"{module_name}.{param_name}" if module_name else param_name
@@ -704,7 +708,12 @@ def collect_autotp_universal_checkpoint_info(model: nn.Module) -> Dict[str, Any]
704708
# ranks. Classify it as TP-replicated; otherwise it falls through to
705709
# the converter's default dim-0 concat and is wrongly expanded (e.g.
706710
# LayerNorm/RMSNorm weights [H] -> [H * tp_degree]).
711+
#
712+
# Such a parameter is describable -- one piece held by every rank -- but the
713+
# map needs the tp degree, which only the partitioned layers carry. Record
714+
# the shape and build the map once the loop has seen one of them.
707715
replicated_patterns.append(pattern)
716+
untouched_shapes[pattern] = tuple(param.shape)
708717
continue
709718

710719
unsupported_reason = conversion_meta.get('unsupported_reason')
@@ -761,6 +770,10 @@ def collect_autotp_universal_checkpoint_info(model: nn.Module) -> Dict[str, Any]
761770
uc_info[SUB_PARAM_SHARD_WIDTHS] = sub_param_shard_widths
762771
if original_vocab_size is not None:
763772
uc_info[ORIGINAL_VOCAB_SIZE] = original_vocab_size
773+
if tp_world_size:
774+
for pattern, shape in untouched_shapes.items():
775+
affine_maps[pattern] = replicated_map(shape, tp_world_size).to_dict()
776+
764777
if affine_maps:
765778
# Published alongside the pattern lists rather than instead of them, so a converter
766779
# that predates the map simply does not see the key and takes the categories.

tests/unit/checkpoint/test_autotp_uc_checkpoint.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1763,3 +1763,87 @@ def test_producer_matches_the_verified_fixture_layout(self):
17631763
assert maps[pattern] == want, (f"emitted map for {pattern} differs from the layout the resume "
17641764
f"fixture verifies:\n emitted {maps[pattern]}\n expected {want}")
17651765
engine.destroy()
1766+
1767+
1768+
class AffineCoverageModel(torch.nn.Module):
1769+
"""Covers each conversion category: column split, row split, replicated, untouched."""
1770+
1771+
def __init__(self, hidden_dim=16, vocab_size=26):
1772+
super().__init__()
1773+
self.embed = torch.nn.Embedding(vocab_size, hidden_dim) # AutoTP leaves this alone
1774+
self.norm = torch.nn.LayerNorm(hidden_dim) # untouched, and not 2-D
1775+
self.fc1 = torch.nn.Linear(hidden_dim, hidden_dim) # column
1776+
self.fc2 = torch.nn.Linear(hidden_dim, hidden_dim) # row
1777+
self.lm_head = torch.nn.Linear(hidden_dim, vocab_size) # vocabulary
1778+
1779+
def forward(self, x):
1780+
h = self.norm(self.embed(x))
1781+
return self.lm_head(self.fc2(self.fc1(h))).sum()
1782+
1783+
1784+
class TestAffineMapCoverage(DistributedTest):
1785+
"""Every parameter the converter can place must carry an affine map.
1786+
1787+
A parameter with no map falls back to its name category, which is the behaviour the IR
1788+
exists to replace. The only parameters allowed to have no map are the ones AutoTP itself
1789+
refuses to describe, which conversion rejects anyway.
1790+
"""
1791+
1792+
world_size = 2
1793+
1794+
def test_every_convertible_parameter_has_a_map(self):
1795+
from deepspeed.checkpoint.constants import (AFFINE_MAP, AFFINE_MAP_PARAMS,
1796+
AUTOTP_UNSUPPORTED_PARAMETER_PATTERNS,
1797+
PARAMETER_WITH_ROW_PARALLELISM_PATTERNS, PARAMETER_WITH_SUB_PARAMS,
1798+
TP_REPLICATED_PARAMETER_PATTERNS, VOCABULARY_PARAMETER_PATTERNS)
1799+
from deepspeed.module_inject.layers import collect_autotp_universal_checkpoint_info
1800+
1801+
config = {
1802+
"train_micro_batch_size_per_gpu": 1,
1803+
"zero_allow_untested_optimizer": True,
1804+
"zero_optimization": {
1805+
"stage": 1
1806+
},
1807+
"tensor_parallel": {
1808+
"autotp_size": self.world_size,
1809+
"partition_config": {
1810+
"use_default_specs":
1811+
False,
1812+
"layer_specs": [
1813+
{
1814+
"patterns": [r".*fc1\.weight$"],
1815+
"partition_type": "column"
1816+
},
1817+
{
1818+
"patterns": [r".*fc2\.weight$"],
1819+
"partition_type": "row"
1820+
},
1821+
{
1822+
"patterns": [r".*lm_head\.weight$"],
1823+
"partition_type": "column",
1824+
"gather_output": True
1825+
},
1826+
],
1827+
},
1828+
},
1829+
}
1830+
model = AffineCoverageModel()
1831+
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)
1832+
engine, _, _, _ = deepspeed.initialize(model=model, optimizer=optimizer, config=config)
1833+
info = collect_autotp_universal_checkpoint_info(engine.module)
1834+
1835+
mapped = set(info.get(AFFINE_MAP, {}).get(AFFINE_MAP_PARAMS, {}))
1836+
unsupported = set(info.get(AUTOTP_UNSUPPORTED_PARAMETER_PATTERNS, {}))
1837+
1838+
categorised = set()
1839+
for key in (TP_REPLICATED_PARAMETER_PATTERNS, PARAMETER_WITH_ROW_PARALLELISM_PATTERNS,
1840+
VOCABULARY_PARAMETER_PATTERNS):
1841+
categorised.update(info.get(key, []))
1842+
for entry in info.get(PARAMETER_WITH_SUB_PARAMS, []):
1843+
categorised.update(entry["patterns"])
1844+
1845+
assert categorised, "model exercised no conversion category, so this proves nothing"
1846+
missing = categorised - mapped - unsupported
1847+
assert not missing, (f"these parameters are placed by a name category but carry no affine map, "
1848+
f"so conversion still depends on the category: {sorted(missing)}")
1849+
engine.destroy()

0 commit comments

Comments
 (0)