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"""Experimental : Comm time"""
16from copy import copy, deepcopy
17from typing import Any, NamedTuple
18from hyper_parallel.auto_parallel.sapp_nd.nd.logger import perf_logger as logger
19import hyper_parallel.auto_parallel.sapp_nd.nd.common.hardware as Hard
20import hyper_parallel.auto_parallel.sapp_nd.nd.dimensions as Dim
21from hyper_parallel.auto_parallel.sapp_nd.nd.common.layer_type import LayerType
22from hyper_parallel.auto_parallel.sapp_nd.nd.debug import PerfParts
23from hyper_parallel.auto_parallel.sapp_nd.memory_estimation.evaluators.comm import EvalLayerComm
24from hyper_parallel.auto_parallel.sapp_nd.memory_estimation.evaluators.utils import EvalUtils
25from hyper_parallel.auto_parallel.sapp_nd.memory_estimation._context import NodeEval, Context
26from hyper_parallel.auto_parallel.sapp_nd.memory_estimation.evaluators.head import EvalHead
27from hyper_parallel.auto_parallel.sapp_nd.memory_estimation.evaluators.tail import EvalTail
28from hyper_parallel.auto_parallel.sapp_nd.memory_estimation.evaluators.body import EvalBody
29from hyper_parallel.auto_parallel.sapp_nd.memory_estimation.evaluators.layer_block import (
30 EvalAttn,
31 EvalFFn,
32 EvalNorm,
33)
34from hyper_parallel.auto_parallel.sapp_nd.perf_estimation.utils_classes import NetworkLevel, PerformanceType
35from hyper_parallel.auto_parallel.sapp_nd.perf_estimation.getters import (
36 get_layer_group_configs,
37 get_layer_switches_by_position,
38 get_model_order,
39 get_table_quantity,
40)
41from hyper_parallel.auto_parallel.sapp_nd.nd.common.cp_types import (
42 CPCommunicationCost,
43 CPAlgo,
44 _resolve_cp_algo,
45)
46from hyper_parallel.auto_parallel.sapp_nd.nd.common.arch_hooks import apply_layer_kind, layer_kinds
47from hyper_parallel.auto_parallel.sapp_nd.nd.common.derive import param_cp
48from hyper_parallel.auto_parallel.sapp_nd.nd.common.cost_model_preprocess import (
49 detect_attention_type,
50 AttentionType,
51 compute_kv_dim,
52 CostModelConfig,
53)
54
55COUNT_OPTIMIZER = False
56
57
58def _cp_resolve_topology(cp, device_per_node, bw_intra, bw_inter):
59 """Resolve CP topology and effective bandwidth.
60
61 Returns:
62 Tuple of (topology_str, effective_bandwidth).
63 """
64 intra_ranks = min(int(cp), int(device_per_node))
65 if cp <= device_per_node:
66 return "intra-node", bw_intra
67 if intra_ranks == 1:
68 return "cross-node", bw_inter
69 intra_fraction = (intra_ranks - 1) / (cp - 1)
70 cross_fraction = 1.0 - intra_fraction
71 bw = intra_fraction * bw_intra + cross_fraction * bw_inter
72 return "mixed", bw
73
74
75def _cp_comm_zero(ccfg):
76 """Return a zero CPCommunicationCost for cp <= 1."""
77 return CPCommunicationCost(
78 kv_volume_per_step=0.0, total_kv_volume=0.0, comm_volume=0.0,
79 ring_steps=0, ring_directions=0,
80 total_comm_time=0.0, exposed_comm_time=0.0,
81 overlap_ratio=0.5, effective_bandwidth=0.0,
82 topology="none", cp_degree=int(ccfg.cp),
83 seq_len=int(ccfg.s), batch_size=int(ccfg.b),
84 attention_type=AttentionType.MHA, kv_dim=0,
85 cp_algo=CPAlgo.COLOSSALAI_CP,
86 )
87
88
89class _CPVolumes(NamedTuple):
90 """Algorithm-specific CP communication volumes of one layer.
91
92 ``comm_volume`` is what :func:`cp_traffic` moves, the only field a score
93 reads; the others are logged.
94 """
95
96 kv_volume_per_step: float
97 total_kv_volume: float
98 comm_volume: float
99 ring_steps: int
100 ring_directions: int
101
102
103def _cp_comm_cost_common(ccfg, volumes, attention_type, kv_dim, cp_algo,
104 topology, effective_bandwidth):
105 """Build CPCommunicationCost with standard time calculation."""
106 overlap_ratio = 0.5
107 total_comm_time = (volumes.total_kv_volume / (effective_bandwidth * 1e9)) * 1e3
108 exposed_comm_time = total_comm_time * (1 - overlap_ratio)
109 return CPCommunicationCost(
110 kv_volume_per_step=volumes.kv_volume_per_step,
111 total_kv_volume=volumes.total_kv_volume,
112 comm_volume=volumes.comm_volume,
113 ring_steps=volumes.ring_steps, ring_directions=volumes.ring_directions,
114 total_comm_time=total_comm_time,
115 exposed_comm_time=exposed_comm_time,
116 overlap_ratio=overlap_ratio,
117 effective_bandwidth=effective_bandwidth,
118 topology=topology, cp_degree=int(ccfg.cp),
119 seq_len=int(ccfg.s), batch_size=int(ccfg.b),
120 attention_type=attention_type, kv_dim=int(kv_dim),
121 cp_algo=cp_algo,
122 )
123
124
125def _ulysses_cp_volumes(ccfg):
126 """Ulysses CP volumes: two All2All over (cp-1)/cp of the local shard."""
127 s, b = ccfg.s, ccfg.b
128 cp = ccfg.cp
129 t = max(1, ccfg.t)
130 local_qkv = s * b * (ccfg.a / t) * ccfg.dh * 2
131 a2a_vol = local_qkv * (cp - 1) / cp
132 return _CPVolumes(a2a_vol, a2a_vol * 2, cp_traffic(ccfg, CPAlgo.ULYSSES_CP), 0, 2)
133
134
135def _ring_cp_volumes(ccfg, kv_dim, cp_algo):
136 """Ring CP volumes: cp-1 P2P steps of s/cp tokens of KV, both directions."""
137 s, b = ccfg.s, ccfg.b
138 cp = ccfg.cp
139 kv_bytes = 4
140 kv_vol_step = (s / cp) * b * kv_dim * kv_bytes
141 total_kv = kv_vol_step * (cp - 1) * 2
142 return _CPVolumes(kv_vol_step, total_kv, cp_traffic(ccfg, cp_algo), int(cp - 1), 2)
143
144
145def cp_traffic(ccfg: CostModelConfig, cp_algo: CPAlgo) -> float:
146 """The bytes a rank moves a micro-batch for one layer's context parallelism.
147
148 Attention exchanges its keys and values, the MLP nothing. colossalai
149 and hybrid CP all-gather K and V over the sequence in the forward and
150 reduce-scatter their gradients in the backward, as HyperParallel runs
151 them (a ring passes the same chunks), each moving (cp - 1) / cp of the
152 sequence a rank. Ulysses all-to-alls the local query, key, value and
153 output between sequence and heads, forward and backward. A
154 linear-attention layer passes its recurrent state to the next rank,
155 and its gradient back, unless it all-to-alls as Ulysses does.
156 """
157 cp, t = ccfg.cp, max(1, ccfg.t)
158 ring = (cp - 1) / cp
159 head = ccfg.dh or ccfg.h / max(1, ccfg.a)
160 kv_width = compute_kv_dim(ccfg)
161 if cp_algo == CPAlgo.ULYSSES_CP:
162 rope = ccfg.dhr if detect_attention_type(ccfg) == AttentionType.MLA else 0
163 widths = ccfg.a * (head + rope) / t + 2 * kv_width + ccfg.a * head / t
164 return 2 * ring * ccfg.s / cp * ccfg.b * widths * ccfg.bytes_compute
165 if ccfg.n_linrec:
166 state_bytes = 4
167 return 2 * ccfg.a / t * head * head * state_bytes
168 return 2 * ring * ccfg.s * ccfg.b * 2 * kv_width * ccfg.bytes_compute
169
170
171def cp_comm_layer_detailed(ccfg: CostModelConfig, ctx: Context = None) -> CPCommunicationCost:
172 """Estimate CP communication cost with detailed breakdown.
173
174 Ring CP (colossalai_cp / hybrid_cp):
175 Ring P2P in both FW and BW directions.
176 Each step transfers (s/cp) tokens of KV data.
177 Total KV volume = kv_volume_per_step * (cp-1) * 2 directions.
178
179 Ulysses CP:
180 All2All in both FW and BW (2 All2All total).
181 Each All2All: every rank sends (cp-1)/cp of its local shard
182 and receives the rest from other ranks.
183 Per-All2All volume = s * b * (a/t) * bytes * (cp-1)/cp (head dims).
184 Total volume = 2 * per-All2All volume.
185
186 *ctx* is not read: the exchange a recompute sends again is priced by
187 the walk that recomputes it (``_recomputed_comm``).
188 """
189 del ctx
190 if ccfg.cp <= 1:
191 return _cp_comm_zero(ccfg)
192
193 if ccfg.a <= 0:
194 raise ValueError(f"Number of attention heads must be positive, got {ccfg.a}")
195
196 kv_dim = compute_kv_dim(ccfg)
197 attention_type = detect_attention_type(ccfg)
198 cp_algo = _resolve_cp_algo(ccfg)
199 topology, effective_bandwidth = _cp_resolve_topology(
200 ccfg.cp, ccfg.device_per_node, ccfg.bw_intra, ccfg.bw_inter)
201
202 if cp_algo == CPAlgo.ULYSSES_CP:
203 volumes = _ulysses_cp_volumes(ccfg)
204 else:
205 volumes = _ring_cp_volumes(ccfg, kv_dim, cp_algo)
206 return _cp_comm_cost_common(
207 ccfg, volumes, attention_type, kv_dim, cp_algo, topology, effective_bandwidth)
208
209
210def fill_dp_table(cfg, tables):
211 """DP"""
212 table_dp = {}
213 table_dp["n_attMM"] = cfg.h * cfg.h / cfg.t
214 table_dp["n_ffMM"] = cfg.h * cfg.hff / cfg.t
215 table_dp["n_normOp"] = 2 * cfg.h / cfg.sp
216
217 if COUNT_OPTIMIZER:
218 table_dp["n_attParamCast"] = (
219 11 * cfg.h * cfg.h / (cfg.d if cfg.has_op else 1)
220 )
221 table_dp["n_ffParamCast"] = (
222 11 * cfg.h * cfg.hff / (cfg.d if cfg.has_op else 1)
223 )
224 for op in table_dp:
225 table_dp[op] *= cfg.bytes_norm if op == "n_normOp" else cfg.bytes_p
226
227 table_exp_dp = deepcopy(table_dp)
228 table_exp_dp["n_ffMM"] = (
229 2
230 * (cfg.n_exp + cfg.n_shared_exp)
231 * cfg.h
232 * cfg.hff_exp
233 / cfg.t
234 * cfg.bytes_p
235 )
236 tables[Dim.DP] = table_dp
237 tables["exp_dp"] = table_exp_dp
238
239
240def fill_tp_table(cfg, tables):
241 """TP"""
242 table_tp = {}
243 high_tp_bias = 11 / 16 if cfg.t >= 8 else 1 # Fix this
244 table_tp["n_gather"] = cfg.b * cfg.s * cfg.h * high_tp_bias
245
246 for op in table_tp:
247 table_tp[op] *= cfg.bytes_compute
248
249 table_exp_tp = deepcopy(table_tp)
250 table_exp_tp["n_gather"] = (
251 cfg.b * cfg.s * cfg.h * 1.5 * (cfg.ep / cfg.d) * cfg.bytes_compute
252 )
253 tables["tp"] = table_tp
254 tables["exp_tp"] = table_exp_tp
255
256
257def fill_ep_table(cfg, tables, device_type):
258 """EP"""
259 intra_devices = device_type.intra_node_num()
260 table_ep = {}
261 inter_node_bias_ep = 1
262 table_ep["n_ffMM"] = (
263 4
264 * cfg.n_chosen_exp
265 * cfg.b
266 * cfg.s
267 * cfg.h
268 * (max(4, cfg.os_max_shard) / cfg.t)
269 * cfg.cap_fact
270 * (
271 cfg.os_max_shard / min(intra_devices, cfg.ep)
272 + (
273 inter_node_bias_ep
274 * cfg.os_max_shard
275 / (cfg.ep / intra_devices)
276 if cfg.ep > intra_devices
277 else 0
278 )
279 )
280 )
281
282 for op in table_ep:
283 table_ep[op] *= cfg.bytes_compute
284 tables[Dim.EP] = table_ep
285
286
287def dp_ratio(cfg, device_type):
288 """formula"""
289 return (
290 0
291 if cfg.comm_d_non_exp == 0
292 else 1
293 - True # overlap_dp, Completely overlap standard DP comm
294 + (
295 1 / 16
296 if cfg.n_exp == 1
297 else 1 / max(1, cfg.ep / device_type.intra_node_num()) / 1.25
298 ) # overlap_op, Bias in overlapping OP comm (todo:make it dynamic too)
299 * (cfg.comm_d_non_exp - 1)
300 * cfg.os_max_shard
301 / cfg.d
302 )
303
304
305def comm_embed_ouput(cfg):
306 """ "formula"""
307 comm_embed = cfg.bytes_compute * cfg.h * cfg.v / cfg.shard_embed
308 comm_output = cfg.h * cfg.v / cfg.t
309 return comm_embed, comm_output
310
311
312def estimate_op_bulk_comm(*args, **kwargs):
313 """FW + BW"""
314 param = {
315 "cfg": args[0],
316 "ccfg": args[1],
317 "stages": args[2],
318 "device_type": args[3],
319 "with_recomp": kwargs.get(
320 "with_recomp", args[4] if len(args) > 4 else False
321 ),
322 "debugger": kwargs.get("debugger", args[5] if len(args) > 5 else None),
323 }
324
325 param["tables"] = {}
326 fill_dp_table(param["cfg"], param["tables"])
327
328 param['dp_ratio'] = dp_ratio(param['cfg'], param['device_type'])
329
330 param["comm_embed"], param["comm_output"] = comm_embed_ouput(param["cfg"])
331
332 if param["cfg"].dc_kv != 0: # Deepseek
333 param["comm_output"] += param["cfg"].h * (
334 2 * param["cfg"].h + param["cfg"].v
335 )
336 param["comm_output"] *= param["cfg"].n_mtp
337
338 param["comm_output"] *= param["cfg"].bytes_p
339
340 fill_tp_table(param["cfg"], param["tables"])
341 fill_ep_table(param["cfg"], param["tables"], param["device_type"])
342
343 lccfgs = get_layer_group_configs(param["cfg"])
344 logger.info(lccfgs)
345 param["layer_count"] = 0
346 param["idx_lccfg"] = 0
347 comms = {Dim.DP: [], Dim.TP: [], Dim.EP: []}
348 # ignores comm recomp, to improve
349 for stage in param["stages"]:
350 comm = {Dim.DP: 0.0, Dim.TP: 0.0, Dim.EP: 0.0}
351 for chunk in stage:
352 for layer in chunk:
353 param["layer_count"], param["idx_lccfg"] = (
354 estimate_op_bulk_comm_layer(
355 param,
356 lccfgs,
357 layer=layer,
358 layer_count=param["layer_count"],
359 idx_lccfg=param["idx_lccfg"],
360 )
361 )
362 if param["ccfg"].ttype == PerformanceType.TIME:
363 for dim, ov in zip([Dim.DP, Dim.TP, Dim.DP], [0.0, 0.0, 0.0]):
364 comm[dim] = estimate_comm_score(
365 param["cfg"],
366 comm[dim],
367 dim,
368 overlap=ov,
369 device=param["device_type"],
370 )
371
372 comm[Dim.DP] *= param["dp_ratio"]
373 comm[Dim.TP] *= param["cfg"].comm_t
374 comm[Dim.EP] *= param["cfg"].comm_ep
375
376 if param["device_type"].name == "A3":
377 logger.info("A3 ratio")
378 comm[Dim.TP] /= 3
379
380 comms[Dim.DP].append(comm[Dim.DP])
381 comms[Dim.TP].append(comm[Dim.TP])
382 comms[Dim.EP].append(comm[Dim.EP])
383
384 if param["debugger"] and param["debugger"].is_enabled():
385 logger.info("DP_COMM = %s", comms[Dim.DP])
386 logger.info("MP_COMM = %s", comms[Dim.TP])
387 logger.info("EP_COMM = %s", comms[Dim.EP])
388 param["debugger"].info[PerfParts.DP_COMM] = comms[Dim.DP]
389 param["debugger"].info[PerfParts.MP_COMM] = comms[Dim.TP]
390 param["debugger"].info[PerfParts.EP_COMM] = comms[Dim.EP]
391
392 res = []
393 for i, c in enumerate(comms[Dim.TP]):
394 res.append(comms[Dim.DP][i] + c + comms[Dim.EP][i])
395
396 return res
397
398
399def estimate_op_bulk_comm_layer(cfg, lccfgs, **kwargs):
400 """for estimate_op_bulk_comm"""
401 if kwargs["layer"] == LayerType.EMBEDDING_LAYER:
402 kwargs["comm"][Dim.DP] += kwargs["param"]["comm_embed"]
403 return kwargs["layer_count"]
404
405 if kwargs["layer"] == LayerType.OUTPUT_LAYER:
406 kwargs["comm"][Dim.DP] += kwargs["param"]["comm_output"]
407 if cfg.dc_kv != 0: # Deepseek
408 lccfg = lccfgs[kwargs["idx_lccfg"]][0]
409 kwargs["comm"][Dim.TP] += cfg.n_mtp * get_table_quantity(
410 lccfg,
411 kwargs["param"]["tables"]["exp_tp"],
412 LayerType.NOT_REC_LAYER,
413 kwargs["param"]["with_recomp"],
414 )
415 return kwargs["layer_count"]
416
417 if (
418 kwargs["idx_lccfg"] + 1 < len(lccfgs)
419 and lccfgs[kwargs["idx_lccfg"]][1] == kwargs["layer_count"]
420 ):
421 kwargs["layer_count"] = 0
422 kwargs["idx_lccfg"] += 1
423
424 lccfg = lccfgs[kwargs["idx_lccfg"]][0]
425 is_moe_layer = lccfg.n_exp > 1
426
427 if is_moe_layer:
428 kwargs["comm"][Dim.DP] += get_table_quantity(
429 lccfg,
430 kwargs["param"]["tables"]["exp_dp"],
431 kwargs["layer"],
432 kwargs["param"]["with_recomp"],
433 )
434 kwargs["comm"][Dim.TP] += get_table_quantity(
435 lccfg,
436 kwargs["param"]["tables"]["exp_tp"],
437 kwargs["layer"],
438 kwargs["param"]["with_recomp"],
439 )
440 kwargs["comm"][Dim.EP] += get_table_quantity(
441 lccfg,
442 kwargs["param"]["tables"][Dim.EP],
443 kwargs["layer"],
444 kwargs["param"]["with_recomp"],
445 )
446 else:
447 kwargs["comm"][Dim.DP] += get_table_quantity(
448 lccfg,
449 kwargs["param"]["tables"][Dim.DP],
450 kwargs["layer"],
451 kwargs["param"]["with_recomp"],
452 )
453 kwargs["comm"][Dim.TP] += get_table_quantity(
454 lccfg,
455 kwargs["param"]["tables"]["tp"],
456 kwargs["layer"],
457 kwargs["param"]["with_recomp"],
458 )
459
460 kwargs["layer_count"] += 1
461 return kwargs["layer_count"], kwargs["idx_lccfg"]
462
463
464def prepare_context():
465 """context object"""
466 ctx = Context()
467 ctx.attn_num_p = EvalAttn.num_params_attn
468 ctx.ffn_num_p = EvalFFn.num_params_ffn
469 # A MoE layer's experts, counted as the memory path's eval config counts them.
470 ctx.ffn_routed_num_p = EvalFFn.num_params_routed_expert
471 ctx.ffn_shared_num_p = EvalFFn.num_params_shared_expert
472 ctx.ffn_router_num_p = EvalFFn.num_params_router
473 ctx.norm_num_p = EvalNorm.num_params_norm
474
475 ctx.node_eval[LayerType.EMBEDDING_LAYER] = NodeEval(
476 EvalHead.num_params_embed, None, None
477 )
478 ctx.node_eval[LayerType.OUTPUT_LAYER] = NodeEval(
479 EvalTail.num_params_output, None, None
480 )
481 ctx.node_eval[LayerType.NOT_REC_LAYER] = NodeEval(
482 EvalBody.num_params_layer, None, None
483 )
484 ctx.enable_accu_log = False
485 return ctx
486
487
488def _recomputed_comm(cfg, ctx, layer):
489 """TP, EP and CP volume a recomputed layer transfers again.
490
491 It is the communication whose buffers the layer's memory no longer keeps:
492 all of it for a fully recomputed layer, and for a selective one what its
493 switches drop, which the memory model's own terms give as the plain
494 volume less the selective one. Parameter traffic is not recomputed. Of
495 CP's traffic, a recompute runs the forward's exchange again, the half
496 the gather switch keeps in a selective layer.
497 """
498 def _volumes(node):
499 ctx.current_node = node
500 kept = EvalUtils.rec_coeff(node == LayerType.SEL_REC_LAYER, EvalUtils.switch(cfg, ctx, "gather"))
501 return (
502 EvalLayerComm.tp_comm_layer(cfg, ctx, 1),
503 EvalLayerComm.ep_comm_layer(cfg, ctx, 1),
504 cp_comm_layer_detailed(cfg, ctx).comm_volume / 2 * kept,
505 )
506
507 kept = ctx.current_node
508 try:
509 plain = _volumes(LayerType.NOT_REC_LAYER)
510 if layer == LayerType.FULL_REC_LAYER:
511 return plain
512 selective = _volumes(LayerType.SEL_REC_LAYER)
513 return tuple(whole - left for whole, left in zip(plain, selective))
514 finally:
515 ctx.current_node = kept
516
517
518def fsdp_root_parts(cfg: Any, layer: Any) -> tuple:
519 """The root's table, the embedding's or the output layer's, as FSDP holds it.
520
521 The parts are as :meth:`EvalLayerComm.fsdp_layer_parts` gives a layer's;
522 a table the output layer shares is its.
523 """
524 if layer == LayerType.EMBEDDING_LAYER:
525 if EvalHead.shares_output_table(cfg):
526 return ()
527 tp = cfg.shard_embed / max(1, cfg.gather_embed or 1)
528 return ((EvalHead.num_params_embed(cfg, None), tp, cfg.shard_embed, cfg.d * tp),)
529 return ((EvalTail.num_params_output(cfg, None), cfg.t, cfg.shard_p_os_non_exp_partial, cfg.d * cfg.cp * cfg.t),)
530
531
532def _fsdp_rounds(cfg) -> tuple:
533 """How many times a micro-batch FSDP gathers a layer's and the root's parameters, and all-reduces.
534
535 Resharded after its forward and its backward, a layer is gathered for
536 both, and the root, kept through its backward, once; kept gathered, each
537 is gathered once a step. The copies of a shard all-reduce once a step,
538 after the reduce-scatters, where an FSDP group shards the parameters,
539 and every micro-batch where none does.
540 """
541 micro = max(1, cfg.m)
542 reshards = bool(getattr(cfg, "reshards", False))
543 # Sharded beyond TP and whatever CP adds to the width (param_cp): with
544 # TP times CP, a HyperParallel shard no wider than CP read as none.
545 shards = cfg.shard_p_os_non_exp_partial > cfg.t * param_cp(cfg)
546 return (2 if reshards else 1 / micro), (1 if reshards else 1 / micro), (1 / micro if shards else 1)
547
548
549def _accumulate_stage_comm(param, stage, stage_id):
550 """Sum the per-layer DP, TP, EP and CP communication volumes of one stage.
551
552 With ``param["with_recomp"]`` or ``param["both"]``, the walk also keeps
553 the same sums with the volume each recomputed layer's recompute transfers
554 again added, the way the compute estimate counts a recomputed op twice.
555 Both sums take every term in the same order as a walk keeping one of
556 them, so each comes out as that walk's would.
557
558 A body layer is priced on the walk's config, in its own kind; the
559 embedding and the output layer on the model's config, as its family left
560 it, whichever layer the walk reached last. Under FSDP (grad_shard_as_params),
561 a layer's DP volume, and the root's, is the traffic its collectives
562 move a micro-batch (:meth:`EvalLayerComm.fsdp_traffic`).
563
564 Returns:
565 ``(comm, again, reduced)``: the volumes by dimension; the same with
566 each recomputed layer's traffic added again, or ``None`` where the
567 walk keeps no such sums; and the part of the DP volume the copies of
568 a shard all-reduce (:meth:`EvalLayerComm.fsdp_reduce_traffic`).
569 """
570 comm = {Dim.DP: 0.0, Dim.TP: 0.0, Dim.EP: 0.0, Dim.CP: 0.0}
571 again = dict(comm) if param["with_recomp"] or param["both"] else None
572 sums = (comm,) if again is None else (comm, again)
573 reduced = 0.0
574 fsdp = bool(getattr(param["cfg"], "grad_shard_as_params", False))
575 layer_gathers, root_gathers, reduces = _fsdp_rounds(param["cfg"]) if fsdp else (0, 0, 0)
576 for chunk_id, chunk in enumerate(stage):
577 for lay_id, layer in enumerate(chunk):
578 param["ctx"].current_node = layer
579 position = (stage_id, chunk_id, lay_id)
580 is_body = layer not in [LayerType.EMBEDDING_LAYER, LayerType.OUTPUT_LAYER]
581 cfg = param["walk"] if is_body else param["cfg"]
582 # A layer's own recompute switches, where the config states several.
583 param["ctx"].switches = param["switches"].get(position) if is_body else None
584 if is_body and position in param["kinds"]:
585 kind = param["kinds"][position]
586 if kind is not None:
587 apply_layer_kind(cfg, kind)
588 logger.info("is layer moe ? %s", cfg.n_exp > 1)
589 param["ctx"].current_node = LayerType.NOT_REC_LAYER
590 logger.info("param ctx %s", param["ctx"])
591 if fsdp:
592 parts = EvalLayerComm.fsdp_layer_parts(cfg, param["ctx"])
593 _add(sums, Dim.DP, EvalLayerComm.fsdp_traffic(cfg, parts, layer_gathers, reduces))
594 reduced += EvalLayerComm.fsdp_reduce_traffic(cfg, parts, reduces)
595 else:
596 _add(sums, Dim.DP, EvalLayerComm.dp_comm_layer(cfg, param["ctx"]))
597 elif fsdp and not is_body:
598 parts = fsdp_root_parts(cfg, layer)
599 _add(sums, Dim.DP, EvalLayerComm.fsdp_traffic(cfg, parts, root_gathers, reduces))
600 reduced += EvalLayerComm.fsdp_reduce_traffic(cfg, parts, reduces)
601
602 _add(sums, Dim.TP, EvalLayerComm.tp_comm_layer(
603 cfg, param["ctx"], 1
604 )) # / 4 #* (param["cfg"].t - 1)
605 _add(sums, Dim.EP, EvalLayerComm.ep_comm_layer(
606 cfg, param["ctx"], 1
607 )) # * param["cfg"].ep
608 if is_body:
609 _add(sums, Dim.CP, cp_comm_layer_detailed(
610 cfg, param["ctx"]
611 ).comm_volume)
612 # min(device_type.level_bound_number[0], param["cfg"].ep)
613 if again is not None and layer in (LayerType.FULL_REC_LAYER, LayerType.SEL_REC_LAYER):
614 tp_again, ep_again, cp_again = _recomputed_comm(cfg, param["ctx"], layer)
615 again[Dim.TP] += tp_again
616 again[Dim.EP] += ep_again
617 again[Dim.CP] += cp_again
618 return comm, again, reduced
619
620
621def _add(sums, dim, volume):
622 """Add *volume* to dimension *dim* of each of *sums*."""
623 for comm in sums:
624 comm[dim] += volume
625
626
627def _stage_comm_time(param, comm):
628 """Turn one stage's volumes into the walk's units, in place: the time model, the nodes, the overlap, A3."""
629 if param["ccfg"].ttype == PerformanceType.TIME:
630 for dim, ov in zip([Dim.DP, Dim.TP, Dim.CP], [0.0, 0.0, 0.0]):
631 comm[dim] = estimate_comm_score(
632 param["cfg"],
633 comm[dim],
634 dim,
635 overlap=ov,
636 device=param["device_type"],
637 )
638
639 dev_per_node = param["device_type"].level_bound_number[0]
640 comm[Dim.TP] *= max(1, param["cfg"].t // dev_per_node)
641 comm[Dim.EP] *= max(1, param["cfg"].ep // dev_per_node)
642 comm[Dim.CP] *= max(1, param["cfg"].cp // dev_per_node)
643
644 # Transitional overlap correction.
645 # The search runs the FLOP path, which has no other overlap
646 # modeling; these factors are the only overlap correction on that
647 # path. The TIME path's estimate_comm_score(overlap=...) call
648 # above is zeroed, so this is the single source of overlap for
649 # both paths.
650 # Defaults (dp=0.9, tp=0.5) are MindFormers-validated overlap, not
651 # test hacks: they made the model match real MindFormers step times.
652 # Re-validating for the hyper-parallel target is a follow-up.
653 # Follow-up: source from hardware, fix estimate_comm_score's dim
654 # list and add latency, then fold this into estimate_comm_score.
655 comm[Dim.DP] *= (1 - param["cfg"].comm_dp_overlap)
656 comm[Dim.TP] *= (1 - param["cfg"].comm_tp_overlap)
657
658 if param["device_type"].name == "A3":
659 logger.info("A3 ratio")
660 comm[Dim.DP] /= 2
661 comm[Dim.TP] /= 2
662 comm[Dim.EP] /= 2
663 comm[Dim.CP] /= 2
664
665
666def _stage_totals(comms):
667 """Each stage's volume over the four dimensions, from per-dimension lists."""
668 return [c + comms[Dim.DP][i] + comms[Dim.EP][i] + comms[Dim.CP][i] for i, c in enumerate(comms[Dim.TP])]
669
670
671def estimate_from_mem_comm(*args, **kwargs):
672 """Each stage's communication, from the memory model's volumes.
673
674 With ``both``, one walk returns ``(plain, recomputed)``: each stage's
675 communication, then the same with each recomputed layer's traffic added
676 again, which ``with_recomp`` alone returns. The debugger records the
677 plain parts.
678 """
679
680 param = {
681 "cfg": args[0],
682 "ccfg": args[1],
683 "stages": args[2],
684 "device_type": args[3],
685 }
686 param["debugger"] = kwargs.get(
687 "debugger", args[5] if len(args) > 5 else None
688 )
689 param["with_recomp"] = kwargs.get(
690 "with_recomp", args[4] if len(args) > 4 else False
691 )
692 param["both"] = kwargs.get("both", False)
693 param["ctx"] = prepare_context()
694 # The layers' kinds go on a copy: the model's config leaves the walk as
695 # it entered it, for the next walk and every read after.
696 param["walk"] = copy(param["cfg"])
697
698 # Each layer's kind, in model order; layers past the stack get no entry,
699 # so no kind and no DP term.
700 param["kinds"] = dict(zip(get_model_order(param["cfg"], param["stages"]), layer_kinds(param["cfg"])))
701 param["switches"] = get_layer_switches_by_position(param["cfg"], param["stages"])
702 comms = {Dim.DP: [], Dim.TP: [], Dim.EP: [], Dim.CP: []}
703 recomms = {Dim.DP: [], Dim.TP: [], Dim.EP: [], Dim.CP: []}
704 # Each stage's all-reduce, the share of its DP time the copies of a shard
705 # spend reducing it: estimate_pipeline takes it out of DP_COMM.
706 reduces = []
707 for stage_id, stage in enumerate(param["stages"]):
708 plain, again, reduced = _accumulate_stage_comm(param, stage, stage_id)
709 comm = again if param["with_recomp"] and not param["both"] else plain
710 reduce_share = reduced / comm[Dim.DP] if comm[Dim.DP] else 0.0
711
712 _stage_comm_time(param, comm)
713 for dim, values in comms.items():
714 values.append(comm[dim])
715 reduces.append(comm[Dim.DP] * reduce_share)
716 if param["both"]:
717 _stage_comm_time(param, again)
718 for dim, values in recomms.items():
719 values.append(again[dim])
720
721 if param["debugger"] and param["debugger"].is_enabled():
722 logger.info("DP_COMM = %s", comms[Dim.DP])
723 logger.info("MP_COMM = %s", comms[Dim.TP])
724 logger.info("EP_COMM = %s", comms[Dim.EP])
725 logger.info("CP_COMM = %s", comms[Dim.CP])
726 param["debugger"].info[PerfParts.DP_COMM] = comms[Dim.DP]
727 param["debugger"].info[PerfParts.DP_REDUCE] = reduces
728 param["debugger"].info[PerfParts.MP_COMM] = comms[Dim.TP]
729 param["debugger"].info[PerfParts.EP_COMM] = comms[Dim.EP]
730 param["debugger"].info[PerfParts.CP_COMM] = comms[Dim.CP]
731 if param["cfg"].cp > 1:
732 # Logged, not stored: debugger.info must hold only numeric PerfParts,
733 # which the debug CSV and the score table are built from.
734 cp_comm_details = cp_comm_layer_detailed(param["cfg"], param["ctx"])
735 logger.info("CP_KV_VOLUME = %s", cp_comm_details.total_kv_volume)
736 logger.info("CP_EXPOSED_TIME = %s", cp_comm_details.exposed_comm_time)
737 logger.info("CP_TOPOLOGY = %s", cp_comm_details.topology)
738 logger.info("CP_BANDWIDTH = %s", cp_comm_details.effective_bandwidth)
739
740 if param["both"]:
741 return _stage_totals(comms), _stage_totals(recomms)
742 return _stage_totals(comms)
743
744
745def estimate_comm(*args, **kwargs):
746 """wrapper; with ``both``, the plain and the recomputed communication from one walk"""
747 cfg, ccfg, stages, device_type = args[0], args[1], args[2], args[3]
748 with_recomp = kwargs.get(
749 "with_recomp", args[4] if len(args) > 4 else False
750 )
751 debugger = kwargs.get("debugger", args[5] if len(args) > 5 else None)
752 # return estimate_op_bulk_comm(cfg, ccfg, stages,
753 # device_type=device_type, with_recomp=with_recomp,
754 # debugger=debugger)
755 return estimate_from_mem_comm(
756 cfg,
757 ccfg,
758 stages,
759 device_type,
760 with_recomp=with_recomp,
761 debugger=debugger,
762 both=kwargs.get("both", False),
763 )
764
765
766def level_efficiency(level):
767 """to improve for Ascend A2"""
768 if level == NetworkLevel.NODE:
769 return 0.7
770 if level == NetworkLevel.CLUSTER:
771 return 0.9
772 raise ValueError
773
774
775def level_bandwidth(level):
776 """to improve for Ascend A2"""
777 if level == NetworkLevel.NODE:
778 return 300
779 if level == NetworkLevel.CLUSTER:
780 return 25
781 raise ValueError
782
783
784def level_latency(level):
785 """to improve for Ascend A2"""
786 if level == NetworkLevel.NODE:
787 return 0.00001
788 if level == NetworkLevel.CLUSTER:
789 return 0.00002
790 raise ValueError
791
792
793def comm_throughput(level):
794 """formula"""
795 eff = level_efficiency(level)
796 bw = level_bandwidth(level)
797 return bw * eff
798
799
800def estimate_comm_size_time(_, comm_size, level):
801 """formula"""
802 th = comm_throughput(level)
803 lat = level_latency(level)
804 return lat + comm_size / th
805
806
807def estimate_comm_score(
808 cfg, comm_volume, dim, overlap=0.0, device=Hard.device_map["A2"]
809):
810 """score assignment"""
811 assignment = device.level_assign(dp=cfg.d, tp=cfg.t, cp=cfg.cp, pp=cfg.p)
812 score = 0
813 for level in range(device.levels):
814 # intra_comm = comm_volume * (1-overlap)
815 # * (assignment[dim][0]-1) / device.intra_node_bw
816 score += (
817 comm_volume
818 * (1 - overlap)
819 * (
820 (assignment[dim][level] - 1)
821 * device.devices_below_level(level)
822 / device.level_bandwidth[level]
823 )
824 )
825 return score