Coverage for  / home / jenkins / .local / lib / python3.10 / site-packages / hyper_parallel / core / shard / ops / parallel_reduce.py: 89%

192 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-07-06 05:41 +0800

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

16Distributed implementation for Reduce operator. 

17""" 

18 

19from copy import deepcopy 

20from typing import Sequence, Union, Tuple, List 

21 

22from hyper_parallel.core.dtensor.layout import Layout 

23from hyper_parallel.platform import get_platform 

24from .parallel_ops import DistributedOp 

25 

26platform = get_platform() 

27Tensor = platform.Tensor 

28 

29 

30StrOrTuple = Union[str, Tuple["StrOrTuple", ...], List["StrOrTuple"]] 

31 

32 

33def _normalize_reduce_args(input_tensor, dim=None, keepdim=False, dtype=None): 

34 """Normalize reduce-family arguments to consistent positional form. 

35 

36 Handles torch.sum / torch.mean / torch.prod / torch.all and MindSpore 

37 SumExt / MeanExt / ReduceMax / MaxDim where *dim* and *keepdim* are 

38 ordinary positional parameters. SumExt and MeanExt also pass a trailing 

39 dtype slot positionally; normalize it here while keeping layout inference 

40 based only on ``input_layout``, ``dim``, and ``keepdim``. 

41 

42 Args: 

43 input_tensor: The input tensor (DTensor or Tensor). 

44 dim: Dimension(s) to reduce. None means all dimensions, () means no reduction. 

45 keepdim: Whether to retain reduced dimensions. 

46 dtype: Optional output dtype. 

47 

48 Returns: 

49 tuple: ``((input_tensor, dim, keepdim, dtype), {})`` — all-positional, empty kwargs. 

50 """ 

51 return (input_tensor, dim, keepdim, dtype), {} 

52 

53 

54class ReduceExtDistributedOpBase(DistributedOp): 

55 """ 

56 Base class for distributed reduce operators. 

57 

58 Args: 

59 op_name (str): Name of the operator to register. 

60 partial_type (list): List of the operator for allreduce. 

61 """ 

62 

63 _TORCH_DTYPE_OP_NAMES = frozenset({"sum", "mean", "prod"}) 

64 _MS_POSITIONAL_DTYPE_OP_NAMES = frozenset({"SumExt", "MeanExt"}) 

65 

66 def __init__(self, op_name, partial_type=None): 

67 super().__init__(op_name) 

68 if partial_type is None: 

69 partial_type = ["sum"] 

70 self.partial_type = partial_type 

71 self._allow_partial_inputs = True 

72 

73 def preprocess(self, args: tuple, kwargs: dict) -> tuple: 

74 """ 

75 Preprocess arguments for reduce operators. 

76 

77 Normalizes ``(input, dim, keepdim)`` across torch and MindSpore call 

78 sites, converts DTensor inputs to local tensors, and builds 

79 ``cache_values`` for layout inference. 

80 

81 Args: 

82 args (tuple): Positional arguments passed to the operator call. 

83 kwargs (dict): Keyword arguments passed to the operator call. 

84 

85 Returns: 

86 tuple: ``(local_args, local_kwargs, cache_values)`` where 

87 ``local_args`` is ``(input_local, dim, keepdim)`` and 

88 ``cache_values`` is ``[input_layout, dim, keepdim]``. 

89 """ 

90 has_dtype_arg = len(args) >= 4 or "dtype" in kwargs 

91 normalized_args, _ = _normalize_reduce_args(*args, **kwargs) 

92 input_tensor, dim, keepdim, dtype = normalized_args 

93 local_args = (input_tensor.to_local(), dim, keepdim) 

94 local_kwargs = {} 

95 if has_dtype_arg: 

96 if self.op_name in self._TORCH_DTYPE_OP_NAMES: 

97 local_kwargs["dtype"] = dtype 

98 elif self.op_name in self._MS_POSITIONAL_DTYPE_OP_NAMES: 

99 local_args += (dtype,) 

100 else: 

101 raise TypeError( 

102 f"For {self.op_name}, the `dtype` argument is not supported." 

103 ) 

104 cache_values = [input_tensor.layout, dim, keepdim] 

105 return local_args, local_kwargs, cache_values 

106 

107 def infer_layout(self, cache_values: list) -> Tuple[tuple, None]: 

108 """ 

