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

271 statements  

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

1# Copyright 2026 Huawei Technologies Co., Ltd. All rights reserved. 

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"""Tensor staging and persist for asynchronous distributed checkpoint save. 

16 

17Staging materializes a host-side snapshot of ``state_dict`` so ``async_save`` can 

18run persistence (plan + I/O) on a copy while training continues on the original tensors. 

19""" 

20 

21import os 

22import copy 

23import pickle 

24import dataclasses 

25import queue 

26import time 

27import traceback 

28import multiprocessing as mp 

29from pathlib import Path 

30from enum import Enum, auto 

31from argparse import Namespace 

32from collections.abc import Callable 

33from typing import Any, Optional, Union 

34from concurrent.futures import Future, ThreadPoolExecutor 

35 

36import numpy as np 

37import torch 

38import torch.distributed as dist 

39import torch.distributed.distributed_c10d as c10d 

40 

41from hyper_parallel.core.dtensor.dtensor import DTensor 

42from hyper_parallel.core.distributed_checkpoint.metadata import ( 

43 Metadata, 

44 CHUNK_INFO, 

45) 

46from hyper_parallel.core.distributed_checkpoint.planner import ( 

47 SavePlan, 

48 SavePlanner, 

49) 

50from hyper_parallel.core.distributed_checkpoint.storage import ( 

51 StorageWriter, 

52 WriteResult, 

53) 

54from hyper_parallel.core.distributed_checkpoint.standard_planner import ( 

55 StandardSavePlanner, 

56) 

57from hyper_parallel.core.distributed_checkpoint.utils import ( 

58 dcp_timer_decorator, 

59 logger, 

60) 

61 

62_COMPLETE_FLAG = b"\x00__PICKLE_WRITE_COMPLETE__\x00" 

63 

64# The join wait sits in the *training* process for as long as the child persists, 

65# so it blocks on the queue with a timeout rather than spinning. 

66_PERSIST_RESULT_POLL_SECONDS = 0.1 

67 

68 

69class DataCopier: 

70 """Type dispatched copy of ``state_dict`` values into host memory. 

71 

72 Copy handlers are registered per type through :meth:`register`; :meth:`copy` resolves the 

73 handler of an object by exact type first and by ``isinstance`` afterwards, falling back to 

74 a generic recursive deep copy for objects no handler was registered for. 

75 """ 

76 

77 _registry = {} 

78 

79 @classmethod 

80 def register(cls, *types: type) -> Callable: 

81 """Register the decorated function as the copy handler of every type given. 

82 

83 Args: 

84 *types (type): Types the handler is responsible for. 

85 

86 Returns: 

87 Callable: Decorator recording the handler and returning it unchanged. 

88 """ 

89 def decorator(func: Callable) -> Callable: 

90 """Record ``func`` against every registered type and hand it back.""" 

91 for t in types: 

92 cls._registry[t] = func 

93 return func 

94 

95 return decorator 

96 

97 @classmethod 

98 def dispatch(cls, obj: Any) -> Optional[Callable]: 

99 """ 

100 Resolve the copy handler of ``obj``: its exact type first, its most derived 

101 registered base class afterwards. 

102 

103 Registration order must not decide the handler. ``DTensor`` is registered after 

104 ``torch.Tensor`` and is a subclass of it, so scanning in registration order would 

105 hand a ``DTensor`` subclass to the plain tensor handler, staging it as a bare local 

106 tensor with its mesh and placements dropped. 

107 

108 Args: 

109 obj (Any): Object to find a copy handler for. 

110 

111 Returns: 

112 Optional[Callable]: The handler, or None when no registered type matches. 

113 """ 

114 if type(obj) in cls._registry: 

115 return cls._registry[type(obj)] 

116 

117 best_type = None 

118 for r_type in cls._registry: 

119 if not isinstance(obj, r_type): 

120 continue 

121 # Unrelated matches (neither one a subclass of the other) keep the first found. 

122 if best_type is None or issubclass(r_type, best_type): 

123 best_type = r_type 

