Coverage for  / home / jenkins / .local / lib / python3.10 / site-packages / hyper_parallel / auto_parallel / sapp_nd / nd / parallelize.py: 77%

325 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-07-06 05:41 +0800

1# Copyright 2024 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"""find parallelization""" 

16 

17import time 

18import copy 

19import multiprocessing as proc 

20import json 

21import os 

22import logging 

23 

24from hyper_parallel.auto_parallel.sapp_nd.memory_estimation.estimate_v2 import EvaluatorV2 

25from hyper_parallel.auto_parallel.sapp_nd.perf_estimation.estimate import estimate_performance 

26 

27from hyper_parallel.auto_parallel.sapp_nd.nd.global_config import GlobalConfig 

28from hyper_parallel.auto_parallel.sapp_nd.nd.logger import logger 

29import hyper_parallel.auto_parallel.sapp_nd.nd.dimensions as Dim 

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

31import hyper_parallel.auto_parallel.sapp_nd.nd.debug as Debug 

32 

33# logger = proc.log_to_stderr() 

34# logger.setLevel(proc.SUBDEBUG) 

35 

36 

37class ParallelizeLayer: 

38 """Parallelize one layer type""" 

39 

40 def __init__( 

41 self, 

42 evaluator, 

43 machine, 

44 global_batch_size=None, 

45 dimensions=None, 

46 **extra_config, 

47 ): 

48 

49 self.enable_debug = logger.level < logging.CRITICAL 

50 self.machine = machine 

51 if "mppb" in extra_config: 

52 manual_ppb = extra_config.pop("mppb") 

53 else: 

54 manual_ppb = False 

55 

56 self.mem_eval = evaluator 

57 

58 self.model_name = self.mem_eval._ccfg.model_name 

59 logger.debug("model is %s", self.model_name) 

60 

61 if "mem_for_ppb" in extra_config: 

62 reserve_mem = extra_config.pop("mem_for_ppb") 

63 self.mem_eval._ccfg.device_capacity.decrease(reserve_mem) 

64 

65 if "max_mem" in extra_config: 

66 max_mem = extra_config.pop("max_mem") 

67 if max_mem is not None: 

68 self.mem_eval._ccfg.device_capacity.set(max_mem) 

69 

70 logger.debug("before global config init") 

71 

72 if "sub_model" in extra_config: 

73 sub_model = extra_config.pop("sub_model") 

74 if sub_model is not None: 

75 self.config = GlobalConfig( 

76 self.mem_eval._ccfg.mm_ccfgs[sub_model], 

77 dimensions, 

78 mppb=manual_ppb, 

79 ) 

80 else: 

81 self.config = GlobalConfig( 

82 self.mem_eval._ccfg, dimensions, mppb=manual_ppb 

83 ) 

84 else: 

85 self.config = GlobalConfig( 

86 self.mem_eval._ccfg, dimensions, mppb=manual_ppb 

87 ) 

88 

89 self.mem_eval.set_passes(**extra_config) 

90 

91 self.machine.update_num_if_none( 

92 self.config.ccfg.strategy_num_devices() 

93 ) 

94 

95 if global_batch_size: 

96 self.global_batch_size = global_batch_size 

97 else: 

98 self.global_batch_size = self.config.ccfg.gbs 

99 

100 self.bound_space() 

101 

102 def bound_space(self): 

103 """Set bounds for parallel dimensions""" 

104 vpp = ( 

105 1 

106 if Dim.VPP in self.config.dimensions 

107 else Dim.VPP.from_config(self.config.ccfg) 

108 ) 

109 pp_bound = min( 

110 self.machine.pipeline_bound(), 

111 self.config.total_layer_num() // vpp, 

112 self.global_batch_size, 

113 ) 

114 Dim.PP.set_bound(pp_bound) 

115 logger.info( 

116 "PP bound is %d, machine bound = %d, L = %d, VPP = %d, B = %d", 

117 pp_bound, 

118 self.machine.pipeline_bound(), 

119 self.config.total_layer_num(), 

120 vpp, 

121 self.global_batch_size, 

122 ) 

123 Dim.EP.set_bound(self.config.ccfg.n_exp) 

124 # if ( 

