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

99 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"""Distributed implementation for the embedding operator.""" 

16 

17from typing import Callable, Optional, Tuple 

18 

19from hyper_parallel.core.dtensor.layout import Layout 

20from .parallel_ops import DistributedOp 

21 

22 

23def _normalize_embedding_args(input_tensor, weight_tensor, padding_idx=None, max_norm=None, 

24 norm_type=2.0, scale_grad_by_freq=False, sparse=False): 

25 return ( 

26 input_tensor, 

27 weight_tensor, 

28 padding_idx, 

29 max_norm, 

30 norm_type, 

31 scale_grad_by_freq, 

32 sparse, 

33 ), {} 

34 

35 

36class EmbeddingDistributedOp(DistributedOp): 

37 """ 

38 Distributed implementation for embedding operators. 

39 Supports Column Parallelism (CP) and Row Parallelism (RP). 

40 """ 

41 _MS_PRIMITIVE_OP_NAMES = frozenset({'Embedding'}) 

42 

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

44 """ 

45 Preprocess arguments for Embedding operator. 

46 

47 Args: 

48 args (tuple): Input arguments containing input and weight tensors. 

49 kwargs (dict): Keyword arguments for embedding options. 

50 

51 Returns: 

52 tuple: (local_args, local_kwargs, cache_values) where cache_values is 

53 [input_layout, weight_layout]. 

54 """ 

55 args, _ = _normalize_embedding_args(*args, **kwargs) 

56 input_tensor, weight_tensor = args[0], args[1] 

57 if self.op_name in self._MS_PRIMITIVE_OP_NAMES: 

58 local_args = (input_tensor.to_local(), weight_tensor.to_local()) + args[2:6] 

59 else: 

60 local_args = (input_tensor.to_local(), weight_tensor.to_local()) + args[2:] 

61 local_kwargs = {} 

62 cache_values = [input_tensor.layout, weight_tensor.layout] 

63 return local_args, local_kwargs, cache_values 

64 

65 def infer_layout(self, cache_values: list) -> Tuple[tuple, None]: # pylint: disable=W0221 

66 """ 

67 Infer output layout for Embedding operator. 

68 

69 Rules: 

70 1. Input and weight must not have Partial status. 

71 2. Input and weight must share the same mesh_shape. 

72 3. Weight must be 2D [vocab_size, embedding_dim]. 

73 4. Output shape is [*input_shape, embedding_dim], preserving input sharding 

74 and inheriting weight embedding-dimension sharding. 

75 5. If weight vocab dimension is sharded, output carries Partial('sum') on 

76 that vocab sharding axis. 

77 

78 Args: 

79 cache_values (list): [input_layout, weight_layout]. 

80 

81 Returns: 

82 tuple: ((output_layout,), None) 

83 

84 Raises: 

85 ValueError: If layouts are missing, partial, incompatible, or weight is not 2D. 

86 """ 

87 if len(cache_values) != 2: 

88 raise ValueError( 

89 f"For {self.op_name}, cache_values length should be 2, but got {len(cache_values)}" 

90 ) 

91 

92 input_layout = cache_values[0] 

93 weight_layout = cache_values[1] 

94 if not input_layout or not weight_layout: 

95 raise ValueError( 

96 f"For {self.op_name}, requires both input and weight layouts." 

97 ) 

98 

99 self._check_partial_inputs([input_layout, weight_layout]) 

100 

101 if input_layout.mesh_shape != weight_layout.mesh_shape: 

102 raise ValueError( 

103 f"For {self.op_name}, input and weight must have the same mesh_shape, " 

104 f"but got input: {input_layout.mesh_shape} and weight: {weight_layout.mesh_shape}" 

105 ) 

106 

107 weight_tensor_map = weight_layout.tensor_map 

108 if len(weight_tensor_map) != 2: 

109 raise ValueError( 

110 f"For {self.op_name}, weight should be 2D [vocab_size, embedding_dim], " 

111 f"but got {len(weight_tensor_map)}D" 

112 ) 

113 

114 # weight_tensor_map: [vocab_size_dim, embed_dim_dim] 

115 w_shard_embed_axis = weight_tensor_map[1] 

116 weight_alias_map = weight_layout.alias_tensor_map 

117 w_shard_vocab_alias = weight_alias_map[0] 

118 

119 # Output shape is [*input_shape, embed_dim] 

120 output_tensor_map = list(input_layout.tensor_map) 

121 output_tensor_map.append(w_shard_embed_axis) 

122 

123 output_layout = Layout( 

124 mesh_shape=input_layout.mesh_shape, 

125 alias_name=input_layout.alias_name, 

126 rank_list=input_layout.rank_list 

127 ) 

128 output_layout.set_tensor_map(tuple(output_tensor_map)) 

129 

130 # If vocab is sharded (Row Parallelism), output is in Partial Sum state 

131 if w_shard_vocab_alias != "None": 

132 # pylint: disable=protected-access 

133 output_layout._partial = list(input_layout.partial) 

134 output_layout.set_partial_by_dev_axis(w_shard_vocab_alias, 'sum') 

135 # pylint: disable=protected-access 

136 output_layout._alias_tensor_map = output_layout._build_readable_tensor_map() 

137 # pylint: disable=protected-access 

138 output_layout.tensor_map_to_placement() 

139 output_layout.update_compact_str() 

140 

141 return ((output_layout,), None) 

142 

143 def _parse_params(self, args, kwargs): 

