Coverage for  / home / jenkins / .local / lib / python3.10 / site-packages / hyper_parallel / auto_parallel / sapp_ppb / pp_config_builder / layer_loader.py: 88%

104 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"""Layer loader — load sapp-ppb Layer objects from native JSON and post-process.""" 

16 

17from __future__ import annotations 

18 

19import logging 

20import os 

21from typing import Any, Dict, List, Optional 

22 

23from hyper_parallel.auto_parallel.sapp_ppb.pp_config_builder.yaml_parser import ( 

24 YamlOptimizationConfig, 

25) 

26 

27try: 

28 from hyper_parallel.auto_parallel.sapp_ppb.sapp.sapp_pipeline import SappPipeline 

29 from hyper_parallel.auto_parallel.sapp_ppb.utils import recompute as Recompute 

30 from hyper_parallel.auto_parallel.sapp_ppb.utils.layer import generate_layers_list 

31except ImportError: 

32 SAPP_PPB_AVAILABLE = False 

33 SappPipeline = None 

34 Recompute = None 

35 generate_layers_list = None # type: ignore[assignment] 

36else: 

37 SAPP_PPB_AVAILABLE = True 

38 

39logger = logging.getLogger(__name__) 

40 

41 

42def _get_pipeline_layer_class() -> Any: 

43 """Return the ``Layer`` class that ``SappPipeline`` actually uses. 

44 

45 ``SappPipeline`` imports ``Layer`` from ``sapp_ppb.utils.layer``. 

46 Depending on import order, the top-level ``sapp_ppb`` package may 

47 resolve to the ``hyper_parallel.auto_parallel.sapp_ppb`` namespace, 

48 creating a *different* ``Layer`` class (with a different 

49 ``type_enum``) than what ``SappPipeline`` uses internally. To 

50 avoid ``filter_layer_type`` returning empty lists due to enum 

51 identity mismatches, we always retrieve ``Layer`` from 

52 ``SappPipeline``'s own module globals. 

53 """ 

54 if SappPipeline is None: 

55 return None 

56 return SappPipeline.__init__.__globals__.get("Layer") 

57 

58 

59def _apply_recompute_considered( 

60 layer: Any, 

61 recompute_considered: Dict[Any, bool], 

62) -> None: 

63 """Override ``recompute_considered_`` on a sapp-ppb Layer object. 

64 

65 The ``Layer`` constructor ignores the ``None`` semantics in 

66 ``backward_time_rec`` and ``memory_activation_rec`` — it treats 

67 ``None`` as "auto-fill with DEFAULT_COEF" rather than "disable". This 

68 method forces the Layer's ``recompute_considered_`` to match the 

69 desired inference so that the ILP solver only creates decision 

70 variables for genuinely configured recompute types. 

71 

72 Args: 

73 layer: A sapp-ppb Layer object with a ``recompute_considered_`` 

74 dict attribute. 

75 recompute_considered: Desired mapping from 

76 :class:`Recompute.TYPE` to bool. 

77 """ 

78 for rec_type in Recompute.TYPE: 

79 layer.recompute_considered_[rec_type] = recompute_considered.get( 

80 rec_type, False, 

81 ) 

82 layer.compute_internal_time() 

83 

84 

85def _head_tail_recompute_considered() -> Dict[Any, bool]: 

86 """Return ``recompute_considered`` for HEAD/TAIL layers. 

87 

88 HEAD and TAIL layers never participate in recompute decisions; only 

89 ``NONE`` is considered. 

90 

91 Returns: 

92 Dict mapping each :class:`Recompute.TYPE` to ``False`` except 

93 ``NONE`` which is ``True``. 

94 """ 

95 return {r: (r == Recompute.TYPE.NONE) for r in Recompute.TYPE} 

96 

97 

98class LayerBuilder: 

