Coverage for  / home / jenkins / .local / lib / python3.10 / site-packages / hyper_parallel / auto_parallel / sapp_ppb / utils / compute_memory.py: 86%

264 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"""Derive per-layer memory parameters from a set of dry-run stage observations.""" 

16import numpy as np 

17 

18import hyper_parallel.auto_parallel.sapp_ppb.utils.recompute as Recompute 

19from hyper_parallel.auto_parallel.sapp_ppb.utils.layer import Layer 

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

21from hyper_parallel.auto_parallel.sapp_ppb.utils.stage import Stage, filter_stage_id 

22 

23 

24class ComputeMemory: 

25 """ 

26 ComputeMemory class to compute the different memories with stages information running (dry) log 

27 

28 stage{A|B} means stage with different configuration A and B 

29 stage{1|2} means stage same configuration but different id (can be id other than 1 or 2) 

30 

31 number_of_stage_ (int): number of stages for the LLM 

32 stagesA_ (list[Stage]): list of dry run stages information, with all the same configuration A, 

33 required at least staged 0, 1, (n-2), (n-1) 

34 Don't set directly stagesA_, but use set_stagesA 

35 stagesB_ (list[Stage]): list of dry run stages information, with all the same configuration B, 

36 different from config A required at least staged 0, 1, (n-2), (n-1) 

37 Don't set directly stagesB_, but use set_stagesB 

38 memory_parameter_ (float): memory_parameter_ of the BODY layer, memory required to run the layer 

39 memory_activation_rec_ (dict[Recompute.TYPE, float]) activation memory per recompute types 

40 recompute_considered_ (dict[Recompute.TYPE, bool]) recomputation types taken into consideration 

41 memory_const_ (float): constant memory required for each stages 

42 memory_head_ (float): memory required to run the head layer 

43 memory_tail_ (float): memory required to run the tail layer 

44 """ 

45 

46 number_of_stage_: int 

47 stages_a: list[Stage] 

48 stages_b: list[Stage] 

49 memory_parameter_: float 

50 memory_activation_rec_: dict[Recompute.TYPE, float] 

51 recompute_considered_: dict[Recompute.TYPE, bool] 

52 memory_const_: float 

53 memory_head_: float 

54 memory_tail_: float 

55 

56 def __init__(self, number_of_stage: int, stages_a: list[Stage] = None, 

57 stages_b: list[Stage] = None) -> None: 

58 """Build a :class:`ComputeMemory` solver instance. 

59 

60 Args: 

61 number_of_stage: Total number of pipeline stages in the target LLM. 

62 stages_a: Dry-run observations with configuration A (at least stages ``0, i, j, n-1``). 

63 stages_b: Dry-run observations with configuration B (must differ from A). 

64 """ 

65 self.number_of_stage_ = number_of_stage 

66 self.set_stages_a(stages_a) 

67 self.set_stages_b(stages_b) 

68 # number_of_stage != len(stages) can be true 

69 self.memory_parameter_ = None 

70 self.memory_activation_rec_ = {r: None for r in Recompute.TYPE} 

71 self.find_recompute_considered() 

72 self.memory_const_ = None 

73 self.memory_head_ = None 

74 self.memory_tail_ = None 

75 

76 def set_stages_a(self, stages: list[Stage]) -> None: 

77 """Assign dry-run observations to configuration A after a consistency check.""" 

78 if stages is None: 

79 self.stages_a = [] 

80 return 

81 for stage1 in stages: 

82 for stage2 in stages: 

83 if not stage1.same_global_config(stage2): 

84 logger.error( 

85 "Cannot set stagesA, all elements don't have the same configuration",) 

86 self.stages_a = [] 

87 return 

88 self.stages_a = stages 

89 

90 def set_stages_b(self, stages: list[Stage]) -> None: 

91 """Assign dry-run observations to configuration B (must differ from A).""" 

92 if stages is None: 

93 self.stages_b = [] 

94 return 

95 for stage1 in stages: 

96 for stage2 in stages: 

97 if not stage1.same_global_config(stage2): 

98 logger.error( 

99 "Cannot set stagesB, all elements don't have the same configuration") 

100 self.stages_b = [] 

101 return 

