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

101 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"""Sort, retain and export recompute plans at fixed parallel degrees. 

16 

17What a plan costs here is the cost model's own estimate of the step under 

18it, the same estimate the search ranks strategies with. A measured run can 

19disagree with it, so the sequence reads as what the cost model expects each 

20further recomputed layer to cost, not as a measured or guaranteed figure. 

21""" 

22 

23import csv 

24import json 

25import math 

26from collections import Counter 

27from pathlib import Path 

28from typing import Any, Dict, List, Mapping, Optional, Sequence, Tuple 

29 

30from hyper_parallel.auto_parallel.sapp_nd.nd.dimensions import Dimensions 

31from hyper_parallel.auto_parallel.sapp_nd.recompute.candidate import MODES, RecomputeChoice, to_records, trainer_plan 

32 

33# The configurations a plot shows when the run asked for no particular number. 

34DEFAULT_TOP = 20 

35 

36# The columns of the CSV a retained sequence is written to. 

37_FIXED_COLUMNS = ( 

38 "changed_layer", "full_layers", "memory_mb", "score", "recompute_mode", "recompute_layers", 

39 "layer_recompute", "stage_memory_mb", "stage_savings", "plan_scope", 

40) 

41 

42 

43def sort_recompute_results(results: Sequence[RecomputeChoice]) -> List[RecomputeChoice]: 

44 """Return recompute plans fastest first by the cost model's estimate, peak memory breaking ties. 

45 

46 Args: 

47 results: Scored plans to order, which are not modified. 

48 

49 Returns: 

50 The plans in ascending score order. 

51 

52 Raises: 

53 ValueError: A plan has no finite score. 

54 """ 

55 if any(result.score is None or not math.isfinite(result.score) for result in results): 

56 raise ValueError("recompute results must have finite performance scores before sorting") 

57 return sorted(results, key=lambda result: (result.score, result.memory)) 

58 

59 

60def _layer_settings(choice: RecomputeChoice) -> Dict[int, Tuple[Any, str, Any]]: 

61 """Expand a named plan per layer, independently of how its consecutive ranges are grouped.""" 

62 defined: Dict[int, Tuple[Any, str, Any]] = {} 

63 for item in choice.ranges: 

64 if item.first < 0 or item.count <= 0 or item.mode not in MODES: 

65 raise ValueError("recompute potentials require valid ranges with a named mode per layer") 

66 for index in range(item.first, item.first + item.count): 

67 if index in defined: 

68 raise ValueError("recompute layer ranges must not overlap") 

69 defined[index] = (item.kind, item.mode, item.option) 

70 return defined 

71 

72 

73def _one_more_full( 

74 defined: Mapping[int, Tuple[Any, str, Any]], proposed: Mapping[int, Tuple[Any, str, Any]] 

75) -> bool: 

76 """Whether *proposed* runs one more layer of one kind full and leaves every other count alone.""" 

77 if any(proposed[index][0] != defined[index][0] or proposed[index][2].link_bandwidth for index in defined): 

78 return False 

79 before = Counter((kind, mode) for kind, mode, _ in defined.values()) 

80 after = Counter((kind, mode) for kind, mode, _ in proposed.values()) 

81 removed, added = before - after, after - before 

82 if sum(removed.values()) != 1 or sum(added.values()) != 1: 

83 return False 

84 old_kind, old_mode = next(iter(removed)) 

85 new_kind, new_mode = next(iter(added)) 

86 return old_kind == new_kind and old_mode != "full" and new_mode == "full" 

87 

88 

89def keep_recompute_potential( 

90 current: RecomputeChoice, 

91 kept: List[RecomputeChoice], 

92 results: Sequence[RecomputeChoice], 

93) -> Optional[RecomputeChoice]: 

94 """Append and return the fastest unused plan that recomputes one more layer of one kind. 

95 

96 Args: 

97 current: The plan whose degrees, kinds and other mode counts are kept. 

98 kept: The plans retained so far; a qualifying plan is appended in place. 

99 results: The scored alternatives, which need not be sorted. 

100 

101 Returns: 

102 The appended plan, or ``None`` when no unused qualifying plan is left. 

103 

104 Raises: 

105 ValueError: A plan has no finite score, or no valid plan per layer. 

106 """ 

107 defined = _layer_settings(current) 

108 previous = [(choice.dimensions, _layer_settings(choice)) for choice in kept] 

109 for result in sort_recompute_results(results): 

110 if result.dimensions != current.dimensions: 

111 continue 

112 proposed = _layer_settings(result) 

113 if proposed.keys() != defined.keys() or not _one_more_full(defined, proposed): 

114 continue 

115 if any(dimensions == result.dimensions and layers == proposed for dimensions, layers in previous): 

116 continue 

117 kept.append(result) 

118 return result 

119 return None 

120 

121 

122def recompute_next_plot_groups( 

123 scored_space: Sequence[tuple], 

124 results: Mapping[Dimensions, Sequence[RecomputeChoice]], 

125 top_num: Optional[int] = None, 

126) -> List[Tuple[Dimensions, List[RecomputeChoice]]]: 

