Unverified Commit 3f6a8274 authored by liuzhe-lz's avatar liuzhe-lz Committed by GitHub
Browse files

Some string changes around experiment module (#4442)

parent d5ed88e4
# Copyright (c) Microsoft Corporation. # Copyright (c) Microsoft Corporation.
# Licensed under the MIT license. # Licensed under the MIT license.
from .exp_config import ExperimentConfig from .experiment_config import ExperimentConfig
from .algorithm import AlgorithmConfig, CustomAlgorithmConfig from .algorithm import AlgorithmConfig, CustomAlgorithmConfig
from .training_services import * from .training_services import *
from .shared_storage import * from .shared_storage import *
...@@ -59,7 +59,7 @@ class ConfigBase: ...@@ -59,7 +59,7 @@ class ConfigBase:
def __init__(self, **kwargs): def __init__(self, **kwargs):
""" """
There are two common ways to use the constructor, There are two common ways to use the constructor,
directly writing Python code and unpacking from JSON(YAML) object: directly writing kwargs and unpacking from JSON (YAML) object:
.. code-block:: python .. code-block:: python
...@@ -77,7 +77,7 @@ class ConfigBase: ...@@ -77,7 +77,7 @@ class ConfigBase:
then using ``hello_world=1``, ``helloWorld=1``, and ``_HELLOWORLD_=1`` in constructor then using ``hello_world=1``, ``helloWorld=1``, and ``_HELLOWORLD_=1`` in constructor
will all assign to the same field. will all assign to the same field.
If ``kwargs`` contain extra keys, a `ValueError` will be raised. If ``kwargs`` contain extra keys, `AttributeError` will be raised.
If ``kwargs`` do not have enough key, missing fields are silently set to `MISSING()`. If ``kwargs`` do not have enough key, missing fields are silently set to `MISSING()`.
You can use ``utils.is_missing()`` to check them. You can use ``utils.is_missing()`` to check them.
...@@ -90,7 +90,7 @@ class ConfigBase: ...@@ -90,7 +90,7 @@ class ConfigBase:
if args: # maybe a key is misspelled if args: # maybe a key is misspelled
class_name = type(self).__name__ class_name = type(self).__name__
fields = ', '.join(args.keys()) fields = ', '.join(args.keys())
raise ValueError(f'{class_name} does not have field(s) {fields}') raise AttributeError(f'{class_name} does not have field(s) {fields}')
# try to unpack nested config # try to unpack nested config
for field in dataclasses.fields(self): for field in dataclasses.fields(self):
...@@ -135,7 +135,7 @@ class ConfigBase: ...@@ -135,7 +135,7 @@ class ConfigBase:
with open(path) as yaml_file: with open(path) as yaml_file:
data = yaml.safe_load(yaml_file) data = yaml.safe_load(yaml_file)
if not isinstance(data, dict): if not isinstance(data, dict):
raise ValueError(f'Conent of config file {path} is not a dict/object') raise TypeError(f'Conent of config file {path} is not a dict/object')
utils.set_base_path(Path(path).parent) utils.set_base_path(Path(path).parent)
config = cls(**data) config = cls(**data)
utils.unset_base_path() utils.unset_base_path()
...@@ -143,10 +143,15 @@ class ConfigBase: ...@@ -143,10 +143,15 @@ class ConfigBase:
def canonical_copy(self): def canonical_copy(self):
""" """
Create a canonicalized copy of the config, and validate it. Create a "canonical" copy of the config, and validate it.
This function is mainly used internally by NNI. This function is mainly used internally by NNI.
Term explanation:
The config schema for end users is more flexible than the format NNI manager accepts,
so config classes have to deal with the conversion.
Here we call the converted format "canonical".
Returns Returns
------- -------
type(self) type(self)
...@@ -186,15 +191,16 @@ class ConfigBase: ...@@ -186,15 +191,16 @@ class ConfigBase:
def _canonicalize(self, parents): def _canonicalize(self, parents):
""" """
The config schema for end users is more flexible than the format NNI manager accepts. To be overrided by subclass.
This method convert a config object to the constrained format accepted by NNI manager.
Convert the config object to canonical format.
The default implementation will: The default implementation will:
1. Resolve all ``PathLike`` fields to absolute path 1. Resolve all ``PathLike`` fields to absolute path
2. Call ``_canonicalize()`` on all children config objects, including those inside list and dict 2. Call ``_canonicalize([self] + parents)`` on all children config objects, including those inside list and dict
Subclasses are recommended to call ``super()._canonicalize(parents)`` at the end of their overrided version. If the subclass has nested config fields, be careful about where to call ``super()._canonicalize()``.
Parameters Parameters
---------- ----------
...@@ -212,6 +218,8 @@ class ConfigBase: ...@@ -212,6 +218,8 @@ class ConfigBase:
def _validate_canonical(self): def _validate_canonical(self):
""" """
To be overrided by subclass.
Validate legality of a canonical config object. It's caller's responsibility to ensure the config is canonical. Validate legality of a canonical config object. It's caller's responsibility to ensure the config is canonical.
Raise exception if any problem found. This function does **not** return truth value. Raise exception if any problem found. This function does **not** return truth value.
...@@ -220,8 +228,6 @@ class ConfigBase: ...@@ -220,8 +228,6 @@ class ConfigBase:
1. Validate that all fields match their type hint 1. Validate that all fields match their type hint
2. Call ``_validate_canonical()`` on children config objects, including those inside list and dict 2. Call ``_validate_canonical()`` on children config objects, including those inside list and dict
Subclasses are recommended to to call ``super()._validate_canonical()``.
""" """
utils.validate_type(self) utils.validate_type(self)
for field in dataclasses.fields(self): for field in dataclasses.fields(self):
...@@ -229,6 +235,10 @@ class ConfigBase: ...@@ -229,6 +235,10 @@ class ConfigBase:
_recursive_validate_child(value) _recursive_validate_child(value)
def __setattr__(self, name, value): def __setattr__(self, name, value):
"""
To prevent typo, config classes forbid assigning to attribute that is not a config field,
unless it starts with underscore.
"""
if hasattr(self, name) or name.startswith('_'): if hasattr(self, name) or name.startswith('_'):
super().__setattr__(name, value) super().__setattr__(name, value)
return return
......
...@@ -47,12 +47,16 @@ class ExperimentConfig(ConfigBase): ...@@ -47,12 +47,16 @@ class ExperimentConfig(ConfigBase):
) )
) )
Fields commented as "training service field" acts like shortcut for all training services.
Users can either specify them here or inside training service config.
In latter case hybrid training services can have different settings.
.. _reference: https://nni.readthedocs.io/en/stable/reference/experiment_config.html .. _reference: https://nni.readthedocs.io/en/stable/reference/experiment_config.html
""" """
# TODO:
# The behavior described below is expected but does not work,
# because some fields are consumed by TrialDispatcher outside environment service.
# Add the lines to docstr when we fix this issue.
# Fields commented as "training service field" acts like shortcut for all training services.
# Users can either specify them here or inside training service config.
# In latter case hybrid training services can have different settings.
experiment_name: Optional[str] = None experiment_name: Optional[str] = None
search_space_file: Optional[utils.PathLike] = None search_space_file: Optional[utils.PathLike] = None
......
...@@ -29,6 +29,10 @@ class RunMode(Enum): ...@@ -29,6 +29,10 @@ class RunMode(Enum):
- Background: stop NNI manager when Python script exits; do not print NNI manager log. (default) - Background: stop NNI manager when Python script exits; do not print NNI manager log. (default)
- Foreground: stop NNI manager when Python script exits; print NNI manager log to stdout. - Foreground: stop NNI manager when Python script exits; print NNI manager log to stdout.
- Detach: do not stop NNI manager when Python script exits. - Detach: do not stop NNI manager when Python script exits.
NOTE:
This API is non-stable and is likely to get refactored in next release.
NNI manager should treat log level more seriously so we can default to "foreground" without being too verbose.
""" """
Background = 'background' Background = 'background'
Foreground = 'foreground' Foreground = 'foreground'
......
...@@ -91,6 +91,9 @@ def create_experiment(args): ...@@ -91,6 +91,9 @@ def create_experiment(args):
run_mode = RunMode.Foreground if foreground else RunMode.Detach run_mode = RunMode.Foreground if foreground else RunMode.Detach
exp.start(port, debug, run_mode) exp.start(port, debug, run_mode)
_logger.info(f'To stop experiment run "nnictl stop {exp.id}" or "nnictl stop --all"')
_logger.info('Reference: https://nni.readthedocs.io/en/stable/Tutorial/Nnictl.html')
def resume_experiment(args): def resume_experiment(args):
exp_id = args.id exp_id = args.id
port = args.port port = args.port
......
...@@ -145,7 +145,7 @@ def test_bad(): ...@@ -145,7 +145,7 @@ def test_bad():
config.validate() config.validate()
except Exception as e: except Exception as e:
exc = e exc = e
assert isinstance(exc, ValueError), tag assert exc is not None
if __name__ == '__main__': if __name__ == '__main__':
test_good() test_good()
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment