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

334 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"""Layer-description JSON generator and YAML-based dry-run configuration parser.""" 

16import json 

17import os 

18import random 

19from dataclasses import asdict, dataclass 

20from math import ceil 

21from typing import Any, Dict, List, Optional, Tuple, Union 

22 

23import numpy as np 

24import yaml 

25 

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

27from hyper_parallel.auto_parallel.sapp_ppb.sapp.sapp_solver import SappSolver 

28from hyper_parallel.auto_parallel.sapp_ppb.utils.check_rules import check_yaml_depth_before_loading 

29from hyper_parallel.auto_parallel.sapp_ppb.utils.computation_analyzer import ComputationAnalyzer 

30from hyper_parallel.auto_parallel.sapp_ppb.utils.compute_memory import ComputeMemory, Stage 

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

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

33 

34random.seed() 

35 

36 

37@dataclass 

38class LayersDescription: 

39 """layers description""" 

40 

41 name: str 

42 type: Layer.type_enum 

43 model_name: str 

44 time: int 

45 nb_layer: int 

46 memory_parameter: int 

47 

48 def __init__( 

49 self, layer_type: Layer.type_enum, time: int, nb_layer: int, model_name: str 

50 ) -> None: 

51 """Store layer metadata using the enum ``layer_type.name`` for both ``type`` and ``name``.""" 

52 self.type = layer_type.name 

53 self.time = time 

54 self.name = layer_type.name 

55 self.nb_layer = nb_layer 

56 self.model_name = model_name 

57 

58 

59@dataclass 

60class ModelInfo: 

61 """basic info of a model""" 

62 

63 name: str 

64 stage_const_mem: int 

65 layers_description: list[LayersDescription] 

66 

67 def __init__(self, model_name: str, head_time: int, body_time: int, 

68 tail_time: int, nb_layer: int) -> None: 

69 """Record HEAD/BODY/TAIL timing and initialise the stage-constant memory to 0.""" 

70 self.name = model_name 

71 self.stage_const_mem = 0 

72 self.to_json_ = None 

73 self.layers_description = [] 

74 self.layers_description.append( 

75 LayersDescription(Layer.type_enum.HEAD, head_time, 1, model_name) 

76 ) 

77 self.layers_description.append( 

78 LayersDescription(Layer.type_enum.BODY, body_time, nb_layer, model_name) 

79 ) 

80 self.layers_description.append( 

81 LayersDescription(Layer.type_enum.TAIL, tail_time, 1, model_name) 

82 ) 

83 

84 def get_layer_by_type(self, layer_type: Layer.type_enum) -> Optional[LayersDescription]: 

85 """Return the layer description matching ``layer_type`` or ``None`` if absent.""" 

86 for layer in self.layers_description: 

87 if layer.type == layer_type.name: 

88 return layer 

89 return None 

90 

91 def set_stage_const_mem(self, mem_const: int) -> None: 

92 """Override the per-stage constant memory component.""" 

93 self.stage_const_mem = mem_const 

94 

95 def layer_memory_update(self, mem_act: Dict[Recompute.TYPE, int], mem_par: int, 

96 mem_head: int, mem_tail: int) -> None: 

97 """Apply the solver-derived memory values onto the internal JSON payload.""" 

98 self.get_layer_by_type(Layer.type_enum.HEAD).memory_parameter = mem_head 

99 self.get_layer_by_type(Layer.type_enum.TAIL).memory_parameter = mem_tail 

100 self.get_layer_by_type(Layer.type_enum.BODY).memory_parameter = mem_par 

101 

102 json_data = asdict(self) 

103 

104 for rec in Recompute.TYPE: 

105 rec_mem = mem_act.get(rec) 

106 if rec_mem is None: 

107 continue 

108 for layer in json_data["layers_description"]: 

109 if layer["type"] == Layer.type_enum.BODY.name: 

110 layer[Recompute.JSON_MEMORY_NAME[rec]] = rec_mem 

111 self.to_json_ = json_data 

112 

113 def dump_json(self, file_name: str) -> None: 

114 """Write the current ``to_json_`` payload to ``file_name`` as indented JSON.""" 

115 with open(file_name, "w", encoding="utf-8") as json_file: 

116 json.dump(self.to_json_, json_file, indent=4) 

117 

118 

119def _as_list(value: Union[str, List[str]]) -> List[str]: 

120 """Wrap a bare string in a single-element list, leaving other values unchanged.""" 

121 return [value] if isinstance(value, str) else value 

122 

123 

124def _accumulate_layer_times(cost_list: Dict[str, float], predefined_layers: Dict[str, int], 

125 head_time: float, body_time: float, tail_time: float 

126 ) -> Tuple[float, float, float]: 

127 """Add each measured layer cost to the HEAD/TAIL/BODY total based on its marker.""" 

128 for layer, time in cost_list.items(): 

129 marker = predefined_layers.get(layer) 

130 if marker == 0: 

131 head_time += time 

132 elif marker == -1: 

133 tail_time += time 

134 else: 

135 body_time += time 

136 return head_time, body_time, tail_time 

137 

138 

139def time_parser(file_name: str, model_name: str) -> Tuple[float, float, float]: 

140 """Parse the HEAD/BODY/TAIL timing values from a SAPP-PPB YAML config file.""" 

141 if file_name is None: 

142 logger.error("input file cannot be none") 

143 raise ValueError("input file cannot be none") 

144 

145 if not file_name.endswith(("yaml", "yml")): 

146 logger.error("Only accept yaml as input format") 

147 raise ValueError(f"Only accept yaml as input format. not {file_name}") 

148 

149 filepath = os.path.realpath(file_name) 

150 with open(filepath, encoding="utf-8") as fp: 

151 check_yaml_depth_before_loading(fp) 

152 fp.seek(0) 

153 cfg_dict = yaml.safe_load(fp) 

154 

155 head_time = 0 

156 body_time = 0 

157 tail_time = 0 

158 

159 if "time_config" in cfg_dict: 

160 head_time = cfg_dict["time_config"].get("head") 

161 body_time = cfg_dict["time_config"].get("body") 

162 tail_time = cfg_dict["time_config"].get("tail") 

163 if {"head", "body", "tail"}.issubset(cfg_dict["time_config"]): 

164 return head_time, body_time, tail_time 

165 

166 if cfg_dict.get("profiling_config"): 

167 head_layers = cfg_dict["profiling_config"].get("head_layers", ["LlamaEmbedding"]) 

168 body_layers = cfg_dict["profiling_config"].get("body_layers", ["LLamaDecodeLayer"]) 

169 tail_layers = cfg_dict["profiling_config"].get("tail_layers", ["lm_head-Linear", "LlamaRMSNorm"]) 

170 head_layers = _as_list(head_layers) 

171 tail_layers = _as_list(tail_layers) 

172 body_layers = _as_list(body_layers) 

173 

174 num_layer = cfg_dict["pipeline_config"]["num_layer"] 

175 micro_batch_num = cfg_dict["profiling_config"]["micro_batch_num"] 

176 timeline_folder_path = cfg_dict["profiling_config"]["folder_path"] 

177 layer_list = { 

178 "pre_defined_layer": { 

179 **dict.fromkeys(head_layers, 0), 

180 **dict.fromkeys(tail_layers, -1), 

181 }, 

182 "auto_partition_layer": dict.fromkeys(body_layers, num_layer), 

183 } 

184 analyzer = ComputationAnalyzer(timeline_folder_path, model_name, micro_batch_num, layer_list) 

185 cost_list = analyzer.layer_with_cost_list 

186 logger.info(cost_list) 

187 head_time, body_time, tail_time = _accumulate_layer_times( 

188 cost_list, layer_list["pre_defined_layer"], head_time, body_time, tail_time 

189 ) 

190 

191 logger.info("head_time: %s, body_time: %s, tail_time: %s", head_time, body_time, tail_time) 

192 

193 return head_time, body_time, tail_time 

194 

195 

196def process_offset(offset: Union[int, List[int], List[List[int]]], 

197 pipeline_num: int) -> Tuple[Union[List[int], List[List[int]]], int]: 

198 """Normalise the YAML ``offset`` field into a list (or list of lists) and return the rounds.""" 

199 rounds = 1 

200 if isinstance(offset, int) and offset == 0: 

201 offset = [0] * pipeline_num 

202 # if offset is list of lists (usually when pp=4) 

203 elif isinstance(offset, list) and any(isinstance(item, list) for item in offset): 

204 tmp_offset = [] 

205 for item in offset: 

206 if isinstance(item, int) and item == 0: 

207 tmp_offset.append([0] * pipeline_num) 

208 elif not (isinstance(item, list) and len(item) == pipeline_num): 

