Coverage for  / home / jenkins / .local / lib / python3.10 / site-packages / hyper_parallel / auto_parallel / sapp_ppb / simulator / pp_simulator.py: 80%

313 statements  

« 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"""Pipeline scheduler simulator: builds block dependencies, computes bubbles and peak memory.""" 

16from __future__ import annotations 

17 

18import copy 

19import sys 

20 

21import numpy as np 

22 

23from hyper_parallel.auto_parallel.sapp_ppb.simulator.causal_error import CausalCommError, CausalError 

24from hyper_parallel.auto_parallel.sapp_ppb.simulator.pipeline_builder import PipelineBuilder 

25from hyper_parallel.auto_parallel.sapp_ppb.simulator.plot_manager import PlotMgr 

26from hyper_parallel.auto_parallel.sapp_ppb.simulator.sim_block import BlockSim, RecBlockSim, SendBlockSim 

27from hyper_parallel.auto_parallel.sapp_ppb.simulator.utils import apply_color, apply_format, format_2d_inputs 

28from hyper_parallel.auto_parallel.sapp_ppb.utils.logger import logger 

29 

30sys.setrecursionlimit(8192) 

31 

32 

33class PipelineSimulator: 

34 r""" 

35 Pipeline Simulator which provide pipeline flow process, bubbles and relative memories for stages. 

36 

37 Args: 

38 block_time (Union[List[int|float], List[List[int|float]]]): Relative forward computing time for each block. 

39 If it is List of List, the outer List indicates number of virtual-pp 

40 while the inner List indicates pp_stage. 

41 micro_num (int): Micro batch number. 

42 comm_time (float, optional): Communication block (send/receive) time. Default: ``0.1``. 

43 layer_recompute (Union[bool, List[int|float], List[List[int|float]]], optional): The block recompute 

44 information. 

45 If it is bool type, the backward block will be extended by block_time depending on whether it is True. 

46 Otherwise it represents relative computing time of recompute for each block. Default: ``False``. 

47 block_mem (Union[bool, List[int|float], List[List[int|float]]], optional): The block memory information. 

48 If it is a number, the memory will be `block_mem` * `block_time`. Otherwise it represents relative memory 

49 for each block. Default: ``1``. 

50 backward_ratio (Union[List[int|float], List[List[int|float]]], optional): The ratios of backward computing 

51 time and forward computing time for each block. Default: ``2``. 

52 

53 Example: 

54 A PipelineSimulator with pp=4, micro=16, each stage has 8 layers and last stage has extra head and 

55 loss computation equivalent to 0.8 layer: 

56 >>> sim = PipelineSimulator([8,8,8,8+0.8], 16, comm_time=0.1) # create an instance of PipelineSimulator 

57 >>> sim.run() # run simulation to scheduler the pipeline (information will be automatically printed) 

58 ————————————— pp: 4, vp: 1, micro: 16 ———————————— 

59 -------------------- bubble -------------------- 

60 real = ideal + imba + comm 

61 0.2658 = 0.1875 + 0.0615 + 0.0168 

62 -------------------- memory -------------------- 

63 peak memory: 32.00, 24.00, 16.00, 8.80 

64 >>> sim.show() # draw the pipeline and memory timeline picture 

65 

66 Show imbalance timeline of vp=2, pp=4, micro=8, total 16 layers with extra equivalent 1.2 layer: 

67 >>> PipelineSimulator([[2,2,2,2],[1,2,3,2+1.2]], 8, comm_time=0.1).run().show() 

68 ————————————— pp: 4, vp: 2, micro: 8 ———————————— 

69 -------------------- bubble -------------------- 

70 real = ideal + imba + comm 

71 0.4971 = 0.1875 + 0.2447 + 0.0649 

72 -------------------- memory -------------------- 

73 peak memory: 18.00, 18.00, 18.00, 14.80 

74 

75 Show timeline of vp=3, pp=8, micro=16, total 48 layers with extra equivalent 0.6 layer. 

76 some of layers are recomputed and set memory correspondingly: 

77 >>> PipelineSimulator([[2,2,2,2,2,2,2,2], 

78 >>> [2,2,2,2,2,2,2,2], 

79 >>> [2,2,2,2,2,2,2,2+0.6]], 16, 0.1, 

80 >>> [[0,0,0,0,0,0,0,0], 

81 >>> [1,0,0,0,0,0,0,0], 

82 >>> [2,2,1,0,0,0,0,0]], 

83 >>> [[2,2,2,2,2,2,2,2], 

84 >>> [1.1,2,2,2,2,2,2,2], 

85 >>> [0.2,0.2,1.1,2,2,2,2,2]]).run().show() 

86 ————————————— pp: 8, vp: 3, micro: 16 ———————————— 

87 -------------------- bubble -------------------- 

88 real = ideal + imba + comm + recompute 

89 0.4444 = 0.1458 + 0.1851 + 0.0724 + 0.0412 

90 -------------------- memory -------------------- 

91 peak memory: 40.40, 43.60, 46.80, 50.00, 46.00, 42.00, 38.00, 34.00 

92 

93 Show timeline without comm for vp=2, pp=15, micro=16, total 96 layers with extra equivalent 1.2 layer: 

94 >>> PipelineSimulator([[3,3,3,3,3,3,3,3,3,3,3,3,3,3,3], 

95 >>> [3,3,3,3,3,3,3,3,4,4,4,4,4,4,3+1.2]], 16).run(False).show() 

96 ————————————— pp:15, vp: 2, micro: 16 ———————————— 

97 -------------------- bubble -------------------- 

98 real = ideal + imba 

99 0.5741 = 0.4375 + 0.1366 

100 -------------------- memory -------------------- 

101 peak memory: 96.00, 96.00, 96.00, 96.00, 96.00, 96.00, 96.00, 93.00, 103.00, 97.00, 91.00, 

102 85.00, 79.00, 73.00, 70.20 

103 """ 

