Coverage for  / home / jenkins / .local / lib / python3.10 / site-packages / hyper_parallel / auto_parallel / sapp_nd / nd / common / cost_model_preprocess.py: 95%

166 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-08-04 05:18 +0800

1# Copyright 2025-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"""parse config for cost model""" 

16import re 

17import inspect 

18from copy import deepcopy 

19from pprint import pformat 

20from enum import Enum 

21 

22from hyper_parallel.auto_parallel.sapp_nd.nd.common.generate_partitions import PartitionGenerator 

23from hyper_parallel.auto_parallel.sapp_nd.memory_estimation.logger import logger 

24 

25 

26class AttentionType(Enum): 

27 """Attention type enumeration.""" 

28 MHA = "mha" 

29 GQA = "gqa" 

30 MLA = "mla" 

31 

32 

33def detect_attention_type(ccfg: "CostModelConfig") -> AttentionType: 

34 """Detect attention type from cost model config. 

35 

36 Detection rules: 

37 1. If kv_lora_rank > 0: MLA 

38 2. If n_kv < a: GQA 

39 3. Otherwise: MHA 

40 

41 Args: 

42 ccfg: Cost model config. 

43 

44 Returns: 

45 AttentionType enum. 

46 

47 Example: 

48 >>> ccfg.kv_lora_rank = 512 

49 >>> ccfg.a = 64 

50 >>> ccfg.n_kv = 64 

51 >>> detect_attention_type(ccfg) 

52 <AttentionType.MLA: 'mla'> 

53 """ 

54 if ccfg.kv_lora_rank > 0: 

55 return AttentionType.MLA 

56 if ccfg.n_kv < ccfg.a: 

57 return AttentionType.GQA 

58 return AttentionType.MHA 

59 

60 

61def compute_kv_dim(ccfg) -> float: 

62 """Return effective KV dimension per TP rank based on attention type. 

63 

64 When TP is active, KV heads are split across TP ranks, so each 

65 rank holds only 1/t of the total KV dimension. MLA is an 

66 exception: the compressed latent vector is not split by TP, 

67 so kv_lora_rank stays unchanged. 

68 

69 Args: 

70 ccfg: Cost model config with attributes a, n_kv, dh, h, t, kv_lora_rank. 

71 

72 Returns: 

73 Effective KV dimension per TP rank (float). 

74 """ 

75 attention_type = detect_attention_type(ccfg) 

76 t = max(1, ccfg.t) 

77 if attention_type == AttentionType.MLA: 

78 return float(ccfg.kv_lora_rank) 

79 if attention_type == AttentionType.GQA: 

80 n_kv = min(ccfg.n_kv if ccfg.n_kv > 0 else ccfg.a, ccfg.a) 

81 return n_kv * ccfg.dh / t 

82 return ccfg.h / t 

83 

84 

85# class CostModelConfig(Config) : 

86class CostModelConfig(PartitionGenerator): 

87 """cost model variables class""" 

88 

89 def __init__( 

90 self, 

91 input_config=None, 

92 hook_cls=None, 

93 framework=None, 

94 source_code=None, 

95 ): 

96 super().__init__(input_config, hook_cls, framework, source_code) 

97 logger.debug( 

98 "parser = %s for %s", str(self.parser), str(self.model_name) 

99 ) 

100 

101 def __str__(self): 

102 return "CostModelConfig attributes:\n" + pformat( 

103 { 

104 k: v 

105 for k, v in vars(self).items() 

106 if isinstance(v, (int, float, str, bool)) 

107 } 

108 ) 

109 

110 def __getattr__(self, attr): 

111 call_source = inspect.currentframe().f_back.f_code.co_name 

112 if attr not in self.__dict__: 

113 logger.warning( 

114 "[%s] Attribute %s does not exist. " 

115 "Value '0' will be assigned.", 

116 call_source, 

117 attr, 

118 ) 

119 return 0 

120 return self.__dict__[attr] 

121 

122 def __copy__(self): 

123 res = object.__new__(type(self)) 

124 res.__dict__.update(self.__dict__) 

125 return res 

126 

127 def __deepcopy__(self, memo): 

128 res = object.__new__(type(self)) 

129 for k, v in self.__dict__.items(): 

130 setattr(res, k, deepcopy(v, memo)) 

131 return res 

132 

133 def fp_bytes(self, precision): 

134 """Return bytes size for datatype""" 

135 if precision and isinstance(precision, str): 

136 res = re.match(r"[^0-9]*([0-9]+)[^0-9]*", precision) 

137 if res: 

138 return int(res.group(1)) // 8 

