Coverage for  / home / jenkins / .local / lib / python3.10 / site-packages / hyper_parallel / auto_parallel / sapp_nd / memory_estimation / evaluators / body.py: 99%

156 statements  

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

16from __future__ import annotations 

17from typing import TYPE_CHECKING, NamedTuple 

18from hyper_parallel.auto_parallel._model_spec import KindActivations 

19from hyper_parallel.auto_parallel._op_records import load_op_records 

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

21from hyper_parallel.auto_parallel.sapp_nd.memory_estimation.evaluators.utils import EvalUtils 

22from hyper_parallel.auto_parallel.sapp_nd.memory_estimation.evaluators.comm import EvalLayerComm 

23from hyper_parallel.auto_parallel.sapp_nd.memory_estimation.evaluators.layer_block import EvalAttn 

24from hyper_parallel.auto_parallel.sapp_nd.nd.common.cp_types import ( 

25 CPMemoryBreakdown, 

26 CPAlgo, 

27 _resolve_cp_algo, 

28) 

29from hyper_parallel.auto_parallel.sapp_nd.nd.common.cost_model_preprocess import ( 

30 detect_attention_type, 

31 compute_kv_dim, 

32) 

33from hyper_parallel.auto_parallel.sapp_nd.nd.common.derive import runs_hyper_selective 

34from hyper_parallel.auto_parallel.sapp_nd.nd.common.layer_type import LayerType 

35 

36if TYPE_CHECKING: 

37 from hyper_parallel.auto_parallel.sapp_nd.nd.common.cost_model_preprocess import CostModelConfig 

38 from hyper_parallel.auto_parallel.sapp_nd.memory_estimation._context import Context 

39 from typing import Optional, Tuple 

40 

41# Bytes per element of the attention tensors that CP shards. 

42_CP_ATTENTION_SCORES_BYTES = 4 

43_CP_SOFTMAX_OUTPUTS_BYTES = 4 

44_CP_DROPOUT_MASK_BYTES = 1 

45_CP_KV_BYTES = 2 * 2 # fp16 key and value 

46 

47 

48class _CPAttentionTerms(NamedTuple): 

49 """Attention memory of one layer under CP, and what CP saves.""" 

50 

51 kv_cache_memory: float 

52 attention_scores_memory: float 

53 softmax_outputs_memory: float 

54 dropout_mask_memory: float 

55 s2_reduction: float 

56 kv_reduction: float 

57 

58 

59def _ulysses_cp_attention_terms( 

60 s: float, b: float, cp: float, kv_dim: float, a_per_rank: float 

61) -> _CPAttentionTerms: 

62 """Ulysses CP: each rank holds all s tokens but a/(t*cp) heads.""" 

63 a_per_cp_rank = a_per_rank / cp 

64 kv_dim_per_cp_rank = kv_dim / cp 

65 s2_items_no_cp = ( 

66 (_CP_ATTENTION_SCORES_BYTES + _CP_SOFTMAX_OUTPUTS_BYTES + _CP_DROPOUT_MASK_BYTES) 

67 * s * s * b * a_per_rank 

68 ) 

69 s2_items_with_cp = ( 

70 (_CP_ATTENTION_SCORES_BYTES + _CP_SOFTMAX_OUTPUTS_BYTES + _CP_DROPOUT_MASK_BYTES) 

71 * s * s * b * a_per_cp_rank 

72 ) 

73 return _CPAttentionTerms( 

74 kv_cache_memory=_CP_KV_BYTES * s * b * kv_dim_per_cp_rank, 

75 attention_scores_memory=_CP_ATTENTION_SCORES_BYTES * s * s * b * a_per_cp_rank, 

76 softmax_outputs_memory=_CP_SOFTMAX_OUTPUTS_BYTES * s * s * b * a_per_cp_rank, 

77 dropout_mask_memory=_CP_DROPOUT_MASK_BYTES * s * s * b * a_per_cp_rank, 

78 s2_reduction=s2_items_no_cp - s2_items_with_cp, 

79 kv_reduction=_CP_KV_BYTES * s * b * kv_dim * ((cp - 1) / cp), 

80 ) 

