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

428 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-10-25 05:45 +0800

1# Copyright 2025-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"""performance estimation""" 

16import functools 

17import json 

18import math 

19import os 

20from copy import deepcopy 

21from dataclasses import replace 

22from types import MappingProxyType 

23from typing import Any, Callable, Dict, NamedTuple, Optional, Tuple 

24import numpy as np 

25 

26from hyper_parallel.auto_parallel.sapp_nd.nd.logger import logger as nd_logger 

27from hyper_parallel.auto_parallel.sapp_nd.nd.logger import perf_logger as logger 

28from hyper_parallel.auto_parallel.sapp_nd.nd.common.config import Config 

29from hyper_parallel.auto_parallel.sapp_nd.nd.common.layer_type import LayerType 

30from hyper_parallel.auto_parallel.sapp_nd.nd.common.cost_model_config import CostModelConfig 

31from hyper_parallel.auto_parallel._layer_stack import LayerStack, StackGroup 

32from hyper_parallel.auto_parallel._op_profiles import LayerKind 

33from hyper_parallel.auto_parallel.sapp_nd.nd.common.arch_hooks import CWrap, check_and_apply_custom_hook 

34import hyper_parallel.auto_parallel.sapp_nd.nd.common.hardware as Hard 

35from hyper_parallel.auto_parallel.sapp_nd.nd.debug import Debug, PerfParts, RealParts, estimation_in_real_parts 

36 

37from hyper_parallel.auto_parallel.sapp_nd.perf_estimation.utils_classes import ( 

38 RatioType, 

39 PerformanceType, 

40 P2PCommType, 

41 RecType, 

42 CustomConfig, 

43) 

44from hyper_parallel.auto_parallel.sapp_nd.perf_estimation.comm_time import estimate_comm 

45from hyper_parallel.auto_parallel.sapp_nd.perf_estimation.getters import ( 

46 get_layer_configs_by_position, 

47 get_layer_switches_by_position, 

48 get_table_quantity, 

49 mla_weights, 

50 selective_shares, 

51) 

52 

53 

54GENERALIZE_PIPELINE_CALCULATION = False 

55MANUAL_P2P_RATIO = 0.002 

56BACKWARD_RATIO = 2 

57# The chunk the gated delta rule's kernel and Transformers' own 

58# implementation run a sequence in. 

59GDN_CHUNK = 64 

60# What a MoE layer's dispatch costs a rank, per token, per hidden unit, per 

61# expert a token routes to and per rank holding experts. The work is the 

62# sorting, permuting, zeroing and accumulating around the all-to-all, which 

63# no matmul count sees, and it is the one cost that measurably grows with the 

64# expert-parallel degree. Over 15 profiled points of Qwen3.5-35B-A3B at 8192 

65# tokens on 64 dies, device compute averages 5.85, 6.19, 6.59, 7.58 and 9.43 s 

66# at EP 1, 2, 4, 8 and 16, a span of 61%, where this model without the term 

67# spans 8%. The increments over EP 1 are 222, 217, 230 and 230 ms a unit of 

68# EP, so they follow EP itself rather than EP - 1: a layer sorts and permutes 

69# its tokens by expert even where it holds every one of them, and the work 

70# grows with the ranks it spreads them over. With the term the estimate runs 

71# 3.8 to 9.5% above the measured compute at every one of those degrees, where 

72# it was 31% below at EP 16, and the rank correlation over the 15 points rises 

73# from +0.51 to +0.64. It does not change which strategy the search picks: 

74# what holds that at EP 1 is the gather priced at 370 ms where 204 was 

75# measured, an overlap threshold rather than a compute term. 

76# 

77# EMPIRICAL, and fitted at one shape. It follows the tokens a rank routes, so 

78# on the 128-token sweeps it adds 3.2 ms a unit of EP, where the measured 

79# compute is flat from EP 4 to EP 64, 196 to 210 ms, and this would add 205 ms 

80# by EP 64: at a shape whose step is launch bound, 53 to 67% of it idle, 

81# neither this term nor the compute model under it holds. The kernel tables 

82# per EP at the training shape name what grows: the routing's sort and index 

83# kernels, launched as often at every degree above 1 and each running longer, 

84# a cost linear in the tokens the all-to-all hands a rank. This coefficient 

85# stands in for that until it is priced from the tokens, so read it as a 

86# calibration carrying the measured shape, not as a model of the dispatch. A 

87# cluster that has measured its own says so with ``context.moe_dispatch``, as 

88# it states the ratios of the parts in a file, and a model whose compute does 

89# not grow with EP states 0, which prices no dispatch: DeepSeek V4.1's crop 

90# measures 3.3% less compute at EP 16 than at EP 1, where ND with this term 

91# prices 60% more. 

92MOE_DISPATCH = 1040 

93# How far a stage's recorded parts may stray from its time: they are its time 

94# taken apart, so only by rounding. 

95PARTS_REL_TOL = 1e-9 

96# The ratio of a ratios file that scales the compute parts and turns ND's 

97# units into milliseconds. 

98COMPUTE_RATIO = "COMPUTE" 

99# Where a ratio a file leaves out is taken from, as ``nd.ratios.fit_ratios`` 

100# fills a part it cannot fit: the all-reduce from the DP traffic it was split 

101# out of, the bubble from P2P time, which measured data cannot tell it from, 

102# and any other part from compute's, which leaves it as ND weighs it. 

103RATIO_FALLBACKS = { 

104 PerfParts.DP_REDUCE.name: PerfParts.DP_COMM.name, 

105 PerfParts.BUBBLE.name: PerfParts.PP_COMM.name, 

106} 

107# The ratios a file was found without, each named once a process. 

108_MISSING_RATIOS = set() 

109# The parts a communication walk records on the debugger, one value a stage. 

110RECORDED_COMM_PARTS = ( 

111 PerfParts.DP_COMM, 

112 PerfParts.DP_REDUCE, 

113 PerfParts.MP_COMM, 

114 PerfParts.EP_COMM, 

115 PerfParts.CP_COMM, 

116) 

117 

118 

119def op_table(cfg, attn=None): 

120 """op compute load formulas 

121 

122 Args: 

123 cfg: the cost-model config. 

124 attn: a layer group whose attention flavour differs from *cfg*'s. 

125 Only the attention entries read it; everything else stays on 

126 *cfg*, so the expert scaling applied by the caller is unaffected. 

127 """ 

