Coverage for  / home / jenkins / .local / lib / python3.10 / site-packages / hyper_parallel / platform / torch / swap_optimizer / adapters.py: 91%

365 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-08-21 04:29 +0800

1# Copyright 2026 Huawei Technologies Co., Ltd 

2# 

3# Licensed under the Apache License, Version 2.0 (the "License"); 

4# you may not use this file except in compliance with the License. 

5# You may obtain a copy of the License at 

6# 

7# http://www.apache.org/licenses/LICENSE-2.0 

8# 

9# Unless required by applicable law or agreed to in writing, software 

10# distributed under the License is distributed on an "AS IS" BASIS, 

11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 

12# See the License for the specific language governing permissions and 

13# limitations under the License. 

14# ============================================================================ 

15"""Torch Adam/AdamW swap optimizer adapters.""" 

16# pylint: disable=protected-access 

17 

18from __future__ import annotations 

19 

20import copy 

21from collections import defaultdict 

22from typing import Any, Dict, Iterable, List, Optional, Tuple 

23 

24import torch 

25 

26from hyper_parallel.core.optimizer.adamw import AdamW as HyperAdamW 

27from hyper_parallel.core.optimizer.adamw import adamw as hyper_adamw 

28from hyper_parallel.core.optimizer.swap_optimizer_base import ( 

29 OptimizerSwapAdapter, 

30 SwapSlot, 

31 UpdateUnit, 

32) 

33 

34 

35class TorchAdamBaseAdapter(OptimizerSwapAdapter): 

36 """Common Torch Adam/AdamW adapter logic.""" 

37 

38 functional_name = "adam" 

39 supported_cls = () 

40 decoupled_weight_decay = False 

41 is_hyper_adamw = False 

42 supports_fused = False 

43 

44 @classmethod 

45 def matches(cls, optimizer: Any) -> bool: 

46 """Return whether this adapter supports ``optimizer``.""" 

47 return isinstance(optimizer, cls.supported_cls) 

48 

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

50 super().__init__(optimizer, config, runtime) 

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

52 

53 def validate(self) -> None: 

54 """Validate unsupported optimizer flags.""" 

55 for group in self.optimizer.param_groups: 

56 if group.get("foreach", False) is True: 

57 raise ValueError("Swap optimizer does not support foreach=True.") 

58 if group.get("fused", False) is True and not self.supports_fused: 

59 raise ValueError("Swap optimizer does not support fused=True.") 

60 if group.get("differentiable", False): 

61 raise ValueError("Swap optimizer does not support differentiable=True.") 

62 if group.get("capturable", False): 

63 raise ValueError("Swap optimizer does not support capturable=True.") 

64 

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

66 """Initialize lazy state and collect this step's update units.""" 

67 if args or kwargs: 

68 raise ValueError("Torch swap optimizer step does not support closure or extra arguments.") 

69 if self.runtime.packed_enabled: 

70 return self._prepare_packed_step() 

71 

72 units = [] 

73 for group_index, group in enumerate(self.optimizer.param_groups): 

74 if self.is_hyper_adamw: 

75 group["step"] = (group.get("step") or 0) + 1 

76 for param in group["params"]: 

77 grad = getattr(param, "grad", None) 

78 if grad is None: 

79 continue 

80 if getattr(grad, "is_sparse", False): 

81 raise ValueError("Swap optimizer only supports dense Adam/AdamW gradients.") 

82 state = self.optimizer.state[param] 

83 self._init_param_state(param, grad, group) 

84 slots = self._build_slots(param, state) 

85 units.append(UpdateUnit( 

86 adapter_index=group_index, 

87 param=param, 

88 grad=grad, 

89 slots=slots, 

90 )) 

91 return {"units": units} 

92 

93 def _prepare_packed_step(self) -> Dict[str, Any]: 

94 """Build a stable packed layout while retaining inactive materialized states.""" 

95 records = [] 

96 for group_index, group in enumerate(self.optimizer.param_groups): 

97 if self.is_hyper_adamw: 

98 group["step"] = (group.get("step") or 0) + 1 

99 for param in group["params"]: 

100 grad = getattr(param, "grad", None) 

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

