Skip to content

Commit aa3914d

Browse files
jinyouzhiCopilotdelock
authored
[AutoTP] Complete uneven sharding and universal checkpoint support (deepspeedai#8185)
Follow up deepspeedai#8146. ## Summary This pull request introduces support for uneven sub-parameter sharding in DeepSpeed's universal checkpoint conversion, updating the universal checkpoint format to version 0.4. The changes ensure that partitioned parameters with sub-parameters of varying sizes are correctly handled during checkpoint conversion, merging, and restoration. Additionally, the PR adds validation to prevent conversion of unsupported checkpoint layouts and improves error handling and metadata validation. Key updates by theme: **Universal Checkpoint Format and Metadata:** - Bumped the universal checkpoint version to 0.4 and introduced the `SUB_PARAM_SHARD_WIDTHS` field to record per-rank widths for each sub-parameter, enabling correct handling of uneven sub-parameter layouts. (`deepspeed/checkpoint/constants.py`, `deepspeed/checkpoint/ds_to_universal.py`) [[1]](diffhunk://#diff-7dfbb96f4f4bdab1e2be9ef97bda5a23e25e32d7e218991c268aea8065aec05eL61-R64) [[2]](diffhunk://#diff-7dfbb96f4f4bdab1e2be9ef97bda5a23e25e32d7e218991c268aea8065aec05eR93-R97) [[3]](diffhunk://#diff-ef90f4743f09a5fcb81bcf3487a1d0f4b638e1191ca2c08c1d1d1f2b2c6b6f76R35) [[4]](diffhunk://#diff-ef90f4743f09a5fcb81bcf3487a1d0f4b638e1191ca2c08c1d1d1f2b2c6b6f76R302-R303) [[5]](diffhunk://#diff-ef90f4743f09a5fcb81bcf3487a1d0f4b638e1191ca2c08c1d1d1f2b2c6b6f76L335-R441) - Added `AUTOTP_UNSUPPORTED_PARAMETER_PATTERNS` to checkpoint metadata and implemented validation to prevent conversion if unsupported parameter patterns are present. (`deepspeed/checkpoint/constants.py`, `deepspeed/checkpoint/ds_to_universal.py`) [[1]](diffhunk://#diff-7dfbb96f4f4bdab1e2be9ef97bda5a23e25e32d7e218991c268aea8065aec05eL61-R64) [[2]](diffhunk://#diff-ef90f4743f09a5fcb81bcf3487a1d0f4b638e1191ca2c08c1d1d1f2b2c6b6f76R47) [[3]](diffhunk://#diff-ef90f4743f09a5fcb81bcf3487a1d0f4b638e1191ca2c08c1d1d1f2b2c6b6f76R950-R957) [[4]](diffhunk://#diff-ef90f4743f09a5fcb81bcf3487a1d0f4b638e1191ca2c08c1d1d1f2b2c6b6f76R1134-R1139) **Parameter Merging and Sharding Logic:** - Enhanced the merging logic to correctly handle missing fragments for ranks with no data in uneven parameter sharding, ensuring proper alignment of slices and placeholder insertion. (`deepspeed/checkpoint/ds_to_universal.py`) [[1]](diffhunk://#diff-ef90f4743f09a5fcb81bcf3487a1d0f4b638e1191ca2c08c1d1d1f2b2c6b6f76R239-R248) [[2]](diffhunk://#diff-ef90f4743f09a5fcb81bcf3487a1d0f4b638e1191ca2c08c1d1d1f2b2c6b6f76R273-R285) - Refactored the sub-parameter merging code to use the new shard widths metadata, supporting both legacy (even) and new (uneven) layouts. Added logic to reconstruct logical shapes with placeholder dimensions. (`deepspeed/checkpoint/ds_to_universal.py`) [[1]](diffhunk://#diff-ef90f4743f09a5fcb81bcf3487a1d0f4b638e1191ca2c08c1d1d1f2b2c6b6f76L302-R327) [[2]](diffhunk://#diff-ef90f4743f09a5fcb81bcf3487a1d0f4b638e1191ca2c08c1d1d1f2b2c6b6f76L335-R441) [[3]](diffhunk://#diff-ef90f4743f09a5fcb81bcf3487a1d0f4b638e1191ca2c08c1d1d1f2b2c6b6f76R463-R482) **Validation and Error Handling:** - Added early validation for unsupported AutoTP conversions to fail fast before expensive extraction steps. (`deepspeed/checkpoint/ds_to_universal.py`) [[1]](diffhunk://#diff-ef90f4743f09a5fcb81bcf3487a1d0f4b638e1191ca2c08c1d1d1f2b2c6b6f76R950-R957) [[2]](diffhunk://#diff-ef90f4743f09a5fcb81bcf3487a1d0f4b638e1191ca2c08c1d1d1f2b2c6b6f76R1134-R1139) - Improved shape consistency checks for pipeline-parallel parameters to ensure all replicas agree on shape. (`deepspeed/checkpoint/ds_to_universal.py`) **Restoration Logic:** - Updated the restoration logic to use the new shard widths metadata, ensuring correct reconstruction of sub-parameters during model loading. (`deepspeed/checkpoint/universal_checkpoint.py`) [[1]](diffhunk://#diff-22d7b9e3b6eac1dc6e989cd8582946321c36f4fc80b527648eeb2d77a8fd3ee2L13-R13) [[2]](diffhunk://#diff-22d7b9e3b6eac1dc6e989cd8582946321c36f4fc80b527648eeb2d77a8fd3ee2R34-R73) These changes collectively improve the robustness and flexibility of DeepSpeed's checkpoint conversion, especially for advanced tensor parallelism scenarios. ## Testing * Added coverage for uneven vocabulary, GQA projections, checkpoint conversion/restore, and PP + TP tied parameters. * loss curve deepspeedai/DeepSpeedExamples#1008 ## Limitations tp_size > num_kv need to further be optimized --------- Signed-off-by: iLeGend <824040212@qq.com> Signed-off-by: Jin, Youzhi <youzhi.jin@intel.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Ma,Guokai <guokai.ma@intel.com> Co-authored-by: Ma, Guokai <guokai.ma@gmail.com>
1 parent 6098f79 commit aa3914d

22 files changed

Lines changed: 3348 additions & 482 deletions

deepspeed/checkpoint/constants.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,9 +58,10 @@
5858
UNIVERSAL_CHECKPOINT_INFO = 'universal_checkpoint_info'
5959
UNIVERSAL_CHECKPOINT_VERSION_KEY = 'universal_checkpoint_version'
6060
# Reserve version 0.1 for the hardcoded logic used in BLOOM-176B training
61-
UNIVERSAL_CHECKPOINT_VERSION_VALUE = 0.3
61+
UNIVERSAL_CHECKPOINT_VERSION_VALUE = 0.4
6262
# Attribute name used to store AutoTP universal-checkpoint metadata on torch Parameters.
6363
DS_AUTOTP_UC_META = "ds_autotp_universal_checkpoint_meta"
64+
AUTOTP_UNSUPPORTED_PARAMETER_PATTERNS = "autotp_unsupported_parameter_patterns"
6465

6566
# Vocabulary padding
6667
VOCAB_TENSOR = 'vocab_tensor'
@@ -89,6 +90,11 @@
8990
TP_REPLICATED_PARAMETER_PATTERNS = 'tp_replicated_parameter_patterns'
9091
PARAMETER_WITH_2_SUB_PARAMS_CAT_DIM_0 = 'parameter_with_2_sub_params_cat_dim_0'
9192
PARAMETER_WITH_SUB_PARAMS = 'parameter_with_sub_params'
93+
# Per-rank width of every sub-parameter, keyed by the same pattern used in
94+
# PARAMETER_WITH_SUB_PARAMS. Kept as a separate top-level key so that converters predating
95+
# uneven sub-parameter support simply do not see it, instead of failing to build a
96+
# SubparamShape from an unexpected field.
97+
SUB_PARAM_SHARD_WIDTHS = 'sub_param_shard_widths' # UCP version 0.4
9298
SUB_PARAMS_SHAPE = 'sub_params_shape'
9399

94100
#########################################

deepspeed/checkpoint/ds_to_universal.py

Lines changed: 150 additions & 22 deletions
Large diffs are not rendered by default.

deepspeed/checkpoint/universal_checkpoint.py

Lines changed: 85 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from typing import List, Tuple, Union
1111
from dataclasses import dataclass
1212
from .constants import (FP32_WEIGHT_KEY, PARAM, VOCAB_TENSOR, CAT_DIM, PARAM_N_SUB_PARAMS, SUB_PARAM_SHAPE,
13-
EP_IS_EXPERT_PARAM, EP_NUM_EXPERTS, DS_AUTOTP_UC_META)
13+
EP_IS_EXPERT_PARAM, EP_NUM_EXPERTS, DS_AUTOTP_UC_META, UNIVERSAL_CHECKPOINT_VERSION_KEY)
1414