128 table = {} 

129 # cfg.s /= cfg.cp 

130 # Q,O cost h x (a*dh) each, K,V cost h x (n_kv*dh); the h*h form is the 

131 # head_dim = h/a special case. A fused output gate doubles Q. 

132 att = attn if attn is not None else cfg 

133 d_h = att.dh or (cfg.h / att.a if att.a else 0) 

134 d_q = att.a * d_h 

135 table["n_attMM"] = ( 

136 1.5 

137 * cfg.b 

138 * cfg.s 

139 * cfg.h 

140 * ( 

141 ((2 if getattr(att, "attn_output_gate", False) else 1) + 1) * d_q 

142 + 2 * att.n_kv * d_h 

143 ) 

144 ) 

145 # The chunked gated delta rule, per value head: within a chunk of 

146 # GDN_CHUNK tokens, its keys against its keys and its queries, its 

147 # solved weights against its values and its decayed keys, and its 

148 # scores against its new values; the state read twice and written once 

149 # a chunk. Linear in the sequence, where an attention score is 

150 # quadratic. The entry exists only for a group that declares the 

151 # flavour, so no other config needs to carry the count. 

152 n_v, d_k, d_v = (getattr(att, name, 0) for name in ("lin_n_v", "lin_d_k", "lin_d_v")) 

153 if n_v * d_k * d_v: 

154 table["n_linrec"] = (3 * cfg.b * cfg.s * n_v 

155 * (2 * GDN_CHUNK * (3 * d_k + 2 * d_v) + 6 * d_k * d_v)) 

156 table["n_ffMM"] = 6 * cfg.b * cfg.s * cfg.h * cfg.hff 

157 # Every head's queries against every key, as wide as a head's queries 

158 # and keys, an MLA head's with its rotary part; then the weights 

159 # against the values, as wide as a head's values. 

160 d_qk = (getattr(att, "qk_nope_head_dim", None) or d_h) + (getattr(att, "dhr", 0) or 0) 

161 table["n_attBMM"] = 3 * cfg.b * cfg.s * cfg.s * att.a * (d_qk + d_h) 

162 table["n_softmax"] = 13 * cfg.a * cfg.b * cfg.s * cfg.s 

163 table["n_headCast"] = 3 * cfg.a * cfg.b * cfg.s * cfg.s 

164 table["n_gather"] = cfg.b * cfg.s * cfg.h * (cfg.t - 1) 

165 table["n_ffAct"] = 21 * cfg.b * cfg.s * cfg.hff 

166 

167 table["n_normOp"] = 30 * cfg.b * cfg.s * cfg.h * cfg.t / cfg.sp 

168 # A QK-norm runs over every head's queries and keys, which TP splits. 

169 # The entry exists only for a model that has one. 

170 if getattr(cfg, "n_qknorm", 0): 

171 table["n_qknorm"] = 30 * cfg.b * cfg.s * (att.a + att.n_kv) * d_h 

172 table["n_dropout"] = ( 

173 3 * cfg.b * cfg.s * max(cfg.a * cfg.s, 3 * cfg.h * cfg.t / cfg.sp) 

174 ) 

175 if cfg.dc_kv != 0: # MLA 

176 weights, _ = mla_weights(cfg) 

177 table["n_attMM"] = 6 * cfg.b * cfg.s * weights / (getattr(cfg, "n_attMM", 0) or 4) 

178 for op in table: 

179 table[op] *= cfg.bytes_p / cfg.t / cfg.cp 

180 # cfg.s *= cfg.cp 

181 return table 

182 

183 

184def _flavour_tables(cfg, attn=None): 

185 """One (dense, expert) table pair for a given attention flavour. 

186 

187 A MoE layer's feed-forward entry prices the experts each token runs, 

188 the shared experts, as wide as a routed one, and, spread over the 

189 layer's feed-forward matmuls, the router and the shared experts' gate; 

190 its activation function's entry the same experts, which run it at 

191 their width, where a dense layer's runs it at the dense width. A MoE 

192 layer also dispatches its tokens, which the table prices apart 

193 (:data:`MOE_DISPATCH`, or the cost the run states, 0 pricing none); a 

194 dense layer does not, so only the expert table carries that entry. 

195 """ 

196 base = op_table(cfg, attn) 

197 exp = deepcopy(base) # Verify this with MF MoEV2 

198 n_ff = getattr(attn if attn is not None else cfg, "n_ffMM", 0) or 3 

199 gate = 1 if cfg.n_shared_exp and getattr(cfg, "shared_expert_gate", None) else 0 

200 experts = cfg.hff_exp * (max(1, cfg.n_chosen_exp) * cfg.cap_fact + cfg.n_shared_exp) 

201 width = experts + (cfg.n_exp + gate) / n_ff 

202 exp["n_ffMM"] *= width / cfg.hff 

203 exp["n_ffAct"] *= experts / cfg.hff 

204 dispatch = getattr(cfg, "moe_dispatch", None) 

205 if dispatch is None: 

206 dispatch = MOE_DISPATCH 

207 exp["n_dispatch"] = ( 

208 dispatch * cfg.b * cfg.s * cfg.h * max(1, cfg.n_chosen_exp) 

209 * max(1, getattr(cfg, "ep", 1) or 1) * cfg.bytes_p / cfg.t / cfg.cp 

210 ) 

211 return base, exp 

212 

213 

214def _regular_layer_flop(cfg, ccfg, lcfg, tables, layer, with_recomp, switches=None): 

215 """Price one regular layer with its group's config, and its own recompute *switches* where it has them. 

216 

217 *tables* holds one table pair per attention flavour and gains one the 

218 first time a flavour is priced. 

219 """ 

220 kind = getattr(lcfg, "attn_kind", "full") 

221 if kind not in tables: 

222 tables[kind] = _flavour_tables(cfg, lcfg) 

223 flop = get_table_quantity( 

224 lcfg, 

225 tables[kind][1] if (lcfg.n_exp > 1) else tables[kind][0], 

226 layer, 

227 with_recomp, 

228 shares=selective_shares(lcfg, layer, switches), 

229 switches=switches, 

230 ) 

231 if ccfg.ttype == PerformanceType.TIME: 