102 if grad is not None: 

103 if getattr(grad, "is_sparse", False): 

104 raise ValueError("Swap optimizer only supports dense Adam/AdamW gradients.") 

105 state = self.optimizer.state[param] 

106 self._init_param_state(param, grad, group) 

107 if state: 

108 self._register_present_slots(param, state) 

109 has_slots = any((id(param), key) in self._slots for key in self._configured_state_keys()) 

110 if grad is None and not has_slots: 

111 continue 

112 records.append((group_index, param, grad)) 

113 

114 self.runtime.prepare_packed_host(self._ordered_slots()) 

115 self.publish_packed_state() 

116 units = [] 

117 for group_index, param, grad in records: 

118 state = self.optimizer.state[param] 

119 slots = self._build_slots(param, state) 

120 if grad is None and not any(slot.swappable and slot.packed for slot in slots): 

121 continue 

122 units.append(UpdateUnit( 

123 adapter_index=group_index, 

124 param=param, 

125 grad=grad, 

126 slots=slots, 

127 )) 

128 return {"units": units} 

129 

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

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

132 return step_context["units"] 

133 

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

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

136 slots = [] 

137 for group in self.optimizer.param_groups: 

138 for param in group["params"]: 

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

140 if state: 

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

142 return tuple(slots) 

143 

144 def step_batch(self, batch: List[UpdateUnit], step_context: Dict[str, Any]) -> None: 

145 """Run Torch functional Adam/AdamW for one batch.""" 

146 del step_context 

147 by_group: Dict[int, List[UpdateUnit]] = defaultdict(list) 

148 for unit in batch: 

149 by_group[unit.adapter_index].append(unit) 

150 for group_index, units in by_group.items(): 

151 group = self.optimizer.param_groups[group_index] 

152 state_steps = [] 

153 params = [] 

154 grads = [] 

155 exp_avgs = [] 

156 exp_avg_sqs = [] 

157 max_exp_avg_sqs = [] 

158 for unit in units: 

159 if unit.grad is None: 

160 continue 

161 state = self.optimizer.state[unit.param] 

162 params.append(unit.param) 

163 grads.append(unit.grad) 

164 exp_avgs.append(self._slot_tensor(unit, "exp_avg", state["exp_avg"])) 

165 exp_avg_sqs.append(self._slot_tensor(unit, "exp_avg_sq", state["exp_avg_sq"])) 

166 if group.get("amsgrad", False): 

167 max_exp_avg_sqs.append( 

168 self._slot_tensor(unit, "max_exp_avg_sq", state["max_exp_avg_sq"]) 

169 ) 

170 if self.is_hyper_adamw: 

171 state_steps.append(None) 

172 else: 

173 state_steps.append(state["step"]) 

174 

175 if not params: 

176 continue 

177 

178 if self.is_hyper_adamw: 

179 if params and params[0].device.type == "cpu": 

180 # torch.optim._functional.adamw increments tensor state_steps 

181 # internally. Hyper AdamW already advanced group["step"] in 

182 # prepare_step(), so feed step - 1 to preserve outer-step 

183 # semantics for CPU-only tests. 

184 step_tensor = torch.tensor(float(group["step"] - 1), dtype=torch.float32) 

185 torch.optim._functional.adamw( 

186 params, 

187 grads, 

188 exp_avgs, 

189 exp_avg_sqs, 

190 max_exp_avg_sqs, 

191 [step_tensor] * len(params), 

192 amsgrad=group["amsgrad"], 

193 beta1=group["betas"][0], 

194 beta2=group["betas"][1], 

195 lr=group["lr"], 

196 weight_decay=group["weight_decay"], 

197 eps=group["eps"], 

198 maximize=group["maximize"], 

199 foreach=False, 

200 capturable=False, 

201 differentiable=False, 

202 fused=False, 

203 grad_scale=None, 

204 found_inf=None, 

205 has_complex=False, 

206 ) 

207 else: 

