Coverage for  / home / jenkins / .local / lib / python3.10 / site-packages / hyper_parallel / core / distributed_checkpoint / hf_storage.py: 98%

122 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-10-25 05:45 +0800

1# Copyright 2026 Huawei Technologies Co., Ltd 

2# 

3# Licensed under the Apache License, Version 2.0 (the "License"); 

4# you may not use this file except in compliance with the License. 

5# You may obtain a copy of the License at 

6# 

7# http://www.apache.org/licenses/LICENSE-2.0 

8# 

9# Unless required by applicable law or agreed to in writing, software 

10# distributed under the License is distributed on an "AS IS" BASIS, 

11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 

12# See the License for the specific language governing permissions and 

13# limitations under the License. 

14# ============================================================================ 

15"""Safetensors checkpoints written outside DCP, loaded through the DCP read path. 

16 

17Two kinds of directory are read, each by a reader of its own, since what makes a directory of one 

18kind valid makes one of the other kind wrong: 

19 

20* :class:`HuggingFaceStorageReader` reads a Hugging Face checkpoint, as ``save_pretrained`` writes 

21 one: whole tensors, each in exactly one file, which is ``model.safetensors`` or one of the files a 

22 ``model.safetensors.index.json`` names. 

23* :class:`TorchShardedSafetensorsReader` reads what torch's ``HuggingFaceStorageWriter`` writes with 

24 ``save_distributed=True`` before consolidating it: every rank's shard of a tensor in a file of its 

25 own, where the shard sits in the whole tensor recorded in the file's header, and no index. 

26 

27A safetensors file opens with a JSON header giving the dtype and shape of every tensor in it, and 

28that is all a load plans against. Both readers build the checkpoint :class:`Metadata` out of those 

29headers, where :class:`FileSystemReader` unpickles a ``.metadata`` file, and inherit the rest of the 

30load: planning, resharding, the replicated-shard broadcast and the reads themselves. 

31 

32Tensors are known by the names the files give them. Mapping those onto the names of a model, and 

33merging or splitting tensors laid out differently from it, is left to the layer above. 

34""" 

35import json 

36import os 

37import struct 

38from collections.abc import Iterator 

39from math import prod 

40from pathlib import Path 

41from typing import Any, Optional 

42 

43from hyper_parallel.core.distributed_checkpoint.filesystem_storage import FileSystemReader 

44from hyper_parallel.core.distributed_checkpoint.metadata import ( 

45 ChunkStorageMetadata, 

46 Metadata, 

47 MetadataIndex, 

48 TensorProperties, 

49 TensorStorageMetadata, 

50) 

51from hyper_parallel.core.distributed_checkpoint.storage import StorageInfo 

52from hyper_parallel.core.distributed_checkpoint.utils import dcp_timer_decorator 

53 

54SAFETENSORS_SUFFIX = ".safetensors" 

55SAFETENSORS_INDEX_SUFFIX = ".safetensors.index.json" 

56# torch's HuggingFaceStorageWriter records where in the whole tensor each tensor of a file starts: a 

57# JSON string under this key of the header's ``__metadata__``, mapping every tensor name in the file 

58# to ``{"saved_offsets": [...]}``. The format allows nothing but strings there, hence JSON in a string. 

59DCP_SHARDING_INFO_KEY = "DCP_SHARDING_INFO" 

60SAVED_OFFSETS_KEY = "saved_offsets" 

61 

62_SAFE_WEIGHTS_NAME = "model.safetensors" 

63_SAFE_WEIGHTS_INDEX_NAME = "model.safetensors.index.json" 

64_HEADER_METADATA_KEY = "__metadata__" 

65_HEADER_LENGTH_BYTES = 8 

66 

67# Every dtype code safetensors (0.8) reads into torch, with the dtype it is read as, spelled the 

68# way a DCP checkpoint records one: ``str(tensor.dtype)``. 

69_SAFETENSORS_DTYPES = { 

70 "BOOL": "torch.bool", 

71 "U8": "torch.uint8", 

72 "I8": "torch.int8", 

73 "U16": "torch.uint16", 

74 "I16": "torch.int16", 

75 "U32": "torch.uint32", 

76 "I32": "torch.int32", 

77 "U64": "torch.uint64", 

78 "I64": "torch.int64", 

79 "F8_E4M3": "torch.float8_e4m3fn", 

80 "F8_E4M3FNUZ": "torch.float8_e4m3fnuz", 

81 "F8_E5M2": "torch.float8_e5m2", 

82 "F8_E5M2FNUZ": "torch.float8_e5m2fnuz", 

83 "F16": "torch.float16", 

84 "BF16": "torch.bfloat16", 

85 "F32": "torch.float32", 

86 "F64": "torch.float64", 

87 "C64": "torch.complex64", 

88} 