109 Infer output layout for reduce operators. 

110 

111 Rules: 

112 1. Input must have a valid mesh_shape. 

113 2. ``dim`` must be ``None``, ``int``, ``tuple[int]``, or ``list[int]`` — 

114 never a ``Tensor``. 

115 3. ``dim`` values must be valid axis indices for the input tensor. 

116 4. Reduce axes are replaced with ``"None"`` (keepdim) or dropped; 

117 Partial status is applied on sharded reduced axes. 

118 

119 Args: 

120 cache_values (list): ``[input_layout, dim, keepdim]``. 

121 

122 Returns: 

123 tuple: ``((output_layout,), None)`` 

124 

125 Raises: 

126 ValueError: If the input layout is missing or ``mesh_shape`` is ``None``. 

127 TypeError: If ``dim`` is a ``Tensor``. 

128 ValueError: If a ``dim`` axis index is out of range. 

129 """ 

130 x_layout = cache_values[0] 

131 dim = cache_values[1] 

132 keepdim = cache_values[2] 

133 

134 if x_layout is None or x_layout.mesh_shape is None: 

135 raise ValueError( 

136 f"For {self.op_name}, input layout cannot be None." 

137 ) 

138 

139 # Check partial inputs 

140 if not self._allow_partial_inputs: 

141 self._check_partial_inputs([x_layout]) 

142 

143 if dim is not None and not isinstance(dim, (int, tuple, list)): 

144 raise TypeError( 

145 f"For {self.op_name}, the `dim` argument should be `None`, `int`, " 

146 f"`tuple[int]`, or `list[int]`, but got {type(dim).__name__}." 

147 ) 

148 

149 # Infer the output shape based on dim and keepdim 

150 output_layout = self._infer_output_layout(x_layout, dim, keepdim) 

151 

152 return ((output_layout,), None) 

153 

154 def _infer_output_layout(self, x_layout, dim, keepdim): 

155 """Infer output layout for reduce operator.""" 

156 # Case 1: dim is None — reduce all dimensions (scalar output or all-None when keepdim). 

157 if dim is None: 

158 return self._handle_all_axis_reduce(x_layout, keepdim) 

159 

160 # Case 2: dim is an empty tuple/list — reduce no dimensions, output layout equals input. 

161 if isinstance(dim, (tuple, list)) and len(dim) == 0: 

162 output_layout = Layout( 

163 mesh_shape=x_layout.mesh_shape, 

164 alias_name=x_layout.alias_name, 

165 rank_list=x_layout.rank_list 

166 ) 

167 return output_layout(*x_layout.alias_tensor_map) 

168 

169 # Case 3: dim is int, tuple, or list with at least one element. 

170 output_layout = Layout( 

171 mesh_shape=x_layout.mesh_shape, 

172 alias_name=x_layout.alias_name, 

173 rank_list=x_layout.rank_list 

174 ) 

175 x_map = x_layout.alias_tensor_map 

176 reduce_alias, x_map = self.replace_axis_with_none(dim, x_layout, keepdim) 

177 output_layout = output_layout(*x_map) 

178 self._apply_partial(output_layout, reduce_alias) 

179 return output_layout 

180 

181 def _handle_all_axis_reduce(self, x_layout, keepdim): 

182 """Handle the case where dim is None, meaning reduce all dimensions.""" 

183 layout = Layout( 

184 mesh_shape=x_layout.mesh_shape, 

185 alias_name=x_layout.alias_name, 

186 rank_list=x_layout.rank_list 

187 ) 

188 

189 if not keepdim: 

190 output_layout = layout() 

191 else: 

192 tensor_map = tuple(["None"] * len(x_layout.alias_tensor_map)) 

193 output_layout = layout(*tensor_map) 

194 

195 self._apply_partial(output_layout, x_layout.alias_tensor_map) 

196 return output_layout 

197 

198 def replace_axis_with_none(self, dim, x_layout, keepdim): 

199 """Replace or drop dimensions depending on keepdim.""" 

200 if not isinstance(dim, (tuple, list)): 

201 dim = [dim] 

202 else: 

203 dim = list(dim) 

204 

205 rank = len(x_layout.alias_tensor_map) 

206 for i, axis_id in enumerate(dim): 

207 if axis_id < 0: 

208 dim[i] = rank + axis_id 

209 if not isinstance(axis_id, int) or dim[i] >= rank or dim[i] < 0: 

210 raise ValueError(f"Invalid reduce axis index {axis_id} at position {i}.") 

211 

212 alias_tensor_map = x_layout.alias_tensor_map 

213 reduce_alias = [alias_tensor_map[axis_id] for axis_id in dim if 

214 alias_tensor_map[axis_id] is not None and alias_tensor_map[axis_id] != "None"] 

215 reduce_alias = self._flatten_aliases(reduce_alias) 

216 

217 if keepdim: 

218 return self._replace_keepdim(alias_tensor_map, reduce_alias) 

219 return self._replace_dropdim(alias_tensor_map, reduce_alias, dim) 

220 

221 def _flatten_aliases(self, reduce_alias): 

222 """Flatten reduce_alias into a list of atomic alias strings.""" 

223 flat = [] 

224 for alias in reduce_alias: 

225 if isinstance(alias, (tuple, list)): 

226 flat.extend(alias) 

227 else: 

228 flat.append(alias) 

229 return flat 

230 

231 def _replace_keepdim(self, alias_tensor_map, reduce_alias): 

232 """keepdim, replace reduce alias with 'None'.""" 

233 new_alias_map = [] 

234 for alias in alias_tensor_map: 

235 if isinstance(alias, (tuple, list)): 

236 new_alias = tuple("None" if item in reduce_alias else item for item in alias) 

237 new_alias_map.append(new_alias) 

238 else: 

239 if alias in reduce_alias: 

240 new_alias_map.append("None") 

241 else: 

242 new_alias_map.append(alias) 

243 new_alias_map = self._compact_tensor_map(new_alias_map) 

244 return reduce_alias, tuple(new_alias_map) 

245 

246 def _replace_dropdim(self, alias_tensor_map, reduce_alias, dim): 

247 """Compress reduce dim.""" 

248 new_alias_map = [] 

249 for i, alias in enumerate(alias_tensor_map): 

250 if i in dim: 

251 continue 

252 if isinstance(alias, (tuple, list)): 

253 new_alias = tuple(item for item in alias if item not in reduce_alias) 

254 if new_alias: 

255 new_alias_map.append(new_alias) 

256 else: 

257 if alias in reduce_alias: 

258 continue 

259 new_alias_map.append(alias) 

260 new_alias_map = self._compact_tensor_map(new_alias_map) 

261 return reduce_alias, tuple(new_alias_map) 

262 

263 def _compact_tensor_map(self, alias_map: Sequence[StrOrTuple]) -> Tuple[StrOrTuple, ...]: 

264 """Extend tensor map of 'None'.""" 

265 

266 def _compress(elem: StrOrTuple) -> StrOrTuple: 

267 if isinstance(elem, (list, tuple)): 

268 compressed = tuple(_compress(e) for e in elem) 

269 if len(compressed) == 1: 

270 return compressed[0] 

271 if all(x == 'None' for x in compressed): 

272 return 'None' 

273 return compressed 

274 return elem 

275 

276 return tuple(_compress(elem) for elem in alias_map) 

277 

278 def _apply_partial(self, out_layout, alias): 

279 """Apply all partial to given alias (string, tuple, list).""" 

280 if alias == "None": 

281 return 

282 if isinstance(alias, (tuple, list)): 

283 for elem in alias: 

284 self._apply_partial(out_layout, elem) 

285 else: 

286 for ops in self.partial_type: 

287 out_layout.set_partial_by_dev_axis(alias, ops) 

288 

289 

290class SumExtDistributedOp(ReduceExtDistributedOpBase): 

291 """Distributed implementation for SumExt operator.""" 

292 

293 def __init__(self, op_name="SumExt"): 

294 super().__init__(op_name, partial_type=["sum"]) 

295 

296 

297class MeanExtDistributedOp(ReduceExtDistributedOpBase): 

298 """Distributed implementation for MeanExt operator.""" 

299 

300 def __init__(self, op_name="MeanExt"): 

301 super().__init__(op_name, partial_type=["avg"]) 

302 

303 

304class ReduceMaxDistributedOp(ReduceExtDistributedOpBase): 

305 """Distributed implementation for ReduceMax operator.""" 

306 

307 def __init__(self, op_name="ReduceMax"): 

308 super().__init__(op_name, partial_type=["max"]) 

309 

310 

311class ProdExtDistributedOp(ReduceExtDistributedOpBase): 

312 """ 

