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

217 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-08-21 04:29 +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"""File system storage implementations for checkpoint save and load.""" 

16import os 

17import pickle 

18from collections import Counter 

19from pathlib import Path 

20from typing import Any, Optional, Union 

21 

22from safetensors import safe_open 

23 

24from hyper_parallel.core.distributed_checkpoint.metadata import Metadata, MetadataIndex 

25from hyper_parallel.core.distributed_checkpoint.planner import ( 

26 LoadPlan, 

27 LoadPlanner, 

28 ReadItem, 

29 SavePlan, 

30 SavePlanner, 

31 WriteItem, 

32) 

33from hyper_parallel.core.distributed_checkpoint.storage import ( 

34 StorageInfo, 

35 StorageReader, 

36 StorageWriter, 

37 WriteResult, 

38 METADATA_FILE_NAME, 

39) 

40from hyper_parallel.core.distributed_checkpoint.util import narrow_tensor_by_index 

41from hyper_parallel.platform import get_platform 

42from hyper_parallel.platform.platform import PlatformType 

43 

44 

45class FileSystemWriter(StorageWriter): 

46 """ 

47 File system storage writer implementation. 

48 

49 Saves checkpoint data to the local file system, organizing tensors 

50 into safetensors files and bytes into separate files. 

51 """ 

52 

53 def __init__(self, checkpoint_dir: Union[Path, str]): 

54 self.checkpoint_dir = Path(checkpoint_dir) if isinstance(checkpoint_dir, str) else checkpoint_dir 

55 self.checkpoint_dir.mkdir(parents=True, exist_ok=True) 

56 self.rank: int = 0 

57 self.is_coordinator: bool = False 

58 self.use_collectives: bool = True 

59 

60 def initialize_writer(self, checkpoint_id: Optional[Union[Path, str]] = None) -> None: 

61 """ 

62 Initialize storage writer with new checkpoint directory. 

63 

64 Args: 

65 checkpoint_id (Optional[Union[Path, str]]): New checkpoint directory path. Default None. 

66 """ 

67 if checkpoint_id: 

68 self.checkpoint_dir = Path(checkpoint_id) if isinstance(checkpoint_id, str) else checkpoint_id 

69 self.checkpoint_dir.mkdir(parents=True, exist_ok=True) 

70 

71 def configure_writer(self, is_coordinator: bool, **kwargs) -> None: 

72 """ 

73 Configure storage writer. 

74 

75 Args: 

76 is_coordinator (bool): Whether this rank is the coordinator. 

77 **kwargs: Additional keyword arguments (e.g., rank, use_collectives). 

78 """ 

79 self.is_coordinator = is_coordinator 

80 self.rank = kwargs.get("rank") if "rank" in kwargs else get_platform().get_rank() 

81 self.use_collectives = kwargs.get("use_collectives", True) 

82 

83 def optimize_local_plan(self, plan: SavePlan) -> SavePlan: 

84 """ 

85 Optimize local plan. 

86 

87 Args: 

88 plan (SavePlan): Local save plan. 

89 

90 Returns: 

91 SavePlan: Optimized local plan. 

92 """ 

93 return plan 

94 

95 def optimize_global_plan(self, plans: list[SavePlan]) -> list[SavePlan]: 

96 """ 

97 Optimize global plan. 

98 

99 Args: 

100 plans (list[SavePlan]): List of local plans from all ranks. 

101 

102 Returns: 

103 list[SavePlan]: Optimized global plans. 

104 """ 

105 return plans 

106 

107 

108 def _serialize_bytes_item(self, item: WriteItem, planner: SavePlanner) -> bytes: 

109 """Serialize a BYTE_IO item payload while preserving current behavior.""" 

110 data = planner.get_data(item) 

111 if isinstance(data, bytes): 

112 return data 

113 return pickle.dumps(data) 

114 

115 

116 def _write_bytes_items(self, plan: SavePlan, planner: SavePlanner) -> list[WriteResult]: 