127 """Select the plans below each next strategy's score, and the next best beyond it. 

128 

129 Args: 

130 scored_space: The parallel configurations in the search's order. 

131 results: The scored alternatives of each configuration, its own plan 

132 among them. 

133 top_num: How many configurations to show; :data:`DEFAULT_TOP` when 

134 omitted. 

135 

136 Returns: 

137 The groups in the search's order, each sorted by score. A group holds 

138 every plan the cost model scores below the next strategy, and the 

139 first plan at or above it. It keeps at least two plans where there 

140 are two, so a group shows an alternative even when the scores tie. 

141 The last group takes the mean size of the groups before it, rounded 

142 half up, at least two and at most what it has; with one 

143 configuration alone it takes up to two plans. 

144 """ 

145 top = scored_space[:max(0, DEFAULT_TOP if top_num is None else top_num)] 

146 if not top: 

147 return [] 

148 groups = [] 

149 for entry, following in zip(top, top[1:]): 

150 config = entry[0] 

151 choices = sort_recompute_results(results.get(config, ())) 

152 count = sum(choice.score < following[2] for choice in choices) 

153 groups.append((config, choices[:max(2, count + 1)])) 

154 count = math.floor(sum(len(choices) for _, choices in groups) / len(groups) + 0.5) if groups else 2 

155 config = top[-1][0] 

156 groups.append((config, sort_recompute_results(results.get(config, ()))[:max(2, count)])) 

157 return groups 

158 

159 

160def recompute_plot_data( 

161 groups: Sequence[Tuple[Dimensions, Sequence[RecomputeChoice]]], 

162) -> List[Tuple[Dimensions, List[dict]]]: 

163 """Prepare the selected plans as chart records, without changing their configuration ranks. 

164 

165 Args: 

166 groups: The selected plans, grouped by fixed parallel configuration. 

167 

168 Returns: 

169 The groups with each plan's score, its parts, its peak memory, the 

170 count of fully recomputed layers and the plan the trainer reads. 

171 """ 

172 prepared = [] 

173 for config, choices in groups: 

174 records = [] 

175 for choice in choices: 

176 mode, layers = trainer_plan(choice) if choice.ranges or choice.mode is not None else (None, None) 

177 records.append({ 

178 "score": choice.score, 

179 "parts": choice.score_parts, 

180 "memory": choice.memory, 

181 "mode": mode, 

182 "layers": layers, 

183 "full_layers": _full_layers(choice), 

184 }) 

185 prepared.append((config, records)) 

186 return prepared 

187 

188 

189def _full_layers(choice: RecomputeChoice) -> Optional[int]: 

190 """How many layers the plan fully recomputes; ``None`` for a plan that is not stated per layer.""" 

191 if not choice.ranges: 

192 return None 

193 return sum(item.count for item in choice.ranges if item.mode == "full") 

194 

195 

196def _plan_scope(choice: RecomputeChoice) -> str: 

197 """How wide a plan is stated: per layer, for the whole model, with offload, or not at all.""" 

198 if any(item.option.link_bandwidth for item in choice.ranges): 

199 return "offload" 

200 if choice.ranges: 

201 return "per_layer" 

202 return "whole_model" if choice.mode is not None else "unavailable" 

203 

204 

205def _csv_row( 

206 choice: RecomputeChoice, 

207 rank: Any, 

208 step: int, 

209 previous: Mapping[int, Tuple[Any, str, Any]], 

210) -> Dict[str, Any]: 

211 """One row of the sequence: the plan, what it changed against the step before, and its estimate.""" 

212 defined = _layer_settings(choice) 

213 changed = [index for index, setting in defined.items() if step and setting != previous[index]] 

214 mode, layers = trainer_plan(choice) if choice.ranges or choice.mode is not None else ("", {}) 

215 row = dict(choice.dimensions) 

216 row.update({ 

217 "TOP": rank, 

218 "step": step, 

219 "changed_layer": ",".join(map(str, changed)), 

220 "full_layers": _full_layers(choice) if choice.ranges else "", 

221 "memory_mb": choice.memory, 

222 "score": choice.score, 

223 "recompute_mode": mode, 

224 "recompute_layers": json.dumps(layers), 

225 "layer_recompute": json.dumps(to_records(choice)), 

226 "stage_memory_mb": json.dumps(choice.stage_memory), 

227 "stage_savings": json.dumps(choice.stage_savings), 

228 "plan_scope": _plan_scope(choice), 

229 }) 

230 return row 

231 

232 

233def write_recompute_csv(potentials: Mapping[Dimensions, Sequence[RecomputeChoice]], path: str) -> None: 

234 """Replace a CSV with each fixed strategy's retained recompute sequence. 

235 

236 The score of every row is the cost model's estimate of the step under 

237 that plan, so a difference between two rows is what the cost model 

238 expects the extra recomputed layer to cost. 

239 

240 Args: 

241 potentials: The retained plans in sequence order, grouped by 

242 parallel configuration. 

243 path: The CSV to write, whose parent directory is created where it 

244 is missing. 

245 """ 

246 target = Path(path) 

247 target.parent.mkdir(parents=True, exist_ok=True) 

248 degrees = list(dict.fromkeys( 

249 name for choices in potentials.values() for choice in choices for name, _ in choice.dimensions 

250 )) 

251 with target.open("w", encoding="utf-8", newline="") as handle: 

252 writer = csv.DictWriter(handle, fieldnames=["TOP", "step"] + degrees + list(_FIXED_COLUMNS)) 

253 writer.writeheader() 

254 for config, choices in potentials.items(): 

255 previous: Mapping[int, Tuple[Any, str, Any]] = {} 

256 for step, choice in enumerate(choices): 

257 writer.writerow(_csv_row(choice, config.rank, step, previous)) 

258 previous = _layer_settings(choice)