313 Distributed implementation for ProdExt operator (product of all elements or along a dim). 

314 Compatible with torch.prod arguments. 

315 """ 

316 

317 def __init__(self, op_name="prod"): 

318 super().__init__(op_name, partial_type=["prod"]) 

319 

320 

321class AllExtDistributedOp(ReduceExtDistributedOpBase): 

322 """ 

323 Distributed implementation for All operator 

324 Returns the cumulative sum of elements of input in the dimension dim. 

325 """ 

326 

327 def __init__(self, op_name="all"): 

328 super().__init__(op_name, partial_type=["all"]) 

329 

330 

331class MaxDistributedOp(ReduceExtDistributedOpBase): 

332 """ 

333 Distributed implementation for Pytorch style Max operator. 

334 

335 Supports three Pytorch behaviors: 

336 1. torch.max(input) -> Global reduction (returns single Tensor) 

337 2. torch.max(input, dim, keepdim=False) -> Dimension reduction (returns (values, indices)) 

338 3. torch.max(input, other) -> Element-wise max (returns single Tensor) 

339 """ 

340 

341 def __init__(self, op_name="max"): 

342 super().__init__(op_name, partial_type=["max"]) 

343 

344 def preprocess(self, args: tuple, kwargs: dict) -> tuple: 

345 """ 

