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

1318 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"""Generic optimizer-state swap runtime, slots and adapter base.""" 

16# pylint: disable=protected-access 

17 

18from __future__ import annotations 

19 

20import contextlib 

21import copy 

22import itertools 

23import logging 

24from dataclasses import dataclass, field 

25from typing import Any, Callable, Dict, Iterable, List, Optional, Protocol, Sequence, Tuple 

26 

27# Optimizer runtime is Torch-only; the legacy checker still flags core imports. 

28import torch # pylint: disable=forbidden-backend-import 

29from torch.optim import optimizer as torch_optimizer # pylint: disable=forbidden-backend-import 

30 

31logger = logging.getLogger(__name__) 

32 

33# Logical optimizer-state keys every swap family in this codebase understands. 

34# This is the vocabulary ``SwapOptimizerConfig.state_keys`` may name; each 

35# adapter still validates that a key belongs to its own optimizer family. 

36KNOWN_STATE_KEYS = ( 

37 "exp_avg", 

38 "exp_avg_sq", 

39 "max_exp_avg_sq", 

40 "momentum_buffer", 

41) 

42_PACKED_ALIGNMENT_BYTES = 512 

43_DEVICE_TYPE = "npu" 

44_MISSING_CHECKPOINT_STATE = object() 

45 

46 

47def _restore_dtensor_metadata(template: Any, local_tensor: Any) -> Any: 

48 """Wrap a local tensor with the logical DTensor layout of ``template``.""" 

49 to_local = getattr(template, "to_local", None) 

50 if not callable(to_local): 

51 return local_tensor 

52 

53 layout = getattr(template, "layout", None) 

54 if hasattr(layout, "tensor_stride"): 

55 logical_stride = layout.tensor_stride 

56 else: 

57 logical_stride = tuple(template.stride()) 

58 if logical_stride is None: 

59 logical_stride = [1] * len(template.shape) 

60 for index in range(len(logical_stride) - 2, -1, -1): 

61 logical_stride[index] = logical_stride[index + 1] * max(int(template.shape[index + 1]), 1) 

62 

63 return type(template).from_local( 

64 local_tensor, 

65 device_mesh=template.device_mesh, 

66 placements=template.placements, 

67 run_check=False, 

68 shape=template.shape, 

69 stride=tuple(logical_stride), 

70 ) 

71 

72 

73@dataclass 

74class SwapSlot: 

75 """One logical optimizer state tensor that may be swapped.""" 

76 

77 name: str 

78 tensor: Any 

79 cpu_tensor: Optional[Any] = None 

80 storage_nbytes: int = 0 

81 swappable: bool = True 

82 state: str = "device" 

83 event: Optional[Any] = None 

84 shape: tuple[int, ...] = () 

85 dtype: Optional[Any] = None 

86 device: Optional[Any] = None 

87 numel: int = 0 

88 host_offset: int = 0 

89 packed: bool = False 

90 logical_tensor: Optional[Any] = None 

91 

92 def bind_tensor(self, tensor: Any) -> None: 

93 """Bind the slot to a host or staging tensor view.""" 

94 device = getattr(tensor, "device", None) 

95 device_type = getattr(device, "type", None) 

96 if self.logical_tensor is not None: 

97 # DTensor exposes no public setter for replacing its local shard. 

98 # Rebind the shell on host too, so it cannot retain an arena view. 

99 setattr(self.logical_tensor, "_local_tensor", tensor) 

100 self.logical_tensor.data = tensor 

101 if self.logical_tensor is not None and device_type != "cpu": 

102 self.tensor = self.logical_tensor 

103 else: 

104 self.tensor = tensor 

105 

106 @property 

107 def checkpoint_tensor(self) -> Any: 

108 """Return the CPU tensor when present, otherwise the live tensor.""" 

109 return self.cpu_tensor if self.cpu_tensor is not None else self.tensor 

110 

111 

112class SwapUpdateUnit(Protocol): 

113 """Minimal contract the pipeline runtime requires of an update unit. 

114 

115 The runtime only ever reads a unit's swappable slots, so an update unit is 

116 free to carry algorithm-specific metadata (a single parameter and gradient, 

117 a Muon HSDP assignment, collective ordering, and so on) as long as it 

118 exposes ``slots``. 

119 """ 

120 

121 slots: List[SwapSlot] 

122 

123 

124@dataclass 

125class UpdateUnit: 

126 """Generic per-parameter update unit: parameter, gradient and swap slots. 

127 

128 Kept as a concrete unit for optimizers whose atomic update is one parameter, 

129 and as the runtime's default unit type. ``adapter_index`` identifies the 

130 owning parameter group. Optimizers with a richer atomic unit (Muon HSDP 

131 assignments, for example) define their own unit exposing ``slots``. 

132 """ 

133 

134 adapter_index: int 

135 param: Any 

136 grad: Any 

137 slots: List[SwapSlot] 

138 

139 

140class PipelineSwapRuntime: 

141 """Torch tensor storage/copy runtime and pipeline orchestration.""" 

142 

143 def __init__(self, config: Any) -> None: 

144 """Initialize instance-owned streams and packed storage metadata.""" 

145 self.config = config 

146 self._copy_stream: Optional[Any] = None 

147 self._packed_enabled = bool(getattr(config, "packed_swap", True)) 

148 self._host_buffers: Dict[Any, Any] = {} 

149 self._host_layout_signature = () 

150 self._packed_batch_plans: List[_PackedBatchPlan] = [] 

151 self._staging_arenas: List[Optional[_StagingArena]] = [None, None] 

152 self._packed_ready_events: Dict[int, Any] = {} 

153 self._packed_offload_events: Dict[int, Any] = {} 

154 self._packed_tail_event: Optional[Any] = None 

155 self._packed_device_views: Dict[Any, Any] = {} 

156 self._packed_slot_bind_hook: Optional[Callable[[SwapSlot], None]] = None 

157 

158 @property 

159 def packed_enabled(self) -> bool: 

160 """Return whether this runtime may build packed state candidates.""" 

161 return self._packed_enabled 

162 

163 @property 

164 def copy_stream(self) -> Optional[Any]: 

165 """Return the transfer stream after its lazy initialization.""" 

166 return self._copy_stream 

167 

168 def set_packed_slot_bind_hook(self, hook: Callable[[SwapSlot], None]) -> None: 

169 """Install the adapter hook that republishes a packed slot's live view.""" 

170 self._packed_slot_bind_hook = hook 

171 

172 def _bind_packed_slot(self, slot: SwapSlot, tensor: Any) -> None: 

173 """Bind a packed view and notify the owning state adapter.""" 

174 slot.bind_tensor(tensor) 

175 if self._packed_slot_bind_hook is not None: 

176 self._packed_slot_bind_hook(slot) 

177 

178 def partition( 

179 self, 

180 units: Sequence[SwapUpdateUnit], 

181 unit_costs: Optional[Sequence[int]] = None, 

182 ) -> List[List[SwapUpdateUnit]]: 

183 """Partition consecutive units using explicit costs or local state bytes. 

184 

185 Args: 

186 units: Atomic updates in execution order, including empty local units. 

187 unit_costs: Optional non-negative integer costs, one per unit. A 

188 distributed adapter supplies rank-independent planning weights. 

189 

190 Returns: 

191 Consecutive batches retaining every unit in its original order. 

192 

193 Raises: 

194 ValueError: If explicit costs have the wrong length or invalid values. 

195 """ 

196 ordered_units = list(units) 

197 if unit_costs is None: 

198 unit_costs = [self._unit_cost(unit) for unit in ordered_units] 

199 else: 

200 unit_costs = list(unit_costs) 

201 if len(unit_costs) != len(ordered_units): 

202 raise ValueError("unit_costs must contain exactly one cost per update unit.") 

203 if any(isinstance(cost, bool) or not isinstance(cost, int) or cost < 0 for cost in unit_costs): 

204 raise ValueError("unit_costs must contain non-negative integers.") 

205 if not ordered_units: 

206 return [] 

207 swap_times = max(1, min(int(self.config.swap_times), len(ordered_units))) 

208 unit_costs = [max(1, cost) for cost in unit_costs] 

209 remaining_cost = sum(unit_costs) 

210 batches: List[List[SwapUpdateUnit]] = [] 

211 start = 0 

212 for batch_index in range(swap_times - 1): 

213 remaining_batches = swap_times - batch_index 

214 

215 # Compare against the remaining average without introducing floating-point rounding. 

216 # Bind the loop state as defaults so the closure captures this iteration's values. 

217 def should_stop_growing( 

218 end: int, 

219 current_cost: int, 

220 next_cost: int, 

221 remaining_batches: int = remaining_batches, 

222 remaining_cost: int = remaining_cost, 

223 start: int = start, 

224 ) -> bool: 

225 """Return whether adding the next unit would not improve balance.""" 

226 current_distance = abs(remaining_cost - current_cost * remaining_batches) 

227 next_distance = abs(remaining_cost - (current_cost + next_cost) * remaining_batches) 

228 return end > start and current_distance <= next_distance 

229 

230 max_end = len(ordered_units) - remaining_batches + 1 

231 end, current_cost = self._grow_balanced_segment(unit_costs, start, max_end, should_stop_growing) 

232 batches.append(ordered_units[start:end]) 

233 remaining_cost -= current_cost 

234 start = end 

235 batches.append(ordered_units[start:]) 

236 return batches 

237 

238 def run_pipeline( 

239 self, 

240 batches: Sequence[Sequence[UpdateUnit]], 

241 step_context: Any, 

242 step_batch: Callable[[List[UpdateUnit], Any], Any], 

243 ) -> List[Any]: 

244 """Run one-batch-ahead prefetch while releasing completed offloads before widening the window.""" 

245 results = [] 

246 batch_lists = [list(batch) for batch in batches] 

247 if not batch_lists: 

248 return results 

249 

250 if self.supports_packed_pipeline(batch_lists): 

251 return self._run_packed_pipeline(batch_lists, step_context, step_batch) 

252 

253 try: 

254 self.prefetch(batch_lists[0]) # prefetch 0 

255 for index, batch_list in enumerate(batch_lists): 

256 self.wait_prefetch(batch_list) # wait_prefetch n 

257 

258 previous_index = index - 1 

259 if previous_index >= 0: 

260 self.wait_offload(batch_lists[previous_index]) # wait_offload n-1 

261 

262 next_index = index + 1 

263 if next_index < len(batch_lists): 

264 self.prefetch(batch_lists[next_index]) # prefetch n+1 

265 

266 results.append(step_batch(batch_list, step_context)) # update n 

267 self.refresh_swappable_slots(batch_list) 

268 self.offload(batch_list) # offload n 

269 

270 self.wait_offload(batch_lists[-1]) 

271 finally: 

272 self._drain_pending_transfers(batch_lists) 

273 return results 

274 

275 def _drain_pending_transfers(self, batch_lists: Sequence[Sequence[UpdateUnit]]) -> None: 

276 """Settle slots left in an intermediate ``h2d``/``d2h`` state. 

277 

278 Called from ``run_pipeline``'s ``finally`` so that an exception raised 

279 mid-pipeline (for example in ``step_batch``) cannot leave prefetched or 

280 offloaded slots holding device storage and a pending stream event. Slots 

281 that already reached a terminal state (``device`` or ``host``) are left 

282 untouched. 

283 """ 

284 for batch_list in batch_lists: 

285 self.wait_prefetch(batch_list) 

286 self.wait_offload(batch_list) 

287 

288 def _run_packed_pipeline( 

289 self, 

290 batches: Sequence[List[UpdateUnit]], 

291 step_context: Any, 

292 step_batch: Callable[[List[UpdateUnit], Any], Any], 

293 ) -> List[Any]: 

294 """Run updates with two reusable staging buffers. 

295 

296 Buffer parity is fixed by batch index. D2H batch ``n`` and H2D batch 

297 ``n + 2`` share one copy-stream chain while the other arena updates. 

298 """ 

299 results = [] 

300 try: 

301 self.begin_packed_step(batches) 

302 self.enqueue_packed_prefetch(0, 0) 

303 if len(batches) > 1: 

304 self.enqueue_packed_prefetch(1, 1) 

305 for batch_index, batch in enumerate(batches): 

306 staging_index = batch_index % 2 

307 self.wait_packed_prefetch(batch_index, staging_index) 

308 completed_index = batch_index - 2 

309 if completed_index >= 0: 

