wandb_controller.py 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712
  1. """Sweep controller.
  2. This module implements the sweep controller.
  3. On error an exception is raised:
  4. ControllerError
  5. Example:
  6. import wandb
  7. #
  8. # create a sweep controller
  9. #
  10. # There are three different ways sweeps can be created:
  11. # (1) create with sweep id from `wandb sweep` command
  12. sweep_id = 'xyzxyz2'
  13. tuner = wandb.controller(sweep_id)
  14. # (2) create with sweep config
  15. sweep_config = {}
  16. tuner = wandb.controller()
  17. tuner.configure(sweep_config)
  18. tuner.create()
  19. # (3) create by constructing programmatic sweep configuration
  20. tuner = wandb.controller()
  21. tuner.configure_search('random')
  22. tuner.configure_program('train-dummy.py')
  23. tuner.configure_parameter('param1', values=[1,2,3])
  24. tuner.configure_parameter('param2', values=[1,2,3])
  25. tuner.configure_controller(type="local")
  26. tuner.create()
  27. #
  28. # run the sweep controller
  29. #
  30. # There are three different ways sweeps can be executed:
  31. # (1) run to completion
  32. tuner.run()
  33. # (2) run in a simple loop
  34. while not tuner.done():
  35. tuner.step()
  36. tuner.print_status()
  37. # (3) run in a more complex loop
  38. while not tuner.done():
  39. params = tuner.search()
  40. tuner.schedule(params)
  41. runs = tuner.stopping()
  42. if runs:
  43. tuner.stop_runs(runs)
  44. """
  45. from __future__ import annotations
  46. import json
  47. import os
  48. import random
  49. import string
  50. import time
  51. from typing import Callable
  52. import yaml
  53. from wandb import env
  54. from wandb.apis import InternalApi
  55. from wandb.sdk import wandb_sweep
  56. from wandb.sdk.launch.sweeps.utils import (
  57. handle_sweep_config_violations,
  58. sweep_config_err_text_from_jsonschema_violations,
  59. )
  60. from wandb.util import get_module
  61. # TODO(jhr): Add metric status
  62. # TODO(jhr): Add print_space
  63. # TODO(jhr): Add print_summary
  64. sweeps = get_module(
  65. "sweeps",
  66. required="wandb[sweeps] is required to use the local controller. "
  67. "Please run `pip install wandb[sweeps]`.",
  68. )
  69. # This should be something like 'pending' (but we need to make sure everyone else is ok with that)
  70. SWEEP_INITIAL_RUN_STATE = sweeps.RunState.pending
  71. def _id_generator(size=10, chars=string.ascii_lowercase + string.digits):
  72. return "".join(random.choice(chars) for _ in range(size))
  73. class ControllerError(Exception):
  74. """Base class for sweep errors."""
  75. class _WandbController:
  76. """Sweep controller class.
  77. Internal datastructures on the sweep object to coordinate local controller with
  78. cloud controller.
  79. Data structures:
  80. controller: {
  81. schedule: [
  82. { id: SCHEDULE_ID
  83. data: {param1: val1, param2: val2}},
  84. ]
  85. earlystop: [RUN_ID, ...]
  86. scheduler:
  87. scheduled: [
  88. { id: SCHEDULE_ID
  89. runid: RUN_ID},
  90. ]
  91. `controller` is only updated by the client
  92. `scheduler` is only updated by the cloud backend
  93. Protocols:
  94. Scheduling a run:
  95. - client controller adds a schedule entry on the controller.schedule list
  96. - cloud backend notices the new entry and creates a run with the parameters
  97. - cloud backend adds a scheduled entry on the scheduler.scheduled list
  98. - client controller notices that the run has been scheduled and removes it from
  99. controller.schedule list
  100. Current implementation details:
  101. - Runs are only schedule if there are no other runs scheduled.
  102. """
  103. def __init__(self, sweep_id_or_config=None, entity=None, project=None):
  104. # sweep id configured in constructor
  105. self._sweep_id: str | None = None
  106. # configured parameters
  107. # Configuration to be created
  108. self._create: dict = {}
  109. # Custom search
  110. self._custom_search: (
  111. Callable[
  112. [dict | sweeps.SweepConfig, list[sweeps.SweepRun]],
  113. sweeps.SweepRun | None,
  114. ]
  115. | None
  116. ) = None
  117. # Custom stopping
  118. self._custom_stopping: (
  119. Callable[
  120. [dict | sweeps.SweepConfig, list[sweeps.SweepRun]],
  121. list[sweeps.SweepRun],
  122. ]
  123. | None
  124. ) = None
  125. # Program function (used for future jupyter support)
  126. self._program_function = None
  127. # The following are updated every sweep step
  128. # raw sweep object (dict of strings)
  129. self._sweep_obj = None
  130. # parsed sweep config (dict)
  131. self._sweep_config: dict | sweeps.SweepConfig | None = None
  132. # sweep metric used to optimize (str or None)
  133. self._sweep_metric: str | None = None
  134. # list of _Run objects
  135. self._sweep_runs: list[sweeps.SweepRun] | None = None
  136. # dictionary mapping name of run to run object
  137. self._sweep_runs_map: dict[str, sweeps.SweepRun] | None = None
  138. # scheduler dict (read only from controller) - used as feedback from the server
  139. self._scheduler: dict | None = None
  140. # controller dict (write only from controller) - used to send commands to server
  141. self._controller: dict | None = None
  142. # keep track of controller dict from previous step
  143. self._controller_prev_step: dict | None = None
  144. # Internal
  145. # Keep track of whether the sweep has been started
  146. self._started: bool = False
  147. # indicate whether there is more to schedule
  148. self._done_scheduling: bool = False
  149. # indicate whether the sweep needs to be created
  150. self._defer_sweep_creation: bool = False
  151. # count of logged lines since last status
  152. self._logged: int = 0
  153. # last status line printed
  154. self._laststatus: str = ""
  155. # keep track of logged actions for print_actions()
  156. self._log_actions: list[tuple[str, str]] = []
  157. # keep track of logged debug for print_debug()
  158. self._log_debug: list[str] = []
  159. # all backend commands use internal api
  160. environ = os.environ
  161. if entity:
  162. env.set_entity(entity, env=environ)
  163. if project:
  164. env.set_project(project, env=environ)
  165. self._api = InternalApi(environ=environ)
  166. if isinstance(sweep_id_or_config, str):
  167. self._sweep_id = sweep_id_or_config
  168. elif isinstance(sweep_id_or_config, (dict, sweeps.SweepConfig)):
  169. self._create = sweeps.SweepConfig(sweep_id_or_config)
  170. # check for custom search and or stopping functions
  171. for config_key, controller_attr in zip(
  172. ["method", "early_terminate"], ["_custom_search", "_custom_stopping"]
  173. ):
  174. if callable(config_key in self._create and self._create[config_key]):
  175. setattr(self, controller_attr, self._create[config_key])
  176. self._create[config_key] = "custom"
  177. self._sweep_id = self.create(from_dict=True)
  178. elif sweep_id_or_config is None:
  179. self._defer_sweep_creation = True
  180. return
  181. else:
  182. raise ControllerError("Unhandled sweep controller type")
  183. sweep_obj = self._sweep_object_read_from_backend()
  184. if sweep_obj is None:
  185. raise ControllerError("Can not find sweep")
  186. self._sweep_obj = sweep_obj
  187. def configure_search(
  188. self,
  189. search: str
  190. | Callable[
  191. [dict | sweeps.SweepConfig, list[sweeps.SweepRun]], sweeps.SweepRun | None
  192. ],
  193. ):
  194. self._configure_check()
  195. if isinstance(search, str):
  196. self._create["method"] = search
  197. elif callable(search):
  198. self._create["method"] = "custom"
  199. self._custom_search = search
  200. else:
  201. raise ControllerError("Unhandled search type.")
  202. def configure_stopping(
  203. self,
  204. stopping: str
  205. | Callable[
  206. [dict | sweeps.SweepConfig, list[sweeps.SweepRun]], list[sweeps.SweepRun]
  207. ],
  208. **kwargs,
  209. ):
  210. self._configure_check()
  211. if isinstance(stopping, str):
  212. self._create.setdefault("early_terminate", {})
  213. self._create["early_terminate"]["type"] = stopping
  214. for k, v in kwargs.items():
  215. self._create["early_terminate"][k] = v
  216. elif callable(stopping):
  217. self._custom_stopping = stopping(kwargs)
  218. self._create.setdefault("early_terminate", {})
  219. self._create["early_terminate"]["type"] = "custom"
  220. else:
  221. raise ControllerError("Unhandled stopping type.")
  222. def configure_metric(self, metric, goal=None):
  223. self._configure_check()
  224. self._create.setdefault("metric", {})
  225. self._create["metric"]["name"] = metric
  226. if goal:
  227. self._create["metric"]["goal"] = goal
  228. def configure_program(self, program):
  229. self._configure_check()
  230. if isinstance(program, str):
  231. self._create["program"] = program
  232. elif callable(program):
  233. self._create["program"] = "__callable__"
  234. self._program_function = program
  235. raise ControllerError("Program functions are not supported yet")
  236. else:
  237. raise ControllerError("Unhandled sweep program type")
  238. def configure_name(self, name):
  239. self._configure_check()
  240. self._create["name"] = name
  241. def configure_description(self, description):
  242. self._configure_check()
  243. self._create["description"] = description
  244. def configure_parameter(
  245. self,
  246. name,
  247. values=None,
  248. value=None,
  249. distribution=None,
  250. min=None,
  251. max=None,
  252. mu=None,
  253. sigma=None,
  254. q=None,
  255. a=None,
  256. b=None,
  257. ):
  258. self._configure_check()
  259. self._create.setdefault("parameters", {}).setdefault(name, {})
  260. if value is not None or (
  261. values is None and min is None and max is None and distribution is None
  262. ):
  263. self._create["parameters"][name]["value"] = value
  264. if values is not None:
  265. self._create["parameters"][name]["values"] = values
  266. if distribution is not None:
  267. self._create["parameters"][name]["distribution"] = distribution
  268. if min is not None:
  269. self._create["parameters"][name]["min"] = min
  270. if max is not None:
  271. self._create["parameters"][name]["max"] = max
  272. if mu is not None:
  273. self._create["parameters"][name]["mu"] = mu
  274. if sigma is not None:
  275. self._create["parameters"][name]["sigma"] = sigma
  276. if q is not None:
  277. self._create["parameters"][name]["q"] = q
  278. if a is not None:
  279. self._create["parameters"][name]["a"] = a
  280. if b is not None:
  281. self._create["parameters"][name]["b"] = b
  282. def configure_controller(self, type):
  283. """Configure controller to local if type == 'local'."""
  284. self._configure_check()
  285. self._create.setdefault("controller", {})
  286. self._create["controller"].setdefault("type", type)
  287. def configure(self, sweep_dict_or_config):
  288. self._configure_check()
  289. if self._create:
  290. raise ControllerError("Already configured.")
  291. if isinstance(sweep_dict_or_config, dict):
  292. self._create = sweep_dict_or_config
  293. elif isinstance(sweep_dict_or_config, str):
  294. self._create = yaml.safe_load(sweep_dict_or_config)
  295. else:
  296. raise ControllerError("Unhandled sweep controller type")
  297. @property
  298. def sweep_config(self) -> dict | sweeps.SweepConfig:
  299. return self._sweep_config
  300. @property
  301. def sweep_id(self) -> str:
  302. return self._sweep_id
  303. def _log(self) -> None:
  304. self._logged += 1
  305. def _error(self, s: str) -> None:
  306. print("ERROR:", s) # noqa: T201
  307. self._log()
  308. def _warn(self, s: str) -> None:
  309. print("WARN:", s) # noqa: T201
  310. self._log()
  311. def _info(self, s: str) -> None:
  312. print("INFO:", s) # noqa: T201
  313. self._log()
  314. def _debug(self, s: str) -> None:
  315. print("DEBUG:", s) # noqa: T201
  316. self._log()
  317. def _configure_check(self) -> None:
  318. if self._started:
  319. raise ControllerError("Can not configure after sweep has been started.")
  320. def _validate(self, config: dict) -> str:
  321. violations = sweeps.schema_violations_from_proposed_config(config)
  322. msg = (
  323. sweep_config_err_text_from_jsonschema_violations(violations)
  324. if len(violations) > 0
  325. else ""
  326. )
  327. return msg
  328. def create(self, from_dict: bool = False) -> str:
  329. if self._started:
  330. raise ControllerError("Can not create after sweep has been started.")
  331. if not self._defer_sweep_creation and not from_dict:
  332. raise ControllerError("Can not use create on already created sweep.")
  333. if not self._create:
  334. raise ControllerError("Must configure sweep before create.")
  335. # validate sweep config
  336. self._create = sweeps.SweepConfig(self._create)
  337. # Create sweep
  338. sweep_id, warnings = self._api.upsert_sweep(self._create)
  339. handle_sweep_config_violations(warnings)
  340. print("Create sweep with ID:", sweep_id) # noqa: T201
  341. sweep_url = wandb_sweep._get_sweep_url(self._api, sweep_id)
  342. if sweep_url:
  343. print("Sweep URL:", sweep_url) # noqa: T201
  344. self._sweep_id = sweep_id
  345. self._defer_sweep_creation = False
  346. return sweep_id
  347. def run(
  348. self,
  349. verbose: bool = False,
  350. print_status: bool = True,
  351. print_actions: bool = False,
  352. print_debug: bool = False,
  353. ) -> None:
  354. if verbose:
  355. print_status = True
  356. print_actions = True
  357. print_debug = True
  358. self._start_if_not_started()
  359. while not self.done():
  360. if print_status:
  361. self.print_status()
  362. self.step()
  363. if print_actions:
  364. self.print_actions()
  365. if print_debug:
  366. self.print_debug()
  367. time.sleep(5)
  368. def _sweep_object_read_from_backend(self) -> dict | None:
  369. specs_json = {}
  370. if self._sweep_metric:
  371. k = ["_step"]
  372. k.append(self._sweep_metric)
  373. specs_json = {"keys": k, "samples": 100000}
  374. specs = json.dumps(specs_json)
  375. # TODO(jhr): catch exceptions?
  376. sweep_obj = self._api.sweep(self._sweep_id, specs)
  377. if not sweep_obj:
  378. return
  379. self._sweep_obj = sweep_obj
  380. self._sweep_config = yaml.safe_load(sweep_obj["config"])
  381. self._sweep_metric = self._sweep_config.get("metric", {}).get("name")
  382. _sweep_runs: list[sweeps.SweepRun] = []
  383. for r in sweep_obj["runs"]:
  384. rr = r.copy()
  385. if "summaryMetrics" in rr and rr["summaryMetrics"]:
  386. rr["summaryMetrics"] = json.loads(rr["summaryMetrics"])
  387. if "config" not in rr:
  388. raise ValueError("sweep object is missing config")
  389. rr["config"] = json.loads(rr["config"])
  390. if "history" in rr:
  391. if isinstance(rr["history"], list):
  392. rr["history"] = [json.loads(d) for d in rr["history"]]
  393. else:
  394. raise ValueError(
  395. "Invalid history value: expected list of json strings: {}".format(
  396. rr["history"]
  397. )
  398. )
  399. if "sampledHistory" in rr:
  400. sampled_history = []
  401. for historyDictList in rr["sampledHistory"]:
  402. sampled_history += historyDictList
  403. rr["sampledHistory"] = sampled_history
  404. _sweep_runs.append(sweeps.SweepRun(**rr))
  405. self._sweep_runs = _sweep_runs
  406. self._sweep_runs_map = {r.name: r for r in self._sweep_runs}
  407. self._controller = json.loads(sweep_obj.get("controller") or "{}")
  408. self._scheduler = json.loads(sweep_obj.get("scheduler") or "{}")
  409. self._controller_prev_step = self._controller.copy()
  410. return sweep_obj
  411. def _sweep_object_sync_to_backend(self) -> None:
  412. if self._controller == self._controller_prev_step:
  413. return
  414. sweep_obj_id = self._sweep_obj["id"]
  415. controller = json.dumps(self._controller)
  416. _, warnings = self._api.upsert_sweep(
  417. self._sweep_config, controller=controller, obj_id=sweep_obj_id
  418. )
  419. handle_sweep_config_violations(warnings)
  420. self._controller_prev_step = self._controller.copy()
  421. def _start_if_not_started(self) -> None:
  422. if self._started:
  423. return
  424. if self._defer_sweep_creation:
  425. raise ControllerError(
  426. "Must specify or create a sweep before running controller."
  427. )
  428. obj = self._sweep_object_read_from_backend()
  429. if not obj:
  430. return
  431. is_local = self._sweep_config.get("controller", {}).get("type") == "local"
  432. if not is_local:
  433. raise ControllerError(
  434. "Only sweeps with a local controller are currently supported."
  435. )
  436. self._started = True
  437. # reset controller state, we might want to parse this and decide
  438. # what we can continue and add a version key, but for now we can
  439. # be safe and just reset things on start
  440. self._controller = {}
  441. self._sweep_object_sync_to_backend()
  442. def _parse_scheduled(self):
  443. scheduled_list = self._scheduler.get("scheduled") or []
  444. started_ids = []
  445. stopped_runs = []
  446. done_runs = []
  447. for s in scheduled_list:
  448. runid = s.get("runid")
  449. objid = s.get("id")
  450. r = self._sweep_runs_map.get(runid)
  451. if not r:
  452. continue
  453. if r.stopped:
  454. stopped_runs.append(runid)
  455. summary = r.summary_metrics
  456. if r.state == SWEEP_INITIAL_RUN_STATE and not summary:
  457. continue
  458. started_ids.append(objid)
  459. if r.state != "running":
  460. done_runs.append(runid)
  461. return started_ids, stopped_runs, done_runs
  462. def _step(self) -> None:
  463. self._start_if_not_started()
  464. self._sweep_object_read_from_backend()
  465. started_ids, stopped_runs, done_runs = self._parse_scheduled()
  466. # Remove schedule entry from controller dict if already scheduled
  467. schedule_list = self._controller.get("schedule", [])
  468. new_schedule_list = [s for s in schedule_list if s.get("id") not in started_ids]
  469. self._controller["schedule"] = new_schedule_list
  470. # Remove earlystop entry from controller if already stopped
  471. earlystop_list = self._controller.get("earlystop", [])
  472. new_earlystop_list = [
  473. r for r in earlystop_list if r not in stopped_runs and r not in done_runs
  474. ]
  475. self._controller["earlystop"] = new_earlystop_list
  476. # Clear out step logs
  477. self._log_actions = []
  478. self._log_debug = []
  479. def step(self) -> None:
  480. self._step()
  481. suggestion = self.search()
  482. self.schedule(suggestion)
  483. to_stop = self.stopping()
  484. if len(to_stop) > 0:
  485. self.stop_runs(to_stop)
  486. def done(self) -> bool:
  487. self._start_if_not_started()
  488. state = self._sweep_obj.get("state")
  489. return state not in [
  490. s.upper()
  491. for s in (
  492. sweeps.RunState.preempting.value,
  493. SWEEP_INITIAL_RUN_STATE.value,
  494. sweeps.RunState.running.value,
  495. )
  496. ]
  497. def _search(self) -> sweeps.SweepRun | None:
  498. search = self._custom_search or sweeps.next_run
  499. next_run = search(self._sweep_config, self._sweep_runs or [])
  500. if next_run is None:
  501. self._done_scheduling = True
  502. return next_run
  503. def search(self) -> sweeps.SweepRun | None:
  504. self._start_if_not_started()
  505. suggestion = self._search()
  506. return suggestion
  507. def _stopping(self) -> list[sweeps.SweepRun]:
  508. if "early_terminate" not in self.sweep_config:
  509. return []
  510. stopper = self._custom_stopping or sweeps.stop_runs
  511. stop_runs = stopper(self._sweep_config, self._sweep_runs or [])
  512. debug_lines = [
  513. " ".join([f"{k}={v}" for k, v in run.early_terminate_info.items()])
  514. for run in stop_runs
  515. if run.early_terminate_info is not None
  516. ]
  517. if debug_lines:
  518. self._log_debug += debug_lines
  519. return stop_runs
  520. def stopping(self) -> list[sweeps.SweepRun]:
  521. self._start_if_not_started()
  522. return self._stopping()
  523. def schedule(self, run: sweeps.SweepRun | None) -> None:
  524. self._start_if_not_started()
  525. # only schedule one run at a time (for now)
  526. if self._controller and self._controller.get("schedule"):
  527. return
  528. schedule_id = _id_generator()
  529. if run is None:
  530. schedule_list = [{"id": schedule_id, "data": {"args": None}}]
  531. else:
  532. param_list = [
  533. "{}={}".format(k, v.get("value")) for k, v in sorted(run.config.items())
  534. ]
  535. self._log_actions.append(("schedule", ",".join(param_list)))
  536. # schedule one run
  537. schedule_list = [{"id": schedule_id, "data": {"args": run.config}}]
  538. self._controller["schedule"] = schedule_list
  539. self._sweep_object_sync_to_backend()
  540. def stop_runs(self, runs: list[sweeps.SweepRun]) -> None:
  541. earlystop_list = list({run.name for run in runs})
  542. self._log_actions.append(("stop", ",".join(earlystop_list)))
  543. self._controller["earlystop"] = earlystop_list
  544. self._sweep_object_sync_to_backend()
  545. def print_status(self) -> None:
  546. status = _sweep_status(self._sweep_obj, self._sweep_config, self._sweep_runs)
  547. if self._laststatus != status or self._logged:
  548. print(status) # noqa: T201
  549. self._laststatus = status
  550. self._logged = 0
  551. def print_actions(self) -> None:
  552. for action, line in self._log_actions:
  553. self._info(f"{action.capitalize()} ({line})")
  554. self._log_actions = []
  555. def print_debug(self) -> None:
  556. for line in self._log_debug:
  557. self._debug(line)
  558. self._log_debug = []
  559. def print_space(self) -> None:
  560. self._warn("Method not implemented yet.")
  561. def print_summary(self) -> None:
  562. self._warn("Method not implemented yet.")
  563. def _get_run_counts(runs: list[sweeps.SweepRun]) -> dict[str, int]:
  564. metrics = {}
  565. categories = [name for name, _ in sweeps.RunState.__members__.items()] + ["unknown"]
  566. for r in runs:
  567. state = r.state
  568. found = "unknown"
  569. for c in categories:
  570. if state == c:
  571. found = c
  572. break
  573. metrics.setdefault(found, 0)
  574. metrics[found] += 1
  575. return metrics
  576. def _get_runs_status(metrics):
  577. categories = [name for name, _ in sweeps.RunState.__members__.items()] + ["unknown"]
  578. mlist = []
  579. for c in categories:
  580. if not metrics.get(c):
  581. continue
  582. mlist.append(f"{c.capitalize()}: {metrics[c]}")
  583. s = ", ".join(mlist)
  584. return s
  585. def _sweep_status(
  586. sweep_obj: dict,
  587. sweep_conf: dict | sweeps.SweepConfig,
  588. sweep_runs: list[sweeps.SweepRun],
  589. ) -> str:
  590. sweep = sweep_obj["name"]
  591. _ = sweep_obj["state"]
  592. run_count = len(sweep_runs)
  593. run_type_counts = _get_run_counts(sweep_runs)
  594. stopped = len([r for r in sweep_runs if r.stopped])
  595. stopping = len([r for r in sweep_runs if r.should_stop])
  596. stopstr = ""
  597. if stopped or stopping:
  598. stopstr = f"Stopped: {stopped}"
  599. if stopping:
  600. stopstr += f" (Stopping: {stopping})"
  601. runs_status = _get_runs_status(run_type_counts)
  602. method = sweep_conf.get("method", "unknown")
  603. stopping = sweep_conf.get("early_terminate", None)
  604. sweep_options = []
  605. sweep_options.append(method)
  606. if stopping:
  607. sweep_options.append(stopping.get("type", "unknown"))
  608. sweep_options = ",".join(sweep_options)
  609. sections = []
  610. sections.append(f"Sweep: {sweep} ({sweep_options})")
  611. if runs_status:
  612. sections.append(f"Runs: {run_count} ({runs_status})")
  613. else:
  614. sections.append(f"Runs: {run_count}")
  615. if stopstr:
  616. sections.append(stopstr)
  617. sections = " | ".join(sections)
  618. return sections