Skip to content

Commit 466ed80

Browse files
authored
【FlexCheckpoint】fix safetensors ckpt integrity (PaddlePaddle#78208)
* fix safetensors ckpt integrity * add test for coverage
1 parent e9a43b2 commit 466ed80

3 files changed

Lines changed: 138 additions & 14 deletions

File tree

python/paddle/distributed/flex_checkpoint/dcp/load_state_dict.py

Lines changed: 57 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,8 @@ def get_checkpoint_files(
8585
# if unique_id is None, all file ends with .metadata and .distcp is returned
8686
if unique_id is None:
8787
unique_id = ''
88+
if use_dist is None:
89+
use_dist = paddle.distributed.get_world_size() > 1
8890
global PATH_TO_CHECKPOINT_FILES
8991
if use_cache and path in PATH_TO_CHECKPOINT_FILES:
9092
return PATH_TO_CHECKPOINT_FILES[path]
@@ -106,26 +108,44 @@ def get_checkpoint_files(
106108
with open(index_file_path, "r") as f:
107109
index_data = json.load(f)
108110
if "weight_map" in index_data:
109-
from safetensors.numpy import safe_open
110-
111111
mapping_key_to_safetensors_file = index_data["weight_map"]
112-
global_safetensors_files = []
112+
# All files referenced in the index
113+
expected_files_in_index = set(
114+
mapping_key_to_safetensors_file.values()
115+
)
116+
117+
# Gather safetensors files visible on each rank, then take union
118+
global_safetensors_files_list = []
113119
if use_dist:
114120
paddle.distributed.all_gather_object(
115-
global_safetensors_files,
121+
global_safetensors_files_list,
116122
safetensors_files,
117123
process_group,
118124
)
119-
global_safetensors_files = list(
120-
{
121-
file
122-
for files in global_safetensors_files
123-
for file in files
124-
}
125-
)
125+
global_safetensors_files = {
126+
file
127+
for files in global_safetensors_files_list
128+
for file in files
129+
}
126130
else:
127-
global_safetensors_files = safetensors_files
128-
for file in global_safetensors_files:
131+
global_safetensors_files = set(safetensors_files)
132+
133+
# Check that every file referenced in the index is visible on at least one rank
134+
missing_files = (
135+
expected_files_in_index - global_safetensors_files
136+
)
137+
assert len(missing_files) == 0, (
138+
f"The following safetensors files are referenced in '{index_file_name}' "
139+
f"but not found on any rank: {sorted(missing_files)}"
140+
)
141+
142+
# Check local files: every key in a locally accessible file must be
143+
# consistent with the index mapping (only open files this rank can see).
144+
# Meanwhile, collect actual keys present locally for the reverse check below.
145+
from safetensors.numpy import safe_open
146+
147+
local_actual_keys = set()
148+
for file in safetensors_files:
129149
if file.endswith(".safetensors"):
130150
file_path = os.path.join(path, file)
131151
with safe_open(file_path, framework="np") as f:
@@ -139,6 +159,30 @@ def get_checkpoint_files(
139159
assert expected_file == file, (
140160
f"Key '{key}' is mapped to file '{expected_file}' in index, but found in file '{file}'"
141161
)
162+
local_actual_keys.add(key)
163+
164+
# Reverse check: every key declared in index must actually exist in
165+
# some safetensors file across the cluster.
166+
expected_keys_in_index = set(
167+
mapping_key_to_safetensors_file.keys()
168+
)
169+
global_actual_keys_list = []
170+
if use_dist:
171+
paddle.distributed.all_gather_object(
172+
global_actual_keys_list,
173+
local_actual_keys,
174+
process_group,
175+
)
176+
global_actual_keys = set().union(*global_actual_keys_list)
177+
else:
178+
global_actual_keys = local_actual_keys
179+
missing_keys_in_files = (
180+
expected_keys_in_index - global_actual_keys
181+
)
182+
assert len(missing_keys_in_files) == 0, (
183+
f"The following keys are declared in '{index_file_name}' weight_map "
184+
f"but not found in any safetensors file: {sorted(missing_keys_in_files)}"
185+
)
142186

143187
if len(metadata_files) == 0:
144188
logger.info(

test/auto_parallel/hybrid_strategy/save_safetensors_load_fc.py

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
import json
1516
import os
1617

1718
import paddle
@@ -153,5 +154,70 @@ def test_save_safetensors_load_fc():
153154
dist.barrier()
154155

155156

157+
def create_index_json(ckpt_path):
158+
"""Create model.safetensors.index.json that maps keys to their safetensors files."""
159+
index_data = {
160+
"weight_map": {
161+
"tensor1": "tensor1.safetensors",
162+
"tensor2": "tensor2.safetensors",
163+
}
164+
}
165+
index_file_path = os.path.join(ckpt_path, "model.safetensors.index.json")
166+
if dist.get_rank() == 0:
167+
with open(index_file_path, "w") as f:
168+
json.dump(index_data, f)
169+
dist.barrier()
170+
171+
172+
def test_save_safetensors_load_fc_with_index():
173+
"""Test saving safetensors and loading with flex checkpoint when model.safetensors.index.json exists."""
174+
ckpt_path = os.getenv("ckpt_path")
175+
dist.init_parallel_env()
176+
177+
save_safetensors_to_ranks(ckpt_path)
178+
179+
# Create index json to exercise the integrity check branch
180+
create_index_json(ckpt_path)
181+
182+
sharded_state_dict = create_sharded_state_dict_for_loading()
183+
184+
from paddle.distributed.flex_checkpoint.dcp.load_state_dict import (
185+
load_state_dict,
186+
)
187+
188+
load_state_dict(sharded_state_dict, ckpt_path, safetensors=True)
189+
190+
loaded_tensor1 = sharded_state_dict["tensor1"].local_tensor
191+
loaded_tensor2 = sharded_state_dict["tensor2"].local_tensor
192+
193+
if dist.get_rank() == 0:
194+
expected_tensor1 = paddle.to_tensor([[0], [2]], dtype='float32')
195+
expected_tensor2 = paddle.to_tensor([[4], [6]], dtype='float32')
196+
197+
assert paddle.allclose(loaded_tensor1, expected_tensor1), (
198+
f"Rank 0 tensor1 mismatch: got {loaded_tensor1}, expected {expected_tensor1}"
199+
)
200+
assert paddle.allclose(loaded_tensor2, expected_tensor2), (
201+
f"Rank 0 tensor2 mismatch: got {loaded_tensor2}, expected {expected_tensor2}"
202+
)
203+
204+
elif dist.get_rank() == 1:
205+
expected_tensor1 = paddle.to_tensor([[1], [3]], dtype='float32')
206+
expected_tensor2 = paddle.to_tensor([[5], [7]], dtype='float32')
207+
208+
assert paddle.allclose(loaded_tensor1, expected_tensor1), (
209+
f"Rank 1 tensor1 mismatch: got {loaded_tensor1}, expected {expected_tensor1}"
210+
)
211+
assert paddle.allclose(loaded_tensor2, expected_tensor2), (
212+
f"Rank 1 tensor2 mismatch: got {loaded_tensor2}, expected {expected_tensor2}"
213+
)
214+
215+
dist.barrier()
216+
217+
156218
if __name__ == "__main__":
157-
test_save_safetensors_load_fc()
219+
test_func = os.getenv("test_func", "test_save_safetensors_load_fc")
220+
if test_func == "test_save_safetensors_load_fc_with_index":
221+
test_save_safetensors_load_fc_with_index()
222+
else:
223+
test_save_safetensors_load_fc()

test/auto_parallel/hybrid_strategy/test_save_load_state_dict.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,20 @@ def test_save_safetensors_load_fc(self):
132132
)
133133
ckpt_path.cleanup()
134134

135+
def test_save_safetensors_load_fc_with_index(self):
136+
"""Test saving safetensors files and loading with flex checkpoint when model.safetensors.index.json exists."""
137+
ckpt_path = tempfile.TemporaryDirectory()
138+
super().setUp(num_of_devices=2, timeout=120, nnode=1)
139+
self.run_test_case(
140+
"save_safetensors_load_fc.py",
141+
user_defined_envs={
142+
"device_num": "2",
143+
"ckpt_path": ckpt_path.name,
144+
"test_func": "test_save_safetensors_load_fc_with_index",
145+
},
146+
)
147+
ckpt_path.cleanup()
148+
135149
def test_save_load_state_dict_with_aoa_config_reverse(self):
136150
"""Test saving state dict and loading with flex checkpoint."""
137151
ckpt_path = tempfile.TemporaryDirectory()

0 commit comments

Comments
 (0)