Coverage for  / home / jenkins / .local / lib / python3.10 / site-packages / hyper_parallel / platform / torch / fully_shard / state_dict_utils.py: 99%

79 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-08-04 05: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"""State dict utilities for fully_shard (torch-specific).""" 

16from typing import Any, Optional 

17 

18import torch 

19import torch.distributed as dist 

20from torch import nn 

21from torch.distributed.checkpoint.state_dict import StateDictOptions 

22 

23from hyper_parallel.core.dtensor.dtensor import DTensor, distribute_tensor 

24 

25 

26def _gather_full_state_dict( 

27 state_dict: dict[str, Any], cpu_offload: bool 

28) -> dict[str, Any]: 

29 """All-gather every DTensor shard into a full tensor. 

30 

31 Args: 

32 state_dict: Model state dict with DTensor or plain tensor values. 

33 cpu_offload: If True, only rank-0 keeps the result on CPU; 

34 other ranks return an empty dict to save memory. 

35 """ 

36 is_rank0 = (not dist.is_initialized()) or (dist.get_rank() == 0) 

37 

38 gathered: dict[str, Any] = {} 

39 for key, val in state_dict.items(): 

40 if isinstance(val, DTensor): 

41 val = val.full_tensor() 

42 if cpu_offload: 

43 if not is_rank0: 

44 del val 

45 continue 

46 if isinstance(val, torch.Tensor): 

47 val = val.cpu() 

48 gathered[key] = val 

49 

50 if cpu_offload and not is_rank0: 

51 return {} 

52 return gathered 

53 

54 

55def _offload_sharded_state_dict( 

56 state_dict: dict[str, Any], 

57) -> dict[str, Any]: 

58 """Move each shard to CPU without all-gathering. 

59 

60 Args: 

61 state_dict: Model state dict with DTensor or plain tensor values. 

62 """ 

63 offloaded: dict[str, Any] = {} 

64 for key, val in state_dict.items(): 

65 if isinstance(val, DTensor): 

66 val = DTensor.from_local( 

67 val.to_local().cpu(), val.device_mesh, val.layout.alias_placements, 

68 ) 

69 elif isinstance(val, torch.Tensor): 

70 val = val.cpu() 

71 offloaded[key] = val 

72 return offloaded 

73 

74 

75def get_model_state_dict( 

76 model: nn.Module, 

77 *, 

78 options: Optional[StateDictOptions] = None, 

79) -> dict[str, Any]: 

80 """Return the model state dict with configurable gathering and offloading. 

81 

82 Behaviour matrix: 

83 

84 +-----------------+-------------+--------------------------------------+ 

85 | full_state_dict | cpu_offload | result | 

86 +=================+=============+======================================+ 

87 | False | False | DTensor (sharded, as-is) | 

88 +-----------------+-------------+--------------------------------------+ 

89 | False | True | DTensor local shard offloaded to CPU | 

90 +-----------------+-------------+--------------------------------------+ 

91 | True | False | full Tensor on **every** rank | 

92 +-----------------+-------------+--------------------------------------+ 

93 | True | True | full Tensor on CPU, **rank 0 only** | 

94 +-----------------+-------------+--------------------------------------+ 

95 

96 Args: 

97 model: The model whose state dict to retrieve. 

98 options: Controls full_state_dict, cpu_offload, 

99 ignore_frozen_params, and broadcast_from_rank0 flags. 

100 

101 Raises: 

102 ValueError: If ``broadcast_from_rank0`` is True while 

103 ``full_state_dict`` is False. 

104 NotImplementedError: If ``broadcast_from_rank0`` is True. 

105 ``broadcast_from_rank0`` requires a cross-rank tensor broadcast, 

106 which is not available. Use ``full_state_dict=True`` with a full 

107 tensor on every rank instead. 

108 """ 

109 options = options or StateDictOptions() 

110 

111 if options.broadcast_from_rank0 and not options.full_state_dict: 

112 raise ValueError( 

113 "full_state_dict must be True when broadcast_from_rank0 is True." 

114 ) 

115 if options.broadcast_from_rank0: 

116 raise NotImplementedError( 

117 "broadcast_from_rank0=True is not supported. " 

118 "broadcast_from_rank0 requires a cross-rank tensor broadcast, " 

119 "which is not available. Use full_state_dict=True with a full " 

120 "tensor on every rank instead." 

121 ) 

122 

123 state_dict: dict[str, Any] = model.state_dict() 

124 

125 if options.ignore_frozen_params: 

126 frozen_keys = { 

127 name for name, p in model.named_parameters() 

128 if not p.requires_grad 

129 } 

130 for key in frozen_keys: 

131 state_dict.pop(key, None) 

132 

133 if options.full_state_dict: 

134 return _gather_full_state_dict(state_dict, options.cpu_offload) 

135 

136 if options.cpu_offload: 

137 return _offload_sharded_state_dict(state_dict) 

138 

139 return state_dict 

140 

141 

142def _scatter_model_state_dict( 

143 model: nn.Module, 

144 state_dict: dict[str, Any], 

145 cpu_offload: bool, 

146 strict: bool, 

147) -> dict[str, Any]: 