209 raise ValueError(f"Unsupported input format offset: {item},", 

210 "please check the length of your offset list and the pipeline number") 

211 else: 

212 tmp_offset.append(item) 

213 offset = tmp_offset 

214 rounds = len(offset) 

215 elif not (isinstance(offset, list) and len(offset) == pipeline_num): 

216 raise ValueError(f"Unsupported input format offset: {offset},", 

217 "please check the length of your offset list and the pipeline number") 

218 

219 return offset, rounds 

220 

221 

222def process_rec_config( 

223 layer_per_stage: int, pipeline_num: int, offset: List[int], 

224 rec_config: Optional[Union[bool, List[int]]] 

225) -> Optional[List[List[int]]]: 

226 """Normalise a per-recomputation-type YAML entry into a two-dimensional list.""" 

227 if rec_config is None or offset is None: 

228 return None 

229 if isinstance(rec_config, bool): 

230 if rec_config: 

231 rec_config = [layer_per_stage] * pipeline_num 

232 rec_config = [recom + bias for recom, bias in (rec_config, offset)] 

233 else: 

234 rec_config = [0] * pipeline_num 

235 rec_config = [rec_config] 

236 elif isinstance(rec_config, list) and len(rec_config) == pipeline_num: 

237 # in order to be compatible with internal_from_yaml, change list into double list 

238 rec_config = [rec_config] 

239 else: 

240 raise ValueError(f"Unsupported input format recompute: {rec_config}, please check the length of list") 

241 

242 return rec_config 

243 

244 

245def instantiate_stage(stage_id: int, pipeline_num: int, nb_layer: int, 

246 layer_per_recompute: Dict[Recompute.TYPE, List[List[int]]], 

247 memory: int) -> Stage: 

248 """Build a :class:`Stage` snapshot from per-recomputation-type layer assignments.""" 

249 stage = Stage( 

250 sid=stage_id, 

251 nb_stage=pipeline_num, 

252 nb_layer=nb_layer, 

253 nb_layer_rec={ 

254 Recompute.TYPE.COMM: layer_per_recompute[Recompute.TYPE.COMM][0][stage_id], 

255 Recompute.TYPE.FULL: layer_per_recompute[Recompute.TYPE.FULL][0][stage_id], 

256 Recompute.TYPE.SLCT: layer_per_recompute[Recompute.TYPE.SLCT][0][stage_id], 

257 Recompute.TYPE.BOTH: layer_per_recompute[Recompute.TYPE.BOTH][0][stage_id], 

258 }, 

259 memory_usage=memory, 

260 ) 

261 return stage 

262 

263 

264def memory_parser(file_name: str) -> Tuple[int, List[Stage], int]: 

265 """Parse a SAPP-PPB memory-usage YAML file into solver-ready stage observations.""" 

266 if file_name is None: 

267 logger.error("input file cannot be none") 

268 raise ValueError("input file cannot be none") 

269 if not file_name.endswith("yaml") and not file_name.endswith("yml"): 

270 logger.error("Only accept yaml as input format") 

271 raise ValueError(f"Only accept yaml as input format. not {file_name}") 

272 

273 filepath = os.path.realpath(file_name) 

274 with open(filepath, encoding="utf-8") as fp: 

275 check_yaml_depth_before_loading(fp) 

276 fp.seek(0) 

277 cfg_dict = yaml.safe_load(fp) 

278 

279 # get pipeline config 

280 pipeline_num = cfg_dict["pipeline_config"]["pipeline_num"] 

281 num_layer = cfg_dict["pipeline_config"]["num_layer"] 

282 offset = cfg_dict["pipeline_config"]["offset"] 

283 

284 offset, rounds = process_offset(offset, pipeline_num) 

285 

286 layer_per_stage = int(num_layer / pipeline_num) 

287 

288 # get recompute config 

289 if rounds > 1: 

290 layer_per_recompute = [] 

291 for i in range(rounds): 

292 rec_config = {} 

293 for rec in Recompute.YAML_NAME.values(): 

294 rec_list = cfg_dict["recompute_config"].get(rec) 

295 if rec_list is None: 

296 continue 

297 rec_config[rec] = process_rec_config(layer_per_stage, pipeline_num, offset[i], rec_list[i]) 

298 rec_config[Recompute.OFFSET] = [offset[i]] 

