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