104 def __init__(self, block_time: list, micro_num: int, *args: object, 

105 comm_time: float = 0.1, 

106 layer_recompute: object = False, block_mem: object = 1, 

107 block_mem_par: object = 0, constant_mem: float = 0, 

108 backward_ratio: object = 2., 

109 sub_fig: object = None, **kwargs: object) -> None: 

110 """Delegate initialisation to :meth:`init` (kept as a named method for subclassing).""" 

111 self.init(block_time, micro_num, comm_time, layer_recompute, block_mem, 

112 block_mem_par, constant_mem, backward_ratio, sub_fig, *args, **kwargs) 

113 

114 # pylint: disable=W0613 

115 def init(self, block_time: list, micro_num: int, comm_time: float, 

116 layer_recompute: object, block_mem: object, block_mem_par: object, 

117 constant_mem: float, backward_ratio: object, 

118 sub_fig: object, *args: object, **kwargs: object) -> None: 

119 """Build the block grid, statistics and communication graph for the simulator.""" 

120 self.micro_num = micro_num 

121 self.pp, self.vp = self._base_init(block_time) 

122 self.block_num = 2 * self.vp * self.micro_num 

123 self.comm_time = comm_time 

124 self._input_format(block_time, layer_recompute, block_mem, block_mem_par, backward_ratio) 

125 self.constant_mem = constant_mem 

126 self._statistic_init() 

127 self._comm = True 

128 self.adjust_func_list = [self.swap_send_rec] 

129 self.sub_fig = sub_fig 

130 # Construct pipeline blocks 

131 if self.vp == 1: 

132 method = '1f1b' 

133 else: 

134 method = kwargs.get('method', 'vpp') 

135 if self.micro_num >= self.pp: 

136 self.adjust_func_list = [self.vpp_send_delay, self.residue_delay] + self.adjust_func_list 

137 pp_builder = PipelineBuilder.get_builder(method) 

138 self.blocks = [pp_builder(self.pp, self.micro_num, self.vp, p, self.block_time[:, p], 

139 self.backward_time[:, p], self.block_mem[:, p], self.block_mem_par[:, p]) 

140 for p in range(self.pp)] 

141 

142 self._build_block() # create connection among compute blocks 

143 self._build_comm_block() # create comm blocks for each compute block 

144 self.peak_memory = None 

145 self.end_time = None 

146 self.lines = None 