232 flop = estimate_comp_flop_time(lcfg, flop) 

233 return flop 

234 

235 

236# Evaluation functions 

237def estimate_op_bulk_comp(cfg, ccfg, stages, with_recomp=False, debugger=None, both=False): 

238 """FW + BW; with ``both``, ``(plain, recomputed)`` from one walk over the layers""" 

239 _ = debugger 

240 # Full attention prices from the model's own attention dimensions. 

241 tables = {"full": _flavour_tables(cfg)} 

242 lccfg_at = get_layer_configs_by_position(cfg, stages) 

243 # Each layer's own recompute switches, where the config states several. 

244 switches_at = get_layer_switches_by_position(cfg, stages) 

245 

246 # One sum a mode, each taking its terms in the order a walk of that mode alone did. 

247 modes = (False, True) if both else (with_recomp,) 

248 sums = tuple([] for _ in modes) 

249 for stage_id, stage in enumerate(stages): 

250 for flops in sums: 

251 flops += [0] 

252 for chunk_id, chunk in enumerate(stage): 

253 for lay_id, layer in enumerate(chunk): 

254 if layer == LayerType.EMBEDDING_LAYER: 

255 continue 

256 

257 if layer == LayerType.OUTPUT_LAYER: 

258 output = (1 if cfg.dc_kv == 0 else cfg.n_mtp) * ( 

259 1 

260 / 16 # bias_imbalance 

261 * 6 

262 * cfg.b 

263 * cfg.v 

264 * cfg.h 

265 * cfg.s 

266 * cfg.bytes_p 

267 / cfg.t 

268 ) 

269 for flops in sums: 

270 flops[-1] += output 

271 continue 

272 

273 position = (stage_id, chunk_id, lay_id) 

274 for flops, mode in zip(sums, modes): 

275 flops[-1] += _regular_layer_flop( 

276 cfg, ccfg, lccfg_at[position], tables, layer, mode, switches_at.get(position) 

277 ) 

278 

279 return sums if both else sums[0] 

280 

281 

282def estimate_comp(cfg, ccfg, stages, with_recomp=False, debugger=None, both=False): 

283 """wrapper""" 

284 return estimate_op_bulk_comp( 

285 cfg, ccfg, stages, with_recomp, debugger=debugger, both=both 

286 ) 

287 

288 

289# Experimental : Flop time 

290 

291 

292def efficiency(x): 

293 """obtained via extrapolation""" 

294 eff = min( 

295 1.0, max(0.1, 0.00004694 * x**3 + 0.0014 * x**2 - 0.0336 * x + 0.1) 

296 ) 

297 return eff 

298 

299 

300def throughput(precision_bytes, flop): 

301 """assumes matrix 

302 

303 Only PerformanceType.TIME reaches this, and nothing in production asks 

304 for it: the live estimate is a relative score whose scale the fitted 

305 ratios own. Its precision_bytes**2 * 1e12 is no device's rate, 

306 4 TFLOP/s at bf16, and no other rate prices a candidate's parts: the 

307 one ND states, a host link's sustained_tflops, only turns an offload 

308 copy's seconds into the score's units where no fitted ratio does. 

309 """ 

310 eff = efficiency(flop / (10.0**12)) 

311 return precision_bytes**2 * (10.0**12) * eff 

312 

313 

314def estimate_comp_flop_time(cfg, flop, is_softmax=False): 

315 """flop from throughput""" 

316 th = throughput( 

317 cfg.bytes_softmax if is_softmax else cfg.bytes_compute, flop 

318 ) 

319 return flop / th 

320 

321 

322############################################################## 

323 

324 

325def get_dynamic_ratio(cfg): 

326 """comm/comp""" 

327 if cfg.n_exp == 1: 

328 return 3 / 2 * (cfg.hff + cfg.s) * (8192 / (cfg.h + cfg.s)) 

329 return 3 / 2 * (cfg.hff_exp + cfg.s) * (8192 / (cfg.h + cfg.s)) 

330 

331 

332def _weigh_comm_parts(debugger, comm_w): 

333 """Weigh each stage's recorded communication parts as the stage time weighs them. 

334 

335 DP_REDUCE, the all-reduce's share of DP_COMM, is weighed where the 

336 communication walk recorded it (``estimate_from_mem_comm``). 

337 """ 

338 for p in [PerfParts.DP_COMM, PerfParts.MP_COMM, PerfParts.EP_COMM, PerfParts.CP_COMM]: 

339 debugger.info[p] = [ 

340 comm_w * c for c in debugger.info[p] 

341 ] 

342 if isinstance(debugger.info[PerfParts.DP_REDUCE], list): 

343 debugger.info[PerfParts.DP_REDUCE] = [comm_w * c for c in debugger.info[PerfParts.DP_REDUCE]] 

344 

345 

346def estimate_stage(*args, **kwargs): 

347 """stage level estimation""" 

348 cfg = args[0] 

349 ccfg = args[1] 

350 compute_perfs = args[2] 

351 comm_perfs = args[3] 

352 recompute_perfs = args[4] 

353 recomm_perfs = args[5] 

354 debugger = kwargs.get("debugger", args[6] if len(args) > 6 else None) 

355 comp_w = 1 

356 comm_w = 1 

357 if ccfg.rtype == RatioType.COMM_ONLY: 

358 comp_w = 0 

359 elif ccfg.rtype == RatioType.COMPUTE_ONLY: 

360 comm_w = 0 

361 elif ccfg.rtype == RatioType.STATIC: 

362 comm_w = 10**4 

363 ccfg.static_ratio = comm_w 

364 elif ccfg.rtype == RatioType.DYNAMIC: 

365 comm_w = get_dynamic_ratio(cfg) 

366 ccfg.dynamic_ratio = comm_w 

367 perf = [ 

368 comp_w * compute_perfs[i] + comm_w * comm_perfs[i] 

369 for i in range(len(compute_perfs)) 

370 ] 

371 logger.info("ratio = %s", comm_w) 

372 # ignores comm recomp, to improve 

373 re_perf = [ 

374 ( 

375 max(0, comp_w * (recompute_perfs[i] - compute_perfs[i])) 

376 + max(0, comm_w * (recomm_perfs[i] - comm_perfs[i])) 

377 ) 

378 / (1 + BACKWARD_RATIO) 

379 for i in range(len(compute_perfs)) 

380 ] 