124 return cls._registry[best_type] if best_type is not None else None 

125 

126 @classmethod 

127 def copy(cls, obj: Any) -> Any: 

128 """ 

129 Copy ``obj`` with its registered handler, or with a generic deep copy as fallback. 

130 

131 Args: 

132 obj (Any): Object to copy, of any type held by a state dict. 

133 

134 Returns: 

135 Any: Copy of ``obj``, with tensor leaves residing in host memory. 

136 """ 

137 copy_func = cls.dispatch(obj) 

138 if copy_func: 

139 return copy_func(obj) 

140 logger.warning( 

141 "The user define object %s not implement copy method and register " 

142 "to DataCopier. So, the general copy method will be used, which may cause " 

143 "unexpected copy behavior and incorrect data storage. Please verify this on your own.", 

144 type(obj)) 

145 return cls._copy_others(obj) 

146 

147 @staticmethod 

148 def _copy_others(obj): 

149 obj_c = copy.deepcopy(obj) 

150 for name, value in obj_c.__dict__.items(): 

151 if callable(value): 

152 continue 

153 setattr(obj_c, name, DataCopier.copy(value)) 

154 return obj_c 

155 

156 

157def _copy_tensor_to_cpu(tensor: torch.Tensor) -> torch.Tensor: 

158 """Return a host-memory copy of a tensor, detached from autograd.""" 

159 t = tensor.detach().to("cpu", copy=True) 

160 if hasattr(tensor, CHUNK_INFO): 

161 setattr(t, CHUNK_INFO, getattr(tensor, CHUNK_INFO)) 

162 return t 

163 

164 

165@DataCopier.register(int, float, complex, bool, str, type(None), Enum, torch.dtype) 

166def _copy_const(obj): 

167 return obj 

168 

169 

170@DataCopier.register(bytes, bytearray) 

171def _copy_bytes(obj): 

172 return bytes(obj) 

173 

174 

175@DataCopier.register(np.ndarray) 

176def _copy_ndarray(obj): 

177 return obj.copy() 

178 

179 

180@DataCopier.register(torch.Tensor) 

181def _copy_tensor(obj): 

182 result = _copy_tensor_to_cpu(obj) 

183 return result 

184 

185 

186@DataCopier.register(DTensor) 

187def _copy_dtensor(obj): 

188 """Stage a DTensor by copying its local shard to host memory, keeping mesh and placements.""" 

189 staged_local = _copy_tensor_to_cpu(obj.to_local()) 

190 # ``shape`` is required for RaggedShard: an unevenly-sharded DTensor cannot 

191 # recover its logical global shape from the local shard alone. 

192 result = DTensor.from_local( 

193 staged_local, 

194 obj.device_mesh, 

195 obj.placements, 

196 shape=tuple(obj.shape), 

197 ) 

198 return result 

199 

200 

201@DataCopier.register(Namespace) 

202def _copy_namespace(obj): 

203 c = DataCopier.copy(vars(obj)) 

204 return Namespace(**c) 

205 

206 

207@DataCopier.register(list) 

208def _copy_list(obj): 

209 result = [DataCopier.copy(item) for item in obj] 

210 return result 

211 

212 

213@DataCopier.register(tuple) 

214def _copy_tuple(obj): 

215 result = tuple(DataCopier.copy(item) for item in obj) 

216 return result 

217 

218 

219@DataCopier.register(dict) 

220def _copy_dict(obj): 

221 result = {} 

222 for k, v in obj.items(): 

223 result[DataCopier.copy(k)] = DataCopier.copy(v) 

224 return result 

225 

226 

227class AsyncPersistStatus(Enum): 

228 """Queue payload status from :func:`_async_persist_worker` to the parent join thread.""" 

229 

230 SUCCESS = auto() 

231 FAILURE = auto() 

232 

233 

234class FileType(Enum): 

235 

236 LOCAL_PLAN = auto() 

237 STORAGE_DATA = auto() 

238 

239 

240@dataclasses.dataclass 

241class AsyncSaveResponse: 