81 

82 

83def _ring_cp_attention_terms( 

84 s: float, b: float, cp: float, kv_dim: float, a_per_rank: float 

85) -> _CPAttentionTerms: 

86 """Ring CP: each rank holds s/cp tokens and a/t heads, KV is all-gathered.""" 

87 return _CPAttentionTerms( 

88 kv_cache_memory=_CP_KV_BYTES * (s / cp) * b * kv_dim, 

89 attention_scores_memory=_CP_ATTENTION_SCORES_BYTES * (s / cp) * s * b * a_per_rank, 

90 softmax_outputs_memory=_CP_SOFTMAX_OUTPUTS_BYTES * (s / cp) * s * b * a_per_rank, 

91 dropout_mask_memory=_CP_DROPOUT_MASK_BYTES * (s / cp) * s * b * a_per_rank, 

92 s2_reduction=( 

93 (_CP_ATTENTION_SCORES_BYTES + _CP_SOFTMAX_OUTPUTS_BYTES + _CP_DROPOUT_MASK_BYTES) 

94 * s * s * b * a_per_rank * ((cp - 1) / cp) 

95 ), 

96 kv_reduction=_CP_KV_BYTES * s * b * kv_dim * ((cp - 1) / cp), 

97 ) 

98 

99 

100class EvalBody: 

101 """Body layer formulas class""" 

102 

103 @staticmethod 

104 def num_params_layer( 

105 ccfg: CostModelConfig, ctx: Context 

106 ) -> Tuple[float, float, float]: 

107 """Parameters count. 

108 

109 Returns a 3-tuple (non_exp, routed, shared): 

110 - non_exp: attention + norm params (and dense FFN if n_exp==1, 

111 the router otherwise) 

112 - routed: routed expert params (0 if n_exp==1) 

113 - shared: shared expert params (0 if n_shared_exp==0 or no pointer) 

114 """ 

115 non_exp = ctx.attn_num_p(ccfg, ctx) + ctx.norm_num_p(ccfg, ctx) 

116 routed = 0.0 

117 shared = 0.0 

118 if ccfg.n_exp == 1: 

119 non_exp += ctx.ffn_num_p(ccfg, ctx) 

120 else: 

121 if ctx.ffn_router_num_p is not None: 

122 non_exp += ctx.ffn_router_num_p(ccfg, ctx) 

123 if ctx.ffn_routed_num_p is not None: 

124 routed = ctx.ffn_routed_num_p(ccfg, ctx) 

125 if ctx.ffn_shared_num_p is not None: 

126 shared = ctx.ffn_shared_num_p(ccfg, ctx) 

127 return (non_exp, routed, shared) 

128 

129 @staticmethod 

130 def stat_p_layer(ccfg: CostModelConfig, ctx: Context) -> float: 

131 """model param""" 

132 non_exp_p, routed_p, shared_p = ctx.eval.num_p(ccfg, ctx) 

133 # Routed experts: EP sharding 

134 routed_mem = routed_p / ccfg.ep * ccfg.bytes_p / ccfg.shard_p_os_exp 

135 # Shared experts: partial DP sharding 

136 shared_mem = shared_p * ccfg.bytes_p / ccfg.shard_p_os_exp_partial 

137 # Non expert 

138 non_exp_mem = non_exp_p * ccfg.bytes_p / ccfg.shard_p_os_non_exp_partial 

139 return non_exp_mem + routed_mem + shared_mem 

140 

141 @staticmethod 

142 def stat_os_layer(ccfg: CostModelConfig, ctx: Context) -> float: 

143 """optim state""" 

144 if ctx.swap_os: 

145 return 0 

146 non_exp_p, routed_p, shared_p = ctx.eval.num_p(ccfg, ctx) 

147 # Routed experts 

148 routed_mem = routed_p / ccfg.ep * ccfg.bytes_optim / ccfg.shard_p_os_exp 

149 # Shared experts 

150 shared_mem = shared_p * ccfg.bytes_optim / ccfg.shard_p_os_exp_partial 

151 # Non expert 

152 non_exp_mem = non_exp_p * ccfg.bytes_optim / ccfg.shard_p_os_non_exp_partial 