208 hyper_adamw( 

209 params, 

210 grads, 

211 exp_avgs, 

212 exp_avg_sqs, 

213 max_exp_avg_sqs, 

214 group["step"], 

215 amsgrad=group["amsgrad"], 

216 beta1=group["betas"][0], 

217 beta2=group["betas"][1], 

218 lr=group["lr"], 

219 weight_decay=group["weight_decay"], 

220 eps=group["eps"], 

221 maximize=group["maximize"], 

222 ) 

223 continue 

224 

225 func = getattr(torch.optim._functional, self.functional_name) 

226 kwargs = { 

227 "amsgrad": group["amsgrad"], 

228 "beta1": group["betas"][0], 

229 "beta2": group["betas"][1], 

230 "lr": group["lr"], 

231 "weight_decay": group["weight_decay"], 

232 "eps": group["eps"], 

233 "maximize": group["maximize"], 

234 "foreach": False, 

235 "capturable": False, 

236 "differentiable": False, 

237 "fused": bool(group.get("fused", False)), 

238 "grad_scale": getattr(self.optimizer, "grad_scale", None), 

239 "found_inf": getattr(self.optimizer, "found_inf", None), 

240 "has_complex": False, 

241 } 

242 if self.functional_name == "adam": 

243 kwargs["decoupled_weight_decay"] = self.decoupled_weight_decay or group.get( 

244 "decoupled_weight_decay", False 

245 ) 

246 func(params, grads, exp_avgs, exp_avg_sqs, max_exp_avg_sqs, state_steps, **kwargs) 

247 

248 def all_slots(self): 

249 """Iterate known swap slots.""" 

250 return tuple(self._ordered_slots()) 

251 

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

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

254 del args, kwargs 

255 self.runtime.synchronize_cpu_mirrors(self.all_slots()) 

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

257 

258 def load_checkpoint_state_dict( 

259 self, 

260 state_dict: Dict[str, Any], 

261 *args: Any, 

262 **kwargs: Any, 

263 ) -> None: 

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

265 del args, kwargs 

266 stripped, removed = self.strip_swappable_state(state_dict) 

267 self.optimizer.load_state_dict(stripped) 

268 self.load_swappable_state(state_dict, removed) 

269 

270 def publish_packed_state(self) -> None: 

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

272 if not self.runtime.packed_enabled: 

273 return 

274 for group in self.optimizer.param_groups: 

275 for param in group["params"]: 

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

277 for key in self._configured_state_keys(): 

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

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

280 continue 

281 if state is None: 

282 state = self.optimizer.state[param] 

283 state[key] = slot.cpu_tensor 

284 

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

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

287 

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

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

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

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

292 

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

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

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

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

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

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

299 """ 

300 exported = { 

301 key: copy.deepcopy(value) 

302 for key, value in state_dict.items() 

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

304 } 

305 exported["param_groups"] = copy.deepcopy(state_dict.get("param_groups", [])) 

306 exported["state"] = {} 

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

308 params_in_order = [] 

309 for group in self.optimizer.param_groups: 

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

311 ids_in_order = [] 

312 for group in saved_groups: 

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

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

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

316 if not saved_state: 

317 continue 

318 exported_state = {} 

319 for key in self._state_keys_for_param(param): 

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

321 if slot is not None and slot.state == "host" and slot.cpu_tensor is not None and key in saved_state: 

322 exported_state[key] = slot.cpu_tensor.detach().clone() 

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

324 if key not in exported_state: 

325 exported_state[key] = copy.deepcopy(value) 

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

327 return exported 

328 

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

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

331 

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

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

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

335 device. This method therefore deep-copies the checkpoint state dict, 

336 removes the Adam buffers that may be swap-managed, and returns them in a 

337 side table keyed by the checkpoint parameter id. 

338 

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

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

341 step counters. The removed tensors must be handed to 

342 ``load_swappable_state`` afterwards so they can be restored with the 

343 correct CPU mirror/device placeholder layout. 

344 """ 

345 stripped = copy.deepcopy(state_dict) 

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

347 swappable_keys = self._configured_state_keys() 

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

349 if not isinstance(saved_state, dict): 

350 continue 

351 for key in swappable_keys: 

352 if key in saved_state: 

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

354 return stripped, removed 

355 

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

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

358 

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

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

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

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

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

364 