89 

90 

91def _loads(document: Any, source: str) -> Any: 

92 """ 

93 Parse a JSON document, saying which one it was when it does not parse. 

94 

95 Args: 

96 document (Any): The text or bytes to parse. 

97 source (str): What the document is, for the error. 

98 

99 Returns: 

100 Any: The parsed document. 

101 

102 Raises: 

103 ValueError: If the document is not JSON. 

104 """ 

105 try: 

106 return json.loads(document) 

107 except (TypeError, ValueError) as e: 

108 raise ValueError(f"{source} is not valid JSON: {e}") from e 

109 

110 

111def _read_header(path: Path) -> dict[str, Any]: 

112 """ 

113 Read the JSON header a safetensors file opens with, and nothing past it. 

114 

115 The header is an eight-byte little-endian length followed by that many bytes of JSON. 

116 

117 Args: 

118 path (Path): The safetensors file. 

119 

120 Returns: 

121 dict[str, Any]: One entry per tensor, plus ``__metadata__`` when the writer left one. 

122 

123 Raises: 

124 ValueError: If the file is too short for the header it declares, or the header is not a 

125 JSON object. 

126 """ 

127 with open(path, "rb") as f: 

128 file_size = os.fstat(f.fileno()).st_size 

129 prefix = f.read(_HEADER_LENGTH_BYTES) 

130 header_length = struct.unpack("<Q", prefix)[0] if len(prefix) == _HEADER_LENGTH_BYTES else None 

131 if header_length is None or header_length > file_size - _HEADER_LENGTH_BYTES: 

132 raise ValueError(f"{path} is not a safetensors file: it is too short for its header.") 

133 header = _loads(f.read(header_length), f"The header of {path}") 

134 if not isinstance(header, dict): 

135 raise ValueError(f"The header of {path} is not a JSON object.") 

136 return header 

137 

138 

139def _sharding_info(header: dict[str, Any], path: Path) -> Optional[dict[str, Any]]: 

140 """ 

141 The offsets torch's Hugging Face writer recorded for the tensors of one file, if it did. 

142 

143 Args: 

144 header (dict[str, Any]): The header of the file. 

145 path (Path): The file, for the error. 

146 

147 Returns: 

148 Optional[dict[str, Any]]: ``{tensor name: {"saved_offsets": [...]}}``, or None for a file 

149 that records none, which is every file Hugging Face writes itself. 

150 

151 Raises: 

152 ValueError: If the recorded offsets are not a JSON object. 

153 """ 

154 extra = header.get(_HEADER_METADATA_KEY) 

155 if not isinstance(extra, dict) or DCP_SHARDING_INFO_KEY not in extra: 

156 return None 

157 info = _loads(extra[DCP_SHARDING_INFO_KEY], f"{DCP_SHARDING_INFO_KEY} of {path}") 

158 if not isinstance(info, dict): 

159 raise ValueError(f"{DCP_SHARDING_INFO_KEY} of {path} is not a JSON object.") 

160 return info 

161 

162 

163def _dtype_and_shape(entry: Any, key: str, path: Path) -> tuple[str, tuple[int, ...]]: 

164 """ 

165 The dtype, spelled as DCP spells it, and the shape of one tensor entry of a header. 

166 

167 Args: 

168 entry (Any): The header entry of the tensor. 

169 key (str): Name of the tensor, for the error. 

170 path (Path): The file, for the error. 

171 

172 Returns: 

173 tuple[str, tuple[int, ...]]: The dtype and the shape. 

174 

175 Raises: 

176 ValueError: If the entry has no valid shape, or a dtype safetensors does not read into torch. 

177 """ 

178 shape = entry.get("shape") if isinstance(entry, dict) else None 

179 if not isinstance(shape, list) or not all(isinstance(dim, int) and dim >= 0 for dim in shape): 

180 raise ValueError(f"Tensor {key!r} in {path} has no valid shape: {entry!r}.") 

181 code = entry.get("dtype") 

182 if not isinstance(code, str) or code not in _SAFETENSORS_DTYPES: 

183 raise ValueError( 

184 f"Tensor {key!r} in {path} has dtype {code!r}, which is none of {', '.join(_SAFETENSORS_DTYPES)}." 

185 ) 

186 return _SAFETENSORS_DTYPES[code], tuple(shape) 

187 

188 