117 """ 

118 Write all BYTE_IO items into one per-rank bytes file. 

119 

120 Args: 

121 plan (SavePlan): Save plan containing WriteItems. 

122 planner (SavePlanner): Save planner used to resolve runtime data. 

123 

124 Returns: 

125 list[WriteResult]: Write results for BYTE_IO items. 

126 """ 

127 byte_items = [item for item in plan.items if item.type.value == "byte_io"] 

128 if not byte_items: 

129 return [] 

130 

131 file_name = f"_rank{self.rank}_.bytes" 

132 file_path = self.checkpoint_dir / file_name 

133 

134 results: list[WriteResult] = [] 

135 

136 with open(file_path, "wb") as f: 

137 for item in byte_items: 

138 payload = self._serialize_bytes_item(item, planner) 

139 offset = f.tell() 

140 f.write(payload) 

141 length = len(payload) 

142 storage_info = StorageInfo( 

143 relative_path=file_name, 

144 offset=offset, 

145 length=length, 

146 ) 

147 results.append( 

148 WriteResult( 

149 index=item.index, 

150 storage_data=storage_info, 

151 ) 

152 ) 

153 

154 return results 

155 

156 def _collect_tensors( 

157 self, plan: SavePlan, planner: SavePlanner 

158 ) -> tuple[dict[str, Any], dict[MetadataIndex, str]]: 

159 """ 

160 Collect tensor data from planner runtime lookup. 

161 

162 Args: 

163 plan (SavePlan): Save plan containing WriteItems. 

164 planner (SavePlanner): Save planner. 

165 

166 Returns: 

167 tuple[dict[str, Any], dict[MetadataIndex, str]]: Tensor data keyed by 

168 physical safetensors key, plus logical index-to-key mapping. 

169 

170 Raises: 

171 RuntimeError: If tensor data cannot be resolved for an item. 

172 """ 

173 tensor_items = [ 

174 item for item in plan.items 

175 if item.type.value == "tensor" and item.tensor_data 

176 ] 

177 fqn_counts = Counter(item.index.fqn for item in tensor_items) 

178 reserved_keys = set(fqn_counts) 

179 used_keys: set[str] = set() 

180 next_chunk_index: dict[str, int] = {} 

181 tensor_dict: dict[str, Any] = {} 

182 tensor_keys: dict[MetadataIndex, str] = {} 

183 

184 for item in tensor_items: 

185 tensor = planner.get_data(item) 

186 if tensor is None: 

187 raise RuntimeError( 

188 f"Tensor data could not be resolved for index {item.index}. " 

189 f"FQN: {item.index.fqn}" 

190 ) 

191 

192 fqn = item.index.fqn 

193 tensor_key = fqn 

194 if fqn_counts[fqn] > 1: 

195 chunk_index = next_chunk_index.get(fqn, 0) 

196 next_chunk_index[fqn] = chunk_index + 1 

197 tensor_key = f"{fqn}.__dcp_chunk_{chunk_index}" 

198 while tensor_key in reserved_keys or tensor_key in used_keys: 

199 tensor_key += "_" 

200 

201 used_keys.add(tensor_key) 

202 tensor_dict[tensor_key] = tensor 

203 tensor_keys[item.index] = tensor_key 

204 return tensor_dict, tensor_keys 

205 

206 def _write_tensors( 

207 self, 

208 plan: SavePlan, 

209 tensor_dict: dict[str, Any], 

210 tensor_keys: dict[MetadataIndex, str], 

211 ) -> list[WriteResult]: 

212 """ 

213 Write all tensors to safetensors file and create WriteResults. 

214 

215 Args: 

216 plan (SavePlan): Save plan containing WriteItems. 

217 tensor_dict (dict[str, Any]): Dictionary mapping physical keys to tensor data. 

218 tensor_keys (dict[MetadataIndex, str]): Logical index-to-key mapping. 

219 

220 Returns: 

221 list[WriteResult]: List of write results for tensor items. 

222 """ 