310 self.wait_packed_offload(completed_index) 

311 self.finish_packed_offload(completed_index) 

312 self.activate_packed_batch(batch_index, staging_index) 

313 results.append(step_batch(batch, step_context)) 

314 self.refresh_swappable_slots(batch) 

315 next_index = batch_index + 2 

316 self.enqueue_packed_offload_prefetch( 

317 batch_index, 

318 next_index if next_index < len(batches) else None, 

319 staging_index, 

320 ) 

321 drain_start = max(0, len(batches) - 2) 

322 for batch_index in range(drain_start, len(batches)): 

323 self.wait_packed_offload(batch_index) 

324 self.finish_packed_offload(batch_index) 

325 finally: 

326 self.release_packed_step_results(results) 

327 self.end_packed_step() 

328 return results 

329 

330 # Both hooks below are dispatched through ``self`` from the packed pipeline 

331 # above and are overridden per backend, so they stay instance 

332 # methods even though the base bodies only discard their arguments. 

333 def release_packed_step_results(self, results: List[Any]) -> None: 

334 """Release backend-specific update outputs before staging teardown.""" 

335 del results 

336 

337 def refresh_swappable_slots(self, batch: Sequence[UpdateUnit]) -> None: 

338 """Refresh slots that become swappable after the optimizer update.""" 

339 del batch 

340 

341 def synchronize_cpu_mirrors(self, slots: Iterable[SwapSlot]) -> None: 

342 """Ensure CPU mirrors contain latest data for checkpointing.""" 

343 slot_list = [slot for slot in _iter_unique_slot_objects(slots) if slot.swappable] 

344 if not slot_list: 

345 return 

346 

347 compute_stream = self.current_stream() 

348 with self.stream_context(compute_stream): 

349 for event in _iter_unique_events(slot_list): 

350 self.wait_event(event, compute_stream) 

351 

352 for slot in slot_list: 

353 if slot.state == "host": 

354 if slot.cpu_tensor is None: 

355 raise RuntimeError(f"Swap slot {slot.name!r} is host-resident but has no CPU mirror.") 

356 continue 

357 if slot.state != "d2h": 

358 self.copy_to_cpu(slot) 

359 self.wait_offload_slot(slot) 

360 

361 checkpoint_event = self.record_event(compute_stream) if compute_stream is not None else None 

362 

363 # Storage release only needs stream ordering above. Host completion is 

364 # required separately because checkpoint_state_dict reads CPU mirrors. 

365 self.wait_event(checkpoint_event, None) 

366 for slot in slot_list: 

367 slot.event = None 

368 self.restore_packed_host_views(slot_list) 

369 

370 def materialize_slots(self, slots: Iterable[SwapSlot]) -> None: 

371 """Materialize host-resident slots on their original device. 

372 

373 HSDP checkpoint broadcasts operate on device tensors. Swap slots are 

374 normally host-resident between optimizer steps, so checkpoint code uses 

375 this helper to reuse the normal H2D stream/event ordering before issuing 

376 a collective. 

377 """ 

378 slot_list = [ 

379 slot for slot in _iter_unique_slot_objects(slots) 

380 if slot.swappable and slot.state == "host" 

381 ] 

382 if not slot_list: 

383 return 

384 for slot in slot_list: 

385 if slot.packed: 

386 device_tensor = torch.empty(slot.shape, dtype=slot.dtype, device=slot.device) 

387 self._bind_packed_slot(slot, device_tensor) 

388 self.prefetch([UpdateUnit(0, None, None, slot_list)]) 

389 self.wait_prefetch([UpdateUnit(0, None, None, slot_list)]) 

390 

391 def restore_packed_host_views(self, slots: Iterable[SwapSlot]) -> None: 

392 """Restore persistent packed host views after temporary materialization.""" 

393 for slot in _iter_unique_slot_objects(slots): 

394 if (slot.packed and slot.state == "host" and slot.cpu_tensor is not None 

395 and slot.tensor is not slot.cpu_tensor): 

396 self._bind_packed_slot(slot, slot.cpu_tensor) 

397 

398 def prefetch(self, batch: Sequence[UpdateUnit]) -> None: 

399 """Prefetch batch slots from CPU to device.""" 

400 slots = [slot for slot in _iter_unique_slots(batch) if slot.swappable and slot.state == "host"] 

401 if not slots: 

402 return 

403 

404 for slot in slots: 

405 if slot.cpu_tensor is None: 

406 raise RuntimeError(f"Swap slot {slot.name!r} is host-resident but has no CPU mirror.") 

407 

408 restored_slots = [] 

409 try: 

410 for slot in slots: 

411 self.restore_device_storage(slot) 

412 restored_slots.append(slot) 

413 except Exception: 

414 for restored_slot in reversed(restored_slots): 

415 self.release_device_storage(restored_slot) 

416 raise 

417 

418 copy_stream = self._get_copy_stream() 

419 compute_event = self._record_current_stream_event() if copy_stream is not None else None 

420 with self.stream_context(copy_stream): 

421 self.wait_event(compute_event, copy_stream) 

422 for slot in slots: 

423 self.copy_to_device(slot) 

424 slot.state = "h2d" 

425 copy_event = self.record_event(copy_stream) if copy_stream is not None else None 

426 for slot in slots: 

427 slot.event = copy_event 

428 

429 def wait_prefetch(self, batch: Sequence[UpdateUnit]) -> None: 

430 """Wait for batch prefetch copies.""" 

431 slots = [slot for slot in _iter_unique_slots(batch) if slot.swappable and slot.state == "h2d"] 

432 if not slots: 

433 return 

434 

435 compute_stream = self.current_stream() 

436 with self.stream_context(compute_stream): 

437 for event in _iter_unique_events(slots): 

438 self.wait_event(event, compute_stream) 

439 for slot in slots: 

440 self.wait_prefetch_slot(slot) 

441 slot.event = None 

442 

443 def offload(self, batch: Sequence[UpdateUnit]) -> None: 

444 """Offload batch slots from device to CPU.""" 

445 slots = [slot for slot in _iter_unique_slots(batch) if slot.swappable and slot.state == "device"] 

446 if not slots: 

447 return 

448 

449 self._enqueue_offload_slots(slots) 

450 

451 def offload_initial_slots(self, slots: Iterable[SwapSlot]) -> None: 

452 """Offload existing device-resident slots before the first optimizer update.""" 

453 slot_list = [ 

454 slot for slot in _iter_unique_slot_objects(slots) 

455 if slot.swappable and slot.state == "device" 

456 ] 

457 if not slot_list: 

458 return 

459 

460 self._enqueue_offload_slots(slot_list) 

461 # Waiting here also releases device storage. Deferring this until the first 

462 # prefetch would leave cold optimizer states resident during forward/backward. 

463 self._wait_offload_slots(slot_list) 

464 

465 def _enqueue_offload_slots(self, slots: Sequence[SwapSlot]) -> None: 

466 """Enqueue D2H copies for device-resident slots.""" 

467 copy_stream = self._get_copy_stream() 

468 compute_event = self._record_current_stream_event() if copy_stream is not None else None 

469 with self.stream_context(copy_stream): 

470 self.wait_event(compute_event, copy_stream) 

471 for slot in slots: 

472 self.copy_to_cpu(slot) 

473 slot.state = "d2h" 

474 copy_event = self.record_event(copy_stream) if copy_stream is not None else None 

475 for slot in slots: 

476 slot.event = copy_event 

477 

478 def wait_offload(self, batch: Sequence[UpdateUnit]) -> None: 

479 """Wait for batch offload copies.""" 

480 slots = [slot for slot in _iter_unique_slots(batch) if slot.swappable and slot.state == "d2h"] 

481 if not slots: 

482 return 

483 

484 self._wait_offload_slots(slots) 

485 

486 def _wait_offload_slots(self, slots: Sequence[SwapSlot]) -> None: 

487 """Wait for D2H copies and release device storage for copied slots.""" 

488 compute_stream = self.current_stream() 

489 with self.stream_context(compute_stream): 

490 for event in _iter_unique_events(slots): 

491 self.wait_event(event, compute_stream) 

492 for slot in slots: 

493 self.wait_offload_slot(slot) 

494 slot.event = None 

495 

496 @staticmethod 

497 def _unit_cost(unit: UpdateUnit) -> int: 

498 return sum(slot.storage_nbytes for slot in unit.slots if slot.swappable) 

499 

500 @staticmethod 

501 def _grow_balanced_segment( 

502 unit_costs: Sequence[int], 

503 start: int, 

504 max_end: int, 

505 should_stop: Callable[[int, int, int], bool], 

506 ) -> Tuple[int, int]: 

507 """Extend a segment from ``start`` while ``should_stop`` stays false. 

508 

509 Returns the exclusive end index and the accumulated cost of ``[start, end)``. 

510 """ 

511 end = start 

512 current_cost = 0 

513 while end < max_end: 

514 next_cost = unit_costs[end] 

515 if should_stop(end, current_cost, next_cost): 

516 break 

517 current_cost += next_cost 

518 end += 1 

519 return end, current_cost 

520 

521 def _get_copy_stream(self) -> Any: 

522 if self._copy_stream is None: 

523 self._copy_stream = self.new_stream() 

524 return self._copy_stream 

525 

526 def _record_current_stream_event(self) -> Any: 

527 current_stream = self.current_stream() 

528 return self.record_event(current_stream) 

529 

530 def populate_slot_metadata(self, slot: SwapSlot, template: Any) -> None: 

531 """Populate stable logical tensor metadata without allocating device state.""" 

532 storage_tensor = self._storage_tensor(template) 

533 slot.shape = tuple(storage_tensor.shape) 

534 slot.dtype = storage_tensor.dtype 

535 slot.device = storage_tensor.device 

536 slot.numel = int(storage_tensor.numel()) 

537 slot.storage_nbytes = slot.numel * int(storage_tensor.element_size()) 

538 

539 def is_packable_template(self, tensor: Any, min_numel: int) -> bool: 

540 """Return whether a state shaped like ``tensor`` can use packed staging.""" 

541 if not self._packed_enabled: 

542 return False 

543 return self.is_swappable_tensor(tensor, min_numel) 

544 

545 @staticmethod 

546 def is_distributed_tensor(tensor: Any) -> bool: 

547 """Return whether ``tensor`` exposes a DTensor local shard.""" 

548 return tensor is not None and callable(getattr(tensor, "to_local", None)) 

549 

550 def prepare_packed_host(self, slots: Sequence[SwapSlot]) -> None: 

551 """Pack logical optimizer states into persistent pinned buffers by dtype.""" 

552 if not self._packed_enabled: 

553 return 

554 

555 packed_slots = [slot for slot in slots if slot.swappable and slot.packed] 

556 signature = self._validate_packed_host_layout(packed_slots) 

557 if signature == self._host_layout_signature: 

558 return 

559 

560 self._prepare_packed_host_sources(packed_slots) 

561 slots_by_dtype: Dict[Any, List[SwapSlot]] = {} 

562 for slot in packed_slots: 

563 slots_by_dtype.setdefault(slot.dtype, []).append(slot) 

564 

565 self._host_buffers = { 

566 dtype: self._pack_dtype_slots(dtype, dtype_slots) 

567 for dtype, dtype_slots in slots_by_dtype.items() 

568 } 

569 self._host_layout_signature = signature 

570 

571 def _validate_packed_host_layout( 

572 self, packed_slots: Sequence[SwapSlot], 

573 ) -> Tuple[Tuple[Any, ...], ...]: 

574 """Validate candidates and return their persistent layout signature.""" 

575 if self._packed_batch_plans or self._packed_tail_event is not None: 

576 raise RuntimeError("Cannot rebuild packed optimizer layout during an active transfer.") 

577 slot_ids = [id(slot) for slot in packed_slots] 

578 if len(slot_ids) != len(set(slot_ids)): 

579 raise RuntimeError("Packed optimizer layout contains a duplicate state slot.") 

580 return tuple( 

581 (id(slot), slot.dtype, slot.shape, slot.numel, slot.device) 

582 for slot in packed_slots 

583 ) 

584 

585 def _prepare_packed_host_sources(self, packed_slots: Sequence[SwapSlot]) -> None: 

586 """Validate source layouts and settle copies before rebuilding host buffers.""" 

587 for slot in packed_slots: 

588 source = slot.checkpoint_tensor 

589 if source is not None: 