242 """Result of :func:`async_save`. 

243 

244 Host staging runs synchronously before :func:`async_save` returns; only checkpoint 

245 **persistence** is asynchronous. ``persist_completion`` completes when the child 

246 process finishes :func:`_save_impl` (plan, collectives, disk I/O) and supplies 

247 :class:`Metadata`. 

248 """ 

249 

250 persist_completion: Future[Metadata] 

251 

252 def get_result(self, timeout: int = None) -> Optional[Metadata]: 

253 """Wait for the save to be persisted and return its metadata. 

254 

255 Args: 

256 timeout (int): Seconds to wait. Default ``None``, which waits indefinitely. 

257 

258 Returns: 

259 Optional[Metadata]: Metadata of the persisted checkpoint. 

260 """ 

261 return self.persist_completion.result(timeout=timeout) 

262 

263 

264@dcp_timer_decorator 

265def build_staged_state_dict(state_dict: dict[str, Any]) -> dict[str, Any]: 

266 """ 

267 Build a deep structural copy of ``state_dict`` with tensor / DTensor data 

268 copied to host memory so the original can be mutated while checkpoint I/O runs. 

269 

270 Uses the same flattening rules as :class:`StandardSavePlanner`. 

271 

272 Args: 

273 state_dict (dict[str, Any]): Nested or flat training state dict. 

274 

275 Returns: 

276 dict[str, Any]: New dict with identical nesting and keys; tensor leaves are staging copies. 

277 """ 

278 return DataCopier.copy(state_dict) 

279 

280 

281def cleanup_and_reinit_process_group( 

282 master_addr: str, 

283 master_port: int, 

284 rank: int, 

285 world_size: int, 

286) -> None: 

287 """ 

288 Cleanup hccl process group and reinitialize gloo process group. 

289 

290 Args: 

291 master_addr (str): Address of the rendezvous the new group meets at. 

292 master_port (int): Port of that rendezvous. 

293 rank (int): Rank of this process in the new group. 

294 world_size (int): Number of processes in the new group. 

295 """ 

296 # Resetting the private c10d state is the only way to drop the process group inherited 

297 # from the parent process, so the accesses below are deliberate. 

298 # pylint: disable=protected-access 

299 

300 # Step 1: clear C++ ProcessGroupRegistry (avoids "already registered" error). 

301 if hasattr(c10d, "_unregister_all_process_groups"): 

302 try: 

303 c10d._unregister_all_process_groups() 

304 except Exception: # pylint: disable=broad-except 

305 pass 

306 

307 # Step 2: reset Python-layer _world to an uninitialised state. 

308 if hasattr(c10d, "_World"): 

309 try: 

310 c10d._world = c10d._World() 

311 except Exception: # pylint: disable=broad-except 

312 pass 

313 # Also reset the module-level init-method string (present in all versions). 

314 if hasattr(c10d, "_default_pg_init_method"): 

315 c10d._default_pg_init_method = None 

316 for attr in ("_default_pg", "_pg_map", "_pg_names", "_pg_group_ranks", 

317 "_pg_backend_config", "_group_count", "_tags_to_pg", 

318 "_pg_to_tag"): 

319 if not hasattr(c10d, attr): 

320 continue 

321 old_val = getattr(c10d, attr) 

322 if isinstance(old_val, dict): 

323 setattr(c10d, attr, {}) 

324 elif isinstance(old_val, int): 

325 setattr(c10d, attr, 0) 

326 else: 

327 setattr(c10d, attr, None) 

328 

329 # Step 3: build a fresh TCPStore on the dedicated gloo port and init. 

330 # rank-0 child is the store master; all others connect as clients. 

331 store = dist.PrefixStore( 

332 prefix=mp.current_process().name, 

333 store=dist.TCPStore( 

334 host_name=master_addr, 

335 port=master_port 

336 ), 

337 ) 

338 

339 # Step 4: init new process group with backend "gloo". 

340 dist.init_process_group( 

341 backend="gloo", 

342 store=store, 

343 rank=rank, 

344 world_size=world_size, 

345 ) 

346 

347 