381 

382 if debugger and debugger.is_enabled(): 

383 _weigh_comm_parts(debugger, comm_w) 

384 debugger.info[PerfParts.FW_COMPUTE] = [ 

385 comp_w * comp / (1 + BACKWARD_RATIO) for comp in compute_perfs 

386 ] 

387 debugger.info[PerfParts.BW_COMPUTE] = [ 

388 fw * BACKWARD_RATIO for fw in debugger.info[PerfParts.FW_COMPUTE] 

389 ] 

390 debugger.info[PerfParts.RECOMPUTE] = re_perf 

391 

392 return [perf[i] + re_perf[i] for i in range(len(perf))] 

393 #penalty_fn(stage) 

394 #return stage 

395 

396 

397def _check_stage_parts(time_sum, stage_time, stage): 

398 """Say so when the parts recorded for a stage do not add up to its time. 

399 

400 The parts are the stage's time taken apart, so they agree with it to 

401 rounding. A gap means a walk recorded less, or more, than the time 

402 counts, and the bubble, what is left of the pipeline's time once the 

403 parts are taken out, silently takes the difference. 

404 """ 

405 if math.isclose(time_sum, stage_time, rel_tol=PARTS_REL_TOL, abs_tol=PARTS_REL_TOL): 

406 return 

407 nd_logger.error( 

408 "the parts recorded for stage %d sum to %.6E where its time is %.6E (%+.3f%%), " 

409 "and the bubble takes the difference", 

410 stage, 

411 time_sum, 

412 stage_time, 

413 100 * (time_sum - stage_time) / stage_time if stage_time else 0.0, 

414 ) 

415 

416 

417def estimate_pipeline(cfg, stage_perfs, stage_focused=None, debugger=None): 

418 """pipeline level estimation""" 

419 logger.info("stage_perfs = %s", stage_perfs) 

420 straggler_time = max(stage_perfs) 

421 sum_time = sum(stage_perfs) 

422 last_straggler_idx = cfg.p - 1 - np.argmax(stage_perfs[::-1]) 

423 logger.info( 

424 "straggler estim is %s and its stage is %s", 

425 straggler_time, 

426 last_straggler_idx, 

427 ) 

428 

429 non_steady_perf = 0 

430 steady_perf = 0 

431 if cfg.p == 1: 

432 if len(stage_perfs) != 1: 

433 raise ValueError("Expected exactly one stage performance") 

434 steady_perf = sum_time * cfg.m 

435 elif cfg.vp == 1: 

436 non_steady_perf = sum_time 

437 if GENERALIZE_PIPELINE_CALCULATION: 

438 last_idx = last_straggler_idx + 1 

439 steady_perf = ( 

440 cfg.m - cfg.p + last_straggler_idx 

441 ) * straggler_time + sum(stage_perfs[last_idx:]) 

442 else: 

443 steady_perf = (cfg.m - 1) * straggler_time 

444 else: 

445 less_extra = cfg.p * (cfg.vp - 1) 

446 # big_extra = less_extra + cfg.p 

447 

448 # we assume that times of all micro-blocks in one vp chunk are the same 

449 straggler_time /= cfg.vp 

450 sum_time /= cfg.vp 

451 

452 # more_memory has more micro blocks in warm-up but it does 

453 # not matter since they will overlap with steady phase 

454 non_steady_perf = sum_time + less_extra * straggler_time 

455 

456 # more_memory is a boost to performance and a nerf to memory 

457 # if cfg.vp_less_memory or True: 

458 # steady_perf = (cfg.m * cfg.vp - less_extra - 1) * straggler_time 

459 # else: 

460 # steady_perf = (cfg.m * cfg.vp - big_extra - 1) * straggler_time 

461 steady_perf = (cfg.m * cfg.vp - less_extra - 1) * straggler_time 

462 straggler_time *= cfg.vp 

463 sum_time *= cfg.vp 

464 

465 pipeline_perf = non_steady_perf + steady_perf 

466 logger.info( 

467 "pipeline_perf = non_steady_perf(%.2E) + steady_perf(%.2E)", 

468 non_steady_perf, 

469 steady_perf, 

470 ) 

471 

472 if stage_focused is not None: 

473 last_straggler_idx = stage_focused 

474 if debugger and debugger.is_enabled(): 

475 time_sum = 0 

476 for k in [ 

477 PerfParts.DP_COMM, 

478 PerfParts.MP_COMM, 

479 PerfParts.EP_COMM, 

480 PerfParts.CP_COMM, 

481 PerfParts.FW_COMPUTE, 

482 PerfParts.BW_COMPUTE, 

483 PerfParts.RECOMPUTE, 

484 ]: 

485 debugger.info[k] = ( 

486 debugger.info[k][last_straggler_idx] * cfg.m 

487 ) # / cfg.vp 

488 logger.info( 

489 "time_sum += debugger[%s] = %.2E", 

490 k, 

491 debugger.info[k], 

492 ) 

493 time_sum += debugger.info[k] 

494 

495 # The bubble stays the pipeline's time less the parts, so the parts 

496 # and the bubble still sum to the total the ratios are fitted on. 

497 _check_stage_parts(time_sum, stage_perfs[last_straggler_idx] * cfg.m, last_straggler_idx) 

498 logger.info( 

499 "straggler time = %.2E. %s x stragglers = %.2E", 

500 straggler_time, 

501 cfg.m, 

502 straggler_time * cfg.m, 

503 ) 

504 

505 bubble = pipeline_perf - time_sum 

506 logger.info( 

507 "bubble(%.2E) = pipeline_perf(%.2E) - time_sum(%.2E)", 

508 bubble, 

509 pipeline_perf, 

510 time_sum, 

511 ) 

512 debugger.info[PerfParts.BUBBLE] = bubble 

513 # DP_COMM timed the stage's whole DP traffic, the time and the bubble 

514 # above with it; its all-reduce is DP_REDUCE's, the rest FSDP's. 

515 reduced = debugger.info[PerfParts.DP_REDUCE] 

516 if isinstance(reduced, list): 

517 debugger.info[PerfParts.DP_REDUCE] = reduced[last_straggler_idx] * cfg.m 

518 debugger.info[PerfParts.DP_COMM] -= debugger.info[PerfParts.DP_REDUCE] 

