Coverage for  / home / jenkins / .local / lib / python3.10 / site-packages / hyper_parallel / auto_parallel / sapp_nd / memory_estimation / validators / ep_constraints.py: 98%

83 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-10-25 05:45 +0800

1# Copyright 2025 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"""EP constraint validators for auto-parallel strategy search. 

16 

17Four constraints that any valid EP strategy must satisfy: 

18 C1: n_experts % ep_degree == 0 (expert divisibility) 

19 C2: hff_exp % t_exp == 0 (expert hidden dim divisibility, t_exp = etp or tp) 

20 C3: dp*tp*pp*cp <= total_devices (device limit, EP borrows from DP) 

21 C4: EP+PP stage expert memory <= device_capacity (stage feasibility) 

22""" 

23from __future__ import annotations 

24from dataclasses import dataclass 

25from inspect import Parameter, Signature 

26from typing import Any 

27 

28 

29@dataclass 

30class ConstraintResult: 

31 """Structured result for a constraint check.""" 

32 

33 name: str 

34 passed: bool 

35 message: str 

36 

37 def __bool__(self): 

38 return self.passed 

39 

40 

41@dataclass(frozen=True) 

42class StageExperts: 

43 """Expert weights hosted by one pipeline stage. 

44 

45 Attributes: 

46 n_moe_layers: Number of MoE layers in this PP stage. 

47 n_exp: Number of experts per MoE layer. 

48 h: Model hidden dimension. 

49 hff_exp: Expert FFN hidden dimension (per expert). 

50 bytes_p: Bytes per parameter. 

51 n_ff_mm: Number of feedforward linear layers per expert 

52 (SwiGLU: 3, standard MLP: 2). 

53 """ 

54 

55 n_moe_layers: int 

56 n_exp: int 

57 h: int 

58 hff_exp: int 

59 bytes_p: int 

60 n_ff_mm: int = 3 

61 

62 

63_LEGACY_STAGE_SIGNATURE = Signature([ 

64 Parameter(name, Parameter.POSITIONAL_OR_KEYWORD) 

65 for name in ("n_moe_layers", "n_exp", "ep", "dp", "h", "hff_exp", "bytes_p", "device_capacity_gb") 

66] + [ 

67 Parameter("zero_level", Parameter.POSITIONAL_OR_KEYWORD, default=2), 

68 Parameter("t_exp", Parameter.POSITIONAL_OR_KEYWORD, default=1), 

69 Parameter("n_ffMM", Parameter.POSITIONAL_OR_KEYWORD, default=3), 

70]) 

71 

72 

73class EpConstraints: 

74 """EP-specific constraint checks for auto-parallel strategy search.""" 

75 

76 @staticmethod 

77 def check_ep_divisibility(n_exp: int, ep: int) -> ConstraintResult: 

78 """C1: n_experts must be divisible by ep_degree.""" 

79 if ep < 1: 

80 return ConstraintResult( 

81 "ep_divisibility", False, 

82 f"ep={ep} must be >= 1") 

83 if n_exp < 1: 

84 return ConstraintResult( 

85 "ep_divisibility", False, 

86 f"n_exp={n_exp} must be >= 1") 

87 if n_exp % ep != 0: 

88 return ConstraintResult( 

89 "ep_divisibility", False, 

90 f"n_exp={n_exp} not divisible by ep={ep}, " 

91 f"remainder={n_exp % ep}") 

92 return ConstraintResult( 

93 "ep_divisibility", True, 

94 f"n_exp={n_exp}/ep={ep} = {n_exp // ep} experts/rank") 

95 

96 @staticmethod 

97 def check_expert_hidden_divisibility( 

98 hff_exp: int, t_exp: int 

99 ) -> ConstraintResult: 

100 """C2: hff_exp must be divisible by expert TP degree. 

101 

102 Args: 

103 hff_exp: Expert FFN hidden dimension. 

104 t_exp: Expert TP degree (= etp if etp > 1, else tp). 

105 etp and tp are alternatives (not multiplicative). 

106 """ 

107 if t_exp < 1: 

108 return ConstraintResult( 

109 "expert_hidden_divisibility", False, 

110 f"t_exp={t_exp} must be >= 1") 

111 if hff_exp <= 0: 

112 return ConstraintResult( 

113 "expert_hidden_divisibility", True, 

114 f"hff_exp={hff_exp} (dense FFN, no expert TP needed)") 

115 if hff_exp % t_exp != 0: 

116 return ConstraintResult( 

117 "expert_hidden_divisibility", False, 

118 f"hff_exp={hff_exp} not divisible by t_exp={t_exp}, " 

119 f"remainder={hff_exp % t_exp}") 

120 return ConstraintResult( 

121 "expert_hidden_divisibility", True, 

122 f"hff_exp={hff_exp}/t_exp={t_exp} = {hff_exp // t_exp}") 

123 

124 @staticmethod 

125 def check_device_limit( 

126 dp: int, tp: int, pp: int, cp: int, total_devices: int 

127 ) -> ConstraintResult: 

128 """C3: dp*tp*pp*cp must not exceed total_devices. 

129 

130 EP borrows devices from DP and does not occupy independent 

131 device slots, so it is not included in the device count. 