102 for stage_a in self.stages_b: 

103 if stage1.same_global_config(stage_a): 

104 logger.error( 

105 "Cannot set stagesB, an elements have the same configuration than stagesA") 

106 self.stages_b = [] 

107 return 

108 self.stages_b = stages 

109 

110 def find_recompute_considered(self) -> None: 

111 """Populate :attr:`recompute_considered_` from the observed ``stages_a`` data.""" 

112 self.recompute_considered_ = {r: False for r in Recompute.TYPE} 

113 self.recompute_considered_[Recompute.TYPE.NONE] = True 

114 

115 for stage in self.stages_a: 

116 for rec in Recompute.TYPE: 

117 if stage.nb_layer_rec_[rec] > 0: 

118 self.recompute_considered_[rec] = True 

119 

120 def _compute_memory_parameter_local_(self, stage1: Stage, stage2: Stage) -> float: 

121 """ 

122 Given 2 stages information with the same configuration, and different id, 

123 Compute the memory_parameter 

124 """ 

125 if stage1.same_config(stage2): 

126 if stage1.id_ != stage2.id_: 

127 res = stage1.memory_usage_ * (stage1.nb_stage_ - stage1.id_) 

128 res -= stage2.memory_usage_ * (stage2.nb_stage_ - stage2.id_) 

129 res /= stage1.id_ - stage2.id_ 

130 res = abs(res) 

131 res /= stage1.nb_layer_ 

132 return res 

133 logger.error( 

134 "stage with same characteristic, BUT SAME ID too, cannot compute memory_parameter") 

135 return 0 

136 logger.error("stage with different characteristic, cannot compute memory_parameter") 

137 return 0 

138 

139 def _compute_memory_parameter_(self, multi_run=False) -> float: 

140 """Compute memory_parameter 

141 With all available stages compute all combinations of memory parameter 

142 and return the mean of all the memory_parameter found 

143 BEWARE: can update memory_parameter_ & memory_activation_rec_ 

144 because of _compute_memories_layers_() 

145 return: memory_parameter 

146 """ 

147 if multi_run or (len(self.stages_a) < 5 and len(self.stages_b) < 5): 

148 memory_parameter_list = [] 

149 for stage1 in self.stages_a: 

150 if stage1.id_ not in [0, (self.number_of_stage_ - 1)]: 

151 mem_param = self._compute_memory_parameter_local_(stage1, stage2) 

152 for stage2 in self.stages_a: 

153 if (stage2.id_ not in [0, (self.number_of_stage_ - 1), 

154 stage1.id_] and mem_param != 0): 

155 memory_parameter_list.append(mem_param) 

156 for stage1 in self.stages_b: 

157 if stage1.id_ not in [0, (self.number_of_stage_ - 1)]: 

158 for stage2 in self.stages_b: 

159 mem_param = self._compute_memory_parameter_local_(stage1, stage2) 

160 if (stage2.id_ not in [0, (self.number_of_stage_ - 1), 

161 stage1.id_] and mem_param != 0): 

162 memory_parameter_list.append(mem_param) 

163 return np.mean(memory_parameter_list) 

164 if self._compute_memories_layers_(): 

165 return self.memory_parameter_ 

166 logger.error("Issue with _compute_memory_parameter_!!!") 

167 return 0 

168 

169 def _compute_memory_activation_(self, rec, multi_run=False) -> float: 

170 """ 

171 Compute memory_activation for a given recomputation type 

172 return: memory_activation 

173 """ 

174 if multi_run or (len(self.stages_a) < 5 and len(self.stages_b) < 5): 

175 # look at solution 4 stages 

176 logger.error("Not implemented yet!!!") 

177 return 0 

178 if self._compute_memories_layers_(): 

179 return self.memory_activation_rec_[rec] 

180 logger.error("Issue with _compute_memory_activation_!!!") 

181 return 0 

182 

183 def zero_offset(self) -> bool: 

184 """Return ``True`` if every stage in ``stages_a`` hosts the same number of layers.""" 

185 nb_layer = self.stages_a[0].nb_layer_ 

186 for s in self.stages_a: 

187 if s.nb_layer_ != nb_layer: 

188 return False 