139 logger.warning("No bytes detected from FP Precision: %s", precision) 

140 return 0 

141 

142 def print_stages_i(self, stage_id, stage): 

143 """for print_stages""" 

144 stage_layers = [] 

145 for chunk in stage: 

146 chunk_lay_occ = [] 

147 if chunk: 

148 layer, count = chunk[0], 1 

149 for lay_id in range(1, len(chunk)): 

150 if chunk[lay_id] == layer: 

151 count += 1 

152 else: 

153 chunk_lay_occ += [f"{count}{layer.name[0]}"] 

154 layer, count = chunk[lay_id], 1 

155 chunk_lay_occ += [f"{count}{layer.name[0]}"] 

156 stage_layers += [chunk_lay_occ] 

157 logger.info("stage _%s : %s", stage_id, stage_layers) 

158 

159 def print_stages(self, stages, spec_stage_id=-1): 

160 """Call after generate_partitions""" 

161 if spec_stage_id == -1: 

162 for stage_id, stage in enumerate(stages): 

163 self.print_stages_i(stage_id, stage) 

164 elif 0 <= spec_stage_id < len(stages): 

165 self.print_stages_i(spec_stage_id, stages[spec_stage_id]) 

166 else: 

167 logger.warning("Incorrect spec_stage_id") 

168 

169 def count_layers(self, stages): 

170 """Count non-embedding and non-output layers in generated stages.""" 

171 return sum(sum(len(layer) for layer in chunk) for chunk in stages) - 2 

172 

173 def print_parallelism(self): 

174 """strategy pretty printer""" 

175 if not self.multimodal: 

176 logger.info("%s Parallelism used :", self.model_name) 

177 logger.info( 

178 "DP %s, TP %s, PP %s, EP %s, CP %s, VPP %s", 

179 self.d, 

180 self.t, 

181 self.p, 

182 self.ep, 

183 self.cp, 

184 self.vp, 

185 ) 

186 logger.info( 

187 "d_exp %s, t_exp %s, os_max_shard %s, etp %s", 

188 self.d_exp, 

189 self.t_exp, 

190 self.os_max_shard, 

191 self.etp, 

192 ) 

193 logger.info( 

194 "shard_grad_exp %s, shard_grad_non_exp %s", 

195 self.shard_grad_exp, 

196 self.shard_grad_non_exp, 

197 ) 

198 logger.info( 

199 "shard_p_os_exp %s, shard_p_os_non_exp %s", 

200 self.shard_p_os_exp, 

201 self.shard_p_os_non_exp, 

202 ) 

203 logger.info( 

204 "shard_embed %s, shard_output_activ %s, shard_rec_input %s", 

205 self.shard_embed, 

206 self.shard_output_activ, 

207 self.shard_recompute_input, 

208 ) 

209 else: 

210 for m in self.mm_ccfgs: 

211 self.mm_ccfgs[m].print_parallelism() 

212 

213 def strategy_num_devices(self): 

214 """total num devices""" 

215 return self.d * self.t * self.cp * self.p 

216 

217 def is_consistent_pp_config(self): 

218 """check if pp/offset/recomputation consistency""" 

219 

220 def is_valid_cfg(cfg): 

221 if cfg is None or isinstance(cfg, (int, bool)): 

222 return True 

223 if not isinstance(cfg, list) or not cfg: 

224 return False 

225 if isinstance(cfg[0], int): 

226 return len(cfg) == self.p 

227 if isinstance(cfg[0], list): 

228 return len(cfg) == self.vp and all( 

229 isinstance(c, list) and len(c) == self.p for c in cfg 

230 ) 

231 return False 

232 

233 return ( 

234 is_valid_cfg(self.offset) 

235 and is_valid_cfg(self.full_rec) 

236 and is_valid_cfg(self.sel_rec) 

237 ) 

238 

239 @staticmethod 

240 def __maybe_set_int(target, attr, value): 

241 """Set an integer strategy attribute when an override is supplied.""" 

242 if isinstance(value, int): 

243 setattr(target, attr, value) 

244 

245 def __strategy_target(self, model_name): 

246 """Get the config object targeted by a strategy update.""" 

247 if not self.multimodal: 

248 return self 

249 if model_name in self.mm_ccfgs: 

250 return self.mm_ccfgs[model_name] 

251 raise TypeError( 

252 f"{self.model_name}: model_name is required (multimodal)" 

253 ) 

254 

255 def set_strategy(self, **kwargs): 

256 """overwrite parallelism""" 

257 model_name = kwargs.get("model_name", None) 

258 dp = kwargs.get("dp", None) 

