Skip to content

Commit f38e9a0

Browse files
committed
Add configurable dtype for ZeRO checkpoint export
Signed-off-by: gaoxiaomo <165135449+gaoxiaomo@users.noreply.github.com>
1 parent 7154a00 commit f38e9a0

6 files changed

Lines changed: 219 additions & 21 deletions

File tree

deepspeed/runtime/engine.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5391,12 +5391,12 @@ def get_layer_state_dict(module, prefix=""):
53915391

53925392
def _copy_recovery_script(self, save_path):
53935393
base_dir = os.path.dirname(os.path.dirname(__file__))
5394-
script = "zero_to_fp32.py"
5395-
src = os.path.join(base_dir, "utils", script)
5396-
dst = os.path.join(save_path, script)
5397-
#logger.info(f"creating recovery script {dst}")
5398-
copyfile(src, dst)
5399-
self._change_recovery_script_permissions(dst)
5394+
for script in ("zero_to_fp32.py", "zero_to_torch.py"):
5395+
src = os.path.join(base_dir, "utils", script)
5396+
dst = os.path.join(save_path, script)
5397+
#logger.info(f"creating recovery script {dst}")
5398+
copyfile(src, dst)
5399+
self._change_recovery_script_permissions(dst)
54005400

54015401
def _change_recovery_script_permissions(self, dst):
54025402
# make executable (safeguard for file shares - Azure as example)

deepspeed/utils/zero_to_fp32.py

Lines changed: 57 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,26 @@ class zero_model_state:
5353
# load to cpu
5454
device = torch.device('cpu')
5555

56+
OUTPUT_DTYPE_NAMES = {
57+
'float32': torch.float32,
58+
'fp32': torch.float32,
59+
'float16': torch.float16,
60+
'fp16': torch.float16,
61+
'bfloat16': torch.bfloat16,
62+
'bf16': torch.bfloat16,
63+
}
64+
65+
66+
def _resolve_output_dtype(dtype):
67+
requested_dtype = dtype
68+
if isinstance(dtype, str):
69+
dtype_name = dtype[6:] if dtype.startswith('torch.') else dtype
70+
dtype = OUTPUT_DTYPE_NAMES.get(dtype_name.lower())
71+
if dtype not in set(OUTPUT_DTYPE_NAMES.values()):
72+
supported = ', '.join(sorted(OUTPUT_DTYPE_NAMES))
73+
raise ValueError(f"Unsupported output dtype {requested_dtype!r}. Choose one of: {supported}")
74+
return dtype
75+
5676