348@dcp_timer_decorator 

349def execute_async_persist_with_gloo( 

350 result_queue: mp.Queue, 

351 staged: dict[str, Any], 

352 checkpoint_id: Optional[Union[Path, str]], 

353 storage_writer: Optional[StorageWriter], 

354 planner: Optional[SavePlanner], 

355 rank: int, 

356 world_size: int, 

357 master_addr: str, 

358 master_port: int, 

359) -> None: 

360 """Child-process entry for gloo-based async save. 

361 Reinitialises a **CPU-only gloo process group** on a dedicated port so that 

362 all collective communication (all_gather_object for plan exchange, 

363 all_gather_object for write_results) can run inside the child process, 

364 exactly mirroring what :func:`_save_impl` does synchronously. 

365 The parent process only stages tensors to CPU; all planning, collectives, 

366 disk I/O and metadata finalisation happen here. 

367 Design rationale 

368 ---------------- 

369 * The training group uses NCCL (or hccl on Ascend) and lives in C++ global 

370 state that is invalidated across ``fork``. We never touch it. 

371 * A fresh ``gloo`` group is initialised via a ``TCPStore`` client on the training 

372 rendezvous itself (``master_addr:master_port``), namespaced by a ``PrefixStore`` 

373 keyed on this process name. The name carries a per-``async_save`` counter, so each 

374 save gets its own key space on the same store; every rank must therefore reach the 

375 same number of gloo saves, or the prefixes diverge and the rendezvous hangs. 

376 * After :func:`_save_impl` completes the gloo group is destroyed so resources 

377 are released cleanly. 

378 

379 Args: 

380 result_queue: IPC queue for returning status / metadata to parent join thread. 

381 staged: CPU-resident staged state dict (output of build_staged_state_dict). 

382 checkpoint_id: Checkpoint directory path. 

383 storage_writer: Pre-constructed StorageWriter (or None → FileSystemWriter). 

384 planner: Pre-constructed SavePlanner (or None → StandardSavePlanner). 

385 rank: Global rank of this process (inherited from parent). 

386 world_size: Total world size (inherited from parent). 

387 master_addr: TCP rendezvous address for the gloo store. 

388 master_port: Training master port; the child connects to that store as a client. 

389 """ 

390 # ``api`` imports this module at load time, so this import stays function local: that is 

391 # what keeps the two from importing each other at load time. 

392 from hyper_parallel.core.distributed_checkpoint.api import _save_impl # pylint: disable=import-outside-toplevel 

393 try: 

394 # Clear the original communication group information and establish the gloo communication. 

395 cleanup_and_reinit_process_group( 

396 master_addr, 

397 master_port, 

398 rank, 

399 world_size 

400 ) 

401 meta = _save_impl( 

402 staged, 

403 checkpoint_id=checkpoint_id, 

404 storage_writer=storage_writer, 

405 planner=planner, 

406 ) 

407 # The plan cache is also returned and can be reused for the next dcp async save. 

408 result_queue.put((AsyncPersistStatus.SUCCESS, (meta, StandardSavePlanner.cached_save_result))) 

409 except Exception: # pylint: disable=broad-except 

410 result_queue.put((AsyncPersistStatus.FAILURE, traceback.format_exc())) 

411 finally: 

412 # Clean up the gloo group we created; ignore errors (process is exiting). 

413 if dist.is_initialized(): 

414 dist.destroy_process_group() 

415 

416 

417@dcp_timer_decorator 

418def execute_async_persist( 

419 result_queue: mp.Queue, 

420 staged: dict[str, Any], 

421 checkpoint_id: Optional[Union[Path, str]], 

422 storage_writer: Optional[StorageWriter], 

423 planner: Optional[SavePlanner], 

424 no_dist: bool, 

425 use_collectives: bool, 

426 use_storage_comm: bool, 

427) -> None: 

