Coverage for  / home / jenkins / .local / lib / python3.10 / site-packages / hyper_parallel / core / shard / api.py: 98%

260 statements  

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

1# Copyright 2025 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"""shard""" 

16import inspect 

17from typing import Union, Callable, Dict, List 

18from functools import wraps 

19 

20import torch.distributed as dist 

21from torch import nn 

22 

23from hyper_parallel.core.dtensor.layout import Layout, DeviceMesh 

24from hyper_parallel.core.dtensor.dtensor import DTensor, _is_alias_placements 

25from hyper_parallel.core.dtensor.placement_types import Placement 

26from hyper_parallel.core.shard.utils import ( 

27 get_cell_construct, 

28 search_parameter_by_name, 

29 set_layout_into_parameter, 

30 update_parameter_by_name, 

31) 

32from hyper_parallel.core.shard.sharding_plan import ShardingPlan 

33 

34 

35def _has_kwargs(func): 

36 """_has_kwargs""" 

37 sig = inspect.signature(func) 

38 return any( 

39 param.default != inspect.Parameter.empty 

40 for param in sig.parameters.values() 

41 ) 

42 

43 

44def _get_param_name(func): 

45 """_get_param_name""" 

46 sig = inspect.signature(func) 

47 return list(sig.parameters.keys()) 

48 

49 

50def _convert_sharding_plan(sharding_plan: Dict, device_mesh: DeviceMesh) -> Dict: 

51 """ 

52 Convert sharding_plan values to Layout objects. 

53 

54 This function recursively traverses the sharding_plan and converts 

55 placement tuples (e.g., (Shard(0), Replicate())) to Layout objects. 

56 

57 Args: 

58 sharding_plan: The original sharding plan with tuple specifications 

59 device_mesh: The DeviceMesh to use for conversion 

60 

61 Returns: 

62 Dict: Converted sharding plan with Layout objects 

63 """ 

64 

65 def _is_placement_tuple(value): 

66 """Check if value is a placement specification tuple. 

67 

68 A placement tuple contains Placement instances (Shard, Replicate) or 

69 alias strings ("dp", "None"). It should NOT be a tuple of placement tuples. 

70 

71 Examples of placement tuples: 

72 (Shard(0), Replicate(), Shard(1)) -> True 

73 ("dp", "tp", "None") -> True 

74 (("dp", "tp"), "None") -> True (multi-axis sharding) 

75 

76 Examples of NON-placement tuples: 

77 ((Shard(0),), (Shard(1),)) -> False (tuple of placement tuples) 

78 """ 

79 if not isinstance(value, tuple) or len(value) == 0: 

80 return False 

81 

82 for item in value: 

83 # Placement instance is valid 

84 if isinstance(item, Placement): 

85 continue 

86 # String (alias name) is valid 

87 if isinstance(item, str): 

88 continue 

89 # Nested tuple needs special handling 

90 if isinstance(item, tuple): 

91 # Nested tuple of strings is valid (multi-axis sharding) 

92 if len(item) > 0 and all(isinstance(x, str) for x in item): 

93 continue 

94 # Nested tuple containing Placement means this is a tuple of placement tuples 

95 if len(item) > 0 and any(isinstance(x, Placement) for x in item): 

96 return False 

97 # Empty tuple or other cases - not valid 

98 return False 

99 # Any other type is not valid in a placement tuple 

100 return False 

101 

102 return True 

103 

104 def _to_layout(value): 

105 """Convert a single sharding specification to Layout.""" 

106 layout = Layout.from_device_mesh(device_mesh) 

107 if _is_alias_placements(value): 

108 result = layout(*value) 

109 else: 

110 result = layout(value) 

111 return result 

112 

113 def _convert_value(value, wrap_single_as_list=False): 

114 """Recursively convert value based on its structure.""" 

115 if value is None: 

116 return None 

117 

118 # Case 1: It's a placement tuple - convert to Layout 