147 self.canvas = None 

148 

149 def run(self, comm: bool = True, print_info: bool = True) -> "PipelineSimulator": 

150 """Run simulation to schedule the pipeline. 

151 

152 Args: 

153 comm: Whether to build the pipeline considering communication dependency and time. 

154 Default: ``True``. 

155 print_info: Whether to automatically print bubble and memory information. 

156 Default: ``True``. 

157 

158 Returns: 

159 The current :class:`PipelineSimulator` instance (for chaining). 

160 

161 Raises: 

162 CausalError: If the block sequences contain a dependency loop. 

163 CausalCommError: If the block-with-comm sequences contain a dependency loop. 

164 """ 

165 self._comm = comm 

166 self._check_loop() 

167 if comm: 

168 self.lines = self._create_lines(*self.adjust_func_list) 

169 self._check_comm_loop() 

170 for b in range(self.block_num): 

171 for p in range(self.pp): 

172 self.blocks[p][b].build_with_comm() 

173 self.lines[0][-1].build_with_comm() 

174 else: 

175 for p in range(self.pp): 

176 for block in self.blocks[p]: 

177 block.build_without_comm() 

178 self._statistic_info() 

179 if print_info: 

180 self.print_info() 

181 return self 

182 

183 def draw(self, comm: bool = True, connect: bool = None) -> "PipelineSimulator": 

184 """Show the pipeline and memory timeline. 

185 

186 Args: 

187 comm: Whether to show the comm blocks. Default: ``True``. 

188 connect: Whether to show the connect arrow of the send-receive pair when the comm 

189 pipeline is built. Default: ``None`` (auto-selected based on ``comm``). 

190 

191 Returns: 

192 The current :class:`PipelineSimulator` instance (for chaining). 

193 """ 

194 self.canvas = PlotMgr(2, ['block', 'memory'], sub_fig=self.sub_fig) 

195 if self._comm: 

196 connect = True if connect is None else connect 

197 self.canvas.draw(self.lines, 0, comm, connect, False, 'timeline') 

198 else: 

199 connect = False if connect is None else connect 

200 self.canvas.draw(self.blocks, 0, comm, connect, False, 'timeline') 

201 self.canvas.draw_mem(self.states.get('block_mem_list', []), 1) 

202 self.canvas.draw_info(self.bubbles, self.peak_memory) 

203 return self 

204 

205 

206 def show(self, comm: bool = True, connect: bool = None, 

207 file_name: str = None) -> "PipelineSimulator": 

208 """Draw the pipeline and display/save it via the canvas.""" 

209 draw_result = self.draw(comm, connect) 

210 if draw_result is None: 

211 raise RuntimeError("draw() returned None.") 

212 self.canvas.show(file_name) 

213 return self 

214 

215 def save(self, file_name: str, comm: bool = True, 

216 connect: bool = None) -> "PipelineSimulator": 

217 """Draw the pipeline and save it to ``file_name``.""" 

218 draw_result = self.draw(comm, connect) 

219 if draw_result is None: 

220 raise RuntimeError("draw() returned None.") 

221 self.canvas.save(file_name) 

222 return self 

223 

224 def print_info(self) -> "PipelineSimulator": 

225 """Log bubble and peak memory information.""" 

226 bubble_colors = ['1;33', '1;32', '1;31', '1;35', '1;36'] 

227 header = '\033[1;37m' + '—' * 13 + \ 

228 f' pp:{self.pp:>2}, vp:{self.vp:>2}, micro:{self.micro_num:>3} ' + \ 

229 '—' * 12 + '\033[0m' 

230 bubble_header = '-' * 20 + ' bubble ' + '-' * 20 

231 bubble_keys = apply_format(apply_color(list(self.bubbles.keys()), bubble_colors)) 

232 bubble_values = apply_format(apply_color(list(self.bubbles.values()), bubble_colors)) 

233 memory_header = '-' * 20 + ' memory ' + '-' * 20 

234 peak_memory = f"peak memory: {', '.join(f'{v:.2f}' for v in self.peak_memory)}" 

235 logger.output( 

236 "%s\n%s\n%s\n%s\n%s\n%s", 

237 header, bubble_header, bubble_keys, bubble_values, memory_header, peak_memory, 

238 ) 