189 return True 

190 

191 def _compute_memories_layers_(self) -> bool: 

192 """check if enough stage number is provided""" 

193 used_rec = Recompute.get_used_list(self.recompute_considered_) 

194 used_rec_num = len(used_rec) 

195 stage_num = len(self.stages_a) 

196 if stage_num == used_rec_num + 3: 

197 return self._compute_memories_layer_bodies_(False) 

198 if stage_num >= used_rec_num + 4: 

199 logger.info("Enabled const memory component because enough stages were given") 

200 if self.zero_offset(): 

201 logger.error( 

202 "The number of layer per stage cannot be the same for all stages " 

203 "when const component is enabled. Some offset must be used" 

204 ) 

205 return False 

206 return self._compute_memories_layer_bodies_(True) 

207 

208 logger.error( 

209 "%s stages found and (%s) recomputation considered" 

210 "is not coherent. There should be 3 or 4 more stages than recomputation considered", 

211 stage_num, 

212 used_rec_num, 

213 ) 

214 return False 

215 

216 def _compute_memories_layer_bodies_local_( 

217 self, unused_rec: list[Recompute.TYPE], 

218 stages: list[Stage]) -> tuple[float, float, float]: 

219 """Compute memory_parameter & memory activation for all recomputation types 

220 Require at least 3 Stages different from first and last stage 

221 """ 

222 variable_factor_list = [] 

223 constant_memory_list = [] 

224 unused_rec.sort(reverse=True) 

225 for stage in stages: 

226 if stage.id_ not in [0, self.number_of_stage_ - 1]: 

227 variable_factor_list.append(stage.get_index_memory_var()) 

228 for rec_i in unused_rec: 

229 variable_factor_list[-1].pop(1 + rec_i) 

230 constant_memory_list.append(stage.memory_usage_) 

231 solution = list( 

232 np.linalg.solve(np.array(variable_factor_list), 

233 np.array(constant_memory_list))) 

234 memory_param = solution.pop(0) 

235 memory_act_rec = Recompute.assign_used(solution, unused_rec) 

236 return (memory_param, memory_act_rec) 

237 

238 

239 

240 def _compute_memories_layer_bodies_local_with_fix_( 

241 self, unused_rec: list[Recompute.TYPE], 

242 stages: list[Stage]) -> tuple[float, float, float]: 

243 """Compute memory_const, memory_parameter & memory activation for all recomputation types 

244 Require at least 4 Stages different from first and last stage 

245 """ 

246 variable_factor_list = [] 

247 constant_memory_list = [] 

248 unused_rec.sort(reverse=True) 

249 for stage in stages: 

250 if stage.id_ not in [0, self.number_of_stage_ - 1]: 

251 variable_factor_list.append([1] + stage.get_index_memory_var()) 

252 for rec_i in unused_rec: 

253 variable_factor_list[-1].pop(2 + rec_i) 

254 constant_memory_list.append(stage.memory_usage_) 

255 logger.debug( 

256 "solve(\n %s, \n %s) ", 

257 np.array(variable_factor_list), 

258 np.array(constant_memory_list), 

259 ) 

260 used_rec = Recompute.get_used_list(self.recompute_considered_) 

261 used_rec_num = len(used_rec) 

262 

263 if len(stages) < used_rec_num + 4: 

264 raise ValueError("Stages given are not enough to solve memory constraints") 

265 if len(stages) == used_rec_num + 4: 

266 solution = list( 

267 np.linalg.solve(np.array(variable_factor_list), 

268 np.array(constant_memory_list))) 

269 else: 

270 logger.warning("Stages given are more than needed, switch to least sqaures method") 

271 solution = list(np.linalg.lstsq(np.array(variable_factor_list), 

272 np.array(constant_memory_list), rcond=None)[0]) 

273 

274 memory_const = solution.pop(0) 

275 memory_param = solution.pop(0) 

276 memory_act_rec = Recompute.assign_used(solution, unused_rec) 

277 return (memory_const, memory_param, memory_act_rec) 

278 

279 def _compute_memories_layer_bodies_(self, with_fix: bool) -> bool: 