590 local = self._storage_tensor(source) 

591 if local.layout != torch.strided or not local.is_contiguous(): 

592 raise ValueError("Packed optimizer states must be dense and contiguous before host packing.") 

593 # A layout rebuild reads CPU mirrors on the host. Stream ordering alone 

594 # is insufficient, including after an initial-state offload. 

595 if self._copy_stream is not None: 

596 self._copy_stream.synchronize() 

597 for event in _iter_unique_events(packed_slots): 

598 self.wait_event(event, None) 

599 if any(slot.state in ("h2d", "d2h") for slot in packed_slots): 

600 self.synchronize_cpu_mirrors(packed_slots) 

601 

602 def invalidate_packed_layout(self) -> None: 

603 """Discard an idle packed layout after state slots have been rebound. 

604 

605 Persistent slot mirrors retain their values until the next host packing. 

606 The instance's copy stream remains available for subsequent steps. 

607 """ 

608 if self._packed_batch_plans or self._packed_tail_event is not None: 

609 raise RuntimeError("Cannot reset optimizer parameters during a packed transfer.") 

610 for arena in self._staging_arenas: 

611 if arena is not None: 

612 arena.dtype_views.clear() 

613 arena.raw_buffer.untyped_storage().resize_(0) 

614 self._staging_arenas = [None, None] 

615 self._packed_device_views.clear() 

616 self._packed_ready_events.clear() 

617 self._packed_offload_events.clear() 

618 self._host_buffers = {} 

619 self._host_layout_signature = () 

620 

621 def _pack_dtype_slots(self, dtype: Any, dtype_slots: Sequence[SwapSlot]) -> Any: 

622 """Pack every slot of one dtype into a single pinned host buffer. 

623 

624 Each slot is re-pointed at its own view inside the new buffer and marked 

625 host-resident, so the packed buffer becomes the slot's CPU mirror from 

626 here on. Any device copy that fed the mirror is released immediately 

627 afterwards, because the host view now owns the only valid values. 

628 """ 

629 total_numel = sum(slot.numel for slot in dtype_slots) 

630 host_buffer = torch.empty(total_numel, dtype=dtype, device="cpu", pin_memory=True) 

631 host_offset = 0 

632 for slot in dtype_slots: 

633 flat_view = host_buffer.narrow(0, host_offset, slot.numel) 

634 host_view = flat_view.view(slot.shape) 

635 source = slot.tensor if slot.state == "device" else slot.checkpoint_tensor 

636 if source is None: 

637 host_view.zero_() 

638 else: 

639 source_tensor = self._storage_tensor(source) 

640 host_view.copy_(source_tensor.detach().reshape(-1).view(slot.shape), non_blocking=False) 

641 if source_tensor.device.type != "cpu" and source is slot.tensor: 

642 self.release_device_storage(slot) 

643 slot.host_offset = host_offset 

644 slot.cpu_tensor = host_view 

645 slot.bind_tensor(host_view) 

646 slot.state = "host" 

647 slot.event = None 

648 host_offset += slot.numel 

649 return host_buffer 

650 

651 def is_swappable_tensor(self, tensor: Any, min_numel: int) -> bool: 

652 """Return whether ``tensor`` can participate in swap.""" 

653 storage_tensor = self._storage_tensor(tensor) 

654 if not isinstance(storage_tensor, torch.Tensor): 

655 return False 

656 if not storage_tensor.is_floating_point(): 

657 return False 

658 if int(storage_tensor.numel()) < int(min_numel): 

659 return False 

660 if storage_tensor.is_sparse: 

661 return False 

662 if not storage_tensor.is_contiguous(): 

663 return False 

664 if storage_tensor.device.type == "cpu": 

665 return False 

666 try: 

667 storage_size = int(storage_tensor.untyped_storage().size()) 

668 expected_size = int(storage_tensor.numel()) * int(storage_tensor.element_size()) 

669 if storage_size != expected_size: 

670 return False 

671 except RuntimeError: 

672 return False 

673 return True 

674 

675 def storage_nbytes(self, tensor: Any) -> int: 

676 """Return storage bytes for a Torch tensor.""" 

677 storage_tensor = self._storage_tensor(tensor) 

678 if not isinstance(storage_tensor, torch.Tensor): 

679 return 0 

680 try: 

681 return int(storage_tensor.untyped_storage().size()) 

682 except RuntimeError: 

683 return int(storage_tensor.numel()) * int(storage_tensor.element_size()) 

684 

685 def make_cpu_tensor(self, tensor: Any) -> Any: 

686 """Create a CPU mirror tensor.""" 

687 storage_tensor = self._storage_tensor(tensor) 

688 if isinstance(storage_tensor, torch.Tensor): 

689 source = storage_tensor.detach() 

690 try: 

691 cpu_tensor = torch.empty_like(source, device="cpu", pin_memory=True) 

692 except RuntimeError: 

693 cpu_tensor = torch.empty_like(source, device="cpu") 

694 cpu_tensor.copy_(source, non_blocking=True) 

695 return cpu_tensor 

696 raise ValueError(f"Expected torch.Tensor for CPU mirror, got {type(tensor)!r}.") 

697 

698 def make_zero_cpu_tensor_like(self, tensor: Any) -> Any: 

699 """Create a zero-valued CPU mirror without materializing device state.""" 

700 storage_tensor = self._storage_tensor(tensor) 

701 if not isinstance(storage_tensor, torch.Tensor): 

702 raise ValueError(f"Expected torch.Tensor for CPU mirror, got {type(tensor)!r}.") 

703 try: 

704 cpu_tensor = torch.empty_like(storage_tensor, device="cpu", pin_memory=True) 

705 except RuntimeError: 

706 cpu_tensor = torch.empty_like(storage_tensor, device="cpu") 

707 cpu_tensor.zero_() 

708 return cpu_tensor 

709 

710 @staticmethod 

711 def make_device_tensor_like(param: Any, saved_tensor: Any) -> Any: 

712 """Create a live state tensor on the parameter device.""" 

713 if not isinstance(saved_tensor, torch.Tensor): 

714 raise ValueError(f"Expected torch.Tensor in optimizer state, got {type(saved_tensor)!r}.") 

715 return saved_tensor.detach().to(device=param.device, dtype=saved_tensor.dtype).clone() 

716 

717 def make_empty_device_tensor_like(self, param: Any, saved_tensor: Any) -> Any: 

718 """Create an uninitialized live state tensor shell on the parameter device.""" 

719 if not isinstance(saved_tensor, torch.Tensor): 

720 raise ValueError(f"Expected torch.Tensor in optimizer state, got {type(saved_tensor)!r}.") 

721 if self.is_distributed_tensor(param): 

722 return self.make_empty_state_like(param) 

723 return torch.empty_like(saved_tensor, device=param.device, dtype=saved_tensor.dtype) 

724 

725 def make_empty_state_like(self, param: Any) -> Any: 

726 """Allocate a state shell with the parameter's local and logical metadata.""" 

727 local = torch.empty_like(self._storage_tensor(param), memory_format=torch.preserve_format) 

728 # Explicit from_local preserves layout even inside SkipDTensorDispatch, 

729 # where empty_like(param) would otherwise return a plain local tensor. 

730 return _restore_dtensor_metadata(param, local) 

731 

732 def copy_to_device(self, slot: SwapSlot) -> None: 

733 """Copy one CPU mirror to device tensor.""" 

734 if slot.state != "host": 

735 return 

736 if slot.cpu_tensor is None: 

737 return 

738 self._storage_tensor(slot.tensor).copy_(slot.cpu_tensor, non_blocking=True) 

739 slot.state = "h2d" 

740 

741 def wait_prefetch_slot(self, slot: SwapSlot) -> None: 

742 """Torch fallback copies are synchronous on CPU and stream-ordered on device.""" 

743 slot.state = "device" 

744 

745 def copy_to_cpu(self, slot: SwapSlot) -> None: 

746 """Copy one device tensor to CPU mirror.""" 

747 source = self._storage_tensor(slot.tensor) 

748 if slot.cpu_tensor is None: 

749 slot.cpu_tensor = self.make_cpu_tensor(source) 

750 else: 

751 slot.cpu_tensor.copy_(source.detach(), non_blocking=True) 

752 slot.state = "d2h" 

753 

754 def wait_offload_slot(self, slot: SwapSlot) -> None: 

755 """Release device storage after D2H copy completes.""" 

756 if self._storage_tensor(slot.tensor).device.type != "cpu": 

757 self.release_device_storage(slot) 

758 slot.state = "host" 

759 

760 def restore_device_storage(self, slot: SwapSlot) -> None: 

761 """Restore device tensor storage before H2D.""" 

762 storage_tensor = self._storage_tensor(slot.tensor) 

763 if storage_tensor.device.type == "cpu": 

764 return 

765 storage = storage_tensor.untyped_storage() 

766 if storage.size() != slot.storage_nbytes: 

767 storage.resize_(slot.storage_nbytes) 

768 

769 def release_device_storage(self, slot: SwapSlot) -> None: 

770 """Release device storage for a swappable tensor.""" 

771 storage_tensor = self._storage_tensor(slot.tensor) 

772 if storage_tensor.device.type == "cpu": 

773 return 

774 storage = storage_tensor.untyped_storage() 

775 if storage.size() != 0: 

776 storage.resize_(0) 

777 

778 @staticmethod 

779 def device_handle() -> Any: 

780 """Return the Torch device module (``torch.npu``) that owns streams and events.""" 

781 try: 

782 return getattr(torch, _DEVICE_TYPE) 

783 except AttributeError as error: 

784 raise RuntimeError( 

785 f"Swap optimizer expects device handle: 'torch.{_DEVICE_TYPE}' failed." 

786 ) from error 

787 

788 def current_stream(self) -> Any: 

789 """Return the current compute stream.""" 

790 return self.device_handle().current_stream() 

791 

792 def new_stream(self) -> Any: 

793 """Create the copy stream.""" 

794 return self.device_handle().Stream() 

795 

796 def stream_context(self, stream: Any) -> Any: 

797 """Return a stream context for ``stream``.""" 

798 if stream is None: 

799 return contextlib.nullcontext() 

800 return self.device_handle().stream(stream) 

801 

802 def record_event(self, stream: Any = None) -> Any: 

803 """Record an event on ``stream``.""" 

804 event = self.device_handle().Event() 

805 if stream is None: 

806 event.record() 

807 else: 

808 event.record(stream) 

809 return event 

810 

811 @staticmethod 

812 def wait_event(event: Any, stream: Any = None) -> None: 

813 """Make ``stream`` wait for ``event``.""" 

814 if event is None: 

815 return 

816 if stream is None: 

817 event.synchronize() 

818 return 

819 event.wait(stream) 

820 

821 def supports_packed_pipeline(self, batches: Sequence[Sequence[UpdateUnit]]) -> bool: 

822 """Make the final, step-specific decision to use the packed pipeline.""" 

823 if not self._packed_enabled or not batches: 

824 return False 

825 units = itertools.chain.from_iterable(batches) 

826 slots = itertools.chain.from_iterable(unit.slots for unit in units) 

827 swappable_slots = [slot for slot in slots if slot.swappable] 

828 if not swappable_slots: 

829 return False 

830 if len({id(slot) for slot in swappable_slots}) != len(swappable_slots): 

831 raise RuntimeError("Packed optimizer batches contain a duplicate state slot.") 

832 devices = {slot.device for slot in swappable_slots} 

833 eligible = len(devices) == 1 and None not in devices and all( 

834 slot.packed 

835 and slot.cpu_tensor is not None 

836 and slot.dtype in self._host_buffers 

837 for slot in swappable_slots 

838 ) 

839 # A packed slot always carries a host mirror and a host-buffer dtype once 

840 # ``prepare_packed_host`` has run, so the only remaining way for 

841 # ``eligible`` to be false is a device mismatch. Per-tensor swap cannot 

842 # recover from that -- the slot tensor is already a CPU host view -- so 

843 # every packed slot must fail loudly rather than fall through silently. 

844 if not eligible and any(slot.packed for slot in swappable_slots): 

845 raise RuntimeError( 

846 "Packed optimizer states cannot fall back to per-tensor swap after " 

847 f"host packing; packed pipeline eligibility failed for local devices {sorted(map(str, devices))}." 

848 ) 

849 return eligible 

850 

851 @staticmethod 

852 def _storage_tensor(tensor: Any) -> Any: 

