Coverage for  / home / jenkins / .local / lib / python3.10 / site-packages / hyper_parallel / auto_parallel / sapp_nd / nd / balancing_adapter.py: 81%

219 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-07-06 05:41 +0800

1# Copyright 2025 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"""Utilities for adapting balancing for a given pipeline configuration""" 

16from dataclasses import dataclass 

17import copy 

18 

19from hyper_parallel.auto_parallel.sapp_nd.nd.logger import logger 

20 

21 

22@dataclass 

23class Pipeline: 

24 """Class storing a pipeline configuration""" 

25 

26 pp: int 

27 vpp: int 

28 

29 def __init__(self, pp, vpp): 

30 self.pp = pp 

31 self.vpp = vpp 

32 

33 def __str__(self): 

34 return "PP: " + str(self.pp) + ", VPP: " + str(self.vpp) 

35 

36 def chunk_stage(self): 

37 """Product of chunks and stages""" 

38 return self.pp * self.vpp 

39 

40 

41def infer_pp_and_vpp(offset): 

42 """Return a pipeline configuration inferred from an offset""" 

43 if is_zero_d(offset): 

44 return Pipeline(1, 1) 

45 if is_one_d(offset): 

46 return Pipeline(len(offset), 1) 

47 if is_two_d(offset): 

48 return Pipeline(len(offset[0]), len(offset)) 

49 raise TypeError(f"Offset {offset} has a wrong type!") 

50 

51 

52class BalancingAdapter: 

53 """Adapt pipeline balancing to a given 'new' pipeline configuration""" 

54 

55 layers: int 

56 prev_pip: Pipeline 

57 

58 def __init__(self, layers, offset, recompute, manual_ppb): 

59 self.layers = layers 

60 self.prev_offset = offset 

61 self.prev_recompute = recompute 

62 self.from_config = manual_ppb 

63 logger.debug( 

64 "init: layers = %d, from_config = %s, offset = %s, recompute = %s", 

65 layers, 

66 str(self.from_config), 

67 str(offset), 

68 str(recompute), 

69 ) 

70 self.prev_pip = infer_pp_and_vpp(offset) 

71 

72 def treat_pp_list(self, new_pip, stages): 

73 """Treat 1D offset or recompute config""" 

74 current_pp_len = len(stages) 

75 logger.debug( 

76 "new pp (%d) = prev pp (%d)? l = %s, len(l) = %d", 

77 new_pip.pp, 

78 self.prev_pip.pp, 

79 str(stages), 

80 len(stages), 

81 ) 

82 if new_pip.pp == current_pp_len: 

83 return stages 

84 new_l = [] 

85 if new_pip.pp > current_pp_len: 

86 while new_pip.pp % len(stages) != 0: 