346 Preprocess arguments for Max/Min operators. 

347 

348 Routes between element-wise mode (two DTensor inputs) and reduction 

349 mode (input, dim, keepdim). All parameters are positional with empty 

350 kwargs for MindSpore Primitive compatibility. 

351 

352 Args: 

353 args (tuple): Positional arguments passed to the operator call. 

354 kwargs (dict): Keyword arguments passed to the operator call. 

355 

356 Returns: 

357 tuple: ``(local_args, local_kwargs, cache_values)``. 

358 """ 

359 # Element-wise mode: torch.max(a, b) — second arg is a DTensor. 

360 if len(args) >= 2 and hasattr(args[1], "_layout"): 

361 input_tensor = args[0] 

362 other_tensor = args[1] 

363 local_args = (input_tensor.to_local(), other_tensor.to_local()) 

364 local_kwargs = {} 

365 cache_values = [input_tensor.layout, other_tensor.layout] 

366 return local_args, local_kwargs, cache_values 

367 

368 # Reduction mode: torch.max(input, dim, keepdim) / MaxDim(input, dim, keepdim). 

369 has_dtype_arg = len(args) >= 4 or "dtype" in kwargs 

370 args, kwargs = _normalize_reduce_args(*args, **kwargs) 

371 input_tensor, dim, keepdim, _ = args 

372 if has_dtype_arg: 

373 raise TypeError( 

374 f"For {self.op_name}, the `dtype` argument is not supported." 

375 ) 

376 # torch.max(x) global reduction: only pass the tensor so the call 

377 # remains torch.max(local_x), not torch.max(local_x, None, False). 

378 if dim is None: 

379 local_args = (input_tensor.to_local(),) 

380 else: 

381 local_args = (input_tensor.to_local(), dim, keepdim) 

382 local_kwargs = {} 

383 

384 cache_values = [input_tensor.layout, dim, keepdim] 

385 return local_args, local_kwargs, cache_values 

386 

387 def infer_layout(self, cache_values: list) -> Tuple[tuple, None]: 

388 """ 

