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

106 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 Expand operator. 

17""" 

18 

19from typing import Tuple 

20 

21from hyper_parallel.core.dtensor.layout import Layout 

22from .parallel_ops import DistributedOp 

23 

24 

25def _normalize_expand_args(input_tensor, *sizes): 

26 return (input_tensor, *sizes), {} 

27 

28 

29def _normalize_expand_as_args(input_tensor, target_tensor): 

30 return (input_tensor, target_tensor), {} 

31 

32 

33class ExpandDistributedOp(DistributedOp): 

34 """Distributed implementation for torch.Tensor.expand.""" 

35 

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

37 """ 

38 Preprocess arguments for Expand operator. 

39 

40 Args: 

41 args (tuple): Input arguments (input_tensor, *sizes). 

42 kwargs (dict): Keyword arguments (none for expand). 

43 

44 Returns: 

45 tuple: (local_args, local_kwargs, cache_values) 

46 """ 

47 args, kwargs = _normalize_expand_args(*args, **kwargs) 

48 input_tensor = args[0] 

49 sizes = tuple(args[1:]) 

50 local_args = (input_tensor.to_local(), *sizes) 

51 cache_values = [input_tensor.layout, input_tensor.shape, sizes] 

52 return local_args, {}, cache_values 

53 

54 @staticmethod 

55 def _validate_input_layouts( 

56 cache_values: list, 

57 op_name: str, 

58 ) -> Tuple[Layout, tuple, tuple, int]: 

59 """Validate all inputs for expand layout inference. 

60 

61 Performs type checks, shape validation, sizes validation, 

62 and dimension compatibility checks. 

63 

64 Rules: 

65 1. input_shape and sizes must be tuples of positive ints. 

66 2. Cannot reduce dimensions (output_ndim < input_ndim). 

67 3. -1 cannot be used for new (prepended) dimensions. 

68 

69 Args: 

70 cache_values: [input_layout, input_shape, sizes] 

71 op_name: Operator name for error messages. 

72 

73 Returns: 

74 tuple: (input_layout, input_shape, sizes, num_new_dims) 

75 

76 Raises: 

77 ValueError: If any validation rule is violated. 

78 """ 

79 if not cache_values: 

80 raise ValueError( 

81 f"For {op_name}, cache_values should contain input layout, " 

82 f"but got empty cache_values." 

83 ) 

84 input_layout = cache_values[0] 

85 

86 if not isinstance(input_layout, Layout): 

87 raise ValueError( 

88 f"For {op_name}, input layout should be a Layout, " 

89 f"but got {type(input_layout)}." 

90 ) 

91 

92 input_shape = cache_values[1] if len(cache_values) > 1 else None 

93 if not isinstance(input_shape, tuple): 

94 raise ValueError( 

95 f"For {op_name}, input_shape should be a tuple, " 

96 f"but got {type(input_shape)}." 

97 ) 

98 

99 sizes = cache_values[2] if len(cache_values) > 2 else None 

100 if sizes is None or len(sizes) < 1: 

101 raise ValueError( 

102 f"For {op_name}, sizes should be a non-empty tuple of ints, " 

103 f"but got {sizes}." 

104 ) 

105 for i, sz in enumerate(sizes): 

106 if not isinstance(sz, int): 

107 raise ValueError( 

108 f"For {op_name}, elements in sizes should be int, " 

109 f"but got {type(sz)} at position {i}." 

110 ) 

111 

112 in_alias_map = input_layout.alias_tensor_map 

113 input_ndim = len(in_alias_map) 

114 output_ndim = len(sizes) 

115 num_new_dims = output_ndim - input_ndim 

116 

117 if len(input_shape) != input_ndim: 

118 raise ValueError( 

119 f"For {op_name}, input_shape length ({len(input_shape)}) " 

120 f"must match input_ndim ({input_ndim})." 

121 ) 

122 

123 if num_new_dims < 0: 

124 raise ValueError( 

125 f"For {op_name}, cannot reduce dimensions with expand, " 

126 f"input has {input_ndim} dims but requested {output_ndim} dims." 

127 ) 

128 

129 # New dimensions cannot use -1 

130 for i in range(num_new_dims): 

131 if sizes[i] == -1: 

132 raise ValueError( 

133 f"For {op_name}, cannot use -1 for new dimension at position {i}, " 

134 f"sizes should be positive integers." 

135 ) 

136 

137 return input_layout, input_shape, sizes, num_new_dims 

138 

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

140 """ 

141 Infer output layout for torch.Tensor.expand. 

142 

143 Rules: 

144 1. Input must not have Partial status. 

145 2. input_shape and sizes must be tuples of positive ints. 

146 3. Cannot reduce dimensions (output_ndim < input_ndim). 

147 4. -1 cannot be used for new (prepended) dimensions. 

148 5. Same-size dimension: preserve original sharding. 

149 6. True broadcast (1 → N): the input dimension must be replicated. 

150 7. New dimensions are replicated. 

151 

152 Args: 

153 cache_values (list): [input_layout, input_shape, sizes] 

154 

155 Returns: 

156 tuple: ((output_layout,), None) 

157 

158 Raises: 

159 ValueError: If any rule above is violated. 