5777
def atoi(text):
5878
return int(text) if text.isdigit() else text
@@ -540,10 +560,13 @@ def _get_fp32_state_dict_from_zero3_checkpoint(world_size, fp32_flat_groups, zer
540560
return state_dict
541561

542562

543-
def to_torch_tensor(state_dict, return_empty_tensor=False):
563+
def to_torch_tensor(state_dict, return_empty_tensor=False, dtype=None):
544564
"""
545565
Convert state_dict of GatheredTensor to torch tensor
546566
"""
567+
if dtype is not None:
568+
dtype = _resolve_output_dtype(dtype)
569+
547570
torch_state_dict = {}
548571
converted_tensors = {}
549572
for name, tensor in state_dict.items():
@@ -554,9 +577,10 @@ def to_torch_tensor(state_dict, return_empty_tensor=False):
554577
else:
555578
converted_tensors[tensor_id] = name
556579
if return_empty_tensor:
557-
torch_state_dict[name] = torch.empty(tensor.shape, dtype=tensor.dtype)
580+
torch_state_dict[name] = torch.empty(tensor.shape, dtype=dtype or tensor.dtype)
558581
else:
559-
torch_state_dict[name] = tensor.contiguous()
582+
contiguous_tensor = tensor.contiguous()
583+
torch_state_dict[name] = contiguous_tensor.to(dtype=dtype) if dtype else contiguous_tensor
560584
return torch_state_dict
561585

562586

@@ -625,25 +649,29 @@ def get_fp32_state_dict_from_zero_checkpoint(checkpoint_dir,
625649
return to_torch_tensor(state_dict)
626650

627651

628-
def convert_zero_checkpoint_to_fp32_state_dict(checkpoint_dir,
629-
output_dir,
630-
max_shard_size="5GB",
631-
safe_serialization=False,
632-
tag=None,
633-
exclude_frozen_parameters=False):
652+
def convert_zero_checkpoint_to_state_dict(checkpoint_dir,
653+
output_dir,
654+
dtype=torch.float32,
655+
max_shard_size="5GB",
656+
safe_serialization=False,
657+
tag=None,
658+
exclude_frozen_parameters=False):
634659
"""
635-
Convert ZeRO 2 or 3 checkpoint into a single fp32 consolidated ``state_dict`` file that can be
660+
Convert ZeRO 2 or 3 checkpoint into a consolidated ``state_dict`` file that can be
636661
loaded with ``torch.load(file)`` + ``load_state_dict()`` and used for training without DeepSpeed.
637662
638663
Args:
639664
- ``checkpoint_dir``: path to the desired checkpoint folder. (one that contains the tag-folder, like ``global_step14``)
640-
- ``output_dir``: directory to the pytorch fp32 state_dict output files
665+
- ``output_dir``: directory for the PyTorch state_dict output files
666+
- ``dtype``: output tensor dtype. Supports float32, float16, and bfloat16 as strings or torch dtypes.
641667
- ``max_shard_size``: the maximum size for a checkpoint before being sharded, default value is 5GB
642668
- ``safe_serialization``: whether to save the model using `safetensors` or the traditional PyTorch way (that uses `pickle`).
643669
- ``tag``: checkpoint tag used as a unique identifier for checkpoint. If not provided will attempt to load tag in the file named ``latest`` in the checkpoint folder, e.g., ``global_step14``
644670
- ``exclude_frozen_parameters``: exclude frozen parameters
645671
"""
646672

673+
dtype = _resolve_output_dtype(dtype)
674+
647675
# Dependency pre-check
648676
if safe_serialization:
649677
try:
@@ -669,7 +697,7 @@ def convert_zero_checkpoint_to_fp32_state_dict(checkpoint_dir,
669697
if max_shard_size is not None:
670698
filename_pattern = weights_name.replace(".bin", "{suffix}.bin").replace(".safetensors", "{suffix}.safetensors")
671699
# an memory-efficient approach for sharding
672-
empty_state_dict = to_torch_tensor(state_dict, return_empty_tensor=True)
700+
empty_state_dict = to_torch_tensor(state_dict, return_empty_tensor=True, dtype=dtype)
673701
state_dict_split = split_torch_state_dict_into_shards(empty_state_dict,
674702
filename_pattern=filename_pattern,
675703
max_shard_size=max_shard_size)
@@ -684,7 +712,7 @@ def convert_zero_checkpoint_to_fp32_state_dict(checkpoint_dir,
684712
filename_to_tensors = state_dict_split.filename_to_tensors.items()
685713
for shard_file, tensors in tqdm(filename_to_tensors, desc="Saving checkpoint shards"):
686714
shard_state_dict = {tensor_name: state_dict[tensor_name] for tensor_name in tensors}
687-
shard_state_dict = to_torch_tensor(shard_state_dict)
715+
shard_state_dict = to_torch_tensor(shard_state_dict, dtype=dtype)
688716
output_path = os.path.join(output_dir, shard_file)
689717
if safe_serialization:
690718
save_file(shard_state_dict, output_path, metadata={"format": "pt"})
@@ -710,6 +738,22 @@ def convert_zero_checkpoint_to_fp32_state_dict(checkpoint_dir,
710738
f.write(content)
711739

712740

741+
def convert_zero_checkpoint_to_fp32_state_dict(checkpoint_dir,
742+
output_dir,
743+
max_shard_size="5GB",
744+
safe_serialization=False,
745+
tag=None,
746+
exclude_frozen_parameters=False):
747+
"""Backward-compatible fp32 checkpoint conversion."""
748+
return convert_zero_checkpoint_to_state_dict(checkpoint_dir,
749+
output_dir,
750+
dtype=torch.float32,
751+
max_shard_size=max_shard_size,
752+
safe_serialization=safe_serialization,
753+
tag=tag,
754+
exclude_frozen_parameters=exclude_frozen_parameters)
755+
756+
713757
def load_state_dict_from_zero_checkpoint(model, checkpoint_dir, tag=None):
714758
"""
715759
1. Put the provided model to cpu

deepspeed/utils/zero_to_torch.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# SPDX-License-Identifier: Apache-2.0
2+
# DeepSpeed Team
3+
4+
import argparse
5+
6+
import deepspeed.utils.zero_to_fp32 as zero_to_fp32
7+
8+
9+
def main(args=None):
10+
parser = argparse.ArgumentParser(
11+
description="Convert a DeepSpeed ZeRO checkpoint to float32, float16, or bfloat16 PyTorch weights.")
12+
parser.add_argument("checkpoint_dir",
13+
type=str,
14+
help="path to the desired checkpoint folder, e.g., path/checkpoint-12")
15+
parser.add_argument("output_dir", type=str, help="directory for the converted PyTorch state_dict files")
16+
parser.add_argument("--dtype",
17+
type=str,
18+
choices=sorted(zero_to_fp32.OUTPUT_DTYPE_NAMES),
19+
required=True,
20+
help="output tensor dtype")
21+
parser.add_argument("--max_shard_size",
22+
type=str,
23+
default="5GB",
24+
help="maximum size of each checkpoint shard, such as 5GB or 500MB")
25+
parser.add_argument("--safe_serialization",
26+
default=False,
27+
action='store_true',
28+
help="save with safetensors instead of PyTorch pickle serialization")
29+
parser.add_argument("-t",
30+
"--tag",
31+
type=str,
32+
default=None,
33+
help="checkpoint tag used as a unique identifier, e.g., global_step1")
34+
parser.add_argument("--exclude_frozen_parameters", action='store_true', help="exclude frozen parameters")
35+
parser.add_argument("-d", "--debug", action='store_true', help="enable debug output")
36+
parsed_args = parser.parse_args(args)
37+
38+
zero_to_fp32.debug = parsed_args.debug
39+
zero_to_fp32.convert_zero_checkpoint_to_state_dict(parsed_args.checkpoint_dir,
40+
parsed_args.output_dir,
41+
dtype=parsed_args.dtype,
42+
max_shard_size=parsed_args.max_shard_size,
43+
safe_serialization=parsed_args.safe_serialization,
44+
tag=parsed_args.tag,
45+
exclude_frozen_parameters=parsed_args.exclude_frozen_parameters)
46+
47+
48+
if __name__ == "__main__":
49+
main()

docs/_tutorials/zero.md

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -282,7 +282,35 @@ Saving fp32 state dict to pytorch_model.bin (total_numel=60506624)
282282

283283
The `zero_to_fp32.py` script gets created automatically when you save a checkpoint.
284284

285-
Note: currently this script uses 2x memory (general RAM) of the size of the final checkpoint.
285+
To reduce the exported checkpoint size, use the accompanying `zero_to_torch.py`
286+
script and choose `float16` or `bfloat16`. The original `zero_to_fp32.py`
287+
behavior remains unchanged.
288+
289+
```bash
290+
$ ./zero_to_torch.py . checkpoint-bf16 --dtype bfloat16
291+
```
292+
293+
The conversion reconstructs the ZeRO master weights in fp32, casts each output
294+
shard immediately before it is saved, and plans shard sizes using the selected
295+
dtype. Shared parameters remain shared in the exported state dict. The same
296+
functionality is available from Python:
297+
298+
```python
299+
import torch
300+
from deepspeed.utils.zero_to_fp32 import convert_zero_checkpoint_to_state_dict
301+
302+
convert_zero_checkpoint_to_state_dict(
303+
checkpoint_dir,
304+
output_dir,
305+
dtype=torch.bfloat16,
306+
)
307+
```
308+
309+
Questions and maintenance: [@gaoxiaomo](https://github.com/gaoxiaomo).
310+
311+
Note: fp32 conversion currently uses about 2x the final checkpoint size in CPU
312+
memory. Lower-precision export still reconstructs the fp32 master weights, so
313+
its peak memory is larger than 2x the final fp16/bf16 checkpoint size.
286314
{: .notice--info}
287315

288316
Alternatively, if you have plenty of spare CPU memory and instead of getting the file you want your model to be updated to its fp32 weights, you can do the following at the end of the training:

docs/code-docs/source/model-checkpointing.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ DeepSpeed provides routines for extracting fp32 weights from the saved ZeRO chec
2323

2424
.. autofunction:: deepspeed.utils.zero_to_fp32.convert_zero_checkpoint_to_fp32_state_dict
2525

26+
.. autofunction:: deepspeed.utils.zero_to_fp32.convert_zero_checkpoint_to_state_dict
27+
2628

2729
Avoiding ZeRO Checkpoint Bloat
2830
------------------------------

tests/unit/checkpoint/test_convert_checkpoint.py

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,68 @@
55

66
import torch
77
import torch.nn as nn
8+
import pytest
89

910
import deepspeed
10-
from deepspeed.utils.zero_to_fp32 import convert_zero_checkpoint_to_fp32_state_dict
11+
import deepspeed.utils.zero_to_fp32 as zero_to_fp32
12+
from deepspeed.utils.zero_to_fp32 import (convert_zero_checkpoint_to_fp32_state_dict,
13+
convert_zero_checkpoint_to_state_dict, to_torch_tensor)
14+
from deepspeed.utils.zero_to_torch import main as zero_to_torch_main
1115
from unit.common import DistributedTest
1216

1317

18+
def test_output_dtype_conversion_preserves_shared_tensors():
19+
tensor = torch.arange(16, dtype=torch.float32)
20+
state_dict = {"weight": tensor, "shared_weight": tensor}
21+
22+
converted = to_torch_tensor(state_dict, dtype="bf16")
23+
assert converted["weight"].dtype == torch.bfloat16
24+
assert id(converted["weight"]) == id(converted["shared_weight"])
25+
26+
empty = to_torch_tensor(state_dict, return_empty_tensor=True, dtype="fp16")
27+
assert empty["weight"].dtype == torch.float16
28+
assert id(empty["weight"]) == id(empty["shared_weight"])
29+
30+
with pytest.raises(ValueError, match="Unsupported output dtype"):
31+
to_torch_tensor(state_dict, dtype="int8")
32+
33+
34+
def test_checkpoint_file_output_dtype(monkeypatch, tmp_path):
35+
36+
def make_state_dict(*args, **kwargs):
37+
weight = torch.linspace(-1, 1, 4096, dtype=torch.float32)
38+
return {"weight": weight, "shared_weight": weight}
39+
40+
monkeypatch.setattr(zero_to_fp32, "get_fp32_state_dict_from_zero_checkpoint", make_state_dict)
41+
fp32_dir = tmp_path / "fp32"
42+
bf16_dir = tmp_path / "bf16"
43+
44+
convert_zero_checkpoint_to_fp32_state_dict("unused", fp32_dir, max_shard_size=None)
45+
convert_zero_checkpoint_to_state_dict("unused", bf16_dir, dtype="bf16", max_shard_size=None)
46+
47+
fp32_state_dict = torch.load(fp32_dir / "pytorch_model.bin")
48+
bf16_state_dict = torch.load(bf16_dir / "pytorch_model.bin")
49+
assert bf16_state_dict["weight"].dtype == torch.bfloat16
50+
assert id(bf16_state_dict["weight"]) == id(bf16_state_dict["shared_weight"])
51+
torch.testing.assert_close(bf16_state_dict["weight"].float(), fp32_state_dict["weight"], rtol=5e-3, atol=5e-3)
52+
assert (bf16_dir / "pytorch_model.bin").stat().st_size < (fp32_dir / "pytorch_model.bin").stat().st_size * 0.6
53+
54+
55+
def test_zero_to_torch_cli_passes_dtype(monkeypatch, tmp_path):
56+
call = {}
57+
58+
def record_conversion(checkpoint_dir, output_dir, **kwargs):
59+
call.update(checkpoint_dir=checkpoint_dir, output_dir=output_dir, **kwargs)
60+
61+
monkeypatch.setattr(zero_to_fp32, "convert_zero_checkpoint_to_state_dict", record_conversion)
62+
zero_to_torch_main(["checkpoint", str(tmp_path), "--dtype", "fp16", "--max_shard_size", "1GB"])
63+
64+
assert call["checkpoint_dir"] == "checkpoint"
65+
assert call["output_dir"] == str(tmp_path)
66+
assert call["dtype"] == "fp16"
67+
assert call["max_shard_size"] == "1GB"
68+
69+
1470
class ModelWithSharedWeights(nn.Module):
1571

1672
def __init__(self):
@@ -43,6 +99,8 @@ def test_convert_zero_checkpoint_to_fp32_state_dict(self, tmpdir):
4399
)
44100
ds_save_dir = tmpdir / "checkpoint_ds"
45101
deepspeed_engine.save_checkpoint(ds_save_dir, tag="checkpoint")
102+
assert (ds_save_dir / "zero_to_fp32.py").exists()
103+
assert (ds_save_dir / "zero_to_torch.py").exists()
46104

47105
model = ModelWithSharedWeights()
48106

@@ -58,3 +116,20 @@ def test_convert_zero_checkpoint_to_fp32_state_dict(self, tmpdir):
58116

59117
# load state_dict into model
60118
model.load_state_dict(state_dict, strict=True)
119+
120+
# Exporting in bfloat16 uses the target dtype for both shard planning
121+
# and serialization. At 300KB this model fits in one bf16 shard but
122+
# would be split if shard planning still counted fp32 bytes.
123+
bf16_save_dir = tmpdir / "checkpoint_bf16"
124+
convert_zero_checkpoint_to_state_dict(ds_save_dir, bf16_save_dir, dtype="bfloat16", max_shard_size="300KB")
125+
bf16_state_dict = torch.load(bf16_save_dir / 'pytorch_model.bin')
126+
assert not (bf16_save_dir / 'pytorch_model.bin.index.json').exists()
127+
128+
assert id(bf16_state_dict['layer1.weight']) == id(bf16_state_dict['layer2.weight'])
129+
for name, tensor in bf16_state_dict.items():
130+
assert tensor.dtype == torch.bfloat16
131+
torch.testing.assert_close(tensor.float(), state_dict[name], rtol=5e-3, atol=5e-3)
132+
133+
fp32_size = (fp32_save_dir / 'pytorch_model.bin').stat().st_size
134+
bf16_size = (bf16_save_dir / 'pytorch_model.bin').stat().st_size
135+
assert bf16_size < fp32_size * 0.6

0 commit comments

Comments
 (0)