119 if _is_placement_tuple(value): 

120 layout = _to_layout(value) 

121 # Wrap single layout in list if required (for input/output in forward) 

122 return [layout] if wrap_single_as_list else layout 

123 

124 # Case 2: It's a dict - recursively process each value 

125 if isinstance(value, dict): 

126 converted_dict = {} 

127 for k, v in value.items(): 

128 converted_dict[k] = _convert_value(v, wrap_single_as_list=False) 

129 return converted_dict 

130 

131 # Case 3: It's a list - recursively process each element 

132 if isinstance(value, list): 

133 return [_convert_value(v, wrap_single_as_list=False) for v in value] 

134 

135 # Case 4: It's a tuple but not a placement tuple - treat as list 

136 if isinstance(value, tuple): 

137 return [_convert_value(v, wrap_single_as_list=False) for v in value] 

138 

139 # Case 5: Other types (e.g., primitives) - return as is 

140 return value 

141 

142 def _convert_forward_plan(forward_plan): 

143 """Convert forward plan with special handling for input/output.""" 

144 if forward_plan is None: 

145 return None 

146 

147 converted = {} 

148 for key, value in forward_plan.items(): 

149 if key.endswith("input") or key.endswith("output"): 

150 # input/output need special handling: 

151 # - dict format: convert each value, keep as dict 

152 # - list/tuple format: convert each element, keep as list 

153 # - single placement tuple: convert and wrap in list 

154 if value is None: 

155 converted[key] = None 

156 elif isinstance(value, dict): 

157 # Dict format for kwargs: {"x": placements, "activation": placements} 

158 converted[key] = {k: _convert_value(v) for k, v in value.items()} 

159 elif isinstance(value, (list, tuple)): 

160 # Check if it's a single placement tuple or a list/tuple of placement tuples 

161 if _is_placement_tuple(value): 

162 # Single placement tuple - wrap in list 

163 converted[key] = [_to_layout(value)] 

164 else: 

165 # List/tuple of placements for multiple positional args 

166 converted[key] = [_convert_value(v) for v in value] 

167 else: 

168 converted[key] = _convert_value(value, wrap_single_as_list=True) 

169 else: 

170 # Other keys in forward plan 

171 converted[key] = _convert_value(value) 

172 return converted 

173 

174 # Main conversion logic 

175 converted_plan = {} 

176 

177 for key, value in sharding_plan.items(): 

178 if key == "forward": 

179 converted_plan[key] = _convert_forward_plan(value) 

180 elif key.endswith("input") or key.endswith("output"): 

181 # Top-level input/output (for callable sharding) 

182 if value is None: 

183 converted_plan[key] = None 

184 elif isinstance(value, dict): 

185 converted_plan[key] = {k: _convert_value(v) for k, v in value.items()} 

186 elif isinstance(value, (list, tuple)): 

187 if _is_placement_tuple(value): 

188 converted_plan[key] = [_to_layout(value)] 

189 else: 

190 converted_plan[key] = [_convert_value(v) for v in value] 

191 else: 

192 converted_plan[key] = _convert_value(value, wrap_single_as_list=True) 

193 else: 

194 # parameter and other keys - use standard recursive conversion 

195 converted_plan[key] = _convert_value(value) 

196 

197 return converted_plan 

198 

199 

200def _parallel_in(func, args, kwargs, layouts): 

201 """_parallel_in""" 

202 if not isinstance(layouts, (list, dict, tuple)): 

203 raise ValueError(f"The in_layout must be a list, tuple or dict, but got {type(layouts)}.") 

204 

205 params_name = _get_param_name(func) 

206 processed_args = list(args) 

207 processed_kwargs = dict(kwargs) 

208 

209 def _get_layout(index, is_list): 

210 """_get_layout""" 

211 if is_list: 

212 return layouts[index] 

213 param_name = params_name[index] 

214 return layouts[param_name] 

215 

216 is_list = isinstance(layouts, (list, tuple)) 