223 if not tensor_dict: 

224 return [] 

225 

226 platform = get_platform() 

227 file_name = f"_rank{self.rank}_.safetensors" 

228 file_path = self.checkpoint_dir / file_name 

229 platform.save_checkpoint(tensor_dict, str(file_path)) 

230 

231 # Record StorageInfo for each tensor 

232 # Note: we don't know per-tensor byte offsets, so offset=0, length=-1 

233 results: list[WriteResult] = [] 

234 for item in plan.items: 

235 if item.type.value == "tensor" and item.tensor_data: 

236 storage_info = StorageInfo( 

237 relative_path=file_name, 

238 offset=0, 

239 length=-1, 

240 tensor_key=tensor_keys[item.index], 

241 ) 

242 results.append( 

243 WriteResult( 

244 index=item.index, 

245 storage_data=storage_info, 

246 ) 

247 ) 

248 return results 

249 

250 def execute_write(self, plan: SavePlan, planner: SavePlanner) -> list[WriteResult]: 

251 """ 

252 Write data to storage and return per-item storage metadata. 

253 

254 Group tensors into safetensors files and bytes into separate files, recording StorageInfo for each item. 

255 

256 Args: 

257 plan (SavePlan): Save plan containing WriteItems. 

258 planner (SavePlanner): Save planner. 

259 

260 Returns: 

261 list[WriteResult]: List of write results with storage metadata. 

262 """ 

263 results: list[WriteResult] = [] 

264 

265 # Write all BYTE_IO items into one file per rank 

266 results.extend(self._write_bytes_items(plan, planner)) 

267 

268 # Collect and write tensors 

269 tensor_dict, tensor_keys = self._collect_tensors(plan, planner) 

270 results.extend(self._write_tensors(plan, tensor_dict, tensor_keys)) 

271 

272 return results 

273 

274 def finalize_checkpoint(self, metadata: Metadata, results: list[list[WriteResult]]) -> None: 

275 """ 

276 Finish writing checkpoint and populate metadata.storage_data. 

277 

278 When use_collectives=True: only coordinator saves global metadata to .metadata. 

279 When use_collectives=False: each rank saves its own metadata to .rank{rank}_metadata, 

280 no cross-rank interaction. 

281 

282 Args: 

283 metadata (Metadata): Checkpoint metadata to update. 

284 results (list[list[WriteResult]]): Write results from all ranks (or single rank when use_collectives=False). 

285 """ 

286 should_save = not self.use_collectives or (self.use_collectives and self.is_coordinator) 

287 if not should_save: 

288 return 

289 

290 # Build storage_data: map MetadataIndex -> StorageInfo 

291 storage_md: dict[MetadataIndex, StorageInfo] = {} 

292 for wr_list in results: 

293 for wr in wr_list: 

294 storage_md[wr.index] = wr.storage_data 

295 metadata.storage_data = storage_md 

296 

297 # Save metadata file 

298 if self.use_collectives: 

299 metadata_file = self.checkpoint_dir / METADATA_FILE_NAME 

300 else: 

301 metadata_file = self.checkpoint_dir / f"{self.rank}{METADATA_FILE_NAME}" 

302 with open(metadata_file, "wb") as f: 

303 pickle.dump(metadata, f) 

304 

305 

306def _copy_tensor_to_target( 

307 req: ReadItem, tensor: Any, target_tensor: Any, planner: LoadPlanner 

308) -> None: 

309 """ 

310 Copy tensor data to target tensor and commit. 

311 

312 Args: 

313 req (ReadItem): ReadItem request. 

314 tensor (Any): Source tensor (tensor-like object). 

315 target_tensor (Any): Target tensor (tensor-like object). 

316 planner (LoadPlanner): Load planner for committing. 

317 """ 

318 if hasattr(target_tensor, "copy_"): 

319 target_tensor.copy_(tensor) 

