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

96 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-10-25 05:45 +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 

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

43 """ 

44 Preprocess arguments for Embedding operator. 

45 

46 Args: 

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

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

49 

50 Returns: 

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

52 [input_layout, weight_layout]. 

53 """ 

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

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

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

57 local_kwargs = {} 

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

59 return local_args, local_kwargs, cache_values 

60 

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

62 """ 

63 Infer output layout for Embedding operator. 

64 

65 Rules: 

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

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

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

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

70 and inheriting weight embedding-dimension sharding. 

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

72 that vocab sharding axis. 

73 

74 Args: 

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

76 

77 Returns: 

78 tuple: ((output_layout,), None) 

79 

80 Raises: 

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

82 """ 

83 if len(cache_values) != 2: 

84 raise ValueError( 

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

86 ) 

87 

88 input_layout = cache_values[0] 

89 weight_layout = cache_values[1] 

90 if not input_layout or not weight_layout: 

91 raise ValueError( 

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

93 ) 

94 

95 self._check_partial_inputs([input_layout, weight_layout]) 

96 

97 if input_layout.mesh_shape != weight_layout.mesh_shape: 

98 raise ValueError( 

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

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

101 ) 

102 

103 weight_tensor_map = weight_layout.tensor_map 

104 if len(weight_tensor_map) != 2: 

105 raise ValueError( 

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

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

108 ) 

109 

110 # weight_tensor_map: [vocab_size_dim, embed_dim_dim] 

111 w_shard_embed_axis = weight_tensor_map[1] 

112 weight_alias_map = weight_layout.alias_tensor_map 

113 w_shard_vocab_alias = weight_alias_map[0] 

114 

115 # Output shape is [*input_shape, embed_dim] 

116 output_tensor_map = list(input_layout.tensor_map) 

117 output_tensor_map.append(w_shard_embed_axis) 

118 

119 output_layout = Layout( 

120 mesh_shape=input_layout.mesh_shape, 

121 alias_name=input_layout.alias_name, 

122 rank_list=input_layout.rank_list 

123 ) 

124 output_layout.set_tensor_map(tuple(output_tensor_map)) 

125 

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

127 if w_shard_vocab_alias != "None": 

128 # pylint: disable=protected-access 

129 output_layout._partial = list(input_layout.partial) 

130 output_layout.set_partial_by_dev_axis(w_shard_vocab_alias, 'sum') 

131 # pylint: disable=protected-access 

132 output_layout._alias_tensor_map = output_layout._build_readable_tensor_map() 

133 # pylint: disable=protected-access 

134 output_layout.tensor_map_to_placement() 

135 output_layout.update_compact_str() 

136 

137 return ((output_layout,), None) 

138 

139 def _parse_params(self, args, kwargs): 

140 """ 

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

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

143 """ 

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

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

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

147 return padding_idx, max_norm, scale_grad 

148 

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

150 new_args, kwargs, is_args_pad, padding_idx): 

151 """ 

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

153 """ 

154 mesh = weight_layout.mesh 

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

156 vocab_coord = mesh.get_local_rank(mesh_dim_idx) 

157 

158 vocab_size_per_partition = weight_tensor.shape[0] 

159 vocab_start_index = int(vocab_coord * vocab_size_per_partition) 

160 vocab_end_index = int(vocab_start_index + vocab_size_per_partition) 

161 

162 # Map global padding_idx to local rank range 

163 if padding_idx is not None: 

164 if vocab_start_index <= padding_idx < vocab_end_index: 

165 mapped_padding_idx = int(padding_idx - vocab_start_index) 

166 if is_args_pad: 

167 new_args[2] = mapped_padding_idx 

168 else: 

169 kwargs['padding_idx'] = mapped_padding_idx 

170 else: 

171 if is_args_pad: 

172 new_args[2] = None 

173 else: 

174 kwargs.pop('padding_idx', None) 

175 

176 # Calculate out-of-bounds mask 

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

178 

179 # Cross-platform cast to matching int dtype 

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

181 

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

183 local_input = input_tensor - vocab_start_index 

184 

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

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

187 # while keeping the code perfectly backend-neutral. 

188 local_input = local_input * mask_int 

189 

190 return local_input, mask_int 

191 

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

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

194 """ 

195 Returns the execution implementation wrapper. 

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

197 """ 

198 weight_layout = cache_values[1] 

199 w_shard_vocab_axis = weight_layout.tensor_map[0] 

200 weight_alias_map = weight_layout.alias_tensor_map 

201 w_shard_vocab_alias = weight_alias_map[0] 

202 w_shard_embed_alias = weight_alias_map[1] 

203 

204 # Use native implementation if no weight sharding is applied 

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

206 return None 

207 

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

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

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

211 

212 # 1. Parameter extraction and validation 

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

214 

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

216 if max_norm is not None: 

217 if w_shard_embed_alias != "None": 

218 raise ValueError( 

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

220 ) 

221 if w_shard_vocab_alias != "None": 

222 raise ValueError( 

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

224 ) 

225 

226 # Check for scale_grad_by_freq with RP 

227 if scale_grad and w_shard_vocab_alias != "None": 

228 raise ValueError( 

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

230 ) 

231 

232 # 2. Row Parallel Processing 

233 input_mask_int = None 

234 if w_shard_vocab_alias != "None": 

235 is_args_pad = len(args) > 2 

236 mapped_input, input_mask_int = self._handle_rp_input( 

237 input_tensor, weight_tensor, weight_layout, w_shard_vocab_axis, 

238 new_args, new_kwargs, is_args_pad, padding_idx 

239 ) 

240 new_args[0] = mapped_input 

241 

242 # 3. Native Operator Execution 

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

244 

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

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

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

248 output = output * expanded_mask 

249 

250 return output 

251 

252 return distributed_embedding_impl