428 """ 

429 Perform asynchronous flushing to disk, 

430 which is not suitable for collective communication, 

431 or use the file system for collective communication. 

432 

433 Args: 

434 result_queue: IPC queue for returning status / metadata to parent join thread. 

435 staged: CPU-resident staged state dict (output of build_staged_state_dict). 

436 checkpoint_id: Checkpoint directory path. 

437 storage_writer: Pre-constructed StorageWriter (or None → FileSystemWriter). 

438 planner: Pre-constructed SavePlanner (or None → StandardSavePlanner). 

439 no_dist: Whether it is a distributed scenario or a standalone scenario. 

440 use_collectives: Total world size (inherited from parent). 

441 use_storage_comm: TCP rendezvous address for the gloo store. 

442 """ 

443 # ``api`` imports this module at load time, so this import has to stay function local. 

444 from hyper_parallel.core.distributed_checkpoint.api import _save_impl # pylint: disable=import-outside-toplevel 

445 try: 

446 meta = _save_impl( 

447 staged, 

448 checkpoint_id=checkpoint_id, 

449 storage_writer=storage_writer, 

450 planner=planner, 

451 no_dist=no_dist, 

452 use_collectives=use_collectives, 

453 use_storage_comm=use_storage_comm 

454 ) 

455 # The plan cache is also returned and can be reused for the next dcp async save. 

456 result_queue.put((AsyncPersistStatus.SUCCESS, (meta, StandardSavePlanner.cached_save_result))) 

457 except Exception: # pylint: disable=broad-except 

458 result_queue.put((AsyncPersistStatus.FAILURE, traceback.format_exc())) 

459 

460 

461def resolve_async_persist_result( 

462 proc: mp.Process, 

463 result_queue: mp.Queue, 

464 persist_future: Future[None], 

465 async_callback: Callable[[], None], 

466) -> None: 

467 """Join persist ``proc`` and complete ``persist_future`` (runs on a background thread).""" 

468 result = None 

469 while result is None: 

470 try: 

471 result = result_queue.get(timeout=_PERSIST_RESULT_POLL_SECONDS) 

472 except queue.Empty: 

473 if proc.is_alive(): 

474 continue 

475 # The child may have put its result and exited while the wait above was 

476 # timing out, so look once more before giving up on it. 

477 try: 

478 result = result_queue.get_nowait() 

479 except queue.Empty: 

480 pass 

481 break 

482 

483 proc.join() 

484 if persist_future.done(): 

485 return 

486 

487 if result is None: 

488 persist_future.set_exception( 

489 RuntimeError( 

490 f"async_persist process exited with code {proc.exitcode} and no result on queue" 

491 ) 

492 ) 

493 return 

494 

495 status, payload = result 

496 if status == AsyncPersistStatus.SUCCESS: 

497 try: 

498 async_callback() 

499 StandardSavePlanner.cached_save_result.update(payload[1]) 

500 except Exception as exc: # pylint: disable=broad-except 

501 persist_future.set_exception(exc) 

502 else: 

503 persist_future.set_result(payload[0]) 

504 elif status == AsyncPersistStatus.FAILURE: 

505 # The child's exception object cannot cross the process boundary, so what 

506 # arrives is its formatted traceback and a RuntimeError carries the text. 

507 persist_future.set_exception(RuntimeError(payload)) 

508 else: 

509 persist_future.set_exception( 

510 RuntimeError(f"async_persist queue returned unexpected status: {status!r}") 

511 ) 

512 

513 

514def batch_read_worker(file_batch: tuple[int], checkpoint_id: str, file_type: FileType) -> list: 

515 """ 

516 Read one batch of files, waiting for the files that the other ranks have not written yet. 

517 

518 Args: 

519 file_batch (tuple[int]): Contiguous file ids this worker is responsible for. 

520 checkpoint_id (str): Checkpoint directory holding the files. 

521 file_type (FileType): Which kind of file to read, local plan or storage data. 

522 

523 Returns: 

524 list: Content of every file of ``file_batch``, in the order of the batch. 

525 

526 Raises: 

527 TimeoutError: If some files are still unreadable after 1800 seconds. 

528 """ 

529 results = [None] * len(file_batch) 

530 file_to_read = file_batch 