239 return self 

240 

241 def _base_init(self, block_time) -> tuple: 

242 r"""init base setting""" 

243 if isinstance(block_time, (list, tuple)): 

244 if all(isinstance(item, (list, tuple)) for item in block_time): 

245 vp = len(block_time) 

246 pp = len(block_time[0]) 

247 elif all(isinstance(item, (int, float)) for item in block_time): 

248 vp = 1 

249 pp = len(block_time) 

250 else: 

251 raise ValueError(f"Unsupported input format block_time: {block_time}") 

252 else: 

253 raise ValueError(f"Unsupported input format block_time: {block_time}") 

254 if self.micro_num < pp: 

255 raise ValueError(f" `micro_num`({self.micro_num}) should equal or larger than `pp`({pp})") 

256 return pp, vp 

257 

258 def _input_format(self, block_time, layer_recompute, block_mem, block_mem_par, backward_ratio) -> None: 

259 r"""format inputs as 2d array""" 

260 self.block_time = format_2d_inputs(block_time, self.vp, self.pp) 

261 if isinstance(layer_recompute, bool): 

262 self.layer_recompute = self.block_time if layer_recompute else format_2d_inputs(0, self.vp, self.pp) 

263 else: 

264 self.layer_recompute = format_2d_inputs(layer_recompute, self.vp, self.pp) 

265 if isinstance(block_mem, (int, float)): 

266 self.block_mem = self.block_time * block_mem 

267 else: 

268 self.block_mem = format_2d_inputs(block_mem, self.vp, self.pp) 

269 

270 if isinstance(block_mem_par, (int, float)): 

271 self.block_mem_par = self.block_time * block_mem_par 

272 else: 

273 self.block_mem_par = format_2d_inputs(block_mem_par, self.vp, self.pp) 

274 

275 self.backward_ratio = format_2d_inputs(backward_ratio, self.vp, self.pp) 

276 

277 def _statistic_init(self) -> None: 

278 r"""init statistic info""" 

279 self.forward_time = self.block_time 

280 self.backward_time = self.block_time * self.backward_ratio + self.layer_recompute 

281 self.states = {'last_time': np.zeros(self.pp), 

282 'warmup_time': np.zeros(self.pp), 

283 'cooldown_time': np.zeros(self.pp), 

284 'stable_free_time': (np.zeros((self.vp, self.pp)), np.zeros((self.vp, self.pp))), 

285 'block_mem_list': [np.array([[0, 0]]) for _ in range(self.pp)]} 

286 self.model_compute_time = (np.sum(self.forward_time) + \ 

287 np.sum(self.forward_time * self.backward_ratio)) * self.micro_num 

288 self.hardware_compute_time = (np.sum(self.forward_time) + np.sum(self.backward_time)) * self.micro_num 

289 self.bubbles = {'real': 0, 

290 'ideal': (self.pp - 1) / self.vp / self.micro_num, 

291 'imba': 0, 

292 'comm': 0} 

293 if np.sum(self.layer_recompute) > 1e-5: 

294 self.bubbles['recompute'] = self.hardware_compute_time / self.model_compute_time - 1 

295 p, v, m = self.pp, self.vp, self.micro_num 

296 if self.vp == 1: 

297 if self.pp == 2: 

298 self.bubbles['comm'] = 4 * m 

299 elif self.pp % 2 == 0: 

300 self.bubbles['comm'] = 4 * p * m + 4 * p ** 2 - 14 * p 

301 else: 

302 self.bubbles['comm'] = 4 * p * m + 4 * p ** 2 - 12 * p 

303 elif self.pp <= 5: 

304 comm_coef_list = [[4, -2, 0], [6, -2, -6], [4, 0, 12], [6, -2, 40]] 

305 self.bubbles['comm'] = np.dot(np.array([p * v * m, m * p, 1]), comm_coef_list[self.pp - 2]) 

306 elif self.pp % 2 == 0: 

307 self.bubbles['comm'] = 4 * p * v * m + 4 * p ** 2 - 13 * p 

308 else: 