189def _saved_offsets( 

190 sharding_info: Optional[dict[str, Any]], key: str, ndim: int, path: Path 

191) -> tuple[int, ...]: 

192 """ 

193 Where in the whole tensor the part of it stored under ``key`` in one file starts. 

194 

195 Args: 

196 sharding_info (Optional[dict[str, Any]]): The offsets the file records, as 

197 :func:`_sharding_info` returns them. 

198 key (str): Name of the tensor. 

199 ndim (int): Number of dimensions of the stored part. 

200 path (Path): The file, for the error. 

201 

202 Returns: 

203 tuple[int, ...]: The offsets, all zero for a file that records none. 

204 

205 Raises: 

206 ValueError: If the file records offsets but not a valid set for this tensor. Taking it to 

207 start at the origin instead could lay it over another shard of the same tensor. 

208 """ 

209 if sharding_info is None: 

210 return (0,) * ndim 

211 entry = sharding_info.get(key) 

212 offsets = entry.get(SAVED_OFFSETS_KEY) if isinstance(entry, dict) else None 

213 if (not isinstance(offsets, list) or len(offsets) != ndim 

214 or not all(isinstance(offset, int) and offset >= 0 for offset in offsets)): 

215 raise ValueError( 

216 f"{DCP_SHARDING_INFO_KEY} of {path} has no valid {SAVED_OFFSETS_KEY} for the " 

217 f"{ndim}-dimensional tensor {key!r}: {entry!r}." 

218 ) 

219 return tuple(offsets) 

220 

221 

222def _read_chunks(path: Path, sharded: bool) -> Iterator[tuple[str, str, ChunkStorageMetadata]]: 

223 """ 

224 Every tensor one safetensors file holds, as a chunk of the tensor its name stands for. 

225 

226 Args: 

227 path (Path): The safetensors file. 

228 sharded (bool): Whether the file is a shard written by torch, which has to record where 

229 each of its tensors starts. Otherwise every tensor in it has to be whole. 

230 

231 Yields: 

232 tuple[str, str, ChunkStorageMetadata]: Name, dtype, and where in the whole tensor it sits. 

233 

234 Raises: 

235 ValueError: If a shard records no offsets, or a file of whole tensors records a tensor as 

236 starting anywhere but the origin. 

237 """ 

238 header = _read_header(path) 

239 sharding_info = _sharding_info(header, path) 

240 if sharded and sharding_info is None: 

241 raise ValueError( 

242 f"{path} records no {DCP_SHARDING_INFO_KEY}, so it is not a shard written by torch's " 

243 f"HuggingFaceStorageWriter; read a Hugging Face checkpoint with HuggingFaceStorageReader." 

244 ) 

245 for key, entry in header.items(): 

246 if key == _HEADER_METADATA_KEY: 

247 continue 

248 dtype, shape = _dtype_and_shape(entry, key, path) 

249 offsets = _saved_offsets(sharding_info, key, len(shape), path) 

250 if not sharded and any(offsets): 

251 raise ValueError( 

252 f"Tensor {key!r} in {path} is the shard at offsets {offsets} of a larger tensor; read a " 

253 f"directory of shards written by torch with TorchShardedSafetensorsReader." 

254 ) 

255 yield key, dtype, ChunkStorageMetadata(offsets=offsets, sizes=shape) 

256 

257 

258def _tensor_size(key: str, chunks: list[ChunkStorageMetadata]) -> tuple[int, ...]: 

259 """ 

260 The size of the tensor a set of chunks makes up, checking that they make up exactly that. 

261 

262 Args: 

263 key (str): Name of the tensor, for the error. 

264 chunks (list[ChunkStorageMetadata]): Every chunk of it, no two at the same offsets. 

265 

266 Returns: 

267 tuple[int, ...]: The smallest size that holds every chunk. 

268 

269 Raises: 

270 ValueError: If the chunks differ in number of dimensions, or hold more or fewer elements 

271 than that size does: a file of the checkpoint is missing, or two chunks overlap. 

272 """ 

273 ndim = len(chunks[0].sizes) 

274 if any(len(chunk.sizes) != ndim for chunk in chunks): 

275 raise ValueError(f"The chunks of tensor {key!r} differ in number of dimensions.") 

276 size = tuple(max(chunk.offsets[dim] + chunk.sizes[dim] for chunk in chunks) for dim in range(ndim)) 

277 held = sum(prod(chunk.sizes) for chunk in chunks) 

278 if held != prod(size): 