519 return pipeline_perf 

520 

521 

522def estimate_p2p_comm(cfg, straggler, ratio=MANUAL_P2P_RATIO, debugger=None): 

523 """pipeline comm""" 

524 nb_send_recv = 0 

525 if cfg.vp == 1: 

526 nb_send_recv = ( 

527 0 

528 if cfg.p == 1 

529 else ( 

530 4 * cfg.m 

531 if cfg.p == 2 

532 else 4 * cfg.p * cfg.m + 4 * cfg.p * cfg.p - 14 * cfg.p 

533 ) 

534 ) 

535 else: 

536 nb_send_recv = ( 

537 0 

538 if cfg.p == 1 

539 else ( 

540 8 * cfg.m * cfg.vp - 4 * cfg.m 

541 if cfg.p == 2 

542 else ( 

543 16 * cfg.m * cfg.vp + 12 

544 if cfg.p == 4 

545 else 4 * cfg.p * cfg.m * cfg.vp 

546 + 4 * cfg.p * cfg.p 

547 - 13 * cfg.p 

548 ) 

549 ) 

550 ) 

551 pp_comm = ratio * nb_send_recv / cfg.p * straggler / cfg.sp 

552 if debugger and debugger.is_enabled(): 

553 debugger.info[PerfParts.PP_COMM] = pp_comm 

554 

555 return pp_comm 

556 

557 

558def estimate_perf(cfg, _, stage_perfs, stage_focused=None, debugger=None): 

559 """wrapper""" 

560 return estimate_pipeline(cfg, stage_perfs, stage_focused=stage_focused, debugger=debugger) 

561 

562 

563def estimate_p2p(cfg, ccfg, stage_perfs, debugger=None): 

564 """wrapper""" 

565 if ccfg.ptype != P2PCommType.MANUAL: 

566 p2p = 0 

567 else: 

568 p2p = estimate_p2p_comm(cfg, max(stage_perfs), debugger=debugger) 

569 if debugger and debugger.is_enabled(): 

570 debugger.info[PerfParts.PP_COMM] = p2p 

571 return p2p 

572 

573 

574def _one_layer_of(stack: Optional[LayerStack], kind: Optional[LayerKind]) -> Optional[LayerStack]: 

575 """The model's stack, reduced to one layer of *kind*. 

576 

577 Its other groups stay, with no layer, so the estimates apply the layer's 

578 kind exactly when they would for the whole model: to a copy of the config 

579 for the compute, whose op tables come from the model's own config, and in 

580 place for the communication. A layer with no kind has no stack. 

581 """ 

582 if kind is None or stack is None: 

583 return None 

584 return replace(stack, groups=(StackGroup(kind, 1),) + tuple(replace(group, count=0) for group in stack.groups)) 

585 

586 

587class PlainTimes(NamedTuple): 

588 """What a layer costs before any recompute, the same for every option of its kind. 

589 

590 Attributes: 

591 compute: The compute estimate without recompute. 

592 comm: The communication estimate without recompute. 

593 config: The config the communication estimate leaves, which the stage 

594 estimate reads. 

595 """ 

596 

597 compute: float 

598 comm: float 

599 config: CostModelConfig 

600 

601 

602def _layer_alone( 

603 cfg: CostModelConfig, kind: Optional[LayerKind], layer_type: LayerType, switches: Optional[Dict[str, int]] 

604) -> Tuple[CostModelConfig, list]: 

605 """One *layer_type* layer of *kind*, alone in the first stage of a fresh copy of *cfg*. 

606 

607 A fresh copy each time: the estimates leave state on the config they are 

608 given. 

609 """ 

610 single = deepcopy(cfg) 

611 if switches is not None: 

612 single.rec_op = Config(dict(switches)) 

613 # The one layer runs *switches*, else the config's rec_op, whatever 

614 # the layers of the model run. 

615 single.layer_switches = None 

616 single.layer_stack = _one_layer_of(single.layer_stack, kind) 

617 single.n = single.d * single.t * single.p 

618 stages = [[[] for _ in range(single.vp)] for _ in range(single.p)] 

619 stages[0][0] = [layer_type] 

620 return single, stages 

621 

622 

623def plain_layer_times( 

624 cfg: CostModelConfig, 

625 kind: Optional[LayerKind], 

626 layer_type: LayerType, 

627 device_type: Any, 

628 ccfg: Optional[CustomConfig] = None, 

629) -> PlainTimes: 

630 """The compute and communication of one layer of *kind* alone, without recompute. 

631 

632 They do not depend on which recompute option the layer runs, so every 

633 option of a kind can share them; the embedding and the output layer have 

634 their own. 

635 """ 

636 ccfg = ccfg if ccfg is not None else CustomConfig() 

637 single, stages = _layer_alone(cfg, kind, layer_type, None) 

638 compute = estimate_comp(single, ccfg, stages)[0] 

639 grouped, stages = _layer_alone(cfg, kind, layer_type, None) 

640 comm = estimate_comm(grouped, ccfg, stages, device_type)[0] 

641 return PlainTimes(compute, comm, grouped) 

642 

643 

644def estimate_layer_times( 

645 cfg: CostModelConfig, 

646 kind: Optional[LayerKind], 

647 layer_type: LayerType, 

648 device_type: Any, 

649 ccfg: Optional[CustomConfig] = None, 

650 switches: Optional[Dict[str, int]] = None, 

651 plain: Optional[PlainTimes] = None, 

652) -> Tuple[float, float]: 

653 """Forward and backward time of one layer, priced as a stage prices it. 

654 

655 Runs the compute, communication and stage estimates of 

656 :func:`estimate_performance` on one layer of *kind*, alone in the first 

657 stage of a copy of *cfg*. For a stage whose layers are all of one kind, 

658 the sum of their times is what the search charges the stage before 

659 pipeline bubbles and point-to-point communication. The search prices each 

660 layer on a config the previous layers' kinds have been applied to, so a 

661 stage that mixes kinds, or holds the embedding or output layer, can 

662 differ by a few percent; here every layer is priced on its own kind, 

663 wherever it sits. 

664 

665 Args: 

666 cfg: The model's cost-model config, before any layer kind is applied. 

667 kind: The layer's kind; ``None`` for the embedding and output, and 

668 for a layer priced on the config as it stands. 

669 layer_type: How the layer runs: plain, selective or full recompute, 

670 embedding or output. 

671 device_type: The device the communication is priced on. 

672 ccfg: Estimator options; the search's defaults when omitted. 

673 switches: The recompute switches a selective layer runs with, 1 to 

674 keep an op and 0 to recompute it; the config's own when omitted. 

675 plain: The layer's :func:`plain_layer_times`, when already priced. 

676 

677 Returns: 

678 ``(forward, backward)``, *backward* including the layer's recompute. 

679 """ 