853 """Return the local tensor whose storage is managed by this runtime.""" 

854 to_local = getattr(tensor, "to_local", None) 

855 return to_local() if callable(to_local) else tensor 

856 

857 @staticmethod 

858 def _first_swappable_slot(batches: Sequence[Sequence[UpdateUnit]]) -> SwapSlot: 

859 """Return the first swappable slot across ``batches``, in iteration order.""" 

860 units = itertools.chain.from_iterable(batches) 

861 slots = itertools.chain.from_iterable(unit.slots for unit in units) 

862 swappable = (slot for slot in slots if slot.swappable) 

863 return next(swappable) 

864 

865 def begin_packed_step(self, batches: Sequence[Sequence[UpdateUnit]]) -> None: 

866 """Build batch transfer plans and materialize two raw staging buffers.""" 

867 first_slot = self._first_swappable_slot(batches) 

868 # FSDP may leave gradient reductions/reshards queued on auxiliary 

869 # streams. A current-stream event cannot cover those streams, so the 

870 # device-wide synchronization is required before optimizer reads state. 

871 getattr(torch, first_slot.device.type).synchronize(first_slot.device) 

872 self._packed_tail_event = None 

873 self._packed_batch_plans = [self._build_packed_batch_plan(batch) for batch in batches] 

874 dtype_layouts, total_bytes, device = self._packed_layout(self._packed_batch_plans) 

875 

876 for staging_index in range(2): 

877 arena = self._materialize_staging_arena(staging_index, total_bytes, device) 

878 layout_signature = tuple( 

879 (dtype, offset, num_bytes) for dtype, (offset, num_bytes) in dtype_layouts.items() 

880 ) 

881 if arena.layout_signature != layout_signature: 

882 arena.dtype_views = { 

883 dtype: arena.raw_buffer.narrow(0, offset, num_bytes).view(dtype) 

884 for dtype, (offset, num_bytes) in dtype_layouts.items() 

885 } 

886 arena.layout_signature = layout_signature 

887 self._drop_packed_views(staging_index) 

888 self._packed_ready_events = {} 

889 self._packed_offload_events = {} 

890 

891 def enqueue_packed_prefetch(self, batch_index: int, staging_index: int) -> None: 

892 """Enqueue one packed H2D and record its ready event.""" 

893 copy_stream = self._get_copy_stream() 

894 with self.stream_context(copy_stream): 

895 self._copy_packed_to_device(batch_index, staging_index) 

896 ready_event = self.record_event(copy_stream) 

897 self._packed_ready_events[batch_index] = ready_event 

898 self._packed_tail_event = ready_event 

899 

900 def wait_packed_prefetch(self, batch_index: int, staging_index: int) -> None: 

901 """Order the compute stream after the batch's packed transfer chain.""" 

902 del staging_index 

903 ready_event = self._packed_ready_events.get(batch_index) 

904 if ready_event is None: 

905 raise RuntimeError(f"Packed optimizer batch {batch_index} has no ready event.") 

906 self.wait_event(ready_event, self.current_stream()) 

907 

908 def activate_packed_batch(self, batch_index: int, staging_index: int) -> None: 

909 """Bind each swap slot to its slice of one staging arena.""" 

910 batch_plan = self._packed_batch_plans[batch_index] 

911 arena = self._require_staging_arena(staging_index) 

912 for dtype, region in batch_plan.regions.items(): 

913 dtype_view = arena.dtype_views[dtype] 

914 for slot in region.slots: 

915 relative_offset = slot.host_offset - region.host_offset 

916 cache_key = ( 

917 staging_index, 

918 id(arena.raw_buffer), 

919 id(dtype_view), 

920 id(slot), 

921 dtype, 

922 relative_offset, 

923 slot.numel, 

924 slot.shape, 

925 ) 

926 device_view = self._packed_device_views.get(cache_key) 

927 if device_view is None: 

928 device_view = dtype_view.narrow(0, relative_offset, slot.numel).view(slot.shape) 

929 self._packed_device_views[cache_key] = device_view 

930 self._bind_packed_slot(slot, device_view) 

931 slot.state = "device" 

932 slot.event = None 

933 

934 def enqueue_packed_offload_prefetch( 

935 self, 

936 batch_index: int, 

937 next_index: Optional[int], 

938 staging_index: int, 

939 ) -> None: 

940 """Serialize current D2H and next same-parity H2D on the copy stream.""" 

941 copy_stream = self._get_copy_stream() 

942 compute_event = self._record_current_stream_event() 

943 with self.stream_context(copy_stream): 

944 self.wait_event(compute_event, copy_stream) 

945 self._copy_packed_to_host(batch_index, staging_index) 

946 # This arena may be overwritten by the following H2D. Exception 

947 # cleanup must use the host result once its D2H has been submitted. 

948 for region in self._packed_batch_plans[batch_index].regions.values(): 

949 for slot in region.slots: 

950 slot.state = "d2h" 

951 if next_index is not None: 

952 self._copy_packed_to_device(next_index, staging_index) 

953 chain_event = self.record_event(copy_stream) 

954 self._packed_offload_events[batch_index] = chain_event 

955 self._packed_tail_event = chain_event 

956 if next_index is not None: 

957 self._packed_ready_events[next_index] = chain_event 

958 

959 def wait_packed_offload(self, batch_index: int) -> None: 

960 """Order the compute stream after a packed transfer chain.""" 

961 offload_event = self._packed_offload_events.get(batch_index) 

962 if offload_event is None: 

963 raise RuntimeError(f"Packed optimizer batch {batch_index} has no offload event.") 

964 self.wait_event(offload_event, self.current_stream()) 

965 

966 def finish_packed_offload(self, batch_index: int) -> None: 

967 """Make persistent pinned views authoritative after D2H completion.""" 

968 batch_plan = self._packed_batch_plans[batch_index] 

969 for region in batch_plan.regions.values(): 

970 for slot in region.slots: 

971 self._bind_packed_slot(slot, slot.cpu_tensor) 

972 slot.state = "host" 

973 slot.event = None 

974 

975 def end_packed_step(self) -> None: 

976 """Drain the packed copy chain and release step-local staging storage.""" 

977 if self._copy_stream is not None: 

978 # A submission can fail after enqueueing a copy but before recording 

979 # its tail event, even when no batch has activated a device view yet. 

980 self._copy_stream.synchronize() 

981 elif self._packed_tail_event is not None: 

982 self.wait_event(self._packed_tail_event, None) 

983 active_slots: Dict[int, Any] = {} 

984 for batch_plan in self._packed_batch_plans: 

985 for region in batch_plan.regions.values(): 

986 for slot in region.slots: 

987 if slot.state in ("device", "d2h"): 

988 active_slots[id(slot)] = slot 

989 if active_slots: 

990 compute_stream = self.current_stream() 

991 synchronize = getattr(compute_stream, "synchronize", None) 

992 if synchronize is not None: 

993 synchronize() 

994 for slot in active_slots.values(): 

995 if slot.state == "device": 

996 slot.cpu_tensor.copy_(self._storage_tensor(slot.tensor).detach(), non_blocking=False) 

997 self._bind_packed_slot(slot, slot.cpu_tensor) 

998 slot.state = "host" 

999 slot.event = None 

1000 for arena in self._staging_arenas: 

1001 if arena is None: 

1002 continue 

1003 # Drop views before shrinking the raw storage; otherwise a view 

1004 # can keep the device allocation alive after the step ends. 

1005 arena.dtype_views = {} 

1006 arena.layout_signature = None 

1007 storage = arena.raw_buffer.untyped_storage() 

1008 if storage.size() != 0: 

1009 storage.resize_(0) 

1010 self._packed_device_views = {} 

1011 self._packed_batch_plans = [] 

1012 self._packed_ready_events = {} 

1013 self._packed_offload_events = {} 

1014 self._packed_tail_event = None 

1015 

1016 @staticmethod 

1017 def _build_packed_batch_plan(batch: Sequence[UpdateUnit]) -> _PackedBatchPlan: 

1018 """Group a batch's packed slots into contiguous host regions by dtype.""" 

1019 slots_by_dtype: Dict[Any, List[SwapSlot]] = {} 

1020 seen_slots = set() 

1021 for unit in batch: 

1022 for slot in unit.slots: 

1023 if not slot.swappable or not slot.packed: 

1024 continue 

1025 if id(slot) in seen_slots: 

1026 raise RuntimeError("Packed optimizer batch contains a duplicate state slot.") 

1027 slots_by_dtype.setdefault(slot.dtype, []).append(slot) 

1028 seen_slots.add(id(slot)) 

1029 

1030 regions = {} 

1031 for dtype, slots in slots_by_dtype.items(): 

1032 host_offset = slots[0].host_offset 

1033 expected_offset = host_offset 

1034 for slot in slots: 

1035 if slot.host_offset != expected_offset: 

1036 raise RuntimeError( 

1037 f"Packed optimizer batch has a non-contiguous {dtype} host range at slot {slot.name!r}." 

1038 ) 

1039 expected_offset += slot.numel 

1040 regions[dtype] = _PackedBatchRegion(dtype, host_offset, expected_offset - host_offset, slots) 

1041 return _PackedBatchPlan(regions) 

1042 

1043 def _materialize_staging_arena(self, staging_index: int, total_bytes: int, device: Any) -> _StagingArena: 

1044 """Allocate or resize one packed device staging arena for the requested layout.""" 

1045 arena = self._staging_arenas[staging_index] 

1046 if arena is None or arena.raw_buffer.device != device: 

1047 raw_buffer = torch.empty(total_bytes, dtype=torch.uint8, device=device) 

1048 arena = _StagingArena(raw_buffer) 

1049 self._staging_arenas[staging_index] = arena 

1050 self._drop_packed_views(staging_index) 

1051 return arena 

1052 

1053 raw_buffer = arena.raw_buffer 

1054 storage = raw_buffer.untyped_storage() 

1055 if storage.size() < total_bytes: 

1056 storage.resize_(total_bytes) 

1057 arena.dtype_views = {} 

1058 arena.layout_signature = None 

1059 self._drop_packed_views(staging_index) 

1060 raw_buffer.set_(storage, 0, (total_bytes,), (1,)) 

1061 return arena 

1062 

1063 def _drop_packed_views(self, staging_index: int) -> None: 

1064 """Drop cached views for one arena after its storage/layout changes.""" 

1065 self._packed_device_views = { 

1066 key: value for key, value in self._packed_device_views.items() if key[0] != staging_index 

1067 } 

1068 

1069 def _copy_packed_to_device(self, batch_index: int, staging_index: int) -> None: 

1070 batch_plan = self._packed_batch_plans[batch_index] 

1071 arena = self._require_staging_arena(staging_index) 

1072 for dtype, region in batch_plan.regions.items(): 

1073 host_view = self._host_buffers[dtype].narrow(0, region.host_offset, region.numel) 

1074 arena.dtype_views[dtype].narrow(0, 0, region.numel).copy_(host_view, non_blocking=True) 

1075 

1076 def _copy_packed_to_host(self, batch_index: int, staging_index: int) -> None: 

1077 batch_plan = self._packed_batch_plans[batch_index] 

1078 arena = self._require_staging_arena(staging_index) 

1079 for dtype, region in batch_plan.regions.items(): 

1080 host_view = self._host_buffers[dtype].narrow(0, region.host_offset, region.numel) 

1081 host_view.copy_(arena.dtype_views[dtype].narrow(0, 0, region.numel), non_blocking=True) 

1082 

1083 def _require_staging_arena(self, staging_index: int) -> _StagingArena: 

1084 arena = self._staging_arenas[staging_index] 

1085 if arena is None: 

1086 raise RuntimeError(f"Packed optimizer staging arena {staging_index} is not materialized.") 

1087 return arena 

1088 

1089 @staticmethod 

1090 def _align_bytes(num_bytes: int) -> int: 