531 first_file_id = file_batch[0] 

532 

533 # Read files in a round-robin manner, 

534 # If a file has not been generated yet, skip it and proceed to the next file. 

535 # Until all files have been read. 

536 timeout = 1800 

537 deadline = time.monotonic() + timeout 

538 while True: 

539 if not file_to_read: 

540 break 

541 if time.monotonic() > deadline: 

542 raise TimeoutError( 

543 f"After waiting for 1800 seconds, " 

544 f"the {file_type.name} files: {file_to_read} still have not been read completely." 

545 ) 

546 file_not_read = [] 

547 for file_id in file_to_read: 

548 ret = load_file(checkpoint_id, file_type, file_id) 

549 if ret is None: 

550 file_not_read.append(file_id) 

551 else: 

552 results[file_id - first_file_id] = ret 

553 file_to_read = tuple(file_not_read) 

554 

555 return results 

556 

557 

558@dcp_timer_decorator 

559def parallel_read_files( 

560 file_list: tuple[int], 

561 checkpoint_id: str, 

562 file_type: FileType, 

563 max_workers: int = 32 

564) -> list: 

565 """ 

566 Split the total file list into multiple batches 

567 based on the number of parallel threads, 

568 then perform multi-threaded parallel reading to improve read efficiency. 

569 """ 

570 if not file_list: 

571 return [] 

572 

573 batches, max_workers = construct_file_batches(file_list, max_workers) 

574 logger.info( 

575 "parallel read files, file type: %s, file count: %d, max read workers: %d", 

576 file_type.name, len(file_list), max_workers 

577 ) 

578 

579 # Use the thread pool to read data concurrently, improving the read efficiency. 

580 with ThreadPoolExecutor(max_workers=max_workers) as executor: 

581 all_results = tuple( 

582 executor.map(lambda b: batch_read_worker(b, checkpoint_id, file_type), batches) 

583 ) 

584 return [result for batch_results in all_results for result in batch_results] 

585 

586 

587def construct_file_batches(file_list: tuple[int], max_workers: int) -> (list[tuple[int]], int): 

588 """ 

589 Split the total file list into multiple batches 

590 based on the number of parallel threads 

591 """ 

592 n = len(file_list) 

593 max_workers = min(max_workers, n) 

594 if max_workers == 0: 

595 raise ValueError("Max workers number for parallel read batch file is 0!") 

596 batch_size = (n + max_workers - 1) // max_workers 

597 # Split the file list into max_workers sublists. 

598 batches = [ 

599 file_list[i * batch_size: (i + 1) * batch_size] 

600 for i in range(max_workers) 

601 ] 

602 batches = [b for b in batches if b] 

603 return batches, max_workers 

604 

605 

606@dcp_timer_decorator 

607def gather_all_results_from_storage( 

608 checkpoint_id: str, 

609 is_coordinator: bool, 

610 world_size: int, 

611 results: Union[SavePlan, WriteResult], 

612 file_type: FileType, 

613 file_id: int 

614) -> list: 

615 """ 

616 All node data is collected from the storage, 

617 which is classified into local plan and storage data. 

618 In the first step, each rank writes its own data. 

619 In the second step, for local plan, each rank reads the file written by all ranks. 

620 For storage data, only rank 0 reads the file written by all ranks. 

621 """ 

622 logger.info("gather all results from storage, file type: %s", file_type.name) 

623 write_file(checkpoint_id, file_type, file_id, results) 

624 if not is_coordinator and file_type != FileType.LOCAL_PLAN: 

625 return [] 

626 

627 # For LOCAL_PLAN, each node has 16 rank processes reading simultaneously. 

628 if file_type == FileType.LOCAL_PLAN: 

