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

182 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"""Loads of checkpoints whose tensors are named and laid out differently from the state dict they fill. 

16 

17A Hugging Face checkpoint stores the query, key and value projections of a layer as three tensors 

18where a model may hold one fused, interleaved tensor, and the experts of a block one tensor apiece 

19where a model stacks them. :class:`RemapLoadPlanner` loads such a checkpoint without ever putting a 

20checkpoint tensor together in memory: it is told, per destination tensor, which box of it comes from 

21which region of which checkpoint tensor, as :class:`RemapBlock` entries, and plans each rank's reads 

22from the shards that rank holds, exactly as :class:`StandardLoadPlanner` plans them for a checkpoint 

23laid out like its state dict. Replicated shards are still read once and broadcast. 

24 

25What a block cannot describe is handed to :class:`DeferredRead`: its checkpoint tensors are read 

26whole and given to a function that puts the result in place itself. 

27""" 

28from collections.abc import Callable, Mapping, Sequence 

29from dataclasses import dataclass 

30from typing import Any, Optional 

31 

32from hyper_parallel.core.distributed_checkpoint.metadata import ( 

33 MetadataIndex, 

34 TensorStorageMetadata, 

35 dtype_element_size, 

36) 

37from hyper_parallel.core.distributed_checkpoint.planner import LoadItemType, LoadPlan, ReadItem 

38from hyper_parallel.core.distributed_checkpoint.ragged import get_ragged_box_tensor 

39from hyper_parallel.core.distributed_checkpoint.standard_planner import StandardLoadPlanner, _is_on_host 

40from hyper_parallel.core.distributed_checkpoint.utils import ( 

41 chunk_to_area, 

42 create_chunk_list_for_tensor, 

43 dcp_timer_decorator, 

44 infer_intersection, 

45 narrow_tensor_by_index, 

46) 

47from hyper_parallel.core.dtensor.dtensor import DTensor 

48 

49# Deferred reads land in no state dict entry, so their items name a shard of their own. The prefix 

50# cannot start a state dict key, which is a dotted module path. 

51_DEFERRED_FQN_PREFIX = "<deferred>/" 

52 

53 

54@dataclass(frozen=True) 

55class RemapBlock: 

56 """ 

57 A box of a destination tensor, and where in one checkpoint tensor each element of it is read from. 

58 

59 The element at index ``x`` of the destination tensor, for ``offsets <= x < offsets + lengths``, is 

60 read from index ``base + coeff @ (x - offsets)`` of the checkpoint tensor ``source``: ``coeff[s][a]`` 

61 is how far along checkpoint dimension ``s`` one step along destination dimension ``a`` moves. 

62 

63 A block whose every varying destination dimension walks a checkpoint dimension of its own one 

64 element at a time is a plain region of the checkpoint tensor in another order, and the read is 

65 copied straight into the destination. Anything else, and any block with ``post`` operations, is 

66 read into a buffer of its own and put in place from there. 

67 

68 Attributes: 

69 offsets (tuple[int, ...]): Where the box starts in the whole destination tensor. 

70 lengths (tuple[int, ...]): Size of the box, positive in every dimension. 

71 source (str): Name of the checkpoint tensor the box is read from. 

72 base (tuple[int, ...]): Index of the checkpoint tensor the first element of the box comes from. 

73 coeff (tuple[tuple[int, ...], ...]): Non-negative steps, one row per checkpoint dimension and one 

74 column per destination dimension. 

75 post (tuple[Callable[[Any], Any], ...]): Elementwise operations applied in order to the values 

76 read, in the dtype the checkpoint stores them in, before they are copied into place. 

77 Default (). 

78 """ 

79 

80 offsets: tuple[int, ...] 

81 lengths: tuple[int, ...] 

82 source: str 

83 base: tuple[int, ...] 

84 coeff: tuple[tuple[int, ...], ...] 

85 post: tuple[Callable[[Any], Any], ...] = () 

86 

87 def __post_init__(self) -> None: 

88 """Reject a block whose parts disagree on the number of dimensions or step backwards.""" 

89 ndim = len(self.offsets) 

90 if len(self.lengths) != ndim or any(length <= 0 for length in self.lengths): 

91 raise ValueError( 

92 f"RemapBlock of {self.source!r} needs one positive length per offset, " 

93 f"got offsets={self.offsets} lengths={self.lengths}" 

94 ) 

95 if len(self.coeff) != len(self.base) or any( 

96 len(row) != ndim or any(step < 0 for step in row) for row in self.coeff 

97 ): 