217 for i, arg in enumerate(args): 

218 if not isinstance(arg, DTensor): 

219 continue 

220 

221 to_layout = _get_layout(i, is_list) 

222 processed_args[i] = arg.redistribute(to_layout.mesh, to_layout.alias_placements) 

223 for k, v in kwargs.items(): 

224 if not isinstance(v, DTensor) or layouts.get(k) is None: 

225 processed_kwargs[k] = v 

226 continue 

227 to_layout = layouts[k] 

228 processed_kwargs[k] = v.redistribute(to_layout.mesh, to_layout.alias_placements) 

229 

230 return tuple(processed_args), processed_kwargs 

231 

232 

233def _parallel_out(outputs, layouts): 

234 """_parallel_out""" 

235 if not isinstance(layouts, (list, tuple)): 

236 raise ValueError(f"The out_layout must be a list or tuple, but got {type(layouts)}.") 

237 if isinstance(outputs, (tuple, list)): 

238 if len(outputs) != len(layouts): 

239 raise ValueError(f"The size of outputs and out_layout must be equal, but got {len(outputs)} and " 

240 f"{len(layouts)}") 

241 new_outputs = [] 

242 for i, arg in enumerate(outputs): 

243 if not isinstance(arg, DTensor) or arg is None: 

244 new_outputs.append(arg) 

245 continue 

246 to_layout = layouts[i] 

247 new_outputs.append(arg.redistribute(to_layout.mesh, to_layout.alias_placements)) 

248 return tuple(new_outputs) 

249 if len(layouts) != 1: 

250 raise ValueError(f"The size of outputs and out_layout must be equal, but got 1 and " 

251 f"{len(layouts)}") 

252 

253 if isinstance(outputs, DTensor): 

254 return outputs.redistribute( 

255 layouts[0].mesh, layouts[0].alias_placements) 

256 return outputs 

257 

258 

259def _forward_pre_hook(cell, args): 

260 """_forward_pre_hook""" 

261 if cell.in_layout is None: 

262 return args 

263 processed_args, _ = _parallel_in(get_cell_construct(cell), args, {}, cell.in_layout) 

264 return processed_args 

265 

266 

267def _forward_pre_with_kwargs_hook(cell, args, kwargs): 

268 """_forward_pre_with_kwargs_hook""" 

269 if cell.in_layout is None: 

270 return args, kwargs 

271 return _parallel_in(get_cell_construct(cell), args, kwargs, cell.in_layout) 

272 

273 

274def _forward_hook(cell, inputs, outputs): # pylint: disable=unused-argument 

275 """_forward_hook""" 

276 if cell.out_layout is None: 

277 return outputs 

278 return _parallel_out(outputs, cell.out_layout) 

279 

280 

281def _forward_with_kwargs_hook(cell, inputs, kwargs, outputs): # pylint: disable=unused-argument 

282 """_forward_with_kwargs_hook""" 

283 return _forward_hook(cell, inputs, outputs) 

284 

285 

286def _split_sharding_plan_key(key: str) -> tuple[str, str]: 

287 """Split a sharding plan key into its module prefix and layout suffix.""" 

288 if '.' not in key: 

289 return "", key 

290 return tuple(key.rsplit('.', 1)) 

291 

292 

293def _register_hook(model: nn.Module, sharding_plan: Dict): 

294 """_register_hook""" 

295 

296 def _register_cell_hook(model, has_inputs_layout, has_outputs_layout): 

297 """_register_cell_hook""" 

298 has_kwargs = _has_kwargs(get_cell_construct(model)) 

299 pre_hook = _forward_pre_with_kwargs_hook if has_kwargs else _forward_pre_hook 

300 hook = _forward_with_kwargs_hook if has_kwargs else _forward_hook 

301 if has_inputs_layout: 

302 model.register_forward_pre_hook(pre_hook, with_kwargs=has_kwargs) 

303 

304 if has_outputs_layout: 