1515

1616
@dataclass
@@ -31,6 +31,46 @@ def _get_param_uc_restore_meta(param):
3131
return getattr(param, DS_AUTOTP_UC_META, None)
3232

3333

34+
def _narrow_sub_params(full_view, partition_dim, sub_dim_sizes, shard_widths, tp_rank, tp_world_size, uc_version):
35+
"""Take this rank's piece of every sub-parameter and concatenate them back together.
36+
37+
``shard_widths`` gives the per-rank width of each sub-parameter, so a fused attention
38+
weight is cut on key/value head boundaries. Metadata written before UCP version 0.4 carries
39+
no widths and can only have been split evenly.
40+
"""
41+
assert sum(sub_dim_sizes) == full_view.shape[partition_dim], (
42+
f"Sub-parameter sizes {list(sub_dim_sizes)} sum to {sum(sub_dim_sizes)}, but dimension {partition_dim} "
43+
f"of the parameter is {full_view.shape[partition_dim]}. The metadata describes a different parameter.")
44+
45+
if uc_version < 0.4:
46+
# An even split is the only layout recoverable without recorded widths. Assuming it for
47+
# an uneven sub-parameter shifts every offset and silently drops the trailing elements,
48+
# so refuse rather than restore wrong weights.
49+
uneven = [size for size in sub_dim_sizes if size % tp_world_size != 0]
50+
assert not uneven, (
51+
f"Sub-parameter sizes {uneven} are not divisible by tp_world_size {tp_world_size}, and this "
52+
"metadata records no sub_param_shard_widths, so its uneven layout cannot be reconstructed.")
53+
shard_widths = [[size // tp_world_size] * tp_world_size for size in sub_dim_sizes]
54+
55+
assert len(shard_widths) == len(sub_dim_sizes), (
56+
f"Got {len(shard_widths)} shard width entries for {len(sub_dim_sizes)} sub-parameters.")
57+
58+
offset = 0
59+
merged_chunks = []
60+
for sub_dim_size, widths in zip(sub_dim_sizes, shard_widths):
61+
assert len(widths) == tp_world_size, (
62+
f"Sub-parameter of size {sub_dim_size} has {len(widths)} shard widths, expected one per tp rank "
63+
f"({tp_world_size}).")
64+
assert sum(widths) == sub_dim_size, (
65+
f"Sub-parameter shard widths {list(widths)} sum to {sum(widths)}, expected {sub_dim_size}.")
66+
start = offset + sum(widths[:tp_rank])
67+
merged_chunks.append(full_view.narrow(partition_dim, start, widths[tp_rank]))
68+
offset += sub_dim_size
69+
70+
slice_tensor = torch.cat(merged_chunks, dim=partition_dim)
71+
return slice_tensor.flatten()
72+
73+
3474
def _resolve_autotp_partition(current_param, ckpt_dict, full_hp_param, tp_rank, tp_world_size):
3575
meta = _get_param_uc_restore_meta(current_param)
3676
if not meta:
@@ -40,7 +80,17 @@ def _resolve_autotp_partition(current_param, ckpt_dict, full_hp_param, tp_rank,
4080
logical_shape = meta.get('logical_shape')
4181
sub_param_shape = meta.get('sub_param_shape')
4282
sub_param_sizes = meta.get('sub_param_sizes')
83+
sub_param_shard_widths = meta.get('sub_param_shard_widths')
84+
partition_sizes = meta.get('partition_sizes')
4385
replicated = meta.get('replicated', False)
86+
uc_version = meta.get(UNIVERSAL_CHECKPOINT_VERSION_KEY, 0.0)
87+
88+
# The layer could not describe how it split this parameter, so conversion refuses it. The
89+
# generic paths below would reassemble it as contiguous rank-ordered slices, which is not
90+
# how a fused layout was cut, so restoring here would silently corrupt the weight.
91+
unsupported_reason = meta.get('conversion', {}).get('unsupported_reason')
92+
if unsupported_reason:
93+
raise RuntimeError(f"Cannot restore a universal checkpoint into this AutoTP parameter: {unsupported_reason}")
4494

4595
if replicated:
4696
assert partition_dim is None
@@ -55,43 +105,43 @@ def _resolve_autotp_partition(current_param, ckpt_dict, full_hp_param, tp_rank,
55105

56106
full_view = full_hp_param.view(logical_shape)
57107

58-
if sub_param_shape is not None:
108+
# sub_param_sizes holds the resolved physical width of each sub-parameter, whereas
109+
# sub_param_shape may be a logical view spec such as (3, -1) whose partition_dim entry is
110+
# the sub-parameter *count*. Reading that count as a width restores only a fraction of the
111+
# parameter, so only fall back to sub_param_shape for older metadata that lacks the sizes.
112+
sub_dim_sizes = None
113+
if sub_param_sizes is not None:
114+
sub_dim_sizes = sub_param_sizes
115+
elif sub_param_shape is not None:
59116
if hasattr(sub_param_shape, "shape") and hasattr(sub_param_shape, "partition_dim"):
60117
shape_spec = sub_param_shape.shape
61118
partition_dim = sub_param_shape.partition_dim
62119
else:
63120
shape_spec = sub_param_shape
64-
65121
sub_dim_sizes = shape_spec[partition_dim]
66-
if not isinstance(sub_dim_sizes, tuple):
67-
sub_dim_sizes = (sub_dim_sizes, )
68122

69-
offset = 0
70-
merged_chunks = []
71-
for sub_dim_size in sub_dim_sizes:
72-
sub_slice = full_view.narrow(partition_dim, offset, sub_dim_size) \
73-
.chunk(tp_world_size, dim=partition_dim)[tp_rank]
74-
merged_chunks.append(sub_slice)
75-
offset += sub_dim_size
123+
if sub_dim_sizes is not None:
124+
if not isinstance(sub_dim_sizes, (tuple, list)):
125+
sub_dim_sizes = (sub_dim_sizes, )
76126

77-
slice_tensor = torch.cat(merged_chunks, dim=partition_dim)
78-
return slice_tensor.flatten()
127+
return _narrow_sub_params(full_view, partition_dim, sub_dim_sizes, sub_param_shard_widths, tp_rank,
128+
tp_world_size, uc_version)
79129

80-
if sub_param_sizes is not None:
81-
if not isinstance(sub_param_sizes, (tuple, list)):
82-
sub_param_sizes = (sub_param_sizes, )
83-
84-
offset = 0
85-
merged_chunks = []
86-
for sub_dim_size in sub_param_sizes:
87-
sub_slice = full_view.narrow(partition_dim, offset, sub_dim_size) \
88-
.chunk(tp_world_size, dim=partition_dim)[tp_rank]
89-
merged_chunks.append(sub_slice)
90-
offset += sub_dim_size
91-
92-
slice_tensor = torch.cat(merged_chunks, dim=partition_dim)
130+
if partition_sizes is not None:
131+
shard_offset = sum(partition_sizes[:tp_rank])
132+
shard_size = partition_sizes[tp_rank]
133+
slice_tensor = full_view.narrow(partition_dim, shard_offset, shard_size)
93134
return slice_tensor.flatten()
94135

136+
# torch.chunk sizes every block as ceil(size / tp_world_size) and shrinks the last one,
137+
# while AutoTP hands one extra element to each of the first `size % tp_world_size` ranks.
138+
# The two disagree once the split is uneven (10/4 -> chunk [3, 3, 3, 1] vs AutoTP
139+
# [3, 3, 2, 2]), and chunk can even return fewer blocks than there are ranks.
140+
partition_dim_size = full_view.shape[partition_dim]
141+
assert partition_dim_size % tp_world_size == 0, (
142+
f"Dimension {partition_dim} of size {partition_dim_size} is not divisible by tp_world_size "
143+
f"{tp_world_size}, and this checkpoint records no partition_sizes, so its uneven layout cannot "
144+
"be reconstructed.")
95145
slice_tensor = full_view.chunk(tp_world_size, dim=partition_dim)[tp_rank]
96146
return slice_tensor.flatten()
97147

@@ -148,15 +198,21 @@ def load_hp_checkpoint_state(self, folder, tp_rank, tp_world_size, ep_rank=0, ep
148198
# implementation b.
149199
# this version requires no additional data passed from the client
150200
# if the shapes already match it must be slices that were averaged - so we just hack around those
151-
if full_hp_param.shape == self.shape:
201+
# AutoTP restore metadata already pins down this rank's slice of the universal tensor,
202+
# including layouts where one rank owns the whole thing (e.g. a single KV head under MQA).
203+
# Collapsing the TP topology here would contradict the recorded per-rank widths.
204+
has_autotp_meta = _get_param_uc_restore_meta(self) is not None
205+
if full_hp_param.shape == self.shape and not has_autotp_meta:
152206
tp_rank = 0
153207
tp_world_size = 1
154208

155209
# special case for word_embeddings weights which get padded differently depending on TP degree.
156210
# the converter to universal currently strips the original padding completely so the saved
157211
# weight is padding-free and we just need to add new padding depending on the target TP
158212
# degree
159-
is_vocab_tensor = ckpt_dict.get(VOCAB_TENSOR, False) and not is_expert_param
213+
# AutoTP restore metadata already describes the exact (possibly uneven) partition layout,
214+
# so the legacy tp-degree-derived vocab padding must not be applied on top of it.
215+
is_vocab_tensor = ckpt_dict.get(VOCAB_TENSOR, False) and not is_expert_param and not has_autotp_meta
160216
if is_vocab_tensor:
161217
# In the absence of data passed from the user wrt new padded vocab specific to tp degree
162218
# we can again derive that data by reverse engineering the target shapes like so:

deepspeed/compile/custom_ops/tp_collectives.py

Lines changed: 37 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -47,30 +47,46 @@ def reduce_from_tp_region_fake(input: torch.Tensor):
4747

4848

4949
@torch.library.custom_op("autotp::gather_from_tp_region", mutates_args=())
50-
def gather_from_tp_region(input: torch.Tensor) -> torch.Tensor:
51-
"""All-gather the last dimension in the forward pass, take this rank's slice in the backward.
50+
def gather_from_tp_region(input: torch.Tensor, partition_sizes: list[int]) -> torch.Tensor:
51+
"""All-gather the last dimension using the frozen shard widths.
5252
5353
Inserted after a column-parallel matmul whose layer asks for gather_output, so that
54-
every rank leaves the layer holding the full output rather than its own shard.
54+
every rank leaves the layer holding the full output rather than its own shard. Unlike the
55+
even-width fast path, this takes an explicit `partition_sizes` list so uneven TP shards are
56+
still reconstructed correctly.
5557
"""
5658
group = get_tp_group()
5759
world_size = dist.get_world_size(group=group)
5860
if world_size == 1:
5961
return input.clone()
6062

61-
local_shard = input.contiguous()
63+
if len(partition_sizes) != world_size:
64+
raise ValueError(f"partition_sizes={partition_sizes} does not match TP world size {world_size}")
65+
66+
local_rank = dist.get_rank(group=group)
67+
local_size = partition_sizes[local_rank]
68+
if input.shape[-1] != local_size:
69+
raise ValueError(
70+
f"Rank {local_rank} produced width {input.shape[-1]}, but partition_sizes expects {local_size}")
71+
72+
max_partition_size = max(partition_sizes)
73+
if local_size == max_partition_size:
74+
local_shard = input.contiguous()
75+
else:
76+
local_shard = input.new_zeros((*input.shape[:-1], max_partition_size))
77+
local_shard[..., :local_size].copy_(input)
78+
6279
flat_gathered = torch.empty((world_size * local_shard.shape[0], *local_shard.shape[1:]),
6380
dtype=local_shard.dtype,
6481
device=local_shard.device)
6582
dist.all_gather_into_tensor(flat_gathered, local_shard, group=group)
6683
shards = flat_gathered.view(world_size, *local_shard.shape)
67-
return torch.cat(shards.unbind(0), dim=-1)
84+
return torch.cat([shards[i].narrow(-1, 0, size) for i, size in enumerate(partition_sizes)], dim=-1)
6885

6986

7087
@torch.library.register_fake("autotp::gather_from_tp_region")
71-
def gather_from_tp_region_fake(input: torch.Tensor):
72-
world_size = dist.get_world_size(group=get_tp_group())
73-
return input.new_empty((*input.shape[:-1], input.shape[-1] * world_size))
88+
def gather_from_tp_region_fake(input: torch.Tensor, partition_sizes: list[int]):
89+
return input.new_empty((*input.shape[:-1], sum(partition_sizes)))
7490

7591

7692
def _copy_to_tp_region_backward(ctx, grad):
@@ -83,14 +99,22 @@ def _reduce_from_tp_region_backward(ctx, grad):
8399
return grad
84100

85101

102+
def _gather_from_tp_region_setup(ctx, inputs, output):
103+
_, partition_sizes = inputs
104+
ctx.partition_sizes = tuple(partition_sizes)
105+
106+
86107
def _gather_from_tp_region_backward(ctx, grad):
87108
group = get_tp_group()
88109
world_size = dist.get_world_size(group=group)
89110
if world_size == 1:
90-
return grad
91-
shard_width = grad.shape[-1] // world_size
92-
shard_start = dist.get_rank(group=group) * shard_width
93-
return grad.narrow(-1, shard_start, shard_width).contiguous()
111+
return (grad, None)
112+
113+
partition_sizes = ctx.partition_sizes
114+
rank = dist.get_rank(group=group)
115+
shard_start = sum(partition_sizes[:rank])
116+
shard_width = partition_sizes[rank]
117+
return grad.narrow(-1, shard_start, shard_width).contiguous(), None
94118

95119

96120
def _setup_context_without_saved_tensors(ctx, inputs, output):
@@ -105,4 +129,4 @@ def _setup_context_without_saved_tensors(ctx, inputs, output):
105129
setup_context=_setup_context_without_saved_tensors)
106130
torch.library.register_autograd("autotp::gather_from_tp_region",
107131
_gather_from_tp_region_backward,
108-
setup_context=_setup_context_without_saved_tensors)
132+
setup_context=_gather_from_tp_region_setup)

0 commit comments

Comments
 (0)