153 return non_exp_mem + routed_mem + shared_mem 

154 

155 @staticmethod 

156 def stat_grad_layer(ccfg: CostModelConfig, ctx: Context) -> float: 

157 """gradients""" 

158 non_exp_p, routed_p, shared_p = ctx.eval.num_p(ccfg, ctx) 

159 # Routed experts 

160 routed_mem = routed_p / ccfg.ep * ccfg.bytes_grad / ccfg.shard_grad_exp 

161 # Shared experts: use shard_grad_exp_partial (independent of os sharding) 

162 shared_mem = shared_p * ccfg.bytes_grad / ccfg.shard_grad_exp_partial 

163 # Non expert 

164 non_exp_mem = non_exp_p * ccfg.bytes_grad / ccfg.shard_grad_non_exp 

165 return non_exp_mem + routed_mem + shared_mem 

166 

167 @staticmethod 

168 def reduced_grad_layer(ccfg: CostModelConfig, ctx: Context) -> Tuple[float, float]: 

169 """The layer's gradients FSDP reduce-scatters, whole as its backward computes them and sharded.""" 

170 non_exp_p, routed_p, shared_p = ctx.eval.num_p(ccfg, ctx) 

171 return EvalUtils.reduced_grads(ccfg, ( 

172 (non_exp_p, ccfg.t, ccfg.shard_grad_non_exp), 

173 (routed_p / ccfg.ep, ccfg.t_exp, ccfg.shard_grad_exp), 

174 (shared_p, ccfg.t_exp, ccfg.shard_grad_exp_partial), 

175 )) 

176 

177 # No recompute and select recompute 

178 

179 @staticmethod 

180 def layer_activ(ccfg: CostModelConfig, ctx: Context) -> float: 

181 """activations""" 

182 census = getattr(ccfg, "kind_activations", None) 

183 if isinstance(census, KindActivations) and ( 

184 ctx.current_node != LayerType.SEL_REC_LAYER or census.ops is not None 

185 or census.selective is not None and runs_hyper_selective(ccfg, EvalUtils.switches(ccfg, ctx))): 

186 return EvalBody.census_activ(ccfg, ctx, census) 

187 attn_size = sum( 

188 [ 

189 ctx.attn_qkv_activ(ccfg, ctx), 

190 ctx.attn_score_activ(ccfg, ctx), 

191 ctx.attn_proj_activ(ccfg, ctx), 

192 ] 

193 ) 

194 if ccfg.n_exp == 1: 

195 ffn_size = ctx.ffn_activ(ccfg, ctx) 

196 else: 

197 ffn_size = ctx.ffn_moe_activ(ccfg, ctx) 

198 norm_size = ctx.norm_activ(ccfg, ctx) 

199 return attn_size + ffn_size + norm_size 

200 

201 @staticmethod 

202 def census_activ(ccfg: CostModelConfig, ctx: Context, census: KindActivations) -> float: 

203 """A layer's activations, as the census of its kind measured them. 

204 

205 What the layer keeps between its passes, or its backward's working 

206 set (:meth:`EvalUtils.census_bytes`), per token of a CP rank's share 

207 of the sequence: TP splits one part, sequence parallelism the 

208 other, and CP that gathers keys and values leaves them whole 

209 (:meth:`EvalAttn.kv_shards`). A selective layer keeps what 

210 :meth:`census_kept` says, and its backward holds what the plain 

211 layer's does. 

212 """ 

213 tokens = ctx.micro_factor * ccfg.s * ccfg.b / max(1, ccfg.cp) 

214 held = census.working / max(1, ccfg.sp) + census.working_tp / max(1, ccfg.t) 

215 # A census counts a rank's share of the keys and values; where CP 

216 # gathers them the attention keeps the rest of the sequence's too. 

217 gathered = EvalAttn.gathered_kv_bytes(ccfg) 

218 if ctx.current_node == LayerType.SEL_REC_LAYER: 

219 return EvalUtils.census_bytes(ctx, tokens, EvalBody.census_kept(ccfg, ctx, census), held + gathered) 

