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

122 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-11-15 07:18 +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 "F8_E8M0": "torch.float8_e8m0fnu", 

84 "F16": "torch.float16", 

85 "BF16": "torch.bfloat16", 

86 "F32": "torch.float32", 

87 "F64": "torch.float64", 

88 "C64": "torch.complex64", 

89} 

90 

91 

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

93 """ 

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

95 

96 Args: 

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

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

99 

100 Returns: 

101 Any: The parsed document. 

102 

103 Raises: 

104 ValueError: If the document is not JSON. 

105 """ 

106 try: 

107 return json.loads(document) 

108 except (TypeError, ValueError) as e: 

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

110 

111 

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

113 """ 

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

115 

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

117 

118 Args: 

119 path (Path): The safetensors file. 

120 

121 Returns: 

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

123 

124 Raises: 

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

126 JSON object. 

127 """ 

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

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

130 prefix = f.read(_HEADER_LENGTH_BYTES) 

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

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

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

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

135 if not isinstance(header, dict): 

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

137 return header 

138 

139 

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

141 """ 

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

143 

144 Args: 

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

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

147 

148 Returns: 

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

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

151 

152 Raises: 

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

154 """ 

155 extra = header.get(_HEADER_METADATA_KEY) 

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

157 return None 

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

159 if not isinstance(info, dict): 

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

161 return info 

162 

163 

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

165 """ 

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

167 

168 Args: 

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

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

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

172 

173 Returns: 

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

175 

176 Raises: 

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

178 """ 

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

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

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

182 code = entry.get("dtype") 

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

184 raise ValueError( 

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

186 ) 

187 return _SAFETENSORS_DTYPES[code], tuple(shape) 

188 

189 

190def _saved_offsets( 

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

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

193 """ 

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

195 

196 Args: 

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

198 :func:`_sharding_info` returns them. 

199 key (str): Name of the tensor. 

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

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

202 

203 Returns: 

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

205 

206 Raises: 

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

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

209 """ 

210 if sharding_info is None: 

211 return (0,) * ndim 

212 entry = sharding_info.get(key) 

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

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

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

216 raise ValueError( 

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

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

219 ) 

220 return tuple(offsets) 

221 

222 

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

224 """ 

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

226 

227 Args: 

228 path (Path): The safetensors file. 

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

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

231 

232 Yields: 

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

234 

235 Raises: 

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

237 starting anywhere but the origin. 

238 """ 

239 header = _read_header(path) 

240 sharding_info = _sharding_info(header, path) 

241 if sharded and sharding_info is None: 

242 raise ValueError( 

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

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

245 ) 

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

247 if key == _HEADER_METADATA_KEY: 

248 continue 

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

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

251 if not sharded and any(offsets): 

252 raise ValueError( 

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

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

255 ) 

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

257 

258 

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

260 """ 

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

262 

263 Args: 

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

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

266 

267 Returns: 

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

269 

270 Raises: 

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

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

273 """ 

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

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

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

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

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

279 if held != prod(size): 

280 raise ValueError( 

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

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

283 ) 

284 return size 

285 

286 

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

288 """ 

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

290 

291 Args: 

292 checkpoint_dir (Path): The checkpoint directory. 

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

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

295 

296 Returns: 

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

298 

299 Raises: 

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

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

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

303 """ 

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

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

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

307 for file_name in file_names: 

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

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

310 if index in storage_data: 

311 raise ValueError( 

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

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

314 ) 

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

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

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

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

319 state_dict_metadata = { 

320 key: TensorStorageMetadata( 

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

322 size=_tensor_size(key, key_chunks), 

323 chunks=key_chunks, 

324 ) 

325 for key, key_chunks in chunks.items() 

326 } 

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

328 

329 

330class HuggingFaceStorageReader(FileSystemReader): 

331 """ 

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

333 

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

335 

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

337 

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

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

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

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

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

343 

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

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

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

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

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

349 

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

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

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

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

354 """ 

355 

356 @dcp_timer_decorator 

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

358 """ 

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

360 

361 Args: 

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

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

364 

365 Returns: 

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

367 

368 Raises: 

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

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

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

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

373 """ 

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

375 

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

377 """ 

378 Name the files that make up the checkpoint. 

379 

380 Returns: 

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

382 

383 Raises: 

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

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

386 """ 

387 index_path = self.checkpoint_dir / _SAFE_WEIGHTS_INDEX_NAME 

388 if index_path.is_file(): 

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

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

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

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

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

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

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

396 raise FileNotFoundError( 

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

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

399 ) 

400 return [_SAFE_WEIGHTS_NAME] 

401 

402 

403class TorchShardedSafetensorsReader(FileSystemReader): 

404 """ 

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

406 

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

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

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

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

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

412 

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

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

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

416 for :class:`HuggingFaceStorageReader`. 

417 

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

419 the state dict may be. 

420 """ 

421 

422 @dcp_timer_decorator 

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

424 """ 

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

426 

427 Args: 

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

429 

430 Returns: 

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

432 

433 Raises: 

434 FileNotFoundError: If the directory holds no safetensors file. 

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

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

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

438 make up the whole tensor. 

439 """ 

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

441 if indexes: 

442 raise ValueError( 

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

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

445 ) 

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

447 if not files: 

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

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