299 layer_per_recompute.append( 

300 Recompute.internal_from_yaml(1, pipeline_num, rec_config, [[layer_per_stage] * pipeline_num]) 

301 ) 

302 else: 

303 rec_config = {} 

304 for rec in Recompute.YAML_NAME.values(): 

305 rec_list = cfg_dict["recompute_config"].get(rec) 

306 rec_config[rec] = process_rec_config(layer_per_stage, pipeline_num, offset, rec_list) 

307 rec_config[Recompute.OFFSET] = [offset] 

308 layer_per_recompute = Recompute.internal_from_yaml(1, pipeline_num, rec_config, 

309 [[layer_per_stage] * pipeline_num]) 

310 # get memory usage 

311 stage_id = cfg_dict["memory_usage"]["body_memories"]["stage_id"] 

312 mem_head_stage = cfg_dict["memory_usage"]["head_memory"] 

313 mem_tail_stage = cfg_dict["memory_usage"]["tail_memory"] 

314 body_memories = cfg_dict["memory_usage"]["body_memories"]["memories"] 

315 stages_a = [] 

316 if rounds > 1: 

317 for i in range(rounds): 

318 for idx, sg_id in enumerate(stage_id[i]): 

319 stages_a.append( 

320 instantiate_stage( 

321 sg_id, pipeline_num, 

322 layer_per_stage + offset[i][sg_id], 

323 layer_per_recompute[i], body_memories[i][idx], 

324 ) 

325 ) 

326 stages_a.append( 

327 instantiate_stage( 

328 0, pipeline_num, layer_per_stage + offset[i][0], 

329 layer_per_recompute[i], mem_head_stage, 

330 ) 

331 ) 

332 stages_a.append( 

333 instantiate_stage( 

334 pipeline_num - 1, pipeline_num, 

335 layer_per_stage + offset[i][pipeline_num - 1], 

336 layer_per_recompute[i], mem_tail_stage, 

337 ) 

338 ) 

339 else: 

340 for idx, sg_id in enumerate(stage_id): 

341 stages_a.append( 

342 instantiate_stage( 

343 sg_id, pipeline_num, layer_per_stage + offset[sg_id], 

344 layer_per_recompute, body_memories[idx], 

345 ) 

346 ) 

347 stages_a.append( 

348 instantiate_stage( 

349 0, pipeline_num, layer_per_stage + offset[0], 

350 layer_per_recompute, mem_head_stage, 

351 ) 

352 ) 

353 stages_a.append( 

354 instantiate_stage( 

355 pipeline_num - 1, pipeline_num, layer_per_stage + offset[pipeline_num - 1], 

356 layer_per_recompute, mem_tail_stage, 

357 ) 

358 ) 

359 

360 return pipeline_num, stages_a, num_layer 

361 

362 

363def initialize_layer_json(model_name: str, file_name: str) -> None: 

364 """Derive a layer-description JSON from a dry-run YAML and dump it to ``./layers``.""" 

365 num_stage, stages_a, num_layer = memory_parser(file_name) 

366 head_time, body_time, tail_time = time_parser(file_name, model_name) 

367 comp_mem = ComputeMemory(number_of_stage=num_stage, stages_a=stages_a) 

368 mi = ModelInfo(model_name, head_time, body_time, tail_time, num_layer) 

369 

370 mem_act = {} 

371 for r in Recompute.TYPE: 

372 if comp_mem.recompute_considered_[r]: 

373 mem_act[r] = int(comp_mem.get_memory_activation(r)) 

374 logger.info( 

375 "[INFO] %s = %f", Recompute.JSON_MEMORY_NAME[r], 

376 int(comp_mem.get_memory_activation(r)) 

377 ) 

378 if comp_mem.get_memory_const() is not None: 

379 mem_const = int(comp_mem.get_memory_const()) 

380 logger.info("[INFO] memory_const = %s", mem_const) 

381 mi.set_stage_const_mem(mem_const) 

382 mem_par = int(comp_mem.get_memory_parameter()) 

383 mem_tail = int(comp_mem.get_memory_tail()) 

384 mem_head = int(comp_mem.get_memory_head()) 

385 logger.info("[INFO] memory_parameter = %s", mem_par) 

386 logger.info("[INFO] memory_tail = %s", mem_tail) 

387 logger.info("[INFO] memory_head = %s", mem_head) 

388 

389 mi.layer_memory_update(mem_act, mem_par, mem_head, mem_tail) 

