Coverage for / home / jenkins / .local / lib / python3.10 / site-packages / hyper_parallel / auto_parallel / sapp_ppb / sapp / sapp_pipeline.py: 91%
427 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-10-25 05:45 +0800
« prev ^ index » next coverage.py v7.13.1, created at 2026-10-25 05:45 +0800
1# Copyright 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"""High-level orchestrator around :class:`SappSolver`: build, solve, simulate, export YAML."""
16import os
17import sys
18from typing import Any, Dict, List, Optional, Union
20import matplotlib.pyplot as plt
21import yaml
23import hyper_parallel.auto_parallel.sapp_ppb.simulator.pp_simulator as sim
24import hyper_parallel.auto_parallel.sapp_ppb.utils.recompute as Recompute
25from hyper_parallel.auto_parallel.sapp_ppb.sapp.sapp_solver import SappSolver
26from hyper_parallel.auto_parallel.sapp_ppb.utils.check_rules import check_yaml_depth_before_loading
27from hyper_parallel.auto_parallel.sapp_ppb.utils.layer import Layer, filter_layer_type
28from hyper_parallel.auto_parallel.sapp_ppb.utils.logger import logger
31class SappPipeline:
32 """pipeline balancer"""
34 def __init__(
35 self,
36 model_name: str,
37 num_of_stage: int,
38 num_of_micro_batch: int,
39 max_memory: int,
40 layers: List[Layer],
41 vpp_less_memory: bool = False,
42 # Add arg dual
43 dual: bool = False,
44 num_of_interleave: int = 1,
45 constant_memory: int = 0,
46 optimization_level: int = 1,
47 extracted_training_params: Optional[Dict[str, int]] = None,
48 seq_split_num: int = 1,
49 use_backward_time: bool = False,
50 ) -> None:
51 """Cache pipeline parameters and index the input ``layers`` by HEAD / BODY / TAIL.
53 Args:
54 model_name (str): Model identifier, used for dump filenames and log prefixes.
55 num_of_stage (int): Number of physical pipeline stages.
56 num_of_micro_batch (int): Number of micro-batches scheduled per iteration.
57 max_memory (int): Per-device memory budget in MB.
58 layers (List[Layer]): Ordered list of layer descriptors covering HEAD/BODY/TAIL.
59 vpp_less_memory (bool, optional): If ``True``, use the less-memory VPP scheduler variant.
60 Default: ``False``.
61 dual (bool, optional): Enable dualpipe-V scheduling support. Default: ``False``.
62 num_of_interleave (int, optional): Virtual-pipeline (VPP) chunk count. Default: ``1``.
63 constant_memory (int, optional): Constant per-stage memory overhead (MB). Default: ``0``.
64 optimization_level (int, optional): Solver optimization level (``0-2``). Default: ``1``.
65 extracted_training_params (Optional[Dict[str, int]], optional): Optional training-config parameters for
66 seqpp. Default: ``None``.
67 seq_split_num (int, optional): Number of sequence splits; ``>1`` enables sequence pipeline.
68 Default: ``1``.
69 """
70 self.model_name_ = model_name
71 self.num_of_stage_ = num_of_stage
72 self.num_of_micro_batch_ = num_of_micro_batch
73 self.num_of_interleave_ = num_of_interleave
74 self.max_memory_ = max_memory
75 self.vpp_less_memory_ = vpp_less_memory
76 # Add arg dual_
77 self.dual_ = dual
78 self.constant_memory_ = constant_memory
79 self.optimization_level = optimization_level
80 self.extracted_training_params_ = extracted_training_params
81 self.seq_split_num_ = seq_split_num
82 self.use_backward_time_ = use_backward_time
83 self.seqpipe_ = self.seq_split_num_ > 1
84 # logger.output("seq chunk: %s",self.seq_split_num_)
86 self.problem_ = None
87 self._simulator: Optional[sim.PipelineSimulator] = None
88 self.layers_ = layers
89 self.layers_sorted_ = {
90 Layer.type_enum.HEAD: filter_layer_type(layers,
91 Layer.type_enum.HEAD),
92 Layer.type_enum.BODY: filter_layer_type(layers,
93 Layer.type_enum.BODY),
94 Layer.type_enum.TAIL: filter_layer_type(layers,
95 Layer.type_enum.TAIL),
96 }
98 @property
99 def simulator(self):
100 """Pipeline simulator instance (available after :meth:`simulate`)."""
101 return self._simulator
103 def has_some_memory_info(self) -> bool:
104 """Check if there is all information for memory constraint."""
105 return self.problem_.has_some_memory_info()
107 def construct_problem(self, solver: str = "pulp") -> None:
108 """Construct the underlying ILP problem using the requested solver backend."""
109 if solver == "pulp":
110 self.problem_ = self._construct_problem_pulp_()
111 elif solver == "other":
112 logger.warning(
113 "No other solver available..., automatically switch to pulp!!!"
114 )
115 self.problem_ = self._construct_problem_pulp_()
116 else:
117 logger.warning(
118 "No other solver available..., automatically switch to pulp!!!"
119 )
120 self.problem_ = self._construct_problem_pulp_()
122 def solve_problem(self, time_limit: int = 90, dump_folder: Optional[str] = None) -> None:
123 """Solve the ILP, optionally dumping the LP model into ``dump_folder``."""
124 self.problem_.solve(time_limit, dump_folder)
126 def get_result(self) -> dict[str, list[list[str]]]:
127 """Get result distribution of the solution (compact form)."""
128 return self.problem_.result()
130 def get_memory_activation(self) -> list[float]:
131 """Get the activation memory per stage for simulator."""
132 return self.problem_.get_simulator_memory_activation()
134 def get_memory_parameter(self) -> list[float]:
135 """Get the parameter memory per stage for simulator."""
136 return self.problem_.get_simulator_memory_parameter()
138 def get_fw_time(self) -> list[float]:
139 """Get the forward time per stage for simulator."""
140 time = self.problem_.get_simulator_forward_time()
141 return time
143 def get_recompute_time(self) -> list[float]:
144 """Get the recompute time per stage for simulator."""
145 time = self.problem_.get_simulator_recompute_time()
146 return time
148 def get_time(self) -> list[float]:
149 """Get the time per stage for simulator."""
150 return self.problem_.get_simulator_time()
152 def naive_layer_per_stage(self,
153 layer_num: int,
154 num_of_interleave: int = 1) -> List[List[int]]:
155 """Return the naive layer-to-stage assignment (``layer_num`` evenly split)."""
156 logger.output("layer_num = %s", layer_num)
157 layer_count = layer_num // (self.num_of_stage_ * num_of_interleave)
158 return [[layer_count] * self.num_of_stage_ for _ in range(num_of_interleave)]
160 def print_yaml_results(self) -> None:
161 """Log the solver output in the MindFormers YAML schema."""
163 for layer in self.layers_sorted_[Layer.type_enum.BODY]:
164 nass = self.naive_layer_per_stage(layer.nb_layer_,
165 self.num_of_interleave_)
166 yaml_format = Recompute.yaml_from_internal(
167 self.num_of_interleave_,
168 self.num_of_stage_,
169 self.problem_.variables_[layer.name_],
170 nass,
171 )
172 logger.output("layer-to-stage assignment baseline is \n\t%s", nass)
173 yaml_results = "\nTo put in yaml configuration:"
174 for y, v in yaml_format.items():
175 yaml_results += f"\n\t{y}: {v}"
176 logger.output(yaml_results)
178 def get_manual_memory_activation(
179 self,
180 each_layer_per_recompute: Dict[Layer, Dict[Recompute.TYPE, List[List[int]]]],
181 interleave_num: int = 1) -> List[List[float]]:
182 """Return the per-stage activation memory for a user-supplied layer assignment."""
183 memory_active = []
184 if self.has_some_memory_info():
185 for inter in range(interleave_num):
186 memory_active.append([])
187 for stage in range(self.num_of_stage_):
188 memory_activation = 0
189 for layer in self.layers_sorted_[Layer.type_enum.BODY]:
190 memory_activation += self._get_layer_memory_activation(
191 each_layer_per_recompute, layer, inter, stage
192 )
193 memory_active[inter].append(memory_activation)
194 return memory_active
196 @staticmethod
197 def _get_layer_memory_activation(each_layer_per_recompute, layer, interleave, stage):
198 """Calculate activation memory for one layer at one pipeline position."""
199 memory_activation = 0
200 unused_recompute_list = Recompute.get_unused_list(each_layer_per_recompute[layer])
201 for rec in Recompute.TYPE:
202 if rec in unused_recompute_list:
203 continue
204 value = each_layer_per_recompute[layer][rec][interleave][stage]
205 if value > 0:
206 memory_activation += value * layer.memory_activation_rec_[rec]
207 return memory_activation
209 def get_manual_memory_parameter(
210 self,
211 each_layer_per_recompute: Dict[Layer, Dict[Recompute.TYPE, List[List[int]]]],
212 interleave_num: int = 1) -> List[List[float]]:
213 """Return the per-stage parameter memory for a user-supplied layer assignment."""
214 memory_param_stage = [0] * self.num_of_stage_
215 for inter in range(interleave_num):
216 for stage in range(self.num_of_stage_):
217 for rec in Recompute.TYPE:
218 for layer in self.layers_sorted_[Layer.type_enum.BODY]:
219 if layer.memory_parameter_ is None:
220 continue
222 if rec in Recompute.get_unused_list(each_layer_per_recompute[layer]):
223 continue
225 value = each_layer_per_recompute[layer][rec][inter][stage]
226 if value <= 0:
227 continue
229 memory_param_stage[stage] += value * layer.memory_parameter_
230 for head in self.layers_sorted_[Layer.type_enum.HEAD]:
231 if head.memory_parameter_ is not None:
232 memory_param_stage[0] += head.memory_parameter_
233 for tail in self.layers_sorted_[Layer.type_enum.TAIL]:
234 if tail.memory_parameter_ is not None:
235 memory_param_stage[self.num_of_stage_ -
236 1] += tail.memory_parameter_
237 memory_param = [memory_param_stage] * interleave_num
238 return memory_param
240 def get_manual_time(
241 self,
242 each_layer_per_recompute: Dict[Layer, Dict[Recompute.TYPE, List[List[int]]]],
243 interleave_num: int = 1) -> List[List[float]]:
244 """Return the per-stage execution time for a user-supplied layer assignment."""
245 time = []
246 for i in range(interleave_num):
247 time.append([])
248 for s in range(self.num_of_stage_):
249 time[i].append(0)
250 for layer in self.layers_sorted_[Layer.type_enum.BODY]:
251 for r in Recompute.TYPE:
252 if each_layer_per_recompute[layer][r][i][s] > 0:
253 time[i][s] += each_layer_per_recompute[layer][r][i][s] * (
254 layer.forward_time_ +
255 layer.backward_time_rec_[r])
257 for head in self.layers_sorted_[Layer.type_enum.HEAD]:
258 time[0][0] += head.forward_time_ + head.backward_time_rec_[Recompute.TYPE.NONE]
259 for tail in self.layers_sorted_[Layer.type_enum.TAIL]:
260 time[interleave_num - 1][self.num_of_stage_ - 1] += (
261 tail.forward_time_
262 + tail.backward_time_rec_[Recompute.TYPE.NONE]
263 )
264 return time
266 def get_manual_fw_time(
267 self,
268 each_layer_per_recompute: Dict[Layer, Dict[Recompute.TYPE, List[List[int]]]],
269 interleave_num: int = 1) -> List[List[float]]:
270 """Return the per-stage forward time for a user-supplied layer assignment."""
271 time = []
272 for i in range(interleave_num):
273 time.append([])
274 for s in range(self.num_of_stage_):
275 time[i].append(0)
276 for layer in self.layers_sorted_[Layer.type_enum.BODY]:
277 for r in Recompute.TYPE:
278 if (r not in Recompute.get_unused_list(each_layer_per_recompute[layer])
279 and each_layer_per_recompute[layer][r][i][s] > 0):
280 time[i][s] += each_layer_per_recompute[layer][r][i][s] * (
281 layer.forward_time_)
282 for head in self.layers_sorted_[Layer.type_enum.HEAD]:
283 time[0][0] += head.forward_time_
284 for tail in self.layers_sorted_[Layer.type_enum.TAIL]:
285 time[interleave_num - 1][self.num_of_stage_ - 1] += tail.forward_time_
286 return time
288 def get_manual_backward_time(
289 self,
290 each_layer_per_recompute: Dict[Layer, Dict[Recompute.TYPE, List[List[int]]]],
291 interleave_num: int = 1) -> List[List[float]]:
292 """Return the per-stage backward time for a user-supplied layer assignment."""
293 time = []
294 for i in range(interleave_num):
295 time.append([])
296 for s in range(self.num_of_stage_):
297 time[i].append(0)
298 for layer in self.layers_sorted_[Layer.type_enum.BODY]:
299 for r in Recompute.TYPE:
300 if (r not in Recompute.get_unused_list(each_layer_per_recompute[layer])
301 and each_layer_per_recompute[layer][r][i][s] > 0):
302 time[i][s] += each_layer_per_recompute[layer][r][i][s] * (
303 layer.backward_time_rec_[r])
304 for head in self.layers_sorted_[Layer.type_enum.HEAD]:
305 time[0][0] += head.backward_time_rec_[Recompute.TYPE.NONE]
306 for tail in self.layers_sorted_[Layer.type_enum.TAIL]:
307 time[interleave_num - 1][self.num_of_stage_ - 1] += (
308 tail.backward_time_rec_[Recompute.TYPE.NONE]
309 )
310 return time
312 def get_manual_recompute_time(
313 self,
314 each_layer_per_recompute: Dict[Layer, Dict[Recompute.TYPE, List[List[int]]]],
315 interleave_num: int = 1) -> List[List[float]]:
316 """Return the per-stage recompute-only time for a user-supplied layer assignment."""
317 logger.output("each_layer_per_recompute = %s", each_layer_per_recompute)
318 time_all_rec = []
319 time_no_rec = []
320 for i in range(interleave_num):
321 time_all_rec.append([])
322 time_no_rec.append([])
323 for s in range(self.num_of_stage_):
324 time_all_rec[i].append(0)
325 time_no_rec[i].append(0)
326 for layer in self.layers_sorted_[Layer.type_enum.BODY]:
327 self._add_manual_recompute_time(
328 each_layer_per_recompute, layer, i, s, time_all_rec, time_no_rec)
330 return [[r - n for r, n in zip(ar, nr)]
331 for ar, nr in zip(time_all_rec, time_no_rec)]
333 def _add_manual_recompute_time(self, each_layer_per_recompute, layer, interleave, stage,
334 time_all_rec, time_no_rec):
335 """Accumulate recompute time for a single layer and stage."""
336 logger.output("backward_time_rec_(%s) = %s", layer, layer.backward_time_rec_)
337 unused_rec = Recompute.get_unused_list(each_layer_per_recompute[layer])
338 for rec in Recompute.TYPE:
339 layer_num = each_layer_per_recompute[layer][rec][interleave][stage]
340 if rec in unused_rec or layer_num <= 0:
341 continue
342 if layer.backward_time_rec_[rec] is None:
343 raise ValueError("No backward tme is specified for this "
344 "recomputation. Recomputation "
345 f"'{Recompute.YAML_NAME[rec]}' is likely not considered")
346 logger.output("r = %s; i = %s; s = %s", rec, interleave, stage)
347 time_all_rec[interleave][stage] += layer_num * layer.backward_time_rec_[rec]
348 time_no_rec[interleave][stage] += layer_num * layer.backward_time_rec_[Recompute.TYPE.NONE]
350 def simulate(self, show: bool = True, file_name: Optional[str] = None,
351 sub_fig: Optional[plt.Figure] = None, comm_time: float = 0.0) -> float:
352 """Run the simulator on the solved schedule and return its estimated total time."""
353 forward_time = self.get_fw_time()
354 recompute_overhead = self.get_recompute_time()
355 backward_time = self.problem_.get_simulator_backward_time() if self.use_backward_time_ else 0
356 stage_mem_par = 0
357 stage_mem_act = 0
358 if self.has_some_memory_info():
359 stage_mem_par = self.get_memory_parameter()
360 stage_mem_act = self.get_memory_activation()
362 return self.simulation(
363 forward_time,
364 recompute_overhead,
365 stage_mem_par,
366 stage_mem_act,
367 self.constant_memory_,
368 backward_time=backward_time,
369 show=show,
370 file_name=file_name,
371 sub_fig=sub_fig,
372 comm_time=comm_time,
373 )
375 def simulate_naive(self, layers: List[Layer], output_folder: str) -> None:
376 """Simulate the naive (even) layer-to-stage assignments for sanity comparison."""
377 num_layers = 0
378 rec_considered = {}
379 for layer in layers:
380 if layer.type_ == Layer.type_enum.BODY:
381 num_layers = layer.nb_layer_
382 rec_considered = layer.recompute_considered_
384 all_recomp = {"offset": 0}
385 no_recomp = {"offset": 0}
386 for rec in [Recompute.TYPE.FULL, Recompute.TYPE.SLCT, Recompute.TYPE.COMM]:
387 if rec_considered.get(rec, False):
388 all_recomp[Recompute.YAML_NAME[rec]] = True
389 no_recomp[Recompute.YAML_NAME[rec]] = False
391 self.simulate_yaml(
392 yaml_format=all_recomp,
393 show=True,
394 interleave_num=self.num_of_interleave_,
395 file_name=os.path.join(output_folder,
396 "result_naive_all_recomp.svg"),
397 )
399 if num_layers % self.num_of_stage_ == 0:
400 self.simulate_yaml(
401 yaml_format=no_recomp,
402 show=True,
403 interleave_num=self.num_of_interleave_,
404 file_name=os.path.join(output_folder,
405 "result_naive_no_recomp.svg"),
406 )
407 else:
408 logger.warning("num layer cannot be divided by num stage")
410 def simulate_comparison(self, manual_config_file: str, output_folder: str) -> None:
411 """Render side-by-side automatic vs manual simulations for every entry in the YAML."""
412 with open(manual_config_file, encoding="utf-8") as fp:
413 check_yaml_depth_before_loading(fp)
414 fp.seek(0)
415 data = yaml.safe_load(fp)
416 yaml_data = {}
417 for manual in data.values():
418 yaml_data[Recompute.OFFSET] = manual.get(Recompute.OFFSET)
419 if isinstance(yaml_data[Recompute.OFFSET], list) and all(
420 isinstance(item, int) for item in yaml_data[Recompute.OFFSET]):
421 yaml_data[Recompute.OFFSET] = [yaml_data[Recompute.OFFSET]]
423 for rec in Recompute.YAML_NAME.values():
424 yaml_data[rec] = manual.get(rec)
425 if isinstance(yaml_data[rec], list) and all(
426 isinstance(item, int) for item in yaml_data[rec]):
427 yaml_data[rec] = [yaml_data[rec]]
428 interleave_num = manual.get("interleave_num",
429 self.num_of_interleave_)
430 show = manual.get("show", False)
431 file_name = manual.get("file_name")
432 full_file_name = os.path.join(output_folder,
433 file_name) if (file_name) else None
435 fig = plt.figure(figsize=(24, 8))
436 sub_figs = fig.subfigures(1, 2, wspace=0.07)
437 sub_figs[0].suptitle('Automatic', fontsize='x-large')
438 try:
439 simulate_result = self.simulate(
440 show=False,
441 file_name=os.path.join(output_folder, "Auto_" + file_name),
442 sub_fig=sub_figs[0],
443 )
444 except Exception:
445 logger.exception("Failed to simulate auto pipeline.")
446 raise
448 if simulate_result is None:
449 raise RuntimeError("simulate() returned None.")
451 sub_figs[1].suptitle('Manual', fontsize='x-large')
452 self.simulate_yaml(yaml_data, False, interleave_num, full_file_name, sub_figs[1])
453 plt.savefig(os.path.join(output_folder, "Comparison_" + file_name))
454 if show:
455 plt.show()
457 def simulate_only_manual(self, manual_config_file: str, output_folder: str) -> None:
458 """Render only the manual simulation for every entry in ``manual_config_file``."""
459 with open(manual_config_file, encoding="utf-8") as fp:
460 check_yaml_depth_before_loading(fp)
461 fp.seek(0)
462 data = yaml.safe_load(fp)
463 yaml_data = {}
464 for manual in data.values():
465 yaml_data[Recompute.OFFSET] = manual.get(Recompute.OFFSET)
466 if isinstance(yaml_data[Recompute.OFFSET], list) and all(
467 isinstance(item, int) for item in yaml_data[Recompute.OFFSET]):
468 yaml_data[Recompute.OFFSET] = [yaml_data[Recompute.OFFSET]]
470 for rec in Recompute.YAML_NAME.values():
471 yaml_data[rec] = manual.get(rec)
472 if isinstance(yaml_data[rec], list) and all(
473 isinstance(item, int) for item in yaml_data[rec]):
474 yaml_data[rec] = [yaml_data[rec]]
475 interleave_num = manual.get("interleave_num",
476 self.num_of_interleave_)
477 show = manual.get("show", False)
478 file_name = manual.get("file_name")
479 full_file_name = os.path.join(output_folder,
480 file_name) if (file_name) else None
482 fig = plt.figure(figsize=(12, 8))
483 self.simulate_yaml(yaml_data, False, interleave_num, full_file_name, fig)
484 plt.savefig(os.path.join(output_folder, "manual_file_" + file_name))
485 if show:
486 plt.show()
488 def simulate_yaml(self, yaml_format: Dict[str, Any], show: bool = True,
489 interleave_num: int = 1,
490 file_name: Optional[str] = None,
491 sub_fig: Optional[plt.Figure] = None) -> float:
492 """Simulate a manual pipeline configuration encoded as a YAML-compatible dict."""
493 layer_num = 0
494 for layer in self.layers_sorted_[Layer.type_enum.BODY]:
495 layer_num += layer.nb_layer_
496 nass = self.naive_layer_per_stage(layer_num,
497 num_of_interleave=interleave_num)
498 layer_per_recompute = Recompute.internal_from_yaml(
499 interleave_num, self.num_of_stage_, yaml_format, nass)
500 each_layer_per_recompute = self.split_layer_per_recompute(layer_per_recompute)
501 return self.simulate_manual(
502 each_layer_per_recompute,
503 show,
504 interleave_num=interleave_num,
505 file_name=file_name,
506 sub_fig=sub_fig
507 )
509 #######################################################################
510 ## ##
511 ## Print Solver Model ##
512 ## ##
513 #######################################################################
514 def _calculate_activation_memory(self, each_layer_per_recompute, v, s):
515 """Calculate activation memory for next and current stage"""
516 act_mem_next = 0
517 act_mem_curr = 0
519 for layer in self.layers_sorted_[Layer.type_enum.BODY]:
520 for rec in Recompute.TYPE:
521 if self.problem_.recompute_considered_[rec]:
522 if each_layer_per_recompute[layer][rec][v + 1][s] > 0: # next
523 act_mem_next += (each_layer_per_recompute[layer][rec][v + 1][s] *
524 layer.memory_activation_rec_[rec])
525 if each_layer_per_recompute[layer][rec][v][s] > 0: # current
526 act_mem_curr += (each_layer_per_recompute[layer][rec][v][s] *
527 layer.memory_activation_rec_[rec])
529 return act_mem_next, act_mem_curr
531 def _compute_parameter_memory_manually_solver(self, each_layer_per_recompute, s, interleave_num=1):
532 """Solver memory model: parameter memory"""
533 param_mem = 0
534 for layer in self.layers_sorted_[Layer.type_enum.BODY]:
535 if layer.memory_parameter_ is not None:
536 param_mem += self._calculate_layer_parameter_memory(
537 layer, each_layer_per_recompute[layer], s, interleave_num)
538 return param_mem
540 def _calculate_layer_parameter_memory(self, layer, layer_per_recompute, s, interleave_num):
541 """Calculate parameter memory for a single layer"""
542 layer_mem = 0
543 for inter in range(interleave_num):
544 for rec in Recompute.TYPE:
545 if self.problem_.recompute_considered_[rec]:
546 if layer_per_recompute[rec][inter][s] > 0:
547 layer_mem += layer_per_recompute[rec][inter][s] * layer.memory_parameter_
548 return layer_mem
550 def _calculate_activation_memory_solver(self, each_layer_per_recompute, s, interleave_num, activation_nums):
551 """Calculate activation memory for a given stage"""
552 act_mem = 0
553 for layer in self.layers_sorted_[Layer.type_enum.BODY]:
554 for inter in range(interleave_num):
555 for rec in Recompute.TYPE:
556 if self.problem_.recompute_considered_[rec]:
557 if each_layer_per_recompute[layer][rec][inter][s] > 0:
558 act_mem += (each_layer_per_recompute[layer][rec][inter][s] *
559 layer.memory_activation_rec_[rec] *
560 activation_nums[inter][s])
561 return act_mem
564 def debug_print_manual_theoretical_memory(
565 self,
566 each_layer_per_recompute: Dict[Layer, Dict[Recompute.TYPE, List[List[int]]]],
567 interleave_num: int = 1) -> None:
568 """Log the per-stage theoretical memory implied by the solver model (debug aid)."""
569 logger.info("%s Manual Theoretical Memory Analysis %s", "=" * 20, "=" * 20)
571 if self.vpp_less_memory_:
572 if self.seqpipe_:
573 activation_nums = self.problem_.compute_activation_seq_nums(
574 self.num_of_stage_, interleave_num, self.seq_split_num_, self.num_of_micro_batch_, True)
575 else:
576 activation_nums = self.problem_.compute_less_activation_nums(
577 self.num_of_stage_, interleave_num)
578 # Add if dual to decide whether dualpipe_v is used
579 elif self.dual_:
580 activation_nums = self.problem_.compute_activation_nums_dual(
581 self.num_of_stage_, interleave_num, self.num_of_micro_batch_)
582 else:
583 if self.seqpipe_:
584 activation_nums = self.problem_.compute_activation_seq_nums(
585 self.num_of_stage_, interleave_num, self.seq_split_num_, self.num_of_micro_batch_, False)
586 else:
587 activation_nums = self.problem_.compute_activation_nums(
588 self.num_of_stage_, interleave_num, self.num_of_micro_batch_)
590 logger.info("Activation nums = %s", activation_nums)
592 # compute for each stage
593 for s in range(self.num_of_stage_):
595 # parameter memory
596 param_mem = self._compute_parameter_memory_manually_solver(each_layer_per_recompute, s, interleave_num)
598 # head memory
599 if s == 0:
600 for head in self.layers_sorted_[Layer.type_enum.HEAD]:
601 if head.memory_parameter_ is not None:
602 param_mem += head.memory_parameter_
604 # tail memory
605 if s == self.num_of_stage_ - 1:
606 for tail in self.layers_sorted_[Layer.type_enum.TAIL]:
607 if tail.memory_parameter_ is not None:
608 param_mem += tail.memory_parameter_
610 # act memory
611 act_mem = self._calculate_activation_memory_solver(each_layer_per_recompute, s,
612 interleave_num, activation_nums)
614 # overhead
615 overhead = 0
617 total = param_mem + act_mem + overhead + self.constant_memory_
619 logger.info("Stage %d Manual Memory Analysis:", s)
620 logger.info("Parameter Memory: %.2f", param_mem)
621 logger.info("Activation Memory: %.2f", act_mem)
622 logger.info("Memory Overhead: %.2f", overhead)
623 logger.info("Constant Memory: %.2f", self.constant_memory_)
624 logger.info("Total Theoretical Memory: %.2f", total)
626 def split_layer_per_recompute(
627 self,
628 layer_per_recompute: Dict[Recompute.TYPE, List[List[int]]]
629 ) -> Dict[Layer, Dict[Recompute.TYPE, List[List[int]]]]:
630 """Split aggregate per-recompute layer counts into counts per BODY layer."""
631 each_layer_per_recompute = {}
632 for layer in self.layers_sorted_[Layer.type_enum.BODY]:
633 rest = layer.nb_layer_
634 each_layer_per_recompute[layer] = {r: [] for r in Recompute.TYPE}
635 for rec in Recompute.TYPE:
636 for i in range(self.num_of_interleave_):
637 each_layer_per_recompute[layer][rec].append([0]*self.num_of_stage_)
638 for s in range(self.num_of_stage_):
639 subtract = min(layer_per_recompute[rec][i][s], rest)
640 layer_per_recompute[rec][i][s] -= subtract
641 rest -= subtract
642 each_layer_per_recompute[layer][rec][i][s] += subtract
643 return each_layer_per_recompute
645 def fuse_layer_per_recompute(
646 self,
647 each_layer_per_recompute: Dict[Layer, Dict[Recompute.TYPE, List[List[int]]]]
648 ) -> Dict[Recompute.TYPE, List[List[int]]]:
649 """Fuse per-layer recompute counts back into aggregate per-recompute-type totals."""
650 all_layers_per_recompute = {r: [] for r in Recompute.TYPE}
651 for rec in Recompute.TYPE:
652 for i in range(self.num_of_interleave_):
653 all_layers_per_recompute[rec].append([])
654 for s in range(self.num_of_stage_):
655 all_layers_per_recompute[rec][i].append(sum(
656 each_layer_per_recompute[layer][rec][i][s]
657 for layer in self.layers_sorted_[Layer.type_enum.BODY]
658 ))
659 return all_layers_per_recompute
662 def simulate_manual(
663 self,
664 each_layer_per_recompute: Optional[Dict[Layer, Dict[Recompute.TYPE, List[List[int]]]]] = None,
665 show: bool = True,
666 interleave_num: int = 1,
667 file_name: Optional[str] = None,
668 sub_fig: Optional[plt.Figure] = None) -> float:
669 """Run the simulator on a user-supplied per-layer recompute strategy."""
670 logger.output("Simulating given strategy: %s", each_layer_per_recompute)
672 for layer in self.layers_sorted_[Layer.type_enum.BODY]:
673 for rec in Recompute.TYPE:
674 if len(each_layer_per_recompute[layer][rec]) != interleave_num:
675 logger.error(
676 "For layer %s with recompute %s, %s does not match interleave number %s",
677 layer,
678 rec,
679 len(each_layer_per_recompute[layer][rec]),
680 interleave_num,
681 )
682 return sys.maxsize
684 for layer in self.layers_sorted_[Layer.type_enum.BODY]:
685 for rec in Recompute.TYPE:
686 if any(x < 0 for sublist in each_layer_per_recompute[layer][rec]
687 for x in sublist):
688 raise ValueError(
689 f"for {rec}, there is strategy less than 0 in "
690 f"{each_layer_per_recompute[layer][rec]}"
691 )
693 forward_time = self.get_manual_fw_time(each_layer_per_recompute,
694 interleave_num)
695 recompute_overhead = self.get_manual_recompute_time(
696 each_layer_per_recompute, interleave_num)
697 backward_time = (
698 self.get_manual_backward_time(
699 each_layer_per_recompute, interleave_num)
700 if self.use_backward_time_
701 else 0
702 )
703 stage_mem_par = 0
704 stage_mem_act = 0
705 if self.has_some_memory_info():
706 stage_mem_par = self.get_manual_memory_parameter(
707 each_layer_per_recompute, interleave_num=interleave_num)
708 stage_mem_act = self.get_manual_memory_activation(
709 each_layer_per_recompute, interleave_num=interleave_num)
711 self.debug_print_manual_theoretical_memory(each_layer_per_recompute, interleave_num)
713 return self.simulation(
714 forward_time,
715 recompute_overhead,
716 stage_mem_par,
717 stage_mem_act,
718 constant_mem=self.constant_memory_,
719 backward_time=backward_time,
720 show=show,
721 file_name=file_name,
722 sub_fig=sub_fig,
723 comm_time=0.0,
724 )
726 def simulation(
727 self,
728 forward_time: List[List[float]],
729 recompute_overhead: Union[int, List[List[float]]] = 0,
730 stage_mem_par: Union[int, List[List[float]]] = 0,
731 stage_mem_act: Union[int, List[List[float]]] = 0,
732 constant_mem: int = 0,
733 backward_time: Union[int, List[List[float]]] = 0,
734 show: bool = True,
735 file_name: Optional[str] = None,
736 sub_fig: Optional[plt.Figure] = None,
737 comm_time: float = 0.0,
738 ) -> float:
739 """Run the low-level :class:`PipelineSimulator` and return its reported end time."""
740 use_comm = comm_time > 0.0
741 if self.has_some_memory_info():
742 logger.output(
743 "PipelineSimulator(\n\t%s, %s,"
744 "\n\tblock_mem_act=%s,"
745 "\n\tblock_mem_par=%s,"
746 "\n\tlayer_recompute=%s,"
747 "\n\tbackward_time=%s,"
748 "\n\tless_memory=%s )",
749 forward_time,
750 self.num_of_micro_batch_,
751 stage_mem_act,
752 stage_mem_par,
753 recompute_overhead,
754 backward_time,
755 self.vpp_less_memory_,
756 )
758 sim_method = "vpp2" if self.vpp_less_memory_ else "vpp"
759 simulator = sim.PipelineSimulator(
760 forward_time,
761 self.num_of_micro_batch_,
762 comm_time=comm_time,
763 block_mem=stage_mem_act,
764 block_mem_par=stage_mem_par,
765 constant_mem=constant_mem,
766 layer_recompute=recompute_overhead,
767 backward_time=backward_time,
768 method=sim_method,
769 sub_fig=sub_fig
770 )
771 else:
772 logger.output(
773 "PipelineSimulator(\n\t%s, %s,"
774 "\n\tlayer_recompute=%s,"
775 "\n\tbackward_time=%s,"
776 "\n\tless_memory=%s )",
777 forward_time,
778 self.num_of_micro_batch_,
779 recompute_overhead,
780 backward_time,
781 self.vpp_less_memory_,
782 )
783 simulator = sim.PipelineSimulator(
784 forward_time,
785 self.num_of_micro_batch_,
786 comm_time=comm_time,
787 layer_recompute=recompute_overhead,
788 backward_time=backward_time,
789 less_memory=self.vpp_less_memory_,
790 sub_fig=sub_fig
791 )
793 simulator.run(comm=use_comm)
794 self._simulator = simulator
795 if file_name:
796 simulator.save(file_name)
797 if show:
798 simulator.show()
799 return simulator.end_time
801 def _construct_problem_pulp_(self) -> SappSolver:
802 """construct the problem using pulp"""
803 prob = SappSolver(
804 num_of_stage=self.num_of_stage_,
805 num_of_micro_batch=self.num_of_micro_batch_,
806 num_of_interleave=self.num_of_interleave_,
807 max_memory=self.max_memory_,
808 vpp_less_memory=self.vpp_less_memory_,
809 # Add arg dual
810 dual = self.dual_,
811 constant_memory=self.constant_memory_,
812 layers=self.layers_,
813 layers_sorted=self.layers_sorted_,
814 optimization_level=self.optimization_level,
815 extracted_training_params=self.extracted_training_params_,
816 seq_split_num=self.seq_split_num_
817 )
818 return prob
820 def _recompute_considered(self):
821 return self.problem_.recompute_considered_
824def choose_interleave(
825 model_name: str,
826 number_of_stage: int,
827 number_of_micro_batch: int,
828 max_memory: int,
829 layers: list[Layer],
830) -> tuple[int, int, dict[str, list[list[str]]]]:
831 """Simulates different interleaves and returns the best."""
832 max_inter = 4
833 best_time = int(sys.maxsize)
834 best_inter = 1
835 best_distribution = {}
837 for inter in range(1, max_inter + 1):
838 pipe = SappPipeline(
839 model_name=model_name,
840 num_of_stage=number_of_stage,
841 num_of_micro_batch=number_of_micro_batch,
842 max_memory=max_memory,
843 layers=layers,
844 num_of_interleave=inter,
845 )
847 pipe.construct_problem(solver="pulp")
848 pipe.solve_problem()
849 time = pipe.simulate(show=False)
850 logger.output("for interleave %s, time = %s", inter, time)
851 if time < best_time:
852 best_time = time
853 best_inter = inter
854 best_distribution = pipe.get_result()
856 return (best_inter, best_time, best_distribution)
859def flatten(inter_stage_list: List[List[float]]) -> List[float]:
860 """Collapse an ``[interleave][stage]`` matrix into a per-stage list via summation."""
861 stage_list = [0] * len(inter_stage_list[0])
862 for inter, _ in enumerate(inter_stage_list):
863 for stage, _ in enumerate(inter_stage_list[inter]):
864 stage_list[stage] += inter_stage_list[inter][stage]
865 return stage_list