680 ccfg = ccfg if ccfg is not None else CustomConfig() 

681 if plain is None: 

682 plain = plain_layer_times(cfg, kind, layer_type, device_type, ccfg) 

683 comp, comm, grouped = plain 

684 # A layer that recomputes nothing runs nothing again. 

685 recomputes = layer_type in (LayerType.FULL_REC_LAYER, LayerType.SEL_REC_LAYER) 

686 recomp = comp 

687 if recomputes and ccfg.retype in {RecType.COMPUTE_ONLY, RecType.WITH}: 

688 single, stages = _layer_alone(cfg, kind, layer_type, switches) 

689 recomp = estimate_comp(single, ccfg, stages, with_recomp=True)[0] 

690 recomm = comm 

691 if recomputes and ccfg.retype in {RecType.COMM_ONLY, RecType.WITH}: 

692 fresh, fresh_stages = _layer_alone(cfg, kind, layer_type, switches) 

693 recomm = estimate_comm(fresh, ccfg, fresh_stages, device_type, with_recomp=True)[0] 

694 

695 # The stage estimate weighs the config the communication estimate leaves. 

696 once = estimate_stage(grouped, ccfg, [comp], [comm], [comp], [comm])[0] 

697 total = estimate_stage(grouped, ccfg, [comp], [comm], [recomp], [recomm])[0] 

698 forward = once / (1 + BACKWARD_RATIO) 

699 return forward, total - forward 

700 

701 

702class LayerTimes: 

703 """Prices the layers of a pipeline balancer's layer description. 

704 

705 The memory backbone calls it for every layer it describes, with the config 

706 it walks and the layer's kind. A config is copied the first time it is 

707 seen, at its embedding layer, before the walk applies any layer kind to 

708 it, and each kind, layer type and set of switches is priced once. What a 

709 kind costs without recompute is priced once for all its options. 

710 """ 

711 

712 def __init__(self, device_type: Any, ccfg: Optional[CustomConfig] = None) -> None: 

713 """Price on *device_type*, with the estimator options *ccfg*.""" 

714 self._device_type = device_type 

715 self._ccfg = ccfg 

716 self._base = {} 

717 self._times = {} 

718 self._plain = {} 

719 

720 def __call__( 

721 self, 

722 cfg: CostModelConfig, 

723 kind: Optional[LayerKind], 

724 layer_type: LayerType, 

725 switches: Optional[Dict[str, int]] = None, 

726 ) -> Tuple[float, float]: 

727 """``(forward, backward)`` of a *layer_type* layer of *kind*, run with *switches*.""" 

728 if id(cfg) not in self._base: 

729 self._base[id(cfg)] = deepcopy(cfg) 

730 key = (id(cfg), kind, layer_type, None if switches is None else tuple(sorted(switches.items()))) 

731 if key not in self._times: 

732 # Every body option of a kind shares its plain times; the 

733 # embedding and the output layer have their own. 

734 role = layer_type if layer_type in (LayerType.EMBEDDING_LAYER, LayerType.OUTPUT_LAYER) else None 

735 plain_key = (id(cfg), kind, role) 

736 if plain_key not in self._plain: 

737 self._plain[plain_key] = plain_layer_times( 

738 self._base[id(cfg)], kind, layer_type, self._device_type, self._ccfg 

739 ) 

740 self._times[key] = estimate_layer_times( 

741 self._base[id(cfg)], kind, layer_type, self._device_type, self._ccfg, switches, 

742 plain=self._plain[plain_key], 

743 ) 

744 return self._times[key] 

745 

746 

747def _part_ratio(coeffs, key): 

748 """The ratio a ratios file states for *key*, or the one fit_ratios would give it. 

749 

750 A file ``fit_ratios`` writes states every key, but one edited by hand, 

751 or fitted by a tree older than a part, may lack some. A missing ratio 

752 falls back as ``fit_ratios`` fills a part it cannot fit (``RATIO_FALLBACKS``), 

753 and each missing key is named once. Compute's ratio is the unit every 

754 other one falls back on, so a file without it is refused. 

755 

756 Raises: 

757 ValueError: When the file states no COMPUTE ratio. 

758 """ 

759 ratio = coeffs.get(key) 

760 if ratio is not None: 

761 return ratio 

762 if key == COMPUTE_RATIO: 

763 raise ValueError( 

764 "the ratios file states no COMPUTE ratio, the one that turns ND's " 

765 "units into milliseconds and that every missing ratio falls back on" 

766 ) 

767 fallback = RATIO_FALLBACKS.get(key, COMPUTE_RATIO) 

768 if key not in _MISSING_RATIOS: 

769 _MISSING_RATIOS.add(key) 

770 nd_logger.error("the ratios file states no %s ratio: that part takes %s's", key, fallback) 

771 return _part_ratio(coeffs, fallback) 

772 

773 

774def apply_regression_coefficients(coeffs, debugger, old_perf): 

775 """ 

776 applies the coefficients present in regression's cache_file 

777 """ 

778 compute_ratio = _part_ratio(coeffs, COMPUTE_RATIO) 

779 for part, raw in list(debugger.info.items()): 

780 if part in (PerfParts.TOTAL, PerfParts.MEMORY): 

781 continue 

782 if part in (PerfParts.FW_COMPUTE, 

783 PerfParts.BW_COMPUTE, 

784 PerfParts.RECOMPUTE): 

785 ratio = compute_ratio 

786 else: 

787 ratio = _part_ratio(coeffs, part.name) 

788 new_val = 0.0 if raw == 0.0 else raw * ratio 

789 debugger.info[part] = new_val 

790 

791 max_idx = max(p.value for p in PerfParts) -1 

792 estimations = [0.0] * max_idx 

793 for part in PerfParts: 

794 if part in (PerfParts.TOTAL, PerfParts.MEMORY): 

795 continue 