320 planner.apply_tensor(req, target_tensor) 

321 else: 

322 # mindspore or non-tensor: copy via commit path 

323 planner.apply_tensor(req, tensor) 

324 

325 

326def _load_bytes_file( 

327 path: str, 

328 reqs: list[ReadItem], 

329 planner: LoadPlanner, 

330 storage_data: dict[MetadataIndex, StorageInfo], 

331) -> None: 

332 """ 

333 Load bytes from a file. 

334 

335 Args: 

336 path (str): Path to the bytes file. 

337 reqs (list[ReadItem]): List of ReadItems for this file. 

338 planner (LoadPlanner): Load planner for loading bytes. 

339 """ 

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

341 for req in reqs: 

342 storage_info = storage_data.get(req.storage_index) 

343 if storage_info is None: 

344 raise KeyError( 

345 f"StorageInfo not found for index {req.storage_index}" 

346 ) 

347 f.seek(storage_info.offset) 

348 value = f.read(storage_info.length) 

349 planner.apply_bytes(req, value) 

350 

351 

352def _get_tensor_size(tensor: Any) -> Optional[tuple]: 

353 """ 

354 Get size/shape of a tensor. 

355 

356 Args: 

357 tensor (Any): Tensor object (tensor-like with shape/size attribute). 

358 

359 Returns: 

360 Optional[tuple]: Tuple of tensor size or None if not available. 

361 """ 

362 if hasattr(tensor, "size") and callable(tensor.size): 

363 return tuple(tensor.size()) 

364 return getattr(tensor, "shape", None) 

365 

366 

367def _get_storage_info( 

368 req: ReadItem, 

369 storage_data: dict[MetadataIndex, StorageInfo], 

370) -> StorageInfo: 

371 """Return physical storage metadata for one read request.""" 

372 storage_info = storage_data.get(req.storage_index) 

373 if storage_info is None: 

374 raise KeyError(f"StorageInfo not found for index {req.storage_index}") 

375 return storage_info 

376 

377 

378def _validate_and_copy_tensor( 

379 req: ReadItem, 

380 tensor: Any, 

381 planner: LoadPlanner, 

382) -> None: 

383 """Validate a loaded tensor slice and copy it to its planner destination.""" 

384 target_tensor = planner.acquire_tensor(req) 

385 if hasattr(target_tensor, "detach"): 

386 target_tensor = target_tensor.detach() 

387 

388 target_size = _get_tensor_size(target_tensor) 

389 tensor_size = _get_tensor_size(tensor) 

390 if target_size is not None and tensor_size is not None and target_size != tensor_size: 

391 raise AssertionError( 

392 f"req {req.storage_index} mismatch sizes " 

393 f"{target_size} vs {tensor_size}" 

394 ) 

395 _copy_tensor_to_target(req, tensor, target_tensor, planner) 

396 

397 

398def _load_torch_tensor_file( 

399 path: str, 

400 reqs: list[ReadItem], 

401 planner: LoadPlanner, 

402 storage_data: dict[MetadataIndex, StorageInfo], 

403) -> None: 

404 """Load tensor slices from a Torch safetensors file.""" 

405 with safe_open(path, framework="pt", device="cpu") as tensor_file: 

406 available_keys = set(tensor_file.keys()) 

407 for req in reqs: 

408 storage_info = _get_storage_info(req, storage_data) 

409 tensor_key = storage_info.tensor_key or req.storage_index.fqn 

410 if tensor_key not in available_keys: 

411 raise KeyError(f"Key {tensor_key} not found in checkpoint file {path}") 

412 tensor_slices = tuple( 

413 slice(int(off), int(off) + int(length)) 

414 for off, length in zip(req.storage_offsets, req.lengths) 

415 ) 

416 if tensor_slices: 

417 tensor = tensor_file.get_slice(tensor_key)[tensor_slices] 

418 else: 