99 """Build sapp-ppb Layer objects from YAML config + native JSON. 

100 

101 Uses :func:`generate_layers_list` from the native sapp-ppb parser 

102 to read the ``layers_description`` from the JSON file, then applies 

103 recompute-considered overrides and consistency validation to produce 

104 a list of sapp-ppb ``Layer`` objects that can be consumed by 

105 :class:`PPBalancer`. 

106 

107 Args: 

108 yaml_config: YAML configuration with pipeline topology and 

109 ILP constraints. 

110 json_path: Path to the native sapp-ppb JSON file (containing 

111 ``layers_description``). 

112 

113 Example: 

114 >>> builder = LayerBuilder(yaml_config, json_path) 

115 >>> layers = builder.layers_sapp_ppb 

116 """ 

117 

118 def __init__( 

119 self, 

120 yaml_config: YamlOptimizationConfig, 

121 json_path: str, 

122 ) -> None: 

123 """Initialize LayerBuilder. 

124 

125 Args: 

126 yaml_config: YAML configuration with pipeline topology. 

127 json_path: Path to the native sapp-ppb JSON file. 

128 

129 Raises: 

130 ImportError: If sapp-ppb module is not available. 

131 ValueError: If ``json_path`` is empty or layers cannot be 

132 parsed. 

133 """ 

134 if not SAPP_PPB_AVAILABLE: 

135 raise ImportError( 

136 "sapp-ppb module is not available. " 

137 "Please ensure sapp-ppb is installed and accessible." 

138 ) 

139 

140 if not json_path: 

141 raise ValueError( 

142 "LayerBuilder requires a json_path. " 

143 "Please provide a valid JSON profile path." 

144 ) 

145 

146 yaml_config.validate() 

147 

148 self.yaml_config = yaml_config 

149 self._memory_limit = yaml_config.memory_limit 

150 self._constant_memory = yaml_config.constant_memory 

151 self._enable_simulation = yaml_config.enable_simulation 

152 self._use_backward_time = yaml_config.use_backward_time 

153 

154 layer_folder = os.path.dirname(json_path) 

155 model_name = os.path.splitext(os.path.basename(json_path))[0] 

156 

157 layers = generate_layers_list(layer_folder, model_name) 

158 if not layers: 

159 raise ValueError( 

160 f"No layers parsed from '{json_path}'. " 

161 f"Ensure the JSON file contains a 'layers_description' section." 

162 ) 

163 

164 self._post_process_layers(layers) 

165 self._validate_recompute_consistency(layers) 

166 self._validate_group_names(layers) 

167 self._validate_num_layer_consistency(layers) 

168 

169 self.layers_sapp_ppb = layers 

170 

171 @property 

172 def memory_limit(self) -> Optional[int]: 

173 """Maximum memory per stage in MB (from YAML config).""" 

174 return self._memory_limit 

175 

176 @property 

177 def constant_memory(self) -> int: 

178 """Constant memory overhead per stage in MB (from YAML config).""" 

179 return self._constant_memory 

180 

181 @property 

182 def use_backward_time(self) -> bool: 

183 """Whether to use backward time in ILP optimization.""" 

184 return self._use_backward_time 

185 

186 @staticmethod 

187 def _post_process_layers(layers: List[Any]) -> None: 

188 """Override recompute_considered on HEAD/TAIL layers. 

189 

190 For HEAD/TAIL layers, only NONE recompute is considered. 

191 BODY layers retain the recompute_considered mask inferred by 

192 the native ``Layer.find_recompute_considered()`` result. 

193 

194 Args: 

195 layers: List of sapp-ppb Layer objects from 

196 :func:`generate_layers_list`. 

197 """ 

198 pipeline_layer = _get_pipeline_layer_class() 

199 if pipeline_layer is None: 

200 return 

201 

202 for layer in layers: 

203 if layer.type_ in (pipeline_layer.type_enum.HEAD, pipeline_layer.type_enum.TAIL): 

204 _apply_recompute_considered( 

205 layer, _head_tail_recompute_considered(), 

206 ) 

207 

208 @staticmethod 

209 def _validate_recompute_consistency(layers: List[Any]) -> None: 

210 """Validate that all BODY groups share the same recompute_considered mask. 

211 

212 In a multi-body-group configuration, all BODY groups must enable 

213 the same set of recompute types so that the ILP solver can 

214 construct a coherent global mask. If different groups enable 

215 different recompute types, the solver would produce incorrect 

216 decision variable assignments. 

217 

218 Args: 

219 layers: List of sapp-ppb Layer objects with 

220 ``.recompute_considered_`` and ``.type_`` attributes. 

221 

222 Raises: 

223 ValueError: If two or more BODY groups have different 

224 ``recompute_considered_`` masks. 

225 """ 