220 kept = census.saved / max(1, ccfg.sp) + census.saved_tp / max(1, ccfg.t) 

221 return EvalUtils.census_bytes(ctx, tokens, kept + gathered, held + gathered) 

222 

223 @staticmethod 

224 def census_kept(ccfg: CostModelConfig, ctx: Context, census: KindActivations) -> float: 

225 """What a selective layer of a kind *census* prices keeps, bytes per token of a CP rank's share. 

226 

227 Under HyperParallel's selective checkpointing, whose switches it has 

228 (:func:`runs_hyper_selective`), what the census measured under it; 

229 the keys and values CP gathers it gathers again to recompute. Under 

230 other switches, what the census's records per op state for each op 

231 they keep (:func:`load_op_records`): an op no switch names, and the 

232 census's ``other``, whatever they say; the keys and values CP 

233 gathers where the attention's batched matmuls are kept. Split as 

234 what the plain layer keeps (:meth:`census_activ`). 

235 """ 

236 if census.selective is not None and runs_hyper_selective(ccfg, EvalUtils.switches(ccfg, ctx)): 

237 return census.selective / max(1, ccfg.sp) + census.selective_tp / max(1, ccfg.t) 

238 records = load_op_records().ops 

239 

240 def keeps(op: str) -> bool: 

241 """Whether the layer keeps what the census states for *op*.""" 

242 switch = records[op].switch if op in records else None 

243 return switch is None or bool(EvalUtils.switch(ccfg, ctx, switch)) 

244 

245 kept = sum(size for op, size in census.ops.items() if keeps(op)) / max(1, ccfg.sp) 

246 kept += sum(size for op, size in census.ops_tp.items() if keeps(op)) / max(1, ccfg.t) 

247 return kept + (EvalAttn.gathered_kv_bytes(ccfg) if keeps("attBMM") else 0) 

248 

249 @staticmethod 

250 def census_working_terms(ccfg: CostModelConfig, ctx: Context) -> Optional[Tuple[float, float]]: 

251 """What a selective layer priced by its census's records per op keeps, and what its backward holds. 

252 

253 In bytes, at one micro-batch: the two terms of its working set as 

254 warm-up ends (:meth:`EvalUtils.census_bytes`), which is not the sum 

255 of its ops' where the layer keeps more than its backward holds. 

256 ``None`` for a layer its census does not price so: plain, fully 

257 recomputed, under HyperParallel's policy, or without a census's 

258 records per op. 

259 """ 

260 census = getattr(ccfg, "kind_activations", None) 

261 if (not isinstance(census, KindActivations) or census.ops is None 

262 or ctx.current_node != LayerType.SEL_REC_LAYER 

263 or census.selective is not None and runs_hyper_selective(ccfg, EvalUtils.switches(ccfg, ctx))): 

264 return None 

265 tokens = ccfg.s * ccfg.b / max(1, ccfg.cp) 

266 held = census.working / max(1, ccfg.sp) + census.working_tp / max(1, ccfg.t) 

267 return tokens * EvalBody.census_kept(ccfg, ctx, census), tokens * (held + EvalAttn.gathered_kv_bytes(ccfg)) 

268 

269 # Full recompute 

270 

271 @staticmethod 

272 def fullrec_layer_activ(ccfg: CostModelConfig, ctx: Context) -> float: 

273 """activations""" 

274 micro_factor = ctx.micro_factor 

275 forward_activation = ( 

276 micro_factor * ccfg.bytes_compute * ccfg.s * ccfg.b * ccfg.h 

277 ) 

278 forward_activation /= ccfg.shard_recompute_input 

279 return forward_activation 

280 

281 @staticmethod 

282 def fullrec_layer_activ_gradclip( 

283 ccfg: CostModelConfig, ctx: Context 

284 ) -> float: 

285 """special case with gradient clipping""" 

286 non_exp_p, routed_p, shared_p = ctx.eval.num_p(ccfg, ctx) 

287 grad_clip_mem = ( 

288 non_exp_p 

289 + routed_p / ccfg.ep * ccfg.bytes_os / ccfg.shard_p_os_exp 

290 + shared_p * ccfg.bytes_os / ccfg.shard_p_os_exp_partial 

291 ) 