98 raise ValueError( 

99 f"RemapBlock of {self.source!r} needs one row of {ndim} non-negative steps per checkpoint " 

100 f"dimension, got base={self.base} coeff={self.coeff}" 

101 ) 

102 

103 

104@dataclass(frozen=True) 

105class DeferredRead: 

106 """ 

107 Checkpoint tensors read whole, and what to do with them once all of them are. 

108 

109 For destination tensors no block can describe. Every rank that plans the read reads the tensors 

110 into host memory itself, never through a broadcast, and ``complete`` is called once with all of 

111 them to put whatever it computes out of them in place. 

112 

113 Attributes: 

114 sources (tuple[str, ...]): Names of the checkpoint tensors to read, each once. 

115 complete (Callable[[dict[str, Any]], None]): Called with ``{name: tensor}`` once every source 

116 has been read. 

117 """ 

118 

119 sources: tuple[str, ...] 

120 complete: Callable[[dict[str, Any]], None] 

121 

122 def __post_init__(self) -> None: 

123 """Reject an empty read and a source named twice, which would never be counted complete.""" 

124 if not self.sources or len(set(self.sources)) != len(self.sources): 

125 raise ValueError(f"DeferredRead needs distinct sources, got {self.sources}") 

126 

127 

128@dataclass(frozen=True) 

129class _Recipe: 

130 """ 

131 How the region one read item stands for is put in place. 

132 

133 Attributes: 

134 block (RemapBlock): The block the item was cut from. 

135 sub_lengths (tuple[int, ...]): Size of the destination box the item fills. 

136 axes (Optional[tuple[tuple[int, int], ...]]): ``(checkpoint dimension, destination dimension)`` 

137 pairs of a read copied straight into place, or None for one gathered from a buffer. 

138 """ 

139 

140 block: RemapBlock 

141 sub_lengths: tuple[int, ...] 

142 axes: Optional[tuple[tuple[int, int], ...]] 

143 

144 

145class _Capture: 

146 """ 

147 Stands where a read would be copied to, and keeps what was read for the planner to place itself. 

148 

149 The storage reader checks the size of what it acquires against the size of what it read, then 

150 copies with ``copy_``. A capture answers both, holding on to the tensor instead of copying it. 

151 """ 

152 

153 __slots__ = ("_size", "value") 

154 

155 def __init__(self, size: Sequence[int]) -> None: 

156 """ 

157 Args: 

158 size (Sequence[int]): Size of the region the read returns. 

159 """ 

160 self._size = tuple(size) 

161 self.value: Any = None 

162 

163 def size(self) -> tuple[int, ...]: 

164 """Size of the region the read returns.""" 

165 return self._size 

166 

167 def copy_(self, tensor: Any) -> "_Capture": 

168 """Keep ``tensor``, the region as read.""" 

169 self.value = tensor 

170 return self 

171 

172 

173def _recipe_key(item: ReadItem) -> tuple: 

174 """What tells the reads of one plan apart, before and after a global plan has marked them.""" 

175 return item.dest_index, item.dest_offsets, item.storage_index, item.storage_offsets 

176 

177 

178def _source_start(block: RemapBlock, sub_offsets: Sequence[int]) -> tuple[int, ...]: 

179 """Checkpoint index the first element of the destination box starting at ``sub_offsets`` comes from.""" 

180 steps = [offset - origin for offset, origin in zip(sub_offsets, block.offsets)] 

181 return tuple(start + sum(c * step for c, step in zip(row, steps)) for start, row in zip(block.base, block.coeff)) 

182 

183 

184def _source_lengths(block: RemapBlock, sub_lengths: Sequence[int]) -> tuple[int, ...]: 

185 """Size of the checkpoint region a destination box of ``sub_lengths`` reads from, gaps included.""" 

186 return tuple(1 + sum(c * (length - 1) for c, length in zip(row, sub_lengths)) for row in block.coeff) 

187 

188 

189def _direct_axes(block: RemapBlock, sub_lengths: Sequence[int]) -> Optional[tuple[tuple[int, int], ...]]: 

190 """ 

191 Pair the dimensions of a read that can be copied straight into its destination box. 

192 

193 Args: 

194 block (RemapBlock): The block the box belongs to. 

195 sub_lengths (Sequence[int]): Size of the box. 

196 

197 Returns: 

198 Optional[tuple[tuple[int, int], ...]]: ``(checkpoint dimension, destination dimension)`` for 

199 every destination dimension the box is longer than one along, when each of them walks a 

200 checkpoint dimension of its own one element at a time and nothing is left to apply. None 

201 otherwise. 

202 """ 

203 if block.post: 

204 return None 