305 model.register_forward_hook(hook, with_kwargs=has_kwargs) 

306 

307 def _set_layouts(model, layouts, set_inputs_layout, set_outputs_layout): 

308 """_set_layouts""" 

309 if set_inputs_layout: 

310 model.in_layout = layouts 

311 

312 if set_outputs_layout: 

313 model.out_layout = layouts 

314 

315 cell_dict = {} 

316 for name, cell in model.named_modules(): 

317 cell_dict[name] = cell 

318 

319 valid_suffix = ["input", "output"] 

320 for key, value in sharding_plan.items(): 

321 if value is None: 

322 continue 

323 prefix, suffix = _split_sharding_plan_key(key) 

324 if suffix not in valid_suffix: 

325 raise ValueError(f"In python shard_module, sharding_plan's forward key must end with input or output, " 

326 f"but got type {suffix}") 

327 

328 set_inputs_layout = suffix == "input" 

329 set_outputs_layout = not set_inputs_layout 

330 register_cell = cell_dict.get(prefix) 

331 if register_cell is None: 

332 raise ValueError(f"Cannot find target cell {prefix!r} in sharding_plan") 

333 

334 _set_layouts(register_cell, value, set_inputs_layout, set_outputs_layout) 

335 _register_cell_hook(register_cell, set_inputs_layout, set_outputs_layout) 

336 

337 

338def _register_local_tensor_hook(cell: nn.Module, return_local_tensor_list: List[str]): 

339 """_register_local_tensor_hook""" 

340 

341 def hook_func(cell, inputs, outputs): # pylint: disable=unused-argument 

342 def _recursive_to_local(out): 

343 if isinstance(out, (tuple, list)): 

344 new_out = [] 

345 for item in out: 

346 new_out.append(_recursive_to_local(item)) 

347 return tuple(new_out) if isinstance(out, tuple) else new_out 

348 if isinstance(out, DTensor): 

349 return out.to_local() 

350 return out 

351 

352 return _recursive_to_local(outputs) 

353 

354 cell_dict = {} 

355 for name, sub_cell in cell.named_modules(): 

356 cell_dict[name] = sub_cell 

357 

358 for cell_name in return_local_tensor_list: 

359 register_cell = cell_dict.get(cell_name) 

360 if register_cell is None: 

361 raise KeyError(f"Cannot find cell {cell_name!r} in sharded cell.") 

362 register_cell.register_forward_hook(hook_func) 

363 

364 

365def _shard_callable(func: Callable, sharding_plan: Dict): 

366 """_shard_callable""" 

367 forward_sharding_plan = sharding_plan.get("forward") 

368 if forward_sharding_plan is None: 

369 return func 

370 

371 @wraps(func) 

372 def _shard_wrapper(*args, **kwargs): 

373 """_shard_wrapper""" 

374 input_layout = forward_sharding_plan.get("input") 

375 output_layout = forward_sharding_plan.get("output") 

376 if input_layout is not None: 

377 args, kwargs = _parallel_in(func, args, kwargs, input_layout) 

378 outputs = func(*args, **kwargs) 

379 if output_layout is not None: 

380 outputs = _parallel_out(outputs, output_layout) 

381 return outputs 

382 

383 return _shard_wrapper 

384 

385 

386def shard_module(model: Union[nn.Module, Callable], device_mesh: DeviceMesh, sharding_plan: ShardingPlan): 

