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

150 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-07-06 05:41 +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""" 

16Distributed implementation for OneHotExt operator. 

17""" 

18 

19from typing import Tuple 

20 

21from hyper_parallel.core.dtensor.layout import Layout 

22from hyper_parallel.platform import get_platform 

23from .parallel_ops import DistributedOp 

24 

25platform = get_platform() 

26 

27 

28def _normalize_one_hot_ext_args(indices, num_classes, on_value, off_value, axis): 

29 return (indices, num_classes, on_value, off_value, axis), {} 

30 

31 

32class OneHotExtDistributedOp(DistributedOp): 

33 """Distributed implementation for OneHotExt operator.""" 

34 

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

36 """ 

37 Preprocess arguments for OneHotExt operator. 

38 

39 Args: 

40 args (tuple): Input arguments (indices, num_classes, on_value, off_value, axis). 

41 kwargs (dict): Keyword arguments (empty for this operator). 

42 

43 Returns: 

44 tuple: (local_args, local_kwargs, cache_values) 

45 """ 

46 args, kwargs = _normalize_one_hot_ext_args(*args, **kwargs) 

47 indices, num_classes, on_value, off_value, axis = args 

48 

49 indices_local = indices.to_local() 

50 on_value_local = on_value.to_local() if hasattr(on_value, '_layout') else on_value 

51 off_value_local = off_value.to_local() if hasattr(off_value, '_layout') else off_value 

52 

53 on_value_layout = on_value.layout if hasattr(on_value, '_layout') else None 

54 off_value_layout = off_value.layout if hasattr(off_value, '_layout') else None 

55 

56 local_args = (indices_local, num_classes, on_value_local, off_value_local, axis) 

57 local_kwargs = {} 

58 cache_values = [indices.layout, on_value_layout, off_value_layout, num_classes, axis] 

59 return local_args, local_kwargs, cache_values 

60 

61 # pylint: disable=W0237 

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

63 """ 

64 Infer output layout for OneHotExt. 

65 

66 Rules: 

67 1. Indices must not have Partial status. 

68 2. num_classes must be int >= -1. 

69 3. axis must be in [-1, 1]. 

70 4. For multi-dimensional input (>1D), axis must be -1 and only dim0 may be sharded. 

71 5. Non-indices inputs must be fully replicated. 

72 6. Output layout inserts a replicated one-hot dimension at the specified axis. 

73 

74 Args: 

75 cache_values (list): [indices_layout, on_value_layout, off_value_layout, num_classes, axis] 

76 

77 Returns: 

78 tuple: ((output_layout,), None) 

79 

80 Raises: 

81 ValueError: If any rule above is violated. 

82 TypeError: If num_classes or axis has invalid type. 

83 """ 

84 indices_layout = cache_values[0] 

85 on_value_layout = cache_values[1] 

86 off_value_layout = cache_values[2] 

87 num_classes = cache_values[3] 

88 axis = cache_values[4] 

89 

90 if indices_layout is None or indices_layout.mesh_shape is None: 

91 raise ValueError( 

92 f"For {self.op_name}, indices layout cannot be None." 

93 ) 

94 

95 if not self._allow_partial_inputs: 

96 self._check_partial_inputs([indices_layout]) 

97 

98 self._validate_num_classes(num_classes) 

99 axis = self._validate_axis(axis) 

100 

101 in_tensor_map = indices_layout.tensor_map 

102 if not in_tensor_map: 

103 raise ValueError( 

104 f"For {self.op_name}, indices tensor_map is empty." 

105 ) 

106 

107 self._validate_multi_dim_restriction(in_tensor_map, axis, indices_layout) 

108 self._validate_inputs_layouts( 

109 [indices_layout, on_value_layout, off_value_layout] 

110 ) 

111 

112 out_tensor_map = self._infer_output_tensor_map(in_tensor_map, axis) 

113 out_layout = self._create_layout_from_tensor_map(indices_layout, out_tensor_map) 

114 out_layout.tensor_map_to_placement() 

115 

116 return ((out_layout,), None) 

117 