205 pairs: dict[int, int] = {} 

206 for axis, length in enumerate(sub_lengths): 

207 if length == 1: 

208 continue 

209 walked = [dim for dim, row in enumerate(block.coeff) if row[axis]] 

210 if len(walked) != 1 or block.coeff[walked[0]][axis] != 1 or walked[0] in pairs: 

211 return None 

212 pairs[walked[0]] = axis 

213 return tuple(sorted(pairs.items())) 

214 

215 

216def _direct_view(region: Any, axes: tuple[tuple[int, int], ...], source_lengths: tuple[int, ...]) -> Any: 

217 """ 

218 View a destination box in the order and shape of the checkpoint region it is copied from. 

219 

220 Args: 

221 region (Any): The destination box. 

222 axes (tuple[tuple[int, int], ...]): Pairs from :func:`_direct_axes`. 

223 source_lengths (tuple[int, ...]): Size of the checkpoint region. 

224 

225 Returns: 

226 Any: A view of ``region``. Only dimensions of size one are added or dropped, so it still 

227 writes through to the destination. 

228 """ 

229 walked = [axis for _, axis in axes] 

230 order = walked + [axis for axis in range(len(region.shape)) if axis not in walked] 

231 if order != sorted(order): 

232 region = region.permute(order) 

233 return region.reshape(source_lengths) 

234 

235 

236def _gather(block: RemapBlock, sub_lengths: tuple[int, ...], read: Any) -> Any: 

237 """ 

238 Pick the elements of a destination box out of the checkpoint region read for it. 

239 

240 Args: 

241 block (RemapBlock): The block the box belongs to. 

242 sub_lengths (tuple[int, ...]): Size of the box. 

243 read (Any): The checkpoint region, starting at the element the box starts with. 

244 

245 Returns: 

246 Any: The values of the box in destination order, with the block's post operations applied. 

247 """ 

248 strides = read.stride() 

249 stride = tuple( 

250 sum(row[axis] * step for row, step in zip(block.coeff, strides)) for axis in range(len(sub_lengths)) 

251 ) 

252 values = read.as_strided(sub_lengths, stride, read.storage_offset()) 

253 for operation in block.post: 

254 values = operation(values) 

255 return values 

256 

257 

258class RemapLoadPlanner(StandardLoadPlanner): 

259 """ 

260 Load planner for a checkpoint whose tensors are named and laid out differently from the state dict. 

261 

262 Every state dict entry to fill is described by ``table``: blocks that tile it, each saying which 

263 region of which checkpoint tensor it is read from. Entries without blocks are left alone, and 

264 ``deferred`` reads whole checkpoint tensors for whatever the blocks leave out. 

265 

266 Each rank intersects the shards it holds with the blocks and reads only those regions. A read that 

267 is a plain region of a checkpoint tensor is copied straight into place; one that has to be gathered 

268 or changed is read into a buffer first. Only whole checkpoint tensors are read, one chunk each, as 

269 :class:`HuggingFaceStorageReader` describes them. 

270 

271 Example:: 

272 

273 planner = RemapLoadPlanner({"linear.weight": [block_from_q, block_from_k]}) 

274 load(state_dict, storage_reader=HuggingFaceStorageReader(path), planner=planner) 

275 """ 

276 

277 def __init__( 

278 self, 

279 table: Optional[Mapping[str, Sequence[RemapBlock]]] = None, 

280 deferred: Sequence[DeferredRead] = (), 

281 broadcast_replicated_tensors: bool = False, 

282 ) -> None: 

283 """ 

284 Args: 

285 table (Optional[Mapping[str, Sequence[RemapBlock]]]): Blocks of every state dict entry to fill, 

286 by its flattened name. Default None, for a subclass that fills ``table`` while configuring. 

287 deferred (Sequence[DeferredRead]): Whole-tensor reads for what no block describes. Default (). 

288 broadcast_replicated_tensors (bool): See :class:`StandardLoadPlanner`; ``load`` overrides it. 

289 Default False. 

290 """ 

291 super().__init__(broadcast_replicated_tensors=broadcast_replicated_tensors) 

292 self.table: dict[str, tuple[RemapBlock, ...]] = { 

293 name: tuple(blocks) for name, blocks in (table or {}).items() 

294 } 

295 self.deferred: tuple[DeferredRead, ...] = tuple(deferred) 

296 self._recipes: dict[tuple, _Recipe] = {} 

297 self._deferred_slots: dict[tuple, tuple[int, str]] = {} 

298 self._captured: dict[int, dict[str, Any]] = {} 

299 

300 @dcp_timer_decorator 