259 tp = kwargs.get("mp", None) 

260 cp = kwargs.get("cp", None) 

261 ep = kwargs.get("ep", None) 

262 op = kwargs.get("op", None) 

263 etp = kwargs.get("etp", None) 

264 pp = kwargs.get("pp", None) 

265 vpp = kwargs.get("vpp", None) 

266 off = kwargs.get("offset", None) 

267 fr = kwargs.get("full_rec", None) 

268 sr = kwargs.get("sel_rec", None) 

269 m = kwargs.get("mb", None) 

270 b = kwargs.get("mbs", None) 

271 target_ccfg = self.__strategy_target(model_name) 

272 

273 for attr, value in ( 

274 ("d", dp), 

275 ("t", tp), 

276 ("ep", ep), 

277 ("etp", etp), 

278 ("cp", cp), 

279 ("vp", vpp), 

280 ("p", pp), 

281 ("m", m), 

282 ("b", b), 

283 ): 

284 self.__maybe_set_int(target_ccfg, attr, value) 

285 target_ccfg.sp = target_ccfg.t 

286 if op is not None and isinstance(op, int): 

287 target_ccfg.os_max_shard = op 

288 # Sync has_op with os_max_shard: op<=1 means no optimizer sharding 

289 target_ccfg.has_op = op > 1 

290 target_ccfg.gbs = target_ccfg.b * target_ccfg.d * target_ccfg.m 

291 logger.debug( 

292 "in ccfg: DP = %d, TP = %d, EP = %d, CP = %d, " 

293 "PP = %d, MB = %d, MBS = %d, VPP = %d", 

294 target_ccfg.d, 

295 target_ccfg.t, 

296 target_ccfg.ep, 

297 target_ccfg.cp, 

298 target_ccfg.p, 

299 target_ccfg.m, 

300 target_ccfg.b, 

301 target_ccfg.vp, 

302 ) 

303 if hasattr(target_ccfg.parser, "config_shard_emb"): 

304 target_ccfg.parser.config_shard_emb() 

305 if hasattr(target_ccfg.parser, "config_shard_recompute"): 

306 target_ccfg.parser.config_shard_recompute() 

307 target_ccfg.parser.config_dp_tp_exp(target_ccfg) 

308 target_ccfg.parser.config_optimizer_shard(target_ccfg) 

309 target_ccfg.parser.config_comm_flag(target_ccfg) 

310 if fr is not None: 

311 target_ccfg.full_rec = fr 

312 if sr is not None: 

313 target_ccfg.sel_rec = sr 

314 if isinstance(off, (int, list)): 

315 target_ccfg.offset = off 

316 if not target_ccfg.is_consistent_pp_config(): 

317 raise AttributeError( 

318 f"{target_ccfg.model_name}: " 

319 "Inconsistent pipeline parallel variables " 

320 f"pp {target_ccfg.p} vpp {target_ccfg.vp} " 

321 f"offset {target_ccfg.offset} " 

322 f"full_rec {target_ccfg.full_rec} " 

323 f"sel_rec {target_ccfg.sel_rec}" 

324 ) 

325 self.__maybe_set_int(target_ccfg, "cp", cp) 

326 

327 def get_strategy(self): 

328 """return parallelism/recompute strategies""" 

329 

330 def strategy(mm): 

331 return { 

332 "dp": mm.d, 

333 "tp": mm.t, 

334 "pp": mm.p, 

335 "ep": mm.ep, 

336 "cp": mm.cp, 

337 "vpp": mm.vp, 

338 "op": mm.os_max_shard, 

339 "gbs": mm.b * mm.m * mm.d, 

340 "sched": mm.pp_sched, 

341 "offset": mm.offset, 

342 "full_rec": mm.full_rec, 

343 "sel_rec": mm.sel_rec, 

344 } 

345 

346 # logger.output("get_strat ccfg") 

347 if self.multimodal: 

348 return {mm.model_name: strategy(mm) for mm in self.mm_ccfgs.values()} 

349 return strategy(self) 

350 

351 def layer_custom_config_callback(self, fun): 

352 """ 

353 Use input fun as callback for layer_custom_config 

354 Only for overwriting cost model variables 

355 """ 

356 for idx, f in enumerate(self.layer_custom_config): 

357 

358 def wrap(e, hook=f[1]): 

359 hook(e) 

360 if isinstance(e, CostModelConfig): 

361 fun(self) 

362 else: 

363 e.set_ccfg(fun) 

364 

365 wrap.__name__ = f"{f[1].__name__}_{fun.__name__}" 

366 self.layer_custom_config[idx] = (f[0], wrap)