309 self.bubbles['comm'] = 6 * p * v * m - 2 * v * p ** 2 + 4 * v * p - 2 * p * m + 6 * p ** 2 - 16 * p 

310 

311 self.bubbles['comm'] *= self.comm_time / self.model_compute_time 

312 

313 def _statistic_info(self) -> None: 

314 r"""compute statistic info""" 

315 for p in range(self.pp): 

316 blocks = self.lines[p] if self._comm else self.blocks[p] 

317 current_mem = self.constant_mem + blocks[0].mem_par 

318 

319 for block in blocks: 

320 if block.type == 'c' and block.state == 'f': 

321 current_mem += block.mem 

322 elif block.type == 'c' and block.state == 'b': 

323 if not self._comm or not block.rec_block: 

324 current_mem -= block.mem 

325 elif block.type == 'r' and block.host.state == 'b': 

326 current_mem -= block.host.mem 

327 block = block.host 

328 else: 

329 continue 

330 self.states['block_mem_list'][p] = np.append(self.states['block_mem_list'][p], 

331 np.array([[block.end, current_mem]]), axis=0) 

332 self.states['block_mem_list'][p] = np.append(self.states['block_mem_list'][p], 

333 np.array([[blocks[-1].end, current_mem]]), axis=0) 

334 self.peak_memory = [np.max((self.states['block_mem_list'][p].T)[1]) for p in range(self.pp)] 

335 self.end_time = max(np.max((self.states['block_mem_list'][p].T)[0]) for p in range(self.pp)) 

336 self.bubbles['real'] = (self.pp * self.end_time - self.model_compute_time) / self.model_compute_time 

337 self.bubbles['imba'] = self.bubbles['real'] - self.bubbles['ideal'] + 1e-10 

338 if not self._comm: 

339 self.bubbles.pop('comm') 

340 else: 

341 self.bubbles['imba'] -= self.bubbles['comm'] 

342 if self.bubbles.get('recompute'): 

343 self.bubbles['imba'] -= self.bubbles['recompute'] 

344 

345 def _get_pre_label(self, label: tuple) -> tuple: 

346 r"""get pre block label""" 

347 t, s, m, v, p = label 

348 if (s, v, p) == ('f', 0, 0): 

349 return ('h', p) 

350 if (s, p) == ('f', 0): 

351 res = (t, s, m, v - 1, self.pp - 1) 

352 return res 

353 if (s, p) == ('b', self.pp - 1): 

354 if v == self.vp - 1: 

355 res = (t, 'f', m, self.vp - 1, p) 

356 return res 

357 res = (t, s, m, v + 1, 0) 

358 return res 

359 if s == 'f': 

360 res = (t, s, m, v, p - 1) 

361 return res 

362 if s == 'b': 

363 res = (t, s, m, v, p + 1) 

364 return res 

365 raise ValueError(f"Illegal label: {label}") 

366 

367 def _build_block(self) -> None: 

368 r"""Build `pre` relation for computation blocks.""" 

369 books = {self.blocks[0][0].pre.label: self.blocks[0][0].pre} 

370 for p in range(self.pp): 

371 for item in self.blocks[p]: 

372 books[item.label] = item 

373 for p in range(self.pp): 

374 block = self.blocks[p][0] 

375 while block is not None: 

376 pre_label = self._get_pre_label(block.label) 

377 block.pre = books.get(pre_label, None) 

378 block = block.right 

379 

380 def _build_comm_block(self) -> None: 

381 r"""Build `send_block` and `rec_block` relation among a computation block and two comm blocks.""" 

382 for p in range(self.pp): 

383 block = self.blocks[p][0] 

384 while block is not None: 

385 pre = block.pre 

386 if pre.stage != block.stage: 

387 block.rec_block = RecBlockSim(p, block.state, block.id, block.chunk, self.comm_time) 

388 pre.send_block = SendBlockSim(pre.stage, pre.state, pre.id, pre.chunk, self.comm_time) 

389 block.rec_block.host = block 

390 block.rec_block.dual = pre.send_block 

391 pre.send_block.host = pre 

392 pre.send_block.dual = block.rec_block 

393 block.depend_pre = block.rec_block 