365 Packed runtimes place checkpoint values directly in persistent pinned 

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

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

368 runtime's swappability criteria are materialized directly on the 

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

370 """ 

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

372 current_groups = self.optimizer.param_groups 

373 self._slots = {} 

374 saved_ids = [] 

375 current_params = [] 

376 for saved_group, current_group in zip(saved_groups, current_groups): 

377 saved_ids.extend(saved_group["params"]) 

378 current_params.extend(current_group["params"]) 

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

380 key_to_tensor = removed.get(saved_id, {}) 

381 if not key_to_tensor: 

382 continue 

383 state = self.optimizer.state[param] 

384 for key, saved_tensor in key_to_tensor.items(): 

385 cpu_tensor = self._cast_swappable_tensor_to_cpu(param, saved_tensor) 

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

387 if self.runtime.is_distributed_tensor(param): 

388 logical_tensor = torch.zeros_like( 

389 param, 

390 memory_format=torch.preserve_format, 

391 ) 

392 slot = self._make_slot(key, logical_tensor) 

393 state[key] = logical_tensor 

394 self.runtime.release_device_storage(slot) 

395 else: 

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

397 state[key] = cpu_tensor 

398 slot.tensor = cpu_tensor 

399 slot.cpu_tensor = cpu_tensor 

400 slot.state = "host" 

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

402 continue 

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

404 slot = self._make_slot(key, device_tensor) 

405 if slot.swappable: 

406 state[key] = device_tensor 

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

408 slot.state = "host" 

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

410 self.runtime.release_device_storage(slot) 

411 else: 

412 device_tensor = self._cast_state_tensor_like_torch( 

413 param, 

414 saved_tensor, 

415 saved_id, 

416 saved_groups, 

417 key, 

418 ) 

419 state[key] = device_tensor 

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

421 if self.runtime.packed_enabled: 

422 self.runtime.prepare_packed_host(self._ordered_slots()) 

423 self.publish_packed_state() 

424 

425 def _init_param_state(self, param: Any, grad: Any, group: Dict[str, Any]) -> None: 

426 """Initialize missing Adam state and swap slots for one parameter.""" 

427 del grad 

428 state = self.optimizer.state[param] 

429 if not self.is_hyper_adamw and len(state) == 0: 

430 step_device = ( 

431 param.device 

432 if group.get("fused", False) 

433 else ("cpu" if self.runtime.packed_enabled else param.device) 

434 ) 

435 state["step"] = torch.zeros((), dtype=torch.float32, device=step_device) 

436 state_keys = ["exp_avg", "exp_avg_sq"] 

437 if group.get("amsgrad", False): 

438 state_keys.append("max_exp_avg_sq") 

439 configured_keys = set(self._configured_state_keys()) 

440 for key in state_keys: 

441 if key in state or (id(param), key) in self._slots: 

442 continue 

443 if ( 

444 key in configured_keys 

445 and not self.runtime.packed_enabled 

446 and self.runtime.is_swappable_tensor(param, self.config.min_numel) 

447 ): 

448 cpu_tensor = self.runtime.make_zero_cpu_tensor_like(param) 

449 device_tensor = torch.empty_like(param, memory_format=torch.preserve_format) 

450 state[key] = device_tensor 

451 slot = self._make_slot(key, device_tensor) 

452 slot.cpu_tensor = cpu_tensor 

453 slot.state = "host" 

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

455 self.runtime.release_device_storage(slot) 

456 continue 

457 if key in configured_keys and self.runtime.is_packable_template(param, self.config.min_numel): 

458 self._slots[(id(param), key)] = self._make_slot(key, None, template=param) 

459 continue 

460 state[key] = torch.zeros_like(param, memory_format=torch.preserve_format) 

461 

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

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

464 for key in self._configured_state_keys(): 

465 tensor = state.get(key) 

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

467 continue 

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

469 

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

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

472 slots = [] 

473 for key in self._state_keys_for_param(param): 

474 tensor = state.get(key) 

475 if tensor is None: 

476 continue 

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

478 if slot is None: 

479 slot = self._make_slot(key, tensor) 

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

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

482 slot = self._make_slot(key, tensor) 

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

484 slots.append(slot) 

485 return slots 

486 

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

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

489 metadata_tensor = tensor if tensor is not None else template 

490 if metadata_tensor is None: 

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

492 if tensor is None: 

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

494 else: 

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

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

497 slot = SwapSlot( 

498 name=key, 

499 tensor=tensor, 

500 cpu_tensor=None, 

501 swappable=swappable, 

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

503 packed=packed, 

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

505 ) 

506 self.runtime.populate_slot_metadata(slot, metadata_tensor) 

507 return slot 

508 

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

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

511 slots = [] 

512 seen_slots = set() 

513 for group in self.optimizer.param_groups: 

514 for param in group["params"]: 

515 for key in self._configured_state_keys(): 

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

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

518 slots.append(slot) 

519 seen_slots.add(id(slot)) 

520 return slots 

521 

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

523 state = self.optimizer.state[param] 

524 keys = self._configured_state_keys() 

525 result = [] 

526 for key in keys: 

527 if key in state: 

528 result.append(key) 

529 elif self.config.state_keys is not None: 

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

531 return tuple(result) 

532 

533 @staticmethod 

534 def _slot_tensor(unit: UpdateUnit, key: str, fallback: Any) -> Any: 

535 """Return an active swap slot tensor, or the optimizer state fallback.""" 

536 for slot in unit.slots: 

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

538 return slot.tensor 

539 return fallback 

540 

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

542 """Return Adam state keys selected for swap by the current config.""" 

543 keys = self.config.state_keys or self._default_state_keys() 

544 result = [] 

545 for key in keys: 

546 if key == "master_param": 

547 if self.config.state_keys is not None: 

548 raise ValueError(f"Requested state key '{key}' is not available for {type(self.optimizer)!r}.") 

549 continue 

550 result.append(key) 

551 return tuple(result) 

552 

553 def _cast_state_tensor_like_torch( 

554 self, 

555 param: Any, 

556 saved_tensor: Any, 

557 saved_id: int, 

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

559 key: str, 

560 ) -> Any: 

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

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

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

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

565 if process is not None: 

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

567 if key == "step": 

568 return saved_tensor.detach().clone() 

569 if param.is_floating_point(): 

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

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

572 

573 def _cast_swappable_tensor_to_cpu(self, param: Any, saved_tensor: Any) -> Any: 

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

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

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

577 if param.is_floating_point(): 

578 return saved_tensor.detach().to(dtype=param.dtype, device="cpu") 

579 return saved_tensor.detach().to(device="cpu") 

580 

581 @staticmethod 

582 def _default_state_keys() -> Tuple[str, ...]: 

583 return ("exp_avg", "exp_avg_sq", "max_exp_avg_sq") 

584 

585 

586class TorchNativeAdamAdapter(TorchAdamBaseAdapter): 

587 """Adapter for ``torch.optim.Adam``.""" 

588 

589 functional_name = "adam" 

590 

591 @classmethod 

592 def matches(cls, optimizer: Any) -> bool: 

593 # AdamW inherits Adam in PyTorch. Keep Adam subclasses supported, but 

594 # let AdamW select its dedicated adapter (which preserves fused=True). 

595 return ( 

596 isinstance(optimizer, torch.optim.Adam) 

597 and not isinstance(optimizer, torch.optim.AdamW) 

598 ) 

599 

600 

601class TorchNativeAdamWAdapter(TorchAdamBaseAdapter): 

602 """Adapter for ``torch.optim.AdamW``.""" 

603 

604 functional_name = "adamw" 

605 supports_fused = True 

606 

607 @classmethod 

608 def matches(cls, optimizer: Any) -> bool: 

609 return isinstance(optimizer, torch.optim.AdamW) 

610 

611 

612class TorchHyperAdamWAdapter(TorchAdamBaseAdapter): 

613 """Adapter for hyper-parallel's fused AdamW.""" 

614 

615 functional_name = "adamw" 

616 supported_cls = (HyperAdamW,) 

617 is_hyper_adamw = True 

618 

619 @classmethod 

620 def matches(cls, optimizer: Any) -> bool: 

621 return isinstance(optimizer, HyperAdamW)