280 """ 

281 Compute memory_parameter, memory_recompute, memory_activation 

282 Require at least 3 Stages different from first and last stage 

283 BEWARE: can update memory_parameter_, memory_recompute_, memory_activation_ 

284 return True if success to update memory_parameter_, memory_recompute_, memory_activation_ 

285 """ 

286 

287 memory_const_a = None 

288 memory_parameter_a = None 

289 memory_recompute_a = {r: None for r in Recompute.TYPE} 

290 

291 memory_const_b = None 

292 memory_parameter_b = None 

293 memory_recompute_b = {r: None for r in Recompute.TYPE} 

294 

295 unused_rec = Recompute.get_unused_list(self.recompute_considered_) 

296 logger.info("unused recomputation: %s", unused_rec) 

297 

298 if with_fix: 

299 if len(self.stages_a) >= 5: 

300 (memory_const_a, 

301 memory_parameter_a, 

302 memory_recompute_a) = (self._compute_memories_layer_bodies_local_with_fix_( 

303 unused_rec, self.stages_a)) 

304 if len(self.stages_b) >= 5: 

305 (memory_const_b, 

306 memory_parameter_b, 

307 memory_recompute_b) = (self._compute_memories_layer_bodies_local_with_fix_( 

308 unused_rec, self.stages_b)) 

309 

310 return self._average_if_needed_fix( 

311 memory_const_a, 

312 memory_parameter_a, 

313 memory_recompute_a, 

314 memory_const_b, 

315 memory_parameter_b, 

316 memory_recompute_b, 

317 ) 

318 if len(self.stages_a) >= 5: 

319 (memory_parameter_a, 

320 memory_recompute_a) = (self._compute_memories_layer_bodies_local_( 

321 unused_rec, self.stages_a)) 

322 if len(self.stages_b) >= 5: 

323 (memory_parameter_b, 

324 memory_recompute_b) = (self._compute_memories_layer_bodies_local_( 

325 unused_rec, self.stages_b)) 

326 

327 return self._average_if_needed( 

328 memory_parameter_a, 

329 memory_recompute_a, 

330 memory_parameter_b, 

331 memory_recompute_b, 

332 ) 

333 

334 def _average_if_needed_fix( 

335 self, 

336 memory_const_a, 

337 memory_parameter_a, 

338 memory_recompute_a, 

339 memory_const_b, 

340 memory_parameter_b, 

341 memory_recompute_b, 

342 ): 

343 """check if average is needed""" 

344 if memory_parameter_a is not None and memory_parameter_a != 0: 

345 if memory_parameter_b is not None and memory_parameter_b != 0: 

346 self.memory_const_ = (memory_const_a + 

347 memory_const_b) / 2 

348 self.memory_parameter_ = (memory_parameter_a + 

349 memory_parameter_b) / 2 

350 Recompute.average([memory_recompute_a, memory_recompute_b]) 

351 else: 

352 self.memory_const_ = memory_const_a 

353 self.memory_parameter_ = memory_parameter_a 

354 self.memory_activation_rec_ = memory_recompute_a 

355 

356 elif memory_parameter_b is not None and memory_parameter_b != 0: 

357 self.memory_const_ = memory_const_b 

358 self.memory_parameter_ = memory_parameter_b 

359 self.memory_activation_rec_ = memory_recompute_b 

360 else: 

361 logger.error("failed to compute memories") 

362 return False 

363 return True 

364 

365 def _average_if_needed(self, memory_parameter_a, memory_recompute_a, memory_parameter_b, 

366 memory_recompute_b,): 

367 """check if average is needed""" 

368 if memory_parameter_a is not None and memory_parameter_a != 0: 

369 if memory_parameter_b is not None and memory_parameter_b != 0: 

370 self.memory_parameter_ = (memory_parameter_a + memory_parameter_b) / 2 

371 Recompute.average([memory_recompute_a, memory_recompute_b]) 

372 else: 

373 self.memory_parameter_ = memory_parameter_a 

374 self.memory_activation_rec_ = memory_recompute_a 

375 

376 elif memory_parameter_b is not None and memory_parameter_b != 0: 

377 self.memory_parameter_ = memory_parameter_b 

378 self.memory_activation_rec_ = memory_recompute_b 

379 else: 