118 # pylint: disable=W0237 

119 def get_expand_impl(self, func, infer_result, cache_values): 

120 """ 

121 Get expanded implementation for OneHotExt operator. 

122 

123 When indices are sharded and num_classes is -1 (auto-detect), returns a 

124 closure that computes the global maximum index across all shards via 

125 AllReduce(max) before calling the original operator. 

126 

127 Args: 

128 func: Original operator callable. 

129 infer_result: Result from infer_layout (unused). 

130 cache_values (list): [indices_layout, on_value_layout, off_value_layout, num_classes, axis] 

131 

132 Returns: 

133 Optional[callable]: Closure or None if no expansion is needed. 

134 """ 

135 # pylint: disable=C0415 

136 import mindspore as ms 

137 from mindspore import ops, Tensor 

138 

139 indices_layout = cache_values[0] 

140 if indices_layout is None or indices_layout.mesh_shape is None: 

141 return None 

142 

143 sharded_axes = self._get_sharded_axes(indices_layout) 

144 if not sharded_axes: 

145 return None 

146 

147 original_op = func 

148 reduce_max = ops.ReduceMax(keep_dims=False) 

149 

150 def expanded_one_hot(indices, num_classes, on_value, off_value, axis): 

151 self._validate_num_classes(num_classes) 

152 self._validate_indices_dtype(indices) 

153 

154 if num_classes != -1: 

155 return original_op(indices, num_classes, on_value, off_value, axis) 

156 

157 local_max = reduce_max(indices, ()) 

158 if not isinstance(local_max, Tensor): 

159 local_max = Tensor(local_max, ms.int64) 

160 

161 local_max_host = int(local_max.asnumpy()) 

162 if local_max_host > 2147483647: 

163 raise ValueError( 

164 f"For {self.op_name}, indices max value {local_max_host} " 

165 f"exceeds int32 range." 

166 ) 

167 

168 zero_dim = local_max.ndim == 0 

169 local_max_i32 = ops.cast(local_max, ms.int32) 

170 

171 if zero_dim: 

172 local_max_i32 = ops.expand_dims(local_max_i32, 0) 

173 

174 global_max_i32 = local_max_i32 

175 for axis_name in sharded_axes: 

176 group = indices_layout.get_comm_group_by_axis(axis_name) 

177 global_max_i32 = platform.differentiable_all_reduce( 

178 global_max_i32, "max", group 

179 ) 

180 

181 if zero_dim: 

182 global_max_i32 = ops.squeeze(global_max_i32, 0) 

183 

184 depth = int(global_max_i32.asnumpy()) + 1 

185 return original_op(indices, depth, on_value, off_value, axis) 

186 

187 return expanded_one_hot 

188 

189 def _validate_num_classes(self, num_classes): 

190 """Validate num_classes parameter.""" 

191 if not isinstance(num_classes, int): 

192 raise TypeError( 

193 f"For {self.op_name}, num_classes should be int, " 

194 f"but got {type(num_classes).__name__}." 

195 ) 

196 if num_classes < -1: 

197 raise ValueError( 

198 f"For {self.op_name}, num_classes should be >= -1, " 

199 f"but got {num_classes}." 

200 ) 

201 

202 def _validate_indices_dtype(self, indices): 

203 """Validate indices dtype.""" 

204 # pylint: disable=C0415 

205 import mindspore as ms 

206 

207 if indices.dtype != ms.int64: 

208 raise TypeError( 

209 f"For {self.op_name}, indices dtype should be int64, " 

210 f"but got {indices.dtype}." 

211 ) 

212 

213 def _get_sharded_axes(self, layout): 

214 """Get all device axes that are used for sharding.""" 

215 sharded_axes = set() 

216 

217 if layout is None or layout.alias_tensor_map is None: 

218 return [] 

219 

220 for dim_alias in layout.alias_tensor_map: 

221 if dim_alias == "None": 

222 continue 

223 

224 if isinstance(dim_alias, tuple): 

225 for axis_name in dim_alias: 

226 if axis_name != "None": 

227 sharded_axes.add(axis_name) 

