Coverage for  / home / jenkins / .local / lib / python3.10 / site-packages / hyper_parallel / auto_parallel / sapp_nd / recompute / knapsack.py: 99%

139 statements  

« 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"""Choosing each layer's option under a memory budget. 

16 

17Every layer runs one option of its kind's front. At a stage that keeps k 

18micro-batches of a layer in flight, an option keeps k times its memory per 

19micro-batch plus its memory held once, less what those charge beyond the 

20layer at k (:meth:`~.front.LayerOption.memory`), and costs its forward and 

21backward time. Choosing one option per layer so that a stage's layers are as 

22fast as possible within the memory left to them is a multiple-choice 

23knapsack. :func:`choose` solves it exactly over memory counted in buckets, 

24each option's memory rounded up to a whole bucket, so a choice never keeps 

25more than the budget. 

26 

27Without pipeline parallelism the model is one stage, and :func:`choose` is 

28the whole answer. With it, :func:`pp_lite` solves every stage of a split and 

29times the pipeline by its slowest stage: a cheap score to rank candidates by 

30before the pipeline balancer places their layers unevenly. 

31""" 

32import math 

33from collections import Counter 

34from dataclasses import dataclass 

35from typing import Callable, Hashable, List, Mapping, Optional, Sequence, Tuple 

36 

37import numpy as np 

38 

39from hyper_parallel.auto_parallel.sapp_nd.recompute.front import LayerOption 

40 

41MEGABYTE = 2 ** 20 

42 

43 

44@dataclass(frozen=True) 

45class Layers: 

46 """Layers of one kind that a stage keeps as many micro-batches of in flight. 

47 

48 Attributes: 

49 kind: The key of the kind's front. 

50 count: How many layers. 

51 in_flight: How many micro-batches of each the stage keeps at once. 

52 """ 

53 

54 kind: Hashable 

55 count: int 

56 in_flight: int = 1 

57 

58 

59@dataclass(frozen=True) 

60class Assignment: 

61 """How many of a group's layers run one option.""" 

62 

63 layers: Layers 

64 option: LayerOption 

65 count: int 

66 

67 

68@dataclass(frozen=True) 

69class StageChoice: 

70 """The options a stage's layers run, and what they cost together. 

71 

72 Attributes: 

73 assignments: Per group of layers, how many run each option, the 

74 slower options first. 

75 memory: The bytes the layers keep, each option's memory counted 

76 exactly rather than in buckets. 

77 time: The layers' forward and backward time. 

78 """ 

79 

80 assignments: Tuple[Assignment, ...] 

81 memory: float 

82 time: float 

83 

84 

85@dataclass(frozen=True) 

86class Stage: 

87 """A pipeline stage: its layers, the memory left to them, and its time outside them. 

88 

89 Attributes: 

90 groups: The stage's layers. 

91 budget: The bytes the layers may keep: the device's memory less 

92 everything on the stage that the choice does not change. 

93 fixed_time: The stage's time outside its layers, the embedding's or 

94 the output layer's. 

95 """ 

96 

97 groups: Tuple[Layers, ...] 

98 budget: float 

99 fixed_time: float = 0.0 

100 

101 

102@dataclass(frozen=True) 

103class PipelineChoice: 

104 """The choice of every stage, and each stage's time.""" 

105 

106 stages: Tuple[StageChoice, ...] 

107 times: Tuple[float, ...] 

108 

109 @property 

110 def slowest(self) -> float: 

111 """The slowest stage's time, which the pipeline runs at.""" 

112 return max(self.times) 

113 

114 

115def _kept(option: LayerOption, in_flight: int) -> float: 

116 """The bytes a layer running *option* keeps with *in_flight* micro-batches in flight.""" 

117 return option.memory(in_flight) 

118 

119 

120def _time(option: LayerOption) -> float: 

121 """A layer's forward and backward time under *option*.""" 

122 return option.forward_time + option.backward_time 

123 

124 

125def _useful(options: Sequence[LayerOption], in_flight: int) -> List[LayerOption]: 

126 """The options no other beats on memory and time at *in_flight* micro-batches in flight.""" 

127 kept, fastest = [], math.inf 

128 for option in sorted(options, key=lambda option: (_kept(option, in_flight), _time(option))): 

129 if _time(option) < fastest: 

130 kept.append(option) 

131 fastest = _time(option) 