394 block.rec_block.depend_pre = pre.send_block 

395 pre.send_block.depend_pre = pre 

396 else: 

397 block.depend_pre = pre 

398 block = block.right 

399 

400 def _check_loop(self) -> None: 

401 r"""check the existence of dependency""" 

402 loop = self.blocks[0][-1].loop() 

403 if loop: 

404 raise CausalError('Block dependency exist loops!', self.blocks, loop) 

405 for p in range(self.pp): 

406 for block in self.blocks[p]: 

407 block.flag = False 

408 

409 def _check_comm_loop(self) -> None: 

410 r"""check the existence of comm dependency""" 

411 loop = self.lines[0][-1].comm_loop() 

412 if loop: 

413 raise CausalCommError('Block comm dependency exist loops!', self.lines, loop) 

414 for p in range(self.pp): 

415 for block in self.lines[p]: 

416 block.flag = False 

417 

418 def _create_lines(self, *adjust_func) -> list[list[BlockSim]]: 

419 r"""create block line for each stage with comm""" 

420 lines = [copy.copy(self.blocks[p]) for p in range(self.pp)] 

421 for p in range(self.pp): 

422 for b in range(self.block_num): 

423 block = self.blocks[p][b] 

424 pre = block.pre 

425 if block.rec_block: 

426 lines[p].insert(lines[p].index(block), block.rec_block) 

427 if pre.type == 'h': 

428 lines[pre.stage].insert(0, pre.send_block) 

429 else: 

430 lines[pre.stage].insert(lines[pre.stage].index(pre) + 1, pre.send_block) 

431 for func in adjust_func: 

432 lines = func(lines) 

433 for p in range(self.pp): 

434 for b, block in enumerate(lines[p]): 

435 if b == 0: 

436 block.depend_left = block.left if block.left else block.host.left 

437 else: 

438 block.depend_left = lines[p][b - 1] 

439 return lines 

440 

441 def _get_block_phase(self, p: int, b: int) -> str: 

442 r"""get block phase""" 

443 r = self.micro_num % self.pp 

444 if b < (self.vp + 1) * self.pp - 2 * p - 2 + r: 

445 return 'warmup' 

446 if b > self.block_num - (self.vp + 1) * self.pp + 2 * p: 

447 return 'cooldown' 

448 return 'stable' 

449 

450 def _send_block_delay(self, lines, p: int, b: int, distance: int) -> None: 

451 r"""adjust send block: delay send block""" 

452 i_send = lines[p].index(self.blocks[p][b].send_block) 

453 send_block = lines[p].pop(i_send) 

454 i_new = lines[p].index(self.blocks[p][b + distance]) + 1 

455 lines[p].insert(i_new, send_block) 

456 

457 def _process_swap(self, block, lines, p, b, i_b, i_bn) -> bool: 

458 r"""process swap in condition""" 

459 if i_bn - i_b == 3: 

460 if p % 2 == 0 and lines[p][i_b + 1].type == 'r' and lines[p][i_b + 2].type == 's': 

461 lines[p][i_b + 1], lines[p][i_b + 2] = lines[p][i_b + 2], lines[p][i_b + 1] 

462 if p % 2 == 1 and lines[p][i_b + 1].type == 's' and lines[p][i_b + 2].type == 'r': 

463 if block.phase == 'warmup' and self.blocks[p][b + 1].phase == 'cooldown': 

464 return False 

465 lines[p][i_b + 1], lines[p][i_b + 2] = lines[p][i_b + 2], lines[p][i_b + 1] 

466 if lines[p][i_b + 1].dual.stage == lines[p][i_b + 2].dual.stage: 

467 pd = lines[p][i_b + 1].dual.stage 

468 j_b1 = lines[pd].index(lines[p][i_b + 1].dual) 

469 j_b2 = lines[pd].index(lines[p][i_b + 2].dual) 

470 if j_b1 > j_b2: 

471 lines[p][i_b + 1], lines[p][i_b + 2] = lines[p][i_b + 2], lines[p][i_b + 1] 

472 if i_bn - i_b == 4: 

473 if lines[p][i_b + 1].dual.stage == lines[p][i_b + 2].dual.stage and \ 