279 raise ValueError( 

280 f"The chunks of tensor {key!r} hold {held} elements between them, where a tensor of size " 

281 f"{size} has {prod(size)}: a file of the checkpoint is missing, or two chunks overlap." 

282 ) 

283 return size 

284 

285 

286def _describe_checkpoint(checkpoint_dir: Path, file_names: list[str], sharded: bool) -> Metadata: 

287 """ 

288 Build the metadata of a checkpoint out of the headers of its safetensors files. 

289 

290 Args: 

291 checkpoint_dir (Path): The checkpoint directory. 

292 file_names (list[str]): The files of the checkpoint, relative to ``checkpoint_dir``. 

293 sharded (bool): Whether the files are shards written by torch; see :func:`_read_chunks`. 

294 

295 Returns: 

296 Metadata: Size, dtype and chunks of every tensor, and the file each chunk is in. 

297 

298 Raises: 

299 ValueError: If a file is not of the kind ``sharded`` says, a header is malformed, or the 

300 chunks of one tensor do not fit together: the same chunk twice, chunks in different 

301 dtypes, or chunks that do not make up the whole tensor. 

302 """ 

303 chunks: dict[str, list[ChunkStorageMetadata]] = {} 

304 dtypes: dict[str, str] = {} 

305 storage_data: dict[MetadataIndex, StorageInfo] = {} 

306 for file_name in file_names: 

307 for key, dtype, chunk in _read_chunks(checkpoint_dir / file_name, sharded): 

308 index = MetadataIndex(fqn=key, offset=chunk.offsets) 

309 if index in storage_data: 

310 raise ValueError( 

311 f"Tensor {key!r} at offsets {chunk.offsets} is in both " 

312 f"{storage_data[index].relative_path} and {file_name}." 

313 ) 

314 if dtypes.setdefault(key, dtype) != dtype: 

315 raise ValueError(f"Tensor {key!r} is {dtype} in {file_name} but {dtypes[key]} in another file.") 

316 chunks.setdefault(key, []).append(chunk) 

317 storage_data[index] = StorageInfo(relative_path=file_name, offset=0, length=-1, tensor_key=key) 

318 state_dict_metadata = { 

319 key: TensorStorageMetadata( 

320 properties=TensorProperties(dtype=dtypes[key]), 

321 size=_tensor_size(key, key_chunks), 

322 chunks=key_chunks, 

323 ) 

324 for key, key_chunks in chunks.items() 

325 } 

326 return Metadata(state_dict_metadata=state_dict_metadata, storage_data=storage_data) 

327 

328 

329class HuggingFaceStorageReader(FileSystemReader): 

330 """ 

331 Storage reader for a Hugging Face checkpoint: whole tensors, each in exactly one file. 

332 

333 Pass it to :func:`load` in place of a checkpoint path:: 

334 

335 load(model.state_dict(), storage_reader=HuggingFaceStorageReader("/models/Qwen3-8B")) 

336 

337 The files read are the ones ``model.safetensors.index.json`` names, or ``model.safetensors`` 

338 alone when there is no index. Nothing else in the directory is read, such as a second copy of 

339 the weights in a file of another name. A file whose header records offsets the way torch's 

340 HuggingFaceStorageWriter does is read as long as every offset is zero: anything else is a shard 

341 of a larger tensor, and a directory of those is for :class:`TorchShardedSafetensorsReader`. 

342 

343 The state dict is filled by name, as from any DCP checkpoint: each of its keys has to name a 

344 tensor in the files, at the same global shape. Beyond that the target is free. A DTensor may 

345 be laid out any way, and each rank reads only its own slice of the file. The dtype may differ 

346 from the file's, and the copy into the target converts it. Ranks holding the same slice read 

347 it once and broadcast it, as :func:`load` does for any checkpoint. 

348 

349 A key the files do not have fails the load, unless the planner is 

350 ``StandardLoadPlanner(allow_partial_load=True)``. A model with tied weights needs that, since 

351 Hugging Face stores a tied tensor once. Quantized tensors are read as they are stored: an FP8 

352 weight arrives as FP8 values, and its scales are tensors of their own. 

353 """ 

354 

355 @dcp_timer_decorator 

356 def load_metadata(self, **kwargs: Any) -> Metadata: 

357 """ 

358 Describe the checkpoint from the headers of its safetensors files, and nothing past them. 

359 

360 Args: 

361 **kwargs (Any): Ignored. :func:`load` asks again with ``rank`` when a first ask finds 

362 nothing, and a Hugging Face checkpoint has no rank-local metadata to find instead. 

363 

364 Returns: 

365 Metadata: Size, dtype and file of every tensor. 

366 

367 Raises: 

368 FileNotFoundError: If the directory holds neither the index nor the single weights 

369 file, or a file the index names is not there. 

370 ValueError: If the index or a header is malformed, a dtype has no torch counterpart, a 

371 tensor is a shard of a larger one, or two files hold the same tensor. 

372 """ 