419 tensor = narrow_tensor_by_index( 

420 tensor_file.get_tensor(tensor_key), 

421 req.storage_offsets, 

422 req.lengths, 

423 ) 

424 _validate_and_copy_tensor(req, tensor, planner) 

425 

426 

427def _load_platform_tensor_file( 

428 path: str, 

429 reqs: list[ReadItem], 

430 planner: LoadPlanner, 

431 storage_data: dict[MetadataIndex, StorageInfo], 

432 platform: Any, 

433) -> None: 

434 """Load tensor slices through the active non-Torch platform adapter.""" 

435 param_dict = platform.load_checkpoint(path) 

436 for req in reqs: 

437 storage_info = _get_storage_info(req, storage_data) 

438 tensor_key = storage_info.tensor_key or req.storage_index.fqn 

439 if tensor_key not in param_dict: 

440 raise KeyError(f"Key {tensor_key} not found in checkpoint file {path}") 

441 tensor = narrow_tensor_by_index( 

442 param_dict[tensor_key], 

443 req.storage_offsets, 

444 req.lengths, 

445 ) 

446 _validate_and_copy_tensor(req, tensor, planner) 

447 

448 

449def _load_tensor_file( 

450 path: str, 

451 reqs: list[ReadItem], 

452 planner: LoadPlanner, 

453 storage_data: dict[MetadataIndex, StorageInfo], 

454) -> None: 

455 """ 

456 Load and process tensors from a safetensors file. 

457 

458 Args: 

459 path (str): Path to the safetensors file. 

460 reqs (list[ReadItem]): List of ReadItems for this file. 

461 planner (LoadPlanner): Load planner for resolving and committing tensors. 

462 storage_data (dict[MetadataIndex, StorageInfo]): Physical storage mapping. 

463 """ 

464 platform = get_platform() 

465 if platform.platform_type == PlatformType.PYTORCH: 

466 _load_torch_tensor_file(path, reqs, planner, storage_data) 

467 return 

468 _load_platform_tensor_file(path, reqs, planner, storage_data, platform) 

469 

470 

471class FileSystemReader(StorageReader): 

472 """ 

473 File system storage reader implementation. 

474 

475 Reads checkpoint data from the local file system, loading tensors 

476 from safetensors files and bytes from separate files. 

477 """ 

478 

479 def __init__(self, checkpoint_dir: Union[Path, str]): 

480 self.checkpoint_dir = Path(checkpoint_dir) if isinstance(checkpoint_dir, str) else checkpoint_dir 

481 # Cached storage layout: MetadataIndex -> StorageInfo (torch-aligned) 

482 self.storage_data: Optional[dict[MetadataIndex, StorageInfo]] = None 

483 self.rank: int = 0 

484 self.is_coordinator: bool = False 

485 

486 def initialize_reader(self, checkpoint_id: Optional[Union[Path, str]] = None) -> None: 

487 """ 

488 Initialize storage reader with new checkpoint directory. 

489 

490 Args: 

491 checkpoint_id (Optional[Union[Path, str]]): New checkpoint directory path. Default None. 

492 """ 

493 if checkpoint_id: 

494 self.checkpoint_dir = Path(checkpoint_id) if isinstance(checkpoint_id, str) else checkpoint_id 

495 

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

497 """ 

498 Load checkpoint metadata from file. 

499 

500 When rank is provided in kwargs: load rank-local metadata from .rank{rank}_metadata 

501 (for checkpoints saved with use_collectives=False). 

502 Otherwise: load global metadata from .metadata. 

503 

504 Args: 

505 **kwargs: Optional arguments (e.g., rank for rank-local metadata). 

506 

507 Returns: 

508 Metadata: Metadata object loaded from file. 

509 """ 

510 rank = kwargs.get("rank") 

511 if rank is not None: 

512 metadata_file = self.checkpoint_dir / f"{rank}{METADATA_FILE_NAME}" 

513 else: 

514 metadata_file = self.checkpoint_dir / METADATA_FILE_NAME 