292 grad_clip_mem *= ccfg.bytes_os / ccfg.shard_p_os_non_exp_partial 

293 grad_clip_mem *= int(ccfg.has_clip) 

294 forward_activation = EvalBody.fullrec_layer_activ(ccfg, ctx) 

295 dp_comm_size = ctx.eval.dyn.comm.dp(ccfg, ctx) 

296 if forward_activation + dp_comm_size > grad_clip_mem: 

297 return forward_activation 

298 logger.debug( 

299 "gradient clipping %s > %s", 

300 EvalUtils.mb(grad_clip_mem), 

301 EvalUtils.mb(forward_activation + dp_comm_size), 

302 ) 

303 return grad_clip_mem 

304 

305 @staticmethod 

306 def fullrec_layer_comm_gradclip( 

307 ccfg: CostModelConfig, ctx: Context 

308 ) -> float: 

309 """special case with gradient clipping""" 

310 if EvalBody.fullrec_layer_activ_gradclip(ccfg, ctx) > 0: 

311 return ctx.eval.dyn.comm.dp(ccfg, ctx) 

312 return 0 

313 

314 @staticmethod 

315 def act_cp_layer( 

316 ccfg: CostModelConfig, 

317 ctx: Context 

318 ) -> CPMemoryBreakdown: 

319 """Estimate CP activation memory impact for one transformer layer. 

320 

321 Ring CP (colossalai_cp / hybrid_cp): 

322 Each rank holds s/cp tokens (Q sharded along seq) and a/t heads; 

323 KV is all-gathered, so only one S dim of the S² score tensor 

324 is divided. 

325 KV cache: (s/cp) × b × kv_dim_per_rank 

326 Attn scores: (s/cp) × s × b × (a/t) 

327 

328 Ulysses CP: 

329 Each rank holds all s tokens but a/(t*cp) heads. 

330 KV cache: s × b × kv_dim_per_rank / cp 

331 Attn scores: s × s × b × (a/(t*cp)) 

332 """ 

333 s, b = ccfg.s, ccfg.b 

334 a = ccfg.a 

335 t = max(1, ccfg.t) 

336 

337 if a <= 0: 

338 raise ValueError(f"Number of attention heads must be positive, got {a}") 

339 

340 cp = ccfg.cp 

341 

342 if cp <= 0: 

343 raise ValueError(f"CP degree must be positive, got {cp}") 

344 

345 attention_type = detect_attention_type(ccfg) 

346 cp_algo = _resolve_cp_algo(ccfg) 

347 

348 kv_dim = compute_kv_dim(ccfg) 

349 a_per_rank = a / t 

350 

351 if cp_algo == CPAlgo.ULYSSES_CP: 

352 terms = _ulysses_cp_attention_terms(s, b, cp, kv_dim, a_per_rank) 

353 else: 

354 terms = _ring_cp_attention_terms(s, b, cp, kv_dim, a_per_rank) 

355 

356 comm_buffer = EvalLayerComm.cp_comm_buffer(ccfg, ctx) 

357 

358 total_memory = ( 

359 terms.kv_cache_memory + terms.attention_scores_memory + 

360 terms.softmax_outputs_memory + terms.dropout_mask_memory + comm_buffer 

361 ) 

362 total_reduction = terms.s2_reduction + terms.kv_reduction - comm_buffer 

363 

364 return CPMemoryBreakdown( 

365 kv_cache_memory=terms.kv_cache_memory, 

366 attention_scores_memory=terms.attention_scores_memory, 

367 softmax_outputs_memory=terms.softmax_outputs_memory, 

368 dropout_mask_memory=terms.dropout_mask_memory, 

369 comm_buffer_memory=comm_buffer, 

370 kv_reduction=terms.kv_reduction, 

371 s2_reduction=terms.s2_reduction, 

372 total_reduction=total_reduction, 

373 total_memory=total_memory, 

374 cp_degree=int(cp), 

375 seq_len=int(s), 

376 attention_type=attention_type, 

377 cp_algo=cp_algo, 

378 )