132 return kept 

133 

134 

135def choose( 

136 groups: Sequence[Layers], 

137 fronts: Mapping[Hashable, Sequence[LayerOption]], 

138 budget: float, 

139 bucket: float = MEGABYTE, 

140) -> Optional[StageChoice]: 

141 """The fastest options for a stage's layers that keep at most *budget* bytes. 

142 

143 Of the fastest choices, the one that keeps the least memory. 

144 

145 Args: 

146 groups: The stage's layers, by kind and micro-batches in flight. 

147 fronts: Each kind's options. 

148 budget: The bytes the layers may keep. 

149 bucket: The memory granularity, in bytes. Each option's memory is 

150 rounded up to a whole bucket, so a coarser bucket solves faster 

151 and may give up a little of the budget. 

152 

153 Returns: 

154 The choice, or ``None`` when not even the lightest options fit. 

155 """ 

156 if budget < 0: 

157 return None 

158 useful = [_useful(fronts[group.kind], group.in_flight) for group in groups] 

159 if sum(group.count * _kept(options[-1], group.in_flight) for group, options in zip(groups, useful)) <= budget: 

160 # Every layer's fastest option fits: nothing to choose. 

161 return _stage_choice( 

162 [Assignment(group, options[-1], group.count) for group, options in zip(groups, useful) if group.count] 

163 ) 

