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"""backbone memory estimation module"""
16from __future__ import annotations
17from typing import TYPE_CHECKING
18import os
19import ast
20import math
21import logging
22import inspect
23import functools
24import pprint
25import importlib
26import matplotlib.pyplot as plt
27from PIL import Image
28from hyper_parallel.auto_parallel.sapp_nd.nd.common.config import Config
29from hyper_parallel.auto_parallel.sapp_nd.nd.logger import logger as nd_logger
30from hyper_parallel.auto_parallel.sapp_nd.nd.common.cost_model_preprocess import CostModelConfig
31from hyper_parallel.auto_parallel.sapp_nd.nd.common.arch_hooks import (
32 apply_layer_kind,
33 check_and_apply_custom_hook,
34 layer_kinds,
35)
36from hyper_parallel.auto_parallel.sapp_nd.nd.common.layer_order import layer_switches
37from hyper_parallel.auto_parallel.sapp_nd.memory_estimation.logger import logger
38from hyper_parallel.auto_parallel.sapp_nd.memory_estimation._context import Context, MemType
39from hyper_parallel.auto_parallel.sapp_nd.memory_estimation.evaluators.utils import EvalUtils
40from hyper_parallel.auto_parallel.sapp_nd.memory_estimation.evaluators.body import EvalBody
41from hyper_parallel.auto_parallel.sapp_nd.memory_estimation.evaluators.tail import EvalTailSingle
42from hyper_parallel.auto_parallel.sapp_nd.nd.common.layer_type import LayerType
43from hyper_parallel.auto_parallel.sapp_nd.memory_estimation.evaluators.comm import EvalLayerComm
44from hyper_parallel.auto_parallel.sapp_nd.memory_estimation._bwd_overhead import _BackwardOverhead
45from hyper_parallel.auto_parallel.sapp_nd.memory_estimation._ppb import _PPB
46from hyper_parallel.auto_parallel.sapp_nd.memory_estimation.hook_base import MemEvalHook
47
48if TYPE_CHECKING:
49 from typing import Any, Dict, List, Optional, Tuple
50
51current_dir = os.path.dirname(os.path.abspath(__file__))
52EVAL_YML = os.path.join(current_dir, "configs_eval/default.yaml")
53
54
55@functools.lru_cache(maxsize=4096)
56def _parameters(fun: Any) -> Tuple[inspect.Parameter, ...]:
57 """A communication formula's parameters, read once a formula rather than once a layer and candidate."""
58 return tuple(inspect.signature(fun).parameters.values())
59
60
61class _Backbone:
62 """backbone class"""
63
64 def __init__(self, config: Any, **kwargs):
65 self._child_cls = self
66 self.mb = EvalUtils.mb
67 self.eval_cfg = Config(kwargs.get("eval_yml", EVAL_YML))
68 self._ctx = kwargs.get("ctx", Context())
69 self._ccfg = kwargs.get("ccfg", None)
70 # During a walk, the regular layers' own recompute switches still to
71 # visit, where the config states several (derive's layer_switches).
72 self._switch_order = None
73 self.framework = kwargs.get("framework", None)
74 self.source_code = kwargs.get("source_code", None)
75 self.hook_cls, self.config_path = None, None
76 # The machine's device count, as run_nd -d states it, for a config
77 # that cannot state its own: an AutoModels yaml has no world size.
78 self.devices = getattr(kwargs.get("machine"), "number", None) or 0
79 if not self._ccfg:
80 self.hook_cls = kwargs.get("hook_cls", self.eval_cfg.hook_class)
81 if isinstance(self.hook_cls, str):
82 self._load_eval_yaml_hook_cls(self.hook_cls)
83 if self.hook_cls and not isinstance(self.hook_cls, MemEvalHook):
84 raise AttributeError(
85 f"'{self.hook_cls}' is not a MemEvalHook instance"
86 )
87 if config:
88 if kwargs.get("log_level", 1) == 0:
89 # logger.setLevel(logging.CRITICAL)
90 nd_logger.setLevel(logging.CRITICAL)
91 self.update_config(config)
92 else:
93 raise AttributeError("missing config")
94 self.evaluator_instances = None
95 # The submodule a multimodal model's stages are fitted on, by name,
96 # or None for a model of one module.
97 self.main_module = None
98 self.ppb = None
99 # Each stage's dynamic memory, in MB, at the two points of the last
100 # stage estimate where its peak can come: as warm-up ends, and as a
101 # later micro-batch's backward ends (0 where that is no candidate).
102 # The stage's Dynamic insight is the larger.
103 self.peak_points: List[Tuple[int, int]] = []
104 self._overhead_obj = _BackwardOverhead(
105 self, self._ccfg, self._ctx, self._inner_dynamic_mem
106 )
107 self._ppb_obj = _PPB(self.eval_cfg, self._inner_dynamic_mem)
108
109 @property
110 def ccfg(self) -> CostModelConfig:
111 """read-only"""
112 return self._ccfg
113
114 @property
115 def ctx(self) -> Context:
116 """read-only"""
117 return self._ctx
118
119 def _load_eval_yaml_hook_cls(self, hook_cls):
120 """hook_class in eval yaml"""
121 target_mod_path = None
122 try:
123 # search in folder 'hooks'
124 hooks_dir = os.path.join(current_dir, "hooks")
125 for f in os.listdir(hooks_dir):
126 if f.endswith(".py"):
127 mod_path = f"hyper_parallel.auto_parallel.sapp_nd.memory_estimation.hooks.{f.split('.')[0]}"
128 spec = importlib.util.find_spec(mod_path)
129 if spec is None or spec.origin is None:
130 continue
131 with open(spec.origin, "r", encoding="utf-8") as mf:
132 source = mf.read()
133 tree = ast.parse(source)
134 mod_cls = None
135 for node in ast.walk(tree):
136 if isinstance(node, ast.ClassDef) and node.name == hook_cls:
137 mod_cls = node
138 break
139 if mod_cls:
140 target_mod_path = mod_path
141 break
142 if target_mod_path:
143 module = importlib.import_module(target_mod_path)
144 self.hook_cls = getattr(module, hook_cls)()
145 except (ModuleNotFoundError, ImportError) as e:
146 print(e)
147
148 # Peak memory estimation
149
150 def update_config(self, new_config: Any) -> None:
151 """processing input config"""
152 if self._ccfg is not None:
153 self._ccfg.update_config(new_config, self.hook_cls, self.framework, self.source_code)
154 else:
155 self._ccfg = CostModelConfig(
156 new_config, self.hook_cls, self.framework, self.source_code, devices=getattr(self, "devices", 0)
157 )
158 if isinstance(new_config, str) and not self.config_path:
159 logger.info(
160 "%s Process config file: %s",
161 "=" * 30,
162 new_config.split("/")[-1],
163 )
164 # Kept whatever its form, so reset_config() can re-parse a config
165 # handed over in memory rather than read from a path.
166 self.config_path = new_config
167 self.evaluator_instances = None
168 self.ppb = None
169
170 def _inner_static_mem(self) -> float:
171 """static memory evaluation for backbone estimation"""
172 self._ctx.node_grad = 0
173 if self._ctx.current_node in self._ctx.node_eval:
174 p = self._ctx.eval.stat.p(self._ccfg, self._ctx)
175 ost = self._ctx.eval.stat.os(self._ccfg, self._ctx)
176 grad = self._ctx.eval.stat.grad(self._ccfg, self._ctx)
177 self._ctx.node_grad = grad
178 res = p + ost + grad
179 self._ctx.save2log("_param", res)
180 # Log routed/shared expert param breakdown for MoE layers
181 if self._ccfg.n_exp > 1 and self.is_regular_layer(self._ctx.current_node):
182 result = self._ctx.eval.num_p(self._ccfg, self._ctx)
183 if isinstance(result, tuple) and len(result) == 3:
184 _, routed_exp_p, shared_exp_p = result
185 routed_exp_mem = (
186 routed_exp_p / self._ccfg.ep
187 * self._ccfg.bytes_p / self._ccfg.shard_p_os_exp
188 )
189 shared_exp_mem = (
190 shared_exp_p
191 * self._ccfg.bytes_p / self._ccfg.shard_p_os_exp_partial
192 )
193 self._ctx.save2log("routed_exp_param", routed_exp_mem)
194 self._ctx.save2log("shared_exp_param", shared_exp_mem)
195 return p + ost + grad
196 return 0
197
198 def _inner_dynamic_mem(
199 self, ppb=False, default_micro_factor=None
200 ) -> tuple[float, float]:
201 """dynamic memory evaluation for backbone estimation"""
202 if self._ctx.current_node in self._ctx.node_eval:
203 self._ctx.seq_chunks = 1
204 if ppb:
205 micro_factor = 1
206 elif default_micro_factor:
207 micro_factor = default_micro_factor
208 else:
209 sched = self._ccfg.pp_sched
210 micro_factor = self._ctx.pp_micro_eval[sched](
211 self._ccfg, self._ctx
212 )
213 self._ctx.micro_factor = max(1, micro_factor)
214 # A schedule that splits the sequence evaluates the node at one
215 # chunk's length, and leaves the config's as it found it.
216 full_s = self._ccfg.s
217 if self._ctx.seq_chunks > 1:
218 self._ccfg.s = full_s / self._ctx.seq_chunks
219 try:
220 activation = self._ctx.eval.dyn.activation(self._ccfg, self._ctx)
221 self._ctx.save2log("_activ", activation)
222 comm = self._inner_comm_mem(micro_factor)
223 finally:
224 self._ccfg.s = full_s
225 return activation, comm
226 return 0, 0
227
228 def _inner_comm_mem(self, micro_factor) -> float:
229 """dynamic memory evaluation for backbone estimation"""
230 if self._ctx.current_node in self._ctx.node_eval:
231 comm_eval_field = self._ctx.eval.dyn.comm
232 comm_cat = {
233 "dp": MemType.AG_COMM,
234 "tp": MemType.AG_COMM,
235 "cp": MemType.AG_COMM,
236 "ep": MemType.A2A_COMM,
237 }
238 comm_mem = {}
239 for k, fun in vars(comm_eval_field).items():
240 if fun is None:
241 continue
242 sig_param = _parameters(fun)
243 if (
244 # any(
245 # p.kind == inspect.Parameter.VAR_KEYWORD
246 # for p in sig_param
247 # )
248 # and len(sig_param) > 2
249 len(sig_param) > 2
250 ):
251 comm = fun(self._ccfg, self._ctx, micro_factor)
252 else:
253 comm = fun(self._ccfg, self._ctx)
254 comm_mem[k] = comm
255 # FSDP that reshards frees a layer's gathered parameters once it
256 # has run: a layer keeps none between its passes, and its
257 # backward holds its own and the next layer's, prefetched, as
258 # its working set counts them. The root's, the embedding and
259 # output tables, stay gathered from the forward on: their
260 # nodes keep them, and a working set holds no second copy.
261 if "dp" in comm_mem and self._ccfg.reshards:
262 if self.is_regular_layer(self._ctx.current_node):
263 comm_mem["dp"] *= self._ctx.working_set
264 elif self._ctx.working_set:
265 comm_mem["dp"] = 0
266 res = EvalUtils.eval_expr_insight(
267 expr=self._ctx.comm_expr,
268 ctx=self._ctx,
269 mem_val=comm_mem,
270 mem_cat=comm_cat,
271 )
272 self._ctx.save2log("_comm", res)
273 return res
274 return 0
275
276 def __subplot_stages(self, stage_i, stat_mem_i, dyn_mem_i, i) -> None:
277 """Plot static and dynamic memory for one pipeline stage."""
278 _, ax = plt.subplots(figsize=(5, 8))
279 bottoms = {"stat": 0, "dyn": 0}
280 color_stat = plt.get_cmap("cividis")
281 color_dyn = plt.get_cmap("plasma")
282 total_lay = sum(len(chunk) for chunk in stage_i)
283 color_denominator = max(1, total_lay - 1)
284 for chunk_id, chunk in enumerate(stage_i):
285 for lay_id, lay_type in enumerate(chunk):
286 real_id = self._ctx.real_lay_ids[chunk_id][i][lay_id]
287 name = f"Lay_{real_id}_{lay_type.name[0]}"
288 idx = chunk_id * len(dyn_mem_i) + lay_id
289 stat = self.mb(stat_mem_i[chunk_id][lay_id])
290 dyn = self.mb(dyn_mem_i[chunk_id][lay_id])
291 ax.bar(
292 0.1,
293 [dyn],
294 bottom=bottoms["dyn"],
295 label=name,
296 color=color_dyn(idx / color_denominator),
297 width=0.15,
298 linewidth=0.5,
299 edgecolor="black",
300 )
301 ax.text(
302 0.2,
303 bottoms["dyn"] + dyn / 2,
304 f"DYN_{name}",
305 va="center",
306 fontsize=5,
307 )
308 ax.bar(
309 0.5,
310 [stat],
311 bottom=bottoms["stat"],
312 label=name,
313 color=color_stat(idx / color_denominator),
314 width=0.15,
315 linewidth=0.5,
316 edgecolor="black",
317 )
318 ax.text(
319 0.6,
320 bottoms["stat"] + stat / 2,
321 f"STAT_{name}",
322 va="center",
323 fontsize=5,
324 )
325 bottoms["dyn"] += dyn
326 bottoms["stat"] += stat
327 ax.axhline(
328 self.get_max_device_memory(),
329 color="red",
330 linewidth=1,
331 ls="dotted",
332 )
333 ax.axhline(
334 bottoms["dyn"] + bottoms["stat"],
335 color="blue",
336 linewidth=1,
337 ls="dashed",
338 )
339 ax.text(
340 0,
341 self.get_max_device_memory(),
342 "Device Memory",
343 color="red",
344 va="bottom",
345 fontsize=8,
346 )
347 ax.text(
348 0.62,
349 bottoms["dyn"] + bottoms["stat"],
350 "Prediction total",
351 color="blue",
352 va="bottom",
353 fontsize=8,
354 )
355 ax.set_xticks([])
356 ax.set_ylabel("Size (MB)")
357 ax.set_xlim([0, 0.8])
358 ax.set_xlabel(f"Stage_{i}")
359
360 def __plot_stages(self, stages, stat_mems, dyn_mems) -> None:
361 """plot bars for estimations"""
362
363 logger.info("Plotting predictions in plots/")
364 if not os.path.exists("plots"):
365 os.makedirs("plots")
366 imgs = []
367 for stage_id in range(self._ccfg.p):
368 self.__subplot_stages(
369 stages[stage_id],
370 stat_mems[stage_id],
371 dyn_mems[stage_id],
372 stage_id,
373 )
374 img = f"plots/MemPlot_stage_{stage_id}.png"
375 imgs += [(stage_id, img)]
376 logger.info("save plot: %s", img)
377 plt.savefig(img, dpi=300, bbox_inches="tight")
378 plt.clf()
379 plt.close()
380 # Concatenate every stage plots
381 if imgs:
382 imgs = sorted(imgs)
383 canvas = [Image.open(i) for _, i in imgs]
384 stage_canva = Image.new(
385 "RGB",
386 (
387 canvas[0].size[0]
388 * 2 ** math.ceil(math.log2(len(canvas)) / 2),
389 canvas[0].size[1]
390 * 2 ** math.floor(math.log2(len(canvas)) / 2),
391 ),
392 )
393 offset_x, offset_y = 0, 0
394 for i in canvas:
395 stage_canva.paste(i, (offset_x, offset_y))
396 offset_x += i.size[0]
397 if offset_x >= stage_canva.size[0]:
398 offset_x = 0
399 offset_y += i.size[1]
400 stage_canva.save("plots/MemPlot_all_stages.png")
401 logger.info("save stage plot: plots/MemPlot_all_stages.png")
402
403 def __update_stage_logs(self, stage_logs: list, stage_id: int) -> None:
404 """update stage's temporary buffers from ctx's buffers"""
405 if not stage_logs[stage_id].node_compute_log:
406 stage_logs[stage_id].node_compute_log = {}
407 stage_logs[stage_id].node_compute_log.update(
408 self._ctx.node_compute_log
409 )
410 if not stage_logs[stage_id].accu_mem_type:
411 stage_logs[stage_id].accu_mem_type = {
412 mt: 0 for mt in list(MemType)
413 }
414 for mem_type in list(MemType):
415 val = self._ctx.accu_mem_type[mem_type]
416 stage_logs[stage_id].accu_mem_type[mem_type] += val
417
418 def __order_layer_switches(self, stages: list) -> Optional[list]:
419 """Each regular layer's own recompute switches, in the order the kinds take; None where rec_op holds them."""
420 per_layer = layer_switches(self._ccfg)
421 if per_layer is None:
422 return None
423 order = list(per_layer)
424 if self._ccfg.pp_sched == "zero_bubble_v":
425 first = sum(len(s[0]) for s in stages) - 1
426 order = order[:first] + order[first:][::-1]
427 return order
428
429 def __order_layer_kinds(self, stages: list, kinds: list) -> list:
430 """Check the layers' kinds against the partition, in the order the stages visit them."""
431 flatten = list(kinds)
432 total_n_lay = self._ccfg.n_lay + self._ccfg.n_mtp
433 total_n_lay_stages = self._ccfg.count_layers(stages)
434 if not self._ccfg.multimodal and total_n_lay != total_n_lay_stages:
435 raise(AttributeError(
436 f"Mismatch of num_layers between parsed value ({total_n_lay})"
437 f" and generated partitions ({total_n_lay_stages})"
438 f" => offset may be incorrect ({self._ccfg.offset})"
439 ))
440 if self._ccfg.n_lay > 0 and len(flatten) != total_n_lay:
441 raise AttributeError(
442 f"layer kinds ({len(flatten)})"
443 f" != num_layers ({total_n_lay})"
444 )
445 if self._ccfg.pp_sched == "zero_bubble_v":
446 n_layer_first_chunk = (
447 sum(len(s[0]) for s in stages) - 1
448 ) # Except embedding layer
449 flatten = (
450 flatten[:n_layer_first_chunk]
451 + flatten[n_layer_first_chunk:][::-1]
452 )
453 return flatten
454
455 def _estimate_backbone(self, *args) -> Tuple[list, Dict]:
456 """Evaluator's main function for estimation"""
457 stages = args[0]
458 spec_stage_id = args[3]
459
460 if spec_stage_id >= self._ccfg.p or spec_stage_id < 0:
461 spec_stage_id = -1
462 # Process partition generation
463 if not stages:
464 stages = self._ccfg.generate_partitions_vpp()
465 # multimodal=self._ccfg.multimodal
466 # )
467 self.__name_main_module(self._ccfg)
468 if not self._ccfg.multimodal:
469 return self.__estimate_stages_backbone(
470 stages, args[1], args[2], spec_stage_id, args[4], layer_kinds(self._ccfg)
471 )
472 res = []
473 original_ccfg = self._ccfg
474 kinds = []
475 self.evaluator_instances = []
476 # Build the layers' kinds, submodule after submodule + Build temporary evaluators
477 for m in self._ccfg.mm_order:
478 self._ccfg.mm_ccfgs[m].config = original_ccfg.config
479 if not self._child_cls:
480 raise AttributeError("expected non null _child_cls")
481 tmp_evaluator = type(self._child_cls)(
482 None,
483 ccfg=self._ccfg.mm_ccfgs[m],
484 trace_fun=getattr(self, "toggle_func_trace", False),
485 )
486 tmp_evaluator.import_eval_yaml()
487 num_layer = tmp_evaluator.get_num_layers()
488 # assert not isinstance(num_layer, tuple)
489 strategy = tmp_evaluator.get_strategy()
490 full_rec = strategy["full_rec"]
491 offset = strategy["offset"]
492 # A user hook class names one hook per submodule; built-in
493 # submodules are dispatched by their arch.
494 if self._ccfg.hooks_dict:
495 self._ccfg.hooks_dict[m](tmp_evaluator)
496 else:
497 check_and_apply_custom_hook(tmp_evaluator)
498 strategy = tmp_evaluator.get_strategy()
499 if (
500 tmp_evaluator.get_num_layers() != num_layer
501 or strategy["full_rec"] != full_rec
502 or strategy["offset"] != offset
503 ):
504 # Reverify num layers, recomp, offset after hook
505 stages[m] = (
506 tmp_evaluator.ccfg.generate_partitions_vpp_unimodal()
507 )
508 # Its layers are priced plain, as the hook left them.
509 tmp_evaluator.ccfg.layer_stack = None
510 kinds += layer_kinds(self._ccfg.mm_ccfgs[m])
511 self.evaluator_instances += [tmp_evaluator]
512 if args[1]:
513 logger.info("Submodule %s", self._ccfg.mm_ccfgs[m].model_name)
514 tmp_evaluator.print_ctx()
515 self.print_stages(stages[m])
516 if args[1]:
517 logger.info(
518 "Combined layer kinds for %s\n%s",
519 self._ccfg.model_name,
520 pprint.pformat([kind.name if kind else None for kind in kinds], compact=True),
521 )
522 logger.info(
523 "Sub evaluator instances for %s\n%s",
524 self._ccfg.model_name,
525 pprint.pformat(self.evaluator_instances, compact=True),
526 )
527
528 res = self.__estimate_stages_backbone(
529 self._ccfg.combine_partition_multimodal(stages),
530 args[1],
531 args[2],
532 spec_stage_id,
533 args[4],
534 kinds,
535 )
536 self._ccfg = original_ccfg
537 return res
538
539 @staticmethod
540 def _stage_buffers(stages: list, pipeline: int) -> Dict:
541 """What the layer loop fills, per stage, chunk and layer, and per stage."""
542 def per_layer(value: Any) -> List[List[List[Any]]]:
543 """One *value* per layer of every chunk of every stage."""
544 return [[[value for _ in c] for c in s] for s in stages]
545
546 return {
547 "stat": per_layer(0),
548 "dyn": per_layer(0),
549 "comm": per_layer(0),
550 "micro": per_layer(1),
551 "grad_out": [0 for _ in stages],
552 "root_grad": [0 for _ in stages],
553 "logs": [Config({}) for _ in range(pipeline)],
554 }
555
556 def __estimate_stages_backbone(self, *args) -> Tuple[list, Dict]:
557 """Evaluator's main function for stage estimation"""
558 stages = args[0]
559 verbose = args[1]
560 compute_ppb = args[2]
561 spec_stage_id = args[3]
562
563 if verbose:
564 logger.info("Partition of layers :")
565 self._ccfg.print_stages(stages, spec_stage_id)
566 insights = []
567 # Compute peak memory
568 flatten = self.__order_layer_kinds(stages, args[5])
569 self._switch_order = self.__order_layer_switches(stages)
570 self._ctx.switches = None
571 if verbose:
572 logger.info(
573 "Layer kinds in stage order\n%s",
574 pprint.pformat(
575 [kind.name if kind else None for kind in flatten], compact=True
576 ),
577 )
578
579 stage_misc = self._stage_buffers(stages, self._ccfg.p)
580 # tmp_ppb_lay_desc = [] # PPB purpose
581 ppb_lay_desc = []
582 record_lay_types = {}
583 self.__chunk_stage_lay_loops(
584 flatten,
585 stages,
586 record_lay_types,
587 stage_misc,
588 verbose,
589 compute_ppb,
590 ppb_lay_desc, # tmp_ppb_lay_desc,
591 )
592 self.__postprocess_stages(
593 stages,
594 record_lay_types,
595 stage_misc,
596 verbose,
597 spec_stage_id,
598 insights,
599 )
600 # PPB Input
601 ppb_input = None
602 if compute_ppb:
603 self._ppb_obj.ppb_withdraw_dominated(ppb_lay_desc)
604 self._ppb_obj.ppb_scale_times(ppb_lay_desc)
605 self._ppb_obj.ppb_combine_bodies(ppb_lay_desc)
606 ppb_input = {"layers_description": ppb_lay_desc}
607 if args[4]: # Plot
608 self.__plot_stages(stages, stage_misc["stat"], stage_misc["dyn"])
609 self._switch_order = None
610 self._ctx.switches = None
611 return insights, ppb_input
612
613 def __update_evaluator(self, node, verbose):
614 if self.evaluator_instances and node == self._ctx.head_node:
615 tmp_eval = self.evaluator_instances.pop(0)
616 self._ccfg = tmp_eval.ccfg
617 self._ctx.copy_tmp_buff(tmp_eval.ctx)
618 self._ctx = tmp_eval.ctx
619 if verbose:
620 logger.info(
621 "Update ccfg and ctx, module : %s",
622 self._ccfg.model_name,
623 )
624
625 def __apply_next_layer_kind(self, *args):
626 """Give the next regular layer its kind before evaluating it."""
627 flatten, verbose = args[0], args[1]
628 record_lay_types = args[2]
629 stage_id, chunk_id, lay_id = args[3], args[4], args[5]
630 node = args[6]
631 kind = None
632 if self.is_regular_layer(node) and flatten:
633 kind = flatten.pop(0)
634 # The layer's own recompute switches, which its context carries.
635 switches = None
636 if self.is_regular_layer(node) and self._switch_order:
637 switches = self._switch_order.pop(0)
638 self._ctx.switches = switches
639 record_lay_types[(stage_id, chunk_id, lay_id)] = (
640 self._ccfg,
641 self._ctx,
642 kind,
643 switches,
644 )
645 if kind is not None:
646 if verbose:
647 logger.info("Apply layer kind %s", kind.name)
648 apply_layer_kind(self, kind)
649 if verbose:
650 logger.info(
651 "stage_id=%s, chunk_id=%s, lay_id=%s, node=%s",
652 stage_id,
653 chunk_id,
654 lay_id,
655 node,
656 )
657 self._ccfg.print_parallelism()
658
659 def __chunk_stage_lay_loops(self, *args):
660 """Evaluate every layer in every stage and chunk."""
661 flatten, stages, record_lay_types = args[0], args[1], args[2]
662 sm = args[3]
663 verbose, compute_ppb, ppb_lay_desc = args[4], args[5], args[6]
664 self._ctx.real_lay_ids = []
665 count = 0
666 for chunk_id in range(self._ccfg.vp):
667 self._ctx.real_lay_ids += [[]]
668 for stage_id in range(self._ccfg.p):
669 self._ctx.real_lay_ids[chunk_id] += [[]]
670 self._ctx.init_tmp_buff()
671 for lay_id in range(len(stages[stage_id][chunk_id])):
672 node = stages[stage_id][chunk_id][lay_id]
673 if self.is_regular_layer(node):
674 self._ctx.real_lay_ids[chunk_id][stage_id] += [count]
675 count += 1
676 else:
677 self._ctx.real_lay_ids[chunk_id][stage_id] += [""]
678 # Update evaluator (multimodal)
679 self.__update_evaluator(node, verbose)
680 # Give the next layer its kind
681 self.__apply_next_layer_kind(
682 flatten,
683 verbose,
684 record_lay_types,
685 stage_id,
686 chunk_id,
687 lay_id,
688 node,
689 )
690 self._ctx.current_stage_id = stage_id
691 self._ctx.current_chunk_id = chunk_id
692 self._ctx.current_lay_id = lay_id
693 self._ctx.current_node = node
694 static_mem = self._inner_static_mem()
695 sm["stat"][stage_id][chunk_id][lay_id] = static_mem
696 activation, comm = self._inner_dynamic_mem()
697 sm["dyn"][stage_id][chunk_id][lay_id] = activation + comm
698 sm["comm"][stage_id][chunk_id][lay_id] = comm
699 sm["micro"][stage_id][chunk_id][lay_id] = self._ctx.micro_factor or 1
700 if self.is_regular_layer(node):
701 sm["grad_out"][stage_id] += self._ctx.node_grad
702 elif node == LayerType.OUTPUT_LAYER and self._ccfg.overlaps_grad_reduce:
703 sm["root_grad"][stage_id] += EvalTailSingle.reduced_grad_out_single(self._ccfg, self._ctx)[0]
704 if verbose:
705 logger.info("pp micro factor for dynamic: %s",self._ctx.micro_factor)
706 # PPB Purpose
707 if compute_ppb:
708 desc = self._ppb_obj.lay_ppb(
709 self._ccfg,
710 self._ctx,
711 sm["stat"][stage_id][chunk_id][lay_id],
712 record_lay_types[(stage_id, chunk_id, lay_id)][2],
713 )
714 self._ppb_obj.add_to_ppb_list(ppb_lay_desc, desc)
715 self.__update_stage_logs(sm["logs"], stage_id)
716
717 def __backward_end(self, stages, stage_id, sm, record_lay_types) -> float:
718 """The stage's dynamic memory as a micro-batch's backward ends, or 0.
719
720 FSDP that holds each layer's reduce-scatter output until the backward
721 ends, and accumulates gradients over several micro-batches, then
722 holds the outputs of the stage's layers beside the gradients it
723 accumulated; FSDP that reduces a layer's gradients while the next
724 layer's backward runs holds whole gradients meanwhile, and the
725 root's, the output table's, until the backward ends. Either's peak
726 can come as the backward ends, when that micro-batch keeps nothing
727 but the other micro-batches in flight still do. The root's gathered
728 tables stay, and the stage's first layer runs its backward last,
729 before its own reduce-scatter starts.
730 """
731 held = sm["grad_out"][stage_id] if self._ccfg.defers_grads and self._ccfg.m > 1 else 0
732 if self._ccfg.freeze or not (held or self._ccfg.overlaps_grad_reduce):
733 return 0
734 kept = 0
735 for chunk_id, chunk in enumerate(stages[stage_id]):
736 for lay_id, node in enumerate(chunk):
737 if self.is_regular_layer(node):
738 micro = sm["micro"][stage_id][chunk_id][lay_id]
739 kept += sm["dyn"][stage_id][chunk_id][lay_id] * (micro - 1) / micro
740 else:
741 kept += sm["comm"][stage_id][chunk_id][lay_id]
742 ccfg, ctx = self._ccfg, self._ctx
743 try:
744 working, pending = self._overhead_obj.first_layer_working_set(stages, stage_id, record_lay_types)
745 kept += self.__held_before_last_backward(stages, stage_id, sm, record_lay_types)
746 finally:
747 self._ccfg, self._ctx = ccfg, ctx
748 held = max(0, held - pending) if held else 0
749 return kept + held + working + sm["root_grad"][stage_id]
750
751 def __name_main_module(self, ccfg) -> None:
752 """Name the submodule a model's stages are fitted on, or none for a model of one module.
753
754 A multimodal model's stages hold its submodules' layers together, and
755 a micro-batch's backward ends on the first of them; the moment the
756 main submodule's own first layer runs its backward is the one a stage
757 has to fit.
758 """
759 if not ccfg.multimodal:
760 self.main_module = None
761 return
762 main = getattr(ccfg, "mm_main", None) or ccfg.mm_order[-1]
763 self.main_module = ccfg.mm_ccfgs[main].model_name
764
765 def __held_before_last_backward(self, stages, stage_id, sm, record_lay_types) -> float:
766 """This micro-batch's activations of the layers whose backward has not run at that moment.
767
768 A stage of one module ends a micro-batch's backward on its first
769 layer, with nothing of that micro-batch left to hold. A stage that
770 also holds a vision tower ends it on the tower's first layer, but
771 the moment the language model's first layer runs its backward, every
772 activation the tower kept is still held: that is the moment the
773 stage has to fit, so what the tower keeps of this micro-batch counts
774 beside the other micro-batches'.
775 """
776 if self.main_module is None:
777 return 0
778 held = 0
779 for chunk_id, chunk in enumerate(stages[stage_id]):
780 for lay_id, node in enumerate(chunk):
781 if not self.is_regular_layer(node):
782 continue
783 if record_lay_types[(stage_id, chunk_id, lay_id)][0].model_name == self.main_module:
784 break
785 micro = sm["micro"][stage_id][chunk_id][lay_id]
786 held += sm["dyn"][stage_id][chunk_id][lay_id] / micro
787 return held
788
789 def __postprocess_stages(self, *args):
790 """Build memory insights from raw stage evaluation buffers."""
791 stages, record_lay_types = args[0], args[1]
792 sm = args[2]
793 verbose, spec_stage_id = args[3], args[4]
794 insights = args[5]
795 points = []
796 for stage_id in range(self._ccfg.p):
797 ins = {} # Mem Insights purpose
798 ins["Static"] = sum(
799 sum(mem for mem in c) for c in sm["stat"][stage_id]
800 )
801 ins["Dynamic"] = sum(
802 sum(mem for mem in c) for c in sm["dyn"][stage_id]
803 )
804 backward_end = self.__backward_end(stages, stage_id, sm, record_lay_types)
805 self._ctx.init_tmp_buff()
806 if not self._ccfg.freeze:
807 ins["Dynamic"] += self._overhead_obj.estimate(
808 stages, stage_id, record_lay_types
809 )
810 self.__update_stage_logs(sm["logs"], stage_id)
811 safety_buffer = 1024 * 1024 * 1024 # 1 GB
812 if ins["Dynamic"] > 0:
813 ins["Dynamic"] += safety_buffer
814 points.append((self.mb(ins["Dynamic"]), self.mb(backward_end + safety_buffer) if backward_end else 0))
815 ins["Dynamic"] = max(ins["Dynamic"], backward_end + safety_buffer)
816 else:
817 points.append((self.mb(ins["Dynamic"]), 0))
818 stage_accu = sm["logs"][stage_id].accu_mem_type
819 ins["ModelParameters"] = self.mb(stage_accu[MemType.MODEL_PARAM])
820 ins["OptimizerStates"] = self.mb(stage_accu[MemType.OPTIM_STATE])
821 ins["AccumulGradients"] = self.mb(stage_accu[MemType.ACCU_GRAD])
822 ins["Attn"] = self.mb(stage_accu[MemType.ATTN_ACTIV])
823 ins["FFn"] = self.mb(stage_accu[MemType.FFN_ACTIV])
824 ins["Norm"] = self.mb(stage_accu[MemType.NORM_ACTIV])
825 ins["AllGather Comm"] = self.mb(stage_accu[MemType.AG_COMM])
826 ins["All2All Comm"] = self.mb(stage_accu[MemType.A2A_COMM])
827
828 if self._ccfg.cp > 1:
829 cp_memory = EvalBody.act_cp_layer(self._ccfg, self._ctx)
830 cp_comm_buffer = EvalLayerComm.cp_comm_buffer(self._ccfg, self._ctx)
831
832 ins["CP KV Cache"] = self.mb(cp_memory.kv_cache_memory)
833 ins["CP Attn Scores"] = self.mb(cp_memory.attention_scores_memory)
834 ins["CP Softmax"] = self.mb(cp_memory.softmax_outputs_memory)
835 ins["CP Comm Buffer"] = self.mb(cp_comm_buffer)
836 ins["CP Reduction"] = self.mb(cp_memory.total_reduction)
837
838 ins["Node Log"] = sm["logs"][stage_id].node_compute_log
839 # VERBOSE
840 if verbose and spec_stage_id in (-1, stage_id):
841 self.__verbose_insights(sm, stage_id, ins)
842 ins["Static"] = self.mb(ins["Static"])
843 ins["Dynamic"] = self.mb(ins["Dynamic"])
844 insights += [ins]
845 self.peak_points = points
846
847 def __verbose_insights(self, *args):
848 """logging purpose"""
849 sm = args[0]
850 stage_id = args[1]
851 ins = args[2]
852 stat_i = max(1, ins["Static"])
853 dyn_i = max(1, ins["Dynamic"])
854 # logs_i = sm["logs"][stage_id]
855 accu_i = sm["logs"][stage_id].accu_mem_type
856 logger.info(
857 "stage _%s : %s MB",
858 stage_id,
859 self.mb(ins["Static"] + ins["Dynamic"]),
860 )
861 logger.info(
862 "\tStatic\t%s\t"
863 "ModelParam %s (%s%%), "
864 "OptimStates %s (%s%%), "
865 "Gradients %s (%s%%)",
866 self.mb(ins["Static"]),
867 ins["ModelParameters"],
868 round(accu_i[MemType.MODEL_PARAM] / stat_i * 100),
869 ins["OptimizerStates"],
870 round(accu_i[MemType.OPTIM_STATE] / stat_i * 100),
871 ins["AccumulGradients"],
872 round(accu_i[MemType.ACCU_GRAD] / stat_i * 100),
873 )
874 logger.info(
875 "\tDynamic\t%s\t"
876 "Attn %s (%d%%), "
877 "FFn %s (%d%%), "
878 "Norm %s (%d%%), "
879 "AllGather Comm %s (%d%%), "
880 "All2All Comm %s (%d%%), ",
881 self.mb(ins["Dynamic"]),
882 ins["Attn"],
883 round(accu_i[MemType.ATTN_ACTIV] / dyn_i * 100),
884 ins["FFn"],
885 round(accu_i[MemType.FFN_ACTIV] / dyn_i * 100),
886 ins["Norm"],
887 round(accu_i[MemType.NORM_ACTIV] / dyn_i * 100),
888 ins["AllGather Comm"],
889 round(accu_i[MemType.AG_COMM] / dyn_i * 100),
890 ins["All2All Comm"],
891 round(accu_i[MemType.A2A_COMM] / dyn_i * 100),
892 )
893 logger.info(
894 "\tNode eval log : \n %s \n %s",
895 "> Foreach: (stage_id,chunk_id,lay_id,name) -> (mem type,value)",
896 pprint.pformat(ins["Node Log"], width=300),
897 )
898
899 def apply_kind(self, kind: Any, ccfg: Any = None, ctx: Any = None) -> None:
900 """Evaluate with a recorded layer's config and context, giving it its kind again."""
901 self._ccfg = ccfg if ccfg else self._ccfg
902 self._ctx = ctx if ctx else self._ctx
903 if kind is not None:
904 apply_layer_kind(self, kind)
905
906 def is_regular_layer(self, _):
907 """child implement"""
908 return False
909
910 def import_eval_yaml(self):
911 """child implement"""
912 pass # pylint: disable=unnecessary-pass
913
914 def get_num_layers(self):
915 """child implement"""
916 return 0
917
918 def get_strategy(self):
919 """child implement"""
920 return {}
921
922 def get_max_device_memory(self):
923 """child implement"""
924 return 0
925
926 def print_stages(self, _):
927 """child implement"""
928 pass # pylint: disable=unnecessary-pass
929
930 def print_ctx(self):
931 """child implement"""
932 pass # pylint: disable=unnecessary-pass