390 mi.dump_json(os.path.join("./layers", model_name + ".json")) 

391 

392 

393def get_stage_const_mem(layer_folder: str, model_name: str) -> int: 

394 """Read ``stage_const_mem`` from ``<layer_folder>/<model_name>.json`` (0 if missing).""" 

395 json_layer = os.path.join(layer_folder, model_name + '.json') 

396 with open(json_layer, encoding="utf-8") as json_file: 

397 json_data = json.load(json_file) 

398 if "stage_const_mem" in json_data: 

399 return json_data["stage_const_mem"] 

400 return 0 

401 

402 

403def _generate_offset_config(rounds, unknowns, target_sum, array_length): 

404 """Generate legal random offset arrays""" 

405 while True: 

406 offset_config_list = [] 

407 flat = [] 

408 for _ in range(rounds): 

409 offset_config = _generate_offset_array(target_sum, array_length) 

410 offset_config_slice = offset_config[1:] 

411 offset_config_slice = offset_config_slice[:-1] 

412 flat.append(offset_config_slice) 

413 offset_config_list.append(offset_config) 

414 flat = np.array(flat) 

415 flat = flat.flatten()[0 : unknowns + 1] 

416 if not np.all(flat == flat[0]): 

417 return offset_config_list 

418 

419 

420def _generate_offset_array(target_sum, array_length): 

421 """Generate a random offset array""" 

422 if target_sum == array_length: 

423 return [0] * array_length 

424 

425 if target_sum < array_length: 

426 logger.error("number of layers must be larger than stage number") 

427 return None 

428 

429 random_array = np.random.randint(1, 10, size=array_length) 

430 total_sum = random_array.sum() 

431 scaled_array = (random_array / total_sum) * target_sum 

432 scaled_array = np.round(scaled_array).astype(int) 

433 scaled_array = np.maximum(scaled_array, 1) 

434 current_sum = scaled_array.sum() 

435 diff = target_sum - current_sum 

436 

437 if diff >= 0: 

438 for i in range(abs(diff)): 

439 scaled_array[i] += 1 

440 else: 

441 count = 0 

442 for i in range(len(scaled_array)): 

443 # backwards iteration to avoid infinite loop 

444 if scaled_array[-1 - i] > 1: 

445 scaled_array[-1 - i] -= 1 

446 count += 1 

447 if count == abs(diff): 

448 break 

449 

450 baseline = target_sum // array_length 

451 offset = scaled_array - baseline 

452 return offset.tolist() 

453 

454 

455def _get_coef_matrix(pp, layer_per_stage, offset_config_list, rec_config_list, considered_rec): 

456 """get coef matrix of equations""" 

457 activation_nums = SappSolver.compute_activation_nums(pp, 1, 0)[0] 

458 coef_matrix = [] 

459 rounds = len(offset_config_list) 

460 for round_ in range(rounds): 

461 for stage in range(pp): 

462 if stage not in [0, pp - 1]: 

463 coef_matrix.append( 

464 [1, layer_per_stage + offset_config_list[round_][stage]] 

465 + Recompute.to_list( 

466 { 

467 rec: rec_config_list[round_][rec][stage] 

468 * activation_nums[stage] 

469 for rec in considered_rec 

470 } 

471 ) 

472 ) 

473 if len(coef_matrix) == 2 + len(considered_rec): 

474 return coef_matrix 

475 return None 

476 

477 

478def print_dryrun_config(offset_config_list: List[List[int]], 

479 rec_config_list: List[Dict[Recompute.TYPE, List[int]]]) -> None: 

480 """Log the dry-run YAML config that the user should execute to collect memory data.""" 

481 logger.output( 

482 "Please dryrun following config, %s round(s) is needed", 

483 len(offset_config_list), 

484 ) 

485 

486 for round_, offset_config in enumerate(offset_config_list): 

487 yaml_config = { 

488 Recompute.OFFSET: [], 

489 Recompute.YAML_NAME[Recompute.TYPE.FULL]: [], 

490 Recompute.YAML_NAME[Recompute.TYPE.SLCT]: [], 

491 Recompute.YAML_NAME[Recompute.TYPE.COMM]: [], 

492 } 

493 yaml_config[Recompute.OFFSET] = offset_config 

494 pp = len(offset_config) 

495 slct = rec_config_list[round_].get(Recompute.TYPE.SLCT, [0] * pp) 