228 else: 

229 sharded_axes.add(dim_alias) 

230 

231 return list(sharded_axes) 

232 

233 def _validate_axis(self, axis): 

234 """Validate axis parameter.""" 

235 if not isinstance(axis, int): 

236 raise TypeError( 

237 f"For {self.op_name}, axis should be int, " 

238 f"but got {type(axis).__name__}." 

239 ) 

240 

241 if axis > 1 or axis < -1: 

242 raise ValueError( 

243 f"For {self.op_name}, axis {axis} is out of range [-1, 1]." 

244 ) 

245 

246 return axis 

247 

248 def _validate_multi_dim_restriction(self, in_tensor_map, axis, indices_layout): 

249 """Validate restriction for multi-dimensional inputs.""" 

250 in_rank = len(in_tensor_map) 

251 if in_rank <= 1: 

252 return 

253 

254 if axis != -1: 

255 raise ValueError( 

256 f"For {self.op_name}, when input dimension is > 1, axis should be -1, " 

257 f"but got {axis}." 

258 ) 

259 

260 alias_map = indices_layout.alias_tensor_map 

261 for i in range(1, len(alias_map)): 

262 if alias_map[i] != "None": 

263 raise ValueError( 

264 f"For {self.op_name}, when input dimension is > 1, " 

265 f"strategy should be data parallel, " 

266 f"but dimension {i} is sharded on '{alias_map[i]}'." 

267 ) 

268 

269 def _validate_inputs_layouts(self, layouts): 

270 """Validate that non-indices inputs are fully replicated.""" 

271 for layout in layouts[1:]: 

272 if layout is None: 

273 continue 

274 alias_map = layout.alias_tensor_map 

275 if alias_map and any(x != "None" for x in alias_map): 

276 raise ValueError( 

277 f"For {self.op_name}, non-indices inputs should be replicated, " 

278 f"but got {alias_map}." 

279 ) 

280 

281 def _infer_output_tensor_map(self, in_tensor_map, axis): 

282 """Infer output tensor map by inserting one-hot dimension at specified axis.""" 

283 in_rank = len(in_tensor_map) 

284 

285 if axis in (-1, in_rank): 

286 insert_pos = in_rank 

287 else: 

288 insert_pos = axis 

289 

290 if insert_pos < 0 or insert_pos > in_rank: 

291 raise ValueError( 

292 f"For {self.op_name}, axis {axis} is out of range " 

293 f"for input with rank {in_rank}." 

294 ) 

295 

296 out_tensor_map = list(in_tensor_map) 

297 out_tensor_map.insert(insert_pos, -1) 

298 return tuple(out_tensor_map) 

299 

300 def _create_layout_from_tensor_map(self, base_layout, out_tensor_map): 

301 """Create output layout from tensor map.""" 

302 out_layout = Layout( 

303 mesh_shape=base_layout.mesh_shape, 

304 alias_name=base_layout.alias_name, 

305 rank_list=base_layout.rank_list, 

306 ) 

307 

308 out_layout.set_tensor_map(out_tensor_map) 

309 out_layout.set_alias_tensor_map( 

310 self._tensor_map_to_alias_tensor_map(base_layout, out_tensor_map) 

311 ) 

312 out_layout.update_compact_str() 

313 return out_layout 

314 

315 def _tensor_map_to_alias_tensor_map(self, base_layout, tensor_map): 

316 """Convert numeric tensor map to alias tensor map.""" 

317 alias_name = base_layout.alias_name 

318 alias_tensor_map = [] 

319 

320 for dim in tensor_map: 

321 if dim == -1: 

322 alias_tensor_map.append("None") 

323 continue 

324 

325 if isinstance(dim, tuple): 

326 names = tuple( 

327 alias_name[len(alias_name) - 1 - d] for d in dim if d != -1 

328 ) 

329 alias_tensor_map.append(names if names else "None") 

330 continue 

331 

332 alias_tensor_map.append(alias_name[len(alias_name) - 1 - dim]) 

333 

334 return tuple(alias_tensor_map) 

335