796 estimations[part.value - 1] = debugger.info.get(part) or 0.0 

797 

798 real_buckets = {rp: [] for rp in RealParts} 

799 real_buckets = estimation_in_real_parts(real_buckets, estimations, old_perf) 

800 

801 perf = ( 

802 real_buckets[RealParts.COMP][-1] 

803 + real_buckets[RealParts.DP_WAIT][-1] 

804 + real_buckets[RealParts.MP_WAIT][-1] 

805 + real_buckets[RealParts.EP_WAIT][-1] 

806 + real_buckets[RealParts.CP_WAIT][-1] 

807 + real_buckets[RealParts.PP_WAIT][-1] 

808 ) 

809 debugger.info[PerfParts.TOTAL] = perf 

810 return perf 

811 

812 

813class _EstimateArgs(NamedTuple): 

814 """Resolved inputs of estimate_performance, unpackable as a tuple.""" 

815 

816 cfg: CostModelConfig 

817 stages: Optional[list] 

818 extra_custom_func: Optional[Callable] 

819 ccfg: Any 

820 debugger: Any 

821 device_type: Any 

822 memory: Any 

823 

824 

825def _resolve_estimate_args(args, kwargs): 

826 """Resolve positional/keyword inputs for estimate_performance. 

827 

828 Returns: 

829 _EstimateArgs of (cfg, stages, extra_custom_func, ccfg, debugger, 

830 device_type, memory). 

831 """ 

832 cfg_input = args[0] 

833 # A copy: the estimate applies layer kinds to its config in place, and the 

834 # caller's config is the one the next estimate starts from. 

835 cfg = ( 

836 deepcopy(cfg_input) 

837 if isinstance(cfg_input, CostModelConfig) 

838 else CostModelConfig(cfg_input) 

839 ) 

840 stages = kwargs.get("stages", args[1] if len(args) > 1 else None) 

841 extra_custom_func = kwargs.get( 

842 "extra_custom_func", args[2] if len(args) > 2 else None 

843 ) 

844 ccfg = kwargs.get("ccfg", args[3] if len(args) > 3 else CustomConfig()) 

845 debugger = kwargs.get("debugger", args[4] if len(args) > 4 else None) 

846 device_type = kwargs.get( 

847 "device_type", args[5] if len(args) > 5 else Hard.device_map["A2"] 

848 ) 

849 memory = kwargs.get("memory", args[6] if len(args) > 6 else None) 

850 return _EstimateArgs(cfg, stages, extra_custom_func, ccfg, debugger, device_type, memory) 

851 

852 

853def _save_recompute(stage_perfs, savings, debugger): 

854 """Stage times less the recompute time each stage saves. 

855 

856 Args: 

857 stage_perfs: Each stage's time, its layers recomputed as the config 

858 says. 

859 savings: The time each stage's layers save with the recompute 

860 options chosen for them instead; ``None`` when none are. 

861 debugger: Records the recompute part of each stage, which is where 

862 the time is saved. 

863 

864 Returns: 

865 Each stage's time. 

866 """ 

867 if savings is None: 

868 return stage_perfs 

869 if len(savings) != len(stage_perfs): 

870 raise ValueError(f"{len(savings)} stage savings for {len(stage_perfs)} stages") 

871 if debugger and debugger.is_enabled(): 

872 debugger.info[PerfParts.RECOMPUTE] = [ 

873 recompute - saved for recompute, saved in zip(debugger.info[PerfParts.RECOMPUTE], savings) 

874 ] 

875 return [perf - saved for perf, saved in zip(stage_perfs, savings)] 

876 

877 

878def _user_hook(cfg: Any) -> Optional[Callable]: 

879 """The hook a unimodal config's hook class gives it, as the memory estimate applies it.""" 

880 hooks = getattr(cfg, "hooks_dict", None) 

881 if not hooks or getattr(cfg, "multimodal", False): 

882 return None 

883 return next(iter(hooks.values())) 

884 

885 

886def _ratios(cache_file): 

887 """The ratios *cache_file* states, parsed again only when the file changes. 

888 

889 A search applies them to every candidate it orders: read and parsed for 

890 each one, they cost 0.7 s on the Qwen3.5 example's 7216 candidates. 

891 """ 

892 stat = os.stat(cache_file) 

893 return _parsed_ratios(cache_file, stat.st_mtime_ns, stat.st_size) 

894 

895 

896@functools.lru_cache(maxsize=8) 

897def _parsed_ratios(cache_file, mtime_ns, size): # pylint: disable=unused-argument 

898 """The ratios file as one read-only mapping; *mtime_ns* and *size* only key the cache.""" 

899 with open(cache_file, 'r', encoding='utf-8') as f: 

900 return MappingProxyType(json.load(f)) 

901 

902 

903def _finalize_perf(perf, cache_file, debugger, memory): 

904 """Apply cached regression coefficients and record debug info. 

905 

906 Returns: 

907 The final performance value. 

908 """ 

909 coeffs = None 

910 cache = False 

911 if cache_file is not None: 

912 coeffs = _ratios(cache_file) 

913 cache = True 

914 

915 if debugger and debugger.is_enabled(): 

916 if cache: 

917 perf = apply_regression_coefficients(coeffs, debugger, perf) 

918 debugger.info[PerfParts.TOTAL] = perf 

919 if memory is not None: 

920 debugger.info[PerfParts.MEMORY] = memory 

921 return perf 

922 

923 

924def _stage_parts(cfg, ccfg, stages, device_type, debugger): 

925 """One config's per-stage compute, recompute, communication and recomputed communication.""" 

926 # Only the plain walks record parts. A stage's time takes what the 

927 # recompute walks add as RECOMPUTE alone; recorded as communication too, 

928 # it made the parts outgrow the time, and the bubble, their difference, 

929 # went negative. 

930 # Where the recompute is priced, one walk prices both modes, each sum 

931 # taking its terms in the order a walk of that mode alone did. 

932 if ccfg.retype in {RecType.COMPUTE_ONLY, RecType.WITH}: 

933 compute_perfs, recompute_perfs = estimate_comp(cfg, ccfg, stages, debugger=debugger, both=True) 

934 else: 

935 compute_perfs = estimate_comp(cfg, ccfg, stages, with_recomp=False, debugger=debugger) 