301 def build_local_plan(self) -> LoadPlan: 

302 """ 

303 Plan this rank's reads: the part of every block inside a shard it holds, and the deferred reads. 

304 

305 Returns: 

306 LoadPlan: One read per (shard, block) overlap, then one per deferred checkpoint tensor. 

307 

308 Raises: 

309 RuntimeError: If the planner is not configured. 

310 ValueError: If the table names an entry the state dict does not have, or a checkpoint 

311 tensor that is missing, stored in shards, or smaller than a block reads. 

312 """ 

313 if self.state_dict is None or self.metadata is None: 

314 raise RuntimeError("Planner not configured") 

315 self._recipes.clear() 

316 self._deferred_slots.clear() 

317 self._captured.clear() 

318 

319 items: list[ReadItem] = [] 

320 for fqn, blocks in self.table.items(): 

321 if fqn not in self.state_dict: 

322 raise ValueError(f"Remap table names {fqn!r}, which the state dict does not have") 

323 items.extend(self._plan_entry(fqn, self.state_dict[fqn], blocks)) 

324 for group_index, group in enumerate(self.deferred): 

325 items.extend(self._plan_deferred(group_index, source) for source in group.sources) 

326 return LoadPlan(items=items) 

327 

328 def acquire_tensor(self, read_item: ReadItem) -> Any: 

329 """ 

330 Where one read is copied to. 

331 

332 Args: 

333 read_item (ReadItem): A read of this planner's plan. 

334 

335 Returns: 

336 Any: A view of the destination for a read copied straight into place, otherwise a 

337 capture that keeps the read for :meth:`apply_tensor`. 

338 """ 

339 key = _recipe_key(read_item) 

340 if key in self._deferred_slots: 

341 return _Capture(read_item.lengths) 

342 recipe = self._recipe(key) 

343 if recipe.axes is None: 

344 return _Capture(read_item.lengths) 

345 return _direct_view(self._destination_box(read_item, recipe), recipe.axes, read_item.lengths) 

346 

347 def apply_tensor(self, read_item: ReadItem, tensor: Any) -> None: 

348 """ 

349 Put a captured read in place: gather a box out of it, or keep it for its deferred read. 

350 

351 Args: 

352 read_item (ReadItem): A read of this planner's plan. 

353 tensor (Any): What :meth:`acquire_tensor` returned, the read already copied into it. 

354 """ 

355 key = _recipe_key(read_item) 

356 slot = self._deferred_slots.get(key) 

357 if slot is not None: 

358 self._keep_deferred(slot, tensor.value) 

359 return 

360 recipe = self._recipe(key) 

361 if recipe.axes is not None: 

362 return 

363 self._destination_box(read_item, recipe).copy_(_gather(recipe.block, recipe.sub_lengths, tensor.value)) 

364 

365 def _shard_element_size(self, index: MetadataIndex) -> int: 

366 """ 

367 Bytes one element of a destination shard takes, which is what a broadcast of it moves. 

368 

369 Args: 

370 index (MetadataIndex): Names the shard. 

371 

372 Returns: 

373 int: Element size of the state dict entry, or the default size for a deferred read. 

374 """ 

375 dtype = getattr(self.state_dict.get(index.fqn), "dtype", None) if self.state_dict else None 

376 return dtype_element_size(None if dtype is None else str(dtype)) 

377 

378 def _plan_entry(self, fqn: str, obj: Any, blocks: Sequence[RemapBlock]) -> list[ReadItem]: 

379 """Plan the reads of one state dict entry, for the shards of it this rank holds.""" 

380 if not self._rank_owns_dtensor_shard(obj): 

381 return [] 

382 broadcastable = not _is_on_host(obj) 

383 items = [] 

384 for local_index, chunk in enumerate(create_chunk_list_for_tensor(obj)): 

385 dest_index = MetadataIndex(fqn=fqn, offset=chunk.offsets, index=local_index) 

386 chunk_area = chunk_to_area(chunk) 

387 for block in blocks: 

388 block_area = tuple((start, start + length) for start, length in zip(block.offsets, block.lengths)) 

389 overlap = infer_intersection(chunk_area, block_area) 

390 if overlap is not None: 

391 items.append(self._plan_block_read(dest_index, chunk.offsets, block, overlap, broadcastable)) 

392 return items 

393 

394 def _plan_block_read( 

395 self, 

396 dest_index: MetadataIndex, 

397 chunk_offsets: tuple[int, ...], 

398 block: RemapBlock, 

399 overlap: tuple[tuple[int, int], ...], 

400 broadcastable: bool, 

401 ) -> ReadItem: 