164 size = int(budget // bucket) 

165 # fastest[w]: the least time of the layers seen so far keeping exactly w buckets. 

166 fastest = np.full(size + 1, np.inf) 

167 fastest[0] = 0.0 

168 steps = [] 

169 for position, (group, options) in enumerate(zip(groups, useful)): 

170 weights = [math.ceil(_kept(option, group.in_flight) / bucket) for option in options] 

171 for _ in range(group.count): 

172 after = np.full(size + 1, np.inf) 

173 picked = np.full(size + 1, -1, dtype=np.int16) 

174 for index, (option, weight) in enumerate(zip(options, weights)): 

175 if weight > size: 

176 continue 

177 candidate = fastest[:size + 1 - weight] + _time(option) 

178 better = candidate < after[weight:] 

179 after[weight:][better] = candidate[better] 

180 picked[weight:][better] = index 

181 fastest = after 

182 steps.append((position, weights, picked)) 

183 if not np.isfinite(fastest).any(): 

184 return None 

185 used = int(np.argmin(fastest)) 

186 counts = Counter() 

187 for position, weights, picked in reversed(steps): 

188 index = int(picked[used]) 

189 counts[position, index] += 1 

190 used -= weights[index] 

191 assignments = [] 

192 for position, (group, options) in enumerate(zip(groups, useful)): 

193 chosen = [(options[index], count) for (found, index), count in counts.items() if found == position] 

194 for option, count in sorted(chosen, key=lambda pair: -_time(pair[0])): 

195 assignments.append(Assignment(group, option, count)) 

196 return _stage_choice(assignments) 

197 

198 

199def least_times( 

200 groups: Sequence[Layers], 

201 fronts: Mapping[Hashable, Sequence[LayerOption]], 

202 budget: float, 

203 bucket: float = MEGABYTE, 

204) -> Callable[[float], float]: 

205 """The least time of a stage's layers within every budget up to *budget*, as :func:`choose` finds it. 

206 

207 One pass over the layers, as :func:`choose` makes for one budget, answers 

208 all of them. 

209 

210 Args: 

211 groups: The stage's layers, by kind and micro-batches in flight. 

212 fronts: Each kind's options. 

213 budget: The largest budget asked, in bytes. 

214 bucket: The memory granularity, as for :func:`choose`. 

215 

216 Returns: 

217 For a budget in bytes, the time of the choice :func:`choose` makes 

218 within it; ``inf`` where none fits. 

219 """ 

220 useful = [_useful(fronts[group.kind], group.in_flight) for group in groups] 

221 heaviest = sum(group.count * _kept(options[-1], group.in_flight) for group, options in zip(groups, useful)) 

222 fastest_time = sum(group.count * _time(options[-1]) for group, options in zip(groups, useful)) 

223 # Past the fastest options' memory every budget has them. 

224 size = int(max(0.0, min(budget, heaviest)) // bucket) 

225 fastest = np.full(size + 1, np.inf) 

226 fastest[0] = 0.0 

227 for group, options in zip(groups, useful): 

228 weights = [math.ceil(_kept(option, group.in_flight) / bucket) for option in options] 

229 for _ in range(group.count): 

230 after = np.full(size + 1, np.inf) 

231 for option, weight in zip(options, weights): 

232 if weight <= size: 

233 np.minimum(after[weight:], fastest[:size + 1 - weight] + _time(option), out=after[weight:]) 

234 fastest = after 

235 within = np.minimum.accumulate(fastest) 

236 

237 def _within(room: float) -> float: 

238 """The least time within *room* bytes.""" 

239 if room < 0: 

240 return math.inf 

241 if heaviest <= room: 

242 return fastest_time 

243 return float(within[min(int(room // bucket), size)]) 

244 

245 return _within 

246 

247 

248def suffix_times( 

249 layers: Sequence[Layers], 

250 fronts: Mapping[Hashable, Sequence[LayerOption]], 

251 budgets: Sequence[float], 

252 bucket: float = MEGABYTE, 

253) -> List[float]: 

254 """For every start, the least time of the layers from it on that keep at most its budget. 

255 

256 One pass over the layers from the last to the first, as :func:`choose` 

257 solves one stage, gives every suffix's fastest choice at once. 

258 

259 Args: 

260 layers: The layers in order, one :class:`Layers` of count 1 each. 

261 fronts: Each kind's options. 

262 budgets: One budget in bytes per start, ``len(layers) + 1`` of them; 

263 the last is the empty suffix's. 

264 bucket: The memory granularity, as for :func:`choose`. 

265 

266 Returns: 

267 ``len(layers) + 1`` times, ``inf`` where a suffix cannot fit its 

268 budget. 

269 """ 

270 if len(budgets) != len(layers) + 1: 

271 raise ValueError(f"{len(budgets)} budgets for {len(layers)} layers; expected one more") 

272 size = int(max(0.0, *budgets) // bucket) 

273 fastest = np.full(size + 1, np.inf) 

274 fastest[0] = 0.0 

275 

276 def _least(start: int) -> float: 

277 """The least time of the suffix now in *fastest* within its budget.""" 

278 if budgets[start] < 0: 

279 return math.inf 

280 return float(np.min(fastest[:int(budgets[start] // bucket) + 1])) 

281 

282 times = [math.inf] * (len(layers) + 1) 

283 times[-1] = _least(len(layers)) 

284 for start in range(len(layers) - 1, -1, -1): 

285 layer = layers[start] 

286 after = np.full(size + 1, np.inf) 

287 for option in _useful(fronts[layer.kind], layer.in_flight): 

288 weight = math.ceil(_kept(option, layer.in_flight) / bucket) 

289 if weight > size: 

290 continue 

291 np.minimum(after[weight:], fastest[:size + 1 - weight] + _time(option), out=after[weight:]) 

292 fastest = after 

293 times[start] = _least(start) 

294 return times 

295 

296 

297def _stage_choice(assignments: Sequence[Assignment]) -> StageChoice: 

298 """A stage's assignments, with the memory they keep and the time they take.""" 

299 return StageChoice( 

300 assignments=tuple(assignments), 

301 memory=sum(item.count * _kept(item.option, item.layers.in_flight) for item in assignments), 

302 time=sum(item.count * _time(item.option) for item in assignments), 

303 ) 

304 

305 

306def pp_lite( 

307 stages: Sequence[Stage], fronts: Mapping[Hashable, Sequence[LayerOption]], bucket: float = MEGABYTE 

308) -> Optional[PipelineChoice]: 

309 """The fastest choice for every stage, each within its own budget. 

310 

311 Stages share no memory, so choosing each stage's fastest options also 

312 makes the slowest stage as fast as it can be. 

313 

314 Args: 

315 stages: The pipeline's stages. 

316 fronts: Each kind's options. 

317 bucket: The memory granularity, as for :func:`choose`. 

318 

319 Returns: 

320 The choice, or ``None`` when some stage cannot fit. 

321 """ 

322 choices = [] 

323 for stage in stages: 

324 choice = choose(stage.groups, fronts, stage.budget, bucket) 

325 if choice is None: 

326 return None 

327 choices.append(choice) 

328 return PipelineChoice( 

329 stages=tuple(choices), 

330 times=tuple(choice.time + stage.fixed_time for choice, stage in zip(choices, stages)), 

331 )