629 max_workers = min(16, max(os.cpu_count() // 32, 1)) 

630 else: 

631 max_workers = min(64, max(os.cpu_count() // 2, 1)) 

632 file_list = tuple(range(0, world_size)) 

633 gathered = parallel_read_files(file_list, 

634 checkpoint_id, 

635 file_type, 

636 max_workers=max_workers) 

637 return gathered 

638 

639 

640def assemble_file_path(checkpoint_id: str, file_type: FileType, file_id: int) -> str: 

641 """ 

642 Assemble file path to exchanges its results through. 

643 

644 Args: 

645 checkpoint_id (str): Checkpoint directory holding the exchange files. 

646 file_type (FileType): Which kind of file, local plan or storage data. 

647 file_id (int): Rank that owns the file. 

648 

649 Returns: 

650 str: Path of that rank's file. 

651 """ 

652 return f"{checkpoint_id}/{file_type.name}_{file_id}.pkl" 

653 

654 

655def _safe_remove_file(file_path: str) -> None: 

656 """ 

657 Delete a file that has been read, without letting the delete fail the load. 

658 

659 What it held has already been read, so a file that will not go is a leftover worth a 

660 warning rather than a reason to fail a save that has its data. One attempt is enough: 

661 what refuses here - a read-only mount, a directory this rank may not write in - does 

662 not clear by asking again. 

663 

664 Args: 

665 file_path (str): File to delete. 

666 """ 

667 try: 

668 os.remove(file_path) 

669 except Exception as e: # pylint: disable=broad-except 

670 logger.warning( 

671 "Could not remove the STORAGE_DATA file %s, because the error: %s", file_path, e 

672 ) 

673 

674 

675def load_file(checkpoint_id: str, file_type: FileType, file_id: int) -> Any: 

676 """ 

677 Reads files from the storage. 

678 Currently, this function is mainly used to 

679 asynchronously save local plan and storage data files. 

680 

681 Args: 

682 checkpoint_id (str): Checkpoint directory holding the file. 

683 file_type (FileType): Which kind of file to read, local plan or storage data. 

684 file_id (int): Id of the file within that kind. 

685 

686 Returns: 

687 Any: The unpickled content of the file. 

688 """ 

689 file_path = assemble_file_path(checkpoint_id, file_type, file_id) 

690 

691 try: 

692 with open(file_path, "rb") as f: 

693 flag_len = len(_COMPLETE_FLAG) 

694 f.seek(0, os.SEEK_END) 

695 size = f.tell() 

696 # determining whether the file length is greater than the flag length 

697 if size < flag_len: 

698 return None 

699 f.seek(size - flag_len) 

700 # determining whether the file has a write complete flag 

701 if f.read(flag_len) != _COMPLETE_FLAG: 

702 return None 

703 f.seek(0) 

704 result = pickle.load(f) 

705 except FileNotFoundError: 

706 # The rank that owns this file has not written it yet; the caller comes back round. 

707 return None 

708 

709 # Delete only the STORAGE_DATA file. Do not delete the LOCAL_PALN file, 

710 # as it is generated only during the initial save. 

711 if file_type == FileType.STORAGE_DATA: 

712 _safe_remove_file(file_path) 

713 return result 

714 

715 

716@dcp_timer_decorator 

717def write_file( 

718 checkpoint_id: str, 

719 file_type: FileType, 

720 file_id: int, 

721 content: Union[SavePlan, WriteResult], 

722) -> None: 

723 """ 

724 Write files to the storage. 

725 Currently, this function is mainly used to 

726 asynchronously save local plan and storage data files. 

727 

728 Args: 

729 checkpoint_id (str): Checkpoint directory to write into. 

730 file_type (FileType): Which kind of file to write, local plan or storage data. 

731 file_id (int): Id of the file within that kind. 

732 content (Union[SavePlan, WriteResult]): What to pickle into it. 

733 """ 

734 final_path = assemble_file_path(checkpoint_id, file_type, file_id) 

735 

736 # To avoid read-write conflicts, the completion flag is appended last: a reader that 

737 # opens the file mid-write finds no flag at the end and retries on its next round. 

738 with open(final_path, "wb") as f: 

739 pickle.dump(content, f, protocol=pickle.HIGHEST_PROTOCOL) 

740 f.write(_COMPLETE_FLAG) 

741 

742 logger.info("write file type: %s, file path: %s", file_type.name, final_path)