144 """ 

145 Extracts padding_idx, max_norm, and scale_grad from args or kwargs. 

146 F.embedding signature: (input, weight, padding_idx, max_norm, norm_type, scale_grad_by_freq...) 

147 """ 

148 padding_idx = args[2] if len(args) > 2 else kwargs.get('padding_idx', None) 

149 max_norm = args[3] if len(args) > 3 else kwargs.get('max_norm', None) 

150 scale_grad = args[5] if len(args) > 5 else kwargs.get('scale_grad_by_freq', False) 

151 return padding_idx, max_norm, scale_grad 

152 

153 def _handle_rp_input(self, input_tensor, weight_tensor, weight_layout, w_shard_vocab_axis, 

154 new_args, kwargs, is_args_pad, padding_idx): 

155 """ 

156 Processes Row Parallelism input: shifts indices, handles padding, and generates masks. 

157 """ 

158 mesh = weight_layout.mesh 

159 mesh_dim_idx = len(mesh.mesh_shape) - 1 - w_shard_vocab_axis 

160 vocab_coord = mesh.get_local_rank(mesh_dim_idx) 

161 

162 vocab_size_per_partition = weight_tensor.shape[0] 

163 vocab_start_index = int(vocab_coord * vocab_size_per_partition) 

164 vocab_end_index = int(vocab_start_index + vocab_size_per_partition) 

165 

166 # Map global padding_idx to local rank range 

167 if padding_idx is not None: 

168 if vocab_start_index <= padding_idx < vocab_end_index: 

169 mapped_padding_idx = int(padding_idx - vocab_start_index) 

170 if is_args_pad: 

171 new_args[2] = mapped_padding_idx 

172 else: 

173 kwargs['padding_idx'] = mapped_padding_idx 

174 else: 

175 if is_args_pad: 

176 new_args[2] = None 

177 else: 

178 kwargs.pop('padding_idx', None) 

179 

180 # Calculate out-of-bounds mask 

181 mask = (input_tensor >= vocab_start_index) & (input_tensor < vocab_end_index) 

182 

183 # Cross-platform cast to matching int dtype 

184 mask_int = mask.to(input_tensor.dtype) if hasattr(mask, "to") else mask.astype(input_tensor.dtype) 

185 

186 # Shift global indices to local range using native scalar broadcast 

187 local_input = input_tensor - vocab_start_index 

188 

189 # Zero out invalid indices mathematically instead of using .where() or clamp(). 

190 # This prevents NPU out-of-bounds memory access during the embedding lookup 

191 # while keeping the code perfectly backend-neutral. 

192 local_input = local_input * mask_int 

193 

194 return local_input, mask_int 

195 

196 def get_expand_impl(self, func: Optional[Callable], infer_result: tuple, # pylint: disable=W0221 

197 cache_values: list) -> Optional[Callable]: 

198 """ 

199 Returns the execution implementation wrapper. 

200 Helper functions are used to keep Cyclomatic Complexity (CCN) low. 

201 """ 

202 weight_layout = cache_values[1] 

203 w_shard_vocab_axis = weight_layout.tensor_map[0] 

204 weight_alias_map = weight_layout.alias_tensor_map 

205 w_shard_vocab_alias = weight_alias_map[0] 

206 w_shard_embed_alias = weight_alias_map[1] 

207 

208 # Use native implementation if no weight sharding is applied 

209 if w_shard_vocab_alias == "None" and w_shard_embed_alias == "None": 

210 return None 

211 

212 def distributed_embedding_impl(*args, **kwargs): 

213 input_tensor, weight_tensor = args[0], args[1] 

214 new_args, new_kwargs = list(args), kwargs.copy() 

215 

216 # 1. Parameter extraction and validation 

217 padding_idx, max_norm, scale_grad = self._parse_params(args, kwargs) 

218 

219 # Check for max_norm with specific error messages for CP and RP 

220 if max_norm is not None: 

221 if w_shard_embed_alias != "None": 

222 raise ValueError( 

223 f"For {self.op_name}, Column-Parallel Embedding does not support `max_norm` parameter." 

224 ) 

225 if w_shard_vocab_alias != "None": 

226 raise ValueError( 

227 f"For {self.op_name}, Row-Parallel Embedding does not support `max_norm` parameter." 

228 ) 

229 

230 # Check for scale_grad_by_freq with RP 

231 if scale_grad and w_shard_vocab_alias != "None": 

232 raise ValueError( 

233 f"For {self.op_name}, Row-Parallel Embedding does not support `scale_grad_by_freq=True`." 

234 ) 

235 

236 # 2. Row Parallel Processing 

237 input_mask_int = None 

238 if w_shard_vocab_alias != "None": 

239 is_args_pad = len(args) > 2 

240 mapped_input, input_mask_int = self._handle_rp_input( 

241 input_tensor, weight_tensor, weight_layout, w_shard_vocab_axis, 

242 new_args, new_kwargs, is_args_pad, padding_idx 

243 ) 

244 new_args[0] = mapped_input 

245 

246 # 3. Native Operator Execution 

247 output = func(*new_args, **new_kwargs) 

248 

249 # 4. Erase invalid partial embeddings (Row-Parallel only) 

250 if w_shard_vocab_alias != "None" and input_mask_int is not None: 

251 expanded_mask = input_mask_int[..., None].to(output.dtype) 

252 output = output * expanded_mask 

253 

254 return output 

255 

256 return distributed_embedding_impl