226 pipeline_layer = _get_pipeline_layer_class() 

227 if pipeline_layer is None: 

228 return 

229 

230 body_groups: Dict[str, Any] = {} 

231 for lay in layers: 

232 if lay.type_ == pipeline_layer.type_enum.BODY: 

233 body_groups[lay.name_] = lay 

234 

235 if len(body_groups) <= 1: 

236 return 

237 

238 ref_name = next(iter(body_groups)) 

239 ref_mask = body_groups[ref_name].recompute_considered_ 

240 

241 inconsistent = [] 

242 for name, lay in body_groups.items(): 

243 if lay.recompute_considered_ != ref_mask: 

244 inconsistent.append(name) 

245 

246 if inconsistent: 

247 raise ValueError( 

248 f"All BODY groups must share the same recompute_considered mask, " 

249 f"but groups {inconsistent} differ from '{ref_name}'. " 

250 f"Please ensure all BODY groups enable the same set of recompute types." 

251 ) 

252 

253 @staticmethod 

254 def _validate_group_names(layers: List[Any]) -> None: 

255 """Validate that body group names do not conflict with ILP solver internal variables. 

256 

257 ``SappSolver._create_variables_to_solve_`` stores internal variables 

258 (e.g. ``max_stage_time``) and body-layer variables in the same 

259 ``variables_`` dict keyed by name. A body group whose name matches 

260 a solver internal variable would overwrite the internal entry, 

261 causing ``'list' object has no attribute 'varValue'`` errors during 

262 result extraction. 

263 

264 Args: 

265 layers: List of sapp-ppb Layer objects with ``.name_`` attribute. 

266 

267 Raises: 

268 ValueError: If any group name conflicts with a solver reserved name. 

269 """ 

270 from hyper_parallel.auto_parallel.sapp_ppb.sapp.sapp_solver import SappSolver # pylint: disable=C0415 

271 pipeline_layer = _get_pipeline_layer_class() 

272 if pipeline_layer is None: 

273 return 

274 

275 reserved = { 

276 v for k, v in vars(SappSolver).items() 

277 if isinstance(v, str) and k.isupper() 

278 } 

279 body_names = { 

280 lay.name_ for lay in layers 

281 if lay.type_ == pipeline_layer.type_enum.BODY 

282 } 

283 conflicts = body_names & reserved 

284 if conflicts: 

285 raise ValueError( 

286 f"body_groups names {conflicts} conflict with ILP solver " 

287 f"internal variables; please use different names" 

288 ) 

289 

290 def _validate_num_layer_consistency(self, layers: List[Any]) -> None: 

291 """Validate that YAML ``num_layer`` matches the actual body layer count from JSON. 

292 

293 When ``num_layer`` is specified in the YAML config, it must agree with 

294 the total ``nb_layer_`` of all BODY layers in the JSON profile. A 

295 mismatch would silently produce incorrect ILP results because the 

296 solver uses the JSON-derived count while the user expects the YAML 

297 value to be authoritative. 

298 

299 Args: 

300 layers: List of sapp-ppb Layer objects with ``.type_`` and 

301 ``.nb_layer_`` attributes. 

302 

303 Raises: 

304 ValueError: If ``num_layer`` is provided and does not match the 

305 total body layers from JSON. 

306 """ 

307 if self.yaml_config.num_layer is None: 

308 return 

309 

310 pipeline_layer = _get_pipeline_layer_class() 

311 if pipeline_layer is None: 

312 return 

313 

314 actual_body = sum( 

315 lay.nb_layer_ for lay in layers 

316 if lay.type_ == pipeline_layer.type_enum.BODY 

317 ) 

318 if actual_body != self.yaml_config.num_layer: 

319 raise ValueError( 

320 f"num_layer in YAML ({self.yaml_config.num_layer}) does not match " 

321 f"the total body layers from JSON ({actual_body}). " 

322 f"Please ensure both sources agree or omit num_layer in YAML." 

323 )