936 recompute_perfs = [0] * cfg.p 

937 if ccfg.retype in {RecType.COMM_ONLY, RecType.WITH}: 

938 comm_perfs, recomm_perfs = estimate_comm(cfg, ccfg, stages, device_type, debugger=debugger, both=True) 

939 else: 

940 comm_perfs = estimate_comm(cfg, ccfg, stages, device_type, with_recomp=False, debugger=debugger) 

941 recomm_perfs = [0] * cfg.p 

942 return compute_perfs, recompute_perfs, comm_perfs, recomm_perfs 

943 

944 

945def _submodule_parts(cfg, ccfg, device_type, debugger): 

946 """A multimodal model's per-stage parts, every submodule's summed. 

947 

948 The submodules of a vision-language model run on one pipeline, so a 

949 stage's time is the work of the layers on it, whichever submodule they 

950 belong to, as the memory model sums what they keep there 

951 (``combine_partition_multimodal``). Each is priced on its own 

952 partitions and under its own family, on a copy, so no submodule leaves 

953 state for the next. 

954 

955 Each submodule's walk also records its communication on the debugger, 

956 stage by stage, and the record is summed the same way. Kept as the 

957 last walk left it, it held the language model's share alone: a vision 

958 tower's collectives went into the bubble, the time less the parts, and 

959 the parts a ratio is fitted on missed them. 

960 """ 

961 partitions = cfg.generate_partitions_vpp() 

962 totals = None 

963 recorded = {} 

964 for name in cfg.mm_order: 

965 sub = deepcopy(cfg.mm_ccfgs[name]) 

966 check_and_apply_custom_hook(sub) 

967 parts = _stage_parts(sub, ccfg, partitions[name], device_type, debugger) 

968 totals = parts if totals is None else tuple( 

969 [left + right for left, right in zip(*pair)] for pair in zip(totals, parts) 

970 ) 

971 if debugger and debugger.is_enabled(): 

972 _add_recorded_comm(recorded, debugger.info) 

973 if recorded: 

974 debugger.info.update(recorded) 

975 return totals 

976 

977 

978def _add_recorded_comm(recorded, info): 

979 """Add the communication a walk recorded, one value a stage, to *recorded*.""" 

980 for part in RECORDED_COMM_PARTS: 

981 stages = info.get(part) 

982 if not isinstance(stages, list): 

983 continue 

984 recorded[part] = ( 

985 [left + right for left, right in zip(recorded[part], stages)] 

986 if part in recorded 

987 else list(stages) 

988 ) 

989 

990 

991# performance estimation 

992def estimate_performance(*args, **kwargs): 

993 """main estimation 

994 

995 With ``stage_savings``, each stage's time is lowered by the recompute 

996 time its layers save under the options chosen for them, before the 

997 pipeline is timed. 

998 """ 

999 ( 

1000 cfg, 

1001 stages, 

1002 extra_custom_func, 

1003 ccfg, 

1004 debugger, 

1005 device_type, 

1006 memory, 

1007 ) = _resolve_estimate_args(args, kwargs) 

1008 cache_file = kwargs.get("cache_file") 

1009 if cache_file is not None and not (debugger and debugger.is_enabled()): 

1010 # The ratios rescale the parts a debugger records, so the parts are 

1011 # recorded whether or not anyone asked to see them: a search that 

1012 # writes no debug output must not rank on the uncalibrated score. 

1013 debugger = Debug(None, info_type=PerfParts) 

1014 

1015 # Process custom model config: the config's own hook when its hook class 

1016 # gives it one, as the memory estimate applies it, else its family. 

1017 user_hook = _user_hook(cfg) 

1018 if extra_custom_func: 

1019 extra_custom_func(cfg) 

1020 elif user_hook is not None: 

1021 user_hook(CWrap(cfg)) 

1022 else: 

1023 logger.info("auto applying custom model config") 

1024 check_and_apply_custom_hook(cfg) 

1025 

1026 # Process partition generation 

1027 if not stages: 

1028 logger.info("stage partitions are generated") 

1029 stages = cfg.generate_partitions_vpp() 

1030 

1031 cfg.n = cfg.d * cfg.t * cfg.p 

1032 

1033 logger.debug( 

1034 "perf_model: DP = %d, TP = %d, EP = %d, PP = %d, MB = %d", 

1035 cfg.d, 

1036 cfg.t, 

1037 cfg.ep, 

1038 cfg.p, 

1039 cfg.m, 

1040 ) 

1041 

1042 logger.info("%s", cfg) 

1043 logger.info(stages) 

1044 logger.info(ccfg) 

1045 

1046 if getattr(cfg, "multimodal", False): 

1047 compute_perfs, recompute_perfs, comm_perfs, recomm_perfs = _submodule_parts( 

1048 cfg, ccfg, device_type, debugger 

1049 ) 

1050 timed = cfg.mm_ccfgs[getattr(cfg, "mm_main", None) or cfg.mm_order[-1]] 

1051 timed.n = cfg.n 

1052 else: 

1053 compute_perfs, recompute_perfs, comm_perfs, recomm_perfs = _stage_parts( 

1054 cfg, ccfg, stages, device_type, debugger 

1055 ) 

1056 timed = cfg 

1057 logger.info("PerfEst: comm_perfs %s", comm_perfs) 

1058 

1059 stage_perfs = estimate_stage( 

1060 timed, 

1061 ccfg, 

1062 compute_perfs, 

1063 comm_perfs, 

1064 recompute_perfs, 

1065 recomm_perfs, 

1066 debugger=debugger, 

1067 ) 

1068 stage_perfs = _save_recompute(stage_perfs, kwargs.get("stage_savings"), debugger) 

1069 logger.info("PerfEst: stage_perfs %s", stage_perfs) 

1070 

1071 stage_focused = kwargs.get("stage_focused", None) 

1072 perf = estimate_perf( 

1073 timed, ccfg, stage_perfs, stage_focused=stage_focused, debugger=debugger 

1074 ) 

1075 perf += estimate_p2p(timed, ccfg, stage_perfs, debugger=debugger) 

1076 logger.info("PerfEst: perf %s", perf) 

1077 

1078 return _finalize_perf(perf, cache_file, debugger, memory) # / cfg.gbs 

1079 

1080# TO-DO 

1081# Fix More Memory 

1082# Add Context Parallelism