160 """ 

161 if not self._allow_partial_inputs and cache_values: 

162 self._check_partial_inputs([cache_values[0]]) 

163 

164 input_layout, input_shape, sizes, num_new_dims = self._validate_input_layouts( 

165 cache_values, self.op_name 

166 ) 

167 

168 in_alias_map = input_layout.alias_tensor_map 

169 input_ndim = len(in_alias_map) 

170 

171 output_map = [] 

172 

173 # New dimensions: always replicated 

174 for _ in range(num_new_dims): 

175 output_map.append("None") 

176 

177 # Process existing dimensions 

178 for i in range(input_ndim): 

179 output_dim_idx = num_new_dims + i 

180 requested_size = sizes[output_dim_idx] 

181 input_size = input_shape[i] 

182 

183 if requested_size in (-1, input_size): 

184 # Dimension unchanged — preserve original sharding 

185 output_map.append(in_alias_map[i]) 

186 elif input_size == 1 and requested_size > 1: 

187 # True broadcast: 1 → N — must be replicated 

188 if in_alias_map[i] != "None": 

189 raise ValueError( 

190 f"For {self.op_name}, cannot expand dimension {i} " 

191 f"which is sharded " 

192 f"(size {input_size} → {requested_size}), " 

193 f"got mapping {in_alias_map[i]}." 

194 ) 

195 output_map.append("None") 

196 else: 

197 raise ValueError( 

198 f"For {self.op_name}, cannot expand dimension {i} " 

199 f"from size {input_size} to {requested_size}." 

200 ) 

201 

202 output_layout = Layout( 

203 mesh_shape=input_layout.mesh_shape, 

204 alias_name=input_layout.alias_name, 

205 rank_list=input_layout.rank_list 

206 ) 

207 output_layout = output_layout(*output_map) 

208 return ((output_layout,), None) 

209 

210 

211class ExpandAsDistributedOp(DistributedOp): 

212 """Distributed implementation for torch.Tensor.expand_as.""" 

213 

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

215 """ 

216 Preprocess arguments for ExpandAs operator. 

217 

218 Args: 

219 args (tuple): Input arguments (input_tensor, target_tensor). 

220 kwargs (dict): Keyword arguments (none for expand_as). 

221 

222 Returns: 

223 tuple: (local_args, local_kwargs, cache_values) 

224 """ 

225 args, kwargs = _normalize_expand_as_args(*args, **kwargs) 

226 input_tensor = args[0] 

227 target_tensor = args[1] 

228 local_args = (input_tensor.to_local(), target_tensor.to_local()) 

229 cache_values = [input_tensor.layout, input_tensor.shape, target_tensor.shape] 

230 return local_args, {}, cache_values 

231 

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

233 """ 

234 Infer output layout for expand_as. 

235 

236 Rules: 

237 1. Input must not have Partial status. 

238 2. target_shape must have at least as many dims as input_global_shape. 

239 3. Matching-size dimensions preserve input sharding. 

240 4. Singleton (size 1) dimensions being expanded to >1 must be unsharded in input. 

241 5. Non-singleton dimensions must match exactly. 

242 

243 Args: 

244 cache_values (list): [input_layout, input_global_shape, target_shape] 

245 

246 Returns: 

247 tuple: ((output_layout,), None) 

248 

249 Raises: 

250 ValueError: If any rule above is violated. 

251 """ 

252 if not cache_values: 

253 raise ValueError( 

254 f"For {self.op_name}, cache_values should contain input layout, " 

255 f"but got empty cache_values." 

256 ) 

257 input_layout = cache_values[0] 

258 if not self._allow_partial_inputs: 

259 self._check_partial_inputs([input_layout]) 

260 

261 in_alias_map = input_layout.alias_tensor_map 

262 input_ndim = len(in_alias_map) 

263 

264 input_global_shape = cache_values[1] 

265 target_shape = cache_values[2] 

266 

267 if not isinstance(target_shape, tuple): 

268 raise ValueError( 

269 f"For {self.op_name}, target_shape should be tuple, " 

270 f"but got {type(target_shape)}." 

271 ) 

272 if not isinstance(input_global_shape, tuple): 

273 raise ValueError( 

274 f"For {self.op_name}, input_global_shape should be tuple, " 

275 f"but got {type(input_global_shape)}." 

276 ) 

277 

278 target_ndim = len(target_shape) 

279 

280 if target_ndim < input_ndim: 

281 raise ValueError( 

282 f"For {self.op_name}, target shape {target_shape} (ndim={target_ndim}) " 

283 f"cannot be smaller than input shape {input_global_shape} (ndim={input_ndim})." 

284 ) 

285 

286 # Align dimensions: right-align input to target shape 

287 num_leading_implicit = target_ndim - input_ndim 

288 aligned_input_shape = (1,) * num_leading_implicit + input_global_shape 

289 aligned_tensor_map = ("None",) * num_leading_implicit + in_alias_map 

290 

291 # Validate expansion rules and build output tensor_map 

292 output_tensor_map = [] 

293 for i, (in_size, tgt_size, shard_spec) in enumerate( 

294 zip(aligned_input_shape, target_shape, aligned_tensor_map) 

295 ): 

296 if in_size == tgt_size: 

297 # Dimension unchanged - preserve sharding pattern 

298 output_tensor_map.append(shard_spec) 

299 elif in_size == 1 and tgt_size > 1: 

300 # Dimension is expanded (broadcast) - must be unsharded 

301 if shard_spec != "None": 

302 raise ValueError( 

303 f"For {self.op_name}, cannot expand sharded dimension {i} " 

304 f"which is going to broadcast (global size 1 -> {tgt_size}), " 

305 f"got mapping {shard_spec}." 

306 ) 

307 output_tensor_map.append("None") 

308 else: 

309 raise ValueError( 

310 f"For {self.op_name}, cannot expand dimension {i} from size " 

311 f"{in_size} to {tgt_size}." 

312 ) 

313 

314 output_layout = Layout( 

315 mesh_shape=input_layout.mesh_shape, 

316 alias_name=input_layout.alias_name, 

317 rank_list=input_layout.rank_list 

318 ) 

319 output_layout = output_layout(*output_tensor_map) 

320 return ((output_layout,), None) 

321