402 """Plan the read of the part of ``block`` that lands in one shard, and remember how to place it.""" 

403 sub_offsets = tuple(start for start, _ in overlap) 

404 sub_lengths = tuple(stop - start for start, stop in overlap) 

405 metadata = self._whole_tensor(block.source) 

406 storage_offsets = _source_start(block, sub_offsets) 

407 lengths = _source_lengths(block, sub_lengths) 

408 if len(lengths) != len(metadata.size) or any( 

409 start + length > size for start, length, size in zip(storage_offsets, lengths, metadata.size) 

410 ): 

411 raise ValueError( 

412 f"A block reads {lengths} at {storage_offsets} of checkpoint tensor {block.source!r}, " 

413 f"which has size {tuple(metadata.size)}" 

414 ) 

415 item = ReadItem( 

416 type=LoadItemType.TENSOR, 

417 dest_index=dest_index, 

418 dest_offsets=tuple(offset - origin for offset, origin in zip(sub_offsets, chunk_offsets)), 

419 storage_index=MetadataIndex(fqn=block.source, offset=metadata.chunks[0].offsets), 

420 storage_offsets=storage_offsets, 

421 lengths=lengths, 

422 broadcastable=broadcastable, 

423 ) 

424 self._recipes[_recipe_key(item)] = _Recipe(block, sub_lengths, _direct_axes(block, sub_lengths)) 

425 return item 

426 

427 def _plan_deferred(self, group_index: int, source: str) -> ReadItem: 

428 """Plan the whole-tensor read of one source of a deferred read.""" 

429 metadata = self._whole_tensor(source) 

430 size = tuple(metadata.size) 

431 item = ReadItem( 

432 type=LoadItemType.TENSOR, 

433 dest_index=MetadataIndex(fqn=f"{_DEFERRED_FQN_PREFIX}{group_index}/{source}"), 

434 dest_offsets=(), 

435 storage_index=MetadataIndex(fqn=source, offset=metadata.chunks[0].offsets), 

436 storage_offsets=(0,) * len(size), 

437 lengths=size, 

438 # Kept in host memory for the conversion that follows, which no collective can write into. 

439 broadcastable=False, 

440 ) 

441 self._deferred_slots[_recipe_key(item)] = (group_index, source) 

442 return item 

443 

444 def _whole_tensor(self, source: str) -> TensorStorageMetadata: 

445 """ 

446 Metadata of a checkpoint tensor stored whole, in one chunk. 

447 

448 Raises: 

449 ValueError: If the checkpoint has no such tensor, or stores it in shards. 

450 """ 

451 metadata = self.metadata.state_dict_metadata.get(source) 

452 if not isinstance(metadata, TensorStorageMetadata): 

453 raise ValueError(f"Checkpoint has no tensor {source!r}") 

454 chunks = metadata.chunks 

455 if len(chunks) != 1 or any(chunks[0].offsets) or tuple(chunks[0].sizes) != tuple(metadata.size): 

456 raise ValueError( 

457 f"Checkpoint tensor {source!r} is stored in {len(chunks)} shards; " 

458 "RemapLoadPlanner reads whole tensors only" 

459 ) 

460 return metadata 

461 

462 def _recipe(self, key: tuple) -> _Recipe: 

463 """The recipe of a read of the current plan.""" 

464 recipe = self._recipes.get(key) 

465 if recipe is None: 

466 raise RuntimeError(f"Read of {key[2].fqn!r} into {key[0].fqn!r} is not part of the current plan") 

467 return recipe 

468 

469 def _destination_box(self, read_item: ReadItem, recipe: _Recipe) -> Any: 

470 """The box of the destination shard one read fills, detached so it is written in place.""" 

471 target = self.state_dict[read_item.dest_index.fqn] 

472 if isinstance(target, DTensor) and target.layout is not None and target.layout.ragged_shard is not None: 

473 local = get_ragged_box_tensor(target, read_item.dest_index) 

474 elif isinstance(target, DTensor): 

475 local = target.to_local() 

476 else: 

477 local = target 

478 return narrow_tensor_by_index(local.detach(), read_item.dest_offsets, recipe.sub_lengths) 

479 

480 def _keep_deferred(self, slot: tuple[int, str], value: Any) -> None: 

481 """Keep one source of a deferred read, and complete the read once it has all of them.""" 

482 group_index, source = slot 

483 captured = self._captured.setdefault(group_index, {}) 

484 captured[source] = value 

485 group = self.deferred[group_index] 

486 if len(captured) == len(group.sources): 

487 del self._captured[group_index] 

488 group.complete(captured)