380 logger.error("failed to compute memories") 

381 return False 

382 return True 

383 

384 def _compute_memory_head_(self) -> float: 

385 """compute the memory for the head""" 

386 head_stages = filter_stage_id(self.stages_a, 0) 

387 head_stages += filter_stage_id(self.stages_b, 0) 

388 memory_head_list = [] 

389 mem_parameter = self.get_memory_parameter() 

390 for head in head_stages: 

391 head_memory = head.memory_usage_ 

392 for rec in Recompute.TYPE: 

393 if self.recompute_considered_[rec] is True: 

394 head_memory -= (head.nb_layer_rec_[rec] * self.get_memory_activation( 

395 rec) * self.number_of_stage_) 

396 head_memory -= (head.nb_layer_) * mem_parameter 

397 memory_head_list.append(head_memory) 

398 return np.mean(memory_head_list) 

399 

400 def _compute_memory_tail_(self) -> float: 

401 """compute the memory for the tail""" 

402 tail_stages = filter_stage_id(self.stages_a, self.number_of_stage_ - 1) 

403 tail_stages += filter_stage_id(self.stages_b, self.number_of_stage_ - 1) 

404 memory_tail_list = [] 

405 for tail in tail_stages: 

406 tail_memory = tail.memory_usage_ 

407 for rec in Recompute.TYPE: 

408 if self.recompute_considered_[rec] is True: 

409 tail_memory -= (tail.nb_layer_rec_[rec] * self.get_memory_activation(rec) * 1) 

410 tail_memory -= (tail.nb_layer_) * self.get_memory_parameter() 

411 memory_tail_list.append(tail_memory) 

412 return np.mean(memory_tail_list) 

413 

414 def get_memory_const(self) -> float: 

415 """Return the solver-derived constant memory component per stage.""" 

416 return self.memory_const_ 

417 

418 def get_memory_parameter(self, force_recompute: bool = False) -> float: 

419 """Return the per-body-layer parameter memory, recomputing on demand.""" 

420 if force_recompute or self.memory_parameter_ is None: 

421 self.memory_parameter_ = self._compute_memory_parameter_() 

422 return self.memory_parameter_ 

423 

424 def get_memory_activation(self, rec: Recompute.TYPE, 

425 force_recompute: bool = False) -> float: 

426 """Return the per-layer activation memory for a given recomputation type.""" 

427 if force_recompute or self.memory_activation_rec_[rec] is None: 

428 self.memory_activation_rec_[rec] = self._compute_memory_activation_(rec) 

429 return self.memory_activation_rec_[rec] 

430 

431 def get_memory_head(self, force_recompute: bool = False) -> float: 

432 """Return the HEAD-layer memory, recomputing on demand.""" 

433 if force_recompute or self.memory_head_ is None: 

434 self.memory_head_ = self._compute_memory_head_() 

435 return self.memory_head_ 

436 

437 def get_memory_tail(self, force_recompute: bool = False) -> float: 

438 """Return the TAIL-layer memory, recomputing on demand.""" 

439 if force_recompute or self.memory_tail_ is None: 

440 self.memory_tail_ = self._compute_memory_tail_() 

441 return self.memory_tail_ 

442 

443 

444def compute_memories(layers: list[Layer], memory_folder: str, number_of_stage: int) -> list[Layer]: 

445 """compute memories""" 

446 filename = "" 

447 # Put some meta information in a predefine .json file like layers info? 

448 with open(memory_folder + filename, encoding="utf-8"): 

449 pass 

450 cm = ComputeMemory(number_of_stage=number_of_stage, stages_a=[], stages_b=[]) 

451 

452 for layer in layers: 

453 if layer.type_ == Layer.type_enum.HEAD: 

454 layer.memory_parameter_ = cm.get_memory_head() 

455 elif layer.type_ == Layer.type_enum.TAIL: 

456 layer.memory_parameter_ = cm.get_memory_tail() 

457 elif layer.type_ == Layer.type_enum.BODY: 

458 layer.memory_parameter_ = cm.get_memory_parameter() 

459 for rec in Recompute.TYPE: 

460 layer.memory_activation_rec_[rec] = cm.get_memory_activation(rec) 

461 return layers