515 

516 if not metadata_file.exists(): 

517 raise FileNotFoundError(f"Metadata file not found: {metadata_file}") 

518 with open(metadata_file, "rb") as f: 

519 metadata = pickle.load(f) 

520 return metadata 

521 

522 def configure_reader(self, metadata: Metadata, is_coordinator: bool, **kwargs) -> None: 

523 """Configure storage reader.""" 

524 # Cache storage_data separately for quick lookup in execute_read. 

525 # This mirrors torch.filesystem, where reader keeps a storage_data dict. 

526 self.storage_data = getattr(metadata, "storage_data", None) 

527 self.is_coordinator = is_coordinator 

528 self.rank = kwargs.get("rank") if "rank" in kwargs else get_platform().get_rank() 

529 

530 def optimize_local_plan(self, plan: LoadPlan) -> LoadPlan: 

531 """ 

532 Optimize local plan. 

533 

534 Args: 

535 plan (LoadPlan): Local load plan. 

536 

537 Returns: 

538 LoadPlan: Optimized local plan. 

539 """ 

540 return plan 

541 

542 def optimize_global_plan(self, plans: list[LoadPlan]) -> list[LoadPlan]: 

543 """ 

544 Optimize global plan. 

545 

546 Args: 

547 plans (list[LoadPlan]): List of local plans from all ranks. 

548 

549 Returns: 

550 list[LoadPlan]: Optimized global plans. 

551 """ 

552 return plans 

553 

554 def _get_storage_path(self, read_item: ReadItem) -> str: 

555 """ 

556 Get storage file path for a read item. 

557 

558 Args: 

559 read_item (ReadItem): ReadItem to get path for. 

560 

561 Returns: 

562 str: Absolute path to the storage file. 

563 """ 

564 if self.storage_data is None: 

565 raise KeyError("Checkpoint metadata.storage_data is required for filesystem read") 

566 storage_info = self.storage_data.get(read_item.storage_index) 

567 if storage_info is None: 

568 raise KeyError(f"StorageInfo not found for index {read_item.storage_index}") 

569 return str(self.checkpoint_dir / storage_info.relative_path) 

570 

571 def _group_items_by_file(self, plan: LoadPlan) -> dict[str, list]: 

572 """ 

573 Group ReadItems by storage file path. 

574 

575 Args: 

576 plan (LoadPlan): Load plan containing ReadItems. 

577 

578 Returns: 

579 dict[str, list[ReadItem]]: Dictionary mapping file paths to lists of ReadItems. 

580 """ 

581 per_file: dict[str, list] = {} 

582 for read_item in plan.items: 

583 path = self._get_storage_path(read_item) 

584 per_file.setdefault(path, []).append(read_item) 

585 return per_file 

586 

587 def execute_read(self, plan: LoadPlan, planner: LoadPlanner) -> None: 

588 """ 

589 Read data from storage. 

590 

591 Aligned with torch filesystem read_data: groups ReadItems by file, 

592 loads each file once, narrows tensors by storage_offsets/lengths for 

593 resharding, then resolves/copies/commits data. 

594 

595 Args: 

596 plan (LoadPlan): Load plan containing ReadItems. 

597 planner (LoadPlanner): Load planner for resolving and committing tensors. 

598 """ 

599 # Group ReadItems by storage file path (like torch per_file) 

600 per_file = self._group_items_by_file(plan) 

601 

602 # Process each file 

603 for path, reqs in per_file.items(): 

604 if not os.path.exists(path): 

605 raise FileNotFoundError(f"Checkpoint file not found: {path}") 

606 

607 if path.endswith(".bytes"): 

608 # BYTE_IO: one bytes file per rank with per-item offsets. 

609 _load_bytes_file(path, reqs, planner, self.storage_data) 

610 else: 

611 # TENSOR: one safetensors file per rank 

612 _load_tensor_file(path, reqs, planner, self.storage_data)