474 lines[p][i_b + 2].dual.stage == lines[p][i_b + 3].dual.stage: 

475 if lines[p][i_b + 1].type == 's' and lines[p][i_b + 2].type == 's' \ 

476 and lines[p][i_b + 3].type == 'r': 

477 lines[p][i_b + 1], lines[p][i_b + 2] = lines[p][i_b + 2], lines[p][i_b + 1] 

478 return True 

479 

480 def swap_send_rec(self, lines: list[list[BlockSim]]) -> list[list[BlockSim]]: 

481 """Adjust send blocks: swap adjacent send/receive pairs where ordering is ambiguous.""" 

482 for p in range(self.pp): 

483 for b, block in enumerate(self.blocks[p]): 

484 if b >= len(self.blocks[p]) - 1: 

485 continue 

486 i_b = lines[p].index(block) 

487 i_bn = lines[p].index(self.blocks[p][b + 1]) 

488 try: 

489 swap_processed = self._process_swap(block, lines, p, b, i_b, i_bn) 

490 except (ValueError, IndexError) as error: 

491 raise RuntimeError( 

492 "Failed to process swap in pipeline simulator." 

493 ) from error 

494 if not swap_processed: 

495 continue 

496 return lines 

497 

498 def vpp_send_delay(self, lines: list[list[BlockSim]]) -> list[list[BlockSim]]: 

499 """Adjust VPP send blocks by delaying them one slot during the stable phase.""" 

500 if self.micro_num % self.pp != 0: 

501 return lines 

502 for p in range(self.pp): 

503 for b, block in enumerate(self.blocks[p]): 

504 if block.send_block is not None and block.phase == 'stable': 

505 self._send_block_delay(lines, p, b, 1) 

506 return lines 

507 

508 def residue_delay(self, lines: list[list[BlockSim]]) -> list[list[BlockSim]]: 

509 """Adjust send blocks when ``micro_num % pp`` leaves a residue micro-batch.""" 

510 r = self.micro_num % self.pp 

511 if r == 0: 

512 return lines 

513 for p in range(self.pp): 

514 for b, block in enumerate(self.blocks[p]): 

515 if block.send_block is None: 

516 continue 

517 if p == self.pp - 1 and block.id < self.pp + r and block.state == 'f': 

518 self._send_block_delay(lines, -1, b, r + max(0, block.id - self.pp + 1)) 

519 elif p == 0 and block.id < self.pp + r and block.state == 'b': 

520 if self.micro_num // self.pp == 1: 

521 self._send_block_delay(lines, 0, b, r) 

522 else: 

523 self._send_block_delay(lines, 0, b, r + self.pp) 

524 elif block.phase == 'stable': 

525 self._send_block_delay(lines, p, b, 1) 

526 return lines 

527 

528 

529if __name__ == '__main__': 

530 

531 # PipelineSimulator([[4, 4, 4, 4], [4, 4, 4, 4], [4, 4, 4, 4 + 0.8]], 8, 0.1, 

532 # [[1, 0, 0, 0], [1, 0, 0, 0], [1, 1, 0, 0]], 

533 # [[1.1, 2, 2, 2], [1.1, 2, 2, 2], [1.1, 1.1, 2, 2]], method='vpp').run().show() 

534 PipelineSimulator( 

535 [[186.0, 171.0, 132.0, 132.0, 132.0, 132.0, 132.0, 132.0, 

536 132.0, 132.0, 132.0, 132.0, 132.0, 132.0, 132.0, 133.0]], 32, 

537 block_mem_act=[[1146, 908, 736, 736, 736, 736, 736, 736, 

538 736, 736, 2623, 2623, 4510, 4510, 8284, 8284]], 

539 block_mem_par=[[14130, 21126, 36252, 36252, 36252, 36252, 36252, 36252, 

540 36252, 36252, 36252, 36252, 36252, 36252, 36252, 38297]], 

541 layer_recompute=[[135.0, 171.0, 132.0, 132.0, 132.0, 132.0, 132.0, 132.0, 

542 132.0, 132.0, 99.0, 99.0, 66.0, 66.0, 0, 0]], 

543 less_memory=False).run().show()