1091 return ((num_bytes + _PACKED_ALIGNMENT_BYTES - 1) // _PACKED_ALIGNMENT_BYTES) * _PACKED_ALIGNMENT_BYTES 

1092 

1093 def _packed_layout(self, batch_plans: Sequence[_PackedBatchPlan]) -> Tuple[Dict[Any, Tuple[int, int]], int, Any]: 

1094 """Return the dtype -> (offset, bytes) staging layout shared by every batch.""" 

1095 max_numel_by_dtype: Dict[Any, int] = {} 

1096 device = None 

1097 for batch_plan in batch_plans: 

1098 for dtype, region in batch_plan.regions.items(): 

1099 max_numel_by_dtype[dtype] = max(max_numel_by_dtype.get(dtype, 0), region.numel) 

1100 if device is None and region.slots: 

1101 device = region.slots[0].device 

1102 if device is None: 

1103 raise RuntimeError("Packed optimizer pipeline has no device-resident state metadata.") 

1104 

1105 dtype_layouts = {} 

1106 byte_offset = 0 

1107 for dtype in sorted(max_numel_by_dtype, key=str): 

1108 byte_offset = self._align_bytes(byte_offset) 

1109 element_size = int(self._host_buffers[dtype].element_size()) 

1110 num_bytes = max_numel_by_dtype[dtype] * element_size 

1111 dtype_layouts[dtype] = (byte_offset, num_bytes) 

1112 byte_offset += num_bytes 

1113 return dtype_layouts, self._align_bytes(byte_offset), device 

1114 

1115 

1116@dataclass 

1117class _PackedBatchRegion: 

1118 """One dtype-contiguous host range transferred for a pipeline batch.""" 

1119 

1120 dtype: Any 

1121 host_offset: int 

1122 numel: int 

1123 slots: List[SwapSlot] 

1124 

1125 

1126@dataclass 

1127class _PackedBatchPlan: 

1128 """Packed transfer regions for one optimizer pipeline batch.""" 

1129 

1130 regions: Dict[Any, _PackedBatchRegion] = field(default_factory=dict) 

1131 

1132 

1133@dataclass 

1134class _StagingArena: 

1135 """One raw device allocation and its dtype-specific views.""" 

1136 

1137 raw_buffer: Any 

1138 dtype_views: Dict[Any, Any] = field(default_factory=dict) 

1139 layout_signature: Any = None 

1140 

1141 

1142def _iter_unique_slots(units: Sequence[UpdateUnit]) -> Iterable[SwapSlot]: 

1143 """Yield slots once by object identity.""" 

1144 slots = itertools.chain.from_iterable(unit.slots for unit in units) 

1145 return _iter_unique_slot_objects(slots) 

1146 

1147 

1148def _iter_unique_slot_objects(slots: Iterable[SwapSlot]) -> Iterable[SwapSlot]: 

1149 """Yield slots once by tensor object identity.""" 

1150 unique_slots: Dict[int, SwapSlot] = {} 

1151 for slot in slots: 

1152 unique_slots.setdefault(id(slot.tensor), slot) 

1153 return unique_slots.values() 

1154 

1155 

1156def _iter_unique_events(slots: Iterable[SwapSlot]) -> Iterable[Any]: 

1157 """Yield non-empty events once by object identity.""" 

1158 seen = set() 

1159 for slot in slots: 

1160 event = slot.event 

1161 if event is None: 

1162 continue 

1163 key = id(event) 

1164 if key in seen: 

1165 continue 

1166 seen.add(key) 

1167 yield event 

1168 

1169 

1170def _slot_tensor(unit: SwapUpdateUnit, key: str, fallback: Any) -> Any: 

1171 """Return an active swap slot tensor for ``key``, or ``fallback``. 

1172 

1173 A slot is authoritative only while it is swappable, still resident on the 

1174 local device and bound to a live tensor; otherwise the optimizer state entry 

1175 is the source of truth for this step. 

1176 """ 

1177 for slot in unit.slots: 

1178 if slot.name == key and slot.swappable and slot.state == "device" and slot.tensor is not None: 

1179 return slot.tensor 

1180 return fallback 

1181 

1182 

1183def validate_state_keys(state_keys: Optional[Sequence[str]]) -> Optional[tuple[str, ...]]: 

1184 """Validate user-provided optimizer state keys. 

1185 

1186 Only lexical validity is checked here: a key must appear in 

1187 :data:`KNOWN_STATE_KEYS`. Whether the wrapped optimizer family actually 

1188 owns that state is decided later by the family adapter, which fails during 

1189 ``validate()``/wrap time. 

1190 """ 

1191 if state_keys is None: 

1192 return None 

1193 normalized = tuple(state_keys) 

1194 invalid = sorted(set(normalized) - set(KNOWN_STATE_KEYS)) 

1195 if invalid: 

1196 raise ValueError( 

1197 "SwapOptimizerConfig.state_keys only supports optimizer state slots " 

1198 f"{KNOWN_STATE_KEYS}, but got {invalid}." 

1199 ) 

1200 return normalized 

1201 

1202 

1203#: Builds the family adapter for a wrapped optimizer. 

1204AdapterFactory = Callable[[Any, Any, PipelineSwapRuntime], "StateSwapAdapter"] 

1205 

1206 

1207class StateSwapAdapter: 

1208 """Algorithm-independent optimizer-state swap adapter base. 

1209 

1210 Owns the slot map, the configured/default state-key selection framework, the 

1211 checkpoint strip/export/restore path and packed-state publication. The 

1212 numerical update and the per-parameter unit construction belong to the 

1213 optimizer family subclasses, which implement :meth:`default_state_keys`, 

1214 :meth:`prepare_step`, :meth:`iter_update_units` and :meth:`step_batch`. 

1215 """ 

1216 

1217 @staticmethod 

1218 def default_state_keys() -> Tuple[str, ...]: 

1219 """Return the family's default swap state keys. 

1220 

1221 Subclasses must override this; the base class deliberately assumes 

1222 nothing about an optimizer's state layout, so there is no default. 

1223 

1224 Raises: 

1225 NotImplementedError: Always, unless a subclass overrides it. 

1226 """ 

1227 raise NotImplementedError 

1228 

1229 def __init__(self, optimizer: Any, config: Any, runtime: Any) -> None: 

1230 """Bind a leaf optimizer to its state slots and transfer runtime.""" 

1231 self.optimizer = optimizer 

1232 self.config = config 

1233 self.runtime = runtime 

1234 self._slots: Dict[Tuple[int, str], SwapSlot] = {} 

1235 self._packed_state_bindings: Dict[int, Tuple[Any, str]] = {} 

1236 self._checkpoint_active = False 

1237 self._checkpoint_broadcasted = False 

1238 self._checkpoint_state_bindings: List[Tuple[Any, str, Any]] = [] 

1239 self.runtime.set_packed_slot_bind_hook(self.on_packed_slot_bound) 

1240 

1241 def iter_update_units(self, step_context: Dict[str, Any]) -> List[UpdateUnit]: 

1242 """Return units collected in ``prepare_step``.""" 

1243 return step_context["units"] 

1244 

1245 def partition_costs( 

1246 self, 

1247 units: Sequence[SwapUpdateUnit], 

1248 step_context: Any, 

1249 ) -> Optional[Sequence[int]]: 

1250 """Return optional planning costs; default to the runtime's local bytes.""" 

1251 del units, step_context 

1252 

1253 def prepare_pipeline( 

1254 self, 

1255 batches: Sequence[Sequence[SwapUpdateUnit]], 

1256 step_context: Any, 

1257 ) -> None: 

1258 """Pack local slots in execution order after logical partitioning.""" 

1259 del step_context 

1260 if not self.runtime.packed_enabled: 

1261 return 

1262 self._refresh_packed_state_bindings() 

1263 active_slots = [slot for batch in batches for unit in batch for slot in unit.slots] 

1264 active_ids = {id(slot) for slot in active_slots} 

1265 # Inactive state stays on host at the layout tail. It must not leave 

1266 # holes inside active dtype regions or keep old packed buffers alive. 

1267 inactive_slots = [slot for slot in self._ordered_slots() if id(slot) not in active_ids] 

1268 self.runtime.prepare_packed_host(active_slots + inactive_slots) 

1269 self.publish_packed_state() 

1270 

1271 def _refresh_packed_state_bindings(self) -> None: 

1272 """Index packed slots by their live optimizer-state owner.""" 

1273 bindings = {} 

1274 for group in self.optimizer.param_groups: 

1275 for param in group["params"]: 

1276 for key in self._configured_state_keys(): 

1277 slot = self._slots.get((id(param), key)) 

1278 if slot is not None and slot.packed: 

1279 bindings[id(slot)] = (param, key) 

1280 self._packed_state_bindings = bindings 

1281 

1282 def on_packed_slot_bound(self, slot: SwapSlot) -> None: 

1283 """Publish a packed host or staging view into optimizer state.""" 

1284 binding = self._packed_state_bindings.get(id(slot)) 

1285 if binding is None: 

1286 raise RuntimeError(f"Packed optimizer slot {slot.name!r} has no live state binding.") 

1287 param, key = binding 

1288 state = self.optimizer.state.get(param) 

1289 if state is None: 

1290 raise RuntimeError(f"Packed optimizer slot {slot.name!r} has no optimizer state entry.") 

1291 state[key] = slot.tensor 

1292 

1293 def initial_slots(self) -> Iterable[SwapSlot]: 

1294 """Discover optimizer states materialized before the swap wrapper was created.""" 

1295 slots = [] 

1296 for group in self.optimizer.param_groups: 

1297 for param in group["params"]: 

1298 state = self.optimizer.state.get(param) 

1299 if state: 

1300 slots.extend(self._build_slots(param, state)) 

1301 return tuple(slots) 

1302 

1303 def finish_step(self, step_context: Any) -> Any: 

1304 """Finish one outer optimizer step.""" 

1305 del step_context 

1306 

1307 def _checkpoint_broadcast_state_keys(self) -> Tuple[str, ...]: 

1308 """Return the wrapped optimizer state fields consumed by its broadcast.""" 

1309 get_state_keys = getattr(self.optimizer, "_state_keys_for_checkpoint_broadcast", None) 

1310 if not callable(get_state_keys): 

1311 return () 

1312 return tuple(get_state_keys()) 

1313 

1314 def _make_checkpoint_cpu_tensor(self, param: Any) -> torch.Tensor: 

1315 """Allocate one host receiver matching the optimizer broadcast contract.""" 

1316 local_param = self.runtime._storage_tensor(param) 

1317 try: 

1318 return torch.empty_like(local_param, dtype=torch.float32, device="cpu", pin_memory=True) 

1319 except RuntimeError: 

1320 return torch.empty_like(local_param, dtype=torch.float32, device="cpu") 

1321 

1322 def _publish_checkpoint_cpu_state(self) -> None: 

1323 """Expose CPU mirrors and host receivers to the fused state broadcast. 

1324 

1325 The wrapped broadcast packs these tensors into one bounded device buffer 

1326 per batch. Existing swap slots therefore stay host-resident, while ranks 

1327 without owner state receive directly into temporary CPU tensors. 

1328 """ 

1329 broadcast_info = getattr(self.optimizer, "_param_to_broadcast_info", None) 

1330 state_keys = self._checkpoint_broadcast_state_keys() 

1331 if not broadcast_info or not state_keys: 

1332 return 

1333 

1334 for param in broadcast_info: 

1335 param_state = self.optimizer.state.get(param) 

1336 if param_state is None: 

1337 param_state = {} 

1338 for key in state_keys: 

1339 slot = self._slots.get((id(param), key)) 

1340 replacement = None 

1341 if slot is not None and slot.swappable: 

1342 if slot.state != "host" or slot.cpu_tensor is None: 

1343 raise RuntimeError( 

1344 f"Checkpoint state slot {key!r} must have an authoritative CPU mirror." 

1345 ) 

1346 replacement = slot.cpu_tensor 

1347 elif key not in param_state: 

1348 replacement = self._make_checkpoint_cpu_tensor(param) 

1349 

1350 if replacement is None: 

1351 continue 

1352 if param not in self.optimizer.state: 

1353 self.optimizer.state[param] = param_state 

1354 previous = param_state.get(key, _MISSING_CHECKPOINT_STATE) 

1355 self._checkpoint_state_bindings.append((param, key, previous)) 

1356 param_state[key] = replacement 

1357 

1358 def _restore_checkpoint_state_bindings(self) -> None: 

1359 """Restore live state entries after exporting the CPU checkpoint values.""" 

1360 for param, key, previous in reversed(self._checkpoint_state_bindings): 

1361 param_state = self.optimizer.state.get(param) 

1362 if param_state is None: 

1363 continue 

1364 if previous is _MISSING_CHECKPOINT_STATE: 

1365 param_state.pop(key, None) 

1366 if not param_state: 

1367 self.optimizer.state.pop(param, None) 

1368 else: 

1369 param_state[key] = previous 

1370 self._checkpoint_state_bindings.clear() 

1371 

1372 def prepare_checkpoint(self) -> None: 

1373 """Publish host-resident swap state for an optional HSDP broadcast.""" 

1374 if self._checkpoint_active: 

1375 return 

1376 self._checkpoint_active = True 

1377 self._checkpoint_broadcasted = False 

1378 try: 

1379 self._refresh_packed_state_bindings() 

1380 # A previous optimizer step may still have a completed D2H event or 

1381 # a live device state. Make the host mirror authoritative first; 

1382 # the broadcast streams it through its bounded device buffer. 

1383 self.runtime.synchronize_cpu_mirrors(self.all_slots()) 

1384 self._publish_checkpoint_cpu_state() 

1385 except Exception: 

1386 try: 

1387 self.runtime.synchronize_cpu_mirrors(self.all_slots()) 

1388 except RuntimeError: # pragma: no cover - best effort after a transfer failure 

1389 logger.exception("Failed to restore swap slots after checkpoint preparation failed") 

1390 finally: 

1391 self._restore_checkpoint_state_bindings() 

1392 self._checkpoint_active = False 

1393 self._checkpoint_broadcasted = False 

1394 raise 

1395 

1396 def finish_checkpoint(self) -> None: 

1397 """Return checkpoint-broadcast state to the normal host-resident form.""" 

1398 if not self._checkpoint_active: 

1399 return 

1400 try: 

1401 self.runtime.synchronize_cpu_mirrors(self.all_slots()) 

1402 finally: 

1403 self._restore_checkpoint_state_bindings() 

1404 self._checkpoint_active = False 

1405 self._checkpoint_broadcasted = False 

1406 

1407 def all_slots(self) -> Iterable[SwapSlot]: 

1408 """Iterate known swap slots.""" 

1409 return tuple(self._ordered_slots()) 

1410 

1411 def tracked_slots(self) -> Iterable[SwapSlot]: 

1412 """Return all registered slots, including parameters removed from groups.""" 

1413 return tuple({id(slot): slot for slot in self._slots.values()}.values()) 

1414 

1415 def rebind_parameter_slots(self, new_to_old_param: Dict[Any, Any]) -> None: 

1416 """Rebuild state bindings using an explicit parameter identity mapping. 

1417 

1418 Args: 

1419 new_to_old_param: New-to-old identities; unrelated leaf entries are 

1420 ignored. CPU mirrors must already be synchronized. 

1421 

1422 Raises: 

1423 ValueError: If a state cannot be mapped to the new local shape. 

1424 """ 

1425 params = [param for group in self.optimizer.param_groups for param in group["params"]] 

1426 local_ids = {id(param) for param in self._params_with_local_state(params)} 

1427 rebound_slots = {} 

1428 rebound_state = {} 

1429 for param in params: 

1430 if id(param) not in local_ids: 

1431 continue 

1432 old_param = new_to_old_param.get(param, param) 

1433 state = self.optimizer.state.get(param, self.optimizer.state.get(old_param)) 

1434 if state is None: 

1435 continue 

1436 state = dict(state) 

1437 for key in self._configured_state_keys(): 

1438 if key not in state: 

1439 continue 

1440 slot = self._slots.get((id(param), key)) 

1441 if slot is None: 

1442 slot = self._slots.get((id(old_param), key)) 

1443 slot = self._rebind_state_slot(param, key, state[key], slot, param is not old_param) 

1444 state[key] = slot.tensor 

1445 rebound_slots[(id(param), key)] = slot 

1446 rebound_state[param] = state 

1447 

1448 # Publish only after all sources have been read. A failed conversion must 

1449 # leave the old slot map (and its authoritative CPU mirrors) recoverable. 

1450 for param in list(self.optimizer.state): 

1451 if isinstance(param, torch.Tensor) and param not in rebound_state: 

1452 self.optimizer.state.pop(param) 

1453 self.optimizer.state.update(rebound_state) 

1454 self._slots = rebound_slots 

1455 self._refresh_packed_state_bindings() 

1456 

1457 def _rebind_state_slot( 

1458 self, param: Any, key: str, tensor: Any, slot: Optional[SwapSlot], replaced: bool, 

1459 ) -> SwapSlot: 

1460 """Preserve a compatible binding or recreate it from authoritative values.""" 

1461 local_param = self.runtime._storage_tensor(param) 

1462 if slot is not None and (tensor is slot.tensor or tensor is slot.cpu_tensor): 

1463 if (not replaced and slot.shape == tuple(local_param.shape) 

1464 and slot.dtype == local_param.dtype and slot.device == local_param.device 

1465 and not self.runtime.is_distributed_tensor(param)): 

1466 return slot 

1467 source = slot.checkpoint_tensor 

1468 else: 

1469 # Identity verifies a missing map; it never infers a replacement. 

1470 if any(tensor is tracked.tensor or tensor is tracked.cpu_tensor for tracked in self._slots.values()): 

1471 raise ValueError("Replacing a swap parameter requires an explicit new_to_old_param mapping.") 

1472 source = tensor 

1473 source = self.runtime._storage_tensor(source) 

1474 if source.numel() != local_param.numel(): 

1475 raise ValueError( 

1476 f"Cannot rebind {key!r} with local shape {tuple(source.shape)} " 

1477 f"to {tuple(local_param.shape)}; restore state for the new layout first." 

1478 ) 

1479 cpu_tensor = self._cast_swappable_tensor_to_cpu(param, source).reshape(local_param.shape) 

1480 device_tensor = self.runtime.make_empty_state_like(param) 

1481 rebound = self._make_slot(key, device_tensor) 

1482 if rebound.swappable: 

1483 rebound.cpu_tensor = self.runtime.make_cpu_tensor(cpu_tensor) 

1484 rebound.state = "host" 

1485 self.runtime.release_device_storage(rebound) 

1486 if rebound.packed: 

1487 rebound.bind_tensor(rebound.cpu_tensor) 

1488 else: 

1489 self.runtime._storage_tensor(device_tensor).copy_(cpu_tensor) 

1490 return rebound 

1491 

1492 def checkpoint_state_dict(self, *args: Any, **kwargs: Any) -> Dict[str, Any]: 

1493 """Return optimizer checkpoint state using CPU mirrors for swapped slots.""" 

1494 del args, kwargs 

1495 self._refresh_packed_state_bindings() 

1496 self.runtime.synchronize_cpu_mirrors(self.all_slots()) 

1497 return self.export_swappable_state(self.optimizer.state_dict()) 

1498 

1499 @contextlib.contextmanager 

1500 def checkpoint_load_context(self, state_dict: Dict[str, Any], flatten: bool) -> Iterable[None]: 

1501 """Provide state keys for FQN loading without an initialization step. 

1502 

1503 Args: 

1504 state_dict: Chain checkpoint keyed by model FQN. 

1505 flatten: Whether the checkpoint uses flattened FQN entries. 

1506 """ 

1507 previous_state = self.optimizer.state 

1508 schema = copy.copy(previous_state) 

1509 schema.clear() 

1510 for group in self.optimizer.param_groups: 

1511 for param in group["params"]: 

1512 name = param.model_name 

1513 if flatten: 

1514 prefix = f"state.{name}." 

1515 schema[param] = { 

1516 key[len(prefix):].split(".", 1)[0]: None 

1517 for key in state_dict if key.startswith(prefix) 

1518 } 

1519 else: 

1520 schema[param] = dict(state_dict.get("state", {}).get(name, {})) 

1521 # DCP uses live state keys to unflatten and calls step() if state is 

1522 # empty. Empty owner ranks must never initiate that extra collective. 

1523 self.optimizer.state = schema 

1524 try: 

1525 yield 

1526 finally: 

1527 if self.optimizer.state is schema: 

1528 self.optimizer.state = previous_state 

1529 

1530 def load_checkpoint_state_dict( 

1531 self, 

1532 state_dict: Dict[str, Any], 

1533 *args: Any, 

1534 **kwargs: Any, 

1535 ) -> None: 

1536 """Load optimizer checkpoint state while restoring swap-managed slots.""" 

1537 del args, kwargs 

1538 stripped, removed = self.strip_swappable_state(state_dict) 

1539 self.optimizer.load_state_dict(stripped) 

1540 self.load_swappable_state(state_dict, removed) 

1541 

1542 def _params_with_local_state(self, params: Sequence[Any]) -> Iterable[Any]: 

1543 """Return parameters whose swapped state belongs to this rank. 

1544 

1545 The default restores every checkpoint entry. Distributed optimizer 

1546 adapters may filter the sequence to preserve rank-local state ownership. 

1547 """ 

1548 return params 

1549 

1550 def publish_packed_state(self) -> None: 

1551 """Publish persistent packed CPU mirrors to the wrapped optimizer state.""" 

1552 if not self.runtime.packed_enabled: 

1553 return 

1554 self._refresh_packed_state_bindings() 

1555 for group in self.optimizer.param_groups: 

1556 for param in group["params"]: 

1557 state = self.optimizer.state.get(param) 

1558 for key in self._configured_state_keys(): 

1559 slot = self._slots.get((id(param), key)) 

1560 if slot is None or not slot.packed or slot.cpu_tensor is None: 

1561 continue 

1562 if state is None: 

1563 state = self.optimizer.state[param] 

1564 state[key] = slot.cpu_tensor 

1565 

1566 def export_swappable_state(self, state_dict: Dict[str, Any]) -> Dict[str, Any]: 

1567 """Build a checkpoint-safe Torch optimizer state dict. 

1568 

1569 Torch optimizer state dicts are keyed by saved parameter ids, while the 

1570 adapter tracks live swap slots by the current parameter objects. This 

1571 method walks both orders together and exports each parameter's optimizer 

1572 state with the data source that currently owns the valid tensor values. 

1573 

1574 If an Adam state tensor such as ``exp_avg`` or ``exp_avg_sq`` has been 

1575 offloaded, the live device tensor may only be a placeholder with its 

1576 storage released. In that case, write a cloned CPU mirror into the 

1577 exported state dict so checkpoints contain the real optimizer values. 

1578 Non-swappable state, metadata, and tensors that are still resident on 

1579 device are deep-copied from the original Torch state dict unchanged. 

1580 """ 

1581 exported = { 

1582 key: self._copy_checkpoint_value(value) 

1583 for key, value in state_dict.items() 

1584 if key not in ("state", "param_groups") 

1585 } 

1586 exported["param_groups"] = self._copy_checkpoint_value( 

1587 state_dict.get("param_groups", []) 

1588 ) 

1589 exported["state"] = {} 

1590 saved_groups = exported.get("param_groups", []) 

1591 params_in_order = [] 

1592 for group in self.optimizer.param_groups: 

1593 params_in_order.extend(group["params"]) 

1594 ids_in_order = [] 

1595 for group in saved_groups: 

1596 ids_in_order.extend(group["params"]) 

1597 for param, param_id in zip(params_in_order, ids_in_order): 

1598 saved_state = state_dict.get("state", {}).get(param_id) 

1599 if not saved_state: 

1600 continue 

1601 exported_state = {} 

1602 for key in self._state_keys_for_param(param): 

1603 slot = self._slots.get((id(param), key)) 

1604 if self.has_host_mirror(slot, key, saved_state): 

1605 local_copy = slot.cpu_tensor.detach().clone() 

1606 exported_state[key] = _restore_dtensor_metadata(param, local_copy) 

1607 for key, value in saved_state.items(): 

1608 if key not in exported_state: 

1609 exported_state[key] = self._copy_checkpoint_value(value) 

1610 exported["state"][param_id] = exported_state 

1611 return exported 

1612 

1613 @staticmethod 

1614 def _copy_checkpoint_value(value: Any) -> Any: 

1615 """Clone checkpoint values without dispatching ``deepcopy`` on DTensor. 

1616 

1617 HSDP state broadcasts may leave a distributed tensor in the wrapped 

1618 optimizer state. Hyper DTensor intentionally rejects unregistered 

1619 ``__deepcopy__`` dispatch, so clone its local shard and restore the 

1620 global layout explicitly. Nested containers are copied recursively; 

1621 scalar metadata still uses regular ``copy.deepcopy`` semantics. 

1622 """ 

1623 if isinstance(value, torch.Tensor): 

1624 to_local = getattr(value, "to_local", None) 

1625 local = to_local() if callable(to_local) else value 

1626 if local is not value: 

1627 local_copy = local.detach().cpu().clone() 

1628 return _restore_dtensor_metadata(value, local_copy) 

1629 return copy.deepcopy(value) 

1630 if isinstance(value, dict): 

1631 return { 

1632 key: StateSwapAdapter._copy_checkpoint_value(item) 

1633 for key, item in value.items() 

1634 } 

1635 if isinstance(value, list): 

1636 return [StateSwapAdapter._copy_checkpoint_value(item) for item in value] 

1637 if isinstance(value, tuple): 

1638 return tuple(StateSwapAdapter._copy_checkpoint_value(item) for item in value) 

1639 return copy.deepcopy(value) 

1640 

1641 @staticmethod 

1642 def has_host_mirror(slot: Optional[SwapSlot], key: str, saved_state: Dict[str, Any]) -> bool: 

1643 """Return whether ``slot`` currently owns the authoritative CPU mirror for ``key``. 

1644 

1645 A slot qualifies only when it has been offloaded to host *and* still 

1646 holds a CPU mirror: after :meth:`export_swappable_state`'s device-side 

1647 fast path, the live tensor may be a storage-released placeholder, so the 

1648 CPU mirror is the only source of the real optimizer values. 

1649 """ 

1650 return ( 

1651 slot is not None 

1652 and slot.state == "host" 

1653 and slot.cpu_tensor is not None 

1654 and key in saved_state 

1655 ) 

1656 

1657 def strip_swappable_state(self, state_dict: Dict[str, Any]) -> Tuple[Dict[str, Any], Dict[int, Dict[str, Any]]]: 

1658 """Split Adam state tensors out before delegating to Torch loading. 

1659 

1660 PyTorch's ``load_state_dict`` eagerly restores tensors into the 

1661 optimizer state. For swap-managed Adam buffers, that would bypass the 

1662 adapter/runtime bookkeeping and can place large tensors directly on the 

1663 device. This method therefore copies the checkpoint state dict's 

1664 *containers*, removes the Adam buffers that may be swap-managed, and 

1665 returns them in a side table keyed by the checkpoint parameter id. 

1666 

1667 Only the container skeleton is rebuilt: leaf tensors are shared with the 

1668 caller's checkpoint instead of being duplicated. The removed buffers are 

1669 consumed by :meth:`load_swappable_state`, which either takes a reference 

1670 to the checkpoint storage (CPU checkpoints, where ``to(device="cpu")`` is 

1671 a no-op) or copies it once into a host mirror. Deep-copying the whole 

1672 state dict would hold one extra full copy of every optimizer tensor on 

1673 host memory for the duration of the load, which for large models is a 

1674 needless doubling of the checkpoint-loading peak. 

1675 

1676 The stripped state dict is safe to pass to the wrapped optimizer's 

1677 ``load_state_dict`` for ordinary fields such as parameter groups and 

1678 step counters: Torch does not mutate the tensors it reads from it. The 

1679 removed tensors must be handed to ``load_swappable_state`` afterwards so 

1680 they can be restored with the correct CPU mirror/device placeholder 

1681 layout. 

1682 """ 

1683 stripped = self._copy_state_dict_containers(state_dict) 

1684 removed: Dict[int, Dict[str, Any]] = {} 

1685 swappable_keys = self._configured_state_keys() 

1686 for param_id, saved_state in list(stripped.get("state", {}).items()): 

1687 if not isinstance(saved_state, dict): 

1688 continue 

1689 for key in swappable_keys: 

1690 if key in saved_state: 

1691 removed.setdefault(param_id, {})[key] = saved_state.pop(key) 

1692 return stripped, removed 

1693 

1694 @staticmethod 

1695 def _copy_state_dict_containers(state_dict: Dict[str, Any]) -> Dict[str, Any]: 

1696 """Shallow-copy a checkpoint state dict, sharing every leaf tensor. 

1697 

1698 Rebuilds only the nesting Torch's ``load_state_dict`` walks: the top 

1699 level, the ``state`` per-parameter mappings, and the ``param_groups`` 

1700 entries. Do not add tensor copies here: this helper exists to keep the 

1701 checkpoint-loading host peak free of a duplicate optimizer state. 

1702 """ 

1703 state = state_dict.get("state") 

1704 copied_state = ( 

1705 {param_id: dict(saved_state) if isinstance(saved_state, dict) else saved_state 

1706 for param_id, saved_state in state.items()} 

1707 if isinstance(state, dict) 

1708 else state 

1709 ) 

1710 param_groups = state_dict.get("param_groups") 

1711 copied_groups = ( 

1712 [dict(group) if isinstance(group, dict) else group for group in param_groups] 

1713 if isinstance(param_groups, list) 

1714 else param_groups 

1715 ) 

1716 copied = {key: value for key, value in state_dict.items() if key not in ("state", "param_groups")} 

1717 if state is not None: 

1718 copied["state"] = copied_state 

1719 if param_groups is not None: 

1720 copied["param_groups"] = copied_groups 

1721 return copied 

1722 

1723 def load_swappable_state(self, original_state_dict: Dict[str, Any], removed: Dict[int, Dict[str, Any]]) -> None: 

1724 """Restore removed Adam buffers under swap runtime control. 

1725 

1726 This is the second half of checkpoint loading. After Torch has loaded 

1727 the stripped state dict, this method maps checkpoint parameter ids back 

1728 to the current parameter objects by walking saved and current parameter 

1729 groups in order. Each removed Adam buffer is then recreated as an 

1730 optimizer state entry and registered as a ``SwapSlot``. 

1731 

1732 Packed runtimes place checkpoint values directly in persistent pinned 

1733 host views. Legacy runtimes retain an empty device placeholder whose 

1734 storage is restored only during prefetch. Buffers that do not meet the 

1735 runtime's swappability criteria are materialized directly on the 

1736 parameter's device and tracked as normal device-resident slots. 

1737 """ 

1738 saved_groups = original_state_dict.get("param_groups", []) 

1739 self._slots = {} 

1740 # Walk saved and current parameter groups in order to map checkpoint ids 

1741 # back to the live parameter objects. 

1742 saved_ids = [ 

1743 saved_id 

1744 for saved_group, current_group in zip(saved_groups, self.optimizer.param_groups) 

1745 for saved_id in saved_group["params"] 

1746 ] 

1747 groups = self.optimizer.param_groups 

1748 current_params = [param for group in groups for param in group["params"]] 

1749 local_param_ids = { 

1750 id(param) for param in self._params_with_local_state(current_params) 

1751 } 

1752 for saved_id, param in zip(saved_ids, current_params): 

1753 if id(param) not in local_param_ids: 

1754 self.optimizer.state.pop(param, None) 

1755 continue 

1756 for key, saved_tensor in removed.get(saved_id, {}).items(): 

1757 self._restore_swappable_entry(param, saved_id, saved_groups, key, saved_tensor) 

1758 if self.runtime.packed_enabled: 

1759 self.runtime.prepare_packed_host(self._ordered_slots()) 

1760 self.publish_packed_state() 

1761 

1762 def _restore_swappable_entry( 

1763 self, 

1764 param: Any, 

1765 saved_id: int, 

1766 saved_groups: List[Dict[str, Any]], 

1767 key: str, 

1768 saved_tensor: Any, 

1769 ) -> None: 

1770 """Restore one removed checkpoint buffer as a host or device-resident slot.""" 

1771 state = self.optimizer.state[param] 

1772 cpu_tensor = self._cast_swappable_tensor_to_cpu(param, saved_tensor) 

1773 if self.runtime.packed_enabled and self.runtime.is_packable_template(param, self.config.min_numel): 

1774 if self.runtime.is_distributed_tensor(param): 

1775 logical_tensor = self.runtime.make_empty_state_like(param) 

1776 slot = self._make_slot(key, logical_tensor) 

1777 state[key] = logical_tensor 

1778 self.runtime.release_device_storage(slot) 

1779 else: 

1780 slot = self._make_slot(key, None, template=param) 

1781 state[key] = cpu_tensor 

1782 slot.tensor = cpu_tensor 

1783 slot.cpu_tensor = cpu_tensor 

1784 slot.state = "host" 

1785 self._slots[(id(param), key)] = slot 

1786 return 

1787 device_tensor = self.runtime.make_empty_device_tensor_like(param, cpu_tensor) 

1788 slot = self._make_slot(key, device_tensor) 

1789 if slot.swappable: 

1790 state[key] = device_tensor 

1791 slot.cpu_tensor = self.runtime.make_cpu_tensor(cpu_tensor) 

1792 slot.state = "host" 

1793 self._slots[(id(param), key)] = slot 

1794 self.runtime.release_device_storage(slot) 

1795 else: 

1796 device_tensor = self._cast_state_tensor_like_torch( 

1797 param, 

1798 saved_tensor, 

1799 saved_id, 

1800 saved_groups, 

1801 key, 

1802 ) 

1803 state[key] = device_tensor 

1804 self._slots[(id(param), key)] = self._make_slot(key, device_tensor) 

1805 

1806 @staticmethod 

1807 def _cast_state_tensor_like_torch( 

1808 param: Any, 

1809 saved_tensor: Any, 

1810 saved_id: int, 

1811 saved_groups: List[Dict[str, Any]], 

1812 key: str, 

1813 ) -> Any: 

1814 """Cast a loaded state tensor using PyTorch optimizer load semantics.""" 

1815 if not isinstance(saved_tensor, torch.Tensor): 

1816 raise ValueError(f"Expected torch.Tensor in optimizer state, got {type(saved_tensor)!r}.") 

1817 process = getattr(torch.optim.Optimizer, "_process_value_according_to_param_policy", None) 

1818 if process is not None: 

1819 return process(param, saved_tensor, saved_id, saved_groups, key).detach().clone() 

1820 if key == "step": 

1821 return saved_tensor.detach().clone() 

1822 if param.is_floating_point(): 

1823 return saved_tensor.detach().to(dtype=param.dtype, device=param.device).clone() 

1824 return saved_tensor.detach().to(device=param.device).clone() 

1825 

1826 @staticmethod 

1827 def _cast_swappable_tensor_to_cpu(param: Any, saved_tensor: Any) -> Any: 

1828 """Cast swappable state dtype like PyTorch while keeping values on CPU.""" 

1829 if not isinstance(saved_tensor, torch.Tensor): 

1830 raise ValueError(f"Expected torch.Tensor in optimizer state, got {type(saved_tensor)!r}.") 

1831 to_local = getattr(saved_tensor, "to_local", None) 

1832 source = to_local() if callable(to_local) else saved_tensor 

1833 if param.is_floating_point(): 

1834 return source.detach().to(dtype=param.dtype, device="cpu") 

1835 return source.detach().to(device="cpu") 

1836 

1837 def _register_present_slots(self, param: Any, state: Dict[str, Any]) -> None: 

1838 """Register configured state tensors that already exist in an optimizer state mapping.""" 

1839 for key in self._configured_state_keys(): 

1840 tensor = state.get(key) 

1841 if tensor is None or (id(param), key) in self._slots: 

1842 continue 

1843 self._slots[(id(param), key)] = self._make_slot(key, tensor) 

1844 

1845 def _build_slots(self, param: Any, state: Dict[str, Any]) -> List[SwapSlot]: 

1846 """Return swap slots associated with the current parameter state.""" 

1847 slots = [] 

1848 for key in self._state_keys_for_param(param): 

1849 tensor = state.get(key) 

1850 if tensor is None: 

1851 continue 

1852 slot = self._slots.get((id(param), key)) 

1853 if slot is None: 

1854 slot = self._make_slot(key, tensor) 

1855 self._slots[(id(param), key)] = slot 

1856 elif slot.tensor is not tensor and slot.cpu_tensor is not tensor: 

1857 slot = self._make_slot(key, tensor) 

1858 self._slots[(id(param), key)] = slot 

1859 slots.append(slot) 

1860 return slots 

1861 

1862 def _make_slot(self, key: str, tensor: Any, template: Optional[Any] = None) -> SwapSlot: 

1863 """Create a swap slot for a state tensor or a packed-state template.""" 

1864 metadata_tensor = tensor if tensor is not None else template 

1865 if metadata_tensor is None: 

1866 raise ValueError(f"Cannot build swap slot {key!r} without a tensor or template.") 

1867 if tensor is None: 

1868 swappable = self.runtime.is_packable_template(metadata_tensor, self.config.min_numel) 

1869 else: 

1870 swappable = self.runtime.is_swappable_tensor(tensor, self.config.min_numel) 

1871 packed = bool(self.runtime.packed_enabled and swappable) 

1872 slot = SwapSlot( 

1873 name=key, 

1874 tensor=tensor, 

1875 cpu_tensor=None, 

1876 swappable=swappable, 

1877 state="device" if tensor is not None else "pending", 

1878 packed=packed, 

1879 logical_tensor=tensor if packed and self.runtime.is_distributed_tensor(tensor) else None, 

1880 ) 

1881 self.runtime.populate_slot_metadata(slot, metadata_tensor) 

1882 return slot 

1883 

1884 def _ordered_slots(self) -> List[SwapSlot]: 

1885 """Return slots in optimizer parameter and configured state-key order.""" 

1886 slots = [] 

1887 seen_slots = set() 

1888 for group in self.optimizer.param_groups: 

1889 for param in group["params"]: 

1890 for key in self._configured_state_keys(): 

1891 slot = self._slots.get((id(param), key)) 

1892 if slot is not None and id(slot) not in seen_slots: 

1893 slots.append(slot) 

1894 seen_slots.add(id(slot)) 

1895 return slots 

1896 

1897 def _state_keys_for_param(self, param: Any) -> Tuple[str, ...]: 

1898 state = self.optimizer.state[param] 

1899 keys = self._configured_state_keys() 

1900 result = [] 

1901 for key in keys: 

1902 if key in state: 

1903 result.append(key) 

1904 elif self.config.state_keys is not None: 

1905 raise ValueError(f"Requested state key '{key}' is not present for parameter.") 

1906 return tuple(result) 

1907 

1908 def _configured_state_keys(self) -> Tuple[str, ...]: 

1909 """Return optimizer state keys selected for swap by the current config.""" 

1910 return tuple(self.config.state_keys or self.default_state_keys()) 

1911 

1912 

1913def _check_step_hooks(optimizer: Any) -> None: 

1914 """Reject step hooks that the swap pipeline bypasses.""" 

1915 if (getattr(optimizer, "_optimizer_step_pre_hooks", None) 

1916 or getattr(optimizer, "_optimizer_step_post_hooks", None) 

1917 or torch_optimizer._global_optimizer_pre_hooks 

1918 or torch_optimizer._global_optimizer_post_hooks): 

1919 raise ValueError( 

1920 "Optimizer-state swap does not support registered step hooks; " 

1921 "remove the hooks or disable swap" 

1922 ) 

1923 

1924 

1925class SwapOptimizer(torch.optim.Optimizer): 

1926 """Torch optimizer wrapper around a family-provided swap adapter. 

1927 

1928 The wrapper is algorithm agnostic: it owns the pipeline runtime, exposes the 

1929 wrapped optimizer's ``param_groups``/``state``/``defaults`` and drives one 

1930 step as ``prepare_step`` -> ``partition`` -> ``run_pipeline`` -> 

1931 ``finish_step``. The concrete adapter is built by the family factory passed 

1932 as ``adapter_factory`` (see ``swap_adam.py``/``swap_muon.py``). 

1933 """ 

1934 

1935 _is_swap_optimizer = True 

1936 

1937 def __init__(self, optimizer: Any, config: Any, adapter_factory: AdapterFactory) -> None: 

1938 """Wrap a leaf with its family adapter and independent transfer runtime.""" 

1939 _check_step_hooks(optimizer) 

1940 # Do not call ``torch.optim.Optimizer.__init__``: the wrapped base 

1941 # optimizer already owns param_groups/state/defaults. Inheriting keeps 

1942 # PyTorch LR schedulers and isinstance checks happy while this wrapper 

1943 # delegates all optimizer state to ``self.optimizer``. 

1944 self.optimizer = optimizer 

1945 self.config = config 

1946 self._step_active = False 

1947 self._reset_active = False 

1948 self._reset_required = False 

1949 self._export_active = False 

1950 self.runtime = PipelineSwapRuntime(config) 

1951 self.adapter = adapter_factory(optimizer, config, self.runtime) 

1952 self.adapter.validate() 

1953 # Torch Adam states are normally lazy, but callers may materialize them 

1954 # before wrapping to avoid first-step initialization in the measured loop. 

1955 initial_slots = tuple(self.adapter.initial_slots()) 

1956 self.runtime.offload_initial_slots(initial_slots) 

1957 self.runtime.prepare_packed_host(initial_slots) 

1958 self.adapter.publish_packed_state() 

1959 

1960 def __getattr__(self, name: str) -> Any: 

1961 """Delegate unknown attributes to the base optimizer.""" 

1962 return getattr(self.optimizer, name) 

1963 

1964 @property 

1965 def param_groups(self) -> List[Dict[str, Any]]: 

1966 """Proxy parameter groups.""" 

1967 return self.optimizer.param_groups 

1968 

1969 @param_groups.setter 

1970 def param_groups(self, value: List[Dict[str, Any]]) -> None: 

1971 """Replace the underlying optimizer's parameter groups.""" 

1972 self.optimizer.param_groups = value 

1973 

1974 @property 

1975 def state(self) -> Dict[Any, Any]: 

1976 """Proxy optimizer state.""" 

1977 return self.optimizer.state 

1978 

1979 @property 

1980 def defaults(self) -> Dict[str, Any]: 

1981 """Proxy optimizer defaults.""" 

1982 return self.optimizer.defaults 

1983 

1984 def add_param_group(self, param_group: Dict[str, Any]) -> None: 

1985 """Proxy param group addition.""" 

1986 self.optimizer.add_param_group(param_group) 

1987 

1988 def zero_grad(self, set_to_none: bool = True) -> None: 

1989 """Proxy gradient clearing.""" 

1990 self.optimizer.zero_grad(set_to_none=set_to_none) 

1991 

1992 def reset_optimizer_parameters( 

1993 self, new_to_old_param: Optional[Dict[torch.nn.Parameter, torch.nn.Parameter]] = None, 

1994 ) -> None: 

1995 """Rebuild optimizer metadata and rebind state at an idle step boundary. 

1996 

1997 Args: 

1998 new_to_old_param: Explicit new-to-old identities after param_groups 

1999 replacement. Omit when parameter identities are unchanged. 

2000 

2001 Raises: 

2002 RuntimeError: If a step, checkpoint or transfer is still active. 

2003 """ 

2004 slots = tuple(self.adapter.tracked_slots()) 

2005 if (self._step_active or self._reset_active or self._export_active or self.adapter._checkpoint_active 

2006 or self.runtime._packed_batch_plans or self.runtime._packed_tail_event is not None 

2007 or any(slot.state in ("h2d", "d2h") for slot in slots)): 

2008 raise RuntimeError("Cannot reset optimizer parameters during a step, checkpoint or transfer.") 

2009 self._reset_active = True 

2010 self._reset_required = True 

2011 try: 

2012 with self._no_grad_context(): 

2013 self.runtime.synchronize_cpu_mirrors(slots) 

2014 reset = getattr(self.optimizer, "reset_optimizer_parameters", None) 

2015 if callable(reset): 

2016 reset() 

2017 self.adapter.rebind_parameter_slots(new_to_old_param or {}) 

2018 self.runtime.invalidate_packed_layout() 

2019 self._reset_required = False 

2020 finally: 

2021 self._reset_active = False 

2022 

2023 def step(self, closure: Optional[Any] = None) -> Any: 

2024 """Run one optimizer step with pipeline state swap.""" 

2025 if closure is not None: 

2026 raise ValueError("Swap optimizer does not support closure.") 

2027 _check_step_hooks(self.optimizer) 

2028 self._check_reset_complete() 

2029 self._step_active = True 

2030 try: 

2031 with self._no_grad_context(): 

2032 step_context = self.adapter.prepare_step() 

2033 units = self.adapter.iter_update_units(step_context) 

2034 unit_costs = self.adapter.partition_costs(units, step_context) 

2035 batches = self.runtime.partition(units, unit_costs=unit_costs) 

2036 self.adapter.prepare_pipeline(batches, step_context) 

2037 self.runtime.run_pipeline(batches, step_context, self.adapter.step_batch) 

2038 return self.adapter.finish_step(step_context) 

2039 finally: 

2040 self._step_active = False 

2041 

2042 def _check_reset_complete(self) -> None: 

2043 """Prevent using a partially rebuilt optimizer after reset failed.""" 

2044 if self._reset_required: 

2045 raise RuntimeError("Optimizer parameter reset has not completed; retry reset before step or checkpoint.") 

2046 

2047 def state_dict(self) -> Dict[str, Any]: 

2048 """Return optimizer state dict using CPU mirrors for swappable tensors.""" 

2049 self._check_reset_complete() 

2050 self._export_active = True 

2051 try: 

2052 return self._export_state_dict() 

2053 finally: 

2054 self._export_active = False 

2055 

2056 def _export_state_dict(self) -> Dict[str, Any]: 

2057 """Reuse an outer chain checkpoint window or open a leaf-owned window.""" 

2058 broadcast = getattr(self.optimizer, "_broadcast_state_fused_for_ckpt", None) 

2059 if not callable(broadcast): 

2060 return self.adapter.checkpoint_state_dict() 

2061 

2062 owns_checkpoint_window = not self.adapter._checkpoint_active 

2063 try: 

2064 if owns_checkpoint_window: 

2065 self._prepare_swap_checkpoint() 

2066 if not self.adapter._checkpoint_broadcasted: 

2067 self._broadcast_state_fused_for_ckpt() 

2068 return self.adapter.checkpoint_state_dict() 

2069 finally: 

2070 if owns_checkpoint_window: 

2071 self.cleanup_synced_state() 

2072 

2073 def _prepare_swap_checkpoint(self) -> None: 

2074 """Publish CPU swap state needed by an HSDP checkpoint broadcast.""" 

2075 self._check_reset_complete() 

2076 broadcast = getattr(self.optimizer, "_broadcast_state_fused_for_ckpt", None) 

2077 if callable(broadcast): 

2078 self.adapter.prepare_checkpoint() 

2079 

2080 @contextlib.contextmanager 

2081 def _swap_checkpoint_load_context(self, state_dict: Dict[str, Any], flatten: bool) -> Iterable[None]: 

2082 """Let chain loading discover state keys without updating any parameters.""" 

2083 previous_active = self._export_active 

2084 self._export_active = True 

2085 try: 

2086 with self.adapter.checkpoint_load_context(state_dict, flatten): 

2087 yield 

2088 finally: 

2089 self._export_active = previous_active 

2090 

2091 def _finish_swap_checkpoint(self) -> None: 

2092 """Return checkpoint-broadcast state to its normal swapped form.""" 

2093 self.adapter.finish_checkpoint() 

2094 

2095 def _broadcast_state_fused_for_ckpt(self) -> None: 

2096 """Broadcast wrapped optimizer state through bounded device buffers.""" 

2097 broadcast = getattr(self.optimizer, "_broadcast_state_fused_for_ckpt", None) 

2098 if not callable(broadcast): 

2099 return 

2100 prepared_here = not self.adapter._checkpoint_active 

2101 try: 

2102 if prepared_here: 

2103 self.adapter.prepare_checkpoint() 

2104 broadcast() 

2105 self.adapter._checkpoint_broadcasted = True 

2106 except Exception: 

2107 if prepared_here: 

2108 self.cleanup_synced_state() 

2109 raise 

2110 

2111 def cleanup_synced_state(self) -> None: 

2112 """Release HSDP checkpoint copies and restore host-resident slots.""" 

2113 try: 

2114 self.adapter.finish_checkpoint() 

2115 finally: 

2116 cleanup = getattr(self.optimizer, "cleanup_synced_state", None) 

2117 if callable(cleanup): 

2118 cleanup() 

2119 

2120 def load_state_dict(self, state_dict: Dict[str, Any]) -> None: 

2121 """Load optimizer state dict while keeping swappable tensors on CPU mirrors.""" 

2122 previous_active = self._export_active 

2123 self._export_active = True 

2124 try: 

2125 self.adapter.load_checkpoint_state_dict(state_dict) 

2126 finally: 

2127 self._export_active = previous_active 

2128 

2129 @contextlib.contextmanager 

2130 def _no_grad_context(self) -> Iterable[None]: 

2131 with torch.no_grad(): 

2132 yield