496 comm = rec_config_list[round_].get(Recompute.TYPE.COMM, [0] * pp) 

497 full = rec_config_list[round_].get(Recompute.TYPE.FULL, [0] * pp) 

498 both = rec_config_list[round_].get(Recompute.TYPE.BOTH, [0] * pp) 

499 

500 yaml_config[Recompute.YAML_NAME[Recompute.TYPE.FULL]] = full 

501 yaml_config[Recompute.YAML_NAME[Recompute.TYPE.SLCT]] = [ 

502 x + y + z for x, y, z in zip(slct, both, full) 

503 ] 

504 yaml_config[Recompute.YAML_NAME[Recompute.TYPE.COMM]] = [ 

505 x + y + z for x, y, z in zip(comm, both, full) 

506 ] 

507 yaml_results = f"for round {round_ + 1}, please dryrun config:" 

508 for y, v in yaml_config.items(): 

509 yaml_results += f"\n\t{y}: {v}" 

510 logger.output(yaml_results) 

511 

512 

513def generate_solvable_config( 

514 pp: int, num_layers: int, 

515 considered_rec: List[Recompute.TYPE] 

516) -> Optional[Tuple[List[List[int]], List[Dict[Recompute.TYPE, List[int]]]]]: 

517 """Generate offset / recompute configs whose coefficient matrix is full-rank.""" 

518 if pp == 2: 

519 logger.error("pp = 2 is not supported yet") 

520 return None 

521 

522 considered_rec.append(Recompute.TYPE.NONE) 

523 rounds = ceil((2 + len(considered_rec)) / (pp - 2)) 

524 layer_per_stage = num_layers // pp 

525 is_solvable = False 

526 offset_config_list = _generate_offset_config( 

527 rounds, 2 + len(considered_rec), num_layers, pp 

528 ) 

529 while not is_solvable: 

530 rec_config_list = [] 

531 for round_ in range(rounds): 

532 offset_config = offset_config_list[round_] 

533 layer_per_recompute = {r: [0] * pp for r in considered_rec} 

534 for rec in considered_rec: 

535 stage_sum = [ 

536 sum(col) for col in zip(*layer_per_recompute.values()) 

537 ] # summation of each rec in each stage 

538 layers_left = [ 

539 offset_config[i] + layer_per_stage - stage_sum[i] for i in range(pp) 

540 ] 

541 layer_per_recompute[rec] = [ 

542 random.randint(0, layers_left[i]) for i in range(pp) 

543 ] 

544 rec_config_list.append(layer_per_recompute) 

545 

546 coef_matrix = _get_coef_matrix( 

547 pp, layer_per_stage, offset_config_list, rec_config_list, considered_rec 

548 ) 

549 coef_rank = np.linalg.matrix_rank(coef_matrix) 

550 if coef_rank == len(considered_rec) + 2: 

551 is_solvable = True 

552 

553 return offset_config_list, rec_config_list 

554 

555 

556def parse_training_config(yaml_path: str) -> Optional[Dict[str, Any]]: 

557 """Extract the training-YAML fields needed to compute operator shapes for seqpp.""" 

558 try: 

559 with open(yaml_path, "r", encoding="utf-8") as file: 

560 config = yaml.safe_load(file) 

561 

562 # Extract the requested values 

563 model_config = config["model"]["model_config"] 

564 parallel_config = config["parallel_config"] 

565 runner_config = config["runner_config"] 

566 

567 # Create a dictionary with the requested parameters 

568 extracted_params = { 

569 "num_heads": model_config["num_heads"], 

570 "hidden_size": model_config["hidden_size"], 

571 "head_dim": int(model_config["hidden_size"] / model_config["num_heads"]), 

572 "seq_length": model_config["seq_length"], 

573 "batch_size": runner_config["batch_size"], 

574 "vocab_size": model_config["vocab_size"], 

575 "data_parallel": parallel_config.get("data_parallel", 1), 

576 "model_parallel": parallel_config.get("model_parallel", 1), 

577 "context_parallel": parallel_config.get("context_parallel", 1), 

578 } 

579 logger.output("Extracted training parameters:") 

580 for key, value in extracted_params.items(): 

581 logger.output("%s: %s", key, value) 

582 

583 return extracted_params 

584 

585 except (yaml.YAMLError, FileNotFoundError, PermissionError) as e: 

586 logger.error("Error parsing Training YAML file: %s", e) 

587 return None