373 return _describe_checkpoint(self.checkpoint_dir, self._weight_files(), sharded=False) 

374 

375 def _weight_files(self) -> list[str]: 

376 """ 

377 Name the files that make up the checkpoint. 

378 

379 Returns: 

380 list[str]: File names relative to the checkpoint directory, sorted. 

381 

382 Raises: 

383 FileNotFoundError: If the directory holds neither the index nor the single weights file. 

384 ValueError: If the index does not map tensor names to file names. 

385 """ 

386 index_path = self.checkpoint_dir / _SAFE_WEIGHTS_INDEX_NAME 

387 if index_path.is_file(): 

388 index = _loads(index_path.read_bytes(), str(index_path)) 

389 weight_map = index.get("weight_map") if isinstance(index, dict) else None 

390 if (not isinstance(weight_map, dict) or not weight_map 

391 or not all(isinstance(file_name, str) for file_name in weight_map.values())): 

392 raise ValueError(f"{index_path} has no weight_map mapping tensor names to file names.") 

393 return sorted(set(weight_map.values())) 

394 if not (self.checkpoint_dir / _SAFE_WEIGHTS_NAME).is_file(): 

395 raise FileNotFoundError( 

396 f"Neither {_SAFE_WEIGHTS_INDEX_NAME} nor {_SAFE_WEIGHTS_NAME} found in {self.checkpoint_dir}. " 

397 f"A directory of shards written by torch is read with TorchShardedSafetensorsReader." 

398 ) 

399 return [_SAFE_WEIGHTS_NAME] 

400 

401 

402class TorchShardedSafetensorsReader(FileSystemReader): 

403 """ 

404 Storage reader for the shards torch's ``HuggingFaceStorageWriter`` writes with ``save_distributed``. 

405 

406 Saving that way without consolidating, every rank writes its shard of each tensor to a file of 

407 its own, ``shard-00001-model-00001-of-00001.safetensors`` and so on, under the tensor's own 

408 name, and records in the file's header where in the whole tensor the shard sits. No index is 

409 written, since a tensor is spread over several files. torchtitan writes such a directory as the 

410 ``sharded`` directory of a checkpoint it saves in Hugging Face format, before consolidating it. 

411 

412 Every safetensors file in the directory is read, and every one has to record those offsets. 

413 The shards of a tensor are its chunks, and have to make up the whole tensor between them, 

414 leaving no gap and overlapping nowhere. A directory with an index holds whole tensors, and is 

415 for :class:`HuggingFaceStorageReader`. 

416 

417 Loading from it is as from any DCP checkpoint; see :class:`HuggingFaceStorageReader` for what 

418 the state dict may be. 

419 """ 

420 

421 @dcp_timer_decorator 

422 def load_metadata(self, **kwargs: Any) -> Metadata: 

423 """ 

424 Describe the checkpoint from the headers of its shard files, and nothing past them. 

425 

426 Args: 

427 **kwargs (Any): Ignored, as :meth:`HuggingFaceStorageReader.load_metadata` ignores them. 

428 

429 Returns: 

430 Metadata: Size, dtype and chunks of every tensor, and the file each chunk is in. 

431 

432 Raises: 

433 FileNotFoundError: If the directory holds no safetensors file. 

434 ValueError: If the directory has an index, a file records no offsets, a header is 

435 malformed, a dtype has no torch counterpart, or the shards of one tensor do not fit 

436 together: the same shard twice, shards in different dtypes, or shards that do not 

437 make up the whole tensor. 

438 """ 

439 indexes = sorted(path.name for path in self.checkpoint_dir.glob(f"*{SAFETENSORS_INDEX_SUFFIX}")) 

440 if indexes: 

441 raise ValueError( 

442 f"{self.checkpoint_dir} has {indexes[0]}, which a checkpoint of whole tensors has and one " 

443 f"of shards does not; read it with HuggingFaceStorageReader." 

444 ) 

445 files = sorted(path.name for path in self.checkpoint_dir.glob(f"*{SAFETENSORS_SUFFIX}")) 

446 if not files: 

447 raise FileNotFoundError(f"No *{SAFETENSORS_SUFFIX} file found in {self.checkpoint_dir}.") 

448 return _describe_checkpoint(self.checkpoint_dir, files, sharded=True)