125 # self.config.dimensions.count(Dim.EP) > 0 

126 # and Dim.EP.from_config(self.config.ccfg) <= 1 

127 # ): 

128 # Dim.EP.set_bound(1) 

129 # self.config.dimensions.remove(Dim.EP) 

130 kv_heads = self.config.ccfg.n_kv 

131 if kv_heads: 

132 Dim.TP.set_bound(kv_heads) 

133 logger.warning( 

134 "Because of n_kv_heads, MP will be limited to %s", 

135 str(kv_heads), 

136 ) 

137 else: 

138 # num_head % (TP * UP) == 0. Add UP later 

139 Dim.TP.set_bound( 

140 Hard.highest_power_of_2_divisor(self.config.ccfg.a) 

141 ) 

142 

143 def filtered_out(self, _): 

144 """Manual conditions to remove config patterns""" 

145 # if parallel_config.has_dim(Dim.EP): 

146 # if self.config.dim_val(Dim.EP, parallel_config) < 8: 

147 # return True 

148 return False 

149 

150 def is_valid(self, parallel_config): 

151 """Check configuration validity""" 

152 if not parallel_config.is_valid(): 

153 logger.warning("configuration %s not valid", str(parallel_config)) 

154 return False 

155 if not self.config.moe_valid(parallel_config): 

156 logger.warning("expert parallel is higher than expert number") 

157 return False 

158 if self.filtered_out(parallel_config): 

159 logger.warning("Config manually filtered out") 

160 return False 

161 gbs = self.config.global_batch_size(parallel_config) 

162 if not gbs == self.global_batch_size: 

163 logger.error( 

164 "wrong global batch size: ccfg is %d, instead of %d", 

165 gbs, 

166 self.global_batch_size, 

167 ) 

168 return False 

169 return True 

170 

171 def memory_estim(self, debugger=None): 

172 """Whether the config fits memory""" 

173 logger.debug("estimate_peak") 

174 verbose = logger.level < logging.INFO 

175 self.mem_eval.set_config(self.config.ccfg) # = self.config.ccfg 

176 # self.mem_eval = EvaluatorV2(self.config) 

177 logger.debug("ccfg = %s", str(self.config.ccfg)) 

178 peak = self.mem_eval.estimate_peak( 

179 verbose=verbose 

180 ) # (logger.level>2)) 

181 logger.debug("peak memory = %d", peak) 

182 if debugger and debugger.is_enabled(): 

183 debugger.info[Debug.MemParts.TOTAL] = peak 

184 return peak 

185 

186 def generate_search_space(self, folder, threads_num): 

187 """Return a search space computed with memory estimation""" 

188 space = ({}, 0) 

189 configs = [] 

190 results = {} 

191 if threads_num: 

192 with proc.Pool(processes=threads_num) as pool: 

193 logger.debug("before loops") 

194 results, size = self.device_loops(space, pool) 

195 logger.debug("%d results", len(results)) 

196 for config, result in results.items(): 

197 logger.debug("result = %s", str(result)) 

198 logger.debug( 

199 "before get: is ready ? %s", str(result.ready()) 

200 ) 

201 logger.debug( 

202 "before get: is successful ? %s", 

203 str(result.successful()), 

204 ) 

205 # if result.successful(): 

206 peak_mem = result.get(1) 

207 logger.debug( 

208 "after get: is ready ? %s", str(result.ready()) 

209 ) 

210 logger.debug( 

211 "after get: is successful ? %s", 

212 str(result.successful()), 

213 ) 

214 logger.debug("peak_mem = %s", str(peak_mem)) 

215 if self.mem_eval.mem_fit(peak_mem): 

216 configs.append((config, peak_mem)) 

217 pool.close() 

218 pool.join() 

219 else: 

220 results, size = self.device_loops(space, None) 

221 for config, peak_mem in results.items(): 

222 if self.mem_eval.mem_fit(peak_mem): 

223 configs.append((config, peak_mem)) 

224 if folder: 

225 self.config.write(folder, config) 

226 logger.output("%d valid configurations generated", size) 

227 logger.output("%d configuration fitting memory to order", len(configs)) 

228 

229 return configs 

230 