387 """ 

388 Defining the input, output and parameters layouts of this cell or Callable. 

389 

390 Note: 

391 - It is valid only in pynative mode. 

392 

393 .. warning:: 

394 The method is currently not supported in Graph mode. 

395 

396 Args: 

397 model (Module or Callable): The model to be sharded. 

398 device_mesh (DeviceMesh): The device mesh for sharding. 

399 sharding_plan (ShardingPlan): Define the layout for the specified parameters, inputs or outputs. 

400 The sharding specification can be: 

401 - tuple of strings for alias format, e.g., ("dp", "None") 

402 - tuple of Placements, e.g., (Shard(0), Replicate()) 

403 

404 Returns: 

405 Module or Callable: The sharded model. 

406 

407 Examples: 

408 >>> # Usage with device_mesh and alias format 

409 >>> mesh = DeviceMesh("npu", (2, 2), nesh_dim_names=("dp", "tp")) 

410 >>> sharding_plan = ShardingPlan( 

411 ... plan={"mlp.weight": ("None", "tp")}, 

412 ... input_plan={"input": ("dp", "None")}, 

413 ... output_plan={"output": ("dp", "tp")} 

414 ... ) 

415 >>> model = shard_module(model, mesh, sharding_plan) 

416 

417 >>> # Usage with device_mesh and Placement format 

418 >>> mesh = DeviceMesh("npu", (2, 2), nesh_dim_names=("dp", "tp")) 

419 >>> sharding_plan = ShardingPlan( 

420 ... plan={"mlp.weight": (Replicate(), Shard(1))}, 

421 ... input_plan={"input": (Shard(0), Replicate())}, 

422 ... output_plan={"output": (Shard(0), Shard(1))} 

423 ... ) 

424 >>> model = shard_module(model, mesh, sharding_plan) 

425 """ 

426 if dist.get_world_size() == 1: 

427 return None 

428 

429 if not isinstance(sharding_plan, ShardingPlan): 

430 raise TypeError(f"The 'sharding_plan' must be an instance of ShardingPlan, " 

431 f"but got {type(sharding_plan)}. Direct dict input is not supported.") 

432 

433 normalized_plan = {} 

434 return_local_tensor_list = None 

435 

436 if sharding_plan.plan: 

437 normalized_plan["parameter"] = sharding_plan.plan 

438 

439 forward_part = {} 

440 

441 if sharding_plan.input_plan: 

442 if not isinstance(sharding_plan.input_plan, dict): 

443 raise TypeError(f"input_plan must be a dict, but got {type(sharding_plan.input_plan)}") 

444 forward_part.update(sharding_plan.input_plan) 

445 

446 if sharding_plan.output_plan: 

447 if not isinstance(sharding_plan.output_plan, dict): 

448 raise TypeError(f"output_plan must be a dict, but got {type(sharding_plan.output_plan)}") 

449 forward_part.update(sharding_plan.output_plan) 

450 

451 if forward_part: 

452 normalized_plan["forward"] = forward_part 

453 

454 if sharding_plan.return_local_tensor: 

455 return_local_tensor_list = sharding_plan.return_local_tensor 

456 

457 # Convert sharding_plan to Layout objects 

458 converted_plan = _convert_sharding_plan(normalized_plan, device_mesh) 

459 

460 if not isinstance(model, nn.Module): 

461 return _shard_callable(model, converted_plan) 

462 

463 param_sharding_plan = converted_plan.get("parameter") 

464 forward_sharding_plan = converted_plan.get("forward") 

465 

466 if param_sharding_plan is not None: 

467 for param_name, layout in param_sharding_plan.items(): 

468 if not isinstance(layout, Layout): 

469 raise ValueError(f"In python shard_module, the type of setting in parameter_plan must be Layout, " 

470 f"but got type {type(layout)}") 

471 result = search_parameter_by_name(model, param_name) 

472 if not result: 

473 raise ValueError(f"{param_name} is configured with a layout, but no instance was found.") 

474 _, _, param = result 

475 if layout.tensor_map is None: 

476 layout.placement_to_tensor_map(param.dim()) 

477 param = set_layout_into_parameter(param, layout) 

478 update_parameter_by_name(result, param) 

479 

480 if forward_sharding_plan is not None: 

481 _register_hook(model, forward_sharding_plan) 

482 

483 if return_local_tensor_list is not None: 

484 _register_local_tensor_hook(model, return_local_tensor_list) 

485 

486 return model