389 Infer output layouts for Max/Min operators. 

390 

391 Rules: 

392 1. Element-wise mode (two Layouts in *cache_values*): output layout 

393 equals the first input layout. 

394 2. Reduction mode: same rules as ``ReduceExtDistributedOpBase``. 

395 3. When ``dim`` is ``None`` (global reduction), returns a single layout. 

396 4. When ``dim`` is specified, returns ``(values_layout, indices_layout)``. 

397 5. ``dim`` must be ``None``, ``int``, ``tuple[int]``, or ``list[int]``. 

398 

399 Args: 

400 cache_values (list): ``[input_layout, dim, keepdim]`` for reduction, 

401 or ``[input_layout, other_layout]`` for element-wise. 

402 

403 Returns: 

404 tuple: ``((output_layout,), None)`` or ``((values_layout, indices_layout), None)``. 

405 

406 Raises: 

407 ValueError: If the input layout is missing or ``mesh_shape`` is ``None``. 

408 TypeError: If ``dim`` is not ``None``, ``int``, ``tuple``, or ``list``. 

409 """ 

410 # Element-wise mode: two Layout objects in cache_values. 

411 if len(cache_values) == 2 and hasattr(cache_values[1], "mesh_shape"): 

412 # Check partial inputs 

413 if not self._allow_partial_inputs: 

414 self._check_partial_inputs(cache_values) 

415 return ((deepcopy(cache_values[0]),), None) 

416 

417 x_layout = cache_values[0] 

418 dim = cache_values[1] 

419 keepdim = cache_values[2] 

420 

421 if x_layout is None or x_layout.mesh_shape is None: 

422 raise ValueError( 

423 f"For {self.op_name}, input layout cannot be None." 

424 ) 

425 

426 # Check partial inputs 

427 if not self._allow_partial_inputs: 

428 self._check_partial_inputs([x_layout]) 

429 

430 if dim is not None and not isinstance(dim, (int, tuple, list)): 

431 raise TypeError( 

432 f"For {self.op_name}, the `dim` argument should be `None`, `int`, " 

433 f"`tuple[int]`, or `list[int]`, but got {type(dim).__name__}." 

434 ) 

435 

436 values_layout = self._infer_output_layout(x_layout, dim, keepdim) 

437 

438 if dim is None: 

439 # torch.max(input) -> Single Tensor 

440 return ((values_layout,), None) 

441 

442 # torch.max(input, dim) -> (values, indices) 

443 indices_layout = deepcopy(values_layout) 

444 return ((values_layout, indices_layout), None) 

445 

446 

447class MinDistributedOp(MaxDistributedOp): 

448 """ 

449 Distributed implementation for Pytorch style Min operator. 

450  

451 Supports three Pytorch behaviors: 

452 1. torch.min(input) -> Global reduction (returns single Tensor) 

453 2. torch.min(input, dim, keepdim=False) -> Dimension reduction (returns (values, indices)) 

454 3. torch.min(input, other) -> Element-wise min (returns single Tensor) 

455 """ 

456 

457 def __init__(self, op_name="min"): 

458 # Call the parent class (MaxDistributedOp) initialization 

459 super().__init__(op_name=op_name) 

460 # Override the partial_type to use "min" instead of "max" for the underlying communication 

461 self.partial_type = ["min"]