148 """Scatter full tensors into DTensor shards matching model's layout. 

149 

150 Inverse of _gather_full_state_dict. Uses distribute_tensor (no communication, 

151 assumes every rank holds the same global tensor). 

152 

153 When ``cpu_offload`` is True the input tensors are expected to live on CPU 

154 (e.g. produced by ``get_model_state_dict(full_state_dict=True, cpu_offload=True)``). 

155 The scattered local shard is moved onto the target parameter's device so the 

156 subsequent ``load_state_dict(assign=True)`` does not leave the parameter on 

157 the wrong device. 

158 

159 Args: 

160 model: The model whose layout the scattered tensors must match. 

161 state_dict: Input state dict whose values are plain (global) tensors. 

162 cpu_offload: Whether the input tensors live on CPU and must be moved 

163 back onto the target parameter's device after scattering. 

164 strict: When True, keys present in ``state_dict`` but absent from the 

165 model are treated as errors (unexpected keys), mirroring the 

166 PyTorch ``load_state_dict(strict=True)`` semantics of the 

167 ``full_state_dict=False`` passthrough path. 

168 

169 Raises: 

170 ValueError: If ``strict`` is True and ``state_dict`` contains keys that 

171 do not exist in the model (unexpected keys). 

172 """ 

173 target_state_dict = model.state_dict() 

174 scattered: dict[str, Any] = {} 

175 unexpected_keys: list[str] = [] 

176 for key, val in state_dict.items(): 

177 target = target_state_dict.get(key) 

178 if target is None: 

179 unexpected_keys.append(key) 

180 continue 

181 if isinstance(target, DTensor): 

182 if isinstance(val, DTensor): 

183 scattered[key] = val 

184 else: 

185 # Slice a plain (global) tensor into a DTensor shard. 

186 placements = ( 

187 target.layout.alias_placements if target.layout else target.placements 

188 ) 

189 scattered[key] = distribute_tensor(val, target.device_mesh, placements) 

190 else: 

191 scattered[key] = val 

192 

193 # When the input came from CPU (cpu_offload=True round-trip), move the 

194 # scattered local shard onto the target parameter's device. assign=True 

195 # would otherwise bind a CPU DTensor to an on-device parameter. 

196 if cpu_offload and isinstance(scattered[key], DTensor) and isinstance(target, DTensor): 

197 dt = scattered[key] 

198 target_device = target._local_tensor.device # pylint: disable=protected-access 

199 scattered[key] = DTensor.from_local( 

200 dt.to_local().to(target_device), 

201 dt.device_mesh, 

202 dt.layout.alias_placements if dt.layout else dt.placements, 

203 ) 

204 

205 if strict and unexpected_keys: 

206 raise ValueError( 

207 f"Unexpected key(s) in state_dict: {unexpected_keys}. " 

208 f"To allow loading a state_dict with extra keys, pass strict=False." 

209 ) 

210 return scattered 

211 

212 

213def set_model_state_dict( 

214 model: nn.Module, 

215 state_dict: dict[str, Any], 

216 *, 

217 options: Optional[StateDictOptions] = None, 

218) -> None: 

219 """Load state dict into model, scattering full tensors to DTensor shards. 

220 

221 Behaviour matrix: 

222 

223 +-----------------+-------------+--------------------------------+ 

224 | full_state_dict | cpu_offload | behaviour | 

225 +=================+=============+================================+ 

226 | True | False | scatter full -> DTensor on dev | 

227 +-----------------+-------------+--------------------------------+ 

228 | True | True | scatter full -> DTensor on dev | 

229 +-----------------+-------------+--------------------------------+ 

230 | False (any) | * | load sharded DTensors as-is | 

231 +-----------------+-------------+--------------------------------+ 

232 

233 Note: 

234 ``ignore_frozen_params`` is intentionally a no-op on the setter path, 

235 mirroring torch.distributed.checkpoint.state_dict: frozen parameters 

236 filtering is a getter-only feature. Callers that strip frozen keys 

237 from the input should also pass ``strict=False``. 

238 

239 Args: 

240 model: The model to load state into. 

241 state_dict: State dict to load. Values may be plain (global) tensors 

242 when ``full_state_dict=True`` or sharded DTensors otherwise. 

243 options: Controls full_state_dict, cpu_offload, strict and 

244 broadcast_from_rank0 flags. 

245 

246 Raises: 

247 ValueError: If ``broadcast_from_rank0`` is True while 

248 ``full_state_dict`` is False. 

249 NotImplementedError: If ``broadcast_from_rank0`` is True. 

250 ``broadcast_from_rank0`` requires a cross-rank tensor broadcast, 

251 which is not available. Use ``full_state_dict=True`` with a full 

252 tensor on every rank instead. 

253 """ 

254 options = options or StateDictOptions() 

255 

256 if options.broadcast_from_rank0 and not options.full_state_dict: 

257 raise ValueError( 

258 "full_state_dict must be True when broadcast_from_rank0 is True." 

259 ) 

260 if options.broadcast_from_rank0: 

261 raise NotImplementedError( 

262 "broadcast_from_rank0=True is not supported. " 

263 "broadcast_from_rank0 requires a cross-rank tensor broadcast, " 

264 "which is not available. Use full_state_dict=True with a full " 

265 "tensor on every rank instead." 

266 ) 

267 

268 # Scatter full tensors into DTensor shards matching the model's layout. 

269 if options.full_state_dict: 

270 scattered = _scatter_model_state_dict( 

271 model, state_dict, options.cpu_offload, options.strict, 

272 ) 

273 else: 

274 scattered = state_dict 

275 

276 model.load_state_dict(scattered, strict=options.strict, assign=True)