132 """ 

133 required = dp * tp * pp * cp 

134 if required > total_devices: 

135 return ConstraintResult( 

136 "device_limit", False, 

137 f"dp={dp}*tp={tp}*pp={pp}*cp={cp}=" 

138 f"{required} > total_devices={total_devices}") 

139 return ConstraintResult( 

140 "device_limit", True, 

141 f"{required}/{total_devices} devices used") 

142 

143 @staticmethod 

144 def check_ep_pp_stage_feasibility(*args: Any, **kwargs: Any) -> ConstraintResult: 

145 """Check stage memory using StageExperts or the legacy numeric arguments. 

146 

147 Args: 

148 *args: A StageExperts object followed by ep, dp and capacity, or the 

149 legacy order n_moe_layers, n_exp, ep, dp, h, hff_exp, bytes_p, 

150 device_capacity_gb, zero_level, t_exp, n_ffMM. 

151 **kwargs: Named fields in the selected form. Legacy n_ffMM remains 

152 accepted and maps to StageExperts.n_ff_mm. 

153 

154 Returns: 

155 Whether the stage expert memory fits the device capacity. 

156 """ 

157 if (args and isinstance(args[0], StageExperts)) or "experts" in kwargs: 

158 return EpConstraints._check_stage_experts(*args, **kwargs) 

159 bound = _LEGACY_STAGE_SIGNATURE.bind(*args, **kwargs) 

160 bound.apply_defaults() 

161 values = bound.arguments 

162 experts = StageExperts( 

163 **{key: values.pop(key) for key in ("n_moe_layers", "n_exp", "h", "hff_exp", "bytes_p")}, 

164 n_ff_mm=values.pop("n_ffMM"), 

165 ) 

166 return EpConstraints._check_stage_experts(experts, **values) 

167 

168 @staticmethod 

169 def _check_stage_experts( 

170 experts: StageExperts, 

171 ep: int, 

172 dp: int, 

173 device_capacity_gb: float, 

174 zero_level: int = 2, 

175 t_exp: int = 1, 

176 ) -> ConstraintResult: 

177 """C4: EP+PP stage expert memory must fit within device capacity. 

178 

179 Args: 

180 experts: Expert weights hosted by this PP stage. 

181 ep: Expert parallelism degree. 

182 dp: Data parallelism degree (for ZeRO sharding). 

183 device_capacity_gb: Device memory capacity in GB. 

184 zero_level: ZeRO optimizer sharding level (2 or 3). 

185 t_exp: Expert TP degree (= etp if etp > 1, else tp). 

186 """ 

187 if experts.n_moe_layers <= 0: 

188 return ConstraintResult( 

189 "ep_pp_stage_feasibility", True, 

190 "Dense-only stage, no expert memory") 

191 if ep < 1: 

192 return ConstraintResult( 

193 "ep_pp_stage_feasibility", False, 

194 f"ep={ep} must be >= 1") 

195 experts_per_rank = experts.n_exp / ep 

196 params_per_expert = experts.n_ff_mm * experts.h * experts.hff_exp / max(t_exp, 1) 

197 param_bytes = ( 

198 experts.n_moe_layers * experts_per_rank * params_per_expert * experts.bytes_p 

199 ) 

200 os_bytes = param_bytes * 2 

201 grad_bytes = param_bytes 

202 if zero_level >= 3: 

203 total = (param_bytes + os_bytes + grad_bytes) / max(dp, 1) 

204 elif zero_level == 2: 

205 total = param_bytes + os_bytes / max(dp, 1) + grad_bytes / max(dp, 1) 

206 else: 

207 total = param_bytes + os_bytes + grad_bytes 

208 total_gb = total / 1e9 

209 if total_gb > device_capacity_gb: 

210 return ConstraintResult( 

211 "ep_pp_stage_feasibility", False, 

212 f"Stage expert mem={total_gb:.1f}GB > capacity=" 

213 f"{device_capacity_gb:.1f}GB " 

214 f"(exp/rank={experts_per_rank:.0f}, ZeRO-{zero_level})") 

215 return ConstraintResult( 

216 "ep_pp_stage_feasibility", True, 

217 f"Stage expert mem={total_gb:.1f}GB <= capacity=" 

218 f"{device_capacity_gb:.1f}GB") 

219 

220 @classmethod 

221 def validate_all(cls, ccfg, total_devices: int, 

222 device_capacity_gb: float) -> list: 

223 """Run all EP constraint checks, return list of ConstraintResult.""" 

224 t_exp = max(ccfg.etp, 1) if ccfg.etp > 1 else max(ccfg.t, 1) 

225 results = [ 

226 cls.check_ep_divisibility(ccfg.n_exp, ccfg.ep), 

227 cls.check_expert_hidden_divisibility(ccfg.hff_exp, t_exp), 

228 cls.check_device_limit( 

229 ccfg.d, ccfg.t, ccfg.p, ccfg.cp, total_devices), 

230 ] 

231 if ccfg.p > 1 and ccfg.n_exp > 1: 

232 n_moe_per_stage = getattr(ccfg, 'n_lay', 1) // ccfg.p 

233 experts = StageExperts( 

234 n_moe_layers=n_moe_per_stage, 

235 n_exp=ccfg.n_exp, 

236 h=ccfg.h, 

237 hff_exp=ccfg.hff_exp, 

238 bytes_p=ccfg.bytes_p, 

239 n_ff_mm=max(getattr(ccfg, 'n_ffMM', 3), 1), 

240 ) 

241 results.append(cls.check_ep_pp_stage_feasibility( 

242 experts, 

243 ep=ccfg.ep, dp=ccfg.d, 

244 device_capacity_gb=device_capacity_gb, 

245 zero_level=int(ccfg.comm_d_exp), t_exp=t_exp, 

246 )) 

247 return results