231 def device_loops(self, space, pool): 

232 """Exploration loop nest level 0: parallel dimensions dividing devices""" 

233 for tp in self.config.space(Dim.TP, self.machine.number): 

234 for pp in self.config.space(Dim.PP, self.machine.number // tp): 

235 for cp in self.config.space( 

236 Dim.CP, self.machine.number // tp // pp 

237 ): 

238 logger.debug( 

239 "dp = %d / %d / %d / %d", 

240 self.machine.number, 

241 tp, 

242 cp, 

243 pp, 

244 ) 

245 dp = self.machine.number // tp // cp // pp 

246 if dp < 1: 

247 break 

248 space = self.batch_loops(space, pool, (dp, tp, pp, cp)) 

249 return space 

250 

251 def batch_loops(self, space, pool, dtpc_p): 

252 """Exploration loop nest level 1: dimensions dividing batch (except already processed DP)""" 

253 dp, _, pp, _ = dtpc_p 

254 # if pp > 1: 

255 for mbs in self.config.space( 

256 Dim.MBS, self.global_batch_size // pp // dp 

257 ): 

258 logger.debug("mbn= %d / %d / %d", self.global_batch_size, dp, mbs) 

259 mbn = self.global_batch_size // dp // mbs 

260 space = self.parallel_loops(space, pool, (dtpc_p, (mbs, mbn))) 

261 # else: 

262 # logger.debug("no pipeline so mbn = 1") 

263 # mbs = self.global_batch_size // dp 

264 # space = self.parallel_loops(space, pool, (dtpc_p, (mbs, 1))) 

265 return space 

266 

267 def parallel_loops(self, space, pool, dims): 

268 """Exploration loop nest level 2: dimensions dependent on others""" 

269 dtpc_p, mbsn = dims 

270 dp, tp, pp, _ = dtpc_p 

271 for ep in self.config.space(Dim.EP, dp * tp): 

272 for vpp in self.config.range_space( 

273 Dim.VPP, min(4, pp, self.config.total_layer_num() // pp) 

274 ): 

275 for op in self.config.space( 

276 Dim.OP, self.config.max_op(dp, tp, ep) 

277 ): 

278 for sp in self.config.bool_space(Dim.SP): 

279 space = self.inside_loop_nest( 

280 space, 

281 pool, 

282 (dtpc_p, mbsn, (ep, vpp, op, sp)), 

283 ) 

284 return space 

285 

286 def inside_loop_nest(self, space, pool, dims): 

287 """Exploration loop nest statements""" 

288 dtpc_p, mbsn, evos_p = dims 

289 configs, size = space 

290 parallel_config = self.config.make_parallel_config( 

291 dtpc_p, mbsn, evos_p 

292 ) 

293 logger.info("test config %d : %s", size, str(parallel_config)) 

294 size += 1 

295 

296 if self.is_valid(parallel_config) and self.config.set_parallel_config( 

297 parallel_config 

298 ): 

299 if pool is None: 

300 if self.enable_debug: 

301 mem_debugger = Debug.Debug( 

302 parallel_config, 

303 info_type=Debug.MemParts, 

304 enable=self.enable_debug, 

305 output_file="debug_mem.csv", 

306 ) 

307 # try: 

308 peak = self.memory_estim(mem_debugger) 

309 mem_debugger.write() 

310 else: 

311 peak = self.memory_estim() 

312 # except: 

313 # logger.error() 

314 # return (configs, size) 

315 else: 

316 # logger.debug("before evaluator copy") 

317 # evaluator = copy.deepcopy(self.mem_eval) 

318 logger.debug("before apply_async") 

319 peak = pool.apply_async( 

320 pool_estimate_memory, 

321 args=(copy.deepcopy(self.config.ccfg),), 

322 # args=(evaluator,), 

323 # self.memory_estim, 

324 ) 

325 logger.debug("after apply_async") 

326 configs[parallel_config] = peak 

327 

328 return (configs, size) 

329 

330 def order_search_space(self, space, threads_num, cache_file): 

331 """Sort the search space computed with performance estimation""" 

332 if not space: 

333 return ([], []) 

334 multiproc = False 

335 if threads_num and threads_num > 5 * len(space): 

336 multiproc = True 

337 scored_space = [] 

338 debug_parts = [] 

339 for config, mem in space: 

340 self.config.set_parallel_config(config) 

341 values = [] 

342 if multiproc: 

343 with proc.Pool(processes=threads_num) as pool: 

344 score = pool.apply_async( 

345 pool_estimate_performance, 

346 args=( 

347 copy.deepcopy(self.config), 

348 self.machine.device, 

349 cache_file, 

350 ), 

351 ) 

352 else: 

353 if self.enable_debug: 

354 debugger = Debug.Debug( 

355 config, 

356 info_type=Debug.PerfParts, 

357 enable=self.enable_debug, 

358 ) 

359 score = estimate_performance( 

360 self.config.ccfg, 

361 debugger=debugger, 

362 device_type=self.machine.device, 

363 memory=mem, 

364 cache_file=cache_file, 

365 ) 

366 debugger.write() 

367 debug_parts = list(debugger.info.keys()) 

368 values = list(debugger.info.values()) 

369 del values[-2:] 

370 del debug_parts[-2:] 

371 else: 

372 score = estimate_performance( 

373 self.config.ccfg, 

374 device_type=self.machine.device, 

375 memory=mem, 

376 ) 

377 scored_space.append((config, mem, score, values)) 

378 

379 logger.info("config %s has score %f", str(config), score) 

380 

381 if multiproc: 

382 new_scored_space = [] 

383 pool.close() 

384 pool.join() 

385 for config, mem, score, values in scored_space: 

386 new_scored_space.append((config, mem, score.get(), values)) 

387 else: 

388 new_scored_space = scored_space 

389 return (sorted(new_scored_space, key=lambda x: x[2]), debug_parts) 

390 

391 def order_space_test_comm_classified(self, space, order_by=2): 

392 """Order the given space with performance estimation""" 

393 scored_space = [] 

394 debug_parts = [] 

395 for config, real_time, real_comm_wait in space: 

396 debugger = Debug.Debug( 

397 config, info_type=Debug.PerfParts, enable=self.enable_debug 

398 ) 

399 self.config.set_parallel_config(config) 

400 peak_mem = self.memory_estim() 

401 score = estimate_performance( 

402 self.config.ccfg, 

403 debugger=debugger, 

404 device_type=self.machine.device, 

405 stage_focused=0, 

406 ) # , memory = mem) 

407 debugger.write() 

408 debug_parts = list(debugger.info.keys()) 

409 values = list(debugger.info.values()) 

410 del values[-2:] 

411 scored_space.append( 

412 (config, peak_mem, real_time, score, values, real_comm_wait) 

413 ) 

414 

415 logger.info("config %s has score %f", str(config), score) 

416 del debug_parts[-2:] 

417 return (sorted(scored_space, key=lambda x: x[order_by]), debug_parts) 

418 

419 def order_space_test(self, space, order_by=2): 

420 """Order the given space with performance estimation""" 

421 scored_space = [] 

422 debug_parts = [] 

423 for config, real_time in space: 

424 debugger = Debug.Debug( 

425 config, info_type=Debug.PerfParts, enable=self.enable_debug 

426 ) 

427 logger.info("Test config %s", str(config)) 

428 self.config.set_parallel_config(config) 

429 logger.debug(self.mem_eval.get_strategy()) 

430 peak_mem = self.memory_estim() 

431 score = estimate_performance( 

432 self.config.ccfg, 

433 debugger=debugger, 

434 device_type=self.machine.device, 

435 ) # , memory = mem) 

436 debugger.write() 

437 debug_parts = list(debugger.info.keys()) 

438 values = list(debugger.info.values()) 

439 del values[-2:] 

440 scored_space.append((config, peak_mem, real_time, score, values)) 

441 

442 logger.info("config %s has score %f", str(config), score) 

443 del debug_parts[-2:] 

444 return (sorted(scored_space, key=lambda x: x[order_by]), debug_parts) 

445 

446 def plot_title(self): 

447 """Generate plot title""" 

448 return ( 

449 f"{self.model_name} on {self.machine.number}" 

450 + f" {self.machine.device} with {self.global_batch_size} GBS" 

451 ) 

452 

453 def run_generation_to_ordering( 

454 self, yaml_folder, threads_num=None, top_num=None, cache_file=None 

455 ): 

456 """Test some functions""" 

457 start = time.time() 

458 space = self.generate_search_space(yaml_folder, threads_num) 

459 generation = time.time() 

460 scored_space, dbg = self.order_search_space( 

461 space, threads_num, cache_file=cache_file 

462 ) 

463 ordering = time.time() 

464 logger.output( 

465 space_to_string(scored_space, max_num=top_num, debug_parts=dbg) 

466 ) 

467 logger.output( 

468 "Space generation took %.2fs and ordering took %.2fs", 

469 generation - start, 

470 ordering - generation, 

471 ) 

472 is_not = " NOT" if not self.config.balancing.from_config else "" 

473 logger.output( 

474 "Offset & Recompute were%s computed from config info", is_not 

475 ) 

476 logger.output( 

477 "Device number is %d, global batch size is %d, dimensions are %s", 

478 self.machine.number, 

479 self.global_batch_size, 

480 str(self.config.dimensions), 

481 ) 

482 if self.enable_debug: 

483 file_path = os.path.dirname(os.path.realpath(__file__)) 

484 output_path = os.path.join(file_path, "output") 

485 if scored_space: 

486 Debug.plot_nd( 

487 scored_space, 

488 output_path, 

489 dbg, 

490 title=self.plot_title(), 

491 max_num=top_num, 

492 ) 

493 return scored_space 

494 

495 def to_ppb(self, scored_space, k, cfg_name): 

496 """Create an input file for pipeline balancing""" 

497 parallel_config = scored_space[k][0] 

498 self.config.set_parallel_config(parallel_config) 

499 self.mem_eval.update_config(self.config) 

500 m = cfg_name + "_nd_to_ppb_" + str(k) 

501 s = self.config.dim_val(Dim.PP, parallel_config) 

502 mb = self.config.dim_val(Dim.MBN, parallel_config) 

503 i = self.config.dim_val(Dim.VPP, parallel_config) 

504 mem = str(self.config.ccfg.device_capacity.to_mb) 

505 filename = ( 

506 os.path.dirname(os.path.realpath(__file__)) 

507 + "/../pipeline_balance/layers/" 

508 + m 

509 + ".json" 

510 ) 

511 with open(filename, "w+", encoding="utf-8") as fp: 

512 json.dump( 

513 self.mem_eval.estimate_layer_memory( 

514 device_type=self.machine.device 

515 ), 

516 fp, 

517 indent=4, 

518 ) 

519 logger.output( 

520 "To run pipeline balancing on configuration %s:" 

521 "\npython run_pipeline_balance.py " 

522 "-m %d -s %d -mb %d -i %d -mem %d", 

523 parallel_config, 

524 m, 

525 s, 

526 mb, 

527 i, 

528 mem, 

529 ) 

530 logger.output("Warning: currently select_recompute_memory \ 

531 should be removed & layer time need to be added") 

532 

533 def test_from_csv(self, csv_f, output_path=None): 

534 """Run estimation tests against a real run profiling in csv format""" 

535 configs, row_num = Debug.get_real_data(csv_f) 

536 configs_estimated, debug_parts = self.order_space_test( 

537 configs, order_by=2 

538 ) 

539 if output_path is not None: 

540 Debug.plot_vs_real( 

541 configs_estimated, 

542 csv_f, 

543 output_path, 

544 debug_parts, 

545 title=self.plot_title(), 

546 ) 

547 correl, topk = Debug.correlation_topk(configs_estimated, csv_f) 

548 return correl, topk, row_num 

549 

550 def test_from_csv_comm_classified( 

551 self, csv_f, output_path=None, plot_idle=False 

552 ): 

553 """Run test to compare estimation with detailed profiling""" 

554 configs = Debug.get_comm_classified_data(csv_f, plot_idle=plot_idle) 

555 configs_estimated, debug_parts = self.order_space_test_comm_classified( 

556 configs, order_by=2 

557 ) 

558 

559 if output_path is not None: 

560 Debug.plot_vs_real_comm_classified( 

561 configs_estimated, 

562 csv_f, 

563 output_path, 

564 debug_parts, 

565 title=self.plot_title(), 

566 plot_idle=plot_idle, 

567 ) 

568 

569 return Debug.correlation_with_classified_comms(configs_estimated) 

570 

571 

572class ParallelizeMultiModal(ParallelizeLayer): 

573 """Parallelize a MultiModel""" 

574 

575 def __init__( 

576 self, 

577 evaluator, 

578 machine, 

579 global_batch_size=None, 

580 dimensions=None, 

581 **extra_config, 

582 ): 

583 

584 super().__init__( 

585 evaluator, 

586 machine, 

587 global_batch_size=global_batch_size, 

588 dimensions=dimensions, 

589 sub_model="deepseekv3", 

590 **extra_config, 

591 ) 

592 

593 

594class Parallelize: # pylint: disable=R0903 

595 """Main class instantiated by one of the above two""" 

596 

597 def __init__( 

598 self, 

599 framework, 

600 config, 

601 machine, 

602 **extra_config, 

603 ): 

604 logger.debug("before evaluator init") 

605 if "model" in extra_config: 

606 model_name = extra_config.pop("model") 

607 mem_eval = EvaluatorV2( 

608 config, framework=framework, hook_cls=model_name, machine=machine 

609 ) 

610 else: 

611 mem_eval = EvaluatorV2(config, framework=framework, machine=machine) 

612 

613 if "global_batch_size" in extra_config: 

614 global_batch_size = extra_config.pop("global_batch_size") 

615 else: 

616 global_batch_size = None 

617 

618 if "dimensions" in extra_config: 

619 dimensions = extra_config.pop("dimensions") 

620 else: 

621 dimensions = None 

622 

623 if mem_eval.ccfg.multimodal: 

624 logger.debug("MultiModal is triggered") 

625 self.instance = ParallelizeMultiModal( 

626 mem_eval, 

627 machine, 

628 global_batch_size=global_batch_size, 

629 dimensions=dimensions, 

630 **extra_config, 

631 ) 

632 else: 

633 self.instance = ParallelizeLayer( 

634 mem_eval, 

635 machine, 

636 global_batch_size=global_batch_size, 

637 dimensions=dimensions, 

638 sub_model=None, 

639 **extra_config, 

640 ) 

641 

642 def __getattr__(self, name): 

643 return self.instance.__getattribute__(name) 

644 

645 

646def space_to_string(space, max_num=None, debug_parts=None): 

647 """Space printer""" 

648 i = 0 

649 s = "" 

650 if max_num is not None: 

651 s += "Top " + str(max_num) + " configurations:\n" 

652 else: 

653 s += "\n" 

654 if len(space) == 0: 

655 return s 

656 s += "\t" 

657 for d in space[0][0].all_dims: 

658 s += str(d) + " " * (6 - len(str(d))) 

659 s += "Memory Performance score " 

660 if debug_parts is not None: 

661 for dbg_part in debug_parts: 

662 s += "\t" + dbg_part.short_name() 

663 s += "\n" 

664 for config in space: 

665 if max_num is not None and max_num == i: 

666 break 

667 s += "\t" 

668 for v in config[0].values(): 

669 s += v + " " * (6 - len(v)) 

670 s += str(config[1]) + " MB " # + str(config[2]) 

671 s += f"{(config[2]):16.12e}" 

672 for v in config[3]: 

673 s += f"\t{(100*v/config[2]):.2f}%" 

674 s += "\n" 

675 i += 1 

676 return s 

677 

678 

679def pool_estimate_memory(config): 

680 """Calls memory estimation for multiprocessing""" 

681 logger.debug("estimate_peak") 

682 # print("estimate_peak") 

683 e = EvaluatorV2(config) 

684 return e.estimate_peak() 

685 

686 

687# def pool_estimate_memory(evaluator): 

688# """Calls memory estimation for multiprocessing""" 

689# logger.debug("estimate_peak") 

690# return evaluator.estimate_peak() 

691 

692 

693def pool_estimate_performance(config, device): 

694 """Calls performance estimation for multiprocessing""" 

695 return estimate_performance(config, device_type=device)