Skip to content

Commit c4d4e08

Browse files
ethanbwaitefacebook-github-bot
authored andcommitted
(torchx/runner) Add runner API to parse scheduler config from string literal (#1096)
Summary: Adds `cfg_from_str` to the runner API to parse scheduler config options from string literals so this logic doesn't need to be duplicated across usecases. Also updates CLI exit message with the full `InvalidRunConfigException` reason string, and fixes a dead link. Differential Revision: D79937463
1 parent 46ed6d9 commit c4d4e08

File tree

3 files changed

+63
-7
lines changed

3 files changed

+63
-7
lines changed

torchx/cli/cmd_run.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -207,8 +207,7 @@ def _run(self, runner: Runner, args: argparse.Namespace) -> None:
207207
" (e.g. `local_cwd`)"
208208
)
209209

210-
scheduler_opts = runner.scheduler_run_opts(args.scheduler)
211-
cfg = scheduler_opts.cfg_from_str(args.scheduler_args)
210+
cfg = dict(runner.cfg_from_str(args.scheduler, args.scheduler_args))
212211
config.apply(scheduler=args.scheduler, cfg=cfg)
213212

214213
component, component_args = _parse_component_name_and_args(
@@ -263,12 +262,14 @@ def _run(self, runner: Runner, args: argparse.Namespace) -> None:
263262
sys.exit(1)
264263
except specs.InvalidRunConfigException as e:
265264
error_msg = (
266-
f"Scheduler arg is incorrect or missing required option: `{e.cfg_key}`\n"
267-
f"Run `torchx runopts` to check configuration for `{args.scheduler}` scheduler\n"
268-
f"Use `-cfg` to specify run cfg as `key1=value1,key2=value2` pair\n"
269-
"of setup `.torchxconfig` file, see: https://pytorch.org/torchx/main/experimental/runner.config.html"
265+
"Invalid scheduler configuration: %s\n"
266+
"To configure scheduler options, either:\n"
267+
" 1. Use the `-cfg` command-line argument, e.g., `-cfg key1=value1,key2=value2`\n"
268+
" 2. Set up a `.torchxconfig` file. For more details, visit: https://pytorch.org/torchx/main/runner.config.html\n"
269+
"Run `torchx runopts %s` to check all available configuration options for the "
270+
"`%s` scheduler."
270271
)
271-
logger.error(error_msg)
272+
print(error_msg % (e, args.scheduler, args.scheduler), file=sys.stderr)
272273
sys.exit(1)
273274

274275
def run(self, args: argparse.Namespace) -> None:

torchx/runner/api.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -486,6 +486,27 @@ def scheduler_run_opts(self, scheduler: str) -> runopts:
486486
"""
487487
return self._scheduler(scheduler).run_opts()
488488

489+
def cfg_from_str(self, scheduler: str, *cfg_literal: str) -> Mapping[str, CfgVal]:
490+
"""
491+
Convenience function around the scheduler's ``runopts.cfg_from_str()`` method.
492+
493+
Usage:
494+
495+
.. doctest::
496+
497+
from torchx.runner import get_runner
498+
499+
runner = get_runner()
500+
cfg = runner.cfg_from_str("local_cwd", "log_dir=/tmp/foobar", "prepend_cwd=True")
501+
assert cfg == {"log_dir": "/tmp/foobar", "prepend_cwd": True, "auto_set_cuda_visible_devices": False}
502+
"""
503+
504+
opts = self._scheduler(scheduler).run_opts()
505+
cfg = {}
506+
for cfg_str in cfg_literal:
507+
cfg.update(opts.cfg_from_str(cfg_str))
508+
return cfg
509+
489510
def scheduler_backends(self) -> List[str]:
490511
"""
491512
Returns a list of all supported scheduler backends.

torchx/runner/test/api_test.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
parse_app_handle,
2929
Resource,
3030
Role,
31+
runopts,
3132
UnknownAppException,
3233
)
3334
from torchx.specs.finder import ComponentNotFoundException
@@ -701,3 +702,36 @@ def test_runner_manual_close(self, _) -> None:
701702
def test_get_default_runner(self, _) -> None:
702703
runner = get_runner()
703704
self.assertEqual("torchx", runner._name)
705+
706+
def test_cfg_from_str(self, _) -> None:
707+
scheduler_mock = MagicMock()
708+
opts = runopts()
709+
opts.add("foo", type_=str, default="", help="")
710+
opts.add("test_key", type_=str, default="", help="")
711+
opts.add("default_time", type_=int, default=0, help="")
712+
opts.add("enable", type_=bool, default=True, help="")
713+
opts.add("disable", type_=bool, default=True, help="")
714+
opts.add("complex_list", type_=List[str], default=[], help="")
715+
scheduler_mock.run_opts.return_value = opts
716+
717+
with Runner(
718+
name=SESSION_NAME,
719+
scheduler_factories={"local_dir": lambda name, **kwargs: scheduler_mock},
720+
) as runner:
721+
self.assertDictEqual(
722+
{
723+
"foo": "bar",
724+
"test_key": "test_value",
725+
"default_time": 42,
726+
"enable": True,
727+
"disable": False,
728+
"complex_list": ["v1", "v2", "v3"],
729+
},
730+
runner.cfg_from_str(
731+
"local_dir",
732+
"foo=bar",
733+
"test_key=test_value",
734+
"default_time=42",
735+
"enable=True,disable=False,complex_list=v1;v2;v3",
736+
),
737+
)

0 commit comments

Comments
 (0)