87 stages.insert(len(stages) // 2, 0) 

88 logger.debug("stages: %s", str(stages)) 

89 factor = new_pip.pp // current_pp_len 

90 for s in stages: 

91 rest = s % factor 

92 for _ in range(factor): 

93 if rest > 0: 

94 new_l.append(s // factor + 1) 

95 rest -= 1 

96 else: 

97 new_l.append(s // factor) 

98 if new_pip.pp < current_pp_len: 

99 for _ in range(current_pp_len % new_pip.pp): 

100 stages.insert(len(stages) // 2, 0) 

101 factor = current_pp_len // new_pip.pp 

102 for i in range(new_pip.pp): 

103 total_rec_layers = 0 

104 for j in range(factor): 

105 total_rec_layers += stages[i * factor + j] 

106 new_l.append(total_rec_layers) 

107 return new_l 

108 

109 def treat_vpp_list(self, new_pip, ll): 

110 """Treat 2D offset or recompute config""" 

111 logger.debug("treat_vpp_list start: ll = %s", str(ll)) 

112 prev_vpp = len(ll) 

113 if prev_vpp < new_pip.vpp: 

114 if prev_vpp == 1: 

115 ll.append([0] * self.prev_pip.pp) 

116 ll[1][-1] = ll[0][-1] 

117 ll[0][-1] = 0 

118 prev_vpp = 2 

119 for _ in range(new_pip.vpp - prev_vpp): 

120 ll.insert(prev_vpp // 2, [0] * self.prev_pip.pp) 

121 logger.debug("B: ll = %s", str(ll)) 

122 

123 prev_layer_per_stage = [ 

124 prev_vpp * (self.layers // self.prev_pip.chunk_stage()) 

125 + sum(stages[p] for stages in make_two_d(self.prev_offset)) 

126 for p in range(self.prev_pip.pp) 

127 ] 

128 new_layer_per_stage = [ 

129 new_pip.vpp * (self.layers // self.prev_pip.chunk_stage()) 

130 + sum(stages[p] for stages in ll) 

131 for p in range(self.prev_pip.pp) 

132 ] 

133 logger.debug( 

134 "prev_layer_per_stage = %s", str(prev_layer_per_stage) 

135 ) 

136 logger.debug("new_layer_per_stage = %s", str(new_layer_per_stage)) 

137 for s in range(self.prev_pip.pp): 

138 for v in range(new_pip.vpp): 

139 if ( 

140 prev_layer_per_stage[s] - new_layer_per_stage[s] > 0 

141 and ll[v][s] < 1 

142 ): 

143 ll[v][s] += 1 

144 new_layer_per_stage[s] += 1 

145 

146 elif prev_vpp > new_pip.vpp: 

147 for _ in range(prev_vpp - new_pip.vpp): 

148 ll[-2] = [sum(x) for x in zip(ll[-1], ll[-2])] 

149 del ll[-1] 

150 logger.debug("C: ll = %s", str(ll)) 

151 new_vpp_list = [] 

152 for stages in ll: 

153 new_vpp_list.append(self.treat_pp_list(new_pip, stages)) 

154 logger.debug("D: new_vpp_list = %s", str(new_vpp_list)) 

155 return new_vpp_list 

156 

157 def treat_recompute_list(self, new_pip, recompute): 

158 """Treat recompute config recursively""" 

159 if all(isinstance(x, int) for x in recompute): 

160 if all(isinstance(x, int) for x in recompute): 

161 if new_pip.vpp == 1: 

162 return self.treat_pp_list(new_pip, recompute) 

163 return self.treat_vpp_list(new_pip, recompute) 

164 elif all(isinstance(x, int) for stages in recompute for x in stages): 

165 if new_pip.vpp == 1: 

166 return self.treat_vpp_list(new_pip, recompute)[0] 

167 return self.treat_vpp_list(new_pip, recompute) 

168 return recompute 

169 

170 def treat_recompute(self, new_pp, new_vpp): 

171 """Treat recompute config for the new given Pipeline config""" 

172 new_pip = Pipeline(new_pp, new_vpp) 

173 if not self.from_config: 

174 return self.default_recompute( 

175 new_pip, copy.deepcopy(self.prev_recompute) 

176 ) 

177 if new_pip == self.prev_pip or isinstance(self.prev_recompute, bool): 

178 return copy.deepcopy(self.prev_recompute) 

179 return self.treat_recompute_list( 

180 new_pip, copy.deepcopy(self.prev_recompute) 

181 ) 

182 

183 def treat_offset(self, new_pp, new_vpp): 

184 """Treat offset for the new given Pipeline config""" 

185 new_pip = Pipeline(new_pp, new_vpp) 

186 if not self.from_config: 

187 return self.make_valid(new_pip, self.default_offset(new_pip)) 

188 offset = copy_offset(self.prev_offset) 

189 if new_pip == self.prev_pip: 

190 logger.debug("Same pipeline, no offset change") 

191 return offset 

192 logger.debug( 

193 "change offset %s from PP = %d, VPP = %d, to PP = %d, VPP = %d", 

194 str(offset), 

195 self.prev_pip.pp, 

196 self.prev_pip.vpp, 

197 new_pip.pp, 

198 new_pip.vpp, 

199 ) 

200 

201 if is_zero_d(offset): 

202 offset = [] 

203 for _ in range(new_pip.vpp): 

204 offset.append([]) 

205 for _ in range(new_pip.pp): 

206 offset[-1].append(0) 

207 return self.make_valid(new_pip, offset) 

208 if is_one_d(offset): 

209 if new_pip.vpp == 1: 

210 return self.make_valid( 

211 new_pip, self.treat_pp_list(new_pip, offset) 

212 ) 

213 return self.make_valid( 

214 new_pip, self.treat_vpp_list(new_pip, [offset]) 

215 ) 

216 if is_two_d(offset): 

217 return self.make_valid( 

218 new_pip, self.treat_vpp_list(new_pip, offset) 

219 ) 

220 raise TypeError(f"Offset {offset} has a wrong type!") 

221 

222 def check_offset(self, new_pip, offset): 

223 """Check offset validity""" 

224 if is_zero_d(offset): 

225 return (new_pip.pp == 1) and (new_pip.vpp == 1) 

226 if is_one_d(offset): 

227 return ( 

228 len(offset) == new_pip.pp 

229 and sum(offset) == self.layers % new_pip.pp 

230 ) 

231 if is_two_d(offset): 

232 return ( 

233 len(offset) == new_pip.vpp 

234 and all(len(stages) == new_pip.pp for stages in offset) 

235 and sum(sum(stages) for stages in offset) 

236 == self.layers % (new_pip.chunk_stage()) 

237 ) 

238 return False 

239 

240 def offset_checker(self, new_pp, new_vpp, offset): 

241 """Log an error message if offset is invalid""" 

242 new_pip = Pipeline(new_pp, new_vpp) 

243 if not self.check_offset(new_pip, offset): 

244 logger.error( 

245 "offset %s is wrong!! pp = %d, vpp = %d, L = %d", 

246 str(offset), 

247 new_pip.pp, 

248 new_pip.vpp, 

249 self.layers, 

250 ) 

251 return False 

252 return True 

253 

254 def make_valid(self, new_pip, offset): 

255 """Transform an invalid offset into a valid one""" 

256 logger.debug("offset to make valid = %s", str(offset)) 

257 flat = make_one_d(offset) 

258 

259 delta = self.layers % (new_pip.chunk_stage()) - sum(flat) 

260 logger.debug("delta = %s", str(delta)) 

261 if delta < 0: 

262 for _ in range(-delta): 

263 top = max(flat) 

264 for i, _ in enumerate(flat): 

265 if flat[i] == top: 

266 flat[i] = flat[i] - 1 

267 break 

268 elif delta > 0: 

269 for _ in range(delta): 

270 bot = min(flat) 

271 for i, _ in enumerate(flat): 

272 # for i, _ in reversed(list(enumerate(flat))): 

273 if flat[i] == bot: 

274 flat[i] = flat[i] + 1 

275 break 

276 logger.debug("valid flat offset = %s", str(flat)) 

277 

278 # cases where sum > (new_pip.vpp * new_pip.pp) 

279 delta = sum(flat) // (new_pip.chunk_stage()) 

280 for i, _ in enumerate(flat): 

281 flat[i] -= delta 

282 

283 logger.debug("new flat offset = %s", str(flat)) 

284 if new_pip.vpp == 1: 

285 return flat 

286 

287 for v in range(new_pip.vpp): 

288 for s in range(new_pip.pp): 

289 offset[v][s] = flat[v * new_pip.pp + s] 

290 return offset 

291 

292 def default_offset(self, new_pip): 

293 """Construct default offset""" 

294 rest = self.layers % (new_pip.chunk_stage()) 

295 offset = [] 

296 for v in range(new_pip.vpp): 

297 offset.insert(0, []) 

298 for s in range(new_pip.pp): 

299 if rest > 0 and v > 0 and s > 0: 

300 offset[0].insert(0, 1) 

301 rest -= 1 

302 else: 

303 offset[0].insert(0, 0) 

304 if offset[0][0] == 1: 

305 offset[0][0] = 0 

306 offset[-1][-1] = 1 

307 return offset 

308 

309 def default_recompute(self, _, recompute_cfg): 

310 """Construct Default recompute config when not taken from config""" 

311 logger.debug( 

312 "recompute config has type %s and value %s", 

313 type(recompute_cfg), 

314 str(recompute_cfg), 

315 ) 

316 return True 

317 

318 

319def copy_offset(offset): 

320 """Copy an offset""" 

321 if is_zero_d(offset): 

322 return offset 

323 return copy.deepcopy(offset) 

324 

325 

326def is_zero_d(offset): 

327 """Check if offset is an int""" 

328 return isinstance(offset, int) 

329 

330 

331def is_one_d(offset): 

332 """Check if offset is an int list""" 

333 return all(isinstance(x, int) for x in offset) 

334 

335 

336def is_two_d(offset): 

337 """Check if offset is an int list list""" 

338 return all(isinstance(x, int) for stages in offset for x in stages) 

339 

340 

341def make_one_d(offset): 

342 """Transform offset is an int list""" 

343 if is_zero_d(offset): 

344 return [0] 

345 if is_one_d(offset): 

346 return offset 

347 if is_two_d(offset): 

348 return [x for stages in offset for x in stages] 

349 raise TypeError(f"Offset {offset} has a wrong type!") 

350 

351 

352def make_two_d(offset): 

353 """Transform offset is an int list list""" 

354 if is_zero_d(offset): 

355 return [[0]] 

356 if is_one_d(offset): 

357 return [offset] 

358 if is_two_d(offset): 

359 return